Professional iphone and ipad application development INTRODUCTION. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxiii Chapter 1
Navigation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Chapter 2
Alerts, Action Sheets, and Modal Views. . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Chapter 3
Custom Table Views. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 1
Chapter 4
The Split View. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 37
Chapter 5
Touch Events. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 59
Chapter 6
Notification Processing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 99
Chapter 7
Networking Concepts. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Chapter 8
Multimedia. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
Chapter 9
Application Preferences. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1 3
Chapter 10 Data Storage. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 Chapter 11
The Pasteboard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
Chapter 12
Unit Testing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 429
Chapter 13
Performance Tuning and Optimization. . . . . . . . . . . . . . . . . . . . . . . . . . . 453
Chapter 14
Integrating iAds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469
Chapter 15
Multitasking. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 8 1
APPENDIX A Your Initial App — First Steps. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505 APPENDIX B iPhone Developer Center. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 5 APPENDIX C Cocoa Touch Static Libraries. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 1 APPENDIX D Apple Developer Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543 INDEX
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Professional
iPhone® and iPad™ Application Development
Professional
iPhone® and iPad™ Application Development Gene Backlin
Professional iPhone® and iPad™ Application Development Published by Wiley Publishing, Inc. 10475 Crosspoint Boulevard Indianapolis, IN 46256 www.wiley.com Copyright © 2011 by Wiley Publishing, Inc., Indianapolis, Indiana Published simultaneously in Canada ISBN: 978-0-470-87819-4 ISBN: 978-1-118-00834-8 (ebk) ISBN: 978-1-118-00835-5 (ebk) ISBN: 978-1-118-00706-8 (ebk) Manufactured in the United States of America 10 9 8 7 6 5 4 3 2 1 No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, scanning or otherwise, except as permitted under Sections 107 or 108 of the 1976 United States Copyright Act, without either the prior written permission of the Publisher, or authorization through payment of the appropriate per-copy fee to the Copyright Clearance Center, 222 Rosewood Drive, Danvers, MA 01923, (978) 750-8400, fax (978) 646-8600. Requests to the Publisher for permission should be addressed to the Permissions Department, John Wiley & Sons, Inc., 111 River Street, Hoboken, NJ 07030, (201) 748-6011, fax (201) 748-6008, or online at http://www.wiley.com/go/permissions. Limit of Liability/Disclaimer of Warranty: The publisher and the author make no representations or warranties with respect to the accuracy or completeness of the contents of this work and specifically disclaim all warranties, including without limitation warranties of fitness for a particular purpose. No warranty may be created or extended by sales or promotional materials. The advice and strategies contained herein may not be suitable for every situation. This work is sold with the understanding that the publisher is not engaged in rendering legal, accounting, or other professional services. If professional assistance is required, the services of a competent professional person should be sought. Neither the publisher nor the author shall be liable for damages arising herefrom. The fact that an organization or Web site is referred to in this work as a citation and/or a potential source of further information does not mean that the author or the publisher endorses the information the organization or Web site may provide or recommendations it may make. Further, readers should be aware that Internet Web sites listed in this work may have changed or disappeared between when this work was written and when it is read. For general information on our other products and services please contact our Customer Care Department within the United States at (877) 762-2974, outside the United States at (317) 572-3993 or fax (317) 572-4002. Wiley also publishes its books in a variety of electronic formats. Some content that appears in print may not be available in electronic books. Library of Congress Control Number: 2010934751 Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Wrox Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its affiliates, in the United States and other countries, and may not be used without written permission. iPhone and iPad are trademarks or registered trademarks of Apple, Inc. All other trademarks are the property of their respective owners. Wiley Publishing, Inc., is not associated with any product or vendor mentioned in this book.
This book is being dedicated to the family unit; past, present, and future. May it never lose sight of its existence.
About the Author
Gene Backlin is owner and principal consultant of MariZack Consulting, formed
in 1991 with one purpose — to help. He has been helping clients for over 30 years, including IBM, McDonnell Douglas, Waste Management, U.S. Environmental Protection Agency, Nations Bank, Bank of America, and Bank One to name a few. He is also a faculty member of DePaul University’s College of Computing and Digital Media.
His childhood interest in electronics helped him break into the computer industry. He still has the Heathkit H-8 digital computer and H-9 video terminal that he built in 1978. He taught himself programming using Extended Benton Harbor Basic. After the IBM-PC was introduced, Gene built the Heathkit H-151 PC-compatible computer, which is still running today. If you ask him about the information revolution, his response would be “Fascinating.” He has developed on computers that loaded programs from paper tape, to the revolutionary NeXT computer (he still has two), to the iPhone and iPad. Gene feels very fortunate to have not only seen an industry evolve, but also to have been an active participant in it. He is thrilled to have penned a companion text to his Developing NeXTSTEP Applications, which he wrote back in 1995.
About the Technical Editor
Daniel W. Meeks has been grep, awk and sed(ing) on Unix systems since Version 9 from Bell Labs and 2.8 BSD. He was at Bell Labs in Naperville during the Cfront Version 1 days of C++. Since then he has been managing technology teams developing in C, Objective-C, C++, Java, and C# in various financial institutions and traveling the world in support of them. Today he spends his weekends putting up infrastructure and develops mobile applications on Apple, Windows and Android as a hobby.
Credits Executive Editor
Carol Long
Vice President and Executive Group Publisher
Richard Swadley Senior Project Editor
Ami Frank Sullivan
Vice President and Executive Publisher
Barry Pruett Technical Editor
Daniel Meeks
Associate Publisher
Jim Minatel Production Editor
Daniel Scribner
Project Coordinator, Cover
Lynsey Stanford Copy Editor
Luann Rouff
Compositor
Editorial Director
Jeff Lytle, Happenstance Type-O-Rama
Robyn B. Siesky Proofreader Editorial Manager
Nancy Bell
Mary Beth Wakefield Indexer Freelancer Editorial Manager
Ron Strauss
Rosemarie Graham Cover Designer Associate Director of Marketing
Michael E. Trent
Ashley Zurcher Cover Image Production Manager
Tim Tate
© Hedda Gjerpen
Acknowledgments
I would first like to thank my wife Roseann, my son Zachary, and my daughter Marissa for putting
up with me while I was writing this book; and my son Ethan for keeping me young (remember what you are to do if this idea ever comes up again). I would also like to thank my mother Mary Louise and daughter Hannah Angel, for giving me the gift of themselves and their spirit, which I carry with me every day of my life. I miss you. To my father, who is there when I need him, and also has a really great name… thank you Gene! To Helen and Jerry, you will never know how much you mean to me. To Dan, I am glad we were able to share time together again after so many years. This book is more than just words and code. It is time and people. The finished product you hold in your hand is a snapshot of events that somehow managed to come together with the very hard work of a lot of people; Carol, Ami, and Luann, I want to specifically thank you three for everything that you did. Finally I would like to once again thank Jean-Marc Krikorian for really doing nothing as he did the last time back in ’95. I cannot forget you, the reader, for taking time out of your day to spend it with me. This book was written for you, and I hope I have helped you in some way. If you have any questions or comments, please let me know, thank you again!
Contents
Introduction Chapter 1: Navigation
Navigation Stack The Navigation Bar UINavigationBarDelegate Protocol Configuring Navigation Bars Pushing and Popping Items
A Simple Navigation Bar Development Steps: A Simple Navigation Bar Test Your Application
The Toolbar A Simple Toolbar Development Steps: A Simple Toolbar Test Your Application
The Tab Bar UITabBarDelegate Protocol Customizing Tab Bars
A Simple Tab Bar Development Steps: A Simple Tab Bar Designing the View Controllers Test Your Application
Summary Chapter 2: Alerts, Action Sheets, and Modal Views
Alerts Alert View Design The UIAlertViewDelegate Protocol
A Simple AlertView for the iPhone-iPod Touch and iPad Development Steps: Loading a File into a UITextView Test Your Application
Action Sheets Action Sheet Design The UIActionSheetDelegate Protocol
xxiii 1
2 2 2 2 3
3 4 17
17 18 18 26
26 26 26
26 27 30 51
51 53
53 54 54
54 55 66
66 66 67
CONTENTS
An Action Sheet for the iPhone-iPod Touch Development Steps: Creating an Action Sheet for the iPhone or iPod Touch Test Your Application
An Action Sheet for the iPad Development Steps: Creating an Action Sheet for the iPad Final Steps: Making the Connections Test Your Application
Modal Views Presenting and Dismissing the Modal View Transition Styles Modal Presentation Styles (iPad only)
A Modal View for the iPhone and iPod Touch Development Steps: Creating a Modal View for the iPhone and iPod Touch Test Your Application
A Modal View for the iPad Development Steps: Creating a Modal View for the iPad Test Your Application
Summary Chapter 3: Custom Table Views
Table Views The Table View Cell The UITableViewDataSource Protocol The UITableViewDelegate Protocol
A Custom Table View Application Development Steps: A Custom Table View Application Creating the Contacts.plist Property List File Test Your Application
Summary Chapter 4: The Split View
The UISplitViewController Class UIPopoverControllerDelegate Protocol UISplitViewControllerDelegate Protocol
A Simple Split View Application Development Steps: A Simple Split View Application for the iPad Creating the DataSource.plist Property List File Test Your Application
Summary xvi
67 68 80
80 81 93 95
95 95 95 95
95 96 105
106 108 119
119 121
121 122 122 122
122 122 133 135
135 137
137 138 138
138 141 145 158
158
CONTENTS
Chapter 5: Touch Events
159
Touch Event Handling
159
Single Touch Multi-Touch Taps Swipes and Gestures
160 160 160 161
A Simple Touch Handler
162
Development Steps: A Simple Touch Handler Test Your Application
A Simple Gesture Recognizer Development Steps: A Simple Gesture Recognizer Test Your Application
Summary Chapter 6: Notification Processing
NSNotifications Concepts The Notification Center Registering for Local Notifications Posting Local Notifications Unregistering an Observer
A Local Named Notification Development Steps: A Local Named Notification Test Your Application
A Local Keyboard Notification Development Steps: A Local Keyboard Notification Test Your Application
Summary Chapter 7: Networking Concepts
Communication over a Network A Simple Network Browser Development Steps: A Simple Network Browser
Peer-to-Peer Device Communications Development Steps: Peer-to-Peer Device Communication Test Your Application
Summary
163 174
174 175 197
197 199
200 200 200 200 201
201 202 211
211 212 225
225 227
228 230 230
244 245 262
262
xvii
CONTENTS
Chapter 8: Multimedia
263
Frameworks for Audio
263
Media Player AV Foundation Audio Toolbox Audio Unit OpenAL
Frameworks for Video MPMoviePlayerController Supported Formats
Playing Audio from the iPod Library Development Steps: Playing Audio from the iPod Library Test Your Application
An Application That Plays Video from the iPod Library
263 264 264 264 264
265 265 265
265 266 291
291
Development Steps: An Application That Plays Video from the iPod Library 292 Test Your Application 312
Summary Chapter 9: Application Preferences
Application Configuration Guidelines for Application Preferences Preference Element Types Implementing Preference Hierarchies Accessing the Application’s Preferences
Setting Simple Preferences Development Steps: Setting Simple Preferences Source Code Listings for Setting Simple Preferences Test Your Application
Creating a Child Pane Preference Hierarchy Development Steps: Creating a Child Pane Preference Hierarchy Test Your Application
Summary Chapter 10: Data Storage
Property Lists Uses for Property Lists Suggested Data Element Types Saving and Restoring a Property List
Core Data The Core Data Stack Managed Objects Managed Object Context xviii
312 313
313 314 314 315 315
315 315 320 325
325 326 338
338 339
339 340 340 340
341 341 341 341
CONTENTS
The Managed Object Model Persistent Store Coordinator Xcode Modeling Tool Fetching Managed Objects Deleting Managed Objects
The Common Premise for Data Storage Development Steps: A Simple Application Using Property Lists Test Your Application Development Steps: A Simple Application Using Core Data Test Your Application
Summary Chapter 11: The Pasteboard
Pasteboard Concepts Named Pasteboards Persistence The Editing Menu Cutting the Selection Pasting the Item Dismissing the Editing Menu
Cutting and Pasting Text Development Steps: Cutting and Pasting Text Test Your Application
Cutting and Pasting Images Development Steps: Cutting and Pasting Images Test Your Application
Creating Custom Menus for the iPad Development Steps: Creating Custom Menus for the iPad Test Your Application
Summary Chapter 12: Unit Testing
Setting Up the Environment Using an Application-Testing Target
A Simple Unit Test Development Steps: A Simple Unit Test Creating the Data.plist Data Source Test Your Application
Unit Test Creation Steps: A Simple Unit Test Unit Testing Your Application Registering your Device Unit Testing Your Application
Summary
342 342 342 343 343
343 344 367 367 394
394 395
396 396 396 397 397 398 398
398 398 407
407 409 416
416 418 427
427 429
429 429
430 431 442 445
446 450 451 452
452
xix
CONTENTS
Chapter 13: Performance Tuning and Optimization
Profiling Using the Simulator Using the Device Benefits of Profiling
A Simple Memory Leak Test Development Steps: A Memory Leak Test Development Steps Continued: Using the Instruments Application Development Steps Continued: Using Build and Analyze from the Main Menu
Summary Chapter 14: Integrating iAds
Joining the iAd Network Setting Up Banking Information Enabling Your Application for iAds Configuring Your iAd Preferences
Preparing Your Application to Use the iAd Network Implementing the ADBannerView Integrating the iAd Framework
A Simple Application Using iAds Development Steps: A Simple Application Using iAds Test Your Application
Summary Chapter 15: Multitasking
Multitasking Services UIApplication Delegate Messages Multitasking Responsibilities Xcode Simulator Multitasking Limitations Device Support of Multitasking
An Application That Multitasks Audio Development Steps: An Application That Multitasks Audio Test Your Application
Summary Appendix A: Your Initial App — First Steps
Xcode Project Builder Available Application Types The Project Window Build and Run in the Simulator xx
453
454 454 454 454
455 459 467 467
468 469
469 470 470 470
470 470 470
471 472 479
479 48 1
481 482 482 483 483
483 484 503
503 505
505 506 508 512
CONTENTS
Interface Builder Creating an Interface Builder Document The Document Window The Library Window The Inspector Window The Connections Panel
Summary Appendix B: iPhone Developer Center
Resources for the iOS 4 SDK
512 513 513 513 514 514
514 515
515
Downloads iOS 4 Reference Library Sample Code
516 516 517
iPhone Developer Program
518
iPhone Provisioning Portal Apple Developer Forums Developer Support Center App Store Resource Center
518 518 519 519
iTunes Connect News & Announcements
520 520
RSS Feed Subscription
520
Summary Appendix C: Cocoa Touch Static Libraries
Xcode Project Template Choose Cocoa Static Library Adding Classes to the Library Source Code Listings for the DataSource Static Library Building the Project
An Xcode Application Project Creating Your New Application Adding the Library to the New Project Adding Library Headers to the App Project Designing the App User Interface Adding Source Code Using the Library Classes Test Your Application
Summary
520 521
521 522 522 523 527
527 528 528 530 530 535 542
542
xxi
CONTENTS
Appendix D: Apple Developer Resources
iPhone Developer Program
543
Developer Centers iOS Resources iOS Developer Resources iOS Developer Guides
543 544 544 544
iTunes App Store Application Distribution Procedures iTunes Connect Developer Guide InDEX
xxii
543
544 545 545 547
Introduction
In 1975 I purchased a Texas Instruments SR-56 pocket calculator. What compelled me to purchase it was the word programmable. I didn’t quite know what that meant, but I knew I had to get it. The SR-56 had 10 memories and 100 program steps. After about a week tinkering with it, I started to understand the power of a program. I still have that calculator today, and while it no longer works, it is a reminder of how fascinating it is to write a program.
Over the decades, I worked with many technologies and languages, and over time, the development process was becoming routine. The excitement that I originally felt with my SR-56 was beginning to fade. That is, until 1989, and one word: NeXT. The excitement had returned. NeXT gave the developer a rich set of tools to produce sophisticated applications within amazing time frames and with relative ease. Developing software just made sense now. When this environment was introduced to the Mac platform, it was the beginning of a great relationship. Today, there is the iPhone, iPod touch, and iPad. For me, 35 years from the day I wrote my first program on my SR-56, I am still like a kid in a candy store. I wrote this book to give you a deeper insight into how you can use these tools to realize your ideas on these devices.
Who This Book Is For This book was written for the developer who is familiar with the Xcode environment and Objective-C language in general. For the reader who is familiar with developing applications but just not up to speed on Xcode and Objective C, please visit the Apple iPhone Dev Center at https://developer.apple.com/iphone/. Each chapter discusses a specific topic or feature of the iPhone/iPad device. Following the discussion, you will find the steps necessary to create a full working example of that feature. The source code that you will develop is structured in a modular fashion, enabling you to extract it and implement the functionality in your own applications.
What This Book Covers The topics covered in this book use the current SDK 4.0 for iPhone and iPod Touch, and the SDK 3.2 for the iPad. Xcode and Interface Builder are the main tools used to create all the applications presented. Finally, with the Instruments application, performance issues are discussed.
How This Book Is Structured Beginning with Chapter 1, the discussion will focus on the application frameworks included in the SDK, and through a step-by-step process, you will design applications that help you understand these frameworks, including when and where they are to be incorporated into your application. In this book, you will learn by doing, and when you’ve completed all the Development Steps sections, you will have experience creating and adding functionality to iPhone and iPad applications using iOS 4. Topics include table views, image views, pickers, data storage, audio and video to name a few. Finally, the book discusses procedures that will ensure your applications perform efficiently, allowing for a desirable user experience from those who purchase your applications from the Apple iTunes App Store.
introdUction
What yoU need to Use this Book To write applications that will run on the iPhone or iPod Touch, you need to download the iPhone SDK 4.0. Included in the SDK 4.0 is the SDK 3.2, which allows applications to be written for the iPad. You can get the SDK at http://developer.apple.com/iphone/. The SDK is free, but you have to register as a developer with Apple to download it. Developer membership is free if you just want to write applications that will run on the included iPhone/iPad simulator. If you actually want to install your applications on your device or would like to sell your applications on the Apple iTunes Store, you have to pay a membership fee. Currently, the most inexpensive fee is $99 per year. Installing the iPhone SDK 4.0 requires a Macintosh computer running Mac OS X 10.6.2 (Snow Leopard) or later.
conventions To help you get the most from the text and keep track of what’s happening, we’ve used a number of conventions throughout the book.
Boxes like this one hold important, not-to-be forgotten information that is directly relevant to the surrounding text.
Notes, tips, hints, tricks, and asides to the current discussion are offset and placed in italics like this. As for styles in the text: ➤➤
New terms and important words are introduced with italics where they are defined.
➤➤
Keyboard strokes look like this: Ctrl+A.
➤➤
File names, URLs, and code within the text appear in a monospace font, like so: persistence. properties.
➤➤
Code is presented in two different ways:
We use a monofont type with no highlighting for most code examples. We use bolded code to emphasize code that’s particularly important in the present context.
soUrce code As you work through the examples in this book, you may choose either to type in all the code manually or to use the source code fi les that accompany the book. All of the source code used in this book is available for download at www.wrox.com. Once at the site, simply locate the book’s title (either by using the Search box or by using one of the title lists) and click the Download Code link on the book’s detail page to obtain all the source code for the book.
xxiv
introdUction
Because many books have similar titles, you may fi nd it easiest to search by ISBN; this book’s ISBN is 978-0-470-87819-4. Once you download the code, just decompress it with your favorite compression tool. Alternately, you can go to the main Wrox code download page at www.wrox.com/dynamic/books/download.aspx to see the code available for this book and all other Wrox books.
errata We make every effort to ensure that there are no errors in the text or in the code. However, no one is perfect, and mistakes do occur. If you fi nd an error in one of our books, like a spelling mistake or faulty piece of code, we would be very grateful for your feedback. By sending in errata you may save another reader hours of frustration, and at the same time you will be helping us provide even higher quality information. To fi nd the errata page for this book, go to www.wrox.com and locate the title using the Search box or one of the title lists. Then, on the Book Search Results page, click the Errata link. On this page you can view all errata that have been submitted for this book and posted by Wrox editors.
A complete book list, including links to errata, is also available at www.wrox.com/ misc-pages/booklist.shtml. If you don’t spot “your” error on the Errata page, click the Errata Form link and complete the form to send us the error you have found. We’ll check the information and, if appropriate, post a message to the book’s errata page and fi x the problem in subsequent editions of the book.
P2P .WroX .com For author and peer discussion, join the P2P forums at p2p.wrox.com. The forums are a Web-based system for you to post messages relating to Wrox books and related technologies and interact with other readers and technology users. The forums offer a subscription feature to e-mail you topics of interest of your choosing when new posts are made to the forums. Wrox authors, editors, other industry experts, and your fellow readers are present on these forums. At http://p2p.wrox.com you will fi nd a number of different forums that will help you not only as you read this book, but also as you develop your own applications. To join the forums, just follow these steps:
1 . 2 . 3 .
Go to p2p.wrox.com and click the Register link.
4 .
You will receive an e-mail with information describing how to verify your account and complete the joining process.
Read the terms of use and click Agree. Complete the required information to join as well as any optional information you wish to provide and click Submit.
xxv
introdUction
You can read messages in the forums without joining P2P but in order to post your own messages, you must join. Once you join, you can post new messages and respond to messages other users post. You can read messages at any time on the Web. If you would like to have new messages from a particular forum e-mailed to you, click the Subscribe to this Forum icon by the forum name in the forum listing. For more information about how to use the Wrox P2P, be sure to read the P2P FAQs for answers to questions about how the forum software works as well as many common questions specific to P2P and Wrox books. To read the FAQs, click the FAQ link on any P2P page.
xxvi
1
navigation What’s in this chaPter? ➤➤
How navigation works on the iPad’s split view
➤➤
Using a toolbar to rotate an image
➤➤
Implementing a simple bank account transaction tracker using a tab bar
Navigation is the process of searching through a hierarchy to arrive at the information you desire. With the iPhone and iPad, navigating through your data is achieved through the following components; each has a specific viewing philosophy: ➤➤
Navigation bar — Arranges the data in a hierarchy that you can navigate by drilling down, and provides a path back to the top
➤➤
Toolbar — Provides a number of options that act on the current view context
➤➤
Tab bar — Provides different views of the same set of data
This chapter presents the steps to create an application for each of these navigation components to demonstrate a simple use case for each style. The device on which your application runs determines the style of navigation you implement. Remember that there is more drilling down on an iPhone than on an iPad because of the limited viewing area. This is very important if you are planning to develop an application that will be available on both devices. The iPad should not be just a duplicate application, in terms of visual presentation.
When considering navigation design for your applications, consult Apple’s User Interface Guidelines for the iPad as well as the iPhone. See Appendix D for documentation sources for these and other developer guides offered by Apple.
2
❘ Chapter 1 Navigation
Navigation Stack The navigation process is stack based. These views are stacked in a last in, first out (LIFO) manner, which is managed by a View Controller. The initial view is the root View Controller, which has views placed over it. Unlike the views that are pushed upon it, the root View Controller can never be removed. The process of navigation involves responding to user interaction, pushing and popping View Controllers on and off the navigation stack. Each current view is responsible for pushing the next view onto the stack. The object that serves as the content provider for navigation items is the navigation bar. Users interact with buttons on the navigation bar, and through delegate messages sent to the View Controller, the pushing or popping of views is performed.
The Navigation Bar The navigation bar basically holds control objects that provide the process of navigating views in an application. The navigation bar provides users with all the controls necessary to push down or pop the views in the application’s hierarchy. Processing of the delegate messages is handled by an associated View Controller through the use of delegate methods.
UINavigationBarDelegate Protocol The View Controller implements the methods of this protocol when it has to either push or pop an item onto or off of the navigation stack. The methods to implement are as follows: ➤➤
➤➤
To push an item 1.
navigationBar:shouldPushItem:
2.
navigationBar:didPushItem:
To pop an item 1.
navigationBar:shouldPopItem:
2.
navigationBar:didPopItem:
Configuring Navigation Bars The navigation bar is located at the top of the view and will display the title of the current view. In addition to the title, the navigation bar may contain button controls that provide action within the context of the current view. To achieve this functionality, the following are available: ➤➤
backBarButtonItem and leftBarButtonItem are positioned on the left.
➤➤
titleView is positioned in the center.
➤➤
rightBarButonItem is positioned on the right.
The navigation bar itself has a few properties that can be modified: ➤➤
barStyle
➤➤
translucent
➤➤
tintColor
A Simple Navigation Bar
❘ 3
Pushing and Popping Items To navigate from view to view, either to continue drilling down the hierarchy (pushing), or backing out back up the hierarchy (popping), the process is handled by the view controllers in your application. The process of navigation is simply a navigation controller managing a series of view controllers on the navigation stack. A view controller is responsible for pushing (drilling down the hierarchy) and popping (backing out up the hierarchy) other view controllers on or off the navigation stack. The process of navigation is simply a navigation controller managing a series of view controllers on the navigation stack, and it works like this:
1. 2. 3. 4.
The UINavigationController is created. The navigation controller pushes the view controller onto the navigation stack. The view controller then presents the next view. The view controller then dismisses the previous view.
A Simple Navigation Bar In this application navigation will consist of displaying the numbers from 1 to 20. The grouping is even or odd numbers. Notice that with the split view of the iPad, portrait orientation presents the navigation bar as a popover; in landscape orientation, the navigation bar is in the left pane of the split view, as shown in Figure 1-1.
Figure 1-1
Tapping Odd will reveal another navigation bar with a list of the odd numbers from 1 to 20, as shown in Figure 1-2. Tapping a number from this list will display the choice in the detail view, and the popover disappears (see Figure 1-3).
4
❘ Chapter 1 Navigation
Figure 1-2
Figure 1-3
Development Steps: A Simple Navigation Bar To create this application that will be the server, execute the following steps:
1.
2.
Start Xcode and create a SplitView-based application and name it NavigationBar-iPad. If you need to see this step, please see Appendix A for the steps to begin a SplitView-based application. In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File and select UIViewController subclass then check the UITableViewController subclass option and name it RootDetailViewController.
For this application, Interface Builder will not be used, as all the views will be programmatically created. Now it is time to enter your logic.
Source Code Listings for the A Simple Navigation Bar Application For this application the NavigationBar_iPadAppDelegate.h and NavigationBar_iPadAppDelegate.m files are not modified and are used as generated.
RootViewController.h Modifications to the Template The additions to the RootViewController class will be two NSArrays to hold the even and odd numbers. The two arrays will be stored in an NSDictionary with even and odd as the keys (see Listing 1-1).
A Simple Navigation Bar
❘ 5
Listing 1-1: The complete RootViewController.h file (Chapter1/NavigationBar-iPad/Classes/ RootViewController.h) #import
@class DetailViewController; @interface RootViewController : UITableViewController { DetailViewController *detailViewController; NSDictionary *groupsDict; NSArray *evenArray; NSArray *oddArray; } @property (nonatomic, retain) IBOutlet DetailViewController *detailViewController; @property (nonatomic, retain) NSDictionary *groupsDict; @property (nonatomic, retain) NSArray *evenArray; @property (nonatomic, retain) NSArray *oddArray; - (void)initData; @end
RootViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the RootViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-2). Listing 1-2: Addition of @synthesize #import “RootViewController.h” #import “DetailViewController.h” #import “RootDetailViewController.h”
@implementation RootViewController @synthesize @synthesize @synthesize @synthesize
detailViewController; groupsDict; evenArray; oddArray;
To initialize the view, the size of the popover is defined, and the even and odd arrays are defined and initialized (see Listing 1-3). Listing 1-3: Initialization of the view #pragma mark #pragma mark View lifecycle - (void)viewDidLoad {
6
❘ Chapter 1 Navigation
[super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; [self setContentSizeForViewInPopover:CGSizeMake(200.0, 100.0)]; [self setTitle:@”Choices”]; [self initData]; } - (void)initData { NSMutableArray *even = [NSMutableArray array]; NSMutableArray *odd = [NSMutableArray array]; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSString *msg = nil; for (int i=0;i<20; i++) { msg = [NSString stringWithFormat:@”Number = %d”, i]; if ( i % 2 == 0 ) { [even addObject:msg]; } else { [odd addObject:msg]; } } [dict setObject:even forKey:@”Even”]; [dict setObject:odd forKey:@”Odd”]; [self setGroupsDict:dict]; } // Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation) interfaceOrientation { return YES; }
To initialize a table view, there are two factors to consider. If you have just a single list of items, you will have one section and a number of rows that represents the items in your list. If you want to separate certain items in the list from others, you then have to consider how many sections into which the list is to be divided. For this application, there is just one list of related data, so the number of sections is one. The number of rows is simply the two arrays in the dictionary groupDict (see Listing 1-4). Listing 1-4: TableView display definition #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [groupsDict count]; }
The tableView:cellForRowAtIndexPath method is where each cell of the table view is assigned a value using the method [[cell textLabel] setText:key] and then displayed. The value of each cell is the key value for each item in the groupsDict. For this application the keys will be Even for the array that contains the even numbers, and Odd for the array that contains the odd numbers (see Listing 1-5).
A Simple Navigation Bar
Listing 1-5: TableView cell display #pragma mark #pragma mark Table view delegate - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”CellIdentifier”; // Dequeue or create a cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:key]; return cell; }
In this application, if the cell labeled Even is tapped, then RootDetailViewController is pushed onto the navigational stack and the list of even numbers is displayed in the resulting view. If the cell labeled Odd is tapped, the odd numbers are displayed. (see Listing 1-6). Listing 1-6: TableView cell selected #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSArray *values = [[self groupsDict] objectForKey:key]; /* When a row is selected, set the detail view controller’s detail item to the item associated with the selected row. */ RootDetailViewController *rootDetailViewController = [[RootDetailViewController alloc] initWithKey:key values:values viewController:[self detailViewController]]; [[self navigationController] pushViewController:rootDetailViewController animated:YES]; [rootDetailViewController release]; }
The complete RootViewController.m file is shown in Listing 1-7.
❘ 7
8
❘ Chapter 1 Navigation
Listing 1-7: The complete RootViewController.m file (Chapter1/NavigationBar-iPad/Classes/ RootViewController.m) #import “RootViewController.h” #import “DetailViewController.h” #import “RootDetailViewController.h”
@implementation RootViewController @synthesize @synthesize @synthesize @synthesize
detailViewController; groupsDict; evenArray; oddArray;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; [self setContentSizeForViewInPopover:CGSizeMake(200.0, 100.0)]; [self setTitle:@”Choices”]; [self initData]; } - (void)initData { NSMutableArray *even = [NSMutableArray array]; NSMutableArray *odd = [NSMutableArray array]; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSString *msg = nil; for (int i=0;i<20; i++) { msg = [NSString stringWithFormat:@”Number = %d”, i]; if ( i % 2 == 0 ) { [even addObject:msg]; } else { [odd addObject:msg]; } } [dict setObject:even forKey:@”Even”]; [dict setObject:odd forKey:@”Odd”]; [self setGroupsDict:dict]; } // Ensure that the view controller supports rotation and that // the split view can therefore // show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView {
A Simple Navigation Bar
// Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [groupsDict count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”CellIdentifier”; // Dequeue or create a cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:key]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSArray *values = [[self groupsDict] objectForKey:key]; /* When a row is selected, set the detail view controller’s detail item to the item associated with the selected row. */ RootDetailViewController *rootDetailViewController = [[RootDetailViewController alloc] initWithKey:key values:values viewController:[self detailViewController]]; [[self navigationController] pushViewController:rootDetailViewController animated:YES]; [rootDetailViewController release]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning {
❘ 9
10
❘ Chapter 1 Navigation
[super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated // in viewDidLoad or on demand. // For example: self.myOutlet = nil; [self setGroupsDict:nil]; [self setEvenArray:nil]; [self setOddArray:nil]; [self setDetailViewController:nil]; } - (void)dealloc { [groupsDict release]; [evenArray release]; [oddArray release]; [detailViewController release]; [super dealloc]; } @end
RootDetailViewController.h Modifications to the Template The RootDetailViewController class will display the actual even or odd values; and when one of the table view cells is selected, the value of the cell will be displayed on the main detail page. The complete RootDetailViewController class is shown in Listing 1-8. Listing 1-8: The complete RootDetailViewController.h file (Chapter1/NavigationBar-iPad/ Classes/RootDetailViewController.h) #import @class DetailViewController; @interface RootDetailViewController : UITableViewController { DetailViewController *detailViewController; NSString *key; NSArray *values; } @property (nonatomic, retain) DetailViewController *detailViewController; @property (nonatomic, retain) NSString *key; @property (nonatomic, retain) NSArray *values; - initWithKey:(NSString *)aKey values:(NSArray *)aValues viewController:(id)viewController; @end
RootDetailViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the RootDetailViewController.m template.
A Simple Navigation Bar
❘ 11
For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-9). Listing 1-9: Addition of @synthesize #import “RootDetailViewController.h” #import “DetailViewController.h”
@implementation RootDetailViewController @synthesize key; @synthesize values; @synthesize detailViewController;
To initialize the view, the size of the popover is defined, and the even and odd arrays are defined and initialized (see Listing 1-10). Listing 1-10: Initialization of the view #pragma mark #pragma mark Initialization - initWithKey:(NSString *)aKey values:(NSArray *)aValues viewController:(id)viewController { [self setKey:aKey]; [self setValues:aValues]; [self setDetailViewController:viewController]; return self; } #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; [self setContentSizeForViewInPopover:CGSizeMake(200.0, 500.0)]; [self setTitle:[self key]]; } #pragma mark #pragma mark Rotation support // Ensure that the view controller supports rotation and that the // split view can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; }
To initialize a table view, there are two factors to consider. The first is the number of sections that your data would be divided up into — for this application there is only one group because all the data is related. The second factor is the number of rows. This is the list from which the users will make their selections. For this application the rows of data will represent all even or all odd numbers (see Listing 1-11).
12
❘ Chapter 1 Navigation
Listing 1-11: TableView display definition #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [[self values] count]; }
The tableView:cellForRowAtIndexPath method is where the table view cells are populated with display details. Because the numberOfRowsInSection used the values count, the values for the table view cell display will be the actual even or odd number (see Listing 1-12). Listing 1-12: TableView cell display #pragma mark #pragma mark Table view appearance // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *value = [[self values] objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... [[cell textLabel] setText:value]; return cell; }
When one of the table view cells is tapped, the row is selected. For this application, the tap causes the number value on the main detail view (see Listing 1-13) to display. Listing 1-13: TableView cell selected #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [[self detailViewController] setText:[[self values] objectAtIndex:[indexPath row]]]; }
A Simple Navigation Bar
The complete RootDetailViewController.m file is shown in Listing 1-14. Listing 1-14: The complete RootDetailViewController.m file (Chapter1/NavigationBar-iPad/ Classes/ RootDetailViewController.m) #import “RootDetailViewController.h” #import “DetailViewController.h”
@implementation RootDetailViewController @synthesize key; @synthesize values; @synthesize detailViewController; #pragma mark #pragma mark Initialization - initWithKey:(NSString *)aKey values:(NSArray *)aValues viewController:(id)viewController { [self setKey:aKey]; [self setValues:aValues]; [self setDetailViewController:viewController]; return self; } #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; [self setContentSizeForViewInPopover:CGSizeMake(200.0, 500.0)]; [self setTitle:[self key]]; } #pragma mark #pragma mark Rotation support // Ensure that the view controller supports rotation and that the // split view can therefore // show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [[self values] count];
❘ 13
14
❘ Chapter 1 Navigation
} #pragma mark #pragma mark Table view appearance // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *value = [[self values] objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... [[cell textLabel] setText:value]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [[self detailViewController] setText:[[self values] objectAtIndex:[indexPath row]]]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated // in viewDidLoad or on demand. // For example: self.myOutlet = nil; [self setDetailViewController:nil]; [self setKey:nil]; [self setValues:nil]; } - (void)dealloc { [detailViewController release]; [key release]; [values release]; [super dealloc]; } @end
A Simple Navigation Bar
❘ 15
DetailViewController.h Modifications to the Template When either the even or the odd values are selected from the navigation bar, the values will be displayed on a label in the center of the DetailViewController’s view. The complete DetailViewController file is shown in Listing 1-15. Listing 1-15: The complete DetailViewController.h file (Chapter1/NavigationBar-iPad/Classes/
DetailViewController.h)
#import @interface DetailViewController : UIViewController { UIPopoverController *popoverController; UIToolbar *toolbar; id detailItem; UILabel *detailDescriptionLabel; } @property (nonatomic, retain) IBOutlet UIToolbar *toolbar; @property (nonatomic, retain) id detailItem; @property (nonatomic, retain) IBOutlet UILabel *detailDescriptionLabel; - (void)setText:(NSString *)newText; @end
DetailViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the DetailViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-16). Listing 1-16: Addition of @synthesize #import “DetailViewController.h” #import “RootViewController.h” @interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; @end @implementation DetailViewController @synthesize toolbar, popoverController, detailItem, detailDescriptionLabel;
The setText method, shown in Listing 1-17, sets the text on the label in the middle of the view, and dismisses the popover. Listing 1-17: The setText method #pragma mark #pragma mark Managing the detail item - (void)setText:(NSString *)newText {
16
❘ Chapter 1 Navigation
[[self detailDescriptionLabel] setText:newText]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } }
The complete DetailViewController.m file is shown in Listing 1-18. Listing 1-18: The complete DetailViewController.m file (Chapter1/NavigationBar-iPad/Classes/ DetailViewController.m) #import “DetailViewController.h” #import “RootViewController.h”
@interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; @end @implementation DetailViewController @synthesize toolbar, popoverController, detailItem, detailDescriptionLabel; #pragma mark #pragma mark Managing the detail item - (void)setText:(NSString *)newText { [[self detailDescriptionLabel] setText:newText]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } } #pragma mark #pragma mark Split view support - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = @”Root List”; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; }
// Called when the view is shown again in the split view, invalidating // the button and popover controller. - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [toolbar setItems:items animated:YES];
The Toolbar
❘ 17
[items release]; self.popoverController = nil; }
#pragma mark #pragma mark Rotation support // Ensure that the view controller supports rotation and that the // split view can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark View lifecycle - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; //self.popoverController = nil; }
#pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { [popoverController release]; [toolbar release]; [detailItem release]; [detailDescriptionLabel release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, launch the iPad Simulator. It should give you the results described at the beginning of A Simple Navigation Bar section.
The Toolbar The toolbar differs from the navigation bar initially, because it appears at the bottom of the view rather than the top, and it also differs because all the available views are are easily accessed from buttons that reside on the toolbar itself. Users do not have to drill down to the view they desire, they simply tap a button on the toolbar and the associated view is immediately displayed. Items that are to appear on the toolbar are equally spaced and include fixed and flexible items that keep the presentation uniform. The thing to keep in mind is that the finger width will have a hard time selecting more than five items due to the size of the human finger and the limited space of the iPhone. The iPad has a larger view to handle the touch of a finger.
18
❘ Chapter 1 Navigation
The items are instances of UIBarButtonItem and can have the following styles: ➤➤
UIBarButtonItemStylePlain
➤➤
UIBarButtonItemStyleBordered
➤➤
UIBarButtonItemStyleDone
They are similar to regular buttons but have additional functionality for use with navigation. The following methods are used for initialization: ➤➤
initWithBarButtonSystemItem:target:action:
➤➤
initWithCustomView:
➤➤
initWithImage:style:target:action:
➤➤
initWithTitle:style:target:action:
A Simple Toolbar In this application, an image is centered in the display. The user will tap several of the angle toolbar items. The image will rotate according to the angle value of the toolbar item, as shown in Figure 1-4.
Development Steps: A Simple Toolbar To create a simple toolbar application, execute the following steps:
1.
Start Xcode and create a View-based application for the iPhone and name it SimpleToolbar-iPhone. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
2.
You need to add one UIImageView and one UIToolbar and four UIBarButtonItems to the project.
3.
➤➤
Select Resources in Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your image and name it, grandpa.png. Use an image around 300 × 300 pixels and click Add as shown in Figure 1-5.
➤➤
Check Copy items into destination group’s folder, and click Add as shown in Figure 1-6.
Double-click the SimpleToolbar_iPhoneView Controller.xib file to launch Interface Builder (see Figure 1-7).
4.
Figure 1-4
From the Interface Builder Library (Tools ➪ Library), choose and drag the following to the View window. Your interface should now look like Figure 1-8: One UIImageView with the size of 260 × 260. To accomplish this do the following:
➤➤
1. 2.
Drag your UIImageView to the View window, where it will take up the entire view.
3.
Change the size of the view to W:260 H:260 and center it on the view.
Choose Tools ➪ Size Inspector from the main menu and you will see W:240 W:128 just under the Frame drop-down in the upper-right corner of the inspector.
A Simple Toolbar
Figure 1-5
Figure 1-7
Figure 1-6
❘ 19
20
❘ Chapter 1 Navigation
➤➤
One UIToolbar and place it at the bottom of the view.
➤➤
Three UIBarButtonItems and place them on the toolbar and choose Tools ➪ Attributes Inspector and enter the following: ➤➤
+45 for the Title and 0 for the Tag for first button
➤➤
+180 for the Title and 1 for the Tag for second button
➤➤
−180 for the Title and 2 for the Tag for third button − 45 for the Title and 3 for the Tag for fourth button
➤➤ ➤➤
One Flexible Space Bar Button Item and place it to the left of your first button. Repeat, but place the other one to the right of the last button (see Figure 1-9).
Figure 1-8
5.
Figure 1-9
Back in the Interface Builder Library, click Classes at the top and scroll to and select your SimpleToolbar_iPhoneViewController class. At the bottom, now choose the Outlets button. Click
the + and add the following outlet, as shown in Figure 1-10: ➤➤
imageView (as a UIImageView instead of id type)
A Simple Toolbar
6.
Choose the Actions button. Then click the + and add the following action, as shown in Figure 1-11: ➤➤
rotateView
Figure 1-10
7.
8.
❘ 21
Figure 1-11
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first popup and then Merge from the second pop-up. The SimpleToolbar_iPhoneViewController.m file appears with your new additions on the left and the original template on the right (see Figure 1-12). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
For the next window, SimpleToolbar_iPhoneViewController.h, your new addition is on the left and the original template is on the right (see Figure 1-13). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
22
❘ Chapter 1 Navigation
Figure 1-12
Figure 1-13
A Simple Toolbar
9.
❘ 23
You now have an Objective-C template that holds your application’s view logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make the connection to: ➤➤
Identify the UIImageView as imageView
To make the connection to identify the UIImageView as imageView, control-click on the File’s Owner icon to bring up the Inspector (see Figure 1-14). 10.
From the right of the File’s Owner Inspector, control-drag from the circle by imageView to the UIImageView imageView until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made.
11.
From the right of the File’s Owner Inspector, control-drag from the circle by rotateView to each of the UIBarButtonItems (see Figure 1-15). Choose File ➪ Save and dismiss the File’s Owner Inspector.
Figure 1-14
Figure 1-15
Now it is time to enter your logic.
Source Code Listings for A Simple Toolbar For this application, the SimpleToolbar_iPhoneAppDelegate.h and SimpleToolbar_ iPhoneAppDelegate.m files are not modified and are used as generated:
SimpleToolbar_iPhoneViewController.h Modifications to the Template You declared the following outlet in Interface Builder: ➤➤
imageView
You must now define the properties for this variable in order to get and set its value (see Listing 1-19). The IBOutlet was moved to the property declaration. Listing 1-19: The complete SimpleToolbar_iPhoneViewController.h file (/Chapter1/
SimpleToolbar-iPhone/Classes/SimpleToolbar_iPhoneViewController.h) #import @interface SimpleToolbar_iPhoneViewController : UIViewController { UIImageView *imageView; } @property (nonatomic, retain) IBOutlet UIImageView *imageView; - (IBAction)rotateView:(id)sender; @end
24
❘ Chapter 1 Navigation
SimpleToolbar_iPhoneViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the SimpleToolbar_iPhoneViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-20). Listing 1-20: Addition of @synthesize #import “SimpleToolbar_iPhoneViewController.h” @implementation SimpleToolbar_iPhoneViewController @synthesize imageView;
When the app launches, the default image, grandpa.png, is loaded and displayed (see Listing 1-21). Listing 1-21: The viewDidLoad method #pragma mark #pragma mark View lifecycle // Implement viewDidLoad to do additional setup after loading the view, // typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; [imageView setImage:[UIImage imageNamed:@”grandpa.jpg”]]; }
When the toolbar item buttons are tapped, the rotateView method is called and, depending on the item’s tag value, determines the rotation angle, as shown in Listing 1-22. Listing 1-22: The rotateView method #pragma mark #pragma mark Action methods - (IBAction)rotateView:(id)sender { static CGFloat angle = 0.0; switch ([sender tag]) { case 0: angle += 45.0; break; case 1: angle += 180.0; break; case 2: angle -= 180.0; break; case 3: angle -= 45.0; break; default: break; } CGAffineTransform transform=CGAffineTransformMakeRotation(angle); [imageView setTransform:transform]; }
A Simple Toolbar
The SimpleToolbar_iPhoneViewController.m file is now complete. Listing 1-23 shows the complete implementation. Listing 1-23: The complete SimpleToolbar_iPhoneViewController.m file (/Chapter1/ SimpleToolbar-iPhone/Classes/SimpleToolbar_iPhoneViewController.h #import “SimpleToolbar_iPhoneViewController.h” @implementation SimpleToolbar_iPhoneViewController @synthesize imageView; #pragma mark #pragma mark View lifecycle // Implement viewDidLoad to do additional setup after loading the view, // typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; [imageView setImage:[UIImage imageNamed:@”grandpa.jpg”]]; } #pragma mark #pragma mark Action methods - (IBAction)rotateView:(id)sender { static CGFloat angle = 0.0; switch ([sender tag]) { case 0: angle += 45.0; break; case 1: angle += 180.0; break; case 2: angle -= 180.0; break; case 3: angle -= 45.0; break; default: break; } CGAffineTransform transform=CGAffineTransformMakeRotation(angle); [imageView setTransform:transform]; } #pragma mark #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setImageView:nil];
❘ 25
26
❘ Chapter 1 Navigation
} - (void)dealloc { [imageView release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the “A Simple Toolbar” section.
The Tab Bar The tab bar is used either when you have different operations on a single procedure or when you have different views on a single data source, such as a weather application that provides views for current temperature, hourly view, five-day forecast, and satellite images. The tab bar provides a convenient way for users to view different perspectives on their data without having to drill down to find the results.
UITabBarDelegate Protocol The UITabBarDelegate protocol defines methods of UITabBar delegates that provide customization of the tab bar itself.
Customizing Tab Bars The process of customizing a tab bar involves adding, removing, or reordering items, and it uses the following methods (one method, tabBar:didSelectItem, is required): ➤➤
tabBar:willBeginCustomizingItems:
➤➤
tabBar:didBeginCustomizingItems:
➤➤
tabBar:willEndCustomizingItems:changed:
➤➤
tabBar:didEndCustomizingItems:changed:
➤➤
tabBar:didSelectItem:
Programmatically, you can customize the tab bar through the beginCustomizingItems method. Calling this method will create a modal view containing the items and a Done button, which when tapped dismisses the modal view.
A Simple Tab Bar This application will simulate bank account transactions, including a summary. Each of the following three options will be represented by a button on the tab bar: ➤➤
Show current balance, as shown in Figure 1-16.
➤➤
Support transactions (deposit and withdrawal), as shown in Figure 1-17.
➤➤
Display a summary of transactions, as shown in Figure 1-18.
A Simple Tab Bar
Figure 1-17
Figure 1-16
❘ 27
Figure 1-18
Development Steps: A Simple Tab Bar To create this application, execute the following steps:
1.
Start Xcode and create a View-based application for the iPhone and name it SimpleTabbar-iPhone. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
2.
Choose File ➪ New File… and select Objective-C class as the subclass of UIViewController, with no options checked, as shown in Figure 1-19, and name the class SecondViewController.
3.
Choose File ➪ New File… and select Objective-C class as the subclass of UIViewController, name the class ThirdViewController, and check the following options: ➤➤
UITableViewController subclass
➤➤
With XIB for user interface
4.
Choose File ➪ New File… and select Objective-C class as the subclass of NSObject, and name the class Transaction.
5.
Choose File ➪ New File… and select Objective-C class as the subclass of NSObject, and name the class PropertyList.
6.
Double-click the MainWindow.xib file to launch Interface Builder. Note that the View Mode is in browser mode and select the Tab Bar Controller (see Figure 1-20).
28
❘ Chapter 1 Navigation
Figure 1-19
Figure 1-20
A Simple Tab Bar
7.
8.
From the Interface Builder Library (Tools ➪ Attributes Inspector), click on the following in the View Controllers section (see Figure 1-21): ➤➤
Double-click First and replace with Balance.
➤➤
Double-click Second and replace with Transaction.
➤➤
Click the plus button and add Summary for the title and TableViewController for the class.
From the MainWindow.xib window, click on the following under the Tab Bar Controller section (see Figure 1-22): ➤➤
9.
10.
❘ 29
Click the third entry, TableViewController (Summary); now, in the Attributes Inspector under Nib name, choose ThirdViewController.
➤➤
Double-click Second and replace with Transaction.
➤➤
Click the plus button and add Summary for the title and TableViewController for the class.
From the main menu, click Tools ➪ Identity Inspector, and choose ThirdViewController for the class identity. Under the Tab Bar Controller entry in the MainWindow.xib window, choose View Controller (Transaction), which is just above the Third View Controller you previously selected. From the Identity Inspector, select SecondViewController for the class identity.
Figure 1-21
Figure 1-22
30
❘ Chapter 1 Navigation
11.
12.
Double-click the Tab Bar Controller entry in the MainWindow.xib window, and your Tab View Controller window should appear as shown in Figure 1-23. Click the third button, Summary, and then click Tools ➪ Identity Inspector and select ThirdViewController from the Class Identity. Finally, choose File ➪ Save.
Designing the View Controllers In this section each button in the tab bar is associated with its own view controller. Each view controller is divided into its own series of steps.
The First View Controller The first view controller will display the current balance in the bank account. To create this view:
1.
Return back to Xcode and in the Groups & Files window, double-click on FirstView.xib to bring up the window, which has some labels already in it. Select and delete these labels.
2.
From Interface Builder (Tools ➪ Identity Inspector), select File’s Owner and FirstViewController as the class. ➤➤
3.
In the Interface Builder Library, click Classes at the top and scroll to and select your FirstViewController class. At the bottom, choose the Outlets button. Click the + and add the following outlet, as shown in Figure 1-25: ➤➤
4.
Add two UILabels next to each other in the middle of the view. Double-click the leftmost label and enter Balance (see Figure 1-24).
balanceLabel (as a UILabel instead of id type)
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save and then Merge. Close the FirstViewController.m window, as there are no changes.
Figure 1-23
Figure 1-24
Figure 1-25
A Simple Tab Bar
The FirstViewController.h file appears with your new additions on the left and the original template on the right (see Figure 1-26). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 1-26
5.
You now have an Objective-C template that holds your application’s view logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
6.
Identify the UILabel as balanceLabel
To make the connection to identify the UILabel as balanceLabel, control-click on the File’s Owner icon to bring up the
Figure 1-27
Inspector (see Figure 1-27).
7.
From the right of the File’s Owner Inspector, control-drag from the circle by balanceLabel to the UILabel balanceLabel until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made (see Figure 1-28). Choose File ➪ Save and dismiss the File’s Owner Inspector.
Figure 1-28
❘ 31
32
❘ Chapter 1 Navigation
The Second View Controller The second view controller is the transaction view, where the deposits and withdrawals are entered. To create this view:
1.
Back in Xcode in the Groups & Files window, double-click on SecondView.xib to bring up the window, which has some labels already in it. Select and delete these labels.
2.
From Interface Builder (Tools ➪ Identity Inspector), select File’s Owner and SecondViewController as the class.
3.
➤➤
Add one UISegmentedControl at the top of the view. Double-click the left segment and enter Deposit. Double-click the right segment and enter Withdrawal.
➤➤
Add one UILabels and one UITextField placed right next to each other in the middle of the view. Double-click the label and enter Amount:.
➤➤
Add one UIButton just below the label and text field. Double-click the button and enter Save (see Figure 1-29).
In the Interface Builder Library, click Classes at the top and scroll to and select your FirstViewController class. At the bottom, choose the Outlets button. Click the + and add the fol-
lowing outlets, as shown in Figure 1-30:
4.
➤➤
amountTextField (as a UITextField instead of id type)
➤➤
transactionType (as a UISegmentedControl instead of id type)
Choose the Actions button. Then click the + and add the following action, as shown in Figure 1-31: ➤➤
Figure 1-29
saveTransaction
Figure 1-30
Figure 1-31
A Simple Tab Bar
5.
❘ 33
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save and then Merge. The SecondViewController.h file appears with your new additions on the left and the original template on the right, as shown in Figure 1-32. ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 1-32
6.
7.
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save and then Merge. The SecondViewController.m file appears with your new additions on the left and the original template on the right, as shown in Figure 1-33. ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
You now have an Objective-C template that holds your application’s view logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UITextField as amountTextField
➤➤
Identify the UISegmentedControl as transactionType
34
❘ Chapter 1 Navigation
Figure 1-33
8.
To make the connection to identify the UITextField as amountTextField, control-click on the File’s Owner icon to bring up the Inspector (see Figure 1-34).
9.
10.
11.
From the right of the File’s Owner Inspector, control-drag from the circle by amountTextField to the UITextField amountTextField until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. From the right of the File’s Owner Inspector, controldrag from the circle by transactionType to the UISegmentedControl transactionType until it is highlighted, then release the mouse.
Figure 1-34
From the right of the File’s Owner Inspector, control-drag from the circle by saveTransaction to the Save button until it is highlighted, then release the mouse and choose Touch Up Inside. (see Figure 1-35). Choose File ➪ Save and dismiss the File’s Owner Inspector. Figure 1-35
A Simple Tab Bar
❘ 35
The Third View Controller The third view controller displays all the transactions that have been entered into the applications. The transaction amounts are separated into deposits and withdrawal sections for clarity.
1.
Back in Xcode, in the Groups & Files window, double-click on ThirdViewController.xib to bring up the window.
2.
From Interface Builder (Tools ➪ Identity Inspector), select File’s Owner and ThirdViewController as the class.
3.
From the Interface Builder (Tools ➪ Attributes Inspector), select the table view and choose Tab Bar for the Bottom Bar in the Simulated User Interface Elements section (see Figure 1-36).
4.
In the Interface Builder Library, click Classes at the top and scroll to and select your ThirdView Controller class. At the bottom, choose the Outlets button. Click the + and add the following outlet, as shown in Figure 1-37. ➤➤
5.
detailTableView (as a UITableView instead of id type)
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save and then Merge. Close the ThirdViewController.m window, as there are no changes to it. The ThirdView Controller.h file appears with your new additions on the left and the original template on the right (see Figure 1-38). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 1-36
Figure 1-37
36
❘ Chapter 1 Navigation
Figure 1-38
6.
You now have an Objective-C template that holds your application’s view logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UITableView as detailTableView
7.
To make the connection to identify the UITableView as detailTableView, click on File’s Owner and select ThirdViewController for the Class Identity. Now control-click on the File’s Owner icon to bring up the Inspector (see Figure 1-39).
8.
From the right of the File’s Owner Inspector, control-drag from the circle by detailTableView to the UITableView detailTableView until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made (see Figure 1-40). Choose File ➪ Save and dismiss the File’s Owner Inspector.
Figure 1-39
Figure 1-40
A Simple Tab Bar
❘ 37
This concludes the user interface design section. The next section contains the source code that will provide the logic for the application.
Source Code Listings for A Simple Tab Bar For this application, the SimpleTabbar_iPhoneAppDelegate.h and SimpleTabbar_ iPhoneAppDelegate.m files are not modified and are used as generated.
FirstViewController.h Modifications to the Template You declared the following outlet in Interface Builder: ➤➤
balanceLabel
You must now define the property for this variable in order to get and set its value (see Listing 1-24). The IBOutlet was moved to the property declaration. Listing 1-24: The complete FirstViewController.h file (Chapter1/Tabbar-iPhone/Classes/ FirstViewController.h) #import #import “PropertyList.h”
@interface FirstViewController : UIViewController { UILabel *balanceLabel; } @property (nonatomic, retain) IBOutlet UILabel *balanceLabel; @end
FirstViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the FirstViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-25). Listing 1-25: Addition of @synthesize #import “FirstViewController.h”
@implementation FirstViewController @synthesize balanceLabel;
When the app launches, any transactions that have occurred are stored in the file Data.plist, which is now loaded; and the current balance is displayed (see Listing 1-26). Listing 1-26: The viewWillAppear method # pragma mark # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@”Data”];
38
❘ Chapter 1 Navigation
if(d != nil) { NSNumber *bal = [d objectForKey:@”balance”]; if(bal != nil) { [balanceLabel setText:[bal stringValue]]; } else { [balanceLabel setText:@”0”]; } } else { [balanceLabel setText:@”0”]; } [super viewWillAppear:animated]; }
Listing 1-27 shows the complete FirstViewController.m implementation. Listing 1-27: The complete FirstViewController.m file (Chapter1/Tabbar-iPhone/Classes/
FirstViewController.m)
#import “FirstViewController.h”
@implementation FirstViewController @synthesize balanceLabel; # pragma mark # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@”Data”]; if(d != nil) { NSNumber *bal = [d objectForKey:@”balance”]; if(bal != nil) { [balanceLabel setText:[bal stringValue]]; } else { [balanceLabel setText:@”0”]; } } else { [balanceLabel setText:@”0”]; } [super viewWillAppear:animated]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setBalanceLabel:nil]; } - (void)dealloc { [balanceLabel release]; [super dealloc]; } @end
A Simple Tab Bar
❘ 39
SecondViewController.h Modifications to the Template You declared the following outlets in Interface Builder: ➤➤
amountTextField
➤➤
transactionType
You must now define the properties for these variables in order to get and set their values (see Listing 1-28). The IBOutlet was moved to the property declaration. Three variables were also added: ➤➤
balance
➤➤
deposits
➤➤
withdrawals
Listing 1-28: The complete SecondViewController.h file (Chapter1/Tabbar-iPhone/Classes/
SecondViewController.h) #import
@interface SecondViewController : UIViewController { UITextField *amountTextField; UISegmentedControl *transactionType; NSNumber *balance; NSArray *deposits; NSArray *withdrawals; } @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain) retain)
IBOutlet UITextField *amountTextField; IBOutlet UISegmentedControl *transactionType; NSNumber *balance; NSArray *deposits; NSArray *withdrawals;
- (IBAction)saveTransaction:(id)sender; @end
SecondViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the SecondViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-29). Listing 1-29: Addition of @synthesize #import “SecondViewController.h” #import “PropertyList.h”
@implementation SecondViewController @synthesize amountTextField; @synthesize transactionType;
40
❘ Chapter 1 Navigation
@synthesize balance; @synthesize deposits; @synthesize withdrawals;
When the app launches, any transactions that have occurred are stored in the file Data.plist, which is now loaded; and the values for the balance, deposits, and withdrawals are retained (see Listing 1-30). Listing 1-30: The viewWillAppear method # pragma mark # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { [amountTextField setDelegate:self]; NSDictionary *d = [PropertyList readFromArchive:@”Data”]; if(d != nil) { NSDictionary *localItems = [d objectForKey:@”items”]; [self setBalance:[d objectForKey:@”balance”]]; [self setDeposits:[localItems objectForKey:@”deposits”]]; [self setWithdrawals:[localItems objectForKey:@”withdrawals”]]; } else { [self setBalance:[NSNumber numberWithDouble:0.0]]; [self setDeposits:[NSArray array]]; [self setWithdrawals:[NSArray array]]; } [super viewWillAppear:animated]; }
When the user taps the Save button, the value entered is either added or subtracted depending on which segmented button is selected (see Listing 1-31). Listing 1-31: The saveTransaction method - (IBAction)saveTransaction:(id)sender { NSMutableDictionary *aDict = [NSMutableDictionary dictionary]; NSMutableDictionary *itemDict = [NSMutableDictionary dictionary]; NSMutableArray *transaction = nil; NSNumberFormatter * formatter = [[NSNumberFormatter alloc] init]; [formatter setNumberStyle:NSNumberFormatterCurrencyStyle]; NSString *amt = [amountTextField text]; double localAmount = [amt doubleValue]; double localBalance = [[self balance] doubleValue]; [formatter release]; if ([transactionType selectedSegmentIndex] == 0) { localBalance += localAmount; transaction = [[self deposits] mutableCopy]; [transaction addObject:[NSNumber numberWithDouble:localAmount]]; [self setDeposits:transaction]; [self setBalance:[NSNumber numberWithDouble:localBalance]]; } else { localBalance -= localAmount; transaction = [[self withdrawals] mutableCopy]; [transaction addObject:[NSNumber numberWithDouble:localAmount]]; [self setWithdrawals:transaction]; [self setBalance:[NSNumber numberWithDouble:localBalance]];
A Simple Tab Bar
} [aDict setObject:[self balance] forKey:@”balance”]; [itemDict setObject:[self deposits] forKey:@”deposits”]; [itemDict setObject:[self withdrawals] forKey:@”withdrawals”]; [aDict setObject:itemDict forKey:@”items”]; if(![PropertyList writeToArchive:@”Data” fromDictionary:aDict]) { NSLog(@”Error writing data to pList.”); } }
When the user taps the Return button on the keyboard, the UITextFieldDelegate must implement the textFieldShouldReturn method so the keyboard will disappear (see Listing 1-32). Listing 1-32: The textFieldShouldReturn method #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { // the user pressed the “Done” button, so dismiss the keyboard [textField resignFirstResponder]; return YES; }
Listing 1-33 shows the complete SecondViewController.m implementation. Listing 1-33: The complete SecondViewController.m file (Chapter1/Tabbar-iPhone/Classes/
SecondViewController.m)
#import “FirstViewController.h”
@implementation FirstViewController @synthesize balanceLabel; # pragma mark # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@”Data”]; if(d != nil) { NSNumber *bal = [d objectForKey:@”balance”]; if(bal != nil) { [balanceLabel setText:[bal stringValue]]; } else { [balanceLabel setText:@”0”]; } } else { [balanceLabel setText:@”0”]; } [super viewWillAppear:animated]; } - (void)didReceiveMemoryWarning {
❘ 41
42
❘ Chapter 1 Navigation
[super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setBalanceLabel:nil]; } - (void)dealloc { [balanceLabel release]; [super dealloc]; } @end
ThirdViewController.h Modifications to the Template You declared the following outlet in Interface Builder: ➤➤
detailTableView
You must now define the properties for this variable in order to get and set its value (see Listing 1-34). The IBOutlet was moved to the property declaration. Three variables were also added: ➤➤
balance
➤➤
deposits
➤➤
withdrawals
Listing 1-34: The complete ThirdViewController.h file (Chapter1/Tabbar-iPhone/Classes/
ThirdViewController.h)
#import
@interface ThirdViewController : UITableViewController { UITableView *detailTableView; NSNumber *balance; NSArray *deposits; NSArray *withdrawals; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
IBOutlet UITableView *detailTableView; NSNumber *balance; NSArray *deposits; NSArray *withdrawals;
@end
ThirdViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the ThirdViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-35).
A Simple Tab Bar
❘ 43
Listing 1-35: Addition of @synthesize #import “ThirdViewController.h” #import “PropertyList.h”
@implementation ThirdViewController @synthesize @synthesize @synthesize @synthesize
detailTableView; balance; deposits; withdrawals;
When the app launches, any transactions that have occurred are stored in the file Data.plist, which is now loaded; and the values for the balance, deposits, and withdrawals are retained. With the stored values now retained, the table view reload data is called; this refreshes the table view and displays the stored data (see Listing 1-36). Listing 1-36: The viewWillAppear method - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@”Data”]; if(d != nil) { NSDictionary *localItems = [d objectForKey:@”items”]; [self setBalance:[d objectForKey:@”balance”]]; [self setDeposits:[localItems objectForKey:@”deposits”]]; [self setWithdrawals:[localItems objectForKey:@”withdrawals”]]; } else { [self setBalance:[NSNumber numberWithDouble:0.0]]; [self setDeposits:[NSArray array]]; [self setWithdrawals:[NSArray array]]; } [[self detailTableView] reloadData]; [super viewWillAppear:animated]; }
As shown in Listing 1-37, data that is used to populate a table view has to supply the table with two values: ➤➤
The number of sections into which the table view will be divided
➤➤
The number of rows in each section
Listing 1-37: The numberOfSectionsInTableView and tableView:numberOfRowsInSection methods #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; }
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. switch (section) {
44
❘ Chapter 1 Navigation
case 0: return [[self deposits] count]; break; case 1: return [[self withdrawals] count]; break; default: return 0; break; } }
There are two sections — the first displays the deposits and the second displays the withdrawals. Each section must have a section header that identifies the contents to the user (see Listing 1-38). Listing 1-38: The tableView:titleForHeaderInSection method // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { switch (section) { case 0: if([[self deposits] count] > 0) { return @”Deposits”; } else { return @”No Deposits”; } break; case 1: if([[self withdrawals] count] > 0) { return @”Withdrawals”; } else { return @”No Withdrawals”; } break; default: return @”“; break; } }
To display each detail row, the table view’s section and row are checked. The section identifies which array to use, deposits or withdrawals; and the row identifies the element in the selected array to retrieve. Because this table view is for display only, you ignore any taps in the table view cells. Listing 1-39 shows the tableView:cellForRowAtIndexPath method. Listing 1-39: The tableView:cellForRowAtIndexPath method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; NSString *cellText = @”“; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc]
A Simple Tab Bar
initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } // Configure the cell... switch ([indexPath section]) { case 0: cellText = [NSString stringWithFormat:@”Deposit #%d = %@”, ([indexPath row]+1), [[self deposits] objectAtIndex:[indexPath row]]]; break; case 1: cellText = [NSString stringWithFormat:@”Withdrawal #%d = %@”, ([indexPath row]+1), [[self withdrawals] objectAtIndex:[indexPath row]]]; break; default: break; } // Configure the cell. [[cell textLabel]setText: cellText]; return cell; }
Listing 1-40 shows the complete ThirdViewController.m implementation. Listing 1-40: The complete ThirdViewController.m file (Chapter1/Tabbar-iPhone/Classes/ ThirdViewController.m) #import “ThirdViewController.h” #import “PropertyList.h”
@implementation ThirdViewController @synthesize @synthesize @synthesize @synthesize
detailTableView; balance; deposits; withdrawals;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; } - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@”Data”]; if(d != nil) { NSDictionary *localItems = [d objectForKey:@”items”]; [self setBalance:[d objectForKey:@”balance”]]; [self setDeposits:[localItems objectForKey:@”deposits”]]; [self setWithdrawals:[localItems objectForKey:@”withdrawals”]]; } else { [self setBalance:[NSNumber numberWithDouble:0.0]]; [self setDeposits:[NSArray array]]; [self setWithdrawals:[NSArray array]];
❘ 45
46
❘ Chapter 1 Navigation
} [[self detailTableView] reloadData]; [super viewWillAppear:animated]; } #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. switch (section) { case 0: return [[self deposits] count]; break; case 1: return [[self withdrawals] count]; break; default: return 0; break; } } // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { switch (section) { case 0: if([[self deposits] count] > 0) { return @”Deposits”; } else { return @”No Deposits”; } break; case 1: if([[self withdrawals] count] > 0) { return @”Withdrawals”; } else { return @”No Withdrawals”; } break; default: return @”“; break; } } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; NSString *cellText = @”“; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
A Simple Tab Bar
if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } // Configure the cell... switch ([indexPath section]) { case 0: cellText = [NSString stringWithFormat:@”Deposit #%d = %@”, ([indexPath row]+1), [[self deposits] objectAtIndex:[indexPath row]]]; break; case 1: cellText = [NSString stringWithFormat:@”Withdrawal #%d = %@”, ([indexPath row]+1), [[self withdrawals] objectAtIndex:[indexPath row]]]; break; default: break; } // Configure the cell. [[cell textLabel]setText: cellText]; return cell; }
#pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { }
#pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated in // viewDidLoad or on demand. // For example: self.myOutlet = nil; [self setBalance:nil]; [self setDeposits:nil]; [self setWithdrawals:nil]; } - (void)dealloc { [detailTableView release]; [balance release]; [deposits release]; [withdrawals release]; [super dealloc]; } @end
❘ 47
48
❘ Chapter 1 Navigation
Transaction.h Modifications to the Template The Transaction class simply stores the balance, deposit, and withdrawal arrays in a NSDictionary, items. To be stored in a plist file, the NSCoding protocol must be implemented. The two values being stored are the balance and the transaction items. ➤➤
balance
➤➤
items
As shown in Listing 1-41, you must now define the properties for these variables in order to get and set their values. Listing 1-41: The complete Transaction.h file (Chapter1/Tabbar-iPhone/Classes/Transaction.h) #import
@interface Transaction : NSObject { NSNumber *balance; NSDictionary *items; } @property (nonatomic, retain) NSNumber *balance; @property (nonatomic, retain) NSDictionary *items; @end
Transaction.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the Transaction.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-42). Listing 1-42: Addition of @synthesize #import “Transaction.h”
@implementation Transaction @synthesize balance; @synthesize items;
The encodeWithCoder method defines the order in which the data will be stored, and the associated initWithCoder method decodes the values when they are retrieved from the file, and initializes the object with the stored values (see Listing 1-43). Listing 1-43: The encodeWithCoder and initWithCoder methods #pragma mark #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self balance] forKey:@”balance”]; [coder encodeObject:[self items] forKey:@”items”]; } - (id)initWithCoder:(NSCoder *)coder {
A Simple Tab Bar
❘ 49
if (self = [super init]) { [self setBalance:[coder decodeObjectForKey:@”balance”]]; [self setItems:[coder decodeObjectForKey:@”items”]]; } return self; }
Listing 1-44 shows the complete Transaction.m implementation. Listing 1-44: The complete Transaction.m file (Chapter1/Tabbar-iPhone/Classes/Transaction.m) #import “Transaction.h”
@implementation Transaction @synthesize balance; @synthesize items; #pragma mark #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self balance] forKey:@”balance”]; [coder encodeObject:[self items] forKey:@”items”]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setBalance:[coder decodeObjectForKey:@”balance”]]; [self setItems:[coder decodeObjectForKey:@”items”]]; } return self; }
@end
PropertyList.h Modifications to the Template The PropertyList class provides factory methods that will retrieve the Data.plist file as well as store the current values after each transaction. It has one variable to hold the data: ➤➤
pList
You must now define the properties for this variable in order to get and set its value (see Listing 1-45). It also supplies the following two factory methods for data processing: ➤➤
readFromArchive
➤➤
writeToArchive
Listing 1-45: The complete PropertyList.h file (Chapter1/Tabbar-iPhone/Classes/PropertyList.h) #import
@interface PropertyList : NSObject { NSDictionary *pList;
50
❘ Chapter 1 Navigation
} @property (nonatomic, retain) NSDictionary *pList; + (NSDictionary *)readFromArchive:(NSString *)aFileName; + (BOOL)writeToArchive:(NSString *)aFileName fromDictionary:(NSDictionary *)aDict; @end
PropertyList.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the PropertyList.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 1-46). Listing 1-46: Addition of @synthesize #import “PropertyList.h”
@implementation PropertyList @synthesize pList;
Listing 1-47 shows the complete PropertyList.m implementation. Listing 1-47: The complete PropertyList.m file (Chapter1/Tabbar-iPhone/Classes/PropertyList.m) #import “PropertyList.h”
@implementation PropertyList @synthesize pList; + (NSDictionary *)readFromArchive:(NSString *)aFileName { NSDictionary *result = nil; NSString *fname = [NSString stringWithFormat:@”%@.plist”, aFileName]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *data = [NSData dataWithContentsOfFile:bundlePath]; if(data != nil) { result = [NSKeyedUnarchiver unarchiveObjectWithData:data]; } return result; } + (BOOL)writeToArchive:(NSString *)aFileName fromDictionary:(NSDictionary *)aDict { NSString *fname = [NSString stringWithFormat:@”%@.plist”, aFileName]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
Summary
❘ 51
NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:aDict]; return [data writeToFile:bundlePath atomically:YES]; } - (void)dealloc { [pList release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the “A Simple Tab Bar” section.
Summary This chapter demonstrated three types of techniques used for navigating data on the iPhone. Navigation bars enable the use of simple taps either to traverse down a hierarchy of data to dig deeper or to return up. Toolbars allow several separate tasks to be performed on the same data within the current context, in this case the balance in the banking account. Tab bars work with common data and allow, through different views, manipulation of that data. The choice of which navigation view to use in your application depends on how your data needs to be presented and/or modified.
2
alerts, action sheets, and Modal Views What’s in this chaPter? ➤➤
The role, design, and usage principles for the alert pop-up
➤➤
The role, design, and usage principles for the action sheet
➤➤
The role, design, and usage principles for modal views to present functionality
Alerts, action sheets, and modal views are temporary views that require immediate user attention. No other application action can take place until they are dismissed. Alerts notify the user of some event and usually provide two buttons: OK, to acknowledge, and Cancel, to dismiss. Action sheets usually present the user with a list of options beyond OK and Cancel. Modal views can be any view presented in a modal fashion, which means no other application operation can proceed until the modal view is dismissed — for example, choosing a song to play from a list. After the user makes a selection, the view is dismissed and the song begins to play. To demonstrate the use of these three temporary views, this chapter provides five complete applications: one for the alert, two for the action sheet, and two for presenting views modally. The action sheet and modal view sections also provide an application for the iPhone as well as the iPad, because each device requires a slightly different design approach.
alerts There is no question about the role of the alert in your applications. You use an alert when you need to inform the user about something critical that prevents normal operation of the application. Two common uses of alerts are as follows: ➤➤
Indicate the current problem and present the user with options to resolve it.
➤➤
When a potential problem has been encountered, provide the user with the option to accept or reject the outcome.
54
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Alert View Design When using alerts, you have control over the number of buttons; however, the buttons cannot be customized. It is important to keep alerts simple and direct and offer only two choices. Apple’s User Interface Guidelines recommend you use action sheets if you need to offer more than two choices. Action sheets are covered in the next section of this chapter. Because you are typically going to be providing the user with only two options, carefully consider the default option. Apple’s User Interface Guidelines recommend making the lighter and right-most button the default option. The button on the left will always be the dark button, and the button on the right will never be dark. The following may be helpful: ➤➤
For a critical action, the button that cancels that action should be on the right, which is light colored.
➤➤
For a noncritical action, the button that cancels that action should be on the left, which is dark colored.
➤➤
Alerts with only one button will have a light-colored button.
One final consideration is about the wording of your message: The text that describes what caused the alert should not contain the word error, as that should be assumed.
The UIAlertViewDelegate Protocol The UIAlertViewDelegate protocol defines the methods a delegate of a UIAlertView object should implement. If you add your own buttons to an alert view, the delegate must implement the alertView:clicked ButtonAtIndex: message to respond when those buttons are clicked; otherwise, your custom buttons do nothing. The alert will also be dismissed after this method is invoked.
A Simple AlertView for the iPhone-iPod Touch and iPad In this application, you are going to simply load a file into a UITextView, depending on the choice the user makes when presented with the alert. Alerts behave the same for the iPhone, iPod touch, and iPad. This app will present users with two options, as shown in Figure 2-1: ➤➤
Found — Open a file that exists.
➤➤
Not Found — Open a file that does not exist.
If the user chooses the Found option, when they tap the Run button, they will be presented with an alert that asks if they would like to either continue with opening the file or cancel the action. If the user wants to continue opening the file, it is loaded into a UITextView as shown in Figure 2-2. If the user chooses the Not Found option, when they tap the Run button they are presented with an alert that notifies them that the file was not found and offers them only one option, to return to the application, as shown in Figure 2-3. The application will also include a Clear button to clear the UITextView.
Figure 2-1
A Simple AlertView for the iPhone-iPod Touch and iPad
Figure 2-2
❘ 55
Figure 2-3
Development Steps: Loading a File into a UITextView To create an alert view, execute the following steps:
1.
Start Xcode and create a View-based application, naming it Alert. If you need help with this step, please see Appendix A for the steps to begin a View-based application.
2.
Double-click the AlertViewController.xib file to launch Interface Builder (see Figure 2-4).
Figure 2-4
56
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
3.
4.
From the Interface Builder Library (Tools ➪ Library), choose the following and drag them to the View window, and enter the associated text: ➤➤
Label — Choose a File Open Option
➤➤
Segmented Control — Found for the left control, Not Found for the right control.
➤➤
TextView
➤➤
Two buttons — Run for the left button, Clear for the right button.
From the Interface Builder Inspector (Tools ➪ Inspector): ➤➤
Click on the View and set the background to white.
➤➤
Click on the TextView and uncheck the Editable option in the Attributes Inspector, which is right next to Text Color, as shown in Figure 2-5.
Figure 2-5
5.
In your view, make the following changes to the controls you added in step 3. Your interface should now look like Figure 2-6. ➤➤
Double-click the Label and type Choose a File Open Option.
➤➤
Double-click the Left Segmented Control and type Found.
A Simple AlertView for the iPhone-iPod Touch and iPad
6.
➤➤
Double-click the Right Segmented Control and type Not Found.
➤➤
Double-click the left Button and type Run.
➤➤
Double-click the right Button and type Clear.
Back in the Interface Builder Library, click Classes at the top and scroll to and select your AlertViewController class. At the bottom, now click the Outlets button. Click the + and add the following outlets, as shown in Figure 2-7: ➤➤
options (as a UISegmentedControl instead of
id type) ➤➤
7.
textView (as a UITextView instead of id type)
Click the Actions button. Then click the + and add the following actions, as shown in Figure 2-8: ➤➤
clearDisplay
➤➤
runChoice Figure 2-6
Figure 2-7
Figure 2-8
❘ 57
58
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
8.
From the main menu, choose File ➪ Write Class Files, and select Save and then Replace. You now have an Objective-C template that holds your program’s logic.
9.
Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the Segmented Control and TextView
➤➤
Connect the runChoice and clearDisplay actions to the File’s Owner
To make the connection to identify the Segmented Control and TextView, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 2-9. 10.
From the right of the File’s Owner Inspector, control-drag from the circle to the Segmented Control options outlet until it is highlighted, then release the mouse. You will see now that the circle is filled, indicating the connection has been made (see Figure 2-10). Do the same for the textView outlet, and
then you may dismiss the File’s Owner Inspector.
11.
From the Run button, control-drag to the File’s Owner icon, then release the mouse. The action runChoice is displayed in the inspector. Click runChoice to make the connection between the button and the action, as shown in Figure 2-11. Do the same for the Clear button to the clearDisplay action.
Figure 2-9
Figure 2-10
A Simple AlertView for the iPhone-iPod Touch and iPad
❘ 59
Figure 2-11
12.
Now that you are finished with the user interface, you have to create FoundFile.txt so there is a file to open. ➤➤
Choose File ➪ New File… from the Xcode menu.
➤➤
Choose Mac OS X ➪ Other ➪ Empty File.
➤➤
Enter FoundFile.txt for the filename.
➤➤
Enter some text in the Xcode Display view and save the file.
Now it is time to turn to the source code, and enter your logic demonstrating the alert.
Source Code Listings for the Alert View For this application you do not modify the AlertAppDelegate.h or AlertAppDelegate.m files, which will be used as generated.
60
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Remember that Interface Builder created template source code after you completed the design in step 8. Listing 2-1 shows the template for the AlertViewController.h interface file, and Listing 2-2 shows the template for the AlertViewController.m implementation file. Listing 2-1: The AlertViewController.h template file #import #import @interface AlertViewController : UIViewController { IBOutlet UISegmentedControl *options; IBOutlet UITextView *textView; } - (IBAction)clearDisplay:(id)sender; - (IBAction)runChoice:(id)sender; @end
Listing 2-2: The AlertViewController.m template file #import “AlertViewController.h” @implementation AlertViewController - (IBAction)clearDisplay:(id)sender { } - (IBAction)runChoice:(id)sender { } @end
Modifications to the AlertViewController.h Template In order to implement the AlertView, you have to include the proper protocol in your code. For the UIAlertVew, the UIAlertViewDelegate protocol defines the methods a delegate of a UIAlertView object should implement. The delegate implements the button actions and any other custom behavior. To implement this in the AlertViewController, make the following addition to the class definition in your AlertViewController.h file: @interface AlertViewController : UIViewController {
You declared two actions, runChoice and clearDisplay. You are going to create these methods, as well as a method to display the file that will be opened when found: ➤➤
openFile — Opens the file when Found is selected.
➤➤
showAlert — Actually create the alerts.
➤➤
displayFile — Displays the contents of the file in the textView.
- (void)openFile:(NSString *)filename alertTitle:(NSString *)title; - (void)showAlert:(NSString *)msg buttons:(NSArray *)buttons alertTitle:(NSString *)title; - (void)displayFile;
A Simple AlertView for the iPhone-iPod Touch and iPad
❘ 61
Because you are going to implement the UIAlertViewDelegate protocol, you must include the following method in your code to process the delegate message the alert view will send: - (void)alertView:(UIAlertView *)view clickedButtonAtIndex:(NSInteger)buttonIndex;
The Segmented Button has two options, with the values 0 and 1, so you need to define two constants that will indicate the tags: #define FOUND_OPTION 0 #define NOT_FOUND_OPTION 1
Finally, you create an instance variable, filePath, to hold the file path where the file that you need to open is located. In addition, you define the protocol for this variable in order to get and set its value: NSString *filePath; @property (nonatomic, retain) NSString *filePath;
Listing 2-3 is the complete modified AlertViewController.h file. Additions and modifications to the original template are in bold. Listing 2-3: The complete modified AlertViewController.h file (Chapter2/Alert/Classes/ AlertViewController.h) #define FOUND_OPTION 0 #define NOT_FOUND_OPTION 1 #import #import @interface AlertViewController : UIViewController { IBOutlet UISegmentedControl *options; IBOutlet UITextView *textView; NSString *filePath; } @property (nonatomic, retain) NSString *filePath; - (IBAction)runChoice:(id)sender; - (IBAction)clearDisplay:(id)sender; - (void)openFile:(NSString *)filename alertTitle:(NSString *)title; - (void)showAlert:(NSString *)msg buttons:(NSArray *)buttons alertTitle:(NSString *)title; - (void)displayFile; - (void)alertView:(UIAlertView *)view clickedButtonAtIndex:(NSInteger)buttonIndex; @end
Modifications to the AlertViewController.m Template Now that the header file has been updated to define the additions to the project to implement our logic, the program logic to process the alerts has to be written.
62
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
When you define the filePath variable and declare the protocol in the AlertViewController.h, it must be matched with @synthesize: @synthesize filePath;
To implement the UIAlertViewDelegate protocol, you must add the alertView:clicksButtonAtIndex method. When the file that the user wants to load has been found, the alert offers two options: ➤➤
The Cancel option does not open the file; it assigns 0 for the tag value.
➤➤
The OK option proceeds with the file opening and display in the textView, and assigns 1 for the tag value.
The code just looks for the tag of 1 for the Cancel button or 0 for the OK button. Listing 2-4 shows how this logic is implemented. Listing 2-4: The alertView:clicksButtonAtIndex method #pragma mark #pragma mark - UIAlertViewDelegate - (void)alertView:(UIAlertView *)view clickedButtonAtIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 1: [self displayFile]; break; default: break; } }
Before displaying the file in the textView, there is a check to see if there are any contents to display. If there aren’t any, an alert will be displayed. If there are contents, they are displayed in the textView (see Listing 2-5). Listing 2-5: The displayFile method - (void)displayFile { NSString *contents = [NSString stringWithContentsOfFile:[self filePath] encoding:NSUTF8StringEncoding error:nil]; if(contents == nil) { NSString *title = @”Display File”; NSString *msg = [NSString stringWithFormat:@”File: %@ was not found !”, [self filePath]]; NSArray *buttons = [NSArray arrayWithObjects:@”OK”, nil]; UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:msg delegate:self cancelButtonTitle:[buttons objectAtIndex:0] otherButtonTitles:nil]; [alert show]; [alert release]; } else { [textView setText:contents]; } }
A Simple AlertView for the iPhone-iPod Touch and iPad
❘ 63
When the user clicks the Run button, the runChoice method is invoked. After checking which Segmented Button is highlighted, the proper alert message is prepared and invokes the openFile:alertTitle: method. The clearDisplay method that clears the textView must also be written, as shown in Listing 2-6. Listing 2-6: The runChoice and clearDisplay action methods #pragma mark #pragma mark - IB Action methods - (IBAction)runChoice:(id)sender { NSString *title = [options titleForSegmentAtIndex:[options selectedSegmentIndex]]; switch ([options selectedSegmentIndex]) { case FOUND_OPTION: [self openFile:@”FoundFile” alertTitle:title]; break; case NOT_FOUND_OPTION: [self openFile:@”NotFoundFile” alertTitle:title]; break; default: NSLog(@”unknown option”); break; } } - (IBAction)clearDisplay:(id)sender { [textView setText:@”“]; }
The openFile:alertTitle: method attempts to open the file from the filename that is passed. If the file is not found, an alert with only one option, OK, is presented to the user. If the file is found, the user can choose to display the file or not. This option will use two buttons, Cancel and Open (see Listing 2-7). Listing 2-7: The openFile:alertTitle: method #pragma mark #pragma mark - Local Action methods - (void)openFile:(NSString *)filename alertTitle:(NSString *)title { NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:@”txt”]; NSString *msg = nil; NSArray *buttons = nil; if(path != nil) { buttons = [NSArray arrayWithObjects:@”Cancel”, @”Open”, nil]; msg = @”Continue Loading File ?”; } else { buttons = [NSArray arrayWithObjects:@”OK”, nil]; msg = @”Click OK to return to app.”; } [self setFilePath:path]; [self showAlert:msg buttons:buttons alertTitle:title]; }
The showAlert:buttons:alertTitle: method will be called with the appropriate message, number of buttons, and title (see Listing 2-8).
64
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Listing 2-8: The showAlert:buttons:alertTitle: method - (void)showAlert:(NSString *)msg buttons:(NSArray *)buttons alertTitle:(NSString *)title { UIAlertView *alert = nil; switch ([buttons count]) { case 2: alert = [[UIAlertView alloc] initWithTitle:title message:msg delegate:self cancelButtonTitle:[buttons objectAtIndex:0] otherButtonTitles:[buttons objectAtIndex:1], nil]; break; default: alert = [[UIAlertView alloc] initWithTitle:title message:msg delegate:self cancelButtonTitle:[buttons objectAtIndex:0] otherButtonTitles:nil]; break; } [alert show]; [alert release]; }
Listing 2-9 is the modified AlertViewController.m file. Listing 2-9: The complete modified AlertViewController.m file (Chapter2/Alert/Classes/
AlertViewController.m)
#import “AlertViewController.h” @implementation AlertViewController @synthesize filePath; #pragma mark #pragma mark - IB Action methods - (IBAction)runChoice:(id)sender { NSString *title = [options titleForSegmentAtIndex:[options selectedSegmentIndex]]; switch ([options selectedSegmentIndex]) { case FOUND_OPTION: [self openFile:@”FoundFile” alertTitle:title]; break; case NOT_FOUND_OPTION: [self openFile:@”NotFoundFile” alertTitle:title]; break; default: NSLog(@”unknown option”); break; } }
A Simple AlertView for the iPhone-iPod Touch and iPad
- (IBAction)clearDisplay:(id)sender { [textView setText:@”“]; } #pragma mark #pragma mark - Local Action methods - (void)openFile:(NSString *)filename alertTitle:(NSString *)title { NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:@”txt”]; NSString *msg = nil; NSArray *buttons = nil; if(path != nil) { buttons = [NSArray arrayWithObjects:@”Cancel”, @”Open”, nil]; msg = @”Continue Loading File ?”; } else { buttons = [NSArray arrayWithObjects:@”OK”, nil]; msg = @”Click OK to return to app.”; } [self setFilePath:path]; [self showAlert:msg buttons:buttons alertTitle:title]; } - (void)showAlert:(NSString *)msg buttons:(NSArray *)buttons alertTitle:(NSString *)title { UIAlertView *alert = nil; switch ([buttons count]) { case 2: alert = [[UIAlertView alloc] initWithTitle:title message:msg delegate:self cancelButtonTitle:[buttons objectAtIndex:0] otherButtonTitles:[buttons objectAtIndex:1], nil]; break; default: alert = [[UIAlertView alloc] initWithTitle:title message:msg delegate:self cancelButtonTitle:[buttons objectAtIndex:0] otherButtonTitles:nil]; break; } [alert show]; [alert release]; } - (void)displayFile { NSString *contents = [NSString stringWithContentsOfFile:[self filePath] encoding:NSUTF8StringEncoding error:nil]; if(contents == nil) { NSString *title = @”File Error”; NSString *msg = [NSString stringWithFormat:@”File: %@ was not found !”, [self filePath]]; NSArray *buttons = [NSArray arrayWithObjects:@”OK”, nil];
❘ 65
66
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:msg delegate:self cancelButtonTitle:[buttons objectAtIndex:0] otherButtonTitles:nil]; [alert show]; [alert release]; } else { [textView setText:contents]; } } #pragma mark #pragma mark - UIAlertViewDelegate - (void)alertView:(UIAlertView *)view clickedButtonAtIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 1: [self displayFile]; break; default: break; } } #pragma mark #pragma mark - memory dealloc method - (void)dealloc { [filePath release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build and try out your app. It should give you results that were described at the beginning of the "A Simple AlertView for the iPhone, iPod Touch and iPad" section.
Action Sheets An action sheet appears when a user taps a button in an application’s toolbar. It provides a list of options associated with a current task. An action sheet differs from an alert in that there is no urgent problem to resolve. It is also quite common to provide a Cancel button that allows the user to not take any action at all. Actions sheets are implemented differently on an iPhone versus an iPad: ➤➤
iPhone — They appear on the bottom of the screen.
➤➤
iPad — They appear within a popover pointing to the related toolbar button.
Action Sheet Design Action sheets should provide the user with more than one option — normally, at least three options.
An Action Sheet for the iPhone-iPod Touch
❘ 67
You should not have to add a message to an action sheet because the button labels, in conjunction with the task being performed, should provide enough information for users to understand their options. When users tap a button, the action sheet disappears. Because an action sheet should provide users with a choice of actions, an action sheet always provides more than one button. When you are designing your action sheets, remember to consider the device you are developing for, the iPhone or iPad.
iPhone, iPod Touch On these devices, an action sheet always emerges from the bottom of the application screen. It hovers over its views and is attached to the side of the screen.
iPad In an iPad application, an action sheet is displayed within a popover so it never occupies the entire width of the screen. It does not need a Cancel button because the popover is dismissed when the user touches outside of it.
The UIActionSheetDelegate Protocol The UIActionSheetDelegate protocol defines the methods that a delegate of a UIActionSheet object should implement. If you add your own buttons to an action sheet, the delegate must implement the actionSheet:clicked ButtonAtIndex: method to respond when those buttons are tapped; otherwise, your custom buttons do nothing. The action sheet is automatically dismissed after the actionSheet:clickedButtonAtIndex: delegate method is invoked.
An Action Sheet for the iPhone-iPod Touch In this application, a user chooses from a list of aces; and when the selected card is displayed, an action sheet provides two options that determine which side of the playing card will be displayed, front or back. This app presents the user with a list of four cards: Ace of Spades, Ace of Hearts, Ace of Clubs and Ace of Diamonds, as shown in Figure 2-12. From the list of the four aces, the user will select one, the ace of spades, for example, and the following sequence of events is shown in Figure 2-13: ➤➤
The card face is displayed.
➤➤
The user taps the Flip button in the upper right corner, and is presented with the option to display the front of the card, back of the card or Cancel the flip.
➤➤
The user chooses to display the back of the card. The back of the card is displayed and the action sheet is dismissed. Figure 2-12
68
❘
chaPter 2 alerts, actioN sheets, aNd Modal views
fiGUre 2-13
Choosing the Card List navigation button will return the user to the original card list.
development steps: creating an action sheet for the iPhone or iPod touch To create an action sheet for the iPhone and iPod touch, execute the following steps:
1 .
Start Xcode and create a Navigation-based application named ActionSheets-iPhone. If you need to see this step, please see Appendix A for the steps to begin a Navigation-based application.
2 .
Add the card images to the project and name the images as follows: spades_a.png, hearts_a.png, clubs_a.png, diamonds_a.png, and back.png. In the project folder, create the folder Images and drag the images into it (see Figure 2-14).
Use card images that are not copyrighted. One source is http://freeware .esoterica.free.fr/html/freecards.html.
fiGUre 2-14
An Action Sheet for the iPhone-iPod Touch
3.
❘ 69
With the images in the Images folder, they must now be imported into the project. To import the images into the Xcode project: ➤➤
From the Xcode menu, choose Project ➪ New Group and enter Images for the group name.
➤➤
Highlight and control-click the Images group and choose Add ➪ Existing Files, as shown in Figure 2-15.
Figure 2-15 ➤➤
Navigate to the newly created Images folder, select all the files, and click Add (see Figure 2-16).
➤➤
On the next dialog, make sure the “Copy items into destination group’s folder” is not checked and choose Add (see Figure 2-17).
Figure 2-16
Figure 2-17
70
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
4.
The initial view in a Navigation-based application is the RootViewController. The RootViewController presents the list of cards in a table view. The user taps one of the table view cells to select a specific card. After the card selection the table view navigates to the DetailViewController that displays the card image. This view will also animate the flipping of the card from front to back.
5.
From Xcode, highlight the Classes group folder and choose File ➪ New File. Choose Cocoa Touch Class ➪ UIViewController subclass and check the UITableViewController subclass as shown in Figure 2-18.
Figure 2-18
At this point, all the classes needed to build this application have been created and are ready for coding, starting with the RootViewController.
Source Code Listings for the Action Sheet For this application you do not modify the ActionSheets_iPhoneAppDelegate.h or ActionSheets_ iPhoneAppDelegate.m files, which will be used as generated.
Modifications to the RootViewController.h Template This application uses two arrays, one that holds the description of the cards and another that holds the names of our .png card image files. In addition, a protocol is defined for these variables in order to get and set their value: NSArray *cardNames; NSArray *cardImages; @property (nonatomic, retain) NSArray *cardNames; @property (nonatomic, retain) NSArray *cardImages;
Listing 2-10 shows the template code for the RootViewController interface class, with modifications in bold.
An Action Sheet for the iPhone-iPod Touch
❘ 71
Listing 2-10: The RootViewController.h template code with modifications file (Chapter2/
ActionSheets-iPhone/Classes/RootViewController.h) #import
@interface RootViewController : UITableViewController { NSArray *cardNames; NSArray *cardImages; } @property (nonatomic, retain) NSArray *cardNames; @property (nonatomic, retain) NSArray *cardImages; @end
Modifications to the RootViewController.m Template When cardNames and cardImages are defined and the protocols declared, they must be matched with @synthesize: @synthesize cardNames; @synthesize cardImages;
When the app launches, the RootViewController.m file’s viewDidLoad method is called. This is where the cardNames and cardImages arrays are initialized and the view’s title is set to Card List. Listing 2-11 shows the viewDidLoad method, with modifications in bold. Listing 2-11: The RootViewController.m viewDidLoad method #import “RootViewController.h” #import “DetailViewController.h”
@implementation RootViewController @synthesize cardNames; @synthesize cardImages; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Card List”]; [self setCardNames:[NSArray arrayWithObjects:@”Ace of Spades”, @”Ace of Hearts”,@”Ace of Clubs”,@”Ace of Diamonds”, nil]]; [self setCardImages:[NSArray arrayWithObjects:@”spades_a.png”, @”hearts_a.png”,@”clubs_a.png”,@”diamonds_a.png”, nil]]; }
There is only one section containing the list of aces. The view controller needs to know how many rows are needed to display this list. This list count is identified through the method tableView:numberOfRowsIn Section, which returns the size of the array that contains the aces, [[self cardNames] count]. Listing 2-12 shows the addition, with modifications in bold.
72
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Listing 2-12: The RootViewController.m tableView:numberOfRowsInSection method #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[self cardNames] count]; }
When the user taps on a card in the list, the card image will be displayed. Listing 2-13 shows the addition, with modifications in bold. Listing 2-13: The RootViewController.m tableView:cellForRowAtIndexPath: method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [[cell textLabel] setText:[NSString stringWithFormat:@”%@”, [[self cardNames] objectAtIndex:row]]]; return cell; }
When the user makes a card selection, the tableView:didSelectRowAtIndexPath: delegate method is invoked. An instance of the DetailViewController is created and the card image name is passed to the controller, letting it know which image to load into the display. Listing 2-14 shows the addition, with modifications in bold. Listing 2-14: The RootViewController.m tableView:didSelectRowAtIndexPath: method #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = indexPath.row; DetailViewController *detailViewController = [[DetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [detailViewController setImageName:[[self cardImages] objectAtIndex:row]];
An Action Sheet for the iPhone-iPod Touch
❘ 73
[[self navigationController] pushViewController:detailViewController animated:YES]; [detailViewController release]; }
To avoid memory leaks, retained memory must be released through the viewDidUnload and dealloc methods. Listing 2-15 shows the variables being released and relinquished, with modifications in bold. Listing 2-15: The RootViewController.m viewDidUnload and dealloc methods - (void)viewDidUnload { [self setCardNames:nil]; [self setCardImages:nil]; } - (void)dealloc { [cardNames release]; [cardImages release]; [super dealloc]; }
Listing 2-16 is the complete modified RootViewController.m file. Listing 2-16: The complete modified RootViewController.m file (Chapter2/ActionSheets-
iPhone/Classes/RootViewController.m) #import “RootViewController.h” #import “DetailViewController.h” @implementation RootViewController @synthesize cardNames; @synthesize cardImages; #pragma mark #pragma mark View lifecycle
- (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Card List”]; [self setCardNames:[NSArray arrayWithObjects:@”Ace of Spades”, @”Ace of Hearts”,@”Ace of Clubs”,@”Ace of Diamonds”, nil]]; [self setCardImages:[NSArray arrayWithObjects: @”spades_a.png”,@”hearts_a.png”, @”clubs_a.png”, @”diamonds_a.png”, nil]]; } #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView
74
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
numberOfRowsInSection:(NSInteger)section { return [[self cardNames] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = indexPath.row; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [[cell textLabel] setText:[NSString stringWithFormat:@”%@”, [[self cardNames] objectAtIndex:row]]]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = indexPath.row; DetailViewController *detailViewController = [[DetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [detailViewController setImageName:[[self cardImages] objectAtIndex:row]]; [[self navigationController] pushViewController:detailViewController animated:YES]; [detailViewController release]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setCardNames:nil]; [self setCardImages:nil]; } - (void)dealloc { [cardNames release]; [cardImages release]; [super dealloc]; } @end
An Action Sheet for the iPhone-iPod Touch
❘ 75
Modifications to the DetailViewController.h Template The RootViewController class now displays the initial list to the user. With each cell having the accessoryType of UITableViewCellAccessoryDisclosureIndicator, when a cell is selected, there is a call to DetailViewController. The first thing to do is to add the UIActionSheetDelegate protocol to the class: @interface DetailViewController : UITableViewController
When one of the action sheet buttons is tapped, the method actionSheet:clickedButtonAtIndex: is called. The index number that is passed identifies the button tapped. In the DetailViewController.h file, declare imageView and imageName variables for the filename of the image, as well as the UIImageView that the image is displayed in: UIImageView *imageView; NSString *imageName;
Include the three following methods: ➤➤
createImageView — Creates the UIImageView that displays the card image
➤➤
addBarButtonItem — Creates the Flip button on the navigation bar that when tapped, brings up the
➤➤
flipTheImage — Called by the action as a result of the Flip button being tapped
action sheet - (void)createImageView; - (void)addBarButtonItem; - (void)flipTheImage;
Listing 2-17 is the complete DetailViewController.h interface file. Listing 2-17: The DetailViewController.h interface file (Chapter2/ActionSheets-iPhone/
Classes/DetailViewController.h) #import
@interface DetailViewController : UITableViewController { UIImageView *imageView; NSString *imageName; } @property (nonatomic, retain) UIImageView *imageView; @property (nonatomic, retain) NSString *imageName; #pragma mark #pragma mark Initialization - (void)createImageView; - (void)addBarButtonItem; #pragma mark #pragma mark Action methods - (void)flipTheImage; @end
76
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
With the DetailViewController.h file defined, it is time to write the code that will provide the logic for the action sheets and display the correct image upon selection. The size of the card image must be defined before it is displayed. #define kImageHeight 215.0 #define kImageWidth 150.0 #define kTopPlacement 80.0 // y coord for the images
When imageName and imageView are defined and the protocols declared, they must be matched with @ synthesize: @synthesize imageName; @synthesize imageView;
When the DetailViewController is called from the RootViewController, the viewDidLoad method is called to initialize the view. Listing 2-18 shows the initialization of the DetailViewController. Listing 2-18: The viewDidLoad method #pragma mark #pragma mark Initialization - (void)viewDidLoad { [super viewDidLoad]; [self createImageView]; [self addBarButtonItem]; }
In the createImageView method, the view that displays the card image is created. In the addBarButtonItem method, the button that is in the navigation bar that creates the action sheet is created. When tapped, this button will flip the card from front to back (see Listing 2-19). Listing 2-19: The createImageView and addBarButtonItem methods - (void)createImageView { CGRect frame = CGRectMake(round((self.view.bounds.size.width - kImageWidth) / 2.0), kTopPlacement, kImageWidth, kImageHeight); [self setImageView:[[[UIImageView alloc] initWithFrame:frame] autorelease]]; [[self view] addSubview:[self imageView]]; [[self imageView] setImage:[UIImage imageNamed:[self imageName]]]; } - (void)addBarButtonItem { UIBarButtonItem *addButton = [[[UIBarButtonItem alloc] initWithTitle:@”Flip” style:UIBarButtonItemStyleBordered target:self action:@selector(flipTheImage)] autorelease]; self.navigationItem.rightBarButtonItem = addButton; }
When the Flip button is tapped, the action sheet is created in the flipTheImage method. The action sheet presents three options to the user: ➤➤
Show the back of the card
➤➤
Show the front of the card
➤➤
Cancel
An Action Sheet for the iPhone-iPod Touch
❘ 77
When a button in the action sheet is tapped, the actionSheet:clickedButtonAtIndex: delegate method is called and the proper image is displayed (see Listing 2-20). Listing 2-20: The flipTheImage and actionSheet:clickedButtonAtIndex: methods #pragma mark #pragma mark Action methods - (void)flipTheImage { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@”Cancel” destructiveButtonTitle:nil otherButtonTitles:@”Show Card Back”, @”Show Card Front”, nil]; [actionSheet showInView:self.view]; [actionSheet release]; } #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: [imageView setImage:[UIImage imageNamed:@”back.png”]]; break; case 1: [imageView setImage:[UIImage imageNamed:[self imageName]]]; break; default: break; } }
Listing 2-21 is the complete DetailViewController.m file. Listing 2-21: The DetailViewController.m implementation file (Chapter2/ActionSheets-iPhone/
Classes/DetailViewController.m)
#import “DetailViewController.h” #define kImageHeight 215.0 #define kImageWidth 150.0 #define kTopPlacement 80.0 // y coord for the images @implementation DetailViewController @synthesize imageName; @synthesize imageView; #pragma mark #pragma mark Initialization - (void)viewDidLoad { [super viewDidLoad]; [self createImageView];
78
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
[self addBarButtonItem]; } - (void)createImageView { CGRect frame = CGRectMake(round((self.view.bounds.size.width – kImageWidth) / 2.0),kTopPlacement, kImageWidth, kImageHeight); [self setImageView:[[[UIImageView alloc] initWithFrame:frame] autorelease]]; [[self view] addSubview:[self imageView]]; [[self imageView] setImage:[UIImage imageNamed:[self imageName]]]; } - (void)addBarButtonItem { UIBarButtonItem *addButton = [[[UIBarButtonItem alloc] initWithTitle:@”Flip” style:UIBarButtonItemStyleBordered target:self action:@selector(flipTheImage)] autorelease]; self.navigationItem.rightBarButtonItem = addButton; } #pragma mark #pragma mark Action methods - (void)flipTheImage { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@”Cancel” destructiveButtonTitle:nil otherButtonTitles:@”Show Card Back”, @”Show Card Front”, nil]; [actionSheet showInView:self.view]; [actionSheet release]; } #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { switch (buttonIndex) { case 0: [imageView setImage:[UIImage imageNamed:@”back.png”]]; break; case 1: [imageView setImage:[UIImage imageNamed:[self imageName]]]; break; default: break; } } #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections.
An Action Sheet for the iPhone-iPod Touch
return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return 0; }
// Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setImageView:nil]; [self setImageName:nil]; } - (void)dealloc { [imageView release]; [imageName release]; [super dealloc]; } @end
❘ 79
80
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you results that were described at the beginning of this topic.
An Action Sheet for the iPad In this application, a user chooses from a list of aces; and when the selected card is displayed, an action sheet provides two options that determine which side of the playing card will be displayed, front or back. The iPad’s UISplitView, which generates the RootViewController as well as the DetailViewController, will be used. While the functionality of this application is exactly the same as the iPhone version, Apple’s suggested UI guidelines recommend that the action sheets be presented within popovers. This app presents the user with a list of four cards: Ace of Spades, Ace of Hearts, Ace of Clubs, and Ace of Diamonds. From the list of the four aces, the user will select one, the ace of spades, for example, and the following sequence of events is shown in Figure 2-19: ➤➤
The card face is displayed.
➤➤
The user taps the Flip button in the upper-right corner and is presented with the option to display the front of the card or the back of the card.
➤➤
The user chooses to display the back of the card and the back of the card is displayed and the action sheet is dismissed.
If this application is in portrait mode, the card list will be in a popover on the left of the display.
Figure 2-19
An Action Sheet for the iPad
❘ 81
Development Steps: Creating an Action Sheet for the iPad To create an action sheet for the iPad, execute the following steps:
1.
Start Xcode and create a SplitView-based application and name it ActionSheets-iPad. If you need to see this step, please see Appendix A for the steps to begin a SplitView-based application.
2.
Add the card images to the project. Use card images that are not copyrighted and name the images as follows: spades_a.png, hearts_a.png, clubs_a.png, diamonds_a.png, and back.png. In the project folder, create the folder Images and drag the images into it (see Figure 2-20).
Figure 2-20
3.
4.
5.
With the images in the Images folder, they must now be imported into the project. To import the images into the Xcode project: ➤➤
From the Xcode menu, choose Project ➪ New Group and enter Images for the group name.
➤➤
Highlight and control-click the Images group and choose Add ➪ Existing Files, as shown in Figure 2-21.
➤➤
Navigate to the newly created Images folder, select all the files, and click Add (see Figure 2-22).
➤➤
On the next dialog, make sure the “Copy items into destination group’s folder” is not checked and choose Add (see Figure 2-23).
Now you have to modify the appearance of the DetailViewController. The view needs two more additions: ➤➤
A Flip button in the toolbar that will have the back of card and front of card popover attached to it
➤➤
The imageView that will be used to display the card image
In the Xcode project, click to open the icon at the bottom that is labeled Nib Files. Double-click on the DetailView.xib file. This will bring up Interface Builder, as shown in Figure 2-24.
82
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Figure 2-21
Figure 2-22
6.
Figure 2-23
From the Interface Builder Library (Tools ➪ Library), choose the following from the Objects option for your view: ➤➤
Fixed Space Bar Button Item — Drag this to the left of the top toolbar and stretch it out three quarters of the way to the right.
➤➤
Bar Button Item — Drag this to the right of the Fixed Space Bar Button Item you just placed on the toolbar. Double-click the button and type Flip.
➤➤
ImageView — First delete the text that is currently on the view by selecting it and pressing Delete. Now drag the ImageView and set the dimensions as X:246, Y:300, W:275, H:404. Click the two autosizing bars that are solid so they are dotted like the others. This enables the view to adjust correctly when you flip the iPad. Your view should look like Figure 2-25.
An Action Sheet for the iPad
Figure 2-24
Figure 2-25
❘ 83
84
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
7.
The outlets and actions will be connected in Interface Builder after the source code for the DetailView Controller has been completed. With SplitView-based applications, Xcode also generates the DetailViewController, unlike the iPhone’s Navigation-based application.
Source Code Listings for the Action Sheet For this application you do not modify the ActionSheets_iPadAppDelegate.h or ActionSheets_ iPadAppDelegate.m files, which will be used as generated.
Modifications to the RootViewController.h Template This application uses two arrays, one that holds the description of the cards and another that holds the names of our .png card image files. In addition, a protocol is defined for these variables in order to get and set their value: NSArray *cardNames; NSArray *cardImages; @property (nonatomic, retain) NSArray *cardNames; @property (nonatomic, retain) NSArray *cardImages;
Listing 2-22 shows the template code for the RootViewController interface class, with modifications in bold. Listing 2-22: The RootViewController.h template code with modifications (Chapter2/
ActionSheets-iPad/Classes/RootViewController.h) #import @class DetailViewController;
@interface RootViewController : UITableViewController { DetailViewController *detailViewController; NSArray *cardNames; NSArray *cardImages; } @property (nonatomic, retain) IBOutlet DetailViewController *detailViewController; @property (nonatomic, retain) NSArray *cardNames; @property (nonatomic, retain) NSArray *cardImages; @end
Modifications to the RootViewController.m Template When cardNames and cardImages are defined and the protocols declared, they must be matched with @synthesize: @synthesize cardNames; @synthesize cardImages;
When the app launches, the RootViewController.m file’s viewDidLoad method is called. This is where the cardNames and cardImages arrays are initialized and the view’s title is set to Card List.
An Action Sheet for the iPad
❘ 85
Listing 2-23 shows the viewDidLoad method, with modifications in bold. Listing 2-23: The RootViewController.m viewDidLoad method #import “RootViewController.h” #import “DetailViewController.h” @implementation RootViewController @synthesize detailViewController; @synthesize cardNames; @synthesize cardImages; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.clearsSelectionOnViewWillAppear = NO; self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0); [self setTitle:@”Card List”]; [self setCardNames:[NSArray arrayWithObjects:@”Ace of Spades”, @”Ace of Hearts”,@”Ace of Clubs”,@”Ace of Diamonds”, nil]]; [self setCardImages:[NSArray arrayWithObjects:@”spades_a.png”, @”hearts_a.png”,@”clubs_a.png”,@”diamonds_a.png”, nil]]; [detailViewController loadImageFromName:[[self cardImages] objectAtIndex:0]]; }
There is only one section containing the list of aces. The view controller needs to know how many rows are needed to display this list. This list count is identified through the method tableView:numberOfRowsIn Section, that returns the size of the array that contains the aces, [[self cardNames] count]. Listing 2-24 shows the addition, with modifications in bold. Listing 2-24: The RootViewController.m tableView:numberOfRowsInSection method #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [[self cardNames] count]; }
With the SplitView, the list of cards are displayed in a popover if the iPad’s orientation is portrait, and on the left if the orientation is landscape, so there is no need to set the cell accessory type. The cell’s accessoryType will be set to UITableViewCellAccessoryNone. The card name is displayed in the list, and the selected card will be identified using indexPath.row. Listing 2-25 shows the addition, with modifications in bold.
86
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Listing 2-25: The RootViewController.m tableView:cellForRowAtIndexPath: method - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”CellIdentifier”; NSInteger row = indexPath.row; // Dequeue or create a cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:[NSString stringWithFormat:@”%@”, [[self cardNames] objectAtIndex:row]]]; return cell; }
When the user makes a card selection, the tableView:didSelectRowAtIndexPath: method is invoked. This informs the DetailViewController to load the proper card image into the display. Listing 2-26 shows the addition, with modifications in bold. Listing 2-26: The RootViewController.m tableView:didSelectRowAtIndexPath: method #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = indexPath.row; [detailViewController loadImageFromName:[[self cardImages] objectAtIndex:row]]; }
To avoid memory leaks, retained memory must be released through the viewDidUnload and dealloc methods. Listing 2-27 shows the variables being released and relinquished, with modifications in bold. Listing 2-27: The RootViewController.m viewDidUnload and dealloc method - (void)viewDidUnload { [self setCardNames:nil]; [self setCardImages:nil]; } - (void)dealloc { [cardNames release]; [cardImages release]; [super dealloc]; }
An Action Sheet for the iPad
❘ 87
Listing 2-28 is the complete modified RootViewController.m file. Listing 2-28: The complete modified RootViewController.m file (Chapter2/ActionSheets-iPad/
Classes/RootViewController.m)
#import “RootViewController.h” #import “DetailViewController.h” @implementation RootViewController @synthesize detailViewController; @synthesize cardNames; @synthesize cardImages; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.clearsSelectionOnViewWillAppear = NO; self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0); [self setTitle:@”Card List”]; [self setCardNames:[NSArray arrayWithObjects:@”Ace of Spades”, @”Ace of Hearts”,@”Ace of Clubs”,@”Ace of Diamonds”, nil]]; [self setCardImages:[NSArray arrayWithObjects:@”spades_a.png”, @”hearts_a.png”,@”clubs_a.png”,@”diamonds_a.png”, nil]]; [detailViewController loadImageFromName:[[self cardImages] objectAtIndex:0]]; } // Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [[self cardNames] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”CellIdentifier”;
88
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
NSInteger row = indexPath.row; // Dequeue or create a cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:[NSString stringWithFormat:@”%@”, [[self cardNames] objectAtIndex:row]]]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = indexPath.row; [detailViewController loadImageFromName:[[self cardImages] objectAtIndex:row]]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setCardNames:nil]; [self setCardImages:nil]; } - (void)dealloc { [detailViewController release]; [cardNames release]; [cardImages release]; [super dealloc]; } @end
Modifications to the DetailViewController.h Template The RootViewController class now displays the card list to the user. Depending on the iPad’s orientation, the card list will be in either a popover or a tableview. The first thing to do is to add the UIActionSheetDelegate protocol to the class. When one of the action sheet buttons is clicked, the method actionSheet:clickedButtonAtIndex: is called. The index number that is passed identifies the button clicked.
An Action Sheet for the iPad
❘ 89
@interface DetailViewController : UIViewController
In the DetailViewController.h file, the variables and protocols for the filename of the image, as well as the UIImageView in which the image will be displayed, are defined: UIImageView *imageView; UIBarButtonItem *flipBarButtonItem; NSString *imageName; @property (nonatomic, retain) IBOutlet UIImageView *imageView; @property (nonatomic, retain) IBOutlet UIBarButtonItem *flipBarButtonItem; @property (nonatomic, retain) NSString *imageName;
The following methods are defined: ➤➤
loadImageFromName — This will load the .png file, given the name of the file.
➤➤
flipTheImage — This will be called by the action as a result of the Flip button being tapped. - (void)loadImageFromName:(NSString *)name; - (IBAction)flipTheImage;
Listing 2-29 is the complete DetailViewController.h interface file that will be used. Listing 2-29: The DetailViewController.h interface file (Chapter2/ActionSheets-iPad/Classes/
DetailViewController.h)
#import @interface DetailViewController : UIViewController { UIPopoverController *popoverController; UIToolbar *toolbar; UIImageView *imageView; UIBarButtonItem *flipBarButtonItem; NSString *imageName; } @property (nonatomic, retain) IBOutlet UIToolbar *toolbar; @property (nonatomic, retain) IBOutlet UIImageView *imageView; @property (nonatomic, retain) IBOutlet UIBarButtonItem *flipBarButtonItem; @property (nonatomic, retain) NSString *imageName; - (void)loadImageFromName:(NSString *)name; - (IBAction)flipTheImage; @end
Note the IBOutlet and IBAction identifiers in the header file. These identifiers allow dedicated outlets and actions to be connected in Interface Builder.
90
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
With the DetailViewController.h file defined, it is time to write the code that will provide the logic for the action sheets as well as the displaying of the correct image upon selection. When you define the imageName and imageView variables and declare the protocols, they must be matched with @synthesize: @synthesize imageName; @synthesize flipBarButtonItem; @synthesize imageView;
With the SplitView, an additional method is declared: splitViewController:willHideViewController: aViewController:withBarButtonItem:forPopoverController:. This method is where the title of the Bar Button Item that holds our card List is set. The viewDidLoad method is modified so an image will be loaded when the application launches. Listing 2-30 shows how to initialize the view controller. Listing 2-30: The view initialization methods #pragma mark #pragma mark Split view support - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = @”Card List”; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; } #pragma mark #pragma mark View lifecycle // Implement viewDidLoad to do additional setup after // loading the view, typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; [self loadImageFromName:[self imageName]]; }
The loadImageFromName method loads the image into the imageView (see Listing 2-31). Listing 2-31: The loadImageFromName method #pragma mark #pragma mark Managing the image - (void)loadImageFromName:(NSString *)name { [self setImageName:name]; // Update the view. [imageView setImage:[UIImage imageNamed:[self imageName]]]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } }
An Action Sheet for the iPad
❘ 91
When the Flip button is tapped, the action sheet is created in the flipTheImage method, and the user has two choices: ➤➤
Show the back of the card
➤➤
Show the front of the card
A Cancel button is not included in this case, because if the user clicks outside the popover, it is automatically dismissed. When a button in the action sheet is tapped, the actionSheet:clickedButtonAtIndex: delegate method is called and the proper image is displayed (see Listing 2-32). Listing 2-32: The flipTheImage and actionSheet:clickedButtonAtIndex: methods #pragma mark #pragma mark IB Action methods - (IBAction)flipTheImage { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:@”Show Card Back”, @”Show Card Front”, nil]; [actionSheet showFromBarButtonItem:[self flipBarButtonItem] animated:YES]; [actionSheet release]; } #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == 0) { [imageView setImage:[UIImage imageNamed:@”back.png”]]; } else { [imageView setImage:[UIImage imageNamed:[self imageName]]]; } }
Listing 2-33 is the complete DetailViewController.m file. Listing 2-33: The DetailViewController.m implementation file (Chapter2/ActionSheets-iPad/
Classes/DetailViewController.h)
#import “DetailViewController.h” #import “RootViewController.h”
@interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; @end @implementation DetailViewController @synthesize toolbar, popoverController; @synthesize imageView; @synthesize flipBarButtonItem;
92
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
@synthesize imageName; #pragma mark #pragma mark Split view support - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController:(UIPopoverController*)pc { barButtonItem.title = @”Card List”; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; } // Called when the view is shown again in the split view, invalidating the // button and popover controller. - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = nil; } #pragma mark #pragma mark Rotation support // Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark View lifecycle // Implement viewDidLoad to do additional setup after // loading the view, typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; [self loadImageFromName:[self imageName]]; } - (void)viewDidUnload { [self setPopoverController:nil]; [self setImageName:nil]; [self setImageView:nil]; } #pragma mark #pragma mark Managing the image - (void)loadImageFromName:(NSString *)name {
An Action Sheet for the iPad
❘ 93
[self setImageName:name]; // Update the view. [imageView setImage:[UIImage imageNamed:[self imageName]]]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } } #pragma mark #pragma mark IB Action methods - (IBAction)flipTheImage { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:@”Show Card Back”, @”Show Card Front”, nil]; [actionSheet showFromBarButtonItem:[self flipBarButtonItem] animated:YES]; [actionSheet release]; } #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == 0) { [imageView setImage:[UIImage imageNamed:@”back.png”]]; } else { [imageView setImage:[UIImage imageNamed:[self imageName]]]; } } - (void)dealloc { [popoverController release]; [toolbar release]; [flipBarButtonItem release]; [imageName release]; [imageView release]; [super dealloc]; } @end
Final Steps: Making the Connections To connect the outlets and actions, double-click the DetailView.xib in the project to bring up the view.
1.
Control-drag from the File’s Owner icon to the UIImageView, release the mouse, and click imageView (see Figure 2-26).
2. 3.
Repeat this for the Flip button and connect the flipBarButtonItem.
Control-drag from the Flip button to the File’s Owner icon, release the mouse, and click the flipTheImage action (see Figure 2-27).
94
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Figure 2-26
Figure 2-27
A Modal View for the iPhone and iPod Touch
❘ 95
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results that were described at the beginning of this topic.
Modal Views Modal views are not actually a view like the alert and the action sheet, but rather a way of presenting a view modally. They do have the same purpose, however, which is to interrupt normal application flow to get input from the user in order to continue.
Presenting and Dismissing the Modal View To present a view controller modally, you must do the following:
1. 2. 3. 4.
Create the view controller you want to present. Set the modalTransitionStyle property of the view controller to the desired value. For the iPad, you may also set the modalPresentationStyle property. Assign a delegate object where appropriate. (For our applications in this section, the UIActionSheetDelegate will be used.)
5.
Call the presentModalViewController:animated: method of the current view controller, passing in the view controller you want to present modally.
6.
When it comes time to dismiss a modal view controller, let the same view controller that presented the modal view controller dismiss it using the method dismissModalViewControllerAnimated:.
Transition Styles The transition style you should use depends on how you plan to use the presented view controller. There are three transition styles to choose from: ➤➤
UIModalTransitionStyleCoverVertical
➤➤
UIModalTransitionStyleFlipHorizontal
➤➤
UIModalTransitionStyleCrossDissolve
Modal Presentation Styles (iPad only) The iPad introduces new options for presenting view controllers modally. With the iPhone, the modally presented views always cover the visible portion of the underlying window. With the iPad, the UIViewController class has a modalPresentationStyle property that determines the appearance of the view controller when it is presented modally. There are four presentation styles to choose from: ➤➤
UIModalPresentationFullScreen
➤➤
UIModalPresentationPageSheet
➤➤
UIModalPresentationFormSheet
➤➤
UIModalPresentationCurrentContext
A Modal View for the iPhone and iPod Touch In this application, the user chooses from a list to indicate how a modal view will be presented or dismissed. The options will be selected from an action sheet list.
96
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
The following transition styles will be demonstrated: ➤➤
UIModalTransitionStyleCoverVertical
➤➤
UIModalTransitionStyleFlipHorizontal
➤➤
UIModalTransitionStyleCrossDissolve
When the app begins, it presents a simple view with a navigation bar titled Modal Views and a BarButtonItem labeled Show (see Figure 2-28). When the user taps Show, the transition style list will appear in an action sheet, as shown in Figure 2-29. When the user taps a transition style, the modal view appears using that transition. To close the window, the user simply taps the Close button. The window disappears using that same transition style (see Figure 2-30).
Figure 2-28
Figure 2-29
Figure 2-30
Development Steps: Creating a Modal View for the iPhone and iPod Touch To create a modal view for the iPhone and iPod touch, execute the following steps:
1.
Start Xcode and create a View-based application named ModalView-iPhone. If you need to see this step, please see Appendix A for the steps to begin a View-based application. Make sure that you choose iPhone for the product.
2.
Double-click ModalView_iPhoneViewController.xib from the Nib Files section of Xcode. This launches Interface Builder. From the Library Inspector, choose and place on your view window the following: ➤➤
3.
Navigation Bar — Place on the top of the view and label it Modal Views.
➤➤
BarButtonItem — Place on the right corner of the navigation bar and label it Show.
➤➤
View — Place in the remaining space below the navigation bar (see Figure 2-31).
In the Library Inspector, click the Classes button and select the ClassModalView_iPhoneView Controller class. At the bottom, now click on the Outlets button. Click the + and add the following outlet, as shown in Figure 2-32: ➤➤
showBarButtonItem as a UIBarButtonItem instead of id type
A Modal View for the iPhone and iPod Touch
4.
❘ 97
Click the Actions button, and then click the + and add the following action, as shown in Figure 2-33: ➤➤
Figure 2-31
showModalView
Figure 2-32
Figure 2-33
5.
From the main menu, choose File ➪ Write Class Files, and select Save and then Replace. You now will have an Objective-C template that will hold the program’s logic.
6.
Before actually programming, all the connections in Interface Builder have to be made to:
7.
➤➤
Identify the showBarButtonItem button
➤➤
Connect the showModalView action to the File’s Owner icon
To make the connection that identifies the showBar ButtonItem, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 2-34.
8.
From the File’s Owner Inspector’s circle on the right, control-drag to the Show button until it is highlighted. Release the mouse and choose showBarButtonItem. The circle is now filled, indicating the connection has been made, as shown in Figure 2-35. Dismiss the File’s Owner Inspector.
Figure 2-34
98
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Figure 2-35
9.
From the Show button, control-drag to the File’s Owner icon and release the mouse. The action showModalView is displayed in the inspector. Click showModalView, as shown in Figure 2-36, and the connection from the Show button to the showModalView action is made. Close the interface in Interface Builder.
10.
11.
Now it’s time for the creation of the modal view. From Xcode, choose File ➪ New File, choose UIViewController subclass, name it NewModalViewController, and select the following option: ➤➤ With XIB for user interface
In the NIB Files section, double-click the NewModalViewController.xib, launching Interface Builder. In the view, do the following: ➤➤
12.
13.
Click on the view and choose Tools ➪ Attributes Inspector to change the background to something other than white.
From the Interface Builder Library (Tools ➪ Library), choose the following and drag them to the view (see Figure 2-37): ➤➤
A Label
➤➤
A Button (and set the title to Close)
From the Library, choose the Classes tab and select NewModalViewController. Click the Outlets button, and then click the + to add the following outlet: ➤➤
label as a UILabel instead of id type
A Modal View for the iPhone and iPod Touch
Figure 2-36
14.
Click the Actions button. Click the + and add the following action: ➤➤
closeModalView
15.
From the main menu, choose File ➪ Write Class Files, and select Save and then Replace. You now will have an Objective-C template that will hold your program’s logic.
16.
Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to:
17.
18.
➤➤
Identify the label label
➤➤
Connect the closeModalView action to the File’s Owner icon
From the File’s Owner Inspector’s circle on the right, control-drag to the label in the view until it is highlighted, then release the mouse and click label. You will see now that the circle is filled, indicating the connection has been made. From the Show button, control-drag to the File’s Owner icon and release the mouse and click closeModalView. Close the Interface Builder dialog.
Figure 2-37
❘ 99
100
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
At this point, it is time to turn to the source code and enter the logic demonstrating the modal views.
Source Code Listings for the iPhone Modal View For this application you do not modify the ModalView_iPhoneAppDelegate.h or ModalView_ iPhoneAppDelegate.m files, which will be used as generated.
Modifications to the ModalView_iPhoneViewController.h Template showModalView is the only action method that is used: - (IBAction)showModalView:(id)sender;
Listing 2-34 shows the code for the ModalView_iPhoneViewController interface class, with modifications in bold. Listing 2-34: The ModalView_iPhoneViewController.h template code with modifications
(Chapter2/ModalView-iPhone/Classes/ModalView_iPhoneViewController.h) #import @interface ModalView_iPhoneViewController : UIViewController { } - (IBAction)showModalView:(id)sender; @end
ModalView_iPhoneViewController.m Modifications to the Template Because rotation will be supported, the following must be included in our code: -(BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; }
When the user taps the Show button, the showModalView method is called, creating and displaying an action sheet that enables the user to choose the transition type. Listing 2-35 shows the showModalView method. Listing 2-35: The showModalView method #pragma mark #pragma mark IB Action methods - (IBAction)showModalView:(id)sender { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil
A Modal View for the iPhone and iPod Touch
❘ 101
otherButtonTitles:@”CoverVertical”, @”FlipHorizontal”, @”CrossDissolve”,nil]; [actionSheet showInView:self.view]; [actionSheet release]; }
When the user chooses the desired transition, the selection is sent to the actionSheet delegate method. The user’s choice is identified from the buttonIndex passed. Listing 2-36 shows how to isolate the correct action from the selection made. Listing 2-36: The actionSheet delegate method #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { NewModalViewController *modalView = [[NewModalViewController alloc] initWithNibName:@”NewModalViewController” bundle:nil]; switch (buttonIndex) { case 0: [modalView setModalTransitionStyle: UIModalTransitionStyleCoverVertical]; [modalView setLabelText: @”UIModalTransitionStyleCoverVertical”]; break; case 1: [modalView setModalTransitionStyle: UIModalTransitionStyleFlipHorizontal]; [modalView setLabelText: @”UIModalTransitionStyleFlipHorizontal”]; break; case 2: [modalView setModalTransitionStyle: UIModalTransitionStyleCrossDissolve]; [modalView setLabelText: @”UIModalTransitionStyleCrossDissolve”]; break; default: [modalView setModalTransitionStyle: UIModalTransitionStyleCoverVertical]; [modalView setLabelText: @”UIModalTransitionStyleCoverVertical”]; break; } [self presentModalViewController:modalView animated:YES]; [modalView release]; }
Listing 2-37 shows the complete ModalView_iPhoneViewController implementation.
102
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Listing 2-37: The ModalView_iPhoneViewController implementation file (Chapter2/
ModalView-iPhone/Classes/ModalView_iPhoneViewController.m) #import “ModalView_iPhoneViewController.h” #import “NewModalViewController.h” @implementation ModalView_iPhoneViewController
// Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark IB Action methods - (IBAction)showModalView:(id)sender { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:@”CoverVertical”, @”FlipHorizontal”, @”CrossDissolve”,nil]; [actionSheet showInView:self.view]; [actionSheet release]; } #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { NewModalViewController *modalView = [[NewModalViewController alloc] initWithNibName:@”NewModalViewController” bundle:nil]; switch (buttonIndex) { case 0: [modalView setModalTransitionStyle: UIModalTransitionStyleCoverVertical]; [modalView setLabelText:@”UIModalTransitionStyleCoverVertical”]; break; case 1: [modalView setModalTransitionStyle: UIModalTransitionStyleFlipHorizontal]; [modalView setLabelText:@”UIModalTransitionStyleFlipHorizontal”]; break; case 2: [modalView setModalTransitionStyle: UIModalTransitionStyleCrossDissolve]; [modalView setLabelText:@”UIModalTransitionStyleCrossDissolve”];
A Modal View for the iPhone and iPod Touch
❘ 103
break; default: [modalView setModalTransitionStyle: UIModalTransitionStyleCoverVertical]; [modalView setLabelText:@”UIModalTransitionStyleCoverVertical”]; break; } [self presentModalViewController:modalView animated:YES]; [modalView release]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } @end
Modifications to the NewModalViewController.h Template An NSString holds the selection’s title value so it will be displayed in our modal view’s label. In addition, a protocol is defined for this variable in order to get and set its value: NSString *labelText; @property (nonatomic, retain) NSString *labelText;
Listing 2-38 shows the template code for the NewModalViewController interface class, with modifications in bold. Listing 2-38: The NewModalViewController.h template code with modifications file
(Chapter2/ModalView-iPhone/Classes/NewModalViewController.h) #import @interface NewModalViewController : UIViewController { NSString *labelText; IBOutlet UILabel *label; } @property (nonatomic, retain) NSString *labelText; - (IBAction)closeModalView:(id)sender; @end
104
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Modifications to the NewModalViewController.m Template When labelText was defined and the protocols were declared in the NewModalViewController.h file, it must be matched with @synthesize: @synthesize labelText;
When the app launches, the viewDidLoad method of the NewModalViewController is called. The label is initialized with the value that was passed by the ModalView_iPhoneViewController indicating which transition was selected. Listing 2-39 shows the viewDidLoad method and the support for the orientation, with modifications in bold. Listing 2-39: The viewDidLoad method #pragma mark #pragma mark Initialization - (void)viewDidLoad { [super viewDidLoad]; [label setText:[self labelText]]; } // Ensure that the view controller supports rotation and that the split // view can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; }
When the user taps the Close button, the closeModalView method is called and the modal view is dismissed. Listing 2-40 shows the closeModalView method and the releasing of memory methods, with modifications in bold. Listing 2-40: The closeModalView method #pragma mark #pragma mark IBAction action methods - (IBAction)closeModalView:(id)sender { [self dismissModalViewControllerAnimated:YES]; } #pragma mark #pragma mark Memory management - (void)viewDidUnload { [self setLabelText:nil]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { [labelText release]; [super dealloc]; }
A Modal View for the iPhone and iPod Touch
❘ 105
Listing 2-41 shows the NewModalViewController implementation. Listing 2-41: The NewModalViewController implementation file
(Chapter2/ModalView-iPhone/Classes/NewModalViewController.m) #import “NewModalViewController.h”
@implementation NewModalViewController @synthesize labelText; #pragma mark #pragma mark Initialization - (void)viewDidLoad { [super viewDidLoad]; [label setText:[self labelText]]; } // Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark IBAction action methods - (IBAction)closeModalView:(id)sender { [self dismissModalViewControllerAnimated:YES]; } #pragma mark #pragma mark Memory management - (void)viewDidUnload { [self setLabelText:nil]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning];. } - (void)dealloc { [labelText release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the "A Modal View for the iPhone and iPod Touch" section.
106
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
A Modal View for the iPad In this application, the user will choose from a list to indicate how a modal view will be presented or dismissed. The options will be selected from an action sheet list. This app will present the following modal presentation styles from within the action sheet: ➤➤
UIModalPresentationFullScreen
➤➤
UIModalPresentationPageSheet
➤➤
UIModalPresentationFormSheet
➤➤
UIModalPresentationCurrentContext
When the app begins, it is a simple view with a navigation bar titled Modal Views and a BarButtonItem labeled Show (see Figure 2-38).
Figure 2-38
When the user taps Show, the modal presentation style list appears in an action sheet, as shown in Figure 2-39. When the user selects a modal presentation style, the modal view will appear using that transition. To close the window, the user simply taps the Close button and the window will disappear using the same modal presentation style (see Figure 2-40).
A Modal View for the iPad
Figure 2-39
Figure 2-40
❘ 107
108
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
Development Steps: Creating a Modal View for the iPad To create a modal view for the iPad, execute the following steps:
1.
Start Xcode and create a View-based application named ModalView-iPad. If you need to see this step, please see Appendix A for the steps to begin a View-based application. Make sure that you choose iPad for the product.
2.
Double-click ModalView_iPhoneViewController.xib from the Nib Files section of Xcode. This launches Interface Builder. From the Library Inspector, choose and place on your view window the following: ➤➤
Navigation Bar — Place on the top of the view and label it Modal Views.
➤➤
BarButtonItem — Place on the right corner of the navigation bar and label it Show.
➤➤
View — Place in the remaining space below the navigation bar (see Figure 2-41).
Figure 2-41
A Modal View for the iPad
3.
In the Library Inspector, click the Classes button and select the ModalView_iPadViewController class. At the bottom, click the Outlets button. Click the + and add the following outlet, as shown in Figure 2-42: ➤➤
4.
❘ 109
showBarButtonItem as a UIBarButtonItem instead of id type
Click the Actions button, and then click the + and add the following action, as shown in Figure 2-43: ➤➤
showModalView
Figure 2-42
Figure 2-43
5.
From the main menu, choose File ➪ Write Class Files, and select Save and then Replace. You now have an Objective-C template that holds your program’s logic.
6.
Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the showBarButtonItem button
➤➤
Connect the showModalView action to the File’s Owner
110
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
7.
To make the connection to identify the showBarButtonItem, control-click on the File’s Owner icon to bring up the Inspector (see Figure 2-44).
8.
From the File’s Owner Inspector’s circle on the right, control-drag to the Show button until it is highlighted, then release the mouse and choose showBarButtonItem. The circle is filled, indicating the connection has been made (see Figure 2-45). Dismiss the File’s Owner Inspector.
Figure 2-44
Figure 2-45
9.
10.
From the Show button, control-drag to the File’s Owner icon, then release the mouse. An inspector with showModalView is displayed. Click showModalView, as shown in Figure 2-46, to make the connection from the Show button to the showModalView action. Close the Interface Builder dialog.
Now for the creation of the modal view. From Xcode, choose File ➪ New File, choose UIViewController subclass, name it NewModalViewController, and select the following options: ➤➤ With XIB for user interface ➤➤
11.
Targeted for iPad
In the NIB Files section, double-click the NewModalViewController.xib file, launching Interface Builder. In the view, do the following: ➤➤
Click on the view and choose Tools ➪ Attributes Inspector to change the background to something other than white.
A Modal View for the iPad
12.
❘ 111
From the Interface Builder Library (Tools ➪ Library), choose the following and drag them to the View window (see Figure 2-47): ➤➤
A Label — For autosizing, make sure only the top bar is solid.
➤➤
A Button — Set the title to Close, and for autosizing, make sure none of the bars are solid.
Figure 2-46
13.
From the Library, choose the Classes tab and select NewModalViewController. Click the Outlets button, and then click the + to add the following outlet: ➤➤
14.
label as a UILabel instead of id type
Click the Actions button. Click the + and add the following action: ➤➤
closeModalView
15.
From the main menu, choose File ➪ Write Class Files, and select Save and then Replace. You now will have an Objective-C template that will hold your program’s logic.
16.
Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to:
17.
➤➤
Identify the label label
➤➤
Connect the closeModalView action to the File’s Owner icon
From the File’s Owner Inspector’s circle on the right, control-drag to the label in the view until it is highlighted, then release the mouse and click label. You will see now that the circle is filled, indicating the connection has been made.
112
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
18.
From the Show button, control-drag to the File’s Owner icon, and release the mouse and click closeModalView. Close the Interface Builder dialog.
Figure 2-47
At this point, it is time to turn to the source code and enter the logic demonstrating the modal views.
Source Code Listings for the iPad Modal View For this application you do not modify the ModalView_iPadAppDelegate.h or ModalView_ iPadAppDelegate.m files, which will be used as generated.
A Modal View for the iPad
❘ 113
Modifications to the ModalView_iPadViewController.h Template Define the showModalView action method, as well as the showBarButtonItem that will attach the popover to it when displaying the action sheet: UIBarButtonItem *showBarButtonItem; @property (nonatomic, retain) IBOutlet UIBarButtonItem *showBarButtonItem; - (IBAction)showModalView:(id)sender;
Listing 2-42 shows the code for the ModalView_iPadViewControllerinterface class, with modifications in bold. Listing 2-42: The ModalView_iPadViewController.h template code with modifications file
(Chapter2/ModalView-iPad/Classes/ModalView_iPadViewController.h) #import #import
@interface ModalView_iPadViewController : UIViewController { UIBarButtonItem *showBarButtonItem; } @property (nonatomic, retain) IBOutlet UIBarButtonItem *showBarButtonItem; - (IBAction)showModalView:(id)sender; @end
Modifications to the ModalView_iPadViewController.m Template When the showBarButtonItem variable is defined and the protocol is declared, it must be matched with @synthesize: @synthesize showBarButtonItem;
Rotation is supported, so the following is included in the code: - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; }
When the user taps the Show button, the showModalView method is called, which creates and displays an action sheet, enabling the user to choose the transition type. Listing 2-43 shows the showModalView method. Listing 2-43: The showModalView method #pragma mark #pragma mark IB Action methods - (IBAction)showModalView:(id)sender { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil
114
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:@”FullScreen”, @”PageSheet”, @”FormSheet”, @”CurrentContext”,nil]; [actionSheet showFromBarButtonItem:[self showBarButtonItem] animated:YES]; [actionSheet release]; }
When the user chooses the transition they want, the selection is sent to the actionSheet delegate method. The user’s choice is identified from the buttonIndex passed. Listing 2-44 shows how to isolate the correct action from the selection made. Listing 2-44: The actionSheet delegate method #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { NewModalViewController *modalView = [[NewModalViewController alloc] initWithNibName:@”NewModalViewController” bundle:nil]; switch (buttonIndex) { case 0: [modalView setModalPresentationStyle: UIModalPresentationFullScreen]; [modalView setLabelText:@”UIModalPresentationFullScreen”]; break; case 1: [modalView setModalPresentationStyle: UIModalPresentationPageSheet]; [modalView setLabelText:@”UIModalPresentationPageSheet”]; break; case 2: [modalView setModalPresentationStyle: UIModalPresentationFormSheet]; [modalView setLabelText:@”UIModalPresentationFormSheet”]; break; case 3: [modalView setModalPresentationStyle: UIModalPresentationCurrentContext]; [modalView setLabelText: @”UIModalPresentationCurrentContext”]; break; default: [modalView setModalPresentationStyle: UIModalPresentationPageSheet]; [modalView setLabelText:@”UIModalPresentationPageSheet”]; break; } [self presentModalViewController:modalView animated:YES]; [modalView release]; }
A Modal View for the iPad
❘ 115
Listing 2-45 shows the ModalView_iPadViewController implementation. Listing 2-45: The ModalView_iPadViewController implementation file (Chapter2/ModalView-
iPad/Classes/ModalView_iPadViewController.m) #import “ModalView_iPadViewController.h” #import “NewModalViewController.h” @implementation ModalView_iPadViewController @synthesize showBarButtonItem;
// Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark IB Action methods - (IBAction)showModalView:(id)sender { UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:@”FullScreen”, @”PageSheet”, @”FormSheet”, @”CurrentContext”,nil]; [actionSheet showFromBarButtonItem:[self showBarButtonItem] animated:YES]; [actionSheet release]; } #pragma mark #pragma mark - UIActionSheetDelegate - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex { NewModalViewController *modalView = [[NewModalViewController alloc] initWithNibName:@”NewModalViewController” bundle:nil]; switch (buttonIndex) { case 0: [modalView setModalPresentationStyle: UIModalPresentationFullScreen]; [modalView setLabelText:@”UIModalPresentationFullScreen”]; break; case 1: [modalView setModalPresentationStyle:UIModalPresentationPageSheet]; [modalView setLabelText:@”UIModalPresentationPageSheet”]; break; case 2: [modalView setModalPresentationStyle:UIModalPresentationFormSheet]; [modalView setLabelText:@”UIModalPresentationFormSheet”];
116
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
break; case 3: [modalView setModalPresentationStyle: UIModalPresentationCurrentContext]; [modalView setLabelText:@”UIModalPresentationCurrentContext”]; break; default: [modalView setModalPresentationStyle:UIModalPresentationPageSheet]; [modalView setLabelText:@”UIModalPresentationPageSheet”]; break; } [self presentModalViewController:modalView animated:YES]; [modalView release]; } #pragma mark #pragma mark Memory management - (void)viewDidUnload { \ [self setShowBarButtonItem:nil]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { [showBarButtonItem release]; [super dealloc]; } @end
Modifications to the NewModalViewController.h Template An NSString will hold the selection’s title value, which is displayed in the modal view’s label. In addition, a property is defined for this variable so the values can be get and set: NSString *labelText; @property (nonatomic, retain) NSString *labelText;
Listing 2-46 shows the template code for the NewModalViewController interface class, with modifications in bold. Listing 2-46: The NewModalViewController.h template code with modifications file (Chapter2/
ModalView-iPad/Classes/NewModalViewController.h) #import #import
@interface NewModalViewController : UIViewController { NSString *labelText; IBOutlet UILabel *label; }
A Modal View for the iPad
❘ 117
@property (nonatomic, retain) NSString *labelText; - (IBAction)closeModalView:(id)sender; @end
Modifications to the NewModalViewController.m Template When labelText is defined and the protocols declared, it must be matched with @synthesize: @synthesize labelText;
When the app launches, the viewDidLoad method of the NewModalViewController is called. The label is initialized with the value that was passed by the ModalView_iPadViewController, indicating which transition was selected. Listing 2-47 shows the viewDidLoad method and the support for the orientation, with modifications in bold. Listing 2-47: The viewDidLoad method #pragma mark #pragma mark Initialization - (void)viewDidLoad { [super viewDidLoad]; [label setText:[self labelText]]; } // Ensure that the view controller supports rotation and that the split // view can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; }
When the user taps the Close button, the closeModalView method is called and the modal view is dismissed. Listing 2-48 shows the closeModalView method and the releasing of memory methods, with modifications in bold. Listing 2-48: The closeModalView method #pragma mark #pragma mark IBAction action methods - (IBAction)closeModalView:(id)sender { [self dismissModalViewControllerAnimated:YES]; } #pragma mark #pragma mark Memory management - (void)viewDidUnload { [self setLabelText:nil]; } - (void)didReceiveMemoryWarning {
118
❘ Chapter 2 Alerts, Action Sheets, and Modal Views
[super didReceiveMemoryWarning]; } - (void)dealloc { [labelText release]; [super dealloc]; }
Listing 2-49 shows the NewModalViewController implementation. Listing 2-49: The NewModalViewController implementation file (Chapter2/ModalView-iPad/ Classes/NewModalViewController.m) #import “NewModalViewController.h” @implementation NewModalViewController
Download from Wow! eBook <www.wowebook.com>
@synthesize labelText; #pragma mark #pragma mark Initialization - (void)viewDidLoad { [super viewDidLoad]; [label setText:[self labelText]]; } // Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark IBAction action methods - (IBAction)closeModalView:(id)sender { [self dismissModalViewControllerAnimated:YES]; } #pragma mark #pragma mark Memory management - (void)viewDidUnload { [self setLabelText:nil]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { [labelText release]; [super dealloc]; } @end
Summary
❘ 119
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the "A Modal View for the iPad" section.
Summary This chapter described the alert, the action sheet, and the modal view. You learned that while the alert is similarly developed for the iPhone and iPad, action sheets and modal views are implemented differently according to the device on which they will run. The five applications you created in this chapter provided an under-the-hood look at how alerts, action sheets, and modal views are created and used, including special considerations with respect to the device on which they run. This chapter also provided you with more experience processing delegate messages — specifically, the alert and the action sheet.
3
Custom Table Views What’s in this chaPter? ➤➤
How to design a custom Table view cell in Interface Builder
➤➤
How to incorporate your custom Table view cell into the standard Table view
➤➤
How to load a property list file into a custom Table view
One of the biggest challenges with mobile devices is presenting data logically, efficiently, and clearly. The most common form of display on mobile devices is the Table view, which is a single-column list of many rows of data. iOS 4 offers a standard Table view that can be plain, indexed, or grouped. However, sometimes you want the cells to have a different look than those offered by Apple. This chapter shows you how to customize the Table view cell, to give your Table view application its own look and feel.
taBle vieWs Simply put, Table views display information in the form of a list. Determining how data is displayed in a Table view depends on two methods: ➤➤ ➤➤
numberOfSectionsInTableView — Determines the number of grouped sections that exists. For a plain list, there is only one section. tableView:numberOfRowsInSection: — Informs the Table view how many rows are con-
tained in each section
Putting information into each Table view cell is the responsibility of the tableView:cellForRowAt IndexPath: method, where the indexPath contains the section, [indexPath section], and row values, [indexPath row]. When the user chooses a particular Table view cell, the tableView:didSelectRowAtIndexPath: method handles the selection. If your application supports removing elements from a Table view, you would provide the tableView:commitEditingStyle: method. Remember that you also have to remove the data
122
❘ Chapter 3 Custom Table Views
element from your storage element; removing the data from the Table view doesn’t automatically remove it from an array, for example. To provide headers for your sections, you supply the tableView:titleForHeaderInSection: method. To provide footers for your sections, you supply the tableView:titleForFooterInSection: method.
The Table View Cell The Table view consists of individual Table view cells, which contain the information that is displayed and is what you see in the view. To design your own Table view cell, Interface Builder provides an instance of UITableViewCell. You drag UI elements, labels, image views, and so on from the library as you do for regular views. However, keep in mind that you are working with a very limited amount of space. Don’t put too many items on the cell.
The UITableViewDataSource Protocol Classes that adopt this protocol provide the Table view with the data that is displayed in the Table view cell. They are responsible for informing the Table view how many rows and sections there are, as well as handling the insertion and deletion of cells.
The UITableViewDelegate Protocol Classes that adopt this protocol provide the Table view with the methods that manage the Table view cell display within the view, as well as the configuration of section headers and footers. They are also responsible for handling the action event when the user taps the Table view cell. The following section takes you through the steps of creating a Table view application and introduces the steps to create and include your custom Table view cell in this application.
A Custom Table View Application When this application launches, the main view contains a list of contacts displayed in a Table view, with a custom look and feel for the Table view cells, as shown in Figure 3-1. The development effort has two main tasks. The first task is to provide for the gathering of contact information. For this application, the contact list is stored in a property list that is created using the property list editor. The second task is to design a custom Table view cell that enables the display of contact information in one cell. Keep in mind that if you alter the height of the Table view cell in your design, you also have to set the Table view’s height in the view controller also. [[self tableView] setRowHeight:65.0];
To develop a Table view application using custom Table view cells, follow the steps in the next section.
Development Steps: A Custom Table View Application To create this application, execute the following steps:
1.
Start Xcode and create a Navigation-based application for iPhone and name it CustomTableViewApp. If you need to see this step, please refer to Appendix A for the steps to begin a Navigation-based application.
2.
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File, choose Cocoa Touch Class and select Objective-C class as a subclass of UITableViewCell and name the file
A Custom Table View Application
❘ 123
CustomTableViewCell. This is the custom Table view cell
that will appear in place of the default Table view cell.
3.
Choose File ➪ New File and under iPhone OS, select Empty XIB and iPhone (for the product). Name the file CustomTableViewCell.
4.
Choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it PropertyList.
5.
Double-click the CustomTableViewCell.xib file in the group NIB Files to launch Interface Builder. Note that you have only a File’s Owner icon and First Responder icon. You need now to add your custom Table view cell.
6.
From the main menu, select Tools ➪ Library, and choose the Objects tab. Select the Table View Cell and drag it into the window that also contains the File’s Owner icon, and doubleclick on the icon to make the Table view cell visible.
7.
Select Tools ➪ Identity Inspector, and select the Class CustomTableViewCell.
8. 9.
Select Tools ➪ Size Inspector, and set H:65.
10.
11.
12.
From the main menu, select Tools ➪ Library, and choose the Objects tab. Select a UILabel and drag it over the main view, place it at the top left, and release the mouse. Select Tools ➪ Size Inspector, and set W:130. Select Tools ➪ Attributes Inspector, and set the font size to 25.
13.
Select a UILabel and drag it over the main view, place it on the lower left, and release the mouse. Set the font size to 13.
14.
Select Tools ➪ Size Inspector, and set W:130.
Figure 3-1
15.
Select three UILabels and drag them over the main view, place them in the middle, one over the other, and release the mouse. Set the font size to 13.
16.
Select Tools ➪ Size Inspector, and set the top and middle label to W:130 and set the bottom label to W:60.
Select Tools ➪ Attributes Inspector, and set the font size to 25.
17. 18.
Select a UILabel and drag it over the main view, place it on the lower right, and release the mouse. Set layout to right justified and the font size to 13. Select File ➪ Save. Your completed interface should look like the one shown in Figure 3-2.
19.
Figure 3-2
Select Tools ➪ Library, select Classes at the top, and scroll to and select your CustomTableViewCell class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure 3-3: ➤➤
cityLabel (as a UILabel instead of id type)
➤➤
firstNameLabel (as a UILabel instead of id type)
➤➤
lastNameLabel (as a UILabel instead of id type)
➤➤
stateLabel (as a UILabel instead of id type)
➤➤
streetLabel (as a UILabel instead of id type)
➤➤
zipLabel (as a UILabel instead of id type)
124
❘ Chapter 3 Custom Table Views
20.
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first popup, and then select Merge from the second pop-up. A window will appear with your new additions to CustomTableViewCell.h on the left and the original template on the right (see Figure 3-4): ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 3-4
Figure 3-3
21.
There were no additions to the CustomTableViewCell.m file and it may be closed. At this point you have an Objective-C template that will hold your application’s logic. Now you need to make all the connections to: ➤➤
Identify the UILabel as cityLabel.
➤➤
Identify the UILabel as firstNameLabel.
➤➤
Identify the UILabel as lastNameLabel.
➤➤
Identify the UILabel as stateLabel.
➤➤
Identify the UILabel as streetLabel.
➤➤
Identify the UILabel as zipLabel.
A Custom Table View Application
❘ 125
22.
To make the connection to identify the UILabel as cityLabel, control-click on the Custom Table View Cell’s icon to bring up the Inspector, as shown in Figure 3-5.
23.
From the right of the Custom Table View Inspector, control-drag from the circle next to cityLabel to UILabel cityLabel until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the rest of the fields in step 21 (see Figure 3-6). Choose File ➪ Save.
24.
Double-click the RootViewController.xib file in the group NIB Files.
25.
Select Tools ➪ Library, select Classes at the top, and scroll to and select your RootViewController class. At the bottom, choose the Outlets button. Click the + and add the following outlet, as shown in Figure 3-7: ➤➤
customTableViewCell (as a CustomTableViewCell instead of id type)
Figure 3-5
Figure 3-6
26.
Figure 3-7
From the main menu of Interface Builder, choose File ➪ Write Class Files, and select Save from the first pop-up and then Merge from the second pop-up. A window will appear with your new additions to RootViewController.h on the left and the original template on the right (see Figure 3-8): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Select File ➪ Save Merge and close the window.
126
❘ Chapter 3 Custom Table Views
Figure 3-8
27. 28.
29.
There were no additions to the RootViewController.m and it may be closed. Return to the CustomTableViewCell.xib file, choose Tools ➪ Identity Inspector, and select File’s Owner’s icon and choose the RootViewController class. Control-drag from the File’s Owner icon to the Custom Table View icon and select customTableViewCell to make the connection. Choose File ➪ Save.
That completes all the user interface additions and connections needed for this application. Now it is time to enter your logic.
Source Code Listings for a Custom Table View Application For this application, the CustomTableViewAppAppDelegate.h and CustomTableViewAppAppDelegate.m files are not modified and are used as generated.
RootViewController.h Modifications to the Template There are minimal changes to the RootViewController.h file: To link the CustomTableViewCell to the Table view, the CustomViewTableCell is declared, as well as an NSDictionary (contacts) for reading from and writing to the property list for storage. Listing 3-1 shows the complete RootViewController.h file.
A Custom Table View Application
❘ 127
Listing 3-1: The complete RootViewController.h file (/Desktop/ForChapter3/ CustomTableViewApp/Classes/RootViewController.h) #import @class CustomTableViewCell; @interface RootViewController : UITableViewController { CustomTableViewCell *customTableViewCell; NSDictionary *contacts; } @property (nonatomic, retain) IBOutlet CustomTableViewCell *customTableViewCell; @property (nonatomic, retain) NSDictionary *contacts;
@end
RotViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the RootViewController.m template. Each of the declared view properties must be matched with @synthesize (see Listing 3-2). Listing 3-2: Addition of @synthesize #import “RootViewController.h” #import “CustomTableViewCell.h” #import “PropertyList.h” #define DARK_BACKGROUND \ [UIColor colorWithRed:151.0/255.0 green:152.0/255.0 \ blue:155.0/255.0 alpha:1.0] #define LIGHT_BACKGROUND \ [UIColor colorWithRed:172.0/255.0 green:173.0/255.0 \ blue:175.0/255.0 alpha:1.0] @implementation RootViewController @synthesize contacts; @synthesize customTableViewCell;
When the RootViewController view initializes in the viewDidLoad method, the stored contacts property list is read and the values are stored in the custom Table view cell (see Listing 3-3). Listing 3-3: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; NSDictionary *contactList = [PropertyList readFromArchive:@”Contacts”]; [self setContacts:[contactList objectForKey:@”contacts”]]; [self setTitle:@”Contacts”]; [[self tableView] setRowHeight:65.0];
128
❘ Chapter 3 Custom Table Views
[[self tableView] setBackgroundColor:DARK_BACKGROUND]; [[self tableView] setSeparatorStyle: UITableViewCellSeparatorStyleSingleLineEtched]; }
How the data values are distributed within the Table view is determined by the number of sections and the number of rows for each section. Because the data is just a list of grouped contacts, there is only one section, and all rows found will be in that section (see Listing 3-4). Listing 3-4: Characteristics of the data source parameters #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[self contacts] allKeys] count]; }
The method tableView:cellForRowAtIndexPath method is where the custom cell for each row is inserted in the table and then populated with the contact information (see Listing 3-5). Listing 3-5: The tableView:cellForRowAtIndexPath: method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSDictionary *contact = [[self contacts] objectForKey:key]; static NSString *CellIdentifier = @”Cell”; CustomTableViewCell *cell = (CustomTableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@”CustomTableViewCell” owner:self options:nil]; cell = customTableViewCell; self.customTableViewCell = nil; } // Configure the cell. [[cell lastNameLabel] setText:[contact objectForKey:@”lastName”]]; [[cell firstNameLabel] setText:[contact objectForKey:@”firstName”]]; [[cell streetLabel] setText:[contact objectForKey:@”street”]]; [[cell cityLabel] setText:[contact objectForKey:@”city”]]; [[cell stateLabel] setText:[contact objectForKey:@”state”]]; [[cell zipLabel] setText:[contact objectForKey:@”zip”]]; return cell; }
A Custom Table View Application
❘ 129
For this application, there is no navigation, so when the user taps a cell, it will be deselected, not left in the selected state. Also, to distinguish one row visually from another, the backgrounds will alternate between dark and light (see Listing 3-6). Listing 3-6: The tableView delegate methods #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if((indexPath.row % 2 == 0)) { cell.backgroundColor = DARK_BACKGROUND; } else { cell.backgroundColor = LIGHT_BACKGROUND; } }
The RootViewController.m class is now complete. Listing 3-7 shows the complete implementation. Listing 3-7: The complete RootViewController.m file (/Desktop/ForChapter3/
CustomTableViewApp/Classes/RootViewController.m) #import “RootViewController.h” #import “CustomTableViewCell.h” #import “PropertyList.h”
#define DARK_BACKGROUND \ [UIColor colorWithRed:151.0/255.0 green:152.0/255.0 \ blue:155.0/255.0 alpha:1.0] #define LIGHT_BACKGROUND \ [UIColor colorWithRed:172.0/255.0 green:173.0/255.0 \ blue:175.0/255.0 alpha:1.0] @implementation RootViewController @synthesize contacts; @synthesize customTableViewCell; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; NSDictionary *contactList = [PropertyList readFromArchive:@”Contacts”]; [self setContacts:[contactList objectForKey:@”contacts”]]; [self setTitle:@”Contacts”]; [[self tableView] setRowHeight:65.0]; [[self tableView] setBackgroundColor:DARK_BACKGROUND]; [[self tableView] setSeparatorStyle: UITableViewCellSeparatorStyleSingleLineEtched];
130
❘ Chapter 3 Custom Table Views
} #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[self contacts] allKeys] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSDictionary *contact = [[self contacts] objectForKey:key]; static NSString *CellIdentifier = @”Cell”; CustomTableViewCell *cell = (CustomTableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@”CustomTableViewCell” owner:self options:nil]; cell = customTableViewCell; self.customTableViewCell = nil; } // Configure the cell. [[cell lastNameLabel] setText:[contact objectForKey:@”lastName”]]; [[cell firstNameLabel] setText:[contact objectForKey:@”firstName”]]; [[cell streetLabel] setText:[contact objectForKey:@”street”]]; [[cell cityLabel] setText:[contact objectForKey:@”city”]]; [[cell stateLabel] setText:[contact objectForKey:@”state”]]; [[cell zipLabel] setText:[contact objectForKey:@”zip”]]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if((indexPath.row % 2 == 0)) { cell.backgroundColor = DARK_BACKGROUND; } else { cell.backgroundColor = LIGHT_BACKGROUND;
A Custom Table View Application
❘ 131
} } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setCustomTableViewCell:nil]; [self setContacts:nil]; } - (void)dealloc { [customTableViewCell release]; [contacts release]; [super dealloc]; }
@end
PropertyList.h Modifications to the Template There is only one change to the PropertyList.h file: The method readFromArchive loads the sample data from the property list file and populates the Table view. Listing 3-8 shows the complete PropertyList.h file. Listing 3-8: The complete PropertyList.h file (/Desktop/ForChapter3/CustomTableViewApp/ Classes/PropertyList.h) #import
@interface PropertyList : NSObject { } + (NSDictionary *)readFromArchive:(NSString *)aFileName; @end
PropertyList.m Modifications to the Template The PropertyList class has only one method. The readFromArchive method reads the property list of names and returns the contents in the form of a NSDictionary to the calling method (see Listing 3-9). Listing 3-9: The complete PropertyList.m file (/Desktop/ForChapter3/CustomTableViewApp/
Classes/PropertyListm.m) #import “PropertyList.h”
@implementation PropertyList + (NSDictionary *)readFromArchive:(NSString *)aFileName {
132
❘ Chapter 3 Custom Table Views
NSString *errorDesc = nil; NSPropertyListFormat format; NSString *plistPath = [[NSBundle mainBundle] pathForResource:aFileName ofType:@”plist”]; NSData *plistXML = [[NSFileManager defaultManager] contentsAtPath:plistPath]; NSDictionary *temp = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorDesc]; if (!temp) { NSLog(@”%s at line %d with message: %@”, __FUNCTION__, __LINE__, errorDesc); [errorDesc release]; } return temp; } @end
CustomTableViewCell.h Modifications to the Template You declared the following six outlets in Interface Builder: ➤➤
cityLabel
➤➤
firstNameLabel
➤➤
lastNameLabel
➤➤
streetLabel
➤➤
stateLabel
➤➤
zipLabel
You must now define the properties for these variables in order to get and set their value. Listing 3-10 shows the complete CustomTableViewCell.h file. Listing 3-10: The complete CustomTableViewCell.h file (/Desktop/ForChapter3/ CustomTableViewApp/Classes/CustomTableViewCell.h) #import
@interface CustomTableViewCell : UITableViewCell { UILabel *cityLabel; UILabel *firstNameLabel; UILabel *lastNameLabel; UILabel *streetLabel; UILabel *stateLabel; UILabel *zipLabel; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
IBOutlet IBOutlet IBOutlet IBOutlet
UILabel UILabel UILabel UILabel
*cityLabel; *firstNameLabel; *lastNameLabel; *streetLabel;
A Custom Table View Application
❘ 133
@property (nonatomic, retain) IBOutlet UILabel *stateLabel; @property (nonatomic, retain) IBOutlet UILabel *zipLabel; @end
CustomTableViewCell.m Modifications to the Template For each of the view properties that were declared, you must match them with @synthesize. Listing 3-11 shows the complete CustomTableViewCell.m file. Listing 3-11: The complete CustomTableViewCell.m file (/Desktop/ForChapter3/ CustomTableViewApp/Classes/CustomTableViewCell.m) #import “CustomTableViewCell.h” @implementation CustomTableViewCell @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
cityLabel; firstNameLabel; lastNameLabel; streetLabel; stateLabel; zipLabel;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { // Initialization code } return self; } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (void)dealloc { [cityLabel release]; [firstNameLabel release]; [lastNameLabel release]; [streetLabel release]; [stateLabel release]; [zipLabel release]; [super dealloc]; }
@end
Creating the Contacts.plist Property List File You have completed all the user interface additions and connections needed for this application. Now it is time to create and enter the contacts into a Contacts.plist file. To create the plist file, follow these steps:
1.
In the Groups & Files section of Xcode, click the Resources group. Choose File ➪ New File, and select Property List from the Resource section of Mac OS X (see Figure 3-9). Name the file Contacts.plist.
134
❘ Chapter 3 Custom Table Views
Figure 3-9
2.
3.
In the Xcode editor, select Root and click the triangle next to it so that it points down. Then, on the far right, click the tab to add a new cell and do the following: ➤➤
Replace New Item with Jones, and select Dictionary for the Type.
➤➤
Click the triangle by Jones, and then click the tab at the far right to add a cell.
➤➤
Replace New Item with lastName, with Type as String and Jones for the Value.
➤➤
Click the plus at the right to add firstName, with Type as String and Joe for the Value.
➤➤
Click the plus at the right to add street as Type String and 123 Normal for the Value.
➤➤
Click the plus at the right to add city as Type String and Normal for the Value.
➤➤
Click the plus at the right to add state as Type String and IL for the Value.
➤➤
Click the plus at the right to add zip as Type String and 69304 for the Value.
Repeat step 2 to add about eight more contacts so your file looks like Figure 3-10.
You are now ready to test your application.
Summary
❘ 135
Figure 3-10
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel, and click Run and Build to try out your app. It should give you the results described at the beginning of the section “A Custom Table View Application.”
Summary The Table view is one of the most frequently used controls on the iPhone and iPod touch. As described in this chapter, in order to fill the Table view with data, the number of sections and number of rows must be known in order for values to be saved to the Table view cell. With UITabelViewCell, Apple has provided a versatile component in its default style, but you can also customize Table view cells, which enables you to create a unique look for your Table view applications. This chapter demonstrated a Table view with labels, but keep in mind that the UITableViewCell is still a view, and therefore can contain a variety of UI components, depending on your application’s requirements.
4
The split View What’s in this chaPter? ➤➤
How to create a Split View-based application
➤➤
How to link data from the RootViewController with the DetailViewController
➤➤
How to handle orientation changes
The split view is a unique combination of views in a master-detail relationship that gives the user a different visual experience depending on the orientation of the iPad. The master is the RootViewController and the detail is the DetailViewController. In landscape mode, both the master and detail views are visible. The master view is a list of options on the left of the screen. When an option is selected, the result occurs in the detail view. In portrait mode, however, the detail takes up the entire view and the master view is available as a popup. The master is accessible through a button on the toolbar on the detail view. Tapping the button reveals a popup controller with the contents of the master view, so selections can still be made. In this chapter you build an application that demonstrates this master-detail relationship, and add gestures that emulate the fl ipping of a page.
the UisPlitvieWcontroller class The UISplitViewController class manages two ViewController classes in a master-detail relationship. By default, the master view is a table view controller, but the detail view can be determined by the developer. The UISplitViewController does not provide any interface by itself; rather, it manages the RootViewController and DetailViewController — especially with respect to the iPad’s orientation. While the UISplitViewController does manage orientation, it does not manage any data communication between the two view controllers it manages; that is your responsibility. The application you build in this chapter passes contact information from the master page to the detail page. The split view is available only on the iPad; attempts to implement it on the iPhone will fail.
138
❘ Chapter 4 The Split View
UIPopoverControllerDelegate Protocol Two methods are defined by this protocol for the delegates of the UIPopoverController class: ➤➤
popoverControllerShouldDismissPopover: — Asks the delegate if the popover should be dismissed when the user attempts to dismiss it. Returning NO will keep the popover visible.
➤➤
popoverControllerDidDismissPopover: — Notifies the delegate that the popover was dismissed.
UISplitViewControllerDelegate Protocol Three methods are defined by this protocol for the delegates of the UISplitViewController class: ➤➤
splitViewController:willHideViewController:withBarButtonItem:forPopover Controller: — Notifies the delegate that the view controller is about to be hidden
➤➤
splitViewController:willShowViewController:invalidatingBarButtonItem: — Notifies
the delegate that the hidden view controller is about to reappear ➤➤
splitViewController:popoverController:willPresentViewController: — Notifies the del-
egate that the hidden view is going to be displayed in a popover
A Simple Split View Application When this application launches in landscape mode, the Table view will be on the left and the Detail view will be on the right. The Table view contains a list of contact names, and the Detail view contains the contact’s first and last name, along with the phone number (see Figure 4-1).
Figure 4-1
A Simple Split View Application
❘ 139
Rotating the iPad to portrait orientation, the Table view moves out of sight and a Contacts button appears on the toolbar at the top, as shown in Figure 4-2.
Figure 4-2
Tapping the button on the toolbar displays a popover with the contact information. Tapping one of the names updates the Detail view with the entire contact information for that person, as shown in Figure 4-3. In landscape orientation, tapping a name in the list also updates the Detail view.
140
❘ Chapter 4 The Split View
Figure 4-3
You could continue tapping on the Table view cells or popover cells, or you could just swipe as if you were turning the page of a book (see Figure 4-4). Remember that the iPad is not a bigger iPhone; it has its own characteristics. The turning of a page is a good example of how to enhance the user experience with this device. Now it is time for the coding. Although the split view may appear complicated, Apple has made it easy to programmatically create applications for this really fine interface.
A Simple Split View Application
❘ 141
Figure 4-4
Development Steps: A Simple Split View Application for the iPad To create this application (which is specific to the iPad), execute the following steps:
1.
Start Xcode and create a Split View–based application for the iPad named SplitViewApp. If you need to see this step, please see Appendix A for the steps to begin a Split View–based application.
142
❘ Chapter 4 The Split View
2.
In the Groups & Files section of Xcode click the Classes group. Choose File ➪ New File. Choose Cocoa Touch Class and select Objective-C class as a subclass of NSObject and name it PropertyList. This is the data storage object that contains your contact information after loading the DataSource.plist file.
3.
Double-click the DetailView.xib file in the group NIB Files to launch Interface Builder (see Figure 4-5).
Figure 4-5
4.
After selecting the main view, select the label “Detail view content goes here” and press the Delete key to eliminate it.
5.
Select Tools ➪ Attributes Inspector, select the main view, and set the Background option to Scroll View Textured Background Color.
6. 7. 8.
From the main menu, select Tools ➪ Library and choose the Objects tab.
Select a UIView and drag it over the main view and release the mouse. Select Tools ➪ Size Inspector and set X:192, Y:251, W:384, and H:502. In addition, remove all Autosizing connections, as indicated when there are no red solid lines and the red box is in the middle of the image.
9. 10. 11.
Select a UILabel and drag it onto the view you just created. Double-click and enter First Name:.
12.
With the user interface complete, return to Xcode and double-click the DetailViewController.h file, modifying it as shown in Listing 4-1. The additions, shown in bold, are the three labels for the first name, last name, and phone values; pageView, the view where you placed the labels; a single page, NSDictionary contacts, which stores the contact information in memory; and a variable to track the current viewed page.
Select another UILabel and drag it to the right of your First Name: label. Repeat this two more times to create two more sets of labels. For the second group of labels, title the left label Last Name:, and for the third set of labels, title the left label Phone:, as shown in Figure 4-6.
A Simple Split View Application
Figure 4-6
Listing 4-1: The complete DetailViewController.m file (Chapter4/SplitViewApp/Classes/
DetailViewController.h)
#import @interface DetailViewController : UIViewController { UIPopoverController *popoverController;
❘ 143
144
❘ Chapter 4 The Split View
UIToolbar *toolbar; UILabel *firstNameLabel; UILabel *lastNameLabel; UILabel *phoneLabel; UIView *pageView; UIView *page; NSDictionary *contacts; int currentPageNumber; UITableView *tableView; } @property (nonatomic, retain) IBOutlet UIToolbar *toolbar; @property @property @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain) retain) retain) retain)
IBOutlet UILabel *firstNameLabel; IBOutlet UILabel *lastNameLabel; IBOutlet UILabel *phoneLabel; IBOutlet UIView *pageView; IBOutlet UIView *page; NSDictionary *contacts; UITableView *tableView;
- (void)createGestureRecognizers; - (void)createSwipeGestureRecognizer:(UIView *)view direction:(UISwipeGestureRecognizerDirection)direction; - (void)setVisiblePage:(int)aPageNumber; @end
With the labels and views entered in the interface file, return to Interface Builder to connect the outlets. You have to make all the connections to: ➤➤
Identify the UILabel as firstNameLabel
➤➤
Identify the UILabel as lastNameLabel
➤➤
Identify the UILabel as phoneLabel
➤➤
Identify the UIView as pageView
13.
To make the connection to identify the UILabel as firstNameLabel, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 4-7.
14.
From the right of the File’s Owner Inspector, control-drag from the circle next to the firstNameLabel to the UILabel firstNameLabel until it is highlighted, then release the mouse. The circle will be filled, indicating the connection has been made. Repeat this for the lastNameLabel and the phoneLabel to UILabel. Connect the UIView as pageView (see Figure 4-8). Select File ➪ Save when you are done.
Figure 4-7
Figure 4-8
A Simple Split View Application
❘ 145
Creating the DataSource.plist Property List File You have completed all the user interface additions and connections that are needed for this application. Now it is time to create and enter the contact into a DataSource.plist file. To create the plist file follow these steps:
1.
In the Groups & Files section of Xcode click the Resources group. Choose File ➪ New File and select Property List from the Resource section of Mac OS X (see Figure 4-9). Name the file DataSource.plist.
Figure 4-9
2.
3. 4.
In the editor window of Xcode, select Root and click the triangle next to Root so it points down. Then, on the far right, click the tab to add a new cell and do the following so your entry looks like Figure 4-10: ➤➤
Replace New Item with Jones, and select Dictionary for the Type.
➤➤
Click the triangle by Jones and then click the tab at the far right to add a cell.
➤➤
Replace New Item with firstName and leave Type as String and add Joe for the Value.
➤➤
Click the plus at the right to add lastName, with Type as String and Jones for the Value.
➤➤
Click the plus at the right to add phone, with Type as String and 555-1212 for the Value.
Repeat step 2 to add a few more contacts so your file looks like Figure 4-11. Choose File ➪ Save to save your project. You have completed the data portion of this application. Now it is time to enter your logic.
146
❘ Chapter 4 The Split View
Figure 4-10
Figure 4-11
A Simple Split View Application
❘ 147
Source Code Listings for A Simple Split View Application For this application, the SplitViewAppAppDelegate.h and SplitViewAppAppDelegate.m files are not modified and are used as generated:
RootViewController.h Modifications to the Template The only addition to the RootViewController is an outlet to allow us to set the title of the UINavigationItem, instead of accepting the default Root View Controller title. Listing 4-2 shows the complete RootViewController.h file, with modifications in bold. Listing 4-2: The complete RootViewController.h file (Chapter4/SplitViewApp/Classes/ RootViewController.h) #import @class DetailViewController; @interface RootViewController : UITableViewController { DetailViewController *detailViewController; UINavigationItem *rootTitle; } @property (nonatomic, retain) IBOutlet DetailViewController *detailViewController; @property (nonatomic, retain) IBOutlet UINavigationItem *rootTitle;
@end
RootViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the RootViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 4-3). Listing 4-3: Addition of @synthesize #import “RootViewController.h” #import “DetailViewController.h” #import “PropertyList.h” @implementation RootViewController @synthesize detailViewController; @synthesize rootTitle;
When the RootViewController view initializes in the viewDidLoad method, the contacts file is loaded and the contact on the detail page will be the first contact in the list that was just loaded. The title of the UINavigationItem is “Contacts,” as shown in Listing 4-4). Listing 4-4: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad {
148
❘ Chapter 4 The Split View
[super viewDidLoad]; self.clearsSelectionOnViewWillAppear = NO; self.contentSizeForViewInPopover = CGSizeMake(320.0, 250.0); [detailViewController setContacts:[PropertyList readFromArchive:@”DataSource”]]; [detailViewController setVisiblePage:0]; [rootTitle setTitle:@”Contacts”]; [detailViewController setTableView:[self tableView]]; }
How information is displayed in a Table view depends on two factors: ➤➤
The number of sections
➤➤
The number of rows in each section
For this application, all the data is related, as it is simply the listing of the last names, so there is only one section. The number of rows is determined by how many contacts are contained in the property list, which was created in the “Creating the DataSource.plist Property List File” section. The numberOfSectionsInTableView and tableView:numberOfRowsInSection: methods are shown in Listing 4-5. Listing 4-5: The numberOfSectionsInTableView and tableView:numberOfRowsInSection:
methods
#pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { return [[[detailViewController contacts] allKeys] count]; }
The actual population of the Table view cell value happens in the tableView:cellForRowAtIndexPath: method. The value for each Table view cell is the last name of each contact, which is the key to each entry in the NSDictionary. Therefore, take the keys from the NSDictionary and sort them, and assign each cell the last name value (see Listing 4-6). Listing 4-6: The tableView:cellForRowAtIndexPath: method - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[detailViewController contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *cellText = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”CellIdentifier”; // Dequeue or create a cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc]
A Simple Split View Application
❘ 149
initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:cellText]; return cell; }
All you need to handle now is the user selecting a Table view cell. In this case, send the row number that was selected to the DetailViewController, which can extract the proper contact from contact list The selection is handled by the tableView:didSelectRowAtIndexPath method, as shown in Listing 4-7. Listing 4-7: The tableView:didSelectRowAtIndexPath: method #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [detailViewController setVisiblePage:[indexPath row]]; }
The RootViewController.m class is now complete. Listing 4-8 shows the complete implementation. Listing 4-8: The complete RootViewController.m file (Chapter4/SplitViewApp/Classes/
RootViewController.m)
#import “RootViewController.h” #import “DetailViewController.h” #import “PropertyList.h” @implementation RootViewController @synthesize detailViewController; @synthesize rootTitle; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.clearsSelectionOnViewWillAppear = NO; self.contentSizeForViewInPopover = CGSizeMake(320.0, 250.0); [detailViewController setContacts:[PropertyList readFromArchive:@”DataSource”]]; [detailViewController setVisiblePage:0]; [rootTitle setTitle:@”Contacts”]; [detailViewController setTableView:[self tableView]]; } // Ensure that the view controller supports rotation and // that the split view can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark -
150
❘ Chapter 4 The Split View
#pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { return [[[detailViewController contacts] allKeys] count]; }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[detailViewController contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *cellText = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”CellIdentifier”; // Dequeue or create a cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:cellText]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [detailViewController setVisiblePage:[indexPath row]]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setRootTitle:nil]; [self setDetailViewController:nil]; } - (void)dealloc { [rootTitle release]; [detailViewController release]; [super dealloc]; } @end
A Simple Split View Application
❘ 151
DetailViewController.h Modifications to the Template The DetailViewController.h file appears in the section “Development Steps: A Simple Split View Application,” in Listing 4-1.
DetailViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the DetailViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 4-9). Listing 4-9: Addition of @synthesize #import “DetailViewController.h” #import “RootViewController.h” @interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; @end @implementation DetailViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
toolbar, popoverController; firstNameLabel; lastNameLabel; phoneLabel; pageView; page; contacts; tableView;
When the DetailViewController view initializes in the viewDidLoad method, gesture recognizers are initialized, as is the UIView page that will provide the turning page animation handled by the gesture recognizers (see Listing 4-10). Listing 4-10: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self createGestureRecognizers]; page = [[UIView alloc] init]; [pageView addSubview:page]; }
The setDetailItem: method, shown in Listing 4-11, takes the selected contact and assigns the associated values for display on the detail page. Listing 4-11: The setDetailItem: method #pragma mark #pragma mark Managing the detail items - (void)setDetailItem:(NSDictionary *)newDetailItem { [firstNameLabel setText:[newDetailItem objectForKey:@”firstName”]]; [lastNameLabel setText:[newDetailItem objectForKey:@”lastName”]];
152
❘ Chapter 4 The Split View
[phoneLabel setText:[newDetailItem objectForKey:@”phone”]]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } }
The split view has two delegate methods that handle the hiding and revealing of the Root View Controller depending on the device’s orientation (see Listing 4-12). Listing 4-12: Hiding and showing the Root View Controller #pragma mark #pragma mark Split view support - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = @”Contacts”; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; } // Called when the view is shown again in the split view, // invalidating the button and popover controller. - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = nil; }
One of the appealing things about the iPad is that it detects swipes and has transitions that emulate the flipping of a page. To detect this, gesture recognizers have to be created and added to the view, and then handled to create the proper transition effect (see Listing 4-13). Listing 4-13: The gesture recognizer logic #pragma mark #pragma mark Gesture Processing methods - (void)createGestureRecognizers { [self createSwipeGestureRecognizer:pageView direction:UISwipeGestureRecognizerDirectionLeft]; [self createSwipeGestureRecognizer:pageView direction:UISwipeGestureRecognizerDirectionRight]; } - (void)createSwipeGestureRecognizer:(UIView *)view direction:(UISwipeGestureRecognizerDirection)direction { UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
A Simple Split View Application
❘ 153
action:@selector(handleSwipeEvent:)]; [recognizer setDirection:direction]; [view addGestureRecognizer:recognizer]; [recognizer release]; } - (void)handleSwipeEvent:(UISwipeGestureRecognizer *)recognizer { int pagenumber = currentPageNumber; int transition = UIViewAnimationTransitionCurlUp; NSIndexPath *scrollIndexPath; switch ([recognizer direction]) { case UISwipeGestureRecognizerDirectionLeft: pagenumber++; transition = UIViewAnimationTransitionCurlUp; break; case UISwipeGestureRecognizerDirectionRight: pagenumber--; transition = UIViewAnimationTransitionCurlDown; break; default: break; } if((pagenumber >= 0) && (pagenumber < [[[self contacts] allKeys] count])) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.75]; [UIView setAnimationBeginsFromCurrentState:YES]; [UIView setAnimationTransition:transition forView:pageView cache:YES]; [self setVisiblePage:pagenumber]; scrollIndexPath = [NSIndexPath indexPathForRow:pagenumber inSection:0]; [tableView selectRowAtIndexPath:scrollIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; [page removeFromSuperview]; [pageView addSubview:page]; [UIView commitAnimations]; } }
Finally, the setVisiblePage method takes a page number and translates it to an actual contact that is used for display. This enables a one-to-one relationship between the page count and index to a contact (see Listing 4-14). Listing 4-14: The setVisiblePage: method #pragma mark #pragma mark Utility methods - (void)setVisiblePage:(int)aPageNumber { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:aPageNumber]; NSDictionary *aData = [[self contacts] objectForKey:key]; [[self firstNameLabel] setText:[aData objectForKey:@”firstName”]];
154
❘ Chapter 4 The Split View
[[self lastNameLabel] setText:[aData objectForKey:@”lastName”]]; [[self phoneLabel] setText:[aData objectForKey:@”phone”]]; currentPageNumber = aPageNumber; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } }
The DetailViewController.m class is now complete. Listing 4-15 shows the complete implementation. Listing 4-15: The complete DetailViewController.m file (Chapter4/SplitViewApp/Classes/
DetailViewController.m)
#import “DetailViewController.h” #import “RootViewController.h” @interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; @end @implementation DetailViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
toolbar, popoverController; firstNameLabel; lastNameLabel; phoneLabel; pageView; page; contacts; tableView;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self createGestureRecognizers]; page = [[UIView alloc] init]; [pageView addSubview:page]; } #pragma mark #pragma mark Managing the detail items - (void)setDetailItem:(NSDictionary *)newDetailItem { [firstNameLabel setText:[newDetailItem objectForKey:@”firstName”]]; [lastNameLabel setText:[newDetailItem objectForKey:@”lastName”]]; [phoneLabel setText:[newDetailItem objectForKey:@”phone”]]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } } #pragma mark #pragma mark Split view support - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController
A Simple Split View Application
withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = @”Contacts”; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; } // Called when the view is shown again in the split view, // invalidating the button and popover controller. - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = nil; } #pragma mark #pragma mark Rotation support - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark Gesture Processing methods - (void)createGestureRecognizers { [self createSwipeGestureRecognizer:pageView direction:UISwipeGestureRecognizerDirectionLeft]; [self createSwipeGestureRecognizer:pageView direction:UISwipeGestureRecognizerDirectionRight]; } - (void)createSwipeGestureRecognizer:(UIView *)view direction:(UISwipeGestureRecognizerDirection)direction { UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeEvent:)]; [recognizer setDirection:direction]; [view addGestureRecognizer:recognizer]; [recognizer release]; } - (void)handleSwipeEvent:(UISwipeGestureRecognizer *)recognizer { int pagenumber = currentPageNumber; int transition = UIViewAnimationTransitionCurlUp; NSIndexPath *scrollIndexPath; switch ([recognizer direction]) { case UISwipeGestureRecognizerDirectionLeft: pagenumber++; transition = UIViewAnimationTransitionCurlUp; break;
❘ 155
156
❘ Chapter 4 The Split View
case UISwipeGestureRecognizerDirectionRight: pagenumber--; transition = UIViewAnimationTransitionCurlDown; break; default: break; } if((pagenumber >= 0) && (pagenumber < [[[self contacts] allKeys] count])) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.75]; [UIView setAnimationBeginsFromCurrentState:YES]; [UIView setAnimationTransition:transition forView:pageView cache:YES]; [self setVisiblePage:pagenumber]; scrollIndexPath = [NSIndexPath indexPathForRow:pagenumber inSection:0]; [tableView selectRowAtIndexPath:scrollIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone]; [page removeFromSuperview]; [pageView addSubview:page]; [UIView commitAnimations]; } } #pragma mark #pragma mark Utility methods - (void)setVisiblePage:(int)aPageNumber { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:aPageNumber]; NSDictionary *aData = [[self contacts] objectForKey:key]; [[self firstNameLabel] setText:[aData objectForKey:@”firstName”]]; [[self lastNameLabel] setText:[aData objectForKey:@”lastName”]]; [[self phoneLabel] setText:[aData objectForKey:@”phone”]]; currentPageNumber = aPageNumber; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setPopoverController:nil]; [self setToolbar:nil]; [self setFirstNameLabel:nil]; [self setLastNameLabel:nil]; [self setPhoneLabel:nil]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning {
A Simple Split View Application
❘ 157
[super didReceiveMemoryWarning]; } - (void)dealloc { [popoverController release]; [toolbar release]; [firstNameLabel release]; [lastNameLabel release]; [phoneLabel release]; [super dealloc]; } @end
PropertyList.h Modifications to the Template There is only one method in this class, readFromArchive, which will read the DataSource.plist file of contact information. Listing 4-16 shows the complete PropertyList.h file. Listing 4-16: The complete PropertyList.h file (Chapter4/SplitViewApp/Classes/PropertyList.h) #import
@interface PropertyList : NSObject { } + (NSDictionary *)readFromArchive:(NSString *)aFileName; @end
PropertyList.m Modifications to the Template The method readFromArchive will read the file and store it in a NSDictionary so the values can easily be accessed. The key to each contact record is the last name of the contact. Listing 4-17 shows the complete PropertyList.m file. Listing 4-17: The complete PropertyList.m file (Chapter4/SplitViewApp/Classes/ PropertyList.m) #import “PropertyList.h” @implementation PropertyList + (NSDictionary *)readFromArchive:(NSString *)aFileName { NSString *errorDesc = nil; NSPropertyListFormat format; NSString *plistPath = [[NSBundle mainBundle] pathForResource:aFileName ofType:@”plist”]; NSData *plistXML = [[NSFileManager defaultManager] contentsAtPath:plistPath]; NSDictionary *temp = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorDesc]; if (!temp) {
158
❘ Chapter 4 The Split View
NSLog(@”%s at line %d with message: %@”, __FUNCTION__, __LINE__, errorDesc); [errorDesc release]; } return temp; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the section “A Simple Split View Application.”
Summary Before the iPad was released, critics were already saying that it would be nothing more than a large iPhone, or an underpowered laptop. At the time, it was hard to imagine what could occupy the conceptual space between a phone and a laptop. When the iPad was finally revealed, it was a hit because it does offer something unique. One aspect of that uniqueness was demonstrated in the application you built in this chapter: The split view and the associated gestures provide users with a rich, intuitive experience.
5
Touch events What’s in this chaPter? ➤➤
How to handle touch events
➤➤
How to create and handle gestures
➤➤
How to simplify gesture recognition with the use of UIGestureRecognizer
With traditional desktop user interaction, events are initiated by the use of the keyboard or mouse. With iOS 4, touch and motion events are delivered to your application. The manager of these events is the UIApplication object and the actual event is a UIEvent object. The most common event that you will track in your application is the touch event interacting with views. The UITouch sequence begins as soon as the user places a fi nger or fi ngers on the screen, and ends when the last fi nger has been removed from the screen. The location of the fi nger or fi ngers resolves itself to a touch point or CGPoint data value. Touch events can be multi-fi ngered, as in the case of a pinch, when you zoom in or out on a photo. Single-fi ngered events can be the tap, the touch and hold, and the drag and swipe. In this chapter you will observe two techniques of event handling that involve touches: The fi rst uses touchesBegan, touchesMoved, touchesEnded and touchesCancelled, which are defi ned by iOS 4. The second, gesture recognition, uses the UIGestureRecognizer, which allows the customization of
gestures in addition to the standard set.
toUch event handlinG A touch event is a UIEvent of the type UIEventTypeTouches. The UITouch object represents the touches themselves and contains the following information: ➤➤
locationInView — Coordinates of the touch
➤➤
previousLocationInView — Previous coordinates
➤➤
tapCount — How many taps were made
➤➤
timestamp — Time of the last touch
➤➤
phase — What phase the touch is in
160
❘ Chapter 5 Touch Events
As an event happens, it is placed on a queue to be distributed by the application to the window where it occurred. From there the event is forwarded to its first responder, in most cases the view where the touch occurred. If it cannot be handled by the view, then the event is forwarded to the next responder, a view controller for example, where it is handled or passed on to the next responder in the responder chain. To handle the multi-touch events, the class must implement the following: ➤➤
In the initialization the view indicates that it will enter the responder chain for events by the following: [self becomeFirstResponder];
➤➤
Include in the class the following, to indicate that the view can become the first to handle the event: - (BOOL)canBecomeFirstResponder { YES; }
To handle the sequence of multi-touch events or phases, the class must implement one or more of the following: ➤➤
When the first finger touches the screen: - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
➤➤
When one or more fingers, held on the screen, move: - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
➤➤
When one or more fingers are removed from the screen: - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
➤➤
When the touch event sequence is cancelled by a system event, such as a phone call: - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
To determine the number of times the user tapped, you get the value of the tapCount property of the UITouch object. The best place to determine the tap count is in the touchesBegan:withEvent:. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesBegan” tapCount:[[touches anyObject] tapCount] touchCount:[touches count]]; }
Single Touch When you are just isolating a single touch, you can grab any UITouch from the NSSet touches as follows: UITouch *aTouch = [touches anyObject];
Multi-Touch In order to track multi-touch events, there are a few things you must do: ➤➤
The multipleTouchEnabled property of the view, to receive multiple touches, must be set to YES.
➤➤
Use CFDictionaryRef to track the touches throughout the phases.
When isolating all the touches in a multi-touch event, you loop through the NSSet touches as follows: if([touches count] > 1) { for (UITouch *touch in touches) { // Isolate each touch point CGPoint *point = [touch locationInView:self]; } }
Taps To isolate a single tap: (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *aTouch = [touches anyObject];
Touch Event Handling
❘ 161
if (aTouch.tapCount == 1) { // your single tap logic } }
To isolate a double tap: (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *aTouch = [touches anyObject]; if (aTouch.tapCount => 2) { // your double tap logic } }
To isolate a single tap from a double tap once the touch event has completed: (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *aTouch = [touches anyObject]; if (aTouch.tapCount == 1) { // your single tap logic } else if(aTouch.tapCount => 2) { // your double tap logic } }
Swipes and Gestures Since the introduction of SDK 3.2, the UIGestureRecognizer class simplifies gesture recognition. Pre-SDK 3.2, you would have to store the location of the initial touch; and then in the touchesMoved method, you would have to ensure that the movement is horizontal and have a movement duration that was long enough. SDK 3.2 provides six gesture recognizers: ➤➤
UITapGestureRecognizer — For taps
➤➤
UIPinchGestureRecognizer — For in, out pinching
➤➤
UIPanGestureRecognizer — For dragging
➤➤
UISwipeGestureRecognizer — For swiping
➤➤
UIRotationGestureRecognizer — For rotating fingers in opposite direction
➤➤
UILongPressGestureRecognizer — For touch with hold
The process to handle these gesture recognizers is to create an instance of one and attach a method to handle the event as follows: UITapGestureRecognizer *fingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleFingerTap:)]; [[self view] addGestureRecognizer:fingerTap];
The method that handles the event receives the gesture recognizer and processes it as follows: - (void)handleFingerTap:(UITapGestureRecognizer *)recognizer { CGPoint tapPoint = [recognizer locationInView:self.view]; [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.5]; [imageView setCenter:tapPoint]; [UIView commitAnimations]; }
To remove a custom gesture recognizer, use the following: [[self view] removeGestureRecognizer:recognizer];
162
❘ Chapter 5 Touch Events
The sequence of events for gesture recognizers is as follows: UIGestureRecognizerStatePossible UIGestureRecognizerStateBegan UIGestureRecognizerStateChanged UIGestureRecognizerStateEnded UIGestureRecognizerStateCancelled UIGestureRecognizerStateFailed UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
To create a custom gesture recognizer: ➤➤
Subclass UIGestureRecognizer.
➤➤
Include the following in your subclass’s header file: #import
➤➤
Override the following methods in your subclass: -
(void)reset; (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
A Simple Touch Handler In this application, an image is centered in the display. The user will touch and hold the image with their finger. The status indicates the touchesBegan phase, as shown in Figure 5-1. With the image still selected, the user drags the image around the display. The status indicates the touchesMoved phase, as shown in Figure 5-2.
To reset the image, the user shakes the device. To simulate the device being shaken, select Hardware ➪ Shake Gesture (see Figure 5-3) from the simulator main menu.
Figure 5-1
Figure 5-2
Figure 5-3
A Simple Touch Handler
❘ 163
Development Steps: A Simple Touch Handler To create this application, execute the following steps:
1.
Start Xcode and create a Window-based application for the iPhone and name it SimpleTouch-iPhone. If you need to see this step, please see Appendix A for the steps to begin a Window-based application.
2.
Choose File ➪ New File… and select Objective-C class as a subclass of UIView, as shown in Figure 5-4, and name the class SimpleTouchView.
Figure 5-4
3.
You need to add one image and one sound file to the project. ➤➤
Select Resources from Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your image, default.png, and click Add (see Figure 5-5).
➤➤
Check Copy items into destination group’s folder (see Figure 5-6), and click Add.
➤➤
To add the sound file, take any one-second mp3 file and rename it Boing.mp3.
➤➤
You need to convert the mp3 file to a caf file. To do so, type the following in a Terminal window (/Applications/Utilities/Terminal.app): /usr/bin/afconvert -f caff -d LEI16 Boing.mp3 Boing.caf
➤➤
Select Resources in Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your sound file, Boing.caf, and click Add.
➤➤
Check Copy items into destination group’s folder, and click Add.
164
❘ Chapter 5 Touch Events
4.
5.
You need to add one framework to the project: ➤➤
Select Frameworks in Xcode’s Groups & Files window on the left.
➤➤
Control-click and choose Add ➪ Existing Frameworks.
➤➤
Select AudioToolbox.framework and click Add.
Double-click the MainWindow.xib file to launch Interface Builder (see Figure 5-7).
Figure 5-5
Figure 5-7
Figure 5-6
A Simple Touch Handler
❘ 165
6.
From the Interface Builder Library (Tools ➪ Attributes Inspector), click on the view and check Multiple Touch as shown in Figure 5-8.
7.
From the Interface Builder menu, select Tools ➪ Library, choose the following and drag them to the View window. Your interface should now look like Figure 5-9: ➤➤
One UIView that covers the entire view. Choose (Tools ➪ Identity Inspector) and choose SimpleTouchView as the class.
➤➤
Three UILabels placed under each other starting from the top of the view.
➤➤
One UIImageView with the size of 75 × 75. To accomplish this, do the following: Drag your UIImageView to the View window below the three labels you just added.
1. 2.
3.
Change the size of the view to W:75 H:75 and center it on the view.
Choose Tools ➪ Size Inspector from the main menu and you will see W:240 W:128 just under the Frame drop-down in the upper-right corner of the inspector.
Figure 5-8
8.
Figure 5-9
Back in the Interface Builder Library, click Classes at the top and scroll to and select your SimpleTouchView class. At the bottom, choose the Outlets button. Click the + and add the following
outlets, as shown in Figure 5-10: ➤➤
imageView (as a UIImageView instead of id type)
166
❘ Chapter 5 Touch Events
9.
➤➤
tapCountLabel (as a UILabel instead of id type)
➤➤
touchCountLabel (as a UILabel instead of id type)
➤➤
touchPhaseLabel (as a UILabel instead of id type)
From the main menu of Interface Builder, choose File ➪ Write Class Files, and select Save from the first pop-up and then select Merge from the second pop-up. Close the SimpleTouchView.m window, as there are no changes. The SimpleTouchView.h file appears with your new additions on the left and the original template on the right (see Figure 5-11). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge from the menu and close the window.
Figure 5-10
10.
Figure 5-11
You now have an Objective-C template that holds your application’s view logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UIImageView as imageView.
➤➤
Identify the UILabel as tapCountLabel.
➤➤
Identify the UILabel as touchCountLabel.
➤➤
Identify the UILabel as touchPhaseLabel.
A Simple Touch Handler
❘ 167
To make the connection to identify the UIImageView as imageView, Control-click on the view on which you placed the labels and imageView to bring up the Inspector (see Figure 5-12).
11.
From the right of the SimpleTouchView Inspector, control-drag from the circle to the UIImageView imageView until it is highlighted, then release the mouse. You will see that the circle is filled, indicating the connection has been made (see Figure 5-13). Repeat this for the following, choose File ➪ Save, and dismiss the SimpleTouchView Inspector: ➤➤
UILabel touchPhaseLabel to the top label
➤➤
UILabel tapCountLabel to the middle label
➤➤
UILabel touchCountLabel to the bottom label
Figure 5-12
Figure 5-13
Now it is time to enter your logic.
Source Code Listings for a Simple Touch Handler For this application, the SimpleTouch_iPhoneAppDelegate.h and SimpleTouch_iPhoneAppDelegate.m files are not modified and are used as generated.
SimpleTouchView.h Modifications to the Template You declared the following outlets in Interface Builder: ➤➤
imageView
➤➤
tapCountLabel
➤➤
touchCountLabel
➤➤
touchPhaseLabel
You must now define the properties for these variables in order to get and set their values (see Listing 5-1). The IBOutlet was moved to the property declaration, and the following were also added: ➤➤
SystemSoundID _soundID
➤➤
updateDisplayValuesWithPhase:tapCount:touchCount:
➤➤
moveView:toPosition
➤➤
loadShakeSound
Listing 5-1: The complete SimpleTouchView.h file (Chapter5/SimpleTouch-iPhone/Classes/
SimpleTouchView.h)
#import #import @interface SimpleTouchView : UIView { UILabel *tapCountLabel; UILabel *touchCountLabel;
168
❘ Chapter 5 Touch Events
UILabel *touchPhaseLabel; UIImageView *imageView; SystemSoundID _soundID; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
IBOutlet IBOutlet IBOutlet IBOutlet
UILabel *tapCountLabel; UILabel *touchCountLabel; UILabel *touchPhaseLabel; UIImageView *imageView;
- (void)updateDisplayValuesWithPhase:(NSString *)phase tapCount:(int)tapCount touchCount:(int)touchCount; - (void)moveView:(UIView *)theView toPosition:(CGPoint)position; - (void)loadShakeSound; @end
SimpleTouchView.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the SimpleTouchView.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 5-2). Listing 5-2: Addition of @synthesize #import “SimpleTouchView.h”
@implementation SimpleTouchView @synthesize @synthesize @synthesize @synthesize
tapCountLabel; touchCountLabel; touchPhaseLabel; imageView;
When the app launches, three things need to happen: ➤➤
The default image and sound file for the device shake motion has to be loaded.
➤➤
The display values must be initialized.
➤➤
The SimpleTouchView class must be added to the NSNotificationCenter (see Listing 5-3).
Listing 5-3: The awakeFromNib, updateDisplayValuesWithPhase, and loadShakeSound methods # pragma mark # pragma mark Initialization routines - (void)awakeFromNib { [imageView setImage:[UIImage imageNamed:@”default.png”]]; [self updateDisplayValuesWithPhase:@”“ tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] addObserver:self
A Simple Touch Handler
selector:@selector(resetImageInView) name:@”shakeDevice” object:nil]; [self becomeFirstResponder]; [self loadShakeSound]; } - (void)updateDisplayValuesWithPhase:(NSString *)phase tapCount:(int)tapCount touchCount:(int)touchCount { [touchPhaseLabel setText: [NSString stringWithFormat:@”Touch Phase: %@”, phase]]; [tapCountLabel setText: [NSString stringWithFormat:@”Tap Count: %d”, tapCount]]; [touchCountLabel setText: [NSString stringWithFormat:@”Touch Count: %d”, touchCount]]; } - (void)loadShakeSound { NSString *path = [[NSBundle mainBundle] pathForResource:@”Boing” ofType:@”caf”]; if(path != nil) { NSURL *aFileURL = [NSURL fileURLWithPath:path isDirectory:NO]; if(aFileURL != nil) { SystemSoundID aSoundID; OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)aFileURL, &aSoundID); if (error == kAudioServicesNoError) { _soundID = aSoundID; } else { NSLog(@”Error %d loading sound at path: %@”, error, path); } } } }
To handle the actual touch events, the following methods are required (see Listing 5-4): ➤➤
touchesBegan:withEvent
➤➤
touchesMoved:withEvent
➤➤
touchesEnded:withEvent
➤➤
touchesCancelled:withEvent
Listing 5-4: The touch event processing methods # pragma mark # pragma mark Touch routines - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesBegan” tapCount:[[touches anyObject] tapCount] touchCount:[touches count]]; for (UITouch *touch in touches) { [self moveView:[touch view] toPosition:[touch locationInView:self]]; } [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10];
❘ 169
170
❘ Chapter 5 Touch Events
[imageView setTransform:CGAffineTransformMakeScale(1.2, 1.2)]; [UIView commitAnimations]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesMoved” tapCount:[[touches anyObject] tapCount] touchCount:[touches count]]; for (UITouch *touch in touches) { [self moveView:[touch view] toPosition:[touch locationInView:self]]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesEnded” tapCount:0 touchCount:0]; for (UITouch *touch in touches) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setCenter:[touch locationInView:self]]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesCancelled” tapCount:0 touchCount:0]; }
The image returns to the center of the view when the device is shaken. The class that will process the motion methods must become firstResponder by returning YES to the canBecomeFirstResponder method. The following methods, shown in Listing 5-5, are used to process motion events: ➤➤
motionBegan:withEvent
➤➤
motionEnded:withEvent
➤➤
motionCancelled:withEvent
Listing 5-5: The motion event processing methods # pragma mark # pragma mark Motion routines - (BOOL)canBecomeFirstResponder { return YES; } - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { } - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (motion == UIEventSubtypeMotionShake ) { AudioServicesPlaySystemSound(_soundID); [self updateDisplayValuesWithPhase:@”Shake Device”
A Simple Touch Handler
❘ 171
tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] postNotificationName:@”shakeDevice” object:self]; } } - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event { }
The SimpleTouchView.m file is now complete. Listing 5-6 shows the complete implementation. Listing 5-6: The complete SimpleTouchView.m file (Chapter5/SimpleTouch-iPhone/Classes/ SimpleTouchView.m) #import “SimpleTouchView.h”
@implementation SimpleTouchView @synthesize @synthesize @synthesize @synthesize
tapCountLabel; touchCountLabel; touchPhaseLabel; imageView;
# pragma mark # pragma mark Initialization routines - (void)awakeFromNib { [imageView setImage:[UIImage imageNamed:@”default.png”]]; [self updateDisplayValuesWithPhase:@”“ tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetImageInView) name:@”shakeDevice” object:nil]; [self becomeFirstResponder]; [self loadShakeSound]; } # pragma mark # pragma mark Touch routines - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesBegan” tapCount:[[touches anyObject] tapCount] touchCount:[touches count]]; for (UITouch *touch in touches) { [self moveView:[touch view] toPosition:[touch locationInView:self]]; } [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setTransform:CGAffineTransformMakeScale(1.2, 1.2)];
172
❘ Chapter 5 Touch Events
[UIView commitAnimations]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesMoved” tapCount:[[touches anyObject] tapCount] touchCount:[touches count]]; for (UITouch *touch in touches) { [self moveView:[touch view] toPosition:[touch locationInView:self]]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesEnded” tapCount:0 touchCount:0]; for (UITouch *touch in touches) { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setCenter:[touch locationInView:self]]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self updateDisplayValuesWithPhase:@”touchesCancelled” tapCount:0 touchCount:0]; } # pragma mark # pragma mark Motion routines - (BOOL)canBecomeFirstResponder { return YES; } - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { } - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (motion == UIEventSubtypeMotionShake ) { AudioServicesPlaySystemSound(_soundID); [self updateDisplayValuesWithPhase:@”Shake Device” tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] postNotificationName:@”shakeDevice” object:self]; } } - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event {
A Simple Touch Handler
} # pragma mark # pragma mark Support routines - (void)updateDisplayValuesWithPhase:(NSString *)phase tapCount:(int)tapCount touchCount:(int)touchCount { [touchPhaseLabel setText: [NSString stringWithFormat:@”Touch Phase: %@”, phase]]; [tapCountLabel setText: [NSString stringWithFormat:@”Tap Count: %d”, tapCount]]; [touchCountLabel setText: [NSString stringWithFormat:@”Touch Count: %d”, touchCount]]; } - (void)loadShakeSound { NSString *path = [[NSBundle mainBundle] pathForResource:@”Boing” ofType:@”caf”]; if(path != nil) { NSURL *aFileURL = [NSURL fileURLWithPath:path isDirectory:NO]; if(aFileURL != nil) { SystemSoundID aSoundID; OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)aFileURL, &aSoundID); if (error == kAudioServicesNoError) { _soundID = aSoundID; } else { NSLog(@”Error %d loading sound at path: %@”, error, path); } } } } -(void)moveView:(UIView *)theView toPosition:(CGPoint)position { [imageView setCenter:position]; } - (void)resetImageInView { [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.10]; [imageView setCenter:[self center]]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } # pragma mark # pragma mark Memory routines - (void)dealloc { [super dealloc]; [imageView release]; [tapCountLabel release]; [touchCountLabel release]; [touchPhaseLabel release]; } @end
❘ 173
174
❘ Chapter 5 Touch Events
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of this topic.
A Simple Gesture Recognizer In this application, an image is centered in the display. Users will have the following gestures available to them: ➤➤
Single tap
➤➤
Double tap
➤➤
Pinch
➤➤
Rotate
➤➤
Swipe
➤➤
Pan or drag
When users tap twice with their finger, the status indicates the handleDoubleTap event, as shown in Figure 5-14. When the user swipes the image down, the status indicates the UISwipeGestureRecognizerDirectionDown event, as shown in Figure 5-15.
Figure 5-14
Figure 5-15
A Simple Gesture Recognizer
❘ 175
To reset the image, the user shakes the device. To simulate the device being shaken, select Hardware ➪ Shake Gesture (see Figure 5-16) from the simulator main menu.
Figure 5-16
Development Steps: A Simple Gesture Recognizer To create an application that uses the UIGestureRecognizer class to recognize touch events, execute the following steps:
1.
Start Xcode and create a Window-based application for the iPad , and name it SimpleGestures-iPad. If you need to see this step, please see Appendix A for the steps to begin a Window-based application.
2.
Choose File ➪ New File… and select Objective-C class as subclass of UIView as shown in Figure 5-17, and name the class SimpleGesturesView.
3.
You need to add one image and one sound file to the project: ➤➤
Select Resources in Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your image, default.png, and click Add (see Figure 5-18).
176
❘ Chapter 5 Touch Events
➤➤
Check Copy items into destination group’s folder (see Figure 5-19), and click Add.
➤➤
To add the sound file, take any one-second mp3 file and rename it Boing.mp3.
➤➤
You need to convert the mp3 file to a caf file. To do so, type the following in a Terminal window (/Applications/Utilities/Terminal.app): /usr/bin/afconvert -f caff -d LEI16 Boing.mp3 Boing.caf
Figure 5-17
Figure 5-18
Figure 5-19
A Simple Gesture Recognizer
4.
5.
➤➤
Select Resources in Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your sound file, Boing.caf, and click Add.
➤➤
Check Copy items into destination group’s folder, and click Add.
❘ 177
You need to add one framework to the project: ➤➤
Select Frameworks in Xcode’s Groups & Files window on the left.
➤➤
Control-click and choose Add ➪ Existing Frameworks.
➤➤
Select AudioToolbox.framework and click Add.
Double-click the MainWindow.xib file to launch Interface Builder (see Figure 5-20).
Figure 5-20
6.
From the Interface Builder Library (Tools ➪ Library), choose the following and drag them to the View window. Your interface should now look like Figure 5-21: ➤➤
One UIView that will cover the entire view. Choose Tools ➪ Identity Inspector, and choose SimpleTouchView as the class.
➤➤
Three UILabels placed under each other starting from the top of the view.
➤➤
Two UISegmentedControls placed next to each other starting from under the labels. Label them as follows:
1. 2.
Double-click the left segment on the left segmented control and label it Pinch. Double-click the right segment on the left segmented control and label it Rotate.
178
❘ Chapter 5 Touch Events
3. 4.
Double-click the left segment on the right segmented control and label it Swipe. Double-click the right segment on the right segmented control and label it Pan.
One UIImageView with the size of 150 × 150. To accomplish this, do the following:
➤➤
Drag your UIImageView to the View window below the three labels you just added.
1. 2.
3.
Change the size of the view to W:150 H:150 and center it on the view.
Choose Tools ➪ Size Inspector from the main menu and you will see W:240 W:128 just under the Frame drop-down in the upper-right corner of the inspector.
Figure 5-21
A Simple Gesture Recognizer
7.
Back in the Interface Builder Library, click Classes at the top and scroll to and select your SimpleGesturesView class. At the bottom, choose the Outlets button. Click the + and add the follow-
ing outlets, as shown in Figure 5-22:
8.
❘ 179
➤➤
imageView (as a UIImageView instead of id type)
➤➤
pinchRotateSegmentedControl (as a UISegmentedControl instead of id type)
➤➤
swipePanSegmentedControl (as a UISegmentedControl instead of id type)
➤➤
tapCountLabel (as a UILabel instead of id type)
➤➤
touchCountLabel (as a UILabel instead of id type)
➤➤
touchPhaseLabel (as a UILabel instead of id type)
Choose the Actions button. Then click the + and add the following actions, as shown in Figure 5-23: ➤➤
choosePinchOrRotate
➤➤
chooseSwipeOrDrag
Figure 5-22
Figure 5-23
180
❘ Chapter 5 Touch Events
9.
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first popup and then select Merge from the second pop-up. The SimpleGesturesView.h file appears with your new additions on the left, and the original template is on the right, as shown in Figure 5-24. ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge from the main menu and close the window .
Figure 5-24
10.
For the next window, SimpleGesturesView.m, your new addition is on the left and the original template is on the right, as shown in Figure 5-25. ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge from the main menu and close the window.
A Simple Gesture Recognizer
❘ 181
Figure 5-25
11.
You now have an Objective-C template that holds your application’s view logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UIImageView as imageView.
➤➤
Identify the UISegmentedControl as pinchRotateSegmentedControl.
➤➤
Identify the UISegmentedControl as swipePanSegmentedControl.
➤➤
Identify the UILabel as tapCountLabel.
➤➤
Identify the UILabel as touchCountLabel.
➤➤
Identify the UILabel as touchPhaseLabel.
To make the connection to identify the UIImageView as imageView, control-click on the view on which you placed the labels and imageView to bring up the Inspector (see Figure 5-26).
12.
From the right of the SimpleGesturesView Inspector, control-drag from the circle to the UIImageView imageView until it is highlighted, then release the mouse. The circle is filled, indicating that the
182
❘ Chapter 5 Touch Events
connection has been made. Repeat this for the following, and then choose File ➪ Save and dismiss the SimpleGesturesView Inspector (see Figure 5-27). ➤➤
UILabel touchPhaseLabel to the top label
➤➤
UILabel tapCountLabel to the middle label
➤➤
UILabel touchCountLabel to the bottom label
➤➤
UISegmentedControl pinchRotateSegmentedControl to the left segmented control
➤➤
UISegmentedControl swipePanSegmentedControl to the right segmented control
➤➤
choosePinchOrRotate action to the left segmented control and click ValueChanged
➤➤
chooseSwipeOrDrag action to the left segmented control and click ValueChanged
Figure 5-26
Figure 5-27
Now it is time to enter your logic.
Source Code Listings for a Simple Gesture Recognizer For this application, the SimpleGestures_iPadAppDelegate.h and SimpleGestures_ iPadAppDelegate.m files are not modified and are used as generated.
SimpleTouchView.h Modifications to the Template You declared the following outlets in Interface Builder: ➤➤
imageView
➤➤
pinchRotateSegmentedControl
➤➤
swipePanSegmentedControl
➤➤
tapCountLabel
➤➤
touchCountLabel
➤➤
touchPhaseLabel
You must now define the properties for these variables in order to get and set their values (see Listing 5-7). The IBOutlet was moved to the property declaration, and the following were also added: ➤➤
SystemSoundID _soundID
➤➤
updateDisplayValuesWithPhase:tapCount:touchCount:
➤➤
loadShakeSound
➤➤
createGestureRecognizers
A Simple Gesture Recognizer
➤➤
createTapGestureRecognizer:
➤➤
createDoubleTapGestureRecognizer:
➤➤
createPinchGestureRecognizer:
➤➤
createSwipeGestureRecognizer:direction:
➤➤
createRotateGestureRecognizer:
➤➤
createPanGestureRecognizer:
Listing 5-7: The complete SimpleGesturesView.h file (Chapter5/SimpleGestures-iPad/
Classes/SimpleGesturesView.h)
#import #import
@interface SimpleGesturesView : UIView { UILabel *tapCountLabel; UILabel *touchCountLabel; UILabel *touchPhaseLabel; UIImageView *imageView; UISegmentedControl *pinchRotateSegmentedControl; UISegmentedControl *swipePanSegmentedControl; UITapGestureRecognizer *tapRecognizer; UIRotationGestureRecognizer *rotateRecognizer; UIPinchGestureRecognizer *pinchRecognizer; UISwipeGestureRecognizer *swipeRecognizer; UIPanGestureRecognizer *panRecognizer; CGPoint previousTranslate; SystemSoundID _soundID; } @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain) retain)
IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet
UILabel *tapCountLabel; UILabel *touchCountLabel; UILabel *touchPhaseLabel; UIImageView *imageView; UISegmentedControl *pinchRotateSegmentedControl; @property (nonatomic, retain) IBOutlet UISegmentedControl *swipePanSegmentedControl; @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain) retain)
UITapGestureRecognizer *tapRecognizer; UIRotationGestureRecognizer *rotateRecognizer; UIPinchGestureRecognizer *pinchRecognizer; UISwipeGestureRecognizer *swipeRecognizer; UIPanGestureRecognizer *panRecognizer;
- (void)updateDisplayValuesWithPhase:(NSString *)phase tapCount:(int)tapCount touchCount:(int)touchCount; -
(void)createGestureRecognizers; (void)createTapGestureRecognizer:(UIView *)view; (void)createDoubleTapGestureRecognizer:(UIView *)view; (void)createPinchGestureRecognizer:(UIView *)view; (void)createSwipeGestureRecognizer:(UIView *)view
❘ 183
184
❘ Chapter 5 Touch Events
direction:(UISwipeGestureRecognizerDirection)direction; - (void)createRotateGestureRecognizer:(UIView *)view; - (void)createPanGestureRecognizer:(UIView *)view; - (void)loadShakeSound; - (IBAction)choosePinchOrRotate:(UISegmentedControl *)aSegmentedControl; - (IBAction)chooseSwipeOrDrag:(UISegmentedControl *)aSegmentedControl; @end
SimpleTouchView.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the SimpleGesturesView.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 5-8). Listing 5-8: Addition of @synthesize #import “SimpleGesturesView.h” @implementation SimpleGesturesView @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
tapCountLabel; touchCountLabel; touchPhaseLabel; imageView; pinchRotateSegmentedControl; swipePanSegmentedControl; tapRecognizer; rotateRecognizer; pinchRecognizer; swipeRecognizer; panRecognizer;
When the app launches, four things need to happen: ➤➤
The default image and sound file for the device shake motion has to be loaded.
➤➤
The gesture recognizers have to be created.
➤➤
The display values must be initialized.
➤➤
The SimpleGesturesView class must be added to the NSNotificationCenter (see Listing 5-9).
Listing 5-9: The awakeFromNib, updateDisplayValuesWithPhase, and loadShakeSound methods # pragma mark # pragma mark Initialization routines - (void)awakeFromNib { [imageView setImage:[UIImage imageNamed:@”default.png”]]; [self updateDisplayValuesWithPhase:@”“ tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetImageInView)
A Simple Gesture Recognizer
name:@”shakeDevice” object:nil]; [self becomeFirstResponder]; [self loadShakeSound]; [self createGestureRecognizers]; } - (void)updateDisplayValuesWithPhase:(NSString *)phase tapCount:(int)tapCount touchCount:(int)touchCount { [touchPhaseLabel setText: [NSString stringWithFormat:@”Touch Phase: %@”, phase]]; [tapCountLabel setText: [NSString stringWithFormat:@”Tap Count: %d”, tapCount]]; [touchCountLabel setText: [NSString stringWithFormat:@”Touch Count: %d”, touchCount]]; } - (void)loadShakeSound { NSString *path = [[NSBundle mainBundle] pathForResource:@”Boing” ofType:@”caf”]; if(path != nil) { NSURL *aFileURL = [NSURL fileURLWithPath:path isDirectory:NO]; if(aFileURL != nil) { SystemSoundID aSoundID; OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)aFileURL, &aSoundID); if (error == kAudioServicesNoError) { _soundID = aSoundID; } else { NSLog(@”Error %d loading sound at path: %@”, error, path); } } } }
The following methods are used to create the gesture events, as shown in Listing 5-10: ➤➤
UITapGestureRecognizer
➤➤
UIPinchGestureRecognizer
➤➤
UISwipeGestureRecognizer
➤➤
UIRotationGestureRecognizer
➤➤
UIPanGestureRecognizer
Listing 5-10: The gesture event processing methods # pragma mark # pragma mark Gesture Recognizer routines - (void)createGestureRecognizers { [self createTapGestureRecognizer:self]; [self createDoubleTapGestureRecognizer:self]; [self createPinchGestureRecognizer:self]; [self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionLeft]; [self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionRight];
❘ 185
186
❘ Chapter 5 Touch Events
[self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionUp]; [self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionDown]; [self createRotateGestureRecognizer:self]; [self createPanGestureRecognizer:self]; } - (void)createTapGestureRecognizer:(UIView *)view { UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapEvent:)]; [view addGestureRecognizer:recognizer]; [self setTapRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer release]; } - (void)createDoubleTapGestureRecognizer:(UIView *)view { UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapEvent:)]; [view addGestureRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer setNumberOfTapsRequired:2]; [recognizer release]; } - (void)createPinchGestureRecognizer:(UIView *)view { UIPinchGestureRecognizer *recognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchEvent:)]; if ([pinchRotateSegmentedControl selectedSegmentIndex] == 0) { [view addGestureRecognizer:recognizer]; } [self setPinchRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer release]; } - (void)createSwipeGestureRecognizer:(UIView *)view direction:(UISwipeGestureRecognizerDirection)direction { UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeEvent:)]; if ([swipePanSegmentedControl selectedSegmentIndex] == 0) { [view addGestureRecognizer:recognizer]; } [recognizer setDirection:direction]; [self setSwipeRecognizer:recognizer]; [recognizer release]; } - (void)createRotateGestureRecognizer:(UIView *)view { UIRotationGestureRecognizer *recognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotateEvent:)]; if ([pinchRotateSegmentedControl selectedSegmentIndex] == 1) { [view addGestureRecognizer:recognizer]; }
A Simple Gesture Recognizer
[self setRotateRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer release]; } - (void)createPanGestureRecognizer:(UIView *)view { UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanEvent:)]; if ([swipePanSegmentedControl selectedSegmentIndex] == 1) { [view addGestureRecognizer:recognizer]; } [recognizer setDelegate:self]; [self setPanRecognizer:recognizer]; [recognizer release]; }
Listing 5-11 shows the following methods, which are used to perform the actions related to each of the GestureRecognizer events: ➤➤
handleTapEvent for UITapGestureRecognizer
➤➤
handleDoubleTapEvent for UITapGestureRecognizer
➤➤
handlePinchEvent for UIPinchGestureRecognizer
➤➤
handleSwipeEvent for UISwipeGestureRecognizer
➤➤
handleRotateEvent for UIRotationGestureRecognizer
➤➤
handlePanEvent for UIPanGestureRecognizer
Listing 5-11: The event action methods # pragma mark # pragma mark Gesture action routines - (void)handleTapEvent:(UITapGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handleTapEvent” tapCount:1 touchCount:1]; CGPoint location = [recognizer locationInView:self]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setCenter:location]; [UIView commitAnimations]; } - (void)handleDoubleTapEvent:(UITapGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handleDoubleTapEvent” tapCount:2 touchCount:1]; CGPoint location = [recognizer locationInView:self]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setCenter:location]; [UIView commitAnimations]; } - (void)handlePinchEvent:(UITapGestureRecognizer *)recognizer {
❘ 187
188
❘ Chapter 5 Touch Events
[self updateDisplayValuesWithPhase:@”handlePinchEvent” tapCount:1 touchCount:2]; CGFloat scale = [(UIPinchGestureRecognizer *)recognizer scale]; if ([recognizer state] == UIGestureRecognizerStateEnded) { previousTranslate = CGPointMake(0, 0); [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } else { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setTransform:CGAffineTransformMakeScale(scale, scale)]; [UIView commitAnimations]; } } - (void)handleSwipeEvent:(UISwipeGestureRecognizer *)recognizer { NSString *swipeDirection = nil; CGPoint location = [recognizer locationInView:self]; [imageView setCenter:location]; if([recognizer direction] == UISwipeGestureRecognizerDirectionLeft) swipeDirection = @”UISwipeGestureRecognizerDirectionLeft”; location.x -= 250.0; } else if([recognizer direction] == UISwipeGestureRecognizerDirectionRight) swipeDirection = @”UISwipeGestureRecognizerDirectionRight”; location.x += 250.0; } else if(([recognizer direction] == UISwipeGestureRecognizerDirectionUp)) swipeDirection = @”UISwipeGestureRecognizerDirectionUp”; location.y -= 250.0; } else if(([recognizer direction] == UISwipeGestureRecognizerDirectionDown)) swipeDirection = @”UISwipeGestureRecognizerDirectionDown”; location.y += 250.0; } if(swipeDirection != nil) { [self updateDisplayValuesWithPhase:swipeDirection tapCount:1 touchCount:1]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.50]; [imageView setCenter:location]; [UIView commitAnimations]; } } - (void)handleRotateEvent:(UIRotationGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handleRotateEvent” tapCount:1 touchCount:2]; CGPoint location = [recognizer locationInView:self]; [imageView setTransform:
{
{
{
{
A Simple Gesture Recognizer
❘ 189
CGAffineTransformMakeRotation([recognizer rotation])]; [imageView setCenter:location]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.50]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } - (void)handlePanEvent:(UIPanGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handlePanEvent” tapCount:1 touchCount:1]; CGPoint location = [recognizer locationInView:self]; CGPoint translate = [recognizer translationInView:imageView]; CGRect newFrame = imageView.frame; newFrame.origin.x += (translate.x - previousTranslate.x); newFrame.origin.y += (translate.y - previousTranslate.y); imageView.frame = newFrame; previousTranslate = translate; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; if ([recognizer state] == UIGestureRecognizerStateEnded) { previousTranslate = CGPointMake(0, 0); [imageView setTransform:CGAffineTransformIdentity]; } else if([recognizer state] == UIGestureRecognizerStateBegan) { [imageView setCenter:location]; [imageView setTransform:CGAffineTransformMakeScale(1.5, 1.5)]; } [UIView commitAnimations]; }
The image returns to the center of the view when the device is shaken. The class that will process the motion methods must become firstResponder by returning YES to the canBecomeFirstResponder method. The following methods, shown in Listing 5-12, are used to process motion events: ➤➤
motionBegan:withEvent
➤➤
motionEnded:withEvent
➤➤
motionCancelled:withEvent
Listing 5-12: The motion event processing methods # pragma mark # pragma mark Motion routines - (BOOL)canBecomeFirstResponder { return YES; } - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { } - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (motion == UIEventSubtypeMotionShake ) { AudioServicesPlaySystemSound(_soundID);
190
❘ Chapter 5 Touch Events
[self updateDisplayValuesWithPhase:@”Shake Device” tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] postNotificationName:@”shakeDevice” object:self]; } } - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event { }
The SimpleGesturesView.m file is now complete. Listing 5-13 shows the complete implementation. Listing 5-13: The complete SimpleGesturesView.m file (Chapter5/SimpleGestures-iPad/ Classes/SimpleGesturesView.m) #import “SimpleGesturesView.h”
@implementation SimpleGesturesView @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
tapCountLabel; touchCountLabel; touchPhaseLabel; imageView; pinchRotateSegmentedControl; swipePanSegmentedControl; tapRecognizer; rotateRecognizer; pinchRecognizer; swipeRecognizer; panRecognizer;
# pragma mark # pragma mark Initialization routines - (void)awakeFromNib { [imageView setImage:[UIImage imageNamed:@”default.png”]]; [self updateDisplayValuesWithPhase:@”“ tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetImageInView) name:@”shakeDevice” object:nil]; [self becomeFirstResponder]; [self loadShakeSound]; [self createGestureRecognizers]; } # pragma mark # pragma mark Gesture Recognizer routines /* This method is where all the GestureRecognizers are created */ - (void)createGestureRecognizers {
A Simple Gesture Recognizer
[self createTapGestureRecognizer:self]; [self createDoubleTapGestureRecognizer:self]; [self createPinchGestureRecognizer:self]; [self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionLeft]; [self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionRight]; [self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionUp]; [self createSwipeGestureRecognizer:self direction:UISwipeGestureRecognizerDirectionDown]; [self createRotateGestureRecognizer:self]; [self createPanGestureRecognizer:self]; } /* This method is where the Single Tap recognizer is created */ - (void)createTapGestureRecognizer:(UIView *)view { UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapEvent:)]; [view addGestureRecognizer:recognizer]; [self setTapRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer release]; } /* This method is where the Double Tap recognizer is created NOTE: the setting of setNumberOfTapsRequired:2, this makes it handle two taps */ - (void)createDoubleTapGestureRecognizer:(UIView *)view { UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapEvent:)]; [view addGestureRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer setNumberOfTapsRequired:2]; [recognizer release]; } /* This method is where the Two finger pinch recognizer is created */ - (void)createPinchGestureRecognizer:(UIView *)view { UIPinchGestureRecognizer *recognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchEvent:)]; if ([pinchRotateSegmentedControl selectedSegmentIndex] == 0) { [view addGestureRecognizer:recognizer]; } [self setPinchRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer release]; } /*
❘ 191
192
❘ Chapter 5 Touch Events
This method is where the Single finger swipe recognizer is created */
- (void)createSwipeGestureRecognizer:(UIView *)view direction:(UISwipeGestureRecognizerDirection)direction { UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeEvent:)]; if ([swipePanSegmentedControl selectedSegmentIndex] == 0) { [view addGestureRecognizer:recognizer]; } [recognizer setDirection:direction]; [self setSwipeRecognizer:recognizer]; [recognizer release]; } /* This method is where the Two finger rotate recognizer is created */ - (void)createRotateGestureRecognizer:(UIView *)view { UIRotationGestureRecognizer *recognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotateEvent:)]; if ([pinchRotateSegmentedControl selectedSegmentIndex] == 1) { [view addGestureRecognizer:recognizer]; } [self setRotateRecognizer:recognizer]; [recognizer setDelegate:self]; [recognizer release]; } /* This method is where the Pan or Drag recognizer is created */ - (void)createPanGestureRecognizer:(UIView *)view { UIPanGestureRecognizer *recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanEvent:)]; if ([swipePanSegmentedControl selectedSegmentIndex] == 1) { [view addGestureRecognizer:recognizer]; } [recognizer setDelegate:self]; [self setPanRecognizer:recognizer]; [recognizer release]; } # pragma mark # pragma mark Gesture action routines /* This method is where the Single Tap event is handled */ - (void)handleTapEvent:(UITapGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handleTapEvent” tapCount:1
A Simple Gesture Recognizer
touchCount:1]; CGPoint location = [recognizer locationInView:self]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setCenter:location]; [UIView commitAnimations]; } /* This method is where the Double Tap event is handled */ - (void)handleDoubleTapEvent:(UITapGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handleDoubleTapEvent” tapCount:2 touchCount:1]; CGPoint location = [recognizer locationInView:self]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setCenter:location]; [UIView commitAnimations]; } /* This method is where the Two finger pinch event is handled */ - (void)handlePinchEvent:(UITapGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handlePinchEvent” tapCount:1 touchCount:2]; CGFloat scale = [(UIPinchGestureRecognizer *)recognizer scale]; if ([recognizer state] == UIGestureRecognizerStateEnded) { previousTranslate = CGPointMake(0, 0); [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } else { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; [imageView setTransform:CGAffineTransformMakeScale(scale, scale)]; [UIView commitAnimations]; } } /* This method is where the Single finger swipe event is handled NOTE: the direction of the swipe is also identified */ - (void)handleSwipeEvent:(UISwipeGestureRecognizer *)recognizer { NSString *swipeDirection = nil; CGPoint location = [recognizer locationInView:self];
❘ 193
194
❘ Chapter 5 Touch Events
[imageView setCenter:location]; if([recognizer direction] == UISwipeGestureRecognizerDirectionLeft) swipeDirection = @”UISwipeGestureRecognizerDirectionLeft”; location.x -= 250.0; } else if([recognizer direction] == UISwipeGestureRecognizerDirectionRight) swipeDirection = @”UISwipeGestureRecognizerDirectionRight”; location.x += 250.0; } else if(([recognizer direction] == UISwipeGestureRecognizerDirectionUp)) swipeDirection = @”UISwipeGestureRecognizerDirectionUp”; location.y -= 250.0; } else if(([recognizer direction] == UISwipeGestureRecognizerDirectionDown)) swipeDirection = @”UISwipeGestureRecognizerDirectionDown”; location.y += 250.0; } if(swipeDirection != nil) { [self updateDisplayValuesWithPhase:swipeDirection tapCount:1 touchCount:1]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.50]; [imageView setCenter:location]; [UIView commitAnimations]; } }
{
{
{
{
/* This method is where the Rotate event is handled */ - (void)handleRotateEvent:(UIRotationGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handleRotateEvent” tapCount:1 touchCount:2]; CGPoint location = [recognizer locationInView:self]; [imageView setTransform: CGAffineTransformMakeRotation([recognizer rotation])]; [imageView setCenter:location]; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.50]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } /* This method is where the Pan or Drag event is handled */ - (void)handlePanEvent:(UIPanGestureRecognizer *)recognizer { [self updateDisplayValuesWithPhase:@”handlePanEvent” tapCount:1 touchCount:1]; CGPoint location = [recognizer locationInView:self];
A Simple Gesture Recognizer
CGPoint translate = [recognizer translationInView:imageView]; CGRect newFrame = imageView.frame; newFrame.origin.x += (translate.x - previousTranslate.x); newFrame.origin.y += (translate.y - previousTranslate.y); imageView.frame = newFrame; previousTranslate = translate; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.10]; if ([recognizer state] == UIGestureRecognizerStateEnded) { previousTranslate = CGPointMake(0, 0); [imageView setTransform:CGAffineTransformIdentity]; } else if([recognizer state] == UIGestureRecognizerStateBegan) { [imageView setCenter:location]; [imageView setTransform:CGAffineTransformMakeScale(1.5, 1.5)]; } [UIView commitAnimations]; } # pragma mark # pragma mark Motion routines /* This section handles the shaking of the device It is important to make sure you have all three methods listed, even if they dont do anything. */ - (BOOL)canBecomeFirstResponder { return YES; } - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { } - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (motion == UIEventSubtypeMotionShake ) { AudioServicesPlaySystemSound(_soundID); [self updateDisplayValuesWithPhase:@”Shake Device” tapCount:0 touchCount:0]; [[NSNotificationCenter defaultCenter] postNotificationName:@”shakeDevice” object:self]; } } - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event { } # pragma mark # pragma mark Support routines - (void)updateDisplayValuesWithPhase:(NSString *)phase tapCount:(int)tapCount touchCount:(int)touchCount { [touchPhaseLabel setText: [NSString stringWithFormat:@”Touch Phase: %@”, phase]];
❘ 195
196
❘ Chapter 5 Touch Events
[tapCountLabel setText: [NSString stringWithFormat:@”Tap Count: %d”, tapCount]]; [touchCountLabel setText: [NSString stringWithFormat:@”Touch Count: %d”, touchCount]]; } - (void)loadShakeSound { NSString *path = [[NSBundle mainBundle] pathForResource:@”Boing” ofType:@”caf”]; if(path != nil) { NSURL *aFileURL = [NSURL fileURLWithPath:path isDirectory:NO]; if(aFileURL != nil) { SystemSoundID aSoundID; OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)aFileURL, &aSoundID); if (error == kAudioServicesNoError) { _soundID = aSoundID; } else { NSLog(@”Error %d loading sound at path: %@”, error, path); } } } } - (void)resetImageInView { [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.10]; [imageView setCenter:[self center]]; [imageView setTransform:CGAffineTransformIdentity]; [UIView commitAnimations]; } # pragma mark # pragma mark Action routines /* This section identifies which choice from the segmented controls are selected. The two segmented controls determine: Pinch or Rotate and Swipe or Drag */ - (IBAction)choosePinchOrRotate:(UISegmentedControl *)aSegmentedControl { if ([aSegmentedControl selectedSegmentIndex] == 0) { [self addGestureRecognizer:pinchRecognizer]; [self removeGestureRecognizer:rotateRecognizer]; } else { [self addGestureRecognizer:rotateRecognizer]; [self removeGestureRecognizer:pinchRecognizer]; } } - (IBAction)chooseSwipeOrDrag:(UISegmentedControl *)aSegmentedControl { if ([aSegmentedControl selectedSegmentIndex] == 0) { [self addGestureRecognizer:swipeRecognizer]; [self removeGestureRecognizer:panRecognizer]; } else { [self addGestureRecognizer:panRecognizer];
Summary
❘ 197
[self removeGestureRecognizer:swipeRecognizer]; } } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if ((touch.view == pinchRotateSegmentedControl) && (gestureRecognizer == tapRecognizer)) { return NO; } if ((touch.view == swipePanSegmentedControl) && (gestureRecognizer == tapRecognizer)) { return NO; } return YES; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } # pragma mark # pragma mark Memory routines - (void)dealloc { [super dealloc]; [imageView release]; [tapCountLabel release]; [touchCountLabel release]; [touchPhaseLabel release]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the "A Simple Gesture Recognizer" section.
Summary In this chapter, two types of touch events were observed: ➤➤
touchesBegan, touchesMoved, touchesEnded and touchesCancelled
➤➤
developer-defined with use of the UIGestureRecognizer class
The first application handled simple touch events with the actions defined by the iOS 4 system. The second application used the UIGestureRecognizer subclass that was introduced in the 3.2 SDK. This subclass not only simplifies gesture recognition, but also allows for custom gestures to be written.
6
notification Processing What’s in this chaPter? ➤➤
Working with notifications
➤➤
How to register to receive notifications
➤➤
The NSNotificationCenter object
➤➤
How to implement a local notification
iOS 4, like any other GUI environment, reflects an infi nite series of events. Touches, swipes, connecting to a network, and the keyboard appearing are just a few examples of events. Notifications are broadcast when these events occur. An application registers to receive these notifications, in order to act when the event occurs. The actual notification is encapsulated in an NSNotification object that will be broadcast. The NSNotification is then broadcast to the NSNotificationCenter. The application that wants to receive the notifications must register with the NSNotificationCenter in order to receive the notifications by adding themselves as an observer:. NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
To find out what notifications are sent by a particular class, look in the documentation for the class; the notification will be listed in the Notifications section. For example, the UIWindow class reference lists: ➤➤
UIWindowDidBecomeVisibleNotification
➤➤
UIWindowDidBecomeHiddenNotification
➤➤
UIWindowDidBecomeKeyNotification
➤➤
UIWindowDidResignKeyNotification
➤➤
UIKeyboardWillShowNotification
➤➤
UIKeyboardDidShowNotification
➤➤
UIKeyboardWillHideNotification
➤➤
UIKeyboardDidHideNotification
200
❘ Chapter 6 Notification Processing
Use the following to register for the UIKeyboardWillShowNotification event: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
Put your handling logic in the keyboardWillShow method that you used in the selector argument: - (void)keyboardWillShow:(NSNotification *)aNotification { CGRect keyboardRect = [[[aNotification userInfo] objectForKey:UIKeyboardBoundsUserInfoKey] CGRectValue]; NSTimeInterval animationDuration = [[[aNotification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; CGRect frame = self.view.frame; frame.size.height -= keyboardRect.size.height; [UIView beginAnimations:@”ResizeForKeyboard” context:nil]; [UIView setAnimationDuration:animationDuration]; self.view.frame = frame; [UIView commitAnimations]; }
The process then is for the application to register for the NSNotification. When the notification is received, there has to be a method to handle the notification. In the preceding example, the application registered to receive the keyboardWillShow. Now when the notification is received, the keyboardWillShow method will be called and will move the view up so that the keyboard does not cover the field that is going to receive the input.
NSNotifications Concepts When an object wants to announce an event, it posts a notification by sending an NSNotification to the NSNotificationCenter. The notification center will then broadcast that the event has occurred to all those objects that registered to receive the notification.
The Notification Center The notification center is a central clearinghouse for event notifications. It is basically a table that contains an observer, and an optional name and optional sender. An observer is required, as it is the object to be notified when the event is posted to the notification center as was shown with the keyboardWillShow notification.
Registering for Local Notifications In order to receive notifications, an object must register with the notification center by using the add Observer:selector:name:object: method: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
Posting Local Notifications When an object wants to announce that an event has occurred, it must post the notification to the notification center by using the postNotification method: [[NSNotificationCenter defaultCenter] postNotificationName:@”setImageBackgroundBlue” object:self];
A Local Named Notification
❘ 201
Unregistering an Observer When an object no longer wants to listen for notifications, it must remove itself from the notification center: [[NSNotificationCenter defaultCenter] removeObserver:self];
If an object no longer wants to receive a specific notification, it may remove itself from the notification center for that event only: [[NSNotificationCenter defaultCenter] removeObserver:self name:AppDataDownloadCompleted object:nil];
A Local Named Notification In this application, the background of a UIImageView can be set to Black or White, depending on which button is pressed. This app will launch and display a UIImageView with a white background and two buttons, Black and White, as shown in Figure 6-1.
When the user selects the Black button, the UIImageView will be set to black as shown in Figure 6-2.
Figure 6-1
Figure 6-2
202
❘ Chapter 6 Notification Processing
Development Steps: A Local Named Notification To create this application, execute the following steps:
1.
Start Xcode and create a View-based application named LocalNamedNotification-iPhone. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
2.
Double-click the LocalNamedNotification_iPhoneViewController.xib file to launch Interface Builder (see Figure 6-3).
Figure 6-3
3.
From the Interface Builder Library (Tools ➪ Library), choose the following and drag them to the View window: One UIImageView sized 280 × 162, placed on the top. To accomplish this do the following:
➤➤
1.
Drag your UIImageView to the View window. By default, it will occupy the entire window, which is fine for now.
2.
Choose Tools ➪ Size Inspector from the main menu and you will see W:320 W:460 just under the Frame drop-down in the upper-right corner of the inspector.
3.
Change the size of the view to W:280 H:162.
➤➤
Two UIButtons
1. 2.
Double-click the left Button and type Black. Double-click the right Button and type White.
A Local Named Notification
❘ 203
Your interface should now look like Figure 6-4.
4.
Back in the Interface Builder Library, click Classes at the top and scroll to and select your LocalNamedNotification_iPhoneViewController class. At the bottom, now choose the Outlets
button. Click the + and add the following outlet, as shown in Figure 6-5: ➤➤
5.
Choose the Actions button. Then click the + and add the following action, as shown in Figure 6-6: ➤➤
6.
colorView (as a UIImageView instead of id type)
buttonAction
From the main menu of Interface Builder, choose File ➪ Write Class Files. Two pop-up windows will appear. Select Save from the first pop-up and then select Merge from the second pop-up. A window will appear with your new additions to LocalNamedNotification_iPhoneViewController.h on the left and the original template on the right (see Figure 6-7).
Figure 6-4
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Now choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge and close the window.
Figure 6-5
Figure 6-6
204
❘ Chapter 6 Notification Processing
7.
For the next window, LocalNamedNotification_iPhoneViewController.m, your new addition is on the left and the original template is on the right (see Figure 6-8). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 6-7
8.
You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UIImageView as colorView
➤➤
Connect the buttonAction actions to the File’s Owner icon
A Local Named Notification
❘ 205
Figure 6-8
To make the connection to identify the UIImageView as colorView, control-click on the File’s Owner icon to bring up the Inspector as shown in Figure 6-9.
9.
10.
From the right of the File’s Owner Inspector, control-drag from the circle to the UIImageView colorView until it is highlighted, then release the mouse. You will see now that the circle is filled, indicating the connection has been made (see Figure 6-10). Dismiss the File’s Owner Inspector.
Figure 6-9
From the Black button, control-drag to the File’s Owner icon, and then release the mouse. The action buttonAction is displayed in the inspector. Click on buttonAction to make the connection between the button and the action, as shown in Figure 6-11. Repeat this for the Blue button, connecting it also to the buttonAction. Select File ➪ Save to save the files.
Now it is time to enter your logic.
206
❘ Chapter 6 Notification Processing
Figure 6-10
Figure 6-11
Source Code Listings for a Local Named Notification For this application, the LocalNamedNotification_iPhoneAppDelegate.m file is modified as follows. When the application launches, two notifications are going to be added to the NSNotificationCenter: ➤➤
setImageBackgroundRed
➤➤
setImageBackgroundBlue
In the application:didFinishLaunchingWithOptions: method, add the following: [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setImageBackgroundRed:) name:@”setImageBackgroundRed” object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setImageBackgroundBlue:) name:@”setImageBackgroundBlue” object:nil];
Now add the setImageBackgroundRed and setImageBackgroundBlue methods (see Listing 6-1).
A Local Named Notification
❘ 207
Listing 6-1: The setImageBackgroundRed and setImageBackgroundBlue methods # pragma mark # pragma mark Notification routines - (void)setImageBackgroundRed:(NSNotification *)note { [[viewController colorView] setBackgroundColor:[UIColor redColor]]; } - (void)setImageBackgroundBlue:(NSNotification *)note { [[viewController colorView] setBackgroundColor:[UIColor blueColor]]; }
When the application terminates or enters into background processing, the application will remove itself from the NSNotification’s defaultCenter and calls applicationDidEnterBackground if it enters the background or applicationWillTerminate if the application is terminating. These two methods are shown in Listing 6-2. Listing 6-2: The applicationDidEnterBackground and applicationWillTerminate methods - (void)applicationDidEnterBackground:(UIApplication *)application { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)applicationWillTerminate:(UIApplication *)application { [[NSNotificationCenter defaultCenter] removeObserver:self]; }
The LocalNamedNotification_iPhoneAppDelegate.m file is now complete. Listing 6-3 shows the complete implementation. Listing 6-3: The complete LocalNamedNotification_iPhoneAppDelegate.m file (Chapter6/ LocalNamedNotification-iPhone/Classes/LocalNamedNotification_iPhoneAppDelegate.m) #import “LocalNamedNotification_iPhoneAppDelegate.h” #import “LocalNamedNotification_iPhoneViewController.h” @implementation LocalNamedNotification_iPhoneAppDelegate @synthesize window; @synthesize viewController;
#pragma mark #pragma mark Application lifecycle - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setImageBackgroundRed:) name:@”setImageBackgroundRed” object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setImageBackgroundBlue:) name:@”setImageBackgroundBlue” object:nil]; // Add the view controller’s view to the window and display. [window addSubview:viewController.view];
208
❘ Chapter 6 Notification Processing
[window makeKeyAndVisible]; return YES; } - (void)applicationWillResignActive:(UIApplication *)application { } - (void)applicationDidEnterBackground:(UIApplication *)application { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)applicationWillEnterForeground:(UIApplication *)application { } - (void)applicationDidBecomeActive:(UIApplication *)application { } - (void)applicationWillTerminate:(UIApplication *)application { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark #pragma mark Memory management - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { } # pragma mark # pragma mark Notification methods - (void)setImageBackgroundRed:(NSNotification *)note { [[viewController colorView] setBackgroundColor:[UIColor redColor]]; } - (void)setImageBackgroundBlue:(NSNotification *)note { [[viewController colorView] setBackgroundColor:[UIColor blueColor]]; } - (void)dealloc { [viewController release]; [window release]; [super dealloc]; } @end
LocalNamedNotification_iPhoneViewController.h Modifications to the Template You declared one outlet colorView in Interface Builder. You must now define the properties for this variable in order to get and set its value (see Listing 6-4). The IBOutlet was moved to the property declaration. Also shown in Listing 6-4, which shows the complete LocalNamedNotification_iPhoneViewController.h file, is a method declared, buttonAction:, which is generated by Interface Builder.
A Local Named Notification
❘ 209
Listing 6-4: The complete LocalNamedNotification_iPhoneViewController .h file (Chapter6/ LocalNamedNotification-iPhone/Classes/ LocalNamedNotification_iPhoneViewController.h) #import @interface LocalNamedNotification_iPhoneViewController : UIViewController { UIImageView *colorView; } @property (nonatomic, retain) IBOutlet UIImageView *colorView; - (IBAction)buttonAction:(id)sender; @end
LocalNamedNotification_iPhoneViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the LocalNamedNotification_iPhoneViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 6-5). Listing 6-5: Addition of @synthesize #import “LocalNamedNotification_iPhoneViewController.h” @implementation LocalNamedNotification_iPhoneViewController @synthesize colorView;
When the user touches the Black or White button, the buttonAction method is called, posting the set ImageBackgroundRed or setImageBackgroundBlue notification, depending on which button was tapped (see Listing 6-6). Listing 6-6: The buttonAction method # pragma mark # pragma mark Notification methods - (IBAction)buttonAction:(id)sender { NSString *command = [sender titleForState:UIControlStateNormal]; if([command isEqualToString:@”White”]) { [[NSNotificationCenter defaultCenter] postNotificationName:@”setImageBackgroundWhite” object:self]; } else if([command isEqualToString:@”Black”]) { [[NSNotificationCenter defaultCenter] postNotificationName:@”setImageBackgroundBlack” object:self]; } }
When the view loads, you want to initialize the colorView. To accomplish that in this case, post the set ImageBackgroundWhite notification in the viewDidLoad method, as shown in Listing 6-7.
210
❘ Chapter 6 Notification Processing
Listing 6-7: The viewDidLoad method - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] postNotificationName:@”setImageBackgroundWhite” object:self]; }
The LocalNamedNotification_iPhoneViewController.m file is now complete. Listing 6-8 shows the complete implementation. Listing 6-8: The complete LocalNamedNotification_iPhoneViewController .m file (Chapter6/ LocalNamedNotification-iPhone/Classes/ LocalNamedNotification_iPhoneViewController.m) #import “LocalNamedNotification_iPhoneViewController.h” @implementation LocalNamedNotification_iPhoneViewController @synthesize colorView;
// Implement viewDidLoad to do additional setup after loading the view, // typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] postNotificationName:@”setImageBackgroundWhite” object:self]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setColorView:nil]; } # pragma mark # pragma mark Notification methods - (IBAction)buttonAction:(id)sender { NSString *command = [sender titleForState:UIControlStateNormal]; if([command isEqualToString:@”White”]) { [[NSNotificationCenter defaultCenter] postNotificationName:@”setImageBackgroundWhite” object:self]; } else if([command isEqualToString:@”Black”]) { [[NSNotificationCenter defaultCenter] postNotificationName:@”setImageBackgroundBlack” object:self]; } } - (void)dealloc { [colorView release]; [super dealloc]; } @end
A Local Keyboard Notification
❘ 211
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the “A Local Named Notification” section.
A Local Keyboard Notification In this application, there are two UITextFields. When the user taps in either of the text fields, the keyboard will appear. In addition, a UIToolbar will be attached to the keyboard. This toolbar will have Next and Previous buttons to switch between text fields, and a Done button to end the text entry. This app will launch and display two UITextFields, as shown in Figure 6-12. When the user selects either of the text fields, the keyboard will appear with the toolbar attached to it, as shown in Figure 6-13.
Figure 6-12
Figure 6-13
212
❘ Chapter 6 Notification Processing
Development Steps: A Local Keyboard Notification To create this application, execute the following steps:
1.
Start Xcode and create a View-based application named LocalNotificationKeyboard-iPhone. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
2.
Double-click the LocalNotificationKeyboard_iPhoneViewController.xib file to launch Interface Builder (see Figure 6-14).
3.
From the Interface Builder Library (Tools ➪ Library), with Objects selected, choose one UIScrollView and drag it to the View window.
4.
Drag one UIView to the controller window right below the File’s Owner icon and release the mouse. Choose Tools ➪ Identity Inspector, and enter ContentView for the name. Your interface should look like Figure 6-15.
5. 6.
Double-click the ContentView to display the view.
Drag two Labels and two UITextFields and place them near the lower section of the window (see Figure 6-16): ➤➤
Double-click the top label and type Entry Field One.
➤➤
Double-click the bottom label and type Entry Field Two.
Figure 6-14
A Local Keyboard Notification
Figure 6-15
7.
Drag one UIToolbar to the controller window next to the File’s Owner icon, and release the mouse.
8. 9.
Double-click the toolbar to display it.
Choose Tools ➪ Attributes Inspector, and select Black Translucent as the style.
10.
Double-click the button labeled Item and rename it Next.
11.
Drag the following to the toolbar (see Figure 6-17):
➤➤
A UIBarButtonItem, and then double-click and type Previous.
➤➤
A Fixed Space Bar Button Item, and stretch it a bit to have a separation away from the other two buttons.
➤➤
A UIBarButtonItem, and then double-click and type Done. While still selected, choose Tools ➪ Attributes Inspector, and choose Done as the style.
Figure 6-17
Figure 6-16
❘ 213
214
❘ Chapter 6 Notification Processing
12.
Back in the Interface Builder Library, click Classes at the top and scroll to and select your LocalNotificationKeyboard_iPhoneViewController class. At the bottom, now choose
the Outlets button. Click the + and add the following outlets, as shown in Figure 6-18:
13.
➤➤
activeField (as a UITextField instead of id type)
➤➤
contentView (as a UIView instead of id type)
➤➤
fieldOneTextField (as a UITextField instead of id type)
➤➤
fieldTwoTextField (as a UITextField instead of id type)
➤➤
nextButton (as a UIBarButtonItem instead of id type)
➤➤
previousButton (as a UIBarButtonItem instead of id type)
➤➤
scrollView (as a UIScrollView instead of id type)
➤➤
toolbar (as a UIToolbar instead of id type)
Choose the Actions button. Then click the + and add the following actions, as shown in Figure 6-19: ➤➤
done
➤➤
next
➤➤
previous
Figure 6-18
Figure 6-19
A Local Keyboard Notification
14.
❘ 215
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save and then Merge. A window will appear with your new additions to LocalNotificationKeyboard_ iPhoneViewController.h on the left, and the original template on the right (see Figure 6-20). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Now choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Now choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge and then close the window.
Figure 6-20
15.
For the next window, LocalNotificationKeyboard_iPhoneViewController.m, your new addition is on the left, and the original template is on the right (see Figure 6-21). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
216
❘ Chapter 6 Notification Processing
Figure 6-21
16.
You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify a UIScrollView as scrollView
➤➤
Identify a UIView as contentView
➤➤
Identify a UITextField as fieldOneTextField
➤➤
Identify a UITextField as fieldTwoTextField
➤➤
Identify a UIToolbar as toolbar
➤➤
Identify a UIBarButtonItem as nextButton
➤➤
Identify a UIBarButtonItem as previousButton
➤➤
Connect the done action to the File’s Owner icon
➤➤
Connect the next action to the File’s Owner icon
➤➤
Connect the previous action to the File’s Owner icon
➤➤
Connect the fieldOneTextField field to the File’s Owner delegate
➤➤
Connect the fieldTwoTextField field to the File’s Owner delegate
A Local Keyboard Notification
17.
❘ 217
To make the connection to identify the UIScrollView as scrollView, control-click on the File’s Owner icon to bring up the Inspector .
18.
From the right of the File’s Owner Inspector, control-drag from the circle to the UIScrollView scrollView until it is highlighted, then release the mouse. You will see now that the circle is filled, indicating the connection has been made (see Figure 6-22).
19.
Repeat for the following: ➤➤
Control-drag from the circle contentView to the UIView with the text fields on it, and release the mouse.
20. 21. 22.
➤➤
Control-drag from the circle fieldOneTextField to the first UITextField and release the mouse.
➤➤
Control-drag from the circle fieldTwoTextField to the second UITextField and release the mouse.
➤➤
Control-drag from the circle nextButton to the Next button on the toolbar and release the mouse.
➤➤
Control-drag from the circle previousButton to the Previous button on the toolbar and release the mouse.
➤➤
Control-drag from the circle toolbar to the Toolbar icon by the File’s Owner icon and release the mouse (see Figure 6-23). Dismiss the File’s Owner Inspector.
From the fieldOneTextField, control-drag to the File’s Owner icon, release the mouse, and click the delegate outlet. Do the same for the fieldTwoTextField field.
Figure 6-22
Figure 6-23
From the Next button, control-drag to the File’s Owner icon, and then release the mouse. The action next is displayed in the inspector. Click next to make the connection between the button and action. Repeat this for the Previous button (to the previous action) and the Done button (to the done action).
Choose File ➪ Save.
Now it is time to enter your logic.
Source Code Listings for a Local Keyboard Notification For this application, the LocalNotificationKeyboard_iPhoneAppDelegate.h and LocalNotification Keyboard_iPhoneAppDelegate.m files are not modified and are used as generated.
LocalNotificationKeyboard_iPhoneAppDelegate.h Modifications to the Template You declared eight outlets in Interface Builder. You must now define the properties for these variables in order to get and set their values. In addition to the variables, the following properties and methods must be added to complete the LocalNotificationKeyboard_iPhoneViewController.h file: (see Listing 6-9): ➤➤
The IBOutlet was moved from the variable declaration to the property declaration.
➤➤
The registerToReceiveKeyboardNotifications method was added.
➤➤
The keyboardShown property was added.
➤➤
The protocol was added.
➤➤
Three methods declared in Interface Builder are: next, previous, and done.
218
❘ Chapter 6 Notification Processing
Listing 6-9: The complete LocalNotificationKeyboard_iPhoneViewController.h file (Chapter6/LocalNotificationKeyboard-iPhone/Classes/LocalNotificationKeyboard_ iPhoneViewController.h) #import @interface LocalNotificationKeyboard_iPhoneViewController : UIViewController { UIView *contentView; UITextField *fieldOneTextField; UITextField *fieldTwoTextField; UIScrollView *scrollView; UIToolbar *toolbar; UIBarButtonItem *previousButton; UIBarButtonItem *nextButton; UITextField *activeField; BOOL keyboardShown; } @property @property @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain) retain) retain) retain)
IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet
UIView *contentView; UITextField *fieldOneTextField; UITextField *fieldTwoTextField; UIScrollView *scrollView; UIToolbar *toolbar; UIBarButtonItem *previousButton; UIBarButtonItem *nextButton;
@property (nonatomic, retain) UITextField *activeField; - (void) registerToReceiveKeyboardNotifications; - (IBAction)done:(id)sender; - (IBAction)next:(id)sender; - (IBAction)previous:(id)sender; @end
LocalNotificationKeyboard_iPhoneAppDelegate.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the LocalNotificationKeyboard_iPhoneAppDelegate.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 6-10). Listing 6-10: Addition of @synthesize #import “LocalNotificationKeyboard_iPhoneViewController.h” @implementation LocalNotificationKeyboard_iPhoneViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
contentView; fieldOneTextField; fieldTwoTextField; scrollView; toolbar; previousButton; nextButton; activeField;
A Local Keyboard Notification
❘ 219
When the view loads, the contentView has to be added to the scrollView in the viewDidLoad method, and the registration for the keyboard notifications is performed in the viewWillAppear method. Listing 6-11) demonstrates these two methods. Listing 6-11: The viewDidLoad and viewDidAppear methods #pragma mark #pragma mark View Initialization methods - (void)viewDidLoad { [super viewDidLoad]; [scrollView addSubview:contentView]; [scrollView setContentSize:[[self view] bounds].size]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; keyboardShown = NO; [self registerToReceiveKeyboardNotifications]; }
In order to receive the notifications when the keyboard is either shown or hidden, registration with the NSNotificationCenter must occur. Listing 6-12 shows the registerToReceiveKeyboardNotifications method. Listing 6-12: The registerToReceiveKeyboardNotifications method #pragma mark #pragma mark event Registration - (void) registerToReceiveKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; }
To attach the toolbar onto the keyboard and slide up the text fields so the keyboard does not cover them, you supply the keyboardWillShow method, as shown in Listing 6-13. Listing 6-13: The keyboardWillShow method - (void)keyboardWillShow:(NSNotification *)aNotification { if (keyboardShown) return; NSDictionary *userInfo = [aNotification userInfo]; NSValue* keyboardOriginValue = [userInfo objectForKey:UIKeyboardFrameEndUserInfoKey]; CGRect keyboardRect = [scrollView convertRect:[ keyboardOriginValue CGRectValue] fromView:nil]; CGFloat keyboardTop = keyboardRect.origin.y;
220
❘ Chapter 6 Notification Processing
CGRect newScrollViewFrame = [[self view] bounds]; newScrollViewFrame.size.height = keyboardTop - scrollView.bounds.origin.y; NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; NSTimeInterval animationDuration; [animationDurationValue getValue:&animationDuration]; [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:animationDuration]; [scrollView setFrame:newScrollViewFrame]; [scrollView scrollRectToVisible:[activeField frame] animated:YES]; [UIView commitAnimations]; keyboardShown = YES; }
To return the text fields when the keyboard is dismissed, you supply the keyboardWillHide method, as shown in Listing 6-14. Listing 6-14: The keyboardWillHide method - (void)keyboardWillHide:(NSNotification *)aNotification { NSDictionary* userInfo = [aNotification userInfo]; NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; NSTimeInterval animationDuration; [animationDurationValue getValue:&animationDuration]; [scrollView setFrame:[[self view] bounds]]; [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:animationDuration]; [UIView commitAnimations]; keyboardShown = NO; }
To ensure that the proper Next and Previous buttons are enabled, and to allow the keyboard to be dismissed, you supply the textFieldShouldBeginEditing and textFieldShouldReturn methods, as shown in Listing 6-15. Listing 6-15: The textFieldShouldBeginEditing and textFieldShouldReturn methods #pragma mark #pragma mark UITextFieldDelegate -(BOOL) textFieldShouldBeginEditing:(UITextField*)textField { if(textField.inputAccessoryView == nil) { textField.inputAccessoryView=toolbar; } activeField = textField; if(activeField == fieldOneTextField) { [nextButton setEnabled:YES]; [previousButton setEnabled:NO];
A Local Keyboard Notification
❘ 221
} else if(activeField == fieldTwoTextField) { [nextButton setEnabled:NO]; [previousButton setEnabled:YES]; } return YES; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; }
Finally, you need to write the next, previous, and done methods, as shown in Listing 6-16. Listing 6-16: The next, previous, and done methods #pragma mark #pragma mark User action methods - (IBAction)done:(id)sender { [activeField resignFirstResponder]; } - (IBAction)next:(id)sender { if(activeField == fieldOneTextField) { [nextButton setEnabled:NO]; [previousButton setEnabled:YES]; [fieldTwoTextField becomeFirstResponder]; } } - (IBAction)previous:(id)sender { if(activeField == fieldTwoTextField) { [nextButton setEnabled:YES]; [previousButton setEnabled:NO]; [fieldOneTextField becomeFirstResponder]; } }
When the application terminates and no longer receives notifications, it must remove itself from the NSNotificationCenter and should be done in the viewDidUnload method as shown in Listing 6-17. Listing 6-17: The viewDidUnload method - (void)viewDidUnload { [self setContentView:nil]; [self setFieldOneTextField:nil]; [self setFieldTwoTextField:nil]; [self setScrollView:nil]; [self setToolbar:nil]; [self setPreviousButton:nil]; [self setNextButton:nil]; [self setActiveField:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self]; }
The LocalNotificationKeyboard_iPhoneViewController.m file is now complete. Listing 6-18 shows the complete implementation.
222
❘ Chapter 6 Notification Processing
Listing 6-18: The complete LocalNotificationKeyboard_iPhoneViewController.m file (Chapter6/LocalNotificationKeyboard-iPhone/Classes/LocalNotificationKeyboard_ iPhoneViewController.m) #import “LocalNotificationKeyboard_iPhoneViewController.h” @implementation LocalNotificationKeyboard_iPhoneViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
contentView; fieldOneTextField; fieldTwoTextField; scrollView; toolbar; previousButton; nextButton; activeField;
#pragma mark #pragma mark User action methods - (IBAction)done:(id)sender { [activeField resignFirstResponder]; } - (IBAction)next:(id)sender { if(activeField == fieldOneTextField) { [nextButton setEnabled:NO]; [previousButton setEnabled:YES]; [fieldTwoTextField becomeFirstResponder]; } } - (IBAction)previous:(id)sender { if(activeField == fieldTwoTextField) { [nextButton setEnabled:YES]; [previousButton setEnabled:NO]; [fieldOneTextField becomeFirstResponder]; } } #pragma mark #pragma mark View Initialization methods - (void)viewDidLoad { [super viewDidLoad]; [scrollView addSubview:contentView]; [scrollView setContentSize:[[self view] bounds].size]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; keyboardShown = NO; [self registerToReceiveKeyboardNotifications]; } #pragma mark #pragma mark event Registration
A Local Keyboard Notification
- (void) registerToReceiveKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } #pragma mark #pragma mark Responding to keyboard events - (void)keyboardWillShow:(NSNotification *)aNotification { if (keyboardShown) return; NSDictionary *userInfo = [aNotification userInfo]; NSValue* keyboardOriginValue = [userInfo objectForKey:UIKeyboardFrameEndUserInfoKey]; CGRect keyboardRect = [scrollView convertRect:[keyboardOriginValue CGRectValue] fromView:nil]; CGFloat keyboardTop = keyboardRect.origin.y; CGRect newScrollViewFrame = [[self view] bounds]; newScrollViewFrame.size.height = keyboardTop - scrollView.bounds.origin.y; NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; NSTimeInterval animationDuration; [animationDurationValue getValue:&animationDuration]; [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:animationDuration]; [scrollView setFrame:newScrollViewFrame]; [scrollView scrollRectToVisible:[activeField frame] animated:YES]; [UIView commitAnimations]; keyboardShown = YES; } - (void)keyboardWillHide:(NSNotification *)aNotification { NSDictionary* userInfo = [aNotification userInfo]; NSValue *animationDurationValue = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey]; NSTimeInterval animationDuration; [animationDurationValue getValue:&animationDuration]; [scrollView setFrame:[[self view] bounds]]; [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:animationDuration]; [UIView commitAnimations];
❘ 223
224
❘ Chapter 6 Notification Processing
keyboardShown = NO; } #pragma mark #pragma mark UITextFieldDelegate -(BOOL) textFieldShouldBeginEditing:(UITextField*)textField { if(textField.inputAccessoryView == nil) { textField.inputAccessoryView=toolbar; } activeField = textField; if(activeField == fieldOneTextField) { [nextButton setEnabled:YES]; [previousButton setEnabled:NO]; } else if(activeField == fieldTwoTextField) { [nextButton setEnabled:NO]; [previousButton setEnabled:YES]; } return YES; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } #pragma mark #pragma mark Cleanup methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContentView:nil]; [self setFieldOneTextField:nil]; [self setFieldTwoTextField:nil]; [self setScrollView:nil]; [self setToolbar:nil]; [self setPreviousButton:nil]; [self setNextButton:nil]; [self setActiveField:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)dealloc { [super dealloc]; [contentView release]; [fieldOneTextField release]; [fieldTwoTextField release]; [scrollView release]; [toolbar release]; [previousButton release]; [nextButton release]; [activeField release]; } @end
Summary
❘ 225
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the “A Local Keyboard Notification” section.
Summary In this chapter, two types of local notifications were observed. The first application, LocalNamedNotification-iPhone, not only registered with the NSNotificationCenter to receive the events, but also was responsible for posting the notification when a button was tapped. The second application, LocalNotificationKeyboard-iPhone, registered with the NSNotificationCenter to receive notifications that were posted by the iOS 4 operating system itself, reacting to the keyboard being shown or hidden.
7
networking Concepts What’s in this chaPter? ➤➤
Publishing network services
➤➤
Browsing available network services
➤➤
Sending data objects between peer devices
➤➤
Sending voice between peer devices
This chapter explains how to register and discover network services, and describes the communication process between devices using the OS’s built in connection software, Bonjour. Two foundation classes are defi ned for managing Bonjour network services: ➤➤
NSNetService — An actual network service
➤➤
NSNetServiceBrowser — A browser for the related network service
The process used by these two foundation classes are defi ned by three basic operations: ➤➤
Registration (server side) — NSNetService
➤➤
Browsing (client-side discovery of domains and services) — NSNetServiceBrowser
➤➤
Resolution (client side) — NSNetService
The first example application in this chapter demonstrates publishing by the server, and browsing by the client to discover domains and services. The results of the process will be listed in a Table view on the device, and the server application is a desktop-based application. When there is a requirement for devices to communicate with each other, iOS 4 has a framework, GameKit, that insulates the details in two classes: ➤➤
GKSession — For networking handling
➤➤
GKPeerPickerController — Provides a UI for connecting
The process used by these two foundation classes are defi ned by three basic operations: ➤➤
Initialization of the GKSession
➤➤
Connection between the users
➤➤
Data exchange between the peers
228
❘ Chapter 7 Networking Concepts
The UI for connecting between devices is bundled in an object called the peer picker. The peer picker handles the following: ➤➤
Enables users to turn on Bluetooth
➤➤
Discovers the phones listed
➤➤
Allows for device invitations
➤➤
Allows for the connections to accept the invitation
➤➤
Completes the connection to the other peer
Communication over a Network The foundation classes NSNetService and NSServiceBrowser insulate you from the actual DNS operations and management, and provide the following operations: ➤➤
Publication — Publishing a service
➤➤
Discovery — Browsing for available services
➤➤
Resolution — Translating service names for use
The server initializes and publishes the NSNetService as follows: NSNetService *localService = [[NSNetService alloc] initWithDomain:@”local” type:@”_hab._tcp.” name:@”“ port:7865]; [localService setDelegate:self]; [self setNetService:localService]; [localService release]; [[self netService] publish];
From there, the control is maintained by NSNetServiceDelegate delegate methods: ➤➤
netServiceWillPublish
➤➤
netServiceDidPublish
➤➤
netService:didNotPublish
➤➤
netServiceDidStop
After the server is started and published it will begin searching for available domains and services through the NSNetServiceBrowser class. To search for domains, the method searchForBrowsableDomains is invoked: NSNetServiceBrowser* browser = [[NSNetServiceBrowser alloc] init]; [browser setDelegate:self]; [browser searchForBrowsableDomains];
After the search for domains has been initiated, the netServiceBrowser:didFindDomain:moreComing: delegate method handles the results of the search. This is where you assemble your list of identified domains: - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindDomain:(NSString*)domain moreComing:(BOOL)moreComing { NSLog(@”didFindDomain - Domain = %@ more = %d”, domain, moreComing); if ([self domains] == nil) [self setDomains:[[NSMutableArray alloc] init]]; [[self domains] addObject:domain]; if(moreComing == 0) {
Communication over a Network
❘ 229
[self.tableView reloadData]; } } - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveDomain:(NSString*)domain moreComing:(BOOL)moreComing { NSLog(@”didRemoveDomain - Domain = %@ more = %d”, domain, moreComing); }
When the domain is known, or has been found as illustrated above, the search for available services is accomplished through the searchForServicesOfType:inDomain: method: NSNetServiceBrowser *localBrowser = [[NSNetServiceBrowser alloc] init]; [localBrowser setDelegate:self]; [self setNetServiceBrowser:localBrowser]; [[self netServiceBrowser] searchForServicesOfType:@”_hab._tcp.” inDomain:@”local”];
After the search for services has been initiated, the netServiceBrowser:didFindServices:moreComing: delegate method handles the results of the search. This is where you assemble your list of identified services: - (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didFindService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { NSLog(@”didFindService %@”, netService); if ([self services] == nil) [self setServices:[[NSMutableArray alloc] init]]; [[self services] addObject:netService]; [self.tableView reloadData]; } - (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didRemoveService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { [[self services] removeObject:netService]; } - (void)netServiceBrowserDidStopSearch: (NSNetServiceBrowser *)aNetServiceBrowser { [[self services] removeAllObjects]; } The netServiceDidResolveAddress: method is a notification from the NSNetService object that it has added an address to its list of addresses for the service. (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict { NSLog(@”didNotResolve - errorDict = %@”, errorDict); } - (void)netServiceDidResolveAddress:(NSNetService *)service { NSLog(@”netServiceDidResolveAddress - name = %@”, [service name]); NSString *msg = [NSString stringWithFormat:@”%@”, [service name]]; [self setTitle:msg]; }
230
❘ Chapter 7 Networking Concepts
If resolution fails to be resolved, the netService:didNotResolve: method is called with a dictionary of NSServicesErrorCodes that can be identified by the NSNetService. The following is a list of identified errors that may occur: ➤➤
NSNetServicesUnknownError
➤➤
NSNetServicesCollisionError
➤➤
NSNetServicesNotFoundError
➤➤
NSNetServicesActivityInProgress
➤➤
NSNetServicesBadArgumentError
➤➤
NSNetServicesCancelledError
➤➤
NSNetServicesInvalidError
➤➤
NSNetServicesTimeoutError
The following application illustrates the process of searching for domains and service discovery and resolution.
A Simple Network Browser In this section, you build two applications. The first application is a Mac OS X desktop server. The second application, the iPhone client, discovers the available domains and identifies and resolves the available service published by the server. In the first part, you create the Mac OS X desktop server; in the second part, you build the browser for the iPhone. When completed, the server will be launched as shown in Figure 7-1.
Figure 7-1
With the server up and published, the iPhone app is launched and the discovered domains and services are listed in a Table view (see Figure 7-2). To create the desktop server and iPhone client projects, follow the steps in the next section.
Development Steps: A Simple Network Browser There are two applications that you build in this section. The first is the server that will be run on your computer and if you accepted the default installation of the iOS 4 SDK, you will have the tools to build this server application. The client part is the iPhone application. If you did not install the Mac OS X subset of the SDK, you may run the installer and install it separately as it does not require you to have to belong to the Mac developer program.
Building the Desktop Server To create this application, which functions as the server, execute the following steps:
1. 2.
Start Xcode and create a Mac OS X Cocoa application named NetworkServer-Mac. In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File, and select Mac OS X Cocoa Objective-C class as a subclass of NSView and name it ServerView.
A Simple Network Browser
3.
Double-click the MainMenu.xib file to launch Interface Builder (see Figure 7-3).
Figure 7-3
Figure 7-2
4.
From the Interface Builder Library (Tools ➪ Library), select and drag the following to the View window. ➤➤
Two Labels
1. 2.
Double-click the left label and type Connection:. Place the second label to the right of the left label.
Your interface should now look like Figure 7-4.
5.
In the Interface Builder Library, click Classes at the top and scroll to and select your ServerView class. At the bottom, now choose the Outlets button. Click the + and add the following outlet, as shown in Figure 7-5: ➤➤
Figure 7-4
messageLabel (as a NSTextField instead of id type)
❘ 231
232
❘ Chapter 7 Networking Concepts
6.
From the main menu of Interface Builder, choose File ➪ Write Class Files, and select Save and then Merge. A window will appear with 0 differences for ServerView.m. Close the window, as there are no modifications to be made. A window will appear with your new additions to ServerView.h on the left and the original template on the right (see Figure 7-6). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge and close the window.
Figure 7-5
Figure 7-6
7.
In the Interface Builder Library, click Objects at the top and scroll until you get to Custom View and drag it into the window by the File’s Owner icon (see Figure 7-7).
8.
With Custom View still selected, choose Tools ➪ Identity Inspector and select ServerView as the class (see Figure 7-8).
9.
You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the NSTextField as messageLabel
Figure 7-7
A Simple Network Browser
10.
11.
❘ 233
To make the connection to identify the NSTextField as a messageLabel, control-click on the Server View icon to bring up the Inspector as shown in Figure 7-9. From the right of the Server View Inspector, control-drag from the circle next to the messageLabel to the NSTextField messageLabel until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made (see Figure 7-10). Dismiss the File’s Owner Inspector and Select File ➪ Save to save the files. Now it is time to enter your logic.
Figure 7-9
Figure 7-8
Figure 7-10
Source Code Listings for the Desktop Server For this application, the Server_MacAppDelegate.h and Server_MacAppDelegate.m files are not modified and are used as generated.
ServerView.h Modifications to the Template You declared one outlet messageLabel in Interface Builder. You must now define the properties for this variable in order to get and set its value (see Listing 7-1). The IBOutlet was moved to the property declaration.
234
❘ Chapter 7 Networking Concepts
There is also an instance variable netService of type NSNetService. Listing 7-1: The complete ServerView.h file (Chapter7/NetworkServer-Mac/ServerView.h) #import
@interface ServerView : NSView { NSTextField *messageLabel; NSNetService *netService; } @property (nonatomic, retain) IBOutlet NSTextField *messageLabel; @property (nonatomic, retain) NSNetService *netService;
- (void)startService; - (void)stopService; - (void)postMessage:(NSString *)messsage; @end
ServerView.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the ServerView.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 7-2). Listing 7-2: Addition of @synthesize #import “ServerView.h”
@implementation ServerView @synthesize messageLabel; @synthesize netService;
As shown in Listing 7-3, the view and service have to be started, and you need to include a method to stop the service. Listing 7-3: Initialization of the view and service start and stop - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { [self startService]; if([self netService] != nil) { NSLog(@”Service was not started”); } } return self; } - (void)startService { NSNetService *localService = [[NSNetService alloc] initWithDomain:@”local” type:@”_hab._tcp.”
A Simple Network Browser
❘ 235
name:@”“ port:7865]; [localService setDelegate:self]; [self setNetService:localService]; [localService release]; [[self netService] publish]; NSString *msg = [NSString stringWithFormat:@”netService:startService”]; [self postMessage:msg]; } -(void)stopService { [[self netService] stop]; [self setNetService:nil]; NSString *msg = [NSString stringWithFormat:@”netService:stopService”]; [self postMessage:msg]; }
Finally, you have to include the delegate methods for the domain and service browsing, and the address resolution (see Listing 7-4). Listing 7-4: NSNetServiceDelegate methods #pragma mark #pragma mark NSNetServiceDelegate methods - (void)netServiceWillPublish:(NSNetService *)sender { NSString *msg = [NSString stringWithFormat:@”netServiceWillPublish - Name = %@”, [sender name]]; [self postMessage:msg]; } - (void)netServiceDidPublish:(NSNetService*)sender { NSString *msg = [NSString stringWithFormat:@”netServiceDidPublish - Name = %@”, [sender name]]; [self postMessage:msg]; } - (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict { NSString *msg = [NSString stringWithFormat:@”netService:didNotPublish - Name = %@ Error = %@”, [sender name], errorDict]; [self postMessage:msg]; } - (void)netServiceDidStop:(NSNetService *)sender { NSString *msg = [NSString stringWithFormat:@”netServiceDidStop - Name = %@”, [sender name]]; [self postMessage:msg]; [self setNetService:nil]; } #pragma mark -
236
❘ Chapter 7 Networking Concepts
#pragma mark Action methods - (void)postMessage:(NSString *)message { [[self messageLabel] setStringValue:message]; }
The complete ServerView.m file is shown in Listing 7-5. Listing 7-5: The complete ServerView.m file (Chapter7/NetworkServer-Mac/ServerView.m) #import “ServerView.h”
@implementation ServerView @synthesize messageLabel; @synthesize netService;
- (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; if (self) { [self startService]; if([self netService] != nil) { NSLog(@”Service was not started”); } } return self; } - (void)startService { NSNetService *localService = [[NSNetService alloc] initWithDomain:@”local” type:@”_hab._tcp.” name:@”“ port:7865]; [localService setDelegate:self]; [self setNetService:localService]; [localService release]; [[self netService] publish]; NSString *msg = [NSString stringWithFormat:@”netService:startService”]; [self postMessage:msg]; } -(void)stopService { [[self netService] stop]; [self setNetService:nil]; NSString *msg = [NSString stringWithFormat:@”netService:stopService”]; [self postMessage:msg]; }
- (void)drawRect:(NSRect)dirtyRect { // Drawing code here. } #pragma mark -
A Simple Network Browser
❘ 237
#pragma mark NSNetServiceDelegate methods - (void)netServiceWillPublish:(NSNetService *)sender { NSString *msg = [NSString stringWithFormat:@”netServiceWillPublish - Name = %@”, [sender name]]; [self postMessage:msg]; } - (void)netServiceDidPublish:(NSNetService*)sender { NSString *msg = [NSString stringWithFormat:@”netServiceDidPublish - Name = %@”, [sender name]]; [self postMessage:msg]; } - (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict { NSString *msg = [NSString stringWithFormat:@”netService:didNotPublish - Name = %@ Error = %@”, [sender name], errorDict]; [self postMessage:msg]; } - (void)netServiceDidStop:(NSNetService *)sender { NSString *msg = [NSString stringWithFormat:@”netServiceDidPublish - Name = %@”, [sender name]]; [self postMessage:msg]; [self setNetService:nil]; } #pragma mark #pragma mark Action methods - (void)postMessage:(NSString *)message { [[self messageLabel] setStringValue:message]; } #pragma mark #pragma mark Memory methods -(void)dealloc { [self stopService]; [super dealloc]; } @end
The iPhone Client To create this part of the application, the iPhone client, execute the following steps:
1.
Start Xcode and create a Navigation-based application for the iPhone and name it NetworkServerBrowser-iPhone. If you need to see this step, please see Appendix A for the steps to begin a Navigation-based application.
2.
With Navigation-based applications using the Table view, you can go straight to the code, as there are no outlets to hook up in Interface Builder. Therefore, choose File ➪ Save and exit Interface Builder.
238
❘ Chapter 7 Networking Concepts
Source Code Listings for the iPhone Client For this application, the NetServiceBrowser_iPhoneAppDelegate.h and NetServiceBrowser_ iPhoneAppDelegate.m files are not modified and are used as generated.
RootViewController.h Modifications to the Template The RootViewController.h and RootViewController.m files are automatically generated with a good deal of template code to drive the Table view. The following was added to the template (see Listing 7-6). The NSNetServiceBrowserDelegate and NSNetServiceDelegate delegate protocols have to be declared. An instance of the NSNetServiceBrowser has to be retained to hold the service itself. There are two arrays to store the domains and services found, and two Boolean variables to indicate domains and services that were found. Listing 7-6: The complete RootViewController.h file (Chapter7/NetworkServerBrowser-iPhone/
Classes/RootViewController.h) #import
@interface RootViewController : UITableViewController { NSNetServiceBrowser *netServiceBrowser; NSMutableArray *domains; NSMutableArray *services; BOOL domainsFound; BOOL servicesFound; } @property (nonatomic, retain) NSNetServiceBrowser *netServiceBrowser; @property (nonatomic, retain) NSMutableArray *domains; @property (nonatomic, retain) NSMutableArray *services; @property (nonatomic, readonly, getter=isDomainsFound) BOOL domainsFound; @property (nonatomic, readonly, getter=isServicesFound) BOOL servicesFound; @end
RootViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the ServerView.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 7-7). Listing 7-7: Addition of @synthesize #import “RootViewController.h”
@implementation RootViewController @synthesize @synthesize @synthesize @synthesize @synthesize
netServiceBrowser; domains; services; domainsFound; servicesFound;
A Simple Network Browser
❘ 239
When the ServerView initializes, the search for the domains and services is performed; and when the view closes, the searches terminate (see Listing 7-8). Listing 7-8: The domain and service searches #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Networks”]; } - (void)viewWillAppear:(BOOL)animated { NSNetServiceBrowser* browser = [[NSNetServiceBrowser alloc] init]; [browser setDelegate:self]; [browser searchForBrowsableDomains]; NSNetServiceBrowser *localBrowser = [[NSNetServiceBrowser alloc] init]; [localBrowser setDelegate:self]; [self setNetServiceBrowser:localBrowser]; [[self netServiceBrowser] searchForServicesOfType:@”_hab._tcp.” inDomain:@”local”]; [super viewWillAppear:animated]; } - (void)viewWillDisappear:(BOOL)animated { [[self netServiceBrowser] stop]; [[self netServiceBrowser] setDelegate:nil]; [self setNetServiceBrowser:nil]; [super viewWillDisappear:animated]; }
The NSNetServiceDelegate methods are implemented to resolve the addresses or to notify when the process failed (see Listing 7-9). Listing 7-9: The NSNetServiceDelegate methods #pragma mark #pragma mark NSNetServiceDelegate - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict { NSLog(@”didNotResolve - errorDict = %@”, errorDict); } - (void)netServiceDidResolveAddress:(NSNetService *)service { NSLog(@”netServiceDidResolveAddress - name = %@”, [service name]); NSString *msg = [NSString stringWithFormat:@”%@”, [service name]]; [self setTitle:msg]; }
The NSNetServiceBrowserDelegate methods are implemented to resolve addresses or to notify when the process failed (see Listing 7-10).
240
❘ Chapter 7 Networking Concepts
Listing 7-10: The NSNetServiceBrowserDelegate methods #pragma mark #pragma mark NSNetServiceBrowserDelegate - (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didFindService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { NSLog(@”didFindService %@”, netService); if ([self services] == nil) [self setServices:[[NSMutableArray alloc] init]]; [[self services] addObject:netService]; [self.tableView reloadData]; } - (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didRemoveService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { [[self services] removeObject:netService]; } - (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)aNetServiceBrowser { [[self services] removeAllObjects]; } - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindDomain:(NSString*)domain moreComing:(BOOL)moreComing { NSLog(@”didFindDomain - Domain = %@ more = %d”, domain, moreComing); if ([self domains] == nil) [self setDomains:[[NSMutableArray alloc] init]]; [[self domains] addObject:domain]; if(moreComing == 0) { [self.tableView reloadData]; } } - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveDomain:(NSString*)domain moreComing:(BOOL)moreComing { NSLog(@”didRemoveDomain - Domain = %@ more = %d”, domain, moreComing); }
The complete RootViewController.m file is shown in Listing 7-11. Listing 7-11: The complete RootViewController.m file (Chapter7/NetworkServerBrowser-iPhone/
Classes/RootViewController.m)
#import “RootViewController.h”
@implementation RootViewController @synthesize netServiceBrowser; @synthesize domains; @synthesize services;
A Simple Network Browser
@synthesize domainsFound; @synthesize servicesFound;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Networks”]; } - (void)viewWillAppear:(BOOL)animated { NSNetServiceBrowser* browser = [[NSNetServiceBrowser alloc] init]; [browser setDelegate:self]; [browser searchForBrowsableDomains]; NSNetServiceBrowser *localBrowser = [[NSNetServiceBrowser alloc] init]; [localBrowser setDelegate:self]; [self setNetServiceBrowser:localBrowser]; [[self netServiceBrowser] searchForServicesOfType:@”_hab._tcp.” inDomain:@”local”]; [super viewWillAppear:animated]; } - (void)viewWillDisappear:(BOOL)animated { [[self netServiceBrowser] stop]; [[self netServiceBrowser] setDelegate:nil]; [self setNetServiceBrowser:nil]; [super viewWillDisappear:animated]; } #pragma mark #pragma mark NSNetServiceBrowserDelegate - (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didFindService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { NSLog(@”didFindService %@”, netService); if ([self services] == nil) [self setServices:[[NSMutableArray alloc] init]]; [[self services] addObject:netService]; [self.tableView reloadData]; } - (void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didRemoveService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { [[self services] removeObject:netService]; } - (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)aNetServiceBrowser { [[self services] removeAllObjects]; } - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser
❘ 241
242
❘ Chapter 7 Networking Concepts
didFindDomain:(NSString*)domain moreComing:(BOOL)moreComing { NSLog(@”didFindDomain - Domain = %@ more = %d”, domain, moreComing); if ([self domains] == nil) [self setDomains:[[NSMutableArray alloc] init]]; [[self domains] addObject:domain]; if(moreComing == 0) { [self.tableView reloadData]; } } - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveDomain:(NSString*)domain moreComing:(BOOL)moreComing { NSLog(@”didRemoveDomain - Domain = %@ more = %d”, domain, moreComing); } #pragma mark #pragma mark NSNetServiceDelegate - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict { NSLog(@”didNotResolve - errorDict = %@”, errorDict); } - (void)netServiceDidResolveAddress:(NSNetService *)service { NSLog(@”netServiceDidResolveAddress - name = %@”, [service name]); NSString *msg = [NSString stringWithFormat:@”%@”, [service name]]; [self setTitle:msg]; }
#pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 2; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { switch (section) { case 0: return [domains count]; break; case 1: return [services count]; break; default: return 0; break; } } // Customize the Header Titles of the table view.
A Simple Network Browser
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { switch (section) { case 0: return @”Domains”; break; case 1: return @”Services”; break; default: return @”Unknown”; break; } } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; NSString *cellText = @”Unknown”; NSNetService *service = nil; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } switch ([indexPath section]) { case 0: cellText = [[self domains] objectAtIndex:indexPath.row]; break; case 1: service = [[self services] objectAtIndex:indexPath.row]; cellText = [service name]; break; default: break; } // Configure the cell. [[cell textLabel]setText: cellText]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSNetService *service = nil; switch ([indexPath section]) { case 1: service = [[self services] objectAtIndex:indexPath.row];
❘ 243
244
❘ Chapter 7 Networking Concepts
[service setDelegate:self]; [service resolveWithTimeout:5]; break; default: break; } } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated in // viewDidLoad or on demand. // For example: self.myOutlet = nil; [self setNetServiceBrowser:nil]; [self setDomains:nil]; [self setServices:nil]; }
- (void)dealloc { [netServiceBrowser release]; [domains release]; [services release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, you will need to start the server on your desktop first. When the server is up, launch the iPhone browser app and it should give you results that were described at the beginning of the “A Simple Network Browser” section.
Peer-to-Peer Device Communications This application, when completed, needs to be installed on two devices; it will not work in the Simulator. When installed on both devices, the life cycle is as follows (see Figure 7-11): ➤➤
If Bluetooth is not on when the app starts, you will be prompted to do so.
➤➤
The search for devices begins.
➤➤
All found devices found are displayed.
➤➤
Upon peer selection, the connection is attempted.
➤➤
The phone that you are trying to connect to is asked if it will accept your invitation to connect.
➤➤
Each peer will click the Send button to exchange a specific message packet with each other, and the received packet is displayed.
➤➤
In addition to sending text packets, this application also sends voice between peers by utilizing the GKVoiceChatClient.
Peer-to-Peer Device Communications
❘ 245
Figure 7-11
Development Steps: Peer-to-Peer Device Communication This application illustrates how to send text as well as voice between devices. You will learn how to send the data, and how to identify what message type is being exchanged. To get started setting up peer-to-peer device communication execute the following steps:
1.
2.
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it MessagePacket.
3.
In the Groups & Files section of Xcode, click on the Frameworks and Control-click and choose Add ➪ Existing Frameworks. Select GameKit.framework and click Add. Repeat for AudioToolbox.framework.
4.
To add a sound file to the project, the sound file has to be converted to a caf file that will be imported into the project. This sound file will be used to alert the user that a text message has arrived.
Start Xcode and create a View-based application for the iPhone and name it PeerCommunication-iPhone. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
➤➤ ➤➤
To add the sound file, take any one-second aif file and rename it Arrived.aif. To convert the aif file to a caf file, type the following in a Terminal window (/Applications/ Utilities/Terminal.app):
/usr/bin/afconvert -f caff -d LEI16 Arrived.aif Arrived.caf
246
❘ Chapter 7 Networking Concepts
5.
➤➤
Select Resources in Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your sound file, Arrived.caf, and click Add.
➤➤
Check Copy items into destination group’s folder, and click Add.
Double-click the PeerCommunication_iPhoneViewController.xib file to launch Interface Builder (see Figure 7-12).
Figure 7-12
6.
From the Interface Builder Library (Tools ➪ Library), select and drag the following to the View window: ➤➤
One UIView to cover the entire view window
➤➤
One UILabel on top of view for the width of the window
➤➤
Two UILabels just below the label above
1. 2.
Double-click the left label and type Name:. Add the second label just to the right of the Name label you previously created. ➤➤
1. 2.
Two UILabels just below the label pair above
Double-click the left label and type Time:. Add the second label just to the right of the Name label you previously created. ➤➤
Two UILabels just below the label pair above
Peer-to-Peer Device Communications
1. 2.
Double-click the left label and type Message:. Add the second label just to the right of the Name label you previously created. ➤➤
One UIButton in the middle of the view. Double-click the button and enter Send.
Your interface should now look like Figure 7-13.
7.
8.
Back in the Interface Builder Library, click Classes at the top and scroll to and select your PeerCommunication_iPhone ViewController class. At the bottom, now choose the Outlets button. Click the + and add the following outlets, as shown in Figure 7-14: ➤➤
messageLabel (as a UILabel instead of id type)
➤➤
nameLabel (as a UILabel instead of id type)
➤➤
statusLabel (as a UILabel instead of id type)
➤➤
timeLabel (as a UILabel instead of id type)
At the bottom, choose the Actions button. Click the + and add the following outlet, as shown in Figure 7-15: ➤➤
sendMessage
Figure 7-14
Figure 7-13
Figure 7-15
❘ 247
248
❘ Chapter 7 Networking Concepts
9.
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up and then Merge from the second pop-up. A window will appear with your new additions to PeerCommunication_iPhoneViewController.m on the left and the original template on the right (see Figure 7-16). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge and close the window.
Download from Wow! eBook <www.wowebook.com>
Figure 7-16
10.
A window will appear with your new additions to PeerCommunication_iPhoneViewController.h on the left and the original template on the right (see Figure 7-17). ➤➤
11.
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge and close the window.
You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UILabel as messageLabel
➤➤
Identify the UILabel as nameLabel
Peer-to-Peer Device Communications
➤➤
Identify the UILabel as statusLabel
➤➤
Identify the UILabel as timeLabel
❘ 249
Figure 7-17
12.
To make the connection to identify the UILabel as messageLabel, Control-click on the File’s Owner icon to bring up the Inspector as shown in Figure 7-18.
13.
From the right of the File’s Owner Inspector, Control-drag from the circle next to the messageLabel to the UILabel messageLabel until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made (see Figure 7-19).
14.
Repeat this for the nameLabel to the UILabel nameLabel, statusLabel, and timeLabel (see Figure 7-20), and then dismiss the File’s Owner Inspector:
Figure 7-18
Figure 7-19
250
❘ Chapter 7 Networking Concepts
Figure 7-20
15.
Control-drag from the Send button to the File’s Owner icon, and select the sendMessage action (see Figure 7-21). Select File ➪ Save to save the files.
Now it is time to enter your logic.
Source Code Listings for Peer-to-Peer Device Communication For this application, the PeerCommunication_iPhoneAppDelegate.h and PeerCommunication_iPhoneAppDelegate.m files are not modified and are used as generated.
Figure 7-21
PeerCommunication_iPhoneViewController.h Modifications to the Template You declared four outlets in Interface Builder: messageLabel, nameLabel, statusLabel, and timeLabel. You must now define the properties for these variables in order to get and set their values (see Listing 7-12). The IBOutlet was moved to the property declaration. The GKPeerPickerControllerDelegate, GKSessionDelegate, and GKVoiceChatClient delegate protocols have to be declared. An instance of the GKSession has to be retained to hold the session itself.
Peer-to-Peer Device Communications
❘ 251
Listing 7-12: The complete PeerCommunication_iPhoneViewController.h file (/Chapter7/
PeerCommunication-iPhone/Classes/PeerCommunication_iPhoneViewController.h) #import #import #import typedef enum { PacketTypeVoice = 0, PacketTypeText = 1, } PacketType; @interface PeerCommunication_iPhoneViewController : UIViewController { GKSession *session; NSString *localUser; NSString *connectedPeerID; UILabel UILabel UILabel UILabel
*nameLabel; *statusLabel; *timeLabel; *messageLabel;
SystemSoundID _soundID; } @property @property @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain) retain) retain) retain)
GKSession *session; NSString *localUser; NSString *connectedPeerID; IBOutlet UILabel *nameLabel; IBOutlet UILabel *statusLabel; IBOutlet UILabel *timeLabel; IBOutlet UILabel *messageLabel;
- (IBAction)sendMessage:(id)sender; - (void)sendPacket:(NSData*)data ofType:(PacketType)type; - (void)browseForPeers; - (void)disconnect:(GKSession *)aSession; - (void)loadSound; @end
PeerCommunication_iPhoneViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the PeerCommunication_iPhoneViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 7-13). Listing 7-13: Addition of @synthesize #import “PeerCommunication_iPhoneViewController.h” #import “GameKit_iPhoneViewController.h” #import “MessagePacket.h” #define kSessionID @”habID” @implementation PeerCommunication_iPhoneViewController @synthesize session;
252
❘ Chapter 7 Networking Concepts
@synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
localUser; connectedPeerID; nameLabel; statusLabel; timeLabel; messageLabel;
When the PeerCommunication_iPhoneViewController initializes, the search for peers begins (see Listing 7-14). Listing 7-14: The initialization of the view controller, search for peers, and view unload #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setLocalUser:[[UIDevice currentDevice].name retain]]; [statusLabel setText:@”Looking for peers...”]; [nameLabel setText:@”“]; [timeLabel setText:@”“]; [messageLabel setText:@”“]; [[GKVoiceChatService defaultVoiceChatService] setRemoteParticipantVolume:5.0]; [self browseForPeers]; [self loadSound]; } - (void)viewDidUnload { [self setLocalUser:nil]; [self setNameLabel:nil]; [self setStatusLabel:nil]; [self setTimeLabel:nil]; [self setMessageLabel:nil]; }
As shown in Listing 7-15, the GKSessionDelegate methods are implemented to control the session state and data. Listing 7-15: The GKSessionDelegate methods #pragma mark #pragma mark GKSessionDelegate methods - (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID { [[self session] acceptConnectionFromPeer:peerID error:nil]; } - (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error { [statusLabel setText:[error localizedDescription]]; } - (void)session:(GKSession *)session didFailWithError:(NSError*)error{ [statusLabel setText:[error localizedDescription]]; } - (void)session:(GKSession *)aSession peer:(NSString *)peerID
Peer-to-Peer Device Communications
didChangeState:(GKPeerConnectionState)state { switch (state) { case GKPeerStateAvailable: [statusLabel setText:@”Available”]; break; case GKPeerStateUnavailable: [statusLabel setText:@”Unavailable”]; break; case GKPeerStateConnected: [self setConnectedPeerID:peerID]; [statusLabel setText:[NSString stringWithFormat:@”Connected to %@”, [aSession displayNameForPeer:peerID]]]; [[GKVoiceChatService defaultVoiceChatService] setClient:self]; [[GKVoiceChatService defaultVoiceChatService] startVoiceChatWithParticipantID:peerID error: nil]; break; case GKPeerStateDisconnected: [statusLabel setText:@”Disconnected”]; [[GKVoiceChatService defaultVoiceChatService] stopVoiceChatWithParticipantID:peerID]; [self disconnect:aSession]; break; case GKPeerStateConnecting: [statusLabel setText:@”Peer Connecting...”]; break; default: break; } }
The GKPeerPickerControllerDelegate methods are implemented to handle the session messages that occur over the the peer-to-peer connection (see Listing 7-16). Listing 7-16: The GKPeerPickerControllerDelegate methods #pragma mark #pragma mark GKPeerPickerControllerDelegate methods - (void)invalidateSession:(GKSession *)aSession { [self disconnect:aSession]; } - (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type { GKSession *localSession = [[GKSession alloc] initWithSessionID:kSessionID displayName:[self localUser] sessionMode:GKSessionModePeer]; return [localSession autorelease]; } - (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession: (GKSession *)aSession { [aSession setDelegate:self]; [aSession setDataReceiveHandler:self withContext:nil]; [self setSession:aSession]; [picker setDelegate:nil]; [picker dismiss]; [picker autorelease];
❘ 253
254
❘ Chapter 7 Networking Concepts
} - (void)peerPickerController:(GKPeerPickerController *)picker didSelectConnectionType:(GKPeerPickerConnectionType)type { if (type == GKPeerPickerConnectionTypeOnline) { [picker setDelegate:nil]; [picker dismiss]; [picker autorelease]; } } - (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker { [picker setDelegate:nil]; [picker autorelease]; if([self session] != nil) { [[self session] setAvailable:NO]; [[self session] setDataReceiveHandler: nil withContext: NULL]; [[self session] setDelegate:nil]; [self setSession:nil]; } [statusLabel setText:@”Connection attempt cancelled...”]; }
In addition to the text being sent, there is also voice transmission, so the GKVoiceChatClient delegate method is implemented, as well as the participantID identification method (see Listing 7-17). Listing 7-17: The GKVoiceChatClient and participantID delegate methods #pragma mark #pragma mark GKVoiceChatClient delegate methods - (void)voiceChatService:(GKVoiceChatService *)voiceChatService sendData:(NSData *)data toParticipantID:(NSString *)participantID { [self sendPacket:data ofType:PacketTypeVoice]; } - (NSString *)participantID { return [session peerID]; }
Listing 7-18 shows the two methods that are used for peer picker session browsing and message transmission, as well as a disconnect method. Listing 7-18: The disconnect, browseForPeers, and sendMessage methods #pragma mark #pragma mark Session methods - (void)disconnect:(GKSession *)aSession { if(aSession != nil) { [aSession disconnectFromAllPeers]; [aSession setAvailable:NO]; [aSession setDataReceiveHandler:nil withContext: NULL]; [aSession setDelegate:nil]; } } #pragma mark #pragma mark Peer methods - (void)browseForPeers {
Peer-to-Peer Device Communications
❘ 255
GKPeerPickerController *picker = [[GKPeerPickerController alloc] init]; [picker setDelegate:self]; [picker setConnectionTypesMask:GKPeerPickerConnectionTypeNearby]; [picker show]; } #pragma mark #pragma mark Action methods - (IBAction)sendMessage:(id)sender { NSMutableArray *array = [NSMutableArray array]; MessagePacket *messagePacket = [[MessagePacket alloc] init]; [messagePacket setName:[self localUser]]; [messagePacket setTime: [[NSDate date] description]]; [messagePacket setMessage:[NSString stringWithFormat:@”Message from %@”, [self localUser]]]; [array addObject:messagePacket]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array]; [self sendPacket:data ofType:PacketTypeText]; [messagePacket release]; }
The data will have to be bundled with a header id to indicate whether the message transmission is audio or text. The bundling occurs in the sendPacket method. When the packet is received, the header is extracted to identify the data extracted (see Listing 7-19). Listing 7-19: The sendPacket and receiveData methods #pragma mark #pragma mark Packet data - (void)sendPacket:(NSData*)data ofType:(PacketType)type { NSError *error; NSMutableData * newPacket = [NSMutableData dataWithCapacity:([data length]+sizeof(uint32_t))]; uint32_t swappedType = CFSwapInt32HostToBig((uint32_t)type); [newPacket appendBytes:&swappedType length:sizeof(uint32_t)]; [newPacket appendData:data]; if (![[self session] sendData:newPacket toPeers:[NSArray arrayWithObject:[self connectedPeerID]] withDataMode:GKSendDataReliable error:&error]) { NSLog(@”%@”,[error localizedDescription]); } } - (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)aSession context:(void *)context { PacketType header; uint32_t swappedHeader; if ([data length] >= sizeof(uint32_t)) { [data getBytes:&swappedHeader length:sizeof(uint32_t)]; header = (PacketType)CFSwapInt32BigToHost(swappedHeader); NSRange packetRange = {sizeof(uint32_t), [data length]-sizeof(uint32_t)}; NSData *packetData = [data subdataWithRange:packetRange]; NSLog(@”header type = %d”, header);
256
❘ Chapter 7 Networking Concepts
if (header == PacketTypeVoice) { [[GKVoiceChatService defaultVoiceChatService] receivedData:packetData fromParticipantID:peer]; } else if (header == PacketTypeText) { AudioServicesPlaySystemSound(_soundID); NSArray *array = [[NSKeyedUnarchiver unarchiveObjectWithData:packetData] mutableCopy]; MessagePacket *messsagePacket = [array objectAtIndex:0]; [statusLabel setText:@”Message received...”]; [nameLabel setText:[messsagePacket name]]; [timeLabel setText:[messsagePacket time]]; [messageLabel setText:[messsagePacket message]]; [array release]; } else { NSLog(@”Unknown packet type %@”, header); } } }
When the user sends a message, a noise will sound to announce that the message is received. The loadSound method, shown in Listing 7-20, loads the sound file into the app. Listing 7-20: The loadSound method #pragma mark #pragma mark Audio processing - (void)loadSound { NSString *path = [[NSBundle mainBundle] pathForResource:@”Arrived” ofType:@”caf”]; if(path != nil) { NSURL *aFileURL = [NSURL fileURLWithPath:path isDirectory:NO]; if(aFileURL != nil) { SystemSoundID aSoundID; OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)aFileURL, &aSoundID); if (error == kAudioServicesNoError) { _soundID = aSoundID; } else { NSLog(@”Error %d loading sound at path: %@”, error, path); } } } }
The complete PeerCommunication_iPhoneViewController.m file is shown in Listing 7-21. Listing 7-21: The complete PeerCommunication_iPhoneViewController.m file (/Chapter7/ PeerCommunication-iPhone/Classes/PeerCommunication_iPhoneViewController.m) #import “PeerCommunication_iPhoneViewController.h” #import “MessagePacket.h” #define kSessionID @”habID” @implementation PeerCommunication_iPhoneViewController @synthesize session;
Peer-to-Peer Device Communications
@synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
localUser; connectedPeerID; nameLabel; statusLabel; timeLabel; messageLabel;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setLocalUser:[[UIDevice currentDevice].name retain]]; [statusLabel setText:@”Looking for peers...”]; [nameLabel setText:@”“]; [timeLabel setText:@”“]; [messageLabel setText:@”“]; [[GKVoiceChatService defaultVoiceChatService] setRemoteParticipantVolume:5.0]; [self browseForPeers]; [self loadSound]; } - (void)viewDidUnload { [self setLocalUser:nil]; [self setNameLabel:nil]; [self setStatusLabel:nil]; [self setTimeLabel:nil]; [self setMessageLabel:nil]; } #pragma mark #pragma mark GKSessionDelegate methods - (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID { [[self session] acceptConnectionFromPeer:peerID error:nil]; } - (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error { [statusLabel setText:[error localizedDescription]]; } - (void)session:(GKSession *)session didFailWithError:(NSError*)error{ [statusLabel setText:[error localizedDescription]]; } - (void)session:(GKSession *)aSession peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state { switch (state) { case GKPeerStateAvailable: [statusLabel setText:@”Available”]; break; case GKPeerStateUnavailable: [statusLabel setText:@”Unavailable”]; break; case GKPeerStateConnected: [self setConnectedPeerID:peerID];
❘ 257
258
❘ Chapter 7 Networking Concepts
[statusLabel setText:[NSString stringWithFormat:@”Connected to %@”, [aSession displayNameForPeer:peerID]]]; [[GKVoiceChatService defaultVoiceChatService] setClient:self]; [[GKVoiceChatService defaultVoiceChatService] startVoiceChatWithParticipantID:peerID error: nil]; break; case GKPeerStateDisconnected: [statusLabel setText:@”Disconnected”]; [[GKVoiceChatService defaultVoiceChatService] stopVoiceChatWithParticipantID:peerID]; [self disconnect:aSession]; break; case GKPeerStateConnecting: [statusLabel setText:@”Peer Connecting...”]; break; default: break; } } #pragma mark #pragma mark GKPeerPickerControllerDelegate methods - (void)invalidateSession:(GKSession *)aSession { [self disconnect:aSession]; } - (GKSession *)peerPickerController:(GKPeerPickerController *)picker sessionForConnectionType:(GKPeerPickerConnectionType)type { GKSession *localSession = [[GKSession alloc] initWithSessionID:kSessionID displayName:[self localUser] sessionMode:GKSessionModePeer]; return [localSession autorelease]; } - (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession: (GKSession *)aSession { [aSession setDelegate:self]; [aSession setDataReceiveHandler:self withContext:nil]; [self setSession:aSession]; [picker setDelegate:nil]; [picker dismiss]; [picker autorelease]; } - (void)peerPickerController:(GKPeerPickerController *)picker didSelectConnectionType:(GKPeerPickerConnectionType)type { if (type == GKPeerPickerConnectionTypeOnline) { [picker setDelegate:nil]; [picker dismiss]; [picker autorelease]; } } - (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker { [picker setDelegate:nil]; [picker autorelease]; if([self session] != nil) {
Peer-to-Peer Device Communications
[[self session] setAvailable:NO]; [[self session] setDataReceiveHandler: nil withContext: NULL]; [[self session] setDelegate:nil]; [self setSession:nil]; } [statusLabel setText:@”Connection attempt cancelled...”]; } #pragma mark #pragma mark GKVoiceChatClient delegate methods - (void)voiceChatService:(GKVoiceChatService *)voiceChatService sendData:(NSData *)data toParticipantID:(NSString *)participantID { [self sendPacket:data ofType:PacketTypeVoice]; } - (NSString *)participantID { return [session peerID]; } #pragma mark #pragma mark Session methods - (void)disconnect:(GKSession *)aSession { if(aSession != nil) { [aSession disconnectFromAllPeers]; [aSession setAvailable:NO]; [aSession setDataReceiveHandler:nil withContext: NULL]; [aSession setDelegate:nil]; } } #pragma mark #pragma mark Peer methods - (void)browseForPeers { GKPeerPickerController *picker = [[GKPeerPickerController alloc] init]; [picker setDelegate:self]; [picker setConnectionTypesMask:GKPeerPickerConnectionTypeNearby]; [picker show]; } #pragma mark #pragma mark Action methods - (IBAction)sendMessage:(id)sender { NSMutableArray *array = [NSMutableArray array]; MessagePacket *messagePacket = [[MessagePacket alloc] init]; [messagePacket setName:[self localUser]]; [messagePacket setTime: [[NSDate date] description]]; [messagePacket setMessage:[NSString stringWithFormat:@”Message from %@”, [self localUser]]]; [array addObject:messagePacket]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array]; [self sendPacket:data ofType:PacketTypeText]; [messagePacket release]; } #pragma mark -
❘ 259
260
❘ Chapter 7 Networking Concepts
#pragma mark Packet data - (void)sendPacket:(NSData*)data ofType:(PacketType)type { NSError *error; NSMutableData * newPacket = [NSMutableData dataWithCapacity:([data length]+sizeof(uint32_t))]; uint32_t swappedType = CFSwapInt32HostToBig((uint32_t)type); [newPacket appendBytes:&swappedType length:sizeof(uint32_t)]; [newPacket appendData:data]; if (![[self session] sendData:newPacket toPeers:[NSArray arrayWithObject:[self connectedPeerID]] withDataMode:GKSendDataReliable error:&error]) { NSLog(@”%@”,[error localizedDescription]); } } - (void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)aSession context:(void *)context { PacketType header; uint32_t swappedHeader; if ([data length] >= sizeof(uint32_t)) { [data getBytes:&swappedHeader length:sizeof(uint32_t)]; header = (PacketType)CFSwapInt32BigToHost(swappedHeader); NSRange packetRange = {sizeof(uint32_t), [data length]-sizeof(uint32_t)}; NSData *packetData = [data subdataWithRange:packetRange]; NSLog(@”header type = %d”, header); if (header == PacketTypeVoice) { [[GKVoiceChatService defaultVoiceChatService] receivedData:packetData fromParticipantID:peer]; } else if (header == PacketTypeText) { AudioServicesPlaySystemSound(_soundID); NSArray *array = [[NSKeyedUnarchiver unarchiveObjectWithData:packetData] mutableCopy]; MessagePacket *messsagePacket = [array objectAtIndex:0]; [statusLabel setText:@”Message received...”]; [nameLabel setText:[messsagePacket name]]; [timeLabel setText:[messsagePacket time]]; [messageLabel setText:[messsagePacket message]]; [array release]; } else { NSLog(@”Unknown packet type %@”, header); } } } #pragma mark #pragma mark Audio processing - (void)loadSound { NSString *path = [[NSBundle mainBundle] pathForResource:@”Arrived” ofType:@”caf”]; if(path != nil) { NSURL *aFileURL = [NSURL fileURLWithPath:path isDirectory:NO]; if(aFileURL != nil) { SystemSoundID aSoundID;
Peer-to-Peer Device Communications
OSStatus error = AudioServicesCreateSystemSoundID((CFURLRef)aFileURL, &aSoundID); if (error == kAudioServicesNoError) { _soundID = aSoundID; } else { NSLog(@”Error %d loading sound at path: %@”, error, path); } } } }
#pragma mark #pragma mark Memory processing - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { [localUser release]; [connectedPeerID release]; [nameLabel release]; [statusLabel release]; [timeLabel release]; [messageLabel release]; [super dealloc]; } @end
MessagePacket Definition The message that will be exchanged between peers contains the following: ➤➤
Name — The sender’s name
➤➤
Time — Time of transmission
➤➤
Message — The message that is exchanged
In order for this object to be sent over a session, it must implement the NSCoding protocol. Listing 7-22 shows the complete MessagePacket.h file, and Listing 7-23 shows the MessagePacket.m file. Listing 7-22: The complete MessagePacket.h file (Chapter7/PeerCommunication-iPhone/
Classes/MessagePacket.h)
#import @interface MessagePacket : NSObject { NSString *name; NSString *time; NSString *message; } @property (nonatomic, retain) NSString *name; @property (nonatomic, retain) NSString *time; @property (nonatomic, retain) NSString *message; @end
❘ 261
262
❘ Chapter 7 Networking Concepts
Listing 7-23: The complete MessagePacket.m file (Chapter7/PeerCommunication-iPhone/
Classes/MessagePacket.m) #import “MessagePacket.h”
@implementation MessagePacket @synthesize name; @synthesize time; @synthesize message; - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self name] forKey:@”name”]; [coder encodeObject:[self time] forKey:@”time”]; [coder encodeObject:[self message] forKey:@”message”]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setName:[coder decodeObjectForKey:@”name”]]; [self setTime:[coder decodeObjectForKey:@”time”]]; [self setMessage:[coder decodeObjectForKey:@”message”]]; } return self; } - (void)dealloc { [name release]; [time release]; [message release]; [super dealloc]; } @end
Test Your Application To test this application, you will need the following: ➤➤
Two devices (iPhone, iPad, or iPod touch older than first generation)
➤➤
Development provisioning profile installed on both devices
➤➤
WiFi network
If you have the preceding and you have installed the app on both devices, it should give you the results described at the beginning of this topic.
Summary In this chapter, the first two applications demonstrated the technique of communicating over a network via a client-server relationship through the use of NSNetService and NSNetServiceBrowser. The first application was the server residing on the desktop computer and the second application was the iPhone client. The final application demonstrated peer-to-peer connectivity, including sending a message packet, as well as voice communications, between devices.
8
Multimedia What’s in this chaPter? ➤➤
How to access the iPod library to retrieve music and video
➤➤
How to use the iOS 4 AVPlayer to play a movie
➤➤
Quickly adding a movie player to your application using the MPMoviePlayerViewController class
It would be safe to say that it’s the media capabilities of the iPhone, iPad, and iPod touch that accounts for their popularity. Integration with iTunes has brought these devices to the next level. This chapter describes how to write media-rich applications. For audio, the techniques and frameworks are demonstrated; and for video, two approaches are compared side by side within one application. The AVPlayer class is an iOS 4 addition, and with iOS 3.2 the MPMoviePlayerViewController class enabled video capability with just a few Objective-C statements. The best thing about this chapter is it shows you how to retrieve both audio and video from the iPod library. For video, I have created a standalone class so you can easily add this capability to your own applications!
frameWorks for aUdio Writing applications that involve the processing of audio has typically been a complicated task. With the iOS frameworks, examined in this section, the task is vastly simplified. When working with audio, you may forget that your iPhone or iPad devices are not dedicated audio tools; they have to respond to higher-priority events, such as phone calls, and alarms. Responding to these interruptions should be an important factor in your applications design.
media Player The Media Player framework provides facilities for playing movies, music, audio podcast, and audio book fi les. In addition to accessing the iPod library, you have the capability through this framework to query its contents by using the MPMediaPredicate class.
264
❘ Chapter 8 Multimedia
AV Foundation The AV Foundation framework provides the following classes for audio processing: ➤➤
AVAudioPlayer — Use this if you need to play audio that is either a file or in memory. The types sup-
➤➤
AVAudioRecorder — Use this when you need to record audio to a file. You can record until you
ported are caf, m4a, mp3, aif, wav, au, snd, and aac. This is the simplest way to play audio. decide to stop or for a specific length of time. The types supported are aac, alac, Linear PCM, IMA4, and iLBC. It is recommended that AVAudioPlayer and AVAudioRecorder be your starting point for audio processing until you need more detailed processing. ➤➤
AVAudioSession — Use this if you need to manage the behavior of the audio or hardware characteristics. The focus is on management of the audio with respect to mixing your audio with background audio and handling routing changes or interruptions. Most of the common functionality is wrapped in AVFoundation/AVAudioSession.h, and the session services are found in AudioToolbox/ AudioServices.h.
➤➤
AVPlayer — Use this when you need to play local or streamed audio.
AV Foundation provides the tools to help you if you need to know more about the details of your asset, as well as create new assets. It also allows access to the camera.
Audio Toolbox The Audio Toolbox contains APIs that perform audio conversion, and provides the capability for reading and writing file-based audio and parsing file streams. Through audio services, three interfaces are available: ➤➤
System Sound Services — For playing short clips
➤➤
Audio Hardware Service — For interfacing with audio hardware
➤➤
Audio Session Services — Enables iOS devices to manage their audio sessions
There is also an interface to create and use audio processing graphs.
Audio Unit You use the Audio Unit framework when you need access to audio files for detailed processing. With its plug-in architecture, it provides a flexible processing chain for your audio that supports real-time input, output, and simultaneous I/O. The plug-ins, also called audio units, provide built-in features such as mixing and panning. The plug-ins are used in VoIP (through the use of the Voice Processing I/O unit), and as a mixing unit for interactive applications. You can use the remote I/O for real-time audio I/O.
OpenAL You use the OpenAL framework when you need special 3D positioning — for example, in gaming applications. OpenAL is an open-source, cross-platform API. One of the most common uses for the OpenAL framework is to add audio to games, as it enhances the user experience because of the ability to employ directional sound. It is built upon Core Audio. Details can be found at the following URL: http://connect.creativelabs.com/openal/default.aspx
Playing Audio from the iPod Library
❘ 265
Frameworks for Video In the preceding section the audio frameworks simplified the task of playing audio. With video, the task of playback is even more challenging. Apple has provided two classes that simplify the process: AVPlayer and MPMoviePlayerController. ➤➤
AVPlayer — AVPlayer, introduced in iOS 4, provides all the functionality to play back video. The actual asset that is played is an instance of an AVPlayerItem.
➤➤
Supported Formats — The supported formats for an AVMetadataItem are defined in the AVMetadataFormat.h file and include ID3, QuickTime and iTunes metadata.
This section covers the frameworks Apple has included in the SDK for video playback.
MPMoviePlayerController Prior to iOS 3.2, MPMoviePlayerController was in its own session, which meant interrupting your current session. Since version 3.2, it now inherits your category settings, and no longer interrupts current processing. With iOS 3.2, you can present your video in an MPMoviePlayerViewController class, which enables full-screen playback versus playback in just a UIView window. Ongoing information about the state of your video is important to your application, especially because the device is not just a media player. The notifications that are broadcast are as follows: ➤➤
The playing state of the movie: start, stop, pause, or seeking forward or backward
➤➤
Scaling mode changes of the movie
➤➤
Full-screen entering or exiting
➤➤
Load state of network movies
➤➤
Movie metadata
Supported Formats The MPMoviePlayerController supports the following formats: ➤➤
H.264 Baseline Profile Level 3.0 video, up to 640 × 480 at 30 fps
➤➤
MPEG-4 Part 2 video
The following sections provide two applications that demonstrate the techniques for processing audio and video, respectively. They offer a compact object architecture that makes it easy for you to implement these rich frameworks into your own applications.
Playing Audio from the iPod Library When this application launches, the main view contains a disabled play-pause button on the top bar; and two buttons, List and Library, on the bottom bar, as shown in Figure 8-1. When tapped, the List button displays the songs that were selected from the iPod library. Tapping the Library button brings up a modal view displaying all the songs in the iPod library, as shown in Figure 8-2. The songs are played in the same order in which they were selected. After the songs are selected, the user taps the Done button and the
Figure 8-1
266
❘
chaPter 8 MultiMedia
music begins. In addition to the music playing, if album art is available the image will be displayed along with the artist, song, and track number, as shown in Figure 8-3. If users decide to listen to another song in the list while the current song is playing, they can tap the List button to make a new selection (see Figure 8-4).
fiGUre 8-2
fiGUre 8-3
fiGUre 8-4
Notice that this view also includes a plus (+) button at the top right for access to the iPod library, enabling users to add selections to the listening list. Now it is time to create your own iPod music player.
Access to the iPod library is not available through the Simulator. This application has to be run on a real device. Therefore, you must be a registered member of the iPhone Developer Program and have the required certificates and provisioning profiles to install on devices. See Appendix D for the Apple Reference Guides for iTunes Connect.
development steps: Playing audio from the iPod library To create this music player application, execute the following steps:
1 .
Start Xcode and create a View-based application for the iPad and name it iPodLibraryPlayer. If you need to see this step, please refer to Appendix A for the steps to begin a View-based application.
2 .
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File, choose Cocoa Touch Class, and select UIViewController subclass class. Make sure that only “With XIB for user interface” is checked and name the file MediaItemsViewController. This displays the list of songs that you selected from the iPod library.
3 .
Double-click the iPodLibraryPlayerViewController.xib file in the group NIB Files to launch Interface Builder (see Figure 8-5).
Playing Audio from the iPod Library
❘ 267
Figure 8-5
4. 5. 6. 7.
8.
Select a UIImageView and drag it onto the main view, placing it just below the navigation bar, and release the mouse. Select Tools ➪ Size Inspector and set X:85, Y:65, W:150, H:150.
9.
Select a UIToolbar and drag it over the main view, placing it at the bottom, and release the mouse. There is one bar button item already on the toolbar. Double-click and enter List for the title.
Select Tools ➪ Attributes Inspector and select the main view and set the Background option to Black Color. From the main menu, select Tools ➪ Library and choose the Objects tab. Select a UINavigationBar and drag it over the main view, placing it at the top, and release the mouse. Select a UIBarButtonItem and drag it over the navigation bar, place it at the left of the bar, and release the mouse. Select Pause for the Identifier.
10.
Select a UIBarButtonItem and drag it over the lower toolbar, placing it to the right of the other bar button item, and release the mouse. Double-click and enter Library for the title.
11.
Select a Flexible Space Bar Button Item and drag it over the lower toolbar, placing it to the left of the List button. Repeat this, but place the new Flexible Space Bar Button Item to the right of the Library button. Your two buttons are now centered on the toolbar.
12.
Select a UILabel and drag it over the main view, place it below the image view, and release the mouse. Stretch the label out to W:250. Repeat this step two more times, placing each label below the previous one.
13.
Select a UILabel and drag it over the main view, place it just above the bottom toolbar, and release the mouse. Stretch the label to W:300. Double-click inside and enter Tap Library to select music from iTunes. Your completed interface should look like Figure 8-6.
268
❘ Chapter 8 Multimedia
14.
Select Tools ➪ Library, choose Classes at the top, and scroll to and select your iPodLibraryPlayerViewController class. At the bottom, now choose the Outlets button. Click the + and add the following outlets, as shown in Figure 8-7: ➤➤
albumArtworkImage (as a UIImageView instead of
id type) ➤➤ ➤➤
artistLabel (as a UILabel instead of id type) navigationBar (as a UINavigationBar instead of
id type)
15.
➤➤
songLabel (as a UILabel instead of id type)
➤➤
trackLabel (as a UILabel instead of id type)
At the bottom, choose the Actions button. Click the + and add the following actions, as shown in Figure 8-8: ➤➤
addMusicFromLibrary
➤➤
changeMusicState
➤➤
showSelectedSongList Figure 8-6
Figure 8-7
Figure 8-8
Playing Audio from the iPod Library
16.
❘ 269
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then select Merge from the second pop-up. A window will appear with your new additions to iPodLibraryPlayerViewController.h on the left and the original template on the right (see Figure 8-9): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference
➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 8-9
17.
The next file to merge your new additions is iPodLibraryPlayerViewController.m, which has your additions on the left and the original template on the right (see Figure 8-10): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference
➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference
➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Select File ➪ Save Merge and close the window.
270
❘ Chapter 8 Multimedia
Figure 8-10
You now have an Objective-C template that will hold your application’s logic. Now you have to make all the connections to: ➤➤
Identify the UIImageView as albumArtworkImage.
➤➤
Identify the UILabel as artistLabel.
➤➤
Identify the UINavigationBar as navigationBar.
➤➤
Identify the UILabel as songLabel.
➤➤
Identify the UILabel as trackLabel.
18.
To make the connection to identify the UIImageView as albumArtworkImage, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 8-11.
19.
From the right of the File’s Owner Inspector, control-drag from the circle next to albumArtworkImage to the UIImageView albumArtworkImage until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for all the other fields you declared above.
20.
From the right of the File’s Owner Inspector, control-drag from the circle next to addMusicFromLibrary to the Library button until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the List button to the showSelectedSongList action and the UIBarButtonItem at the top left of the navigation bar to the changeMusicState action (see Figure 8-12). Choose File ➪ Save.
Playing Audio from the iPod Library
Figure 8-11
❘ 271
Figure 8-12
21.
Back in Xcode, double-click the MediaItemsViewController.xib file in the group NIB Files to bring up the view in Interface Builder.
22.
From the main menu, select Tools ➪ Library and choose the Objects tab.
23.
Select a UINavigationBar and drag it over the main view, placing it at the top, and release the mouse. Double-click Title and enter Music List.
24.
Select a UIBarButtonItem and drag it over the navigation bar, placing it at the left, and release the mouse. Select Done for the Identifier.
25.
Select a UIBarButtonItem and drag it over the navigation bar, placing it at the right, and release the mouse. Select Add for the Identifier.
26.
Select a UITableView and drag it over the navigation bar, placing it at the right, and release the mouse. Your completed interface should look like Figure 8-13.
27.
Select Tools ➪ Library, choose Classes at the top, and scroll to and select your MediaItemsView Controller class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure 8-14: ➤➤ ➤➤
addMusicButton (as a UIBarButtonItem instead of id type) mediaTableView (as a UITableView instead of
id type) 28.
29.
At the bottom, choose the Actions button. Click the + and add the following actions, as shown in Figure 8-15: ➤➤
addMusicFromLibrary
➤➤
done
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then select Merge from the second pop-up. A window will appear with your new additions to MediaItemsViewController.m on the left and the original template on the right (see Figure 8-16): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window. Figure 8-13
272
❘ Chapter 8 Multimedia
Figure 8-14
30.
Figure 8-15
The next file to merge your new additions is MediaItemsViewController.h, which has your additions on the left and the original template on the right (see Figure 8-17): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UIBarButtonItem as addMusicButton.
➤➤
Identify the UITableView as mediaTableView.
Playing Audio from the iPod Library
Figure 8-16
Figure 8-17
❘ 273
274
❘ Chapter 8 Multimedia
31.
To make the connection to identify the UIBarButtonItem as addMusicButton, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 8-18.
32.
From the right of the File’s Owner Inspector, control-drag from the circle next to addMusicButton to UIBarButtonItem addMusicButton until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the mediaTableView.
33.
From the right of the File’s Owner Inspector, control-drag from the circle next to addMusicFromLibrary to the UIBarButtonItem addMusicButton until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for done to UIBarButtonItem Done (see Figure 8-19).
Figure 8-18
34.
Figure 8-19
Now you need to connect the Table view so that the MediaItemsViewController class will act as the delegate
and data source. This enables your application to get Table view delegate messages and act as a data source to populate the table. Control-click on the Table view to bring up the Table View Inspector. From the right of dataSource in the inspector, control-drag to the Table view and release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the delegate entry (see Figure 8-20), and then select File ➪ Save. 35.
Figure 8-20
Finally, you have to import the following frameworks into your project in order for it build successfully: ➤➤
AudioToolbox.framework
➤➤
AVFoundation.framework
➤➤
MediaPlayer.framework
To import the frameworks, control-click the Frameworks group, select Add ➪ Existing Frameworks, and then select the preceding frameworks and click Add (see Figure 8-21).
That completes all the user interface additions and connections you need for this application. Now it is time to enter your logic.
Source Code Listings for an Application That Plays Audio from the iPod Library For this application, the iPodLibraryPlayerAppDelegate.h and iPodLibraryPlayerAppDelegate.m files are not modified and are used as generated. Figure 8-21
Playing Audio from the iPod Library
iPodLibraryPlayerViewController.h Modifications to the Template To choose items from the iPod library, the view controller has to implement the MPMediaPickerControllerDelegate and AVAudioPlayerDelegate protocols. To play music from the iPod library, the view controller has to have an instance of MPMusicPlayerController to use. To store songs returned from the iPod library, the view controller has to have an instance of MPMediaItemCollection to use. In addition to the retrieval, playing and storage of the music, the music player’s current state must be monitored to indicate the playing or paused state. Listing 8-1 shows the complete iPodLibraryPlayerViewController.h file. Listing 8-1: The complete iPodLibraryPlayerViewController.h file (/Chapter8/ iPodLibraryPlayer/Classes/iPodLibraryPlayerViewController.h) #import #import #import #import
<MediaPlayer/MediaPlayer.h>
@interface iPodLibraryPlayerViewController : UIViewController <MPMediaPickerControllerDelegate, AVAudioPlayerDelegate> { MPMusicPlayerController *musicPlayerController; MPMediaItemCollection *localMediaItemCollection; UIImageView *albumArtworkImage; UILabel *artistLabel; UINavigationBar *navigationBar; UILabel *songLabel; UILabel *trackLabel; UIBarButtonItem *pauseButton; UIBarButtonItem *playButton; } @property (nonatomic, retain) MPMusicPlayerController *musicPlayerController; @property (nonatomic, retain) MPMediaItemCollection *localMediaItemCollection; @property (nonatomic, retain) UIBarButtonItem *pauseButton; @property (nonatomic, retain) UIBarButtonItem *playButton; @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain) retain)
IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet
UIImageView *albumArtworkImage; UILabel *artistLabel; UINavigationBar *navigationBar; UILabel *songLabel; UILabel *trackLabel;
- (IBAction)addMusicFromLibrary:(id)sender; - (IBAction)changeMusicState:(id)sender; - (IBAction)showSelectedSongList:(id)sender; - (void)createPlayPauseButtons; - (void)registerForNotifications; - (void)unregisterForNotifications; - (void)nowPlayingItemDidChange:(id)aNotification;
❘ 275
276
❘ Chapter 8 Multimedia
- (void)playbackStateDidChange:(id)aNotification; -
(void)updateMediaItemCollection:(MPMediaItemCollection *)mediaItemCollection; (void)done; (void)updatePlayerDisplay:(MPMediaItem *)nowPlayingItem; (void)refreshAlbumInfo;
@end
iPodLibraryPlayerViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the iP odLibraryPlayerViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 8-2). Listing 8-2: Addition of @synthesize #import “iPodLibraryPlayerViewController.h” #import “MediaItemsViewController.h” @implementation iPodLibraryPlayerViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
musicPlayerController; localMediaItemCollection; albumArtworkImage; artistLabel; navigationBar; songLabel; trackLabel; pauseButton; playButton;
When the iPodLibraryPlayerViewController view initializes in the viewDidLoad method, the play-pause button has to be created, as well as an instance of the MPMusicPlayerController. In addition, the play-pause button has to initially be disabled, as there is no music to play. Finally, two notifications have to be monitored to handle if the song changed or the playback state had changed through the MPMusicPlayerControllerNowPlayingItemDidChangeNotification and MPMusicPlayerControllerPlaybackStateDidChangeNotification events respectively (see Listing 8-3). Listing 8-3: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self createPlayPauseButtons]; [self setMusicPlayerController: [MPMusicPlayerController applicationMusicPlayer]]; [[[navigationBar topItem] leftBarButtonItem] setEnabled:NO]; [self registerForNotifications]; } - (void)viewWillAppear:(BOOL)animated {
Playing Audio from the iPod Library
❘ 277
[super viewWillAppear:animated]; [[navigationBar topItem] setTitle:@”Music Player”]; [songLabel setText:@”“]; [artistLabel setText:@”“]; [trackLabel setText:@”“]; } - (void)createPlayPauseButtons { UIBarButtonItem *play = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:@selector(changeMusicState:)]; UIBarButtonItem *pause = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause target:self action:@selector(changeMusicState:)]; [self setPlayButton:play]; [self setPauseButton:pause]; [play release]; [pause release]; }
The registerForNotifications method also has a corresponding unregisterForNotifications method when the view controller ends. As with all notifications, there is an associated method to handle the notification, as shown in Listing 8-4. Listing 8-4: The registerForNotifications and unregisterForNotifications: methods #pragma mark #pragma mark Notification registration - (void)registerForNotifications { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(nowPlayingItemDidChange:) name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification object:musicPlayerController]; [notificationCenter addObserver:self selector:@selector(playbackStateDidChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:musicPlayerController]; [musicPlayerController beginGeneratingPlaybackNotifications]; } - (void)unregisterForNotifications { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification object:musicPlayerController]; [notificationCenter removeObserver:self name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:musicPlayerController]; [musicPlayerController endGeneratingPlaybackNotifications]; [musicPlayerController release];
278
❘ Chapter 8 Multimedia
} #pragma mark #pragma mark Notification handlers - (void)nowPlayingItemDidChange:(id)aNotification { [self updatePlayerDisplay:[musicPlayerController nowPlayingItem]]; } - (void)playbackStateDidChange:(id)aNotification { MPMusicPlaybackState playbackState = [musicPlayerController playbackState]; switch (playbackState) { case MPMusicPlaybackStatePaused: case MPMusicPlaybackStateStopped: [[navigationBar topItem] setLeftBarButtonItem:playButton]; break; case MPMusicPlaybackStatePlaying: [[navigationBar topItem] setLeftBarButtonItem:pauseButton]; break; default: break; } }
The main player view has three buttons to perform the following functions: ➤➤
Play-pause music
➤➤
Display the selected song list
➤➤
Add music from the library
The respective action methods for these three buttons are changeMusicState, showSelectedSongList and addMusicFromLibrary (see Listing 8-5). Listing 8-5: The action methods #pragma mark #pragma mark Button Action handlers - (IBAction)changeMusicState:(id)sender { switch ([musicPlayerController playbackState]) { case MPMusicPlaybackStatePaused: case MPMusicPlaybackStateStopped: [musicPlayerController play]; break; case MPMusicPlaybackStatePlaying: [musicPlayerController pause]; break; default: break; } } - (IBAction)showSelectedSongList:(id)sender { MediaItemsViewController *controller = [[MediaItemsViewController alloc] initWithNibName:@”MediaItemsViewController” bundle:nil]; [controller setDelegate:self]; [self presentModalViewController: controller animated: YES];
Playing Audio from the iPod Library
❘ 279
[controller release]; } - (IBAction)addMusicFromLibrary:(id)sender { MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeMusic]; [picker setDelegate:self]; [picker setAllowsPickingMultipleItems:YES]; [picker setPrompt:@”Choose one or more songs”]; [self presentModalViewController: picker animated: YES]; [picker release]; }
When the iPod library is requested, the Media Item picker appears and the mediaPicker:didPickMedia Items and mediaPickerDidCancel delegate methods have to be handled. The handler must also remember to dismiss the view, as it was displayed modally (see Listing 8-6). Listing 8-6: The Media Item Picker delegate methods #pragma mark #pragma mark Media Item Picker Delegate - (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection { [self dismissModalViewControllerAnimated:YES]; [self updateMediaItemCollection:mediaItemCollection]; } - (void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker { [self refreshAlbumInfo]; [self dismissModalViewControllerAnimated: YES]; }
When all the selections from the iPod library are complete, the updateMediaCollection method is called to either initialize the local list or add to it, with the items returned from the picker (see Listing 8-7). Listing 8-7: The updateMediaCollection method #pragma mark #pragma mark Media Item Action methods - (void)updateMediaItemCollection:(MPMediaItemCollection *)mediaItemCollection { if (mediaItemCollection) { if ([self localMediaItemCollection] == nil) { [self setLocalMediaItemCollection:mediaItemCollection]; [musicPlayerController setQueueWithItemCollection:[self localMediaItemCollection]]; [musicPlayerController play]; } else { BOOL musicCurrentlyPlaying = ([musicPlayerController playbackState] == MPMusicPlaybackStatePlaying); MPMediaItem *nowPlayingItem = [musicPlayerController nowPlayingItem]; NSTimeInterval currentPlaybackTime = [musicPlayerController currentPlaybackTime]; NSMutableArray *currentMediaItems = [[localMediaItemCollection items] mutableCopy];
280
❘ Chapter 8 Multimedia
NSArray *mediaItemsToAdd = [mediaItemCollection items]; [currentMediaItems addObjectsFromArray:mediaItemsToAdd]; [self setLocalMediaItemCollection:[MPMediaItemCollection collectionWithItems:(NSArray *)currentMediaItems]]; [currentMediaItems release]; [musicPlayerController setQueueWithItemCollection: [self localMediaItemCollection]]; [musicPlayerController setNowPlayingItem:nowPlayingItem]; [musicPlayerController setCurrentPlaybackTime:currentPlaybackTime]; if(musicCurrentlyPlaying) { [musicPlayerController play]; } } [[[navigationBar topItem] leftBarButtonItem] setEnabled:YES]; } }
Whenever any change is made to the song playing, the updatePlayerDisplay method handles any user interface requirements to sync the song with the album information. The done method simply dismisses the modal view after refreshing the album information (see Listing 8-8). Listing 8-8: The updatePlayerDisplay, refreshAlbumInfo, and done method - (void)updatePlayerDisplay:(MPMediaItem *)nowPlayingItem { if(nowPlayingItem) { UIImage *artworkImage = nil; NSString *aSong = [nowPlayingItem valueForProperty:MPMediaItemPropertyTitle]; NSString *aAlbumTitle = [nowPlayingItem valueForProperty:MPMediaItemPropertyAlbumTitle]; NSString *aArtist = [nowPlayingItem valueForProperty:MPMediaItemPropertyArtist]; NSString *aTrack = [nowPlayingItem valueForProperty:MPMediaItemPropertyAlbumTrackNumber]; MPMediaItemArtwork *itemArtwork = [nowPlayingItem valueForProperty:MPMediaItemPropertyArtwork]; if (itemArtwork) { artworkImage = [itemArtwork imageWithSize:CGSizeMake (150, 150)]; [[self albumArtworkImage] setImage:artworkImage]; [[self albumArtworkImage] setHidden:NO]; } else { [[self albumArtworkImage] setHidden:YES]; } [musicPlayerController play]; [[navigationBar topItem] setTitle:aAlbumTitle]; [[self songLabel] setText:[NSString stringWithFormat:@”Song: %@”, aSong]]; [[self artistLabel] setText:[NSString stringWithFormat:@”Artist: %@”, aArtist]]; [[self trackLabel] setText:[NSString stringWithFormat:@”Track Number: %@”, aTrack]]; }
Playing Audio from the iPod Library
} - (void)refreshAlbumInfo { BOOL musicCurrentlyPlaying = ([musicPlayerController playbackState] == MPMusicPlaybackStatePlaying); if(musicCurrentlyPlaying) { MPMediaItem *nowPlayingItem = [musicPlayerController nowPlayingItem]; NSTimeInterval currentPlaybackTime = [musicPlayerController currentPlaybackTime]; [musicPlayerController setNowPlayingItem:nowPlayingItem]; [musicPlayerController setCurrentPlaybackTime:currentPlaybackTime]; [self updatePlayerDisplay:nowPlayingItem]; } } - (void)done { [self refreshAlbumInfo]; [self dismissModalViewControllerAnimated: YES]; }
The iPodLibraryPlayerViewController.m class is now complete. Listing 8-9 shows the complete implementation. Listing 8-9: The complete iPodLibraryPlayerViewController.m file (/Chapter8/ iPodLibraryPlayer/Classes/iPodLibraryPlayerViewController.m) #import “iPodLibraryPlayerViewController.h” #import “MediaItemsViewController.h” @implementation iPodLibraryPlayerViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
musicPlayerController; localMediaItemCollection; albumArtworkImage; artistLabel; navigationBar; songLabel; trackLabel; pauseButton; playButton;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self createPlayPauseButtons]; [self setMusicPlayerController: [MPMusicPlayerController applicationMusicPlayer]]; [[[navigationBar topItem] leftBarButtonItem] setEnabled:NO]; [self registerForNotifications]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [[navigationBar topItem] setTitle:@”Music Player”]; [songLabel setText:@”“];
❘ 281
282
❘ Chapter 8 Multimedia
[artistLabel setText:@”“]; [trackLabel setText:@”“]; } - (void)createPlayPauseButtons { UIBarButtonItem *play = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:@selector(changeMusicState:)]; UIBarButtonItem *pause = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause target:self action:@selector(changeMusicState:)]; [self setPlayButton:play]; [self setPauseButton:pause]; [play release]; [pause release]; } #pragma mark #pragma mark Notification registration - (void)registerForNotifications { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(nowPlayingItemDidChange:) name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification object:musicPlayerController]; [notificationCenter addObserver:self selector:@selector(playbackStateDidChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:musicPlayerController]; [musicPlayerController beginGeneratingPlaybackNotifications]; } - (void)unregisterForNotifications { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification object:musicPlayerController]; [notificationCenter removeObserver:self name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:musicPlayerController]; [musicPlayerController endGeneratingPlaybackNotifications]; [musicPlayerController release]; } #pragma mark #pragma mark Notification handlers - (void)nowPlayingItemDidChange:(id)aNotification { [self updatePlayerDisplay:[musicPlayerController nowPlayingItem]]; } - (void)playbackStateDidChange:(id)aNotification {
Playing Audio from the iPod Library
MPMusicPlaybackState playbackState = [musicPlayerController playbackState]; switch (playbackState) { case MPMusicPlaybackStatePaused: case MPMusicPlaybackStateStopped: [[navigationBar topItem] setLeftBarButtonItem:playButton]; break; case MPMusicPlaybackStatePlaying: [[navigationBar topItem] setLeftBarButtonItem:pauseButton]; break; default: break; } } #pragma mark #pragma mark Button Action handlers - (IBAction)changeMusicState:(id)sender { switch ([musicPlayerController playbackState]) { case MPMusicPlaybackStatePaused: case MPMusicPlaybackStateStopped: [musicPlayerController play]; break; case MPMusicPlaybackStatePlaying: [musicPlayerController pause]; break; default: break; } } - (IBAction)showSelectedSongList:(id)sender { MediaItemsViewController *controller = [[MediaItemsViewController alloc] initWithNibName:@”MediaItemsViewController” bundle:nil]; [controller setDelegate:self]; [self presentModalViewController: controller animated: YES]; [controller release]; } - (IBAction)addMusicFromLibrary:(id)sender { MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeMusic]; [picker setDelegate:self]; [picker setAllowsPickingMultipleItems:YES]; [picker setPrompt:@”Choose one or more songs”]; [self presentModalViewController: picker animated: YES]; [picker release]; } #pragma mark #pragma mark Media Item Picker Delegate - (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection { [self dismissModalViewControllerAnimated:YES]; [self updateMediaItemCollection:mediaItemCollection];
❘ 283
284
❘ Chapter 8 Multimedia
} - (void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker { [self refreshAlbumInfo]; [self dismissModalViewControllerAnimated: YES]; } #pragma mark #pragma mark Media Item Action methods - (void)updateMediaItemCollection:(MPMediaItemCollection *)mediaItemCollection { if (mediaItemCollection) { if ([self localMediaItemCollection] == nil) { [self setLocalMediaItemCollection:mediaItemCollection]; [musicPlayerController setQueueWithItemCollection:[self localMediaItemCollection]]; [musicPlayerController play]; } else { BOOL musicCurrentlyPlaying = ([musicPlayerController playbackState] == MPMusicPlaybackStatePlaying); MPMediaItem *nowPlayingItem = [musicPlayerController nowPlayingItem]; NSTimeInterval currentPlaybackTime = [musicPlayerController currentPlaybackTime]; NSMutableArray *currentMediaItems = [[localMediaItemCollection items] mutableCopy]; NSArray *mediaItemsToAdd = [mediaItemCollection items]; [currentMediaItems addObjectsFromArray:mediaItemsToAdd]; [self setLocalMediaItemCollection:[MPMediaItemCollection collectionWithItems:(NSArray *)currentMediaItems]]; [currentMediaItems release]; [musicPlayerController setQueueWithItemCollection: [self localMediaItemCollection]]; [musicPlayerController setNowPlayingItem:nowPlayingItem]; [musicPlayerController setCurrentPlaybackTime:currentPlaybackTime]; if(musicCurrentlyPlaying) { [musicPlayerController play]; } } [[[navigationBar topItem] leftBarButtonItem] setEnabled:YES]; } } - (void)done { [self refreshAlbumInfo]; [self dismissModalViewControllerAnimated: YES]; } - (void)updatePlayerDisplay:(MPMediaItem *)nowPlayingItem { if(nowPlayingItem) { UIImage *artworkImage = nil; NSString *aSong = [nowPlayingItem valueForProperty:MPMediaItemPropertyTitle]; NSString *aAlbumTitle =
Playing Audio from the iPod Library
[nowPlayingItem valueForProperty:MPMediaItemPropertyAlbumTitle]; NSString *aArtist = [nowPlayingItem valueForProperty:MPMediaItemPropertyArtist]; NSString *aTrack = [nowPlayingItem valueForProperty:MPMediaItemPropertyAlbumTrackNumber]; MPMediaItemArtwork *itemArtwork = [nowPlayingItem valueForProperty:MPMediaItemPropertyArtwork]; if (itemArtwork) { artworkImage = [itemArtwork imageWithSize:CGSizeMake (150, 150)]; [[self albumArtworkImage] setImage:artworkImage]; [[self albumArtworkImage] setHidden:NO]; } else { [[self albumArtworkImage] setHidden:YES]; } [musicPlayerController play]; [[navigationBar topItem] setTitle:aAlbumTitle]; [[self songLabel] setText:[NSString stringWithFormat:@”Song: %@”, aSong]]; [[self artistLabel] setText:[NSString stringWithFormat:@”Artist: %@”, aArtist]]; [[self trackLabel] setText:[NSString stringWithFormat:@”Track Number: %@”, aTrack]]; } } - (void)refreshAlbumInfo { BOOL musicCurrentlyPlaying = ([musicPlayerController playbackState] == MPMusicPlaybackStatePlaying); if(musicCurrentlyPlaying) { MPMediaItem *nowPlayingItem = [musicPlayerController nowPlayingItem]; NSTimeInterval currentPlaybackTime = [musicPlayerController currentPlaybackTime]; [musicPlayerController setNowPlayingItem:nowPlayingItem]; [musicPlayerController setCurrentPlaybackTime:currentPlaybackTime]; [self updatePlayerDisplay:nowPlayingItem]; } } #pragma mark #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setMusicPlayerController:nil]; [self setLocalMediaItemCollection:nil]; [self setNavigationBar:nil]; [self setPlayButton:nil]; [self setPauseButton:nil]; [self setSongLabel:nil]; [self setArtistLabel:nil]; [self setTrackLabel:nil];
❘ 285
286
❘ Chapter 8 Multimedia
[self setAlbumArtworkImage:nil]; } - (void)dealloc { [musicPlayerController release]; [localMediaItemCollection release]; [navigationBar release]; [playButton release]; [pauseButton release]; [songLabel release]; [artistLabel release]; [trackLabel release]; [albumArtworkImage release]; [super dealloc]; } @end
MediaItemsViewController.h Modifications to the Template You declared two outlets in Interface Builder: mediaTableView and addMusicButton. You must now define the properties for these variables in order to get and set their values. The IBOutlet was moved to the property declaration. An iPodLibraryPlayerViewController delegate variable was declared to identify the delegate class, in this case the iPodLibraryPlayerViewController, that will handle the save action, when the user taps the add music or done button. In order to process the media picker delegate messages, you have to implement the MPMediaPicker ControllerDelegate protocol, so you add that to the class definition in addition to the Table view delegate handling, where you implement the UITableViewDelegate protocol. Listing 8-10 shows the complete MediaItemsViewController.h file. Listing 8-10: The complete MediaItemsViewController.h file (/Chapter8/iPodLibraryPlayer/ Classes/MediaItemsViewController.h) #import <MediaPlayer/MediaPlayer.h> @class iPodLibraryPlayerViewController; @interface MediaItemsViewController : UIViewController <MPMediaPickerControllerDelegate, UITableViewDelegate> { iPodLibraryPlayerViewController *delegate; UIBarButtonItem *addMusicButton; UITableView *mediaTableView; } @property (nonatomic, retain) iPodLibraryPlayerViewController *delegate; @property (nonatomic, retain) IBOutlet UIBarButtonItem *addMusicButton; @property (nonatomic, retain) IBOutlet UITableView *mediaTableView; - (IBAction)addMusicFromLibrary:(id)sender; - (IBAction)done:(id)sender; @end
Playing Audio from the iPod Library
❘ 287
MediaItemsViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the MediaItemsViewController.m template. Each declared view property must be matched with @synthesize, as shown in Listing 8-11. Listing 8-11: Addition of @synthesize #import “MediaItemsViewController.h” #import “iPodLibraryPlayerViewController.h” @implementation MediaItemsViewController @synthesize delegate; @synthesize addMusicButton; @synthesize mediaTableView;
When the MediaItemsViewController view initializes, if there are any songs in the local collection, the Table view will contain those items (see Listing 8-12). Listing 8-12: The Table view data source and delegate processing methods #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[delegate localMediaItemCollection] items] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MPMediaItem *item = [[[delegate localMediaItemCollection] items] objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... [[cell textLabel] setText:[item valueForProperty:MPMediaItemPropertyTitle]]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView
288
❘ Chapter 8 Multimedia
didSelectRowAtIndexPath:(NSIndexPath *)indexPath { MPMediaItem *item = [[[delegate localMediaItemCollection] items] objectAtIndex:[indexPath row]]; [[delegate musicPlayerController] setNowPlayingItem:item]; [delegate updatePlayerDisplay:item]; [self dismissModalViewControllerAnimated: YES]; [tableView deselectRowAtIndexPath: indexPath animated: YES]; }
When the media picker has selected items to return to the collection, the mediaPicker:didPickMedia Items method has to be handled to add the new media to the existing collection, and then invoke the Table view to display the updated information, or the picker can return cancel, which simply dismisses the modal view (see Listing 8-13). Listing 8-13: The mediaPicker:didPickMediaItems: and mediaPickerDidCancel: methods #pragma mark #pragma mark Media Item Picker Delegate - (void) mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection { [self dismissModalViewControllerAnimated:YES]; [[self delegate] updateMediaItemCollection:mediaItemCollection]; [[self mediaTableView] reloadData]; } - (void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker { [self dismissModalViewControllerAnimated: YES]; }
Two actions are available from this view. Users can select a song they want to hear and then tap Done to dismiss the list or they can add more songs to the list from the iPod library by tapping the plus button, letting the addMusicFromLibrary method handle the task (see Listing 8-14). Listing 8-14: The action methods #pragma mark #pragma mark Action methods - (IBAction)done:(id)sender { [delegate done]; } - (IBAction)addMusicFromLibrary:(id)sender { MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeMusic]; [picker setDelegate:self]; [picker setAllowsPickingMultipleItems:YES]; [picker setPrompt:@”Choose one or more songs”]; [self presentModalViewController: picker animated: YES]; [picker release]; }
The MediaItemsViewController.m class is now complete. Listing 8-15 shows the complete implementation.
Playing Audio from the iPod Library
❘ 289
Listing 8-15: The complete MediaItemsViewController.m file (/Chapter8/iPodLibraryPlayer/
Classes/MediaItemsViewController.m)
#import “MediaItemsViewController.h” #import “iPodLibraryPlayerViewController.h” @implementation MediaItemsViewController @synthesize delegate; @synthesize addMusicButton; @synthesize mediaTableView; #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[delegate localMediaItemCollection] items] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MPMediaItem *item = [[[delegate localMediaItemCollection] items] objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... [[cell textLabel] setText:[item valueForProperty:MPMediaItemPropertyTitle]]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { MPMediaItem *item = [[[delegate localMediaItemCollection] items] objectAtIndex:[indexPath row]]; [[delegate musicPlayerController] setNowPlayingItem:item]; [delegate updatePlayerDisplay:item]; [self dismissModalViewControllerAnimated: YES]; [tableView deselectRowAtIndexPath: indexPath animated: YES];
290
❘ Chapter 8 Multimedia
} #pragma mark #pragma mark Media Item Picker Delegate - (void) mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection { [self dismissModalViewControllerAnimated:YES]; [[self delegate] updateMediaItemCollection:mediaItemCollection]; [[self mediaTableView] reloadData]; } - (void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker { [self dismissModalViewControllerAnimated: YES]; } #pragma mark #pragma mark Action methods - (IBAction)done:(id)sender { [delegate done]; } - (IBAction)addMusicFromLibrary:(id)sender { MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeMusic]; [picker setDelegate:self]; [picker setAllowsPickingMultipleItems:YES]; [picker setPrompt:@”Choose one or more songs”]; [self presentModalViewController: picker animated: YES]; [picker release]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [super viewDidUnload]; // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setDelegate:nil]; [self setAddMusicButton:nil]; [self setMediaTableView:nil]; } - (void)dealloc { [delegate release]; [addMusicButton release]; [mediaTableView release]; [super dealloc]; } @end
an application That Plays Video from the iPod library
❘ 291
test your application Now that you have everything completed, choose the device from the Xcode panel, make sure it is connected, and click Run and Build to try out your app. It should give you the results described at the beginning of the section “An Application That Plays Audio from the iPod Library.”
Remember that the iPod library is not accessible from within the Simulator, it is only accessible from the device. So, the app simply won’t work on the Simulator, as no media will be listed.
an aPPlication that Plays video from the iPod liBrary When this application launches, the main view displays the list of videos contained in your iPod library, as shown in Figure 8-22. A segmented control on the bottom toolbar enables users to choose which media technology they would like to use to view non-DRM movies: ➤➤
AVPlayer — Available in iOS 4 and later
➤➤
MPMoviePlayerViewController — Available in iOS 3.2 and later
When the user taps on a movie selection from the list with the AVPlayer selected on the bottom, the video will begin to play as shown in Figure 8-23. When the user taps on a movie selection from the list with the MPPlayer selected on the bottom, the video will begin to play as shown in Figure 8-24. Notice the built-in controls that come for free with the MPMoviePlayerController. Now it is time to create your own iPod video player.
fiGUre 8-22
fiGUre 8-23
fiGUre 8-24
292
❘
chaPter 8 MultiMedia
Access to the iPod video library is not available through the Simulator. This application has to be run on a real device. Therefore, you must be a registered member of the iPhone Developer Program and have the required certificates and provisioning profiles to install on devices. See Appendix D for the Apple Reference Guides for iTunes Connect.
development steps: an application that Plays video from the iPod library To create this video player application, execute the following steps:
1 .
Start Xcode and create a View-based application for the iPad and name it iPodLibraryMoviePlayer. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
2 .
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File and select UIViewController subclass class. Make sure that only “With XIB for user interface” is checked, and name the file MediaPlaybackViewController. This plays the movie that you have selected from the iPod video library.
3 .
Now choose File ➪ New File, choose Cocoa Touch Class, and select Objective-C class, and make sure that it is a subclass of UIView and name the file MediaPlaybackView. This is the container view that MediaPlaybackViewController uses to play the movie in.
4 .
Choose File ➪ New File and select Objective-C class; and make sure that it is a subclass of NSObject and name the file iPodLibrary. This is your link into the iPod video library.
5 .
Double-click the iPodLibraryMoviePlayerViewController.xib file in the group NIB Files to launch Interface Builder (see Figure 8-25).
fiGUre 8-25
An Application That Plays Video from the iPod Library
❘ 293
6. 7.
8.
Select a UIToolbar and drag it over the main view, place it at the bottom, and release the mouse. Click the Item button and tap the Delete key to delete the button.
9.
Select a UISegmentedControl and drag it over the bottom toolbar and release the mouse. Double-click the left button and enter AVPlayer. Double-click the other button and enter MPPlayer.
10.
11.
12.
From the main menu, select Tools ➪ Library and choose the Objects tab. Select a UINavigationBar and drag it over the main view, place it at the top, and release the mouse. Double-click the title and enter Movies.
Select a Flexible Space Bar Button Item and drag it over the bottom toolbar to the left of the AVPlayer button. Repeat again but place the control to the right of the MPPlayer button. Select a UITableView and drag it over the middle of the view and release the mouse. Your view should look like Figure 8-26. Select Tools ➪ Library, choose Classes at the top, and scroll to and select your iPodLibraryMovie PlayerViewController class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure 8-27:
13.
➤➤
movieTableView (as a UITableView instead of id type)
➤➤
playerSwitch (as a UISegmentedControl instead of id type)
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then select Merge from the second pop-up. A window will appear with your new additions to iPodLibraryMoviePlayerView Controller.h on the left and the original template on the right (see Figure 8-28): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 8-26
Figure 8-27
294
❘ Chapter 8 Multimedia
Figure 8-28
There were no actions entered through Interface Builder, so no modifications to the iPodLibraryMovie PlayerViewController. m template are needed. You now have an Objective-C template that will hold your application’s logic. Now you have to make all the connections to:
14.
➤➤
Identify the UITableView as movieTableView.
➤➤
Identify the UISegmentedControl as playerSwitch.
To make the connection to identify the UITableView as movieTableView, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 8-29:
15.
16.
17.
From the right of the File’s Owner Inspector, control-drag from the circle next to movieTableView to UITableView movie TableView until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the playerSwitch field you also declared above.
Figure 8-29
Now the Table view has two requirements. It needs a delegate and a datasSource to work properly. Control-click the Table view to bring up the Table View Inspector (see Figure 8-30). Control-drag from the circle next to dataSource to the File’s Owner icon until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been
Figure 8-30
An Application That Plays Video from the iPod Library
❘ 295
made. Repeat for the delegate outlet to the File’s Owner icon. As shown in Figure 8-31, both the Table View and File’s Owner Inspectors indicate that the connections have been made. Choose File ➪ Save. 18.
Back in Xcode, double-click the MediaPlaybackViewController.xib file in the group NIB Files to bring up the view in Interface Builder.
19.
From the main menu, select Tools ➪ Library and choose the Objects tab.
20.
Select a UINavigationBar and drag it over the main view, place it at the top, and release the mouse.
21.
Select a UIBarButtonItem and drag it over the navigation bar, placing it at the left, and release the mouse. Select Pause for the Identifier.
22.
Select a UIBarButtonItem and drag it over the navigation bar, placing it at the right, and release the mouse. Select Done for the Identifier.
23.
Select a UIView and drag it over the main view and release the mouse. Select Black Color for the Background.
24.
Choose Tools ➪ Identity Inspector and select MediaPlaybackView for the class. Your completed interface should look like Figure 8-32.
Figure 8-31
25.
Figure 8-32
Select Tools ➪ Library, choose Classes at the top, and scroll to and select your MediaPlayback ViewController class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure 8-33:
26.
➤➤
navigationBar (as a UINavigationBar instead of id type)
➤➤
playbackView (as a MediaPlaybackView instead of id type)
At the bottom, choose the Actions button. Click the + and add the following actions, as shown in Figure 8-34: ➤➤
changePlaybackState
➤➤
done
296
❘ Chapter 8 Multimedia
Figure 8-33
27.
28.
Figure 8-34
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then select Merge from the second pop-up. A window will appear with your new additions to MediaPlaybackViewController.m on the left and the original template on the right (see Figure 8-35): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
The next file to merge your new additions is MediaPlaybackViewController.h that has your additions on the left and the original template on the right (see Figure 8-36): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
An Application That Plays Video from the iPod Library
Figure 8-35
Figure 8-36
❘ 297
298
❘ Chapter 8 Multimedia
You now have an Objective-C template that will hold your application’s logic. Before you begin programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UINavigationBar as navigationBar.
➤➤
Identify the MediaPlaybackView as playbackView.
29.
To make the connection to identify the UINavigationBar as navigationBar, control-click on the File’s Owner icon to bring up the Inspector, shown in Figure 8-37.
30.
From the right of the File’s Owner Inspector, control-drag from the circle next to navigationBar to UINavigationBar navigationBar until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the playbackView.
31.
From the right of the File’s Owner Inspector, control-drag from the circle next to changePlayback State to UIBarButtonItem changePlaybackState until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for done to UIBarButtonItem Done (see Figure 8-38).
Figure 8-37
Figure 8-38
Finally, you have to import the following frameworks into your project in order for it to build successfully: ➤➤
AVFoundation.framework
➤➤
MediaPlayer.framework
To import the frameworks, control-click the Frameworks group and select Add ➪ Existing Frameworks, and then select the preceding frameworks and click Add (see Figure 8-39). You have completed all the user interface additions and connections that are needed for this application. Now it is time to enter your logic.
Source Code Listings for an Application That Plays Video from the iPod Library For this application, the iPodLibraryMoviePlayerApp Delegate.h file is not modified; however, when you quit an application in iOS 4, it doesn’t terminate. It stays active in the background. For this application, you have to ensure that the play-pause button correctly identifies the application’s current state. You do not want the pause button to appear when there is no movie playing because that doesn’t indicate the current state of the player; you want the play button to appear.
Figure 8-39
An Application That Plays Video from the iPod Library
❘ 299
iPodLibraryMoviePlayerAppDelegate.m Modifications to the Template When the application awakes from the background, the applicationWillEnterForeground method is called, which is where you restore the application to its proper state. Listing 8-16 shows the complete iPodLibraryMoviePlayerAppDelegate.m file. The additions to the template are in bold. Listing 8-16: The complete iPodLibraryMoviePlayerAppDelegate.m file (/Chapter8/ iPodLibraryMoviePlayer/Classes/iPodLibraryMoviePlayerAppDelegate.m) #import “iPodLibraryMoviePlayerAppDelegate.h” #import “iPodLibraryMoviePlayerViewController.h” #import “MediaPlaybackViewController.h” @implementation iPodLibraryMoviePlayerAppDelegate @synthesize window; @synthesize viewController; #pragma mark #pragma mark Application lifecycle - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. // Add the view controller’s view to the window and display. [window addSubview:viewController.view]; [window makeKeyAndVisible]; return YES; } - (void)applicationWillResignActive:(UIApplication *)application { /* Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. */ } - (void)applicationDidEnterBackground:(UIApplication *)application { /* Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. If your application supports background execution, called instead of applicationWillTerminate: when the user quits. */
300
❘ Chapter 8 Multimedia
} - (void)applicationWillEnterForeground:(UIApplication *)application { /* Called as part of transition from the background to the inactive state: here you can undo many of the changes made on entering the background. */ MediaPlaybackViewController *mvc = [viewController mediaPlaybackViewController]; [[[mvc navigationBar] topItem] setLeftBarButtonItem:[mvc playButton]]; } - (void)applicationDidBecomeActive:(UIApplication *)application { /* Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. */ } - (void)applicationWillTerminate:(UIApplication *)application { /* Called when the application is about to terminate. See also applicationDidEnterBackground:. */ } #pragma mark #pragma mark Memory management - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { /* Free up as much memory as possible by purging cached data objects that can be recreated (or reloaded from disk) later. */ } - (void)dealloc { [viewController release]; [window release]; [super dealloc]; }
@end
iPodLibraryMoviePlayerViewController.h Modifications to the Template In addition to the movieTableView and playerSwitch declared in Interface Builder, you need to declare an NSArray to hold the movieList returned from the iPod library and an instance reference to the MediaPlaybackViewController class. Remember that this class has to act as the delegate and data source for the Table view, so the UITableViewDelegate and UITableViewDataSource protocols are listed.
Listing 8-17 shows the complete iPodLibraryMoviePlayerViewController.h file.
An Application That Plays Video from the iPod Library
❘ 301
Listing 8-17: The complete iPodLibraryMoviePlayerViewController.h file (/Chapter8/
iPodLibraryMoviePlayer/Classes/iPodLibraryMoviePlayerViewController.h) #import @class MediaPlaybackViewController; @interface iPodLibraryMoviePlayerViewController : UIViewController { UITableView *movieTableView; UISegmentedControl *playerSwitch; MediaPlaybackViewController *mediaPlaybackViewController; NSArray* movieList; } @property (nonatomic, retain) IBOutlet UITableView *movieTableView; @property (nonatomic, retain) IBOutlet UISegmentedControl *playerSwitch; @property (nonatomic, retain) MediaPlaybackViewController *mediaPlaybackViewController; @property (nonatomic, retain) NSArray* movieList; @end
iPodLibraryMoviePlayerViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the iPodLibraryMoviePlayerViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 8-18). Listing 8-18: Addition of @synthesize #import #import #import #import
“iPodLibraryMoviePlayerViewController.h” “MediaPlaybackViewController.h” “iPodLibrary.h” <MediaPlayer/MediaPlayer.h>
@implementation iPodLibraryMoviePlayerViewController @synthesize @synthesize @synthesize @synthesize
movieTableView; playerSwitch; movieList; mediaPlaybackViewController;
When the iPodLibraryPlayerViewController view initializes in the viewDidLoad method, the iPod library is queried for a list of movies on the device (see Listing 8-19). Listing 8-19: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setMovieList:[iPodLibrary movieList]]; }
302
❘ Chapter 8 Multimedia
Because this class has a Table view it uses for displaying the list of movies, the number of sections and the number of rows for each section are identified. The actual data item to be displayed in the list is handled in the tableView:CellForRowAt method. As this is just a simple list of movies, only one section is needed (see Listing 8-20). Listing 8-20: The Table view data source methods #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
// Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [movieList count]; }
// Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell. [[cell textLabel] setText:[iPodLibrary movieTitle:[movieList objectAtIndex:[indexPath row]]]]; return cell; }
When the user taps a Table view cell to choose the movie, the tableView:didSelectRowAtIndexPath method will identify the selection and open the proper movie viewer based on whether AVPlayer or MPPlayer was selected. If the AVPlayer is selected, our custom MediaPlaybackViewController, which works with the MediaPlaybackView, is called to display the movie. If the MPPlayer is selected, it uses Apple’s custom MPMoviePlayerViewController to display the movie (see Listing 8-21). Listing 8-21: The tableView:didSelectRowAtIndexPath method #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
An Application That Plays Video from the iPod Library
❘ 303
NSURL *aURL = [movieList objectAtIndex:[indexPath row]]; if([playerSwitch selectedSegmentIndex] == 0) { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; MediaPlaybackViewController *controller = [[MediaPlaybackViewController alloc] initWithNibName:@”MediaPlaybackViewController” bundle:nil]; [controller setMovieURL:aURL]; [controller setMovieTitle:[[cell textLabel] text]]; [controller setStartPlayer:YES]; [self setMediaPlaybackViewController:controller]; [self presentModalViewController: controller animated: YES]; }else { MPMoviePlayerViewController *player = [[MPMoviePlayerViewController alloc] initWithContentURL:aURL]; [self presentModalViewController:player animated:YES]; [player release]; } [tableView deselectRowAtIndexPath: indexPath animated: YES]; }
The iPodLibraryMoviePlayerViewController.m class is now complete. Listing 8-22 shows the complete implementation. Listing 8-22: The complete iPodLibraryMoviePlayerViewController.m file (/Chapter8/ iPodLibraryMoviePlayer/Classes/iPodLibraryMoviePlayerViewController.m) #import #import #import #import
“iPodLibraryMoviePlayerViewController.h” “MediaPlaybackViewController.h” “iPodLibrary.h” <MediaPlayer/MediaPlayer.h>
@implementation iPodLibraryMoviePlayerViewController @synthesize @synthesize @synthesize @synthesize
movieTableView; playerSwitch; movieList; mediaPlaybackViewController;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setMovieList:[iPodLibrary movieList]]; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view.
304
❘ Chapter 8 Multimedia
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
// Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [movieList count]; }
// Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell. [[cell textLabel] setText:[iPodLibrary movieTitle:[movieList objectAtIndex:[indexPath row]]]]; return cell; }
#pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSURL *aURL = [movieList objectAtIndex:[indexPath row]]; if([playerSwitch selectedSegmentIndex] == 0) { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; MediaPlaybackViewController *controller = [[MediaPlaybackViewController alloc] initWithNibName:@”MediaPlaybackViewController” bundle:nil]; [controller setMovieURL:aURL]; [controller setMovieTitle:[[cell textLabel] text]]; [controller setStartPlayer:YES]; [self setMediaPlaybackViewController:controller]; [self presentModalViewController: controller animated: YES]; }else { MPMoviePlayerViewController *player = [[MPMoviePlayerViewController alloc] initWithContentURL:aURL]; [self presentModalViewController:player animated:YES]; [player release];
An Application That Plays Video from the iPod Library
❘ 305
} [tableView deselectRowAtIndexPath: indexPath animated: YES]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setMovieTableView:nil]; [self setPlayerSwitch:nil]; [self setMediaPlaybackViewController:nil]; } - (void)dealloc { [movieTableView release]; [playerSwitch release]; [mediaPlaybackViewController release]; [super dealloc]; } @end
MediaPlaybackViewController.h Modifications to the Template You declared two outlets in Interface Builder: navigationBar and playbackView. You must now define the properties for these variables in order to get and set their values. The IBOutlet was moved to the property declaration. In order to toggle the play-pause buttons, you have to define variables for them so the user interface shows the proper button. The play-pause buttons are created in their own method for clarification when reading the code. You also have to keep track of the movie URL and title, and an instance of the AVPlayer is needed. Listing 8-23 shows the complete MediaPlaybackViewController.h file. Listing 8-23: The complete MediaPlaybackViewController.h file (/Chapter8/ iPodLibraryMoviePlayer/Classes/MediaPlaybackViewController.h) #import @class MediaPlaybackView; @class AVPlayer; @interface MediaPlaybackViewController : UIViewController { UINavigationBar *navigationBar; MediaPlaybackView *playbackView; UIBarButtonItem *pauseButton; UIBarButtonItem *playButton; NSURL *movieURL; NSString *movieTitle; AVPlayer* avPlayer; BOOL startPlayer; } @property (nonatomic, retain) IBOutlet UINavigationBar *navigationBar;
306
❘ Chapter 8 Multimedia
@property (nonatomic, retain) IBOutlet MediaPlaybackView *playbackView; @property @property @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) UIBarButtonItem *pauseButton; retain) UIBarButtonItem *playButton; retain) NSURL *movieURL; retain) NSString *movieTitle; retain) AVPlayer* avPlayer; getter=isStartPlayer) BOOL startPlayer;
- (IBAction)changePlaybackState:(id)sender; - (IBAction)done:(id)sender; - (void)createPlayPauseButtons;
@end
MediaPlaybackViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the MediaPlaybackViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 8-24). Listing 8-24: Addition of @synthesize #import “MediaPlaybackViewController.h” #import “MediaPlaybackView.h” #import
@implementation MediaPlaybackViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
navigationBar; playbackView; movieURL; movieTitle; avPlayer; startPlayer; playButton; pauseButton;
When the MediaPlaybackViewController view initializes, an instance of the AVPlayer is created with the passed-in URL. The play-pause buttons are created and the navigation bar is assigned the title of the movie (see Listing 8-25). Listing 8-25: The view initialization methods #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setAvPlayer:[[AVPlayer allocWithZone:[self zone]] initWithURL:movieURL]]; [playbackView setPlayer:[self avPlayer]];
An Application That Plays Video from the iPod Library
❘ 307
[self createPlayPauseButtons]; [self setWantsFullScreenLayout:YES]; [[navigationBar topItem] setTitle:[self movieTitle]]; } - (void)createPlayPauseButtons { UIBarButtonItem *play = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:@selector(changePlaybackState:)]; UIBarButtonItem *pause = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause target:self action:@selector(changePlaybackState:)]; [self setPlayButton:play]; [self setPauseButton:pause]; [play release]; [pause release]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if([self isStartPlayer]) { [self changePlaybackState:playButton]; } }
There are two action methods, changePlaybackState and done. The changePlaybackState is called when the play-pause button is pressed. Depending on whether the player is paused or played, the appropriate button is displayed (see Listing 8-26). Listing 8-26: The action methods #pragma mark #pragma mark Button Action handlers - (IBAction)changePlaybackState:(id)sender { if(sender == pauseButton) { [avPlayer pause]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; } else { [avPlayer play]; [[navigationBar topItem] setLeftBarButtonItem:pauseButton]; } } - (IBAction)done:(id)sender { [avPlayer pause]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; [self dismissModalViewControllerAnimated: YES]; }
The MediaPlaybackViewController.m class is now complete. Listing 8-27 shows the complete implementation.
308
❘ Chapter 8 Multimedia
Listing 8-27: The complete MediaPlaybackViewController.m file (/Chapter8/
iPodLibraryMoviePlayer/Classes/MediaPlaybackViewController.m) #import “MediaPlaybackViewController.h” #import “MediaPlaybackView.h” #import
@implementation MediaPlaybackViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
navigationBar; playbackView; movieURL; movieTitle; avPlayer; startPlayer; playButton; pauseButton;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setAvPlayer:[[AVPlayer allocWithZone:[self zone]] initWithURL:movieURL]]; [playbackView setPlayer:[self avPlayer]]; [self createPlayPauseButtons]; [self setWantsFullScreenLayout:YES]; [[navigationBar topItem] setTitle:[self movieTitle]]; } - (void)createPlayPauseButtons { UIBarButtonItem *play = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:@selector(changePlaybackState:)]; UIBarButtonItem *pause = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause target:self action:@selector(changePlaybackState:)]; [self setPlayButton:play]; [self setPauseButton:pause]; [play release]; [pause release]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if([self isStartPlayer]) { [self changePlaybackState:playButton]; } } #pragma mark -
An Application That Plays Video from the iPod Library
#pragma mark Button Action handlers - (IBAction)changePlaybackState:(id)sender { if(sender == pauseButton) { [avPlayer pause]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; } else { [avPlayer play]; [[navigationBar topItem] setLeftBarButtonItem:pauseButton]; } } - (IBAction)done:(id)sender { [avPlayer pause]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; [self dismissModalViewControllerAnimated: YES]; } #pragma mark #pragma mark Additional view handlers - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [avPlayer pause]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark #pragma mark Memory Management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [super viewDidUnload]; // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setNavigationBar:nil]; [self setPlaybackView:nil]; [self setMovieURL:nil]; [self setMovieTitle:nil]; [self setAvPlayer:nil]; [self setPlayButton:nil]; [self setPauseButton:nil]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; } - (void)dealloc { [navigationBar release]; [playbackView release]; [movieURL release]; [movieTitle release]; [avPlayer release];
❘ 309
310
❘ Chapter 8 Multimedia
[playButton release]; [pauseButton release]; [super dealloc]; } @end
MediaPlaybackView.h Modifications to the Template The MediaPlaybackView is the container in which the AVPlayer plays the movie. Listing 8-28 shows the complete MediaPlaybackView.h file. Listing 8-28: The complete MediaPlaybackView.h file (/Chapter8/iPodLibraryMoviePlayer/ Classes/MediaPlaybackView.h) #import @class AVPlayer; @interface MediaPlaybackView : UIView { } @property (nonatomic, retain) AVPlayer* player; @end
MediaPlaybackView.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the MediaPlaybackViewController.m template. The class just has to provide a view layer in which the player resides. The class itself is a fully self-contained view controller designed to play video. Listing 8-29 shows the complete MediaPlaybackView.m file. Listing 8-29: The complete MediaPlaybackView.m file (/Chapter8/iPodLibraryMoviePlayer/ Classes/MediaPlaybackView.m) #import “MediaPlaybackView.h” #import
@implementation MediaPlaybackView
+ (Class)layerClass { return [AVPlayerLayer class]; } - (AVPlayer*)player { return [(AVPlayerLayer*)[self layer] player]; } - (void)setPlayer:(AVPlayer*)player { [(AVPlayerLayer*)[self layer] setPlayer:player]; } @end
An Application That Plays Video from the iPod Library
❘ 311
iPodLibrary.h Modifications to the Template The iPodLibrary class is the interface to the iPod library; it has two factory methods that return the movie list and title of the movie for a specific URL created internally by Apple. Listing 8-30 shows the complete iPodLibrary.h file. Listing 8-30: The complete iPodLibrary.h file (/Chapter8/iPodLibraryMoviePlayer/Classes/
iPodLibrary.h)
#import @interface iPodLibrary : NSObject { } + (NSArray *)movieList; + (NSString *)movieTitle:(NSURL *)aURL; @end
iPodLibrary.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the iPodLibrary.m template. In order to get the list of movies that is in the iPod library, you have to create an MPMediaQuery. To have only video returned, you have to set a query filter of the type MPMediaTypeAny and MPMediaTypeAnyAudio. What is returned is a list of MPMediaItems in which you will store the MPMediaItemPropertyAssetURL, the location of the song in the iPod library. The title, as an AVAsset, is retrieved, and its metadata is queried for the title and then returned to the calling method. Listing 8-31 shows the complete iPodLibrary.m file. Listing 8-31: The complete iPodLibrary.m file (/Chapter8/iPodLibraryMoviePlayer/Classes/
iPodLibrary.m)
#import “iPodLibrary.h” #import <MediaPlayer/MediaPlayer.h> #import
@implementation iPodLibrary + (NSArray *)movieList { NSMutableArray *list = [[[NSMutableArray allocWithZone:NULL] init] autorelease]; MPMediaQuery *query = [[MPMediaQuery alloc] init]; [query addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithInteger: (MPMediaTypeAny ^ MPMediaTypeAnyAudio)] forProperty:MPMediaItemPropertyMediaType]]; for (MPMediaItem* item in [query items]) { [list addObject:[item valueForProperty: MPMediaItemPropertyAssetURL]]; } [query release]; return list;
312
❘
chaPter 8 MultiMedia
} + (NSString *)movieTitle:(NSURL *)aURL { NSString *aTitle = nil; if(aURL) { AVAsset* aAsset = [[AVURLAsset allocWithZone:NULL] initWithURL:aURL options:nil]; for (AVMetadataItem* metadata in [aAsset commonMetadata]) { if ([[metadata commonKey] isEqualToString:AVMetadataCommonKeyTitle]) { aTitle = [metadata stringValue]; break; } } } return aTitle; } @end
test your application Now that you have everything completed, choose the device from the Xcode panel, make sure it is connected, and click Run and Build to try out your app. It should give you the results described at the beginning of the section “An Application That Plays Video from the iPod Library.”
Remember that the iPod library is not accessible from within the Simulator, it is only accessible from the device, so the app won’t work in the Simulator, as there will be no media listed.
sUmmary This was fun! One of the great things about the iPhone, iPad, and iPod touch is the media experience they offer. Whether listening to music or watching movies, these devices have revolutionized the way we interact with the arts. In this chapter, the first application focused on getting audio from the iPod library. From the library, a playlist was created. This music queue could be listened to as is or the list could be accessed and a favorite song selected. In the second application, the iPod library was queried for movies; and two approaches to viewing were demonstrated. Introduced with iOS 4.0, the AVPlayer requires a bit more coding but works with the AV Foundation framework; the second option works with a iOS 3.2 addition, the MPMoviePlayerViewController. With this class, very little work is required, providing an easy way to present a full-featured player with a minimal amount of coding.
9
application Preferences What’s in this chaPter? ➤➤
The role of the Settings.bundle file in processing application preferences
➤➤
How to read from and write to the application preferences
➤➤
Using child panes to define a hierarchy of application preferences
Applications can be configured through preferences. In desktop applications, preference activity is coded as part of the application itself. With the iPhone and iPad, these settings are not part of the actual applications; rather, they are found in a separate Settings application. In order to integrate with the Settings application, you must include a Settings.bundle fi le in your application. This bundle works directly with the Settings application, which is responsible for maintaining the values that are provided to your application. At runtime, your application can easily retrieve these settings. In this chapter you develop two applications that store information that can be modified by the standard Settings application (found on the iPhone and iPad devices). The fi rst application will retrieve fi rst name, last name, and phone number information that is located in the Settings.bundle and display the values in a Table view. The second application will retrieve fi rst name, last name, and bank account type information that is located in the Settings.bundle and display the values in a Table view. What makes the second application different is that the bank account information is stored in a separate settings page fi le, demonstrating the hierarchy of child panes from the main preference root.
aPPlication confiGUration Application preferences in the Settings application are displayed in a hierarchical layout. When the Settings application is launched, every custom application is checked for the presence of a Settings.bundle. For each Settings.bundle found, that application’s name and icon is added to the list that appears on the main page of the Settings application. When the user taps on
314
❘ Chapter 9 Application Preferences
an application’s name in the list, the Root.plist of that application is loaded and the preferences are displayed. To change the value of a specific preference, the user taps on it. To include an icon that will be displayed on the main page of the Settings application, you need to include a 29 × 29 pixel file named Icon-Settings.png. If you do not include this file in your project, the default Icon.png file will be used.
Guidelines for Application Preferences The preferences that you want to be accessible through the Settings application are those that will rarely change. Account information would be one example. You should also avoid duplicating these same preferences in your application itself. Seeing the same preferences in both the application and the Settings application will confuse users. For preferences that are likely to be changed frequently, you can provide buttons on a view in your application. By flipping that view around, users can change them immediately. There is no hard-and-fast rule about where you should place your application’s preferences, but remember the following: ➤➤
Keep user convenience in mind.
➤➤
Do not duplicate preferences in both your application and the Settings application.
Preference Element Types How a preference is displayed depends on its element type. Table 9-1 describes the element types that are available. Table 9-1: Preference Element Types Element Type
Description
Text Field
For a title or editable text field. The key is PSTextFieldSpecifier.
Title
For read-only strings. The key is PSTitleValueSpecifier.
Toggle Switch
For two-value representation. The key is PSToggleSwitchSpecifier.
Slider
For a specified min and max range. The key is PSSliderSpecifier.
Multi Value
For a list of values. The key is PSMultiValueSpecifier.
Group
For group organization and display. The key is PSGroupSpecifier.
Child Pane
For implementing hierarchical preferences. The key is PSChildPaneSpecifier.
Setting Simple Preferences
❘ 315
Implementing Preference Hierarchies Application preferences can be implemented using a hierarchy of settings pages that the user can navigate to. To implement this, each child pane has to reside in its own settings page or plist file. There is only one requirement concerning settings page files; the main page in the hierarchy always stores its preferences in the Root.plist.
Accessing the Application’s Preferences While the Settings application is where applications preferences are modified, to retrieve a value, your application uses the NSUserDefaults class, as shown in Listing 9-1. Listing 9-1: Setting the title of a view with contact preference NSString *kContactPreferenceLKey = @”contact_preference”; . . . [self setTitle:[[NSUserDefaults standardUserDefaults] stringForKey:kContactPreferenceLKey]];
Setting Simple Preferences In this Navigation-based application, the following preferences have been set and will be displayed in the cells of the UITableView: ➤➤
Contact phone number
➤➤
First name
➤➤
Last name
This app presents the aforementioned values, which have been stored in the Settings.bundle (see Figure 9-1). In the Settings application, there will be an entry for the SimplePreference application, as shown in Figure 9-2.
When the user taps on SimplePreference, the settings that are customizable are presented for modification, as shown in Figure 9-3.
Development Steps: Setting Simple Preferences To create this application, execute the following steps.
1.
2.
Start Xcode and create a Navigation-based application named SimplePreference. If you need to see this step, please see Appendix A for the steps to begin a Navigation-based application.
Figure 9-1
From the main menu, choose File ➪ New File, and then choose Resource ➪ Settings Bundle, as shown in Figure 9-4. Click Next take the default name Settings.bundle and then click Finish.
316
❘ Chapter 9 Application Preferences
Figure 9-2
Figure 9-4
Figure 9-3
Setting Simple Preferences
❘ 317
3.
Click the disclosure triangle to reveal Root.plist. Select Root.plist and then select Root to highlight it in the editor window as shown in Figure 9-5.
4.
With the Root still selected in the editor window, select from the main menu, View ➪ Property List Type ➪ iPhone Settings plist. You will see the Root changed to iPhone Settings Schema as shown in Figure 9-6.
Figure 9-5
Figure 9-6
318
❘ Chapter 9 Application Preferences
You now are ready to begin adding your application’s preferences. There are three preferences to be stored: ➤➤
First Name
➤➤
Last Name
➤➤
Phone Number
To create the structure, follow these steps: Select Strings Filename and Click the + button on the right.
1. 2.
3.
Click the disclosure triangle on the Preference Items key to reveal the default items that were generated for you.
4.
Click to the left of the disclosure triangle on Item 2 (Toggle Switch – Enabled) and when selected, on your keyboard tap the Delete key to delete it.
5. 6.
Once again, tap the Delete key on your keyboard again to delete the Item 2 (Slider) entry also.
7. 8. 9.
With Item 2 (Text Field – Name) selected, click the + button and choose Group.
Choose Settings Page Title for the key and enter in the value section SimplePreference, which is the title of your application.
With Item 1 (Text Field – Name) now selected, choose Edit ➪ Copy and Edit ➪ Paste from the main menu. With Item 3 (Group – ) selected, click the + button to add another Text Field. The template for your three preferences is created as shown in Figure 9-7. Click File ➪ Save from the main menu to save your changes.
Figure 9-7
Setting Simple Preferences
❘ 319
To enter the detail for the structure you just created, follow these steps:
1. 2.
Click the disclosure triangle on the left of Item 0 and enter Account Info for the Title.
3.
Click the disclosure triangle on the left of Item 2 and enter Last Name for the Title, last_name_ preference for the Identifier, and Jones for the Default Value.
Click the disclosure triangle on the left of Item 3 and enter Contact Info for the Title.
4. 5.
6.
With the contact_preference Identifier highlighted, click the + button, add Keyboard Type, and select Numbers and Punctuation.
7.
With Keyboard Type highlighted, click the + button and set the Default Value to 312-555-1212, as shown in Figure 9-8.
8.
Click File ➪ Save from the main menu.
Click the disclosure triangle on the left of Item 1 and enter First Name for the Title, first_name_ preference for the Identifier, and Joe for the Default Value.
Click the disclosure triangle on the left of Item 4 and enter Phone for the Title and contact_preference for the Identifier.
Figure 9-8
With your preferences created, it is now time to enter the logic that will read them.
320
❘ Chapter 9 Application Preferences
Source Code Listings for Setting Simple Preferences For this application, SimplePreferenceAppDelegate.h or SimplePreferenceAppDelegate.m are not modified and are used as generated. To start, you create an object AppPreferences that reads from the Settings.bundle and sets the system defaults with the values. From that point on, when a user changes a value in the Settings application, the default will be updated with the new value that was entered. To create the AppPreferences.m file, click New File… from the main menu. Choose Objective-C class, and NSObject as the Subclass object as shown in Figure 9-9.
Figure 9-9
The design behind AppPreferences is simple: Whatever object creates an instance of AppPreferences should only have to pass it the preference keys it needs to store. In this application, it is the RootViewController that will be storing three preferences, so you define three keys: NSString *kFirstNamePreferenceKey = @”first_name_preference”; NSString *kLastNameLPreferenceLKey = @”last_name_preference”; NSString *kContactPreferenceLKey = @”contact_preference”;
The RootViewController will then create an instance of the AppPreferences object, passing those parameters: [self setAppPreferences:[[AppPreferences alloc] initWithKeys:[NSArray arrayWithObjects: kFirstNamePreferenceKey, kLastNameLPreferenceLKey, kContactPreferenceLKey, nil]]];
Setting Simple Preferences
❘ 321
At that point, the system defaults are initialized, and will work with the Settings application to get the preferences saved by the user, who has the option to change them. To retrieve them, the caller only has to know the identifier of the preference, and the value will be returned: [self setTitle:[[self appPreferences] valueForKey:kContactPreferenceLKey]];
The design of the AppPreferences object is straightforward; it has two properties and three methods. The complete interface is shown in Listing 9-2. The complete implementation is shown in Listing 9-3. Listing 9-2: The complete AppPreferences.h file (Chapter09/SimplePreference/Classes/ AppPreferences.h) #import @interface AppPreferences : NSObject { NSArray *keys; NSDictionary *preferences; } @property (nonatomic, retain) NSArray *keys; @property (nonatomic, retain) NSDictionary *preferences; - initWithKeys:(NSArray *)aKeys; - (NSDictionary *)initPreferencesWithKeyArray:(NSArray *)aKeys; - (NSDictionary *)bundleSettings; @end
Listing 9-3: The complete AppPreferences.m file (Chapter09/SimplePreference/Classes/ AppPreferences.m) #import “AppPreferences.h” @implementation AppPreferences @synthesize keys; @synthesize preferences; - initWithKeys:(NSArray *)aKeys { [self setKeys:aKeys]; [self setPreferences:[self initPreferencesWithKeyArray:aKeys]]; return self; } - (NSDictionary *)initPreferencesWithKeyArray:(NSArray *)aKeys { NSDictionary *settings = [self bundleSettings]; NSMutableDictionary *localDefaults = [[NSMutableDictionary alloc] init]; NSString *prefItem = nil; for (prefItem in settings) { NSDictionary *tempDict = [settings objectForKey:prefItem]; id defaultValue = nil; if([[tempDict objectForKey:@”Type”] isEqualToString:@”PSMultiValueSpecifier”]) { NSArray *titles = [tempDict objectForKey:@”Titles”]; NSNumber *index = [tempDict objectForKey:@”DefaultValue”]; defaultValue = [titles objectAtIndex:[index intValue]]; } else {
322
❘ Chapter 9 Application Preferences
defaultValue = [tempDict objectForKey:@”DefaultValue”]; } [localDefaults setObject:tempDict forKey:prefItem]; } [[NSUserDefaults standardUserDefaults] registerDefaults:localDefaults]; [[NSUserDefaults standardUserDefaults] synchronize]; return localDefaults; } - (NSDictionary *)bundleSettings { NSString *pathStr = [[NSBundle mainBundle] bundlePath]; NSString *settingsBundlePath = [pathStr stringByAppendingPathComponent:@”Settings.bundle”]; NSString *finalPath = [settingsBundlePath stringByAppendingPathComponent:@”Root.plist”]; NSMutableDictionary *plistDict = [NSMutableDictionary dictionary]; NSDictionary *settingsDict = [NSDictionary dictionaryWithContentsOfFile:finalPath]; NSMutableArray *array = [settingsDict objectForKey:@”PreferenceSpecifiers”]; NSDictionary *tempDict = nil; NSDictionary *tempChildDict = nil; for(tempDict in array) { if([tempDict objectForKey:@”Key”]) { NSString *typeValueStr = [tempDict objectForKey:@”Type”]; if([typeValueStr isEqualToString:@”PSChildPaneSpecifier”]) { NSString *keyValueStr = [NSString stringWithFormat:@”%@.plist”, [tempDict objectForKey:@”Key”]]; NSString *plist = [settingsBundlePath stringByAppendingPathComponent:keyValueStr]; NSDictionary *childSettings = [NSDictionary dictionaryWithContentsOfFile:plist]; NSArray *childArray = [childSettings objectForKey:@”PreferenceSpecifiers”]; if(childArray) { for(tempChildDict in childArray) { [array addObject:tempChildDict]; if([tempChildDict objectForKey:@”Key”]) { [plistDict setObject:tempChildDict forKey:[tempChildDict objectForKey:@”Key”]]; } } } } else { [plistDict setObject:tempDict forKey:[tempDict objectForKey:@”Key”]]; } } } return plistDict; } - (NSString *)valueForKey:(NSString *)key { NSString *keyValue = [[NSUserDefaults standardUserDefaults] stringForKey:key]; NSDictionary *tempDict = [[self preferences] objectForKey:key];
Setting Simple Preferences
❘ 323
if([[tempDict objectForKey:@”Type”] isEqualToString:@”PSMultiValueSpecifier”]) { NSArray *a = [tempDict objectForKey:@”Titles”]; int index = [keyValue intValue]; keyValue = [a objectAtIndex:index]; } else if(keyValue == nil) { keyValue = [tempDict objectForKey:@”DefaultValue”]; } return keyValue; } @end
In the RootViewController.h file, add an appPreferences variable that will hold the AppPreferences object (see Listing 9-4). Listing 9-4: The complete RootViewController.h file (Chapter09/SimplePreference/Classes/
RootViewController.h)
#import @class AppPreferences; @interface RootViewController : UITableViewController { AppPreferences *appPreferences; } @property (nonatomic, retain) AppPreferences *appPreferences; @end
The preferences will be loaded when the navigation view is loaded via the method viewDidLoad in the RootViewController.m file; and after initialization, the title of the view is set with the contact preferences through the kContactPreferenceKey, as shown in Listing 9-5. Listing 9-5: The viewDidLoad method - (void)viewDidLoad { [super viewDidLoad]; [self setAppPreferences:[[AppPreferences alloc] initWithKeys:[NSArray arrayWithObjects: kFirstNamePreferenceKey, kLastNameLPreferenceLKey, kContactPreferenceLKey, nil]]]; [self setTitle:[[self appPreferences] valueForKey:kContactPreferenceLKey]]; }
The complete listing of the RootViewController.m file is shown in Listing 9-6. Listing 9-6: The complete RootViewController.m file (Chapter09/SimplePreference/Classes/ RootViewController.m) #import “RootViewController.h” #import “AppPreferences.h” NSString *kFirstNamePreferenceKey = @”first_name_preference”; NSString *kLastNameLPreferenceLKey = @”last_name_preference”;
324
❘ Chapter 9 Application Preferences
NSString *kContactPreferenceLKey = @”contact_preference”;
@implementation RootViewController @synthesize appPreferences; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad];
[self setAppPreferences:[[AppPreferences alloc] initWithKeys:[NSArray arrayWithObjects: kFirstNamePreferenceKey, kLastNameLPreferenceLKey, kContactPreferenceLKey, nil]]]; [self setTitle:[[self appPreferences] valueForKey:kContactPreferenceLKey]]; } #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[self appPreferences] keys] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = [indexPath row]; NSArray *keys = [[[self appPreferences] keys] sortedArrayUsingSelector: @selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:row]; NSString *keyValue = [[self appPreferences] valueForKey:key]; NSString *cellText = [NSString stringWithFormat:@”%@=%@”, key, keyValue]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc]
Creating a Child Pane Preference Hierarchy
❘ 325
initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } [[cell textLabel] setText:cellText]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setAppPreferences:nil]; } - (void)dealloc { [super dealloc]; [appPreferences release]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you results that were described at the beginning of the “Setting Simple Preferences” section.
Creating a Child Pane Preference Hierarchy In this Navigation-based application, the following preferences are set and will be displayed in the cells of the UITableView: ➤➤
Bank Account Information
➤➤
First Name
➤➤
Last Name
The difference between the Setting Simple Preferences application in the previous section and this application is the Bank Account preference information is stored it its own .plist file, Account.plist, instead of the Root.plist file. This establishes a hierarchy with the Root.plist being the parent and Account.plist being the child pane. The hierarchy becomes apparent when you view the preferences in the Settings application. The Bank Account preferences will be on its own settings page that is navigated off the Root page.
326
❘ Chapter 9 Application Preferences
This app will present the Bank Account values mentioned above, which have been stored in the Settings. bundle. In the Settings application, there will be an entry for the ChildPanePreference application. When the user taps on ChildPanePreference, the First Name and Last Name are presented for modification as well as a cell labeled Account Details for the Bank Account preferences (see Figure 9-10). When the user taps Account Details, another settings page appears for the Account Type preference, as shown in Figure 9-11. When the user taps on Account Type, three values (Checking, Savings and Mortgage) are the options available for the user to select, as shown in Figure 9-12.
Figure 9-10
Figure 9-11
Figure 9-12
Development Steps: Creating a Child Pane Preference Hierarchy To create this application, execute the following steps:
1.
Start Xcode and create a Navigation-based application named ChildPanePreference. If you need to see this step, please see Appendix A for the steps to begin a Navigation-based application.
2.
From the main menu, choose File ➪ New File, and then choose Resource ➪ Settings Bundle (see Figure 9-13). Click Next and then Finish.
3.
Click the disclosure triangle to reveal Root.plist. Select Root.plist and then select it again to highlight it in the editor window as shown in Figure 9-14.
Creating a Child Pane Preference Hierarchy
Figure 9-13
Figure 9-14
❘ 327
328
❘ Chapter 9 Application Preferences
4.
From the main menu, choose View ➪ Property List Type ➪ iPhone Settings plist. You now are ready to begin adding your application’s preferences (see Figure 9-15).
Figure 9-15
There are two preferences to be stored: ➤➤
First Name
➤➤
Last Name
To create the structure, follow these steps:
1. 2.
Select Strings Filename and click the + button on the right.
3.
Click the disclosure triangle on the Preference Items key to reveal the default items that were generated for you.
4.
Click to the left of the disclosure triangle on Item 2 (Toggle Switch – Enabled) and when selected, tap the Delete key on your keyboard to delete it.
5. 6.
Once again, tap the Delete key on your keyboard to delete the Item 2 (Slider) entry also.
7. 8. 9.
With Item 2 (Text Field – Name) selected, click the + button and choose Group.
Choose Settings Page Title for the key and enter in the values section ChildPanePreference, which is the title of your application.
With Item 1 (Text Field – Name) now selected, choose Edit ➪ Copy and Edit ➪ Paste from the main menu. With Item 3 (Group – ) selected, click the + button to add another Text Field. With Item 4 selected, click the disclosure triangle and click Type.
Creating a Child Pane Preference Hierarchy
10.
Just below Title, select Key, and add BankAccount in the value section.
11. 12. 13.
14.
Click File ➪ Save from the main menu.
❘ 329
Where it says Text Field on the right, in the value section, you will see two triangles: one pointed up, the other pointed down. Click the arrows and select Child Pane. Just below Type select the Title key, add Account Details. With Key selected, click the + key and add Filename for the key and for the value, enter the value BankAccount, as shown in Figure 9-16.
Figure 9-16
5.
To enter the detail for the structure you just created, follow these steps: ➤➤
Click the disclosure triangle on the left of Item 0 and enter Personal Info for the Title.
➤➤
Click the disclosure triangle on the left of Item 1 and enter First Name for the Title, first_ name_preference for the Identifier, and Joe for the Default Value.
➤➤
Click the disclosure triangle on the left of Item 2 and enter Last Name for the Title, last_name_ preference for the Identifier, and Jones for the Default Value.
➤➤
Click the disclosure triangle on the left of Item 3 and enter Account Info for the Title (see Figure 9-17).
➤➤
Click File ➪ Save from the main menu.
330
❘ Chapter 9 Application Preferences
Figure 9-17
6.
In the Child Pane entry, the filename that was entered was BankAccount. That .plist has to be created now: ➤➤
7.
From the main menu, choose File ➪ New File, choose Resource under Mac OS X, and then choose Property List and click Next.
➤➤
Enter BankAccount for the name and click Finish.
➤➤
With BankAccount.plist selected and Root selected in the editor window, from the main menu choose View ➪ Property List Type ➪ iPhone Settings plist. You now are ready to begin adding the child pane’s preferences.
To begin the entry of the BankAccount.plist preferences, select the iPhone Settings Schema: ➤➤
With the iPhone Settings Schema selected, select the Return key and choose Settings Page Title, and enter Bank Account Info.
➤➤
With Settings Page Title selected, click the + key, add Strings Filename, and add BankAccount.
➤➤
With Strings Filename selected, click the + key and add Preference Items (see Figure 9-18).
➤➤
Click File ➪ Save from the main menu.
Creating a Child Pane Preference Hierarchy
❘ 331
Figure 9-18
8.
To enter the detail for the structure you just created, follow these steps: ➤➤
Click to the left of Item 0 and from the keyboard tap the Delete key.
➤➤
Click to the left of Item 0 and from the keyboard tap the Delete key again.
➤➤
Click to the left of Item 0 and from the keyboard tap the Delete key again.
➤➤
Click the disclosure triangle on the left of Item 0 and enter Account Details.
➤➤
Click to the left of Item 1 and from the keyboard tap the Delete key.
➤➤
Click the disclosure triangle on the left of Item 1 and enter Account Type for the Title, account_type_preference for the Identifier, and 0 for the Default Value.
➤➤
With Default Value selected, click the + key and add Titles.
➤➤
Click the disclosure triangle on the left of Titles and add three items: Checking, Savings, and Mortgage.
➤➤
Click the disclosure triangle on the left of Titles to close it; and then with it still selected, click the + button to add Values.
332
❘ Chapter 9 Application Preferences
➤➤
Click the disclosure triangle on the left of Values and add three items: 0, 1, and 2 (see Figure 9-19).
➤➤
Click File ➪ Save from the main menu.
Figure 9-19
With your preferences created, now it’s time to enter the logic that will read them.
Source Code Listings for Creating a Child Pane Preference Hierarchy For this application, ChildPanePreferenceAppDelegate.h or ChildPanePreferenceAppDelegate.m are not modified and are used as generated. To start, you will create an AppPreferences object that will read from the Settings.bundle and set the system defaults with the values. From that point on, when the user changes a value in the Settings application, the default will be updated with the new value that was entered. To create the AppPreferences object, click New File… from the main menu. Choose Objective-C class, and NSObject as the Subclass object, as shown in Figure 9-20. The design behind AppPreferences is the same as described earlier: Whatever object creates an instance of AppPreferences should only have to pass it the preferences it needs to store. In this application, it is the RootViewController that stores three preferences: NSString *kFirstNamePreferenceKey = @”first_name_preference”; NSString *kLastNameLPreferenceLKey = @”last_name_preference”; NSString *kContactPreferenceLKey = @”contact_preference”;
The RootViewController will then create an instance of the AppPreferences object, passing those parameters: [self setAppPreferences:[[AppPreferences alloc] initWithKeys:[NSArray arrayWithObjects: kFirstNamePreferenceKey, kLastNameLPreferenceLKey, kContactPreferenceLKey, nil]]];
Creating a Child Pane Preference Hierarchy
❘ 333
Figure 9-20
The preferences that are set by the Settings application are stored in [NSUserDefaults standardUser Defaults]. To retrieve a preference value, the application only has to use the key identifier; kContact PreferenceLKey, for example, and the value stored will be returned: [self setTitle:[[self appPreferences] valueForKey:kContactPreferenceLKey]];
The design of the AppPreferences object is straightforward. It has two properties and three methods, as shown in Listing 9-7; the complete implementation is shown in Listing 9-8. Listing 9-7: The complete AppPreferences.h file (Chapter09/ChildPanePreference/Classes/ AppPreferences.h) #import @interface AppPreferences : NSObject { NSArray *keys; NSDictionary *preferences; } @property (nonatomic, retain) NSArray *keys; @property (nonatomic, retain) NSDictionary *preferences; - initWithKeys:(NSArray *)aKeys; - (NSDictionary *)initPreferencesWithKeyArray:(NSArray *)aKeys; - (NSDictionary *)bundleSettings; @end
334
❘ Chapter 9 Application Preferences
Listing 9-8: The complete AppPreferences.m file (Chapter09/ ChildPanePreference/Classes/ AppPreferences.m) #import “AppPreferences.h” @implementation AppPreferences @synthesize keys; @synthesize preferences; - initWithKeys:(NSArray *)aKeys { [self setKeys:aKeys]; [self setPreferences:[self initPreferencesWithKeyArray:aKeys]]; return self; } - (NSDictionary *)initPreferencesWithKeyArray:(NSArray *)aKeys { NSDictionary *settings = [self bundleSettings]; NSMutableDictionary *localDefaults = [[NSMutableDictionary alloc] init]; NSString *prefItem = nil; for (prefItem in settings) { NSDictionary *tempDict = [settings objectForKey:prefItem]; id defaultValue = nil; if([[tempDict objectForKey:@”Type”] isEqualToString:@”PSMultiValueSpecifier”]) { NSArray *titles = [tempDict objectForKey:@”Titles”]; NSNumber *index = [tempDict objectForKey:@”DefaultValue”]; defaultValue = [titles objectAtIndex:[index intValue]]; } else { defaultValue = [tempDict objectForKey:@”DefaultValue”]; } [localDefaults setObject:tempDict forKey:prefItem]; } [[NSUserDefaults standardUserDefaults] registerDefaults:localDefaults]; [[NSUserDefaults standardUserDefaults] synchronize]; return localDefaults; } - (NSDictionary *)bundleSettings { NSString *pathStr = [[NSBundle mainBundle] bundlePath]; NSString *settingsBundlePath = [pathStr stringByAppendingPathComponent:@”Settings.bundle”]; NSString *finalPath = [settingsBundlePath stringByAppendingPathComponent:@”Root.plist”]; NSMutableDictionary *plistDict = [NSMutableDictionary dictionary]; NSDictionary *settingsDict = [NSDictionary dictionaryWithContentsOfFile:finalPath]; NSMutableArray *array = [settingsDict objectForKey:@”PreferenceSpecifiers”]; NSDictionary *tempDict = nil; NSDictionary *tempChildDict = nil; for(tempDict in array) { if([tempDict objectForKey:@”Key”]) { NSString *typeValueStr = [tempDict objectForKey:@”Type”]; if([typeValueStr isEqualToString:@”PSChildPaneSpecifier”]) { NSString *keyValueStr = [NSString stringWithFormat:@”%@.plist”, [tempDict objectForKey:@”Key”]];
Creating a Child Pane Preference Hierarchy
❘ 335
NSString *plist = [settingsBundlePath stringByAppendingPathComponent:keyValueStr]; NSDictionary *childSettings = [NSDictionary dictionaryWithContentsOfFile:plist]; NSArray *childArray = [childSettings objectForKey:@”PreferenceSpecifiers”]; if(childArray) { for(tempChildDict in childArray) { [array addObject:tempChildDict]; if([tempChildDict objectForKey:@”Key”]) { [plistDict setObject:tempChildDict forKey:[tempChildDict objectForKey:@”Key”]]; } } } } else { [plistDict setObject:tempDict forKey:[tempDict objectForKey:@”Key”]]; } } } return plistDict; } - (NSString *)valueForKey:(NSString *)key { NSString *keyValue = [[NSUserDefaults standardUserDefaults] stringForKey:key]; NSDictionary *tempDict = [[self preferences] objectForKey:key]; if([[tempDict objectForKey:@”Type”] isEqualToString:@”PSMultiValueSpecifier”]) { NSArray *a = [tempDict objectForKey:@”Titles”]; int index = [keyValue intValue]; keyValue = [a objectAtIndex:index]; } else if(keyValue == nil) { keyValue = [tempDict objectForKey:@”DefaultValue”]; } return keyValue; } @end
In the RootViewController.h file, add an appPreferences variable that will hold the AppPreferences object (see Listing 9-9). Listing 9-9: The complete RootViewController.h file (Chapter09/ ChildPanePreference/ Classes/RootViewController.h) #import @class AppPreferences; @interface RootViewController : UITableViewController { AppPreferences *appPreferences; } @property (nonatomic, retain) AppPreferences *appPreferences; @end
336
❘ Chapter 9 Application Preferences
The preferences will be loaded when the navigation view is loaded via the method viewDidLoad in the RootViewController.m file; and after initialization, the title of the view is set with the contact preferences through the kContactPreferenceKey, as shown in Listing 9-10. Listing 9-10: The viewDidLoad method - (void)viewDidLoad { [super viewDidLoad]; [self setAppPreferences:[[AppPreferences alloc] initWithKeys:[NSArray arrayWithObjects: kFirstNamePreferenceKey, kLastNameLPreferenceLKey, kAccountTypePreferenceLKey, nil]]]; [self setTitle:[[self appPreferences] valueForKey:kAccountTypePreferenceLKey]]; }
The complete listing of the RootViewController.m file is shown in Listing 9-11. Listing 9-11: The complete RootViewController.m file (Chapter09/ ChildPanePreference/ Classes/RootViewController.m) #import “RootViewController.h” #import “AppPreferences.h” NSString *kFirstNamePreferenceKey = @”first_name_preference”; NSString *kLastNameLPreferenceLKey = @”last_name_preference”; NSString *kAccountTypePreferenceLKey = @”account_type_preference”;
@implementation RootViewController @synthesize appPreferences; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setAppPreferences:[[AppPreferences alloc] initWithKeys:[NSArray arrayWithObjects: kFirstNamePreferenceKey, kLastNameLPreferenceLKey, kAccountTypePreferenceLKey, nil]]]; [self setTitle:[[self appPreferences] valueForKey:kAccountTypePreferenceLKey]]; } #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1;
Creating a Child Pane Preference Hierarchy
} // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[self appPreferences] keys] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = [indexPath row]; NSArray *keys = [[[self appPreferences] keys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:row]; NSString *keyValue = [[self appPreferences] valueForKey:key]; NSString *cellText = [NSString stringWithFormat:@”%@=%@”, key, keyValue]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } [[cell textLabel] setText:cellText]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { // Releases the view if it doesn’t have a superview. [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setAppPreferences:nil]; } - (void)dealloc { [super dealloc]; [appPreferences release]; } @end
❘ 337
338
❘ Chapter 9 Application Preferences
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you results that were described at the beginning of the “Creating a Child Pane Preference Hierarchy” section.
Summary This chapter described two approaches to setting an application’s preferences. Both approaches used the Settings.bundle file and defined the preferences in the Root.plist file that are displayed and modified using the separate Settings application. The first application just used the single Root.plist file for the storage and retrieval of the preferences. The second application, in addition to using the Root.plist file for storage and retrieval of the preferences, defined a hierarchy by using a second BankAccount.plist child pane file to store and retrieve preferences related to the type of bank account the user wanted to store. With these two applications and the AppPreference class, your applications can implement preferences with relative ease.
10
Data storage What’s in this chaPter? ➤➤
How to save user-entered information locally in a Property List
➤➤
How to use the Core Data framework
➤➤
Working with the Xcode Data Modeling tool
Imagine you have just designed an application for which you have worked out all the details except one: How can you store information locally? The data you want to store is not application preferences, which are handled by the settings bundle. The information you want to store is user-entered information. iOS 4 provides two very elegant ways for you to handle this: Property lists and Core Data. In this chapter you will build an application that stores the fi rst name, last name, and phone contact information in both of these formats. Building the same application in each format highlights the different data storage techniques you can offer in your own applications. Which format is the right one to use? That depends on your application’s needs. For small quantities of data, the XML property list may be the easiest to implement. For larger amounts of data, the Core Data solution may be appropriate. Keep in mind that the target of your application is a mobile device; it was not designed to house a central warehouse database of enormous size. Apple has provided efficient methods that are more than adequate to handle your data storage needs. For larger storage needs Apple does provide MobileMe as one possible solution.
ProPerty lists Property lists, which use the extension .plist, are simple XML-formatted files that enable the storage of hierarchical data. The advantage to using a property list is that you create data that is formally structured, using a very portable format that allows for data exchange with other platforms and applications.
340
❘ Chapter 10 Data Storage
Uses for Property Lists When you need to store small amounts of data — for example, under a megabyte — the property list provides a very highly structured solution. If your data is in binary form, property lists are not recommended because they are text based. As you will see from the application in this chapter, you can take your own custom objects and store them into a plist file. Your class will have to implement the NSCoding protocol and provide the following methods so the class can be archived and unarchived properly: ➤➤
initWithCoder:
➤➤
encodeWithCoder:
Suggested Data Element Types As previously mentioned, binary data is not a good candidate for property list storage, but the following data element types are suggested: ➤➤
NSArray
➤➤
NSDictionary
➤➤
NSString
➤➤
NSData
➤➤
NSDate
➤➤
NSNumber
If you are going to store your own custom objects, the following must be implemented to enable successful storage and retrieval: ➤➤
The NSCoding protocol
@interface Person : NSObject
➤➤
The initWithCoder and encodeWithCoder methods:
- (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setLastName:[coder decodeObjectForKey:@”firstName”]]; [self setFirstName:[coder decodeObjectForKey:@”lastName”]]; [self setPhone:[coder decodeObjectForKey:@”phone”]]; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self firstName] forKey:@”firstName”]; [coder encodeObject:[self lastName] forKey:@”lastName”]; [coder encodeObject:[self phone] forKey:@”phone”]; }
Notice that the order in which you encode the data has to be the same when you initialize.
Saving and Restoring a Property List The application described in this chapter will read the property list into an NSDictionary, and take the NSDictionary as the data source and write it out to the file.
Core Data
❘ 341
To read the plist file into an NSDictionary, you use the NSData dataWithContentsOfFile method and then use the NSKeyArchiver object to load it into the NSDictionary: NSDictionary *result = nil; NSData *aData = [NSData dataWithContentsOfFile:bundlePath]; if(aData != nil) { result = [NSKeyedUnarchiver unarchiveObjectWithData:aData]; }
To write a NSDictionary out to a file, you first archive the object into NSData format by using the NSKeyedArchiver archivedDataWithRootObject:, and then to write it out to a file you use the writeToFile:atomically: method: NSData *aData = [NSKeyedArchiver archivedDataWithRootObject:plistDict]; BOOL status = [aData writeToFile:bundlePath atomically:YES];
Core Data While the property list provides a key-value solution to data storage, Core Data also provides the following: ➤➤
One-to-many relationships
➤➤
Grouping and filtering
➤➤
Lazy loading of objects
➤➤
Built-in property value validation
Within the Core Data framework, the built-in data functionality is provided through a managed object context.
The Core Data Stack The Core Data stack is a collection of managed objects within a managed object context. This context works with object stores that are represented by a managed object model. The object model collaborates with the managed object context to persist data in a form that resembles a database. The persistent store does not have to be a database, but databases like SQLite work directly with Core Data to persist your data.
Managed Objects The Core Data framework provides the class NSManagedObject, which represents a record in a database. A managed object represents the data that you work with in your application. Every managed object is always linked directly to a managed object context.
Managed Object Context The Core Data framework provides the class NSManagedObjectContext, which represents an object area whose entire purpose is to manage a collection of managed objects. Through a group of model objects, the managed object context provides a view of a persistent store or stores, depending on the model. It is through the managed object context that the features of Core Data are provided, such as validation, relationships, and complete object management.
342
❘ Chapter 10 Data Storage
The Managed Object Model The Core Data framework provides the class NSManagedObjectModel, which is an object representation of a database schema. The model itself is a collection of NSEntityDescriptions, which are an object representation of a database entity. Core Data uses the managed object model as a map directly between managed objects and records in the database.
Persistent Store Coordinator The Core Data framework provides a complete architecture that enables you to take data from your application and store it in a persistent store or series of persistent stores DWM: thus — save, restore, undo. The management of all of these persistent stores is the responsibility of the NSPersistentStoreCoordinator. Your application does not directly interact with this object, but it is responsible for managing all the persistent object stores that are used by your application. The best part about this object is that by the time all the stores reach the managed object context, it will seem as if all the data is coming from one organized store.
Xcode Modeling Tool Core Data managed objects are a collection of entity and property descriptions, or models. Xcode provides a modeling tool that enables the Core Data model to be created graphically. Using the data modeling tool, you can create the entities you need and the properties that are part of that entity. It also enables you to graphically establish relationships between entities. Figure 10-1 is the model used for the Core Data application in this chapter. The entity that is used is a member of a Person class.
Figure 10-1
The Common Premise for Data Storage
❘ 343
Fetching Managed Objects The process of fetching managed objects from a persistent store involves a managed object context and an instance of NSFetchRequest. The fetch request can be filtered by the instance of NSPredicate: [fetchRequest setPredicate:[NSPredicate predicateWithFormat: @”(accountNum IN %@)”, accountNums]];
You can sort the fetch request by using the NSSortDescriptor: NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@”lastName” ascending:YES];
If you are going to display in a Table view, there is a NSFetchResultsController that manages your result set with respect to memory management and the contents of the Table view cells.
Deleting Managed Objects It is important to understand that just because a managed object was created within a managed object context, that doesn’t mean the managed object has been put into the database. Remember that you have to save the managed object context in order to persist the object. This also holds true for deleting managed objects. When you delete a managed object, the managed object context marks it for deletion, but you still have to save the managed object context to remove the managed object from the database, as shown in the following example: if (editingStyle == UITableViewCellEditingStyleDelete) { // Delete the managed object for the given index path NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; [context deleteObject:[[self fetchedResultsController] objectAtIndexPath:indexPath]]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); abort(); } }
The Core Data application described in the following section provides the same functionality as the property list example, but the storage of contact data is handled using the Core Data framework.
The Common Premise for Data Storage The premise for the following applications is the same for property lists as well as Core Data. The difference is how the contact data is stored. The first application uses property lists, sometimes called plists. The second application uses Core Data to store the contact data. When each of these applications launch for the first time, an empty Table view appears. It is empty because no contact information has been added yet. In addition to the empty Table view, there is an Edit button on the left, which enables the deletion of people in the list, and a plus (+) button on the right, which enables the addition of new people. The following steps represent the process of adding, displaying and deleting a contact, which is shown in Figure 10-2. ➤➤
Tapping the plus button to add a new person brings up an entry screen for first name, last name, and contact phone number. After entry of the contact information, the user taps the Save or Cancel button. If Save was selected, the new entry is displayed in the list.
344
❘ Chapter 10 Data Storage
➤➤
Only the last name is displayed in the list. To see contact details, the user taps the name entry and the details are displayed.
➤➤
To delete an entry from the list, the Edit button is tapped and a red minus is presented to the left of the last name.
➤➤
If the red minus is tapped, the minus rotates to a vertical position and a red Delete button appears to the right of the last name.
Figure 10-2
Tapping the Delete button removes the entry from both the list and storage. Tapping Done exits Edit mode and the application is ready to accept more additions. The next section describes the steps to create this application, which uses a property list to store contact information.
Development Steps: A Simple Application Using Property Lists To create the application that stores contact data using the property list, execute the following steps:
1.
Start Xcode and create a Navigation-based application for the iPhone and do not select “Use Core Data for storage.” Name the project PListStorage. If you need to see this step, please refer to Appendix A for the steps to begin a Navigation-based application.
2.
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File, choose Cocoa Touch Class, and select Objective-C class as a subclass of NSObject and name it PropertyList. This is your data storage object, which handles the reading and writing of your data to property lists.
The Common Premise for Data Storage
❘ 345
3.
The data that is going to be stored is a Person object. To create this class, choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it Person.
4.
To be able to add a new Person who will be stored in the property list, you need to create a new view controller. Choose File ➪ New File and select the UIViewController subclass, making sure that “With XIB for user interface” is checked. Name this new class PersonAddViewController.
5.
To display the details of a selected person, you need to create a new view controller. Choose File ➪ New File and select the UIViewController subclass, making sure that “UITableViewController subclass” and “With XIB for user interface” are checked. Name this new class PersonDetailViewController.
6.
Double-click the PersonAddViewController.xib file to launch Interface Builder (see Figure 10-3).
Figure 10-3
7.
From the Interface Builder main menu, select Tools ➪ Attributes Inspector, click the View window, and set the Background option to Group Table View Background Color.
8. 9.
From the main menu, select Tools ➪ Library and choose the Objects tab.
10.
Select a UITextField and drag it over the main view near the top and release the mouse. Repeat this two more times so you wind up with three stacked text fields, as shown in Figure 10-4. Select the first text field and do the following: ➤➤
Enter First Name for the Placeholder.
➤➤
Set the font to size 17.
➤➤
Set Capitalize to Words.
346
❘ Chapter 10 Data Storage
11.
12.
Repeat the preceding steps for the next two fields using Last Name for the placeholder for the second text field, and Phone for the placeholder on the third text field, as shown in Figure 10-5. Select Tools ➪ Library, choose Classes at the top, and scroll to and select your PersonAddViewController class. At the bottom, now choose the Outlets button. Click the + and add
the following outlets (see Figure 10-6):
13.
➤➤
firstNameTextField (as a UITextField instead of id type)
➤➤
lastNameTextField (as a UITextField instead of id type)
➤➤
phoneTextField (as a UITextField instead of id type)
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then Merge from the second pop-up. A window will appear with your new additions to PersonAddViewController.h on the left and the original template on the right (see Figure 10-7): ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
There aren’t any changers to the file, so just close it. You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to:
14.
➤➤
Identify the UITextField as firstNameTextField.
➤➤
Identify the UITextField as lastNameTextField.
➤➤
Identify the UITextField as phoneTextField.
To make the connection to identify the UITextField as firstNameTextField, control-click on the File’s Owner icon to bring up the Inspector as shown in Figure 10-8.
Figure 10-4
Figure 10-5
Figure 10-6
The Common Premise for Data Storage
15.
❘ 347
From the right of the File’s Owner Inspector, control-drag from the circle next to the firstNameTextField to the UITextField firstNameTextField until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the lastNameTextField and phoneTextField (see Figure 10-9).
Figure 10-7
Figure 10-8
16.
Figure 10-9
To set the PersonAddViewController as the delegate of the three text fields, control-drag from the firstNameTextField to the File’s Owner Icon and select delegate. Repeat this for the lastName TextField and phoneTextField. By doing this, when the user taps the Done button on the keyboard, the keyboard is dismissed. Select File ➪ Save to save the files.
You have completed all the user interface additions and connections that are needed for this application. Now it is time to enter your logic.
348
❘ Chapter 10 Data Storage
Source Code Listings for a Simple Application Using Property Lists For this application, the PListStorageAppDelegate.h and PListStorageAppDelegate.m files are not modified and are used as generated.
PersonAddViewController.h Modifications to the Template You declared three outlets in Interface Builder: firstNameTextField, lastNameTextField, and phoneTextField. You must now define the properties for these variables in order to get and set their values. The IBOutlet was moved to the property declaration. An id delegate variable was declared to identify the delegate class, in this case the RootViewController, that will handle the save action, when the user taps the Save button to save the contact person just entered. In order to process the text field delegate messages, you have to implement the UITextFieldDelegate protocol, so you add that to the class definition. Listing 10-1 shows the complete PersonAddViewController.h file. Listing 10-1: The complete PersonAddViewController.h file (/Chapter10/PListStorage/Classes/
PersonAddViewController.h) #import
@interface PersonAddViewController : UIViewController { UITextField *firstNameTextField; UITextField *lastNameTextField; UITextField *phoneTextField; id delegate; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
IBOutlet UITextField *firstNameTextField; IBOutlet UITextField *lastNameTextField; IBOutlet UITextField *phoneTextField; id delegate;
@end
PersonAddViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the PersonAddViewController.m template. Because the delegate class that will handle the save action is the RootViewController, you need to add the #import directive to the class.
For each of the view properties that were declared, you must match them with @synthesize (see Listing 10-2). Listing 10-2: Addition of @synthesize #import “PersonAddViewController.h” #import “RootViewController.h”
@implementation PersonAddViewController @synthesize @synthesize @synthesize @synthesize
firstNameTextField; lastNameTextField; phoneTextField; delegate;
The Common Premise for Data Storage
❘ 349
When the PersonAddViewController view initializes in the viewDidLoad method, the process of configuring the navigation bar begins (see Listing 10-3). To identify the function of the view to the user, the title of Add Contact is assigned. The Save and Cancel buttons must also be added, and the focus set on the first text field that will be entered. Listing 10-3: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @”Add Contact”; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Cancel” style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Save” style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; }
When the user taps a text field, the keyboard automatically appears, but in order to dismiss the keyboard, the current text field that has the focus has to resign being the first responder. This is done through a UITextFieldDelegate method named textFieldShouldReturn, as shown in Listing 10-4. Listing 10-4: The textFieldShouldReturn method #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; }
Finally, to respond to the Save and Cancel buttons, you have to provide the associated methods. Both methods simply call the delegate class, in this case the RootViewController, to handle the action (see Listing 10-5). Listing 10-5: The save and cancel methods #pragma mark #pragma mark Action methods - (void)save { [[self delegate] savePerson:self];
350
❘ Chapter 10 Data Storage
} - (void)cancel { [[self delegate] cancel]; }
The PersonAddViewController.m file is now complete. See Listing 10-6 for the complete implementation. Listing 10-6: The complete PersonAddViewController.m file (/Chapter10/PListStorage/
Classes/PersonAddViewController.m) #import “PersonAddViewController.h” #import “RootViewController.h”
@implementation PersonAddViewController @synthesize @synthesize @synthesize @synthesize
firstNameTextField; lastNameTextField; phoneTextField; delegate;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @”Add Contact”; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Cancel” style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Save” style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; } #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } #pragma mark -
The Common Premise for Data Storage
❘ 351
#pragma mark Action methods - (void)save { [[self delegate] savePerson:self]; } - (void)cancel { [[self delegate] cancel]; } #pragma mark #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setFirstNameTextField:nil]; [self setLastNameTextField:nil]; [self setPhoneTextField:nil]; [super viewDidUnload]; } - (void)dealloc { [firstNameTextField release]; [lastNameTextField release]; [phoneTextField release]; [super dealloc]; } @end
RootViewController.h Modifications to the Template In a Navigation-based application, the RootViewController is the central view that the user sees when the app is launched. It is from there that all navigation of data begins. In this application, an NSDictionary of contacts is declared. This dictionary contains members of the Person class that was created earlier. The PropertyList class takes this dictionary of Person objects and stores them out to a plist file. You also have to provide the savePerson and cancel delegate methods that are called from the PersonAddViewController class. Listing 10-7 shows the complete RootViewController.h file. Listing 10-7: The complete RootViewController.h file (/Chapter10/PListStorage/Classes/
RootViewController.h)
#import @class PersonAddViewController; @interface RootViewController : UITableViewController { NSDictionary *contacts; } @property (nonatomic, retain) NSDictionary *contacts; - (void)addPerson:(id)sender;
352
❘ Chapter 10 Data Storage
- (void)savePerson:(PersonAddViewController *)sender; - (void)cancel; @end
RootViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the RootViewController.m template. Because the RootViewController is the base view for this application, all the classes are imported. The PersonAddViewController class carries out the adding of a new contact. The PersonDetailViewController class carries out the display of the details for the person. The object being created, saved, and displayed is a member of the Person class. The PropertyList class carries out the storage of contact information. Each contact property declared must be matched with @synthesize (see Listing 10-8). Listing 10-8: Addition of @synthesize #import #import #import #import #import
“RootViewController.h” “PersonAddViewController.h” “PersonDetailViewController.h” “Person.h” “PropertyList.h”
@implementation RootViewController @synthesize contacts;
When the RootViewController view initializes in the viewDidLoad method, the loading of any previously stored information is performed and the NSDictionary variable, contacts, is initialized with those values. To identify the function of the view to the user, the title of “contacts” is assigned to the navigation bar. In addition, the Edit and plus buttons must be added to the navigation bar (see Listing 10-9). Listing 10-9: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setContacts:[PropertyList dictionaryFromPropertyList:@”Contacts”]]; [self setTitle:@”Contacts”]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; }
When the user taps the plus button to add a new contact, the PersonAddViewController is presented for entry and notified that the RootViewController will handle its delegate messages of save and cancel, as shown in Listing 10-10.
The Common Premise for Data Storage
❘ 353
Listing 10-10: The addPerson method #pragma mark #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; }
The RootViewController responds to the savePerson and cancel actions that were sent from the PersonAddViewController. The cancel method simply dismisses the entry view. The savePerson method gets the latest contacts dictionary, initializes a new Person contact, and adds it to the contacts dictionary and writes out the updated data to the property list, as shown in Listing 10-11. Listing 10-11: The savePerson and cancel delegate methods #pragma mark #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { NSMutableDictionary *aContacts = [contacts mutableCopy]; Person *person = [[Person alloc] init]; [person setFirstName:[[sender firstNameTextField] text]]; [person setLastName:[[sender lastNameTextField] text]]; [person setPhone:[[sender phoneTextField] text]]; [aContacts setObject:person forKey:[person lastName]]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@”Contacts” dictionary:aContacts]; [aContacts release]; [person release]; [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; }
In a Table view, the display of information is determined by two factors: ➤➤
The number of sections into which the information is divided
➤➤
The number of rows of data for each section
For this application, there is only one section, as the data is all contact information. The number of rows for this section is determined by how many contacts are stored in the NSDictionary contacts, as shown in Listing 10-12.
354
❘ Chapter 10 Data Storage
Listing 10-12: The numberOfSectionsInTableView and tableView:numberOfRowsInSection
methods
#pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (contacts == nil) { [self setContacts:[NSDictionary dictionary]]; } return [[self contacts] count]; }
The Table view will display just the last name in the list by using the tableView:cellForRowAtIndexPath: method. The last name is also the key to the contacts dictionary. To display the list sorted, the keys from the contacts dictionary are stored in a temporary array and sorted. The row from the indexPath of the Table view provides the index values of the array. With this index, the value is retrieved from the dictionary, as shown in Listing 10-13. Listing 10-13: The tableView:cellForRowAtIndexPath method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *cellText = nil; NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; cellText = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [[cell textLabel] setText:cellText]; return cell; }
To delete a contact from the list, the user taps the Edit button, followed by the red minus, and finally the Delete button. The tableView:commitEditingStyle:forRowAtIndexPath delegate method handles this action and removes the cell from the Table view, but you have to remove it from the dictionary and the stored plist file, as shown in Listing 10-14.
The Common Premise for Data Storage
❘ 355
Listing 10-14: The tableView:commitEditingStyle:forRowAtIndexPath method #pragma mark #pragma mark Table view editing // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; NSMutableDictionary *aContacts = [[self contacts] mutableCopy]; [aContacts removeObjectForKey:contact]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@”Contacts” dictionary:aContacts]; [aContacts release]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } }
Finally, to respond to the request for contact details, when the user taps on a specific contact, that Person object is retrieved from the contacts dictionary and stored in an instance of the PersonDetailViewController class, which is then pushed on the navigation stack for display of the details (see Listing 10-15). Listing 10-15: The tableView:didSelectRowAtIndexPath method #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; Person *person = [[self contacts] objectForKey:contact]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; }
The RootViewController.m class is now complete. Listing 10-16 shows the complete implementation.
356
❘ Chapter 10 Data Storage
Listing 10-16: The complete RootViewController.m file (/Chapter10/PListStorage/Classes/
RootViewController.m) #import #import #import #import #import
“RootViewController.h” “PersonAddViewController.h” “PersonDetailViewController.h” “Person.h” “PropertyList.h”
@implementation RootViewController @synthesize contacts;
#pragma mark #pragma mark View lifecycle #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setContacts:[PropertyList dictionaryFromPropertyList:@”Contacts”]]; [self setTitle:@”Contacts”]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; } #pragma mark #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; } #pragma mark #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { NSMutableDictionary *aContacts = [contacts mutableCopy]; Person *person = [[Person alloc] init];
The Common Premise for Data Storage
[person setFirstName:[[sender firstNameTextField] text]]; [person setLastName:[[sender lastNameTextField] text]]; [person setPhone:[[sender phoneTextField] text]]; [aContacts setObject:person forKey:[person lastName]]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@”Contacts” dictionary:aContacts]; [aContacts release]; [person release]; [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; } #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (contacts == nil) { [self setContacts:[NSDictionary dictionary]]; } return [[self contacts] count]; }
// Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *cellText = nil; NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; cellText = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [[cell textLabel] setText:cellText]; return cell;
❘ 357
358
❘ Chapter 10 Data Storage
} #pragma mark #pragma mark Table view editing // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; NSMutableDictionary *aContacts = [[self contacts] mutableCopy]; [aContacts removeObjectForKey:contact]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@”Contacts” dictionary:aContacts]; [aContacts release]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } }
#pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; Person *person = [[self contacts] objectForKey:contact]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContacts:nil];
The Common Premise for Data Storage
❘ 359
} - (void)dealloc { [contacts release]; [super dealloc]; } @end
PersonDetailViewController.h Modifications to the Template The PersonDetailViewController class is initialized and displayed when the user has selected a contact from the list in the RootViewController and then wishes to see details about the selected contact. The only value that is maintained is the single selected Person object. Listing 10-17 shows the complete PersonDetailViewController.h file. Listing 10-17: The complete PersonDetailViewController.h file (/Chapter10/PListStorage/ Classes/PersonDetailViewController.h) #import @class Person; @interface PersonDetailViewController : UITableViewController { Person *contact; } @property (nonatomic, retain) Person *contact; @end
PersonDetailViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the PersonDetailViewController.m template. Because the only addition was the single Person contact, the only modification needed is matching it with @synthesize (see Listing 10-18). Listing 10-18: Addition of @synthesize #import “PersonDetailViewController.h” #import “Person.h” @implementation PersonDetailViewController @synthesize contact;
When the PersonDetailViewController view initializes in the viewDidLoad method, you only have to identify the function of the view to the user; the title of “Contact Detail” is assigned as the title of the navigation bar (see Listing 10-19). Listing 10-19: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad {
360
❘ Chapter 10 Data Storage
[super viewDidLoad]; [self setTitle:@”Contact Detail”]; }
For the details, the last name is isolated from the other contact details. To do this, two sections are defined. The first section isolates the last name; the second section isolates the first name and phone number. With two sections now defined, the first section will just have one row, that being the last name. The second section will contain two rows for the first name and phone number, as shown in Listing 10-20. Listing 10-20: The numberOfSectionsInTableView and tableView:numberOfRowsInSection
methods
#pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; // last name only } else { rows = 2; // first name and phone number } return rows; }
Because there are two sections, descriptive headers are added to identify each section and accomplished by using the tableView:titleForHeaderInSection method, as shown in Listing 10-21. Listing 10-21: The tableView:titleForHeaderInSection method // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @”Last Name”; break; case 1: sectionTitle = @”Details”; break; default: break; } return sectionTitle; }
In the first section, the last name appears. In the second section, the first name appears in the first row and the phone number appears in the second row, as shown in Listing 10-22.
The Common Premise for Data Storage
Listing 10-22: The tableView:cellForRowAtIndexPath method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; }
The PersonDetailViewController.m class is now complete. See Listing 10-23 for the complete implementation. Listing 10-23: The complete PersonDetailViewController.m file (/Chapter10/PListStorage/
Classes/PersonDetailViewController.m) #import “PersonDetailViewController.h” #import “Person.h”
@implementation PersonDetailViewController @synthesize contact; #pragma mark -
❘ 361
362
❘ Chapter 10 Data Storage
#pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Contact Detail”]; } #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; // last name only } else { rows = 2; // first name and phone number } return rows; } // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @”Last Name”; break; case 1: sectionTitle = @”Details”; break; default: break; } return sectionTitle; }
// Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
The Common Premise for Data Storage
} // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // Navigation logic may go here. Create and push another view controller. } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContact:nil]; } - (void)dealloc { [contact release]; [super dealloc]; } @end
Person.h Modifications to the Template The stored contact information is an instance of the Person class. Three values are saved: ➤➤
firstName
➤➤
lastName
➤➤
phone
❘ 363
364
❘ Chapter 10 Data Storage
In order to store a class to a file, the class must implement the NSCoding protocol. Listing 10-24 shows the complete Person.h file. Listing 10-24: The complete Person.h file (/Chapter10/PListStorage/Classes/Person.h) #import @interface Person : NSObject { NSString *firstName; NSString *lastName; NSString *phone; } @property (nonatomic, retain) NSString *firstName; @property (nonatomic, retain) NSString *lastName; @property (nonatomic, retain) NSString *phone; @end
Person.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the Person.m template. Each of the declared view properties must be matched with @synthesize (see Listing 10-25). Listing 10-25: Addition of @synthesize #import “Person.h” @implementation Person @synthesize firstName; @synthesize lastName; @synthesize phone;
By implementing the NSCoding protocol, the object must include two methods to ensure proper reading and writing of information: encodeWithCoder and initWithCoder (see Listing 10-26). Listing 10-26: The encodeWithCoder and initWithCoder methods #pragma mark #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self firstName] forKey:@”firstName”]; [coder encodeObject:[self lastName] forKey:@”lastName”]; [coder encodeObject:[self phone] forKey:@”phone”]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setLastName:[coder decodeObjectForKey:@”firstName”]]; [self setFirstName:[coder decodeObjectForKey:@”lastName”]]; [self setPhone:[coder decodeObjectForKey:@”phone”]]; } return self; }
The Common Premise for Data Storage
❘ 365
The Person.m class is now complete. Listing 10-27 shows the complete implementation. Listing 10-27: The complete Person.m file (/Chapter10/PListStorage/Classes/Person.m) #import “Person.h”
@implementation Person @synthesize firstName; @synthesize lastName; @synthesize phone;
#pragma mark #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self firstName] forKey:@”firstName”]; [coder encodeObject:[self lastName] forKey:@”lastName”]; [coder encodeObject:[self phone] forKey:@”phone”]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setLastName:[coder decodeObjectForKey:@”firstName”]]; [self setFirstName:[coder decodeObjectForKey:@”lastName”]]; [self setPhone:[coder decodeObjectForKey:@”phone”]]; } return self; } #pragma mark #pragma mark Memory methods - (void)dealloc { [firstName release]; [lastName release]; [phone release]; [super dealloc]; } @end
Modifications to the PropertyList.h and PropertyList.m Templates To store and retrieve data from a property list, a class PropertyList is created. Only two methods are declared: ➤➤
dictionaryFromPropertyList
➤➤
writePropertyListFromDictionary:dictionary
Notice that both methods are factory methods, as there is no reason to create an actual instance of the PropertyList class. The method dictionaryFromPropertyList is called in the initialization of the RootViewController, and the writePropertyListFromDictionary:dictionary method is called when the contact is saved or removed.
366
❘ Chapter 10 Data Storage
Listing 10-28 shows the complete PropertyList.h file and Listing 10-29 shows the complete PropertyList.m file. Listing 10-28: The complete PropertyList.h file (/Chapter10/PListStorage/Classes/PropertyList.h) #import
@interface PropertyList : NSObject { } + (NSDictionary *)dictionaryFromPropertyList:(NSString *)filename; + (BOOL)writePropertyListFromDictionary:(NSString *)filename dictionary:(NSDictionary *)plistDict; @end
Listing 10-29: The complete PropertyList.m file (/Chapter10/PListStorage/Classes/PropertyList.m) #import “PropertyList.h”
@implementation PropertyList + (NSDictionary *)dictionaryFromPropertyList:(NSString *)filename { NSDictionary *result = nil; NSString *fname = [NSString stringWithFormat:@”%@.plist”, filename]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *aData = [NSData dataWithContentsOfFile:bundlePath]; if(aData != nil) { result = [NSKeyedUnarchiver unarchiveObjectWithData:aData]; } return result; } + (BOOL)writePropertyListFromDictionary:(NSString *)filename dictionary:(NSDictionary *)plistDict { NSString *fname = [NSString stringWithFormat:@”%@.plist”, filename]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *aData = [NSKeyedArchiver archivedDataWithRootObject:plistDict]; return [aData writeToFile:bundlePath atomically:YES]; } @end
The Common Premise for Data Storage
❘ 367
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the section “A Simple Application Using Property Lists and Core Data.” Works! Now that you have completed this application using property lists, it is time to build the same functionality into another application. The difference with this new application is how the contact information is stored, retrieved and removed. The previous applications used property lists; this application will use Core Data.
Development Steps: A Simple Application Using Core Data To create this application that stores contact data using Core Data, execute the following steps:
1.
Start Xcode and create a Navigation-based application for the iPhone, and select “Use Core Data for storage.” Name the project CoreDataStorage. If you need to see this step, please refer to Appendix A for the steps to begin a Navigation-based application.
2.
The data that is going to be stored is a Person object. To create this class, choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it Person.
3.
In order to add a new Person who will be stored in the Core Data, you need to create a new view controller. Choose File ➪ New File, and select the UIViewController subclass. Be sure that only “With XIB for user interface” is checked. Name this new class PersonAddViewController.
4.
To display details about a selected person, you need to create a new view controller. Choose File ➪ New File and select the UIViewController subclass. Check both “UITableViewController subclass” and “With XIB for user interface.” Name this new class PersonDetailViewController.
5.
Double-click the PersonAddViewController.xib file to launch Interface Builder (see Figure 10-10).
Figure 10-10
368
❘ Chapter 10 Data Storage
6.
From the Interface Builder main menu, select Tools ➪ Attributes Inspector, and click the View window and set the Background option to Group Table View Background Color.
7. 8.
From the main menu, select Tools ➪ Library and choose the Objects tab.
9.
Select a UITextField and drag it over the main view near the top and release the mouse. Repeat this two more times so you wind up with three stacked text fields (see Figure 10-11). Select the first text field and do the following: ➤➤
Enter First Name for the Placeholder.
➤➤
Set the font to size 17.
➤➤
Set Capitalize to Words.
10.
Repeat the previous steps for the next two fields using Last Name for the placeholder on the second text field, and Phone for the placeholder on the third text field (see Figure 10-12).
11.
Select Tools ➪ Library, choose Classes at the top, and scroll to and select your PersonAddView Controller class. At the bottom, choose the Outlets button. Click the + and add the following outlets,
as shown in Figure 10-13:
12.
➤➤
firstNameTextField (as a UITextField instead of id type)
➤➤
lastNameTextField (as a UITextField instead of id type)
➤➤
phoneTextField (as a UITextField instead of id type)
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then Merge from the second popup. A window will appear with your new additions to PersonAddViewController.h on the left and the original template on the right (see Figure 10-14):
Figure 10-11
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 10-12
Figure 10-13
The Common Premise for Data Storage
❘ 369
Figure 10-14
There aren’t any changes to the PersonAddViewController.m file, so just close it. You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UITextField as firstNameTextField.
➤➤
Identify the UITextField as lastNameTextField.
➤➤
Identify the UITextField as phoneTextField.
13.
To connect the UITextField to firstNameTextField, control-click the File’s Owner icon to bring up the Inspector as shown in Figure 10-15.
14.
From the right of the File’s Owner Inspector, control-drag from the circle next to the firstNameTextField to the UITextField firstNameTextField until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the lastNameTextField and phoneTextField (see Figure 10-16).
Figure 10-15
Figure 10-16
370
❘ Chapter 10 Data Storage
15.
To set the PersonAddViewController as the delegate of the three text fields, control-drag from the firstNameTextField to the File’s Owner icon and select delegate. Repeat this for the lastName TextField and phoneTextField. This is done so that when the user taps the Done button on the keyboard, the keyboard is dismissed. Select File ➪ Save to save the file.
That completes all the user interface additions and connections that are needed for this application. Now it is time to enter your logic.
Source Code Listings for a Simple Application Using Core Data For this application, the PListStorageAppDelegate.h and PListStorageAppDelegate.m files are not modified and are used as generated.
PersonAddViewController.h Modifications to the Template You declared three outlets in Interface Builder: firstNameTextField, lastNameTextField, and phoneTextField. You must now define the properties for these variables in order to get and set their values. The IBOutlet was moved to the property declaration. An id delegate variable was declared to identify the delegate class, in this case the RootViewController, that will handle the save action when the user taps the Save button to save the contact person just entered. In order to process the text field delegate messages, you have to implement the UITextFieldDelegate protocol, so you add that to the class definition. Listing 10-30 shows the complete PersonAddViewController.h file. Listing 10-30: The complete PersonAddViewController.h file (/Chapter10/CoreDataStorage/ Classes/PersonAddViewController.h) #import
@interface PersonAddViewController : UIViewController { UITextField *firstNameTextField; UITextField *lastNameTextField; UITextField *phoneTextField; id delegate; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
IBOutlet UITextField *firstNameTextField; IBOutlet UITextField *lastNameTextField; IBOutlet UITextField *phoneTextField; id delegate;
@end
PersonAddViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the PersonAddViewController.m template. Because the delegate class that will handle the save action is the RootViewController, you need to add the #import directiveto the class.
In addition, each of the declared view properties must be matched with @synthesize (see Listing 10-31).
The Common Premise for Data Storage
❘ 371
Listing 10-31: Addition of @synthesize #import “PersonAddViewController.h” #import “RootViewController.h”
@implementation PersonAddViewController @synthesize @synthesize @synthesize @synthesize
firstNameTextField; lastNameTextField; phoneTextField; delegate;
When the RootViewController view initializes in the viewDidLoad method, the process of configuring the navigation bar begins. To identify the function of the view to the user, the title “Add Contact” is assigned. The Save and Cancel buttons must be added, and the focus set on the first text field that will be entered (see Listing 10-32). Listing 10-32: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @”Add Contact”; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Cancel” style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Save” style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; }
When the user taps a text field, the keyboard automatically appears, but in order to dismiss the keyboard, the current text field with the focus has to resign being the first responder. This is done through a UITextFieldDelegate method textFieldShouldReturn, shown in Listing 10-33. Listing 10-33: The textFieldShouldReturn method #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField {
372
❘ Chapter 10 Data Storage
[textField resignFirstResponder]; return YES; }
Finally, to respond to the Save and Cancel buttons, you have to provide the associated methods, both of which simply call the delegate class, in this case the RootViewController, to handle the action (see Listing 10-34). Listing 10-34: The save and cancel methods #pragma mark #pragma mark Action methods - (void)save { [[self delegate] savePerson:self]; } - (void)cancel { [[self delegate] cancel]; }
The PersonAddViewController.m class is now complete. Listing 10-35 shows the complete implementation. Listing 10-35: The complete PersonAddViewController.m file (/Chapter10/CoreDataStorage/ Classes/PersonAddViewController.m) #import “PersonAddViewController.h” #import “RootViewController.h”
@implementation PersonAddViewController @synthesize @synthesize @synthesize @synthesize
firstNameTextField; lastNameTextField; phoneTextField; delegate;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @”Add Contact”; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Cancel” style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Save” style:UIBarButtonItemStyleDone
The Common Premise for Data Storage
❘ 373
target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; } #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } #pragma mark #pragma mark Action methods - (void)save { [[self delegate] savePerson:self]; } - (void)cancel { [[self delegate] cancel]; } #pragma mark #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setFirstNameTextField:nil]; [self setLastNameTextField:nil]; [self setPhoneTextField:nil]; [super viewDidUnload]; } - (void)dealloc { [firstNameTextField release]; [lastNameTextField release]; [phoneTextField release]; [super dealloc]; } @end
RootViewController.h Modifications to the Template In a Navigation-based application, the RootViewController is the central view that the user sees when the app is launched. It is from there that all navigation of data begins. In this application, an NSFetchedResultsController manages the result set returned from a fetch for a Person entity within the NSManagedObjectContext. You also have to provide the savePerson and cancel delegate methods that are called from the PersonAddViewController class.
374
❘ Chapter 10 Data Storage
Listing 10-36 shows the complete RootViewController.h file. Listing 10-36: The complete RootViewController.h file (/Chapter10/CoreDataStorage/Classes/
RootViewController.h)
#import #import @class PersonAddViewController; @interface RootViewController : UITableViewController { @private NSFetchedResultsController *fetchedResultsController_; NSManagedObjectContext *managedObjectContext_; } @property (nonatomic, retain) NSManagedObjectContext *managedObjectContext; @property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController; - (void)addPerson:(id)sender; - (void)savePerson:(PersonAddViewController *)sender; - (void)cancel; @end
RootViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the RootViewController.m template. Because the RootViewController is the base view for this application, imports for all the classes are required. The PersonAddViewController class handles the adding of a new contact. The PersonDetailViewController class handles the display of details about the person. The object being created, saved, and displayed is a member of the Person class. Note also that there is a template-generated private method, configureCell:IndexPath:, that is used to identify the value that will be displayed on each Table view cell. Each declared contact’s view property must be matched with @synthesize (see Listing 10-37). Listing 10-37: Addition of @synthesize #import #import #import #import
“RootViewController.h” “PersonAddViewController.h” “PersonDetailViewController.h” “Person.h”
@interface RootViewController () - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; @end
@implementation RootViewController @synthesize fetchedResultsController=fetchedResultsController_; @synthesize managedObjectContext=managedObjectContext_;
The Common Premise for Data Storage
❘ 375
When the RootViewController view initializes in the viewDidLoad method, the loading of any previously stored information is performed by the NSFetchedResultsController instance. The Person entities are accessed from the NSManagedObjectContext and are represented as NSManagedObjects. To identify the function of the view to the user, the title “Contacts” is assigned as the title of the navigation bar; and the Edit and plus buttons must be added to the navigation bar. To refresh the Table view after the fetching of data, the reloading of the Table view is called in the viewWillAppear method (see Listing 10-38). Listing 10-38: The viewDidLoad and viewWillAppear methods #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Contacts”]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; NSError *error; if (![[self fetchedResultsController] performFetch:&error]) { // Update to handle the error appropriately. NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); exit(-1); // Fail } }
// Implement viewWillAppear: to do additional setup before the // view is presented. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [[self tableView] reloadData]; }
When the user taps the plus button to add a new contact, the addPerson method handles the action, and the view in the PersonAddViewController class is displayed to all contact information, to be entered as shown in Listing 10-39. Listing 10-39: The addPerson method #pragma mark #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController =
376
❘ Chapter 10 Data Storage
[[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; }
The RootViewController responds to the savePerson and cancel actions that were sent from the PersonAddViewController. The cancel method simply dismisses the entry view. The savePerson method gets the managedObjectsContext for the Person entity and creates a new NSManagedObject. The values that were entered in the PersonAddViewController class are used to populate the Person object and then save the context as shown in Listing 10-40. Listing 10-40: The savePerson and cancel delegate methods #pragma mark #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { // Create a new instance of the entity managed by the // fetched results controller. NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; NSEntityDescription *entity = [[[self fetchedResultsController] fetchRequest] entity]; NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context]; [newManagedObject setValue:[[sender firstNameTextField] text] forKey:@”firstName”]; [newManagedObject setValue:[[sender lastNameTextField] text] forKey:@”lastName”]; [newManagedObject setValue:[[sender phoneTextField] text] forKey:@”phone”]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); abort(); } [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; }
In a Table view, the display of information is determined by two factors: ➤➤
The number of sections into which the information is divided
➤➤
The number of rows of data for each section
For this application, there is only one section, as the data is all contact information. The number of rows for this section is determined by how many objects exist for the NSFetchedResultsSectionInfo section, as shown in Listing 10-41.
The Common Premise for Data Storage
Listing 10-41: The numberOfSectionsInTableView and tableView:numberOfRowsInSection
methods
#pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[[self fetchedResultsController] sections] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { id sectionInfo = [[[self fetchedResultsController] sections] objectAtIndex:section]; return [sectionInfo numberOfObjects]; }
The Table view will display just the last name in the list. To display the list sorted, the fetchedResultsController method needs to be configured to define the sort field in ascending or descending order, as shown in Listing 10-42. Listing 10-42: The fetchedResultsController method #pragma mark #pragma mark Fetched results controller - (NSFetchedResultsController *)fetchedResultsController { if (fetchedResultsController_ != nil) { return fetchedResultsController_; } /* Set up the fetched results controller. */ // Create the fetch request for the entity. NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // Edit the entity name as appropriate. NSEntityDescription *entity = [NSEntityDescription entityForName:@”Person” inManagedObjectContext:self.managedObjectContext]; [fetchRequest setEntity:entity]; // Set the batch size to a suitable number. [fetchRequest setFetchBatchSize:20]; // Edit the sort key as appropriate. NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@”lastName” ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil]; [fetchRequest setSortDescriptors:sortDescriptors]; // Edit the section name key path and cache name if appropriate. // nil for section name key path means “no sections”. NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
❘ 377
378
❘ Chapter 10 Data Storage
managedObjectContext:[self managedObjectContext] sectionNameKeyPath:nil cacheName:@”Root”]; [aFetchedResultsController setDelegate:self]; [self setFetchedResultsController:aFetchedResultsController]; [aFetchedResultsController release]; [fetchRequest release]; [sortDescriptor release]; [sortDescriptors release]; NSError *error = nil; if (![fetchedResultsController_ performFetch:&error]) { NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); abort(); } return fetchedResultsController_; }
In order to display the last name in the Table view, the last name has to be retrieved from core data. In the method configureCell:atIndexPath: the last name is fetched from the fetchedResultsController and inserted into an NSManagedObject. The last name value is then retrieved from the managed object and displayed in the Table view cell. The specific row that is used to retrieve the last name is passed into the tableView:cellForRowAtIndex Path: method as an NSIndexPath object, as shown in Listing 10-43. Listing 10-43: The configureCell:atIndexPath: and tableView:cellForRowAtIndexPath: method #pragma mark #pragma mark TableView cell appearance - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { NSManagedObject *managedObject = [[self fetchedResultsController] objectAtIndexPath:indexPath]; [[cell textLabel] setText:[[managedObject valueForKey:@”lastName”] description]]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [self configureCell:cell atIndexPath:indexPath]; return cell; }
The Common Premise for Data Storage
❘ 379
To delete a contact from the list, the user taps the Edit button, then the red minus, and finally the Delete button. The tableView:commitEditingStyle:forRowAtIndexPath delegate method handles this action, removing the cell from the Table view, but you also have to remove it from Core Data as shown in Listing 10-44. Listing 10-44: The tableView:commitEditingStyle:forRowAtIndexPath method // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Delete the managed object for the given index path NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; [context deleteObject:[[self fetchedResultsController] objectAtIndexPath:indexPath]]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); abort(); } } }
Finally, to respond to the request to display contact details, when the user taps on a specific contact, that Person object is retrieved using the fetchedResultController and stored in an instance of the PersonDetailViewController class, which is then pushed on the navigation stack for display of the details, as shown in Listing 10-45. Listing 10-45: The tableView:didSelectRowAtIndexPath method #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { Person *person = (Person *)[[self fetchedResultsController] objectAtIndexPath:indexPath]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; }
The modifications to the RootViewController.m class template are now complete. See Listing 10-46 for the complete implementation.
380
❘ Chapter 10 Data Storage
Listing 10-46: The complete RootViewController.m file (/Chapter10/CoreDataStorage/Classes/ RootViewController.m) #import #import #import #import
“RootViewController.h” “PersonAddViewController.h” “PersonDetailViewController.h” “Person.h”
@interface RootViewController () - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; @end
@implementation RootViewController @synthesize fetchedResultsController=fetchedResultsController_; @synthesize managedObjectContext=managedObjectContext_;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Contacts”]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; NSError *error; if (![[self fetchedResultsController] performFetch:&error]) { // Update to handle the error appropriately. NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); exit(-1); // Fail } } // Implement viewWillAppear: to do additional setup before the // view is presented. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [[self tableView] reloadData]; }
#pragma mark #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self];
The Common Premise for Data Storage
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; } #pragma mark #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { // Create a new instance of the entity managed by the // fetched results controller. NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; NSEntityDescription *entity = [[[self fetchedResultsController] fetchRequest] entity]; NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context]; [newManagedObject setValue:[[sender firstNameTextField] text] forKey:@”firstName”]; [newManagedObject setValue:[[sender lastNameTextField] text] forKey:@”lastName”]; [newManagedObject setValue:[[sender phoneTextField] text] forKey:@”phone”]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); abort(); } [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; }
#pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[[self fetchedResultsController] sections] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { id sectionInfo = [[[self fetchedResultsController] sections] objectAtIndex:section]; return [sectionInfo numberOfObjects];
❘ 381
382
❘ Chapter 10 Data Storage
} #pragma mark #pragma mark TableView cell appearance - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { NSManagedObject *managedObject = [[self fetchedResultsController] objectAtIndexPath:indexPath]; [[cell textLabel] setText:[[managedObject valueForKey:@”lastName”] description]]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [self configureCell:cell atIndexPath:indexPath]; return cell; } // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Delete the managed object for the given index path NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; [context deleteObject:[[self fetchedResultsController] objectAtIndexPath:indexPath]]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); abort(); } } } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { // The table view should not be re-orderable. return NO;
The Common Premise for Data Storage
} #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { Person *person = (Person *)[[self fetchedResultsController] objectAtIndexPath:indexPath]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; }
#pragma mark #pragma mark Fetched results controller - (NSFetchedResultsController *)fetchedResultsController { if (fetchedResultsController_ != nil) { return fetchedResultsController_; } /* Set up the fetched results controller. */ // Create the fetch request for the entity. NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // Edit the entity name as appropriate. NSEntityDescription *entity = [NSEntityDescription entityForName:@”Person” inManagedObjectContext:self.managedObjectContext]; [fetchRequest setEntity:entity]; // Set the batch size to a suitable number. [fetchRequest setFetchBatchSize:20]; // Edit the sort key as appropriate. NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@”lastName” ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil]; [fetchRequest setSortDescriptors:sortDescriptors]; // Edit the section name key path and cache name if appropriate. // nil for section name key path means “no sections”. NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[self managedObjectContext] sectionNameKeyPath:nil cacheName:@”Root”]; [aFetchedResultsController setDelegate:self];
❘ 383
384
❘ Chapter 10 Data Storage
[self setFetchedResultsController:aFetchedResultsController]; [aFetchedResultsController release]; [fetchRequest release]; [sortDescriptor release]; [sortDescriptors release]; NSError *error = nil; if (![fetchedResultsController_ performFetch:&error]) { NSLog(@”Unresolved error %@, %@”, error, [error userInfo]); abort(); } return fetchedResultsController_; } #pragma mark #pragma mark Fetched results controller delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [[self tableView] beginUpdates]; }
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch(type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; } }
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { UITableView *tableView = self.tableView; switch(type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break;
The Common Premise for Data Storage
case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths: [NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeUpdate: [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; break; case NSFetchedResultsChangeMove: [tableView deleteRowsAtIndexPaths: [NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; [tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; } }
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [[self tableView] endUpdates]; }
/* // Implementing the above methods to update the table view in response to individual changes may have performance implications if a large number of changes are made simultaneously. If this proves to be an issue, you can instead just implement controllerDidChangeContent: which notifies the delegate that all section and object changes have been processed. - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { // In the simplest, most efficient, case, reload the table view. [self.tableView reloadData]; } */ #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setFetchedResultsController:nil]; } - (void)dealloc { [fetchedResultsController_ release]; [managedObjectContext_ release]; [super dealloc]; } @end
❘ 385
386
❘ Chapter 10 Data Storage
PersonDetailViewController.h Modifications to the Template The PersonDetailViewController class is initialized and displayed when the user has selected a contact from the list in the RootViewController and then wants to see details about that contact. The only value that is maintained is the single selected Person object. Listing 10-47 shows the complete PersonDetailViewController.h file. Listing 10-47: The complete PersonDetailViewController.h file (/Chapter10/CoreDataStorage/ Classes/PersonDetailViewController.h) #import @class Person; @interface PersonDetailViewController : UITableViewController { Person *contact; } @property (nonatomic, retain) Person *contact; @end
PersonDetailViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the PersonDetailViewController.m template. Because the only addition was the single Person contact, the only modification needed is matching it with @synthesize (see Listing 10-48). Listing 10-48: Addition of @synthesize #import “PersonDetailViewController.h” #import “Person.h” @implementation PersonDetailViewController @synthesize contact;
When the PersonDetailViewController view initializes in the viewDidLoad method, you only have to identify the function of the view to the user. The title of “Contact Detail” is assigned as the title of the navigation bar (see Listing 10-49). Listing 10-49: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Contact Detail”]; }
For the details, the last name will be isolated from the other contact details. To do this, two sections are defined. The first section will isolate the last name; the second section isolates the first name and phone number.
The Common Premise for Data Storage
❘ 387
With two sections now defined, the first section should have just one row, that being the last name. The second section should contain two rows, for the first name and phone number, as shown in Listing 10-50. Listing 10-50: The numberOfSectionsInTableView and tableView:numberOfRowsInSection
methods
#pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; // last name only } else { rows = 2; // first name and phone number } return rows; }
Because there are two sections, descriptive headers are added to identify each one, as shown in Listing 10-51. Listing 10-51: The tableView:titleForHeaderInSection method // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @”Last Name”; break; case 1: sectionTitle = @”Details”; break; default: break; } return sectionTitle; }
In the first section, the last name will appear. In the second section, the first name will appear in the first row and the phone number will appear in the second row, as shown in Listing 10-52. Listing 10-52: The tableView:cellForRowAtIndexPath method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row];
388
❘ Chapter 10 Data Storage
NSString *cellText = nil; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; }
The PersonDetailViewController.m class is now complete. Listing 10-53 shows the complete implementation. Listing 10-53: The complete PersonDetailViewController.m file (/Chapter10/CoreDataStorage/
Classes/PersonDetailViewController.m) #import “PersonDetailViewController.h” #import “Person.h”
@implementation PersonDetailViewController @synthesize contact; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Contact Detail”];
The Common Premise for Data Storage
} #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; } else { rows = 2; } return rows; } // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @”Last Name”; break; case 1: sectionTitle = @”Details”; break; default: break; } return sectionTitle; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) {
❘ 389
390
❘ Chapter 10 Data Storage
case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContact:nil]; } - (void)dealloc { [contact release]; [super dealloc]; } @end
Person.h Modifications to the Template The contact information that is stored is an instance of a Person class. Three values are saved: ➤➤
firstName
➤➤
lastName
➤➤
phone
The Common Premise for Data Storage
❘ 391
In order to store a class using Core Data, you must include the header and declare the Person class a subclass of NSManagedObject. Listing 10-54 shows the complete Person.h file. Listing 10-54: The complete Person.h file (/Chapter10/CoreDataStorage/Classes/Person.h) #import @interface Person : NSManagedObject { } @property (nonatomic, retain) NSString *firstName; @property (nonatomic, retain) NSString *lastName; @property (nonatomic, retain) NSString *phone; @end
Person.m Modifications to the Template With the header file updated to define the additions to the template, nothing more has to be done to the Person.m template other than matching each declared view property with @synthesize. The Person.m class is now complete. See Listing 10-55 for the complete implementation. Listing 10-55: The complete Person.m file (/Chapter10/CoreDataStorage/Classes/Person.m) #import “Person.h”
@implementation Person @synthesize firstName; @synthesize lastName; @synthesize phone; @end
Data Model Creation for A Simple Application Using Core Data With the user interface and source code completed, it is time to customize the data model template to include the Person class being used.
CoreDataStorage.xcdatamodel Modifications to the Template When the option to use Core Data was checked when the project was created as a Navigation-based application, in step 1, a default data model was generated. In the default state the following are defined: ➤➤
Event entity
➤➤
timeStamp attribute of class Date
To customize the data model to work with your application, do the following:
1. 2.
In the Groups & Files window of Xcode, select Resources and then choose CoreDataStorage .xcdatamodeld to bring up the data modeler, shown in Figure 10-17.
In the Entity section, select Entity and change the Name and Class to Person as shown in Figure 10-18.
392
❘ Chapter 10 Data Storage
Figure 10-17
Figure 10-18
3.
In the Property section, select timestamp and change the name to firstName, uncheck Optional, and change the type to String (see Figure 10-19).
The Common Premise for Data Storage
4.
❘ 393
Repeat for lastName and phone, with a type of String, by clicking the plus in the Property section at the bottom and choosing Add Attribute, as shown in Figure 10-20.
Figure 10-19
Figure 10-20
394
❘
chaPter 10 data storage
5 .
When completed, your data model should look like Figure 10-21.
fiGUre 10-21
test your application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the section “A Simple Application Using Property Lists and Core Data.”
If, when running in the Simulator, you encounter the error “The model used to open the store is incompatible with the one used to create the store,” you have to completely eliminate your build folder and remove the app from the Simulator.
sUmmary Using the property list to store and retrieve small amounts of data is a quick and fairly easy process to implement, and it can be an ideal solution for your applications. The fact that the plist fi le itself is readable and can be modified from any text editor makes it a popular choice. However, the Xcode data modeling tool and the elegant framework offered by Core Data should not be overlooked, even though it may take a few more steps to get your data storage needs together.
11
The Pasteboard What’s in this chaPter? ➤➤
Items you can place on the UIPasteboard
➤➤
Identifying and performing cut, copy, and paste operations
➤➤
Customizing menu items on the iPad
With the introduction of iPhone OS 3.0, users were able to copy text, images, or other data in one application and paste that data to either another location within the same application or a different application.
If you’re getting confused by references between iPhone and iPad here, remember this: At this time, the iPad is the only device that allows the developer to customize the standard Cut, Copy, Paste editing menu. The iPhone works with only the options that Apple gives the developer. The copy-cut-paste process is included in the following: ➤➤
UITextView
➤➤
UITextField
➤➤
UIWebView
The UIKit framework provides the following classes and protocols that provide the architecture so you can implement cut, copy, and paste in your applications: ➤➤
The UIPasteboard class provides pasteboards, allowing for the sharing of data within an application or between applications by writing and reading data to and from a pasteboard.
➤➤
The UIMenuController class displays the editing menu, allowing selected items to be copied, cut, or pasted into the pasteboard.
➤➤
The UIResponder class declares the method canPerformAction:withSender:, which determines what editing commands appear on the menu.
➤➤
The UIResponderStandardEditActions class is invoked when the user taps one of the commands in the editing menu.
396
❘ Chapter 11 The Pasteboard
In this chapter you will learn how to cut, copy, and paste text and images in your applications and for the iPad, as well as how to customize the standard menu to add your own menu items.
Pasteboard Concepts A pasteboard allows the exchange of data within applications or between applications. The most common use for pasteboards is handling copy, cut, and paste operations. The basic tasks you perform with a pasteboard object are twofold: ➤➤
To write data to a pasteboard
➤➤
To read data from a pasteboard
There are a number of ways to represent data on a pasteboard, including the following: ➤➤
Named pasteboards
➤➤
Pasteboard persistence
➤➤
The editing menu
➤➤
Cutting the selection
➤➤
Pasting the item
The item representations that can be placed on the pasteboard are as follows: ➤➤
UIPasteboardTypeListString — An array of strings, including kUTTypeUTF8PlainText
➤➤
UIPasteboardTypeListURL — An array of URLs, including kUTTypeURL
➤➤
UIPasteboardTypeListImage — An array of images, including kUTTypePNG and kUTTypeJPEG
➤➤
UIPasteboardTypeListColor — An array of colors
Named Pasteboards Pasteboards must have unique names and may be either: ➤➤
Public (system pasteboards)
➤➤
Private (application pasteboards), as they are created by applications
When you will have data types that are specific to your application and outside the types offered by the system pasteboard, you can create your own application pasteboard, by using pasteboardWithName:create:. In addition to specific data types, application pasteboards differ from system pasteboards in that they are not persistent by default. If you decide to make the data persistent, you have to keep in mind that to do so reduces the available memory to the device overall, and thus should be a well thought-out decision. The class UIPasteboard defines two system pasteboards, each with its own name and purpose: ➤➤
UIPasteboardNameGeneral — For cut, copy, and paste involving a wide range of data types.
➤➤
UIPasteboardNameFind — For search operations. The string in the search bar (UISearchBar)
is written to this pasteboard. To obtain the object representing the find pasteboard, call the pasteboardWithName:create: class method, passing in UIPasteboardNameFind for the name.
Persistence System pasteboards are persistent. Application pasteboards, by default, are not persistent and are removed when the application terminates. To make an application pasteboard persistent, set its persistent property to YES. A persistent application pasteboard is removed only when a user uninstalls the owning application.
Pasteboard Concepts
❘ 397
The owner of the pasteboard is the object that last put the data on it. The pasteboard can hold single or multiple items.
The Editing Menu In order to copy or cut something it must be selected, and you must manage that selection. To manage the selection, follow these steps (see Listing 11-1):
1. 2. 3.
Get the sharedMenuController. Compute the selection rectangle boundaries by calling setTargetRect:inView:. Display the menu options by calling setMenuVisible:animated.
Listing 11-1: Displaying the editing menu UIMenuController *theMenu = [UIMenuController sharedMenuController]; CGRect selectionRect = CGRectMake(currentSel.x, currentSel.y, SIDE, SIDE); [theMenu setTargetRect:selectionRect inView:self]; [theMenu setMenuVisible:YES animated:YES];
By default, all the editing commands are visible (copy:, paste:, and so on). However, using the canPerformAction:withSender: method, you can filter what options are available. Listing 11-2 allows only the copy operation. Listing 11-2: The canPerformAction:withSender: method - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (action == @selector(cut:)) { return NO; } else if (action == @selector(copy:)) { return YES; } else if (action == @selector(paste:)) { return NO; } else if (action == @selector(select:)) { return NO; } else if (action == @selector(selectAll:)) { return NO; } else { return [super canPerformAction:action withSender:sender]; } }
Cutting the Selection In response to a copy: or cut: message, the data is written to the pasteboard, as shown in Listing 11-3. Listing 11-3: Copy and cut operations - (void)copy:(id)sender { [self placeImageOnPasteboard:[self imageView]]; } - (void)placeImageOnPasteboard:(id)view { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; appPasteBoard.persistent = YES; NSData *data = UIImagePNGRepresentation([view image]);
398
❘ Chapter 11 The Pasteboard
[appPasteBoard setData:data forPasteboardType:@”com.marizack.CopyPasteImage.imageView”]; } - (void)cut:(id)sender { [self copy:sender]; [imageView setHidden:YES]; }
Pasting the Item In response to a paste: message, you read an object from the pasteboard and assign it to the appropriate data value, as shown in Listing 11-4. Listing 11-4: Pasting data from the pasteboard to a selection - (void)paste:(id)sender { UIPasteboard *appPasteBoard = [ UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; NSData *data = [appPasteBoard dataForPasteboardType: @”com.marizack.CopyPasteImage.imageView”]; pasteView.image = [UIImage imageWithData:data]; }
Dismissing the Editing Menu When the cut:, copy:, or paste: command returns, the editing menu is automatically hidden. You can keep it visible with the following line of code: [UIMenuController setMenuController].menuVisible = YES;
Cutting and Pasting Text In this application, the title of the UITableViewCell will be copied to the pasteboard and then pasted to the title of the UITableView. This app will present the user with a UITableView that has nine rows populated, as shown in Figure 11-1. When the user selects and holds a particular cell, rather than quickly tapping, a menu will appear, allowing the label of the cell to be copied to the pasteboard (see Figure 11-2). When the user taps the button in the upper-right corner, the contents of the pasteboard will be pasted into the title of the UITableView, as shown in Figure 11-3.
Development Steps: Cutting and Pasting Text To create this application, execute the following steps.
1.
Start Xcode and create a Navigation-based application named CopyPasteText-iPhone. If you need to see this step, please see Appendix A for the steps to begin a Navigation-based application.
2.
Double-click the RootViewController.m file to bring the class into its own text window, as shown in Figure 11-4.
Cutting and Pasting Text
Figure 11-1
Figure 11-4
Figure 11-2
Figure 11-3
❘ 399
400
❘
chaPter 11 the pasteboard
3 .
In the upper-right corner next to the padlock icon, you will see a pair of squares. Click on these squares to bring up the counterpart of the RootViewController.m file, the RootViewController.h file (see Figure 11-5).
You’ll note that I have you click on the pair of squares to switch between the RootViewController.m and RootViewController.h in order to introduce you to some of the tools from the IDE. If you’re looking for more information on the IDE, you can find details about good resources in Appendix D, “Apple Developer Resources,” under the “iOS Developer Resources” section related to Xcode.
fiGUre 11-5
Now it is time to enter your logic.
source Code listings for Cutting and Pasting Text For this application, the CopyPasteText_iPhoneAppDelegate.h or CopyPasteText_ iPhoneAppDelegate.m fi les are not modified and are used as generated. In the RootViewController.h fi le, add a BOOL variable menuVisible that will indicate if the Copy menu item is being displayed. In addition, defi ne the property for this variable in order to get and set its value (see Listing 11-5).
Cutting and Pasting Text
❘ 401
Listing 11-5: The complete RootViewController.h file (Chapter11/CopyPasteText-iPhone/ Classes/RootViewController.h) #import @interface RootViewController : UITableViewController { BOOL menuVisible; } @property (nonatomic, getter=isMenuVisible) BOOL menuVisible; @end
When the application launches, the title of the UITableView is set, and the button that will paste the pasteboard contents is created. This happens in the viewDidLoad method of the RootViewController.m file, as shown in Listing 11-6. Listing 11-6: The viewDidLoad method - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Pasteboard Display”]; UIBarButtonItem *addButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(readFromPasteboard:)] autorelease]; [[self navigationItem] setRightBarButtonItem:addButton]; }
To indicate that nine rows will be displayed, modify the tableView:numberOfRowsInSection: method as shown in Listing 11-7. Listing 11-7: The tableView:numberOfRowsInSection method // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 9; }
To allow the text in the Table view cell to be copied to the pasteboard, you must create a custom UITableViewCell. To create the CopyTableViewCell, you must choose File ➪ New File from the main menu and select Objective-C class as a subclass of UITableViewCell from the drop-down list, as shown in Figure 11-6. The RootViewController will be the delegate of the CopyTableViewCell, so the CopyTableViewCell.h file must declare a delegate variable. In addition, define the property for this variable in order to get and set its value (see Listing 11-8). Listing 11-8: The complete CopyTableViewCell.h file (Chapter11/CopyPasteText-iPhone/
Classes/CopyTableViewCell.h) #import
@interface CopyTableViewCell : UITableViewCell {
402
❘ Chapter 11 The Pasteboard
id delegate; } @property (nonatomic, retain) id delegate; @end
Figure 11-6
When the CopyTableViewCell is touched, it becomes highlighted. That event is processed by the setHighlighted:animated: method. This method will call the method showMenu in the RootViewController to display the Copy menu item above the cell (see Listing 11-9). Listing 11-9: The setHighlighted:animated: method - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { [[self delegate] performSelector:@selector(showMenu:) withObject:self afterDelay:0.9f]; [super setHighlighted:highlighted animated:animated]; }
In order for the CopyTableViewCell to display and process the Copy action, the code must declare both that it can be a FirstResponder and that it can perform the cut: action. Listing 11-10 details these declarations. Listing 11-10: The canBecomeFirstResponder and canPerformAction:withSender: methods - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender{
Cutting and Pasting Text
❘ 403
if (action == @selector(cut:)) { return NO; } else if (action == @selector(copy:)) { return YES; } else if (action == @selector(paste:)) { return NO; } else if (action == @selector(select:)) { return NO; } else if (action == @selector(selectAll:)) { return NO; } else { return [super canPerformAction:action withSender:sender]; } }
In the CopyTableViewCell, all that is left is to define the copy method itself. The copy operation is a twostep process:
1. 2.
Create the pasteboard. Copy the CopyTableViewCell’s label onto the pasteboard (see Listing 11-11).
Listing 11-11: The copy method - (void)copy:(id)sender { UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; [pasteboard setString:[[self textLabel]text]]; }
The CopyTableViewCell class is now complete. Listing 11-12 shows the complete implementation. Listing 11-12: The complete CopyTableViewCell.m file (Chapter11/CopyPasteText-iPhone/
Classes/CopyTableViewCell.m) #import “CopyTableViewCell.h”
@implementation CopyTableViewCell @synthesize delegate; - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { } return self; } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { [[self delegate] performSelector:@selector(showMenu:) withObject:self afterDelay:0.9f]; [super setHighlighted:highlighted animated:animated];
404
❘ Chapter 11 The Pasteboard
} - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender{ if (action == @selector(cut:)) { return NO; } else if (action == @selector(copy:)) { return YES; } else if (action == @selector(paste:)) { return NO; } else if (action == @selector(select:)) { return NO; } else if (action == @selector(selectAll:)) { return NO; } else { return [super canPerformAction:action withSender:sender]; } } - (void)copy:(id)sender { UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; [pasteboard setString:[[self textLabel]text]]; } - (void)dealloc { [super dealloc]; } @end
Now that the CopyTableViewCell has been created, the RootViewController will create nine of these custom table view cells in its view, through the tableView:cellForRowAtIndexPath: method, as shown in Listing 11-13. Listing 11-13: The tableView:cellForRowAtIndexPath: method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; CopyTableViewCell *cell = (CopyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[CopyTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setDelegate:self]; } // Configure the cell. NSString *text = [NSString stringWithFormat:@”Row %d”, [indexPath row]]; [[cell textLabel] setText:text]; return cell; }
Cutting and Pasting Text
❘ 405
When a cell is just tapped by the user, the tap will be processed as normal. If the user taps and holds, then the menu will be displayed. When the menu is being displayed, any further taps will be ignored until the copy is processed or dismissed, as shown in Listing 11-14. Listing 11-14: The tableView:didSelectRowAtIndexPath: method - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if([self isMenuVisible]) { return; } [[[self tableView] cellForRowAtIndexPath:indexPath] setSelected:NO animated:YES]; }
Listing 11-15 shows the check that is made to ensure that the menu will be visible only when the cell has been highlighted with a touch and hold. Listing 11-15: The showMenu method - (void)showMenu:(id)cell { if ([cell isHighlighted]) { [cell becomeFirstResponder]; UIMenuController * menu = [UIMenuController sharedMenuController]; [menu setTargetRect: [cell frame] inView: [self view]]; [menu setMenuVisible: YES animated: YES]; } }
Finally, after the user has copied the cell’s label on the pasteboard, a tap of the button in the title bar on the right will take the text off the pasteboard and set the title of the UITableView (see Listing 11-16). Listing 11-16: The readFromPasteboard method - (void)readFromPasteboard:(id)sender { [self setTitle:[NSString stringWithFormat:@”Pasteboard = %@”, [[UIPasteboard generalPasteboard] string]]]; }
The RootViewController is now complete. Listing 11-17 shows the complete implementation. Listing 11-17: The complete RootViewController.m file (Chapter11/CopyPasteText-iPhone/ Classes/RootViewController.m) #import “RootViewController.h” #import “CopyTableViewCell.h”
@implementation RootViewController @synthesize menuVisible; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad];
406
❘ Chapter 11 The Pasteboard
[self setTitle:@”Pasteboard Display”]; UIBarButtonItem *addButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(readFromPasteboard:)] autorelease]; [[self navigationItem] setRightBarButtonItem:addButton]; } #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; }
// Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 9; }
// Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @”Cell”; CopyTableViewCell *cell = (CopyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[CopyTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setDelegate:self]; } // Configure the cell. NSString *text = [NSString stringWithFormat:@”Row %d”, [indexPath row]]; [[cell textLabel] setText:text]; return cell; } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath { if([self isMenuVisible]) { return; } [[[self tableView] cellForRowAtIndexPath:indexPath] setSelected:NO animated:YES]; } #pragma mark -
Cutting and Pasting Images
❘ 407
#pragma mark Menu management - (void)showMenu:(id)cell { if ([cell isHighlighted]) { [cell becomeFirstResponder]; UIMenuController * menu = [UIMenuController sharedMenuController]; [menu setTargetRect: [cell frame] inView: [self view]]; [menu setMenuVisible: YES animated: YES]; } } - (void)readFromPasteboard:(id)sender { [self setTitle:[NSString stringWithFormat:@”Pasteboard = %@”, [[UIPasteboard generalPasteboard] string]]]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated in viewDidLoad or // on demand. // For example: self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of this topic.
Cutting and Pasting Images In this application, you are going to cut or copy an existing image to the UIPasteboard, then retrieve that image from the pasteboard and paste it into another UIImageView. The steps to accomplish this are basically the same as the text application; it is only how the data is represented on the pasteboard that differs. This app will present to the user two images, as shown in Figure 11-7: ➤➤
The original image, grandpa.png
➤➤
A default stub image, default.png, into which the user will paste the original image
When the user touches and holds the original image, the Cut and Copy menu will appear, as shown in Figure 11-8. When the user presses the Cut menu item, the image is placed upon the general pasteboard. When the user touches and holds the default stub image, the Paste menu appears, as shown in Figure 11-9.
408
❘ Chapter 11 The Pasteboard
When the user presses the Paste menu item, the image is retrieved from the general pasteboard and placed into the UIImageView that previously held the default stub image, as shown in Figure 11-10.
Figure 11-7
Figure 11-8
Figure 11-9
Figure 11-10
Cutting and Pasting Images
❘ 409
Development Steps: Cutting and Pasting Images To create this application, execute the following steps:
1.
Start Xcode and create a View-based application for the iPhone named CopyPasteImage-iPhone. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
2.
You need to add two images to the project:
3.
➤➤
Select Other Resources in Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your two images, default.png and grandpa.png, and click Add (see Figure 11-11).
➤➤
Check Copy items into destination group’s folder, and click Add as shown in Figure 11-12.
Double-click the CopyPasteImage_iPhoneViewController.xib file to launch Interface Builder (see Figure 11-13).
Figure 11-11
Figure 11-13
Figure 11-12
410
❘
chaPter 11 the pasteboard
4 .
From the Interface Builder Library (Tools ➪ Library), choose the two UIImageViews sized 180 × 162, and drag them to the View window so they are placed one above the other, by executing the following substeps: ➤➤
Drag your first UIImageView to the View window. By default, it will occupy the entire window, which is fine for now.
The icon in interface builder is Image View — when you choose it you should know it is UIImageView by the description. I refer to it by the class in this step because when the outlets are declared, they will be UIImageView declarations. ➤➤
Choose Tools ➪ Size Inspector from the main menu. You will see W:320, W:460 just under the Frame drop-down in the upper-right corner of the inspector.
➤➤
Change the size of the view to W:180 H:162.
➤➤
Adjust your resized UIImageView to be centered at the top.
➤➤
Choose Tools ➪ Attribute Inspector, select the Image drop-down control, and select grandpa .png, which will be the image that the user will Cut or Copy.
➤➤
At the bottom, check User Interaction Enabled.
➤➤
Choose Edit ➪ Duplicate and you will now have a duplicate of your resized UIImageView.
➤➤
Adjust your second UIImageView to be just below the original UIImageView, and select default.png from the Image drop-down control in the Attribute Inspector. This will be the default image that will be pasted over when the user chooses Paste.
Your populated view should resemble the one shown in Figure 11-14.
5 .
Back in the Interface Builder Library, click Classes at the top and scroll to select your CopyPasteImage_iPhoneViewController class. At the bottom, now click the Outlets button. Click the + and add the following outlets, as shown in Figure 11-15: ➤➤
imageView as a UIImageView
➤➤
pasteView as a UIImageView
➤➤
selectedView as a UIImageView
6 .
From the main menu of Interface Builder, choose File ➪ Write Class Files, and select Save and then Replace. You now have an Objective-C template that will hold your application’s logic.
7 .
Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to:
8 .
➤➤
Identify and connect the original imageView to the File’s Owner icon
➤➤
Identify and connect the original pasteView to the File’s Owner icon
fiGUre 11-14
To make the connection to identify the imageView outlet, control-click and hold over the File’s Owner icon. Still holding the mouse, drag until over the top of UIImageView that displays the grandpa.png image and release. In the window that pops up, click imageView to make the connection (see Figure 11-16). Repeat this to connect the pasteView outlet to the UIImageView that displays the default.png image.
Cutting and Pasting Images
Figure 11-15
9.
Figure 11-16
Choose File ➪ Save and File ➪ Quit to exit Interface Builder.
You are now finished with the user interface, so it is time to turn to the source code and enter the logic.
Source Code Listings for Cutting and Pasting Images For this application, the CopyPasteImage_iPhoneAppDelegate.h and CopyPasteImage_ iPhoneAppDelegate.m files are not modified and are used as generated. Remember that Interface Builder created template source code. Listing 11-18 shows the template for the CopyPasteImage_iPhoneViewController.h interface file and Listing 11-19 shows the template for the CopyPasteImage_iPhoneViewController.m implementation file. Listing 11-18: The CopyPasteImage_iPhoneViewController.h template #import #import @interface CopyPasteImage_iPhoneViewController : UIViewController { IBOutlet UIImageView *imageView; IBOutlet UIImageView *pasteView;
❘ 411
412
❘ Chapter 11 The Pasteboard
IBOutlet UIImageView *selectedView; } @end
Listing 11-19: The CopyPasteImage_iPhoneViewController.m template #import “CopyPasteImage_iPhoneViewController.h” @implementation CopyPasteImage_iPhoneViewController @end
CopyPasteImage_iPhoneViewController.h Modifications to the Template You declared three outlets — imageView, pasteView, and selectedView — in Interface Builder. You must now define the properties for these variables in order to get and set their value (see Listing 11-20). The IBOutlet was moved to the property declaration, and removed from the selectedView altogether, as it is not a view used in the interface; it is just used for temporary storage. There is also a new method declared, placeImageOnPasteboard, which will be used by the cut: and copy: methods to place the selected image onto the pasteboard. Listing 11-20: The complete modified CopyPasteImage_iPhoneViewController.h file (Chapter11/CopyPasteImage-iPhone/Classes/CopyPasteImage_iPhoneViewController.h) #import #import @interface CopyPasteImage_iPhoneViewController : UIViewController { UIImageView *imageView; UIImageView *pasteView; UIImageView *selectedView; } @property (nonatomic, retain) IBOutlet UIImageView *imageView; @property (nonatomic, retain) IBOutlet UIImageView *pasteView; @property (nonatomic, retain) UIImageView *selectedView; - (void)placeImageOnPasteboard:(id)view; @end
CopyPasteImage_iPhoneViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the CopyPasteImage_iPhoneViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 11-21). Listing 11-21: Addition of @synthesize @implementation CopyPasteImage_iPhoneViewControl @synthesize imageView; @synthesize pasteView; @synthesize selectedView;
Cutting and Pasting Images
❘ 413
When the user touches the top image, the imageView will be the object that represents the active view; and when the user touches the bottom image, the pasteView will be the object that represents the active view. The method that you will implement is touchesBegan:withEvent:, shown in Listing 11-22. Listing 11-22: The touchesBegan:withEvent: method - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSSet *copyTouches = [event touchesForView:imageView]; NSSet *pasteTouches = [event touchesForView:pasteView]; [self becomeFirstResponder]; if ([copyTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:imageView afterDelay:0.9f]; } else if([pasteTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:pasteView afterDelay:0.9f]; } [super touchesBegan:touches withEvent:event]; }
Notice the afterDelay argument. This will distinguish between a mere tap and a touch and hold. When the user touches and holds the image, the showMenu: method will be called. This is the method that displays the Cut and Copy menu options for the user to choose (see Listing 11-23). Listing 11-23: The showMenu: method - (void)showMenu:(id)view { [self setSelectedView:view]; UIMenuController * menu = [UIMenuController sharedMenuController]; [menu setTargetRect: CGRectMake(5, 10, 1, 1) inView: view]; [menu setMenuVisible: YES animated: YES]; }
In order for the menu to appear, two steps have to be performed: ➤➤
The view has to declare that it can become a FirstResponder.
➤➤
Implement the canPerformAction: withSender: method, which identifies what operations will be supported.
The supported operations will be Cut and Copy for the imageView, and Paste for the pasteView. This is illustrated in Listing 11-24. Listing 11-24: canBecomeFirstResponder and canPerformAction:withSender: methods - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender{ if (action == @selector(cut:)) { return ([self selectedView] == imageView) ? YES : NO; } else if (action == @selector(copy:)) { return ([self selectedView] == imageView) ? YES : NO; } else if (action == @selector(paste:)) { return ([self selectedView] == pasteView) ? YES : NO;
414
❘ Chapter 11 The Pasteboard
} else if (action == @selector(select:)) { return NO; } else if (action == @selector(selectAll:)) { return NO; } else { return [super canPerformAction:action withSender:sender]; } }
All that is left now are the actual cut, copy, and paste operations with the pasteboard, as shown in Listing 11-25. Notice that in the cut operation, the image is merely hidden, and that both the cut and copy use the method placeImageOnPasteboard to place the image on the pasteboard. To get the image off the pasteboard, it is retrieved as an NSData object and converted to a UIImage by the imageWithData method. Listing 11-25: The cut, copy, and paste and placeImageOnPasteboard methods - (void)cut:(id)sender { [self copy:sender]; [imageView setHidden:YES]; } - (void)copy:(id)sender { [self placeImageOnPasteboard:[self imageView]]; } - (void)paste:(id)sender { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; NSData *data = [appPasteBoard dataForPasteboardType: @”com.marizack.CopyPasteImage.imageView”]; pasteView.image = [UIImage imageWithData:data]; } - (void)placeImageOnPasteboard:(id)view { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; appPasteBoard.persistent = YES; NSData *data = UIImagePNGRepresentation([view image]); [appPasteBoard setData:data forPasteboardType:@”com.marizack.CopyPasteImage.imageView”]; }
Listing 11-26 shows the complete modified CopyPasteImage_iPhoneViewController.m file. Listing 11-26: The complete modified CopyPasteImage_iPhoneViewController.m file
(Chapter11/CopyPasteImage-iPhone/Classes/CopyPasteImage_iPhoneViewController.m) #import “CopyPasteImage_iPhoneViewController.h” @implementation CopyPasteImage_iPhoneViewController
@synthesize imageView; @synthesize pasteView;
Cutting and Pasting Images
@synthesize selectedView; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSSet *copyTouches = [event touchesForView:imageView]; NSSet *pasteTouches = [event touchesForView:pasteView]; [self becomeFirstResponder]; if ([copyTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:imageView afterDelay:0.9f]; } else if([pasteTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:pasteView afterDelay:0.9f]; } [super touchesBegan:touches withEvent:event]; } - (void)showMenu:(id)view { [self setSelectedView:view]; UIMenuController * menu = [UIMenuController sharedMenuController]; [menu setTargetRect: CGRectMake(5, 10, 1, 1) inView: view]; [menu setMenuVisible: YES animated: YES]; } - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender{ if (action == @selector(cut:)) { return ([self selectedView] == imageView) ? YES : NO; } else if (action == @selector(copy:)) { return ([self selectedView] == imageView) ? YES : NO; } else if (action == @selector(paste:)) { return ([self selectedView] == pasteView) ? YES : NO; } else if (action == @selector(select:)) { return NO; } else if (action == @selector(selectAll:)) { return NO; } else { return [super canPerformAction:action withSender:sender]; } } - (void)cut:(id)sender { [self copy:sender]; [imageView setHidden:YES]; } - (void)copy:(id)sender { [self placeImageOnPasteboard:[self imageView]]; } - (void)paste:(id)sender { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; NSData *data = [appPasteBoard dataForPasteboardType: @”com.marizack.CopyPasteImage.imageView”]; pasteView.image = [UIImage imageWithData:data];
❘ 415
416
❘ Chapter 11 The Pasteboard
} - (void)placeImageOnPasteboard:(id)view { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; appPasteBoard.persistent = YES; NSData *data = UIImagePNGRepresentation([view image]); [appPasteBoard setData:data forPasteboardType:@”com.marizack.CopyPasteImage.imageView”]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setImageView:nil]; [self setPasteView:nil]; [self setSelectedView:nil]; } - (void)dealloc { [imageView release]; [pasteView release]; [selectedView release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of this topic.
Creating Custom Menus for the iPad In this application, you are going to cut or copy an existing image to the UIPasteboard, then retrieve that image from the pasteboard and paste it into another UIImageView, similar to the iPhone application; however, SDK 3.2 introduced the capability to customize the menu, and to create a custom menu item that allows the user to restore the original image after it has been cut. This app will present to the user two images, as shown in Figure 11-17: ➤➤
The original image, grandpa.png
➤➤
A default stub image, default.png, into which the user will paste the original image
When the user touches and holds the original image, the Cut and Copy menu will appear, as shown in Figure 11-18. When the user presses the Cut menu item, the image will be placed on the general pasteboard. When the user touches and holds the default stub image, the Paste menu will appear, as shown in Figure 11-19. When the user presses the Paste menu item, the image will be retrieved from the general pasteboard and placed into the UIImageView that previously held the default stub image, as shown in Figure 11-20. When the user presses the Restore View menu item, the image will be retrieved from the general pasteboard and placed back into the original UIImageView, as shown in Figure 11-21.
Creating Custom Menus for the iPad
Figure 11-17
Figure 11-20
Figure 11-19
Figure 11-18
Figure 11-21
❘ 417
418
❘ Chapter 11 The Pasteboard
Development Steps: Creating Custom Menus for the iPad To create this application, execute the following steps:
1.
Start Xcode and create a View-based application for the iPad named CopyPasteImage-iPad. If you need to see this step, please see Appendix A for the steps to begin a View-based application.
2.
You need to add two images to the project. ➤➤
Select Other Resources in Xcode’s Groups & Files window on the left.
➤➤
Choose Project ➪ Add To Project.
➤➤
Select your two images, default.png and grandpa.png, and click Add (see Figure 11-22).
➤➤
Check Copy items into destination group’s folder, and click Add (see Figure 11-23).
Figure 11-22
Figure 11-23
3.
Double-click the CopyPasteImage_iPadViewController.xib file to launch Interface Builder (see Figure 11-24).
4.
From the Interface Builder Library (Tools ➪ Library), select and drag two UIImageViews sized 375 × 360 to the View window, placing one above the other, by executing the following steps: ➤➤
Drag your first UIImageView to the View window. By default, it will occupy the entire window, which is fine for now.
➤➤
Choose Tools ➪ Size Inspector from the main menu. You will see W:768 W:1004 just under the Frame drop-down in the upper-right corner of the inspector.
➤➤
Change the size of the view to W:375 H:360.
➤➤
Adjust your resized UIImageView to be centered at the top.
➤➤
Choose Tools ➪ Attribute Inspector, select the Image drop-down control, and choose one of your images, which will be the image that the user cuts or copies.
➤➤
At the bottom, check User Interaction Enabled.
➤➤
Choose Edit ➪ Duplicate and you will now have a duplicate of your resized UIImageView.
➤➤
Adjust your second UIImageView to be just below the original UIImageView, and choose the other image from the Image drop-down control in the Attribute Inspector. This will be the default image that is pasted over when the user chooses Paste. Your populated View should resemble the one in Figure 11-25.
Creating Custom Menus for the iPad
Figure 11-24
Figure 11-25
❘ 419
420
❘ Chapter 11 The Pasteboard
5.
Back in the Interface Builder Library, click Classes at the top and scroll to select your CopyPasteImage _iPadViewController class. At the bottom, click the Outlets button. Then click the + and add the following outlets, as shown in Figure 11-26:
6.
➤➤
imageView as a UIImageView
➤➤
pasteView as a UIImageView
➤➤
selectedView as a UIImageView
From the main menu of Interface Builder, choose File ➪ Write Class Files, and then Save ➪ Merge. A window will appear with your new additions to CopyPasteImage_iPadViewController.h on the left and the original template on the right. In the lower-right corner, choose Actions ➪ Choose Left, and then choose File ➪ Save from the first pop-up and select Merge from the second pop-up and close the window (see Figure 11-27). For the next window, CopyPasteImage_iPadViewController.m, just close it, as no new additions are generated. You now have an Objective-C template that will hold your application’s logic.
Figure 11-27
Figure 11-26
7.
Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify and connect the original imageView to the File’s Owner icon
➤➤
Identify and connect the original pasteView to the File’s Owner icon
Creating Custom Menus for the iPad
❘ 421
8.
To make the connection to identify the imageView, control-click and hold over the File’s Owner icon. Still holding the mouse, drag until over the top UIImageView and release. In the window that pops up, click imageView to make the connection (see Figure 11-28). Repeat this to connect the pasteView outlet to the UIImageView that displays the default.png image.
9.
Choose File ➪ Save and File ➪ Quit to exit Interface Builder.
Figure 11-28
You are now finished with the user interface, so it’s time to turn to the source code and enter the logic.
Source Code Listings for Creating Custom Menus for the iPad For this application, CopyPasteImage_iPadAppDelegate.h and CopyPasteImage_iPadAppDelegate.m are not modified and are used as generated. Remember that Interface Builder created template source code. Listing 11-27 shows the template for the CopyPasteImage_iPadAppDelegate.h interface file and Listing 11-28 shows the template for the CopyPasteImage_iPadAppDelegate.m implementation file. Listing 11-27: The CopyPasteImage_iPadViewController.h template #import @interface CopyPasteImage_iPadViewController : UIViewController { IBOutlet UIImageView *imageView; IBOutlet UIImageView *pasteView; IBOutlet UIImageView *selectedView; } @end
Listing 11-28: The CopyPasteImage_iPadViewController.m template #import “CopyPasteImage_iPadViewController.h” @implementation CopyPasteImage_iPadViewController // Override to allow orientations other than the default portrait orientation.
422
❘ Chapter 11 The Pasteboard
- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } @end
CopyPasteImage_iPadViewController.h Modifications to the Template You declared three outlets, imageView, pasteView, and selectedView, in Interface Builder. You must now define the properties for these variables in order to get and set their value (see Listing 11-29). The IBOutlet was moved to the property declaration, and removed from the selectedView altogether, as it is not a view used in the interface; it is just used for temporary storage. There is also a new method declared, placeImageOnPasteboard:, which will be used by the cut: and copy: methods to place the selected image onto the pasteboard. Listing 11-29: The complete modified CopyPasteImage_iPadViewController.h file (Chapter11/ CopyPasteImage-iPad/Classes/CopyPasteImage_iPadViewController.h) #import @interface CopyPasteImage_iPadViewController : UIViewController { UIImageView *imageView; UIImageView *pasteView; UIImageView *selectedView; } @property (nonatomic, retain) IBOutlet UIImageView *imageView; @property (nonatomic, retain) IBOutlet UIImageView *pasteView; @property (nonatomic, retain) UIImageView *selectedView; - (void)placeImageOnPasteboard:(id)view; @end
CopyPasteImage_iPadViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the CopyPasteImage_iPadViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 11-30).
Creating Custom Menus for the iPad
❘ 423
Listing 11-30: Addition of @synthesize @implementation CopyPasteImage_iPadViewController @synthesize imageView; @synthesize pasteView; @synthesize selectedView;
When the user touches the top image, the imageView will be the object that represents the active view; and when the user touches the bottom image, the pasteView will be the object that represents the active view. The method that you implement is touchesBegan:withEvent: (see Listing 11-31). Listing 11-31: The touchesBegan:withEvent: method - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSSet *copyTouches = [event touchesForView:imageView]; NSSet *pasteTouches = [event touchesForView:pasteView]; [self becomeFirstResponder]; if ([copyTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:imageView afterDelay:0.9f]; } else if([pasteTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:pasteView afterDelay:0.9f]; } [super touchesBegan:touches withEvent:event]; }
Notice the afterDelay argument. This will distinguish between a mere tap and a touch and hold. When the user touches and holds the image, the showMenu: method will be called. This is the method that displays the Cut and Copy menu options, as well as the user-created Restore View menu item (see Listing 11-32). Listing 11-32: The showMenu: method - (void)showMenu:(id)view { [self setSelectedView:view]; UIMenuController * menu = [UIMenuController sharedMenuController]; if(view == pasteView) { UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@”Restore View” action:@selector(restoreView:)]; menu.arrowDirection = UIMenuControllerArrowLeft; menu.menuItems = [NSArray arrayWithObject:menuItem]; } [menu setTargetRect: CGRectMake(5, 10, 1, 1) inView: view]; [menu setMenuVisible: YES animated: YES]; }
In order for the menu to appear, two steps have to be performed: ➤➤
The view has to declare that it can become a FirstResponder.
➤➤
Implement the canPerformAction: withSender: method, which identifies what operations will be supported.
The supported operations will be Cut and Copy for the imageView, and Paste and Restore View for the pasteView. This is illustrated in Listing 11-33.
424
❘ Chapter 11 The Pasteboard
Listing 11-33: canBecomeFirstResponder and canPerformAction:withSender: methods - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender{ if (action == @selector(cut:)) { return ([self selectedView] == imageView) ? YES : NO; } else if (action == @selector(copy:)) { return ([self selectedView] == imageView) ? YES : NO; } else if (action == @selector(paste:)) { return ([self selectedView] == pasteView) ? YES : NO; } else if (action == @selector(select:)) { return NO; } else if (action == @selector(selectAll:)) { return NO; } else if (action == @selector(restoreView:)) { return ([self selectedView] == pasteView) ? YES : NO; } else { return [super canPerformAction:action withSender:sender]; } }
All that is left now are the actual cut, copy, paste, and restoreView operations with the pasteboard, as shown in Listing 11-34. Notice that in the cut operation, the image is just merely hidden and that both the cut and copy operations use the method placeImageOnPasteboard to place the image on the pasteboard. To get the image off the pasteboard, it is retrieved as an NSData object and converted to a UIImage by the imageWithData method. Listing 11-34: The cut, copy, and paste restoreView and placeImageOnPasteboard methods - (void)cut:(id)sender { [self copy:sender]; [imageView setHidden:YES]; } - (void)copy:(id)sender { [self placeImageOnPasteboard:[self imageView]]; } - (void)paste:(id)sender { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; NSData *data = [appPasteBoard dataForPasteboardType: @”com.marizack.CopyPasteImage.imageView”]; pasteView.image = [UIImage imageWithData:data]; } - (void)restoreView:(id)sender { [imageView setHidden:NO]; } - (void)placeImageOnPasteboard:(id)view { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES];
Creating Custom Menus for the iPad
❘ 425
appPasteBoard.persistent = YES; NSData *data = UIImagePNGRepresentation([view image]); [appPasteBoard setData:data forPasteboardType:@”com.marizack.CopyPasteImage.imageView”]; }
Listing 11-35 is the complete modified CopyPasteImage_iPadViewController.m file. Listing 11-35: The complete modified CopyPasteImage_iPadViewController.m file (Chapter11/ CopyPasteImage-iPad/Classes/CopyPasteImage_iPadViewController.m) #import “CopyPasteImage_iPadViewController.h” @implementation CopyPasteImage_iPadViewController @synthesize imageView; @synthesize pasteView; @synthesize selectedView; - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSSet *copyTouches = [event touchesForView:imageView]; NSSet *pasteTouches = [event touchesForView:pasteView]; [self becomeFirstResponder]; if ([copyTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:imageView afterDelay:0.9f]; } else if([pasteTouches count] > 0) { [self performSelector:@selector(showMenu:) withObject:pasteView afterDelay:0.9f]; } [super touchesBegan:touches withEvent:event]; } - (void)showMenu:(id)view { [self setSelectedView:view]; UIMenuController * menu = [UIMenuController sharedMenuController]; if(view == pasteView) { UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:@”Restore View” action:@selector(restoreView:)]; menu.arrowDirection = UIMenuControllerArrowLeft; menu.menuItems = [NSArray arrayWithObject:menuItem]; } [menu setTargetRect: CGRectMake(5, 10, 1, 1) inView: view]; [menu setMenuVisible: YES animated: YES]; } - (BOOL)canBecomeFirstResponder { return YES; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender{ if (action == @selector(cut:)) { return ([self selectedView] == imageView) ? YES : NO; } else if (action == @selector(copy:)) {
426
❘ Chapter 11 The Pasteboard
return ([self } else if (action return ([self } else if (action return NO; } else if (action return NO; } else if (action return ([self } else { return [super } }
selectedView] == imageView) ? YES : NO; == @selector(paste:)) { selectedView] == pasteView) ? YES : NO; == @selector(select:)) { == @selector(selectAll:)) { == @selector(restoreView:)) { selectedView] == pasteView) ? YES : NO; canPerformAction:action withSender:sender];
- (void)cut:(id)sender { [self copy:sender]; [imageView setHidden:YES]; } - (void)copy:(id)sender { [self placeImageOnPasteboard:[self imageView]]; } - (void)paste:(id)sender { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; NSData *data = [appPasteBoard dataForPasteboardType: @”com.marizack.CopyPasteImage.imageView”]; pasteView.image = [UIImage imageWithData:data]; } - (void)restoreView:(id)sender { [imageView setHidden:NO]; } - (void)placeImageOnPasteboard:(id)view { UIPasteboard *appPasteBoard = [UIPasteboard pasteboardWithName:@”CopyPasteImage” create:YES]; appPasteBoard.persistent = YES; NSData *data = UIImagePNGRepresentation([view image]); [appPasteBoard setData:data forPasteboardType:@”com.marizack.CopyPasteImage.imageView”]; }
// Override to allow orientations other than the default portrait orientation. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view.
Summary
❘ 427
// e.g. self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of this topic.
Summary In this chapter, two types of data were placed and retrieved on the UIPasteboard: text and an image. In both cases, creating a submenu enabled the user to choose the desired action. The cutting, copying, and pasting of images required a conversion to NSData. SDK 3.2 enables the creation of new submenu items. The iPad version of image processing in this chapter added a Restore View menu item to illustrate this new capability of adding custom menu items.
12
Unit Testing What’s in this chaPter? ➤➤
How to create a unit test application target
➤➤
How to create a unit test bundle
➤➤
How to create an application unit test
➤➤
Running unit tests on the device
You have spent a great deal of time creating your application and you cannot wait to get it into the iTunes App Store, but have you tested it enough? The last thing you want to do is release an application full of bugs. Previously, traditional testing meant running an application interactively and waiting for issues to emerge; there was no consistent arsenal of standard tests. Over the years, developers have struggled with the most economical way to integrate testing into the development lifecycle and still deliver quality applications on time. The unit test case process has been welcomed into the project lifecycle because of its ease of implementation and repetitive automated execution. Through a suite of standardized test cases, applications are tested as they evolve over time, to identify bugs immediately, not later after implementation. Xcode allows you to easily create a suite of unit test cases to assist you in your quality control phase of your application’s lifecycle. This chapter provides you with the steps to build a sample application followed by the creation of unit test cases you can execute to test your application.
settinG UP the environment To set up a testing environment, you will be making a copy of your normal application target. Using the copy, you will add the following in order to run unit tests on your application: ➤➤
A separate application-testing target
➤➤
An iOS 4 unit test bundle target
➤➤
A unit test class for test cases
Using an application-testing target Creation of a separate target for testing is important because the unit testing should co-exist with your application development, not be embedded into it. It should remain separate, but yet still have access to all the functionality.
430
❘ Chapter 12 Unit Testing
Within the application-testing target there is the unit test bundle. The unit test bundle holds the unit test cases. The application-testing target is dependent on the testing bundle, so when the application is launched for testing, the bundle provides the object on which the tests can run against. The unit test class is a generic template that you customize with your own unit test cases. The Objective-C test case class template included is pre-populated with all the necessary imports to support the testing environment. Within the unit test class, you write individual test case methods. These methods are like any normal method with one exception: The method name has to begin with the word “test.” In the sample you write in this chapter, the test case methods are as follows: ➤➤
testAppDelegate
➤➤
testViewController
➤➤
testNamesCount
➤➤
testPickerSelection
In addition to the test case methods, the unit test class includes setUp and tearDown methods for initialization and cleanup of the environment, respectively. Application unit test cases cannot be run in the simulator. To be able to actually run the application test cases on your device, you need to install a development provisioning file. One thing to note is that running unit test cases against your application is not an interactive process. All control, including supplying of values, is controlled by the test cases themselves.
A Simple Unit Test In this application, a UIPickerView displays a list of names on one component and a list of ages on the other component. When the user selects a name or an age, the selected values are displayed in a name or an age text field, respectively (see Figure 12-1): As this is a unit test, there will not be any user interaction. The selecting of the components will be performed programmatically: [pickerView selectRow:1 inComponent:0 animated:NO]; [pickerView selectRow:2 inComponent:1 animated:NO];
The check for validity will be a programmatic check on what was selected. For the name, the following is used: [pickerView selectedRowInComponent:0]
For the age, this is used: [pickerView selectedRowInComponent:1]
The following test are performed in this unit test: ➤➤
testAppDelegate
➤➤
testViewController
➤➤
testNamesCount
➤➤
testPickerSelection Figure 12-1
A Simple Unit Test
❘ 431
Development Steps: A Simple Unit Test To create this application that will be tested, execute the following steps:
1.
Start Xcode and create a Window-based application for iPhone and name it UnitTests. If you need to see this step, please see Appendix A for the steps to begin a Window-based application.
2.
In the Groups & Files section of Xcode, click on the Classes group. Choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it PickerViewController.
3.
With the Classes group still selected, choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it PropertyList.
4.
Double-click the MainWindow.xib file to launch Interface Builder (see Figure 12-2).
Figure 12-2
5.
From the Interface Builder Library (Tools ➪ Library), choose and drag the following to the View window. Your interface should now look like Figure 12-3: ➤➤
Two UILabels
1. 2.
Double-click the top label and type Name:. Double-click the label just below the top label and type Age:.
432
❘ Chapter 12 Unit Testing
➤➤
Two UITextFields
1. 2.
➤➤
Two UILabels on the middle selection bar of the UIPickerView:
1. 2.
6.
The lower text field is located to the right of the Age label.
One UIPickerView at the bottom of the view
➤➤
The top text field is located to the right of the Name label.
Double-click the first label and type Name: and place it on the bar in the middle. Double-click the second label and type Age: and place it on the bar at the right.
Back in the Interface Builder Library, choose an NSObject (it is labeled Object), and drag it right below the First Responder icon (see Figure 12-4).
Figure 12-3
Figure 12-4
7.
Choose Tools ➪ Identity Inspector and select PickerViewController for the class, as shown in Figure 12-5.
8.
Back in the Interface Builder Library, click Classes at the top and scroll to and select your Picker ViewController class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure 12-6: ➤➤ ➤➤
ageTextField (as a UITextField instead of id type) nameTextField (as a UITextField instead of id pickerView (as a UIPickerView instead
of id type)
9.
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up and then select Merge from the second pop-up. There were no changes to the PickerViewController.m file, so just close it. A window will appear with your new additions to PickerViewController.h on the left and the original template on the right (see Figure 12-7). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge from the main menu and close the window.
A Simple Unit Test
Figure 12-5
10.
❘ 433
Figure 12-6
You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UITextField as ageTextField.
➤➤
Identify the UITextField as nameTextField.
➤➤
Identify the UIPickerView as pickerView.
To make the connection to identify the UITextField as ageTextField, control-click the Picker View Controller icon to bring up the Inspector (see Figure 12-8).
11.
From the right of the Picker View Controller Inspector, control-drag from the circle next to the ageTextField to the UITextField ageTextField until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made (see Figure 12-9). Dismiss the File’s Owner Inspector.
12.
Repeat step 11 for the nameTextField to the UITextField nameTextField, and the pickerView to the UIPickerView pickerView (see Figure 12-10), and then select File ➪ Save to save the files.
434
❘ Chapter 12 Unit Testing
Figure 12-7
Figure 12-8
Figure 12-9
Figure 12-10
13.
Control-drag from the pickerView to the Picker View Controller icon, and select the dataSource outlet (see Figure 12-11).
14.
Control-drag from the pickerView to the Picker View Controller icon, select the delegate outlet, and then select File ➪ Save from the main menu, to save the files.
Now it is time to enter your logic.
A Simple Unit Test
Figure 12-11
Source Code Listings for a Simple Unit Test For this application, the UnitTestsAppDelegate.h file is modified as follows. Add an instance variable, pickerViewController, to hold the Picker View Controller object (see Listing 12-1). Listing 12-1: The complete UnitTestsAppDelegate.h file (Chapter12/UnitTests/Classes/ UnitTestsAppDelegate.h) #import @class PickerViewController; @interface UnitTestsAppDelegate : NSObject { UIWindow *window; PickerViewController *pickerViewController;
❘ 435
436
❘ Chapter 12 Unit Testing
} @property (nonatomic, retain) IBOutlet UIWindow *window; @property (nonatomic, retain) IBOutlet PickerViewController *pickerViewController; @end
The UnitTestsAppDelegate.m file is modified as follows. Add @synthesize pickerViewController to match the declaration (see Listing 12-2). Listing 12-2: The complete UnitTestsAppDelegate.m file (Chapter12/UnitTests/Classes/ UnitTestsAppDelegate.m) #import “UnitTestsAppDelegate.h” @implementation UnitTestsAppDelegate @synthesize window; @synthesize pickerViewController;
#pragma mark #pragma mark Application lifecycle - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [window makeKeyAndVisible]; return YES; }
- (void)applicationWillResignActive:(UIApplication *)application { }
- (void)applicationDidEnterBackground:(UIApplication *)application { }
- (void)applicationWillEnterForeground:(UIApplication *)application { }
- (void)applicationDidBecomeActive:(UIApplication *)application { }
- (void)applicationWillTerminate:(UIApplication *)application { }
#pragma mark #pragma mark Memory management - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { }
A Simple Unit Test
❘ 437
- (void)dealloc { [window release]; [super dealloc]; }
@end
Connecting the PickerViewController class to the UnitTestsAppDelegate Because the UnitTestsAppDelegate class is modified to include the PickerViewController class, a connection in Interface Builder has to be made. To connect the two classes together perform the following:
1.
Double-click on MainWindow.xib to bring up Interface Builder and control-click on the Unit Tests App Delegate icon to bring up the Unit Tests App Delegate Inspector.
2.
From the right of the Picker View Controller Inspector, control-drag from the circle next to the pickerView Controller to the Picker View Controller icon until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made (see Figure 12-12). Dismiss the Picker View Controller Inspector, and select File ➪ Save to save the files.
Figure 12-12
PickerViewController.h Modifications to the Template You declared three outlets in Interface Builder: pickerView, nameTextField, and ageTextField. You must now define the properties for these variables in order to get and set their values (see Listing 12-3). The IBOutlet was moved to the property declaration. There is also an instance variable names of type NSArray. Listing 12-3: The complete PickerViewController.h file (Chapter12/UnitTests/Classes/
PickerViewController.h)
#import
@interface PickerViewController : NSObject { UIPickerView *pickerView; UITextField *nameTextField; UITextField *ageTextField; NSArray *names; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
IBOutlet UIPickerView *pickerView; IBOutlet UITextField *nameTextField; IBOutlet UITextField *ageTextField; NSArray *names;
@end
PickerViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the PickerViewController.m template.
438
❘ Chapter 12 Unit Testing
For each of the view properties that were declared, you must match them with @synthesize (see Listing 12-4). Listing 12-4: Addition of @synthesize #import “PickerViewController.h” #import “PropertyList.h”
@implementation PickerViewController @synthesize @synthesize @synthesize @synthesize
pickerView; nameTextField; ageTextField; names;
When the PickerViewController initializes, the Data.plist file, containing the names and ages, is loaded into the application’s memory (see Listing 12-5). Listing 12-5: The init method #pragma mark #pragma mark Initialization - init { [self setNames:[[NSDictionary dictionaryFromPropertyList:@”Data”] objectForKey:@”names”]]; return self; }
To populate the pickerView, the methods shown in Listing 12-6 provide the values that will be displayed. Listing 12-6: The pickerView data source methods #pragma mark #pragma mark UIPickerViewDataSource - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 2; } - (NSInteger)pickerView:(UIPickerView *)aPickerView numberOfRowsInComponent:(NSInteger)component { return [[self names] count]; } - (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component { CGFloat componentWidth = 0.0; if (component == 0) componentWidth = 180.0; else componentWidth = 100.0; return componentWidth; } - (NSString *)pickerView:(UIPickerView *)aPickerView
A Simple Unit Test
titleForRow:(NSInteger)row forComponent:(NSInteger)component { NSString *returnStr = @”“; NSDictionary *dict = [names objectAtIndex:row]; if (component == 0) { returnStr = [dict objectForKey:@”name”]; } else { returnStr = [dict objectForKey:@”age”]; } return returnStr; }
As shown in Listing 12-7, when a value is selected in the pickerView, pickerView:didSelectRow: inComponent: is called. Listing 12-7: The pickerView:didSelectRow:inComponent: delegate method #pragma mark #pragma mark UIPickerViewDelegate - (void)pickerView:(UIPickerView *)aPickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component { NSString *name = [[names objectAtIndex:[pickerView selectedRowInComponent:0]] objectForKey:@”name”]; NSString *age = [[names objectAtIndex:[pickerView selectedRowInComponent:1]] objectForKey:@”age”]; [nameTextField setText:name]; [ageTextField setText:age]; }
The PickerViewController.m is now complete. Listing 12-8 shows the complete implementation. Listing 12-8: The complete PickerViewControllerm.m file (Chapter12/UnitTests/Classes/ PickerViewControllerm.m) #import “PickerViewController.h” #import “PropertyList.h”
@implementation PickerViewController @synthesize @synthesize @synthesize @synthesize
pickerView; nameTextField; ageTextField; names;
#pragma mark #pragma mark Initialization - init { [self setNames:[[NSDictionary dictionaryFromPropertyList:@”Data”] objectForKey:@”names”]]; return self;
❘ 439
440
❘ Chapter 12 Unit Testing
} #pragma mark #pragma mark UIPickerViewDataSource - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 2; } - (NSInteger)pickerView:(UIPickerView *)aPickerView numberOfRowsInComponent:(NSInteger)component { return [[self names] count]; } - (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component { CGFloat componentWidth = 0.0; if (component == 0) componentWidth = 180.0; else componentWidth = 100.0; return componentWidth; } - (NSString *)pickerView:(UIPickerView *)aPickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component { NSString *returnStr = @”“; NSDictionary *dict = [names objectAtIndex:row]; if (component == 0) { returnStr = [dict objectForKey:@”name”]; } else { returnStr = [dict objectForKey:@”age”]; } return returnStr; } #pragma mark #pragma mark UIPickerViewDelegate - (void)pickerView:(UIPickerView *)aPickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component { NSString *name = [[names objectAtIndex:[pickerView selectedRowInComponent:0]] objectForKey:@”name”]; NSString *age = [[names objectAtIndex:[pickerView selectedRowInComponent:1]] objectForKey:@”age”]; [nameTextField setText:name]; [ageTextField setText:age]; } @end
A Simple Unit Test
❘ 441
PropertyList.h Category Interface Objective-C has a wonderful feature called categories. Categories enable developers to extend a class without having to subclass the object. With categories, you extend a class by adding methods; you cannot add instance variables, and overriding existing methods is discouraged. In this application, NSDictionary is extended to load a plist file. Because the data that is stored in the Data.plist file resembles a dictionary, the contents of the file are accessed through the use of keys. One thing that is missing in the NSDictionary class is the capability to load the file into memory. Through the use of categories, this functionality is added in a PropertyList class and is a category of NSDictionary. Listing 12-9 shows the complete PropertyList.h interface. Listing 12-9: The complete PropertyList.h file (Chapter12/UnitTests/Classes/ PropertyList.h) #import
@interface NSDictionary(PropertyList) + (NSDictionary *)dictionaryFromPropertyList:(NSString *)filename; @end
PropertyList.m Category Implementation The only method in this category will be dictionaryFromPropertyList to load the plist file into an NSDictionary. Notice that the method is a factory method. This is by choice, not a requirement of categories. Listing 12-10 shows the complete implementation. Listing 12-10: The complete PropertyList.m file (Chapter12/UnitTests/Classes/ PropertyList.m) #import “PropertyList.h”
@implementation NSDictionary(PropertyList) + (NSDictionary *)dictionaryFromPropertyList:(NSString *)filename { NSString *errorDesc = nil; NSPropertyListFormat format; NSString *plistPath = [[NSBundle mainBundle] pathForResource:filename ofType:@”plist”]; NSData *plistXML = [[NSFileManager defaultManager] contentsAtPath:plistPath]; NSDictionary *temp = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorDesc]; if (!temp) { NSLog(@”%@”, errorDesc); [errorDesc release]; } return temp; } @end
442
❘ Chapter 12 Unit Testing
Creating the Data.plist Data Source The Data.plist file is simply an array of dictionaries that holds the name and age values. The pickerView is divided into two components; the name will be on one component and the age will be on the other component. To create the Data.plist file, follow these steps:
1.
In the Groups & Files section of Xcode, click the Resources group. Choose File ➪ New File, select Property List in the Mac OS X section (see Figure 12-13), and name it Data.plist.
Figure 12-13
2.
3.
4.
In the Groups & Files section of Xcode, click the Data.plist file and click Root in the edit window to highlight it. ➤➤
With the Root highlighted, click the tab on the far right. This will create a new item under the Root.
➤➤
Enter names to replace New Item, and choose Array instead of String (see Figure 12-14).
With “names” highlighted, click the triangle so it points down and then click the tab to create a new item: ➤➤
Choose Dictionary instead of String.
➤➤
Click the triangle so it points down and then click the tab to create a new item.
➤➤
Enter name to replace New Item and enter 25 for the value (see Figure 12-15).
Click Item 0 to highlight it and then click the triangle to close it: ➤➤
Choose Edit ➪ Copy.
➤➤
Choose Edit ➪ Paste five more times so you wind up with six items: Item 0–Item 5 (see Figure 12-16).
A Simple Unit Test
Figure 12-14
Figure 12-15
5.
Click the triangle for Item 1 through Item 5 to reveal the keys and values. Replace the following: ➤➤
Replace Joe with Bill for Item 1 name.
➤➤
Replace 25 with 30 for Item 1 age.
➤➤
Replace Joe with Ann for Item 2 name.
➤➤
Replace 25 with 19 for Item 2 age.
❘ 443
444
❘ Chapter 12 Unit Testing
➤➤
Replace Joe with Maria for Item 3 name.
➤➤
Replace 25 with 38 for Item 3 age.
➤➤
Replace Joe with Pam for Item 4 name.
➤➤
Replace 25 with 53 for Item 4 age.
➤➤
Replace Joe with Art for Item 5 name.
➤➤
Replace 25 with 65 for Item 5 age.
Choose File ➪ Save to save your changes (see Figure 12-17).
Figure 12-16
Figure 12-17
A Simple Unit Test
❘ 445
Listing 12-11 shows the completed Data.plist file. Listing 12-11: The complete Data.plist file (Chapter12/UnitTests/Classes/ Data.plist) names <array> name <string>Joe age <string>25 name <string>Bill age <string>30 name <string>Ann age <string>19 name <string>Maria age <string>38 name <string>Pam age <string>53 name <string>Art age <string>65
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the Simple Unit Test section. It is important that the application runs correctly before the creation and execution of the unit tests.
446
❘
chaPter 12 uNit testiNg
Unit test creation stePs: a simPle Unit test You have just completed the steps to build your application. It is now time to create the unit test cases that will be executed to ensure the application is operating correctly. To create these unit test cases, execute the following steps:
1 . 2 . 3 . 4 . 5 . 6 .
Click the triangle to open Targets. Highlight the UnitTests target. Choose Edit ➪ Duplicate and rename the new target UnitTestsTesting. Choose Project ➪ New Target, and choose Unit Test Bundle and name it UnitTestsBundle. Click Finish. Your targets will open. Click the Properties tab and note the Identifier entry.
Of course, in order to test the application, it must be installed on your device. That means you will have to generate a development provisioning profile, a procedure described later in the chapter. One of the steps, however, is to create a unique id for your application. Choose an id and enter it in the Bundle Identifier value field:
The id to replace Apple’s default bundle identifi er has to be unique and match the same id that is used when you create the provisioning profiles. Do not use com.ezeprod.UnitTests. Substitute with your own unique id. Replace the id com.yourcompany.${PRODUCT_NAME:rfc1034identifier} with a unique id. For the example in this chapter, I chose com.ezeprod.UnitTests (see Figure 12-18). You must choose something else, but be sure to use the same id when you generate your development provisioning profile.
fiGUre 12-18
Unit Test Creation Steps: A Simple Unit Test
❘ 447
2.
3.
Click the triangle on UnitTestsTesting and drag the UnitTestsBundle just above Copy Bundle Resources, as shown in Figure 12-19.
4.
Click the triangle on Copy Bundle Resources and Products and drag UnitTestsBundle.octest into the Copy Bundle Resources above the MainWindow.xib (see Figure 12-20).
Select the UnitTestTesting target, choose File ➪ Get Info, and change com.yourcompany.${PRODUCT_ NAME:rfc1034identifier} to com.ezeprod.UnitTests. As before, you may choose something else as long as it is the same id used when you generate your development provisioning profile.
Figure 12-19
Figure 12-20
448
❘ Chapter 12 Unit Testing
5.
Click the UnitTests project at the top of Groups & Files, choose Project ➪ New Group, and name the group Tests. With Tests highlighted, choose File ➪ New File and select Objective-C test case class as shown in Figure 12-21.
6.
Click Next, name the file AppTests, and select UnitTestsBundle as the only target (see Figure 12-22). Then click Finish.
Figure 12-21
Figure 12-22
Unit Test Creation Steps: A Simple Unit Test
❘ 449
There will be four tests run against this application and they are as follows: ➤➤
testAppDelegate — Test to ensure the application launches and is running.
➤➤
testViewController — Test to ensure the viewController has initialized.
➤➤
testNamesCount — Test to ensure the application properly loaded the list of names, and that all 6 names are available
➤➤
testPickerSelection — Test to ensure that the pickerView was initialized properly and
that selections from the pickerView are the names that are expected.
Source Code Listings for a Simple Unit Test Listing 12-12 shows the complete AppTests.h file, and Listing 12-13 shows AppTest.m. Listing 12-12: The complete AppTests.h file (Chapter12/UnitTests/ /AppTests.h) #import <SenTestingKit/SenTestingKit.h> #import //#import “application_headers” as required #import “UnitTestsAppDelegate.h” #import “PickerViewController.h”
@interface AppTests : SenTestCase { UnitTestsAppDelegate *appDelegate; PickerViewController *viewController; NSDictionary *pList; } @property (nonatomic, retain) UnitTestsAppDelegate *appDelegate; @property (nonatomic, retain) PickerViewController *viewController; @property (nonatomic, retain) NSDictionary *pList; @end
Listing 12-13: The complete AppTests.m file (Chapter12/UnitTests/ /AppTests.m) #import “AppTests.h” #import “PropertyList.h”
@implementation AppTests @synthesize appDelegate; @synthesize viewController; @synthesize pList; - (void) setUp { NSLog(@”%@ setUp”, [self name]); [self setAppDelegate:[[UIApplication sharedApplication] delegate]]; [self setViewController:[appDelegate pickerViewController]]; [self setPList:[NSDictionary dictionaryFromPropertyList:@”Data”]]; STAssertNotNil([self pList], @”Data.plist was not able to be loaded”); } - (void) tearDown { [pList release]; [appDelegate release]; [viewController release];
450
❘
chaPter 12 uNit testiNg
NSLog(@”%@ tearDown”, [self name]); } - (void)testAppDelegate { STAssertNotNil([self appDelegate], @”Cannot find the application delegate”); } - (void)testViewController { STAssertNotNil([self viewController], @”Cannot find the pickerViewController “); } - (void)testNamesCount { NSArray *names = [[self pList] objectForKey:@”names”]; int num = [names count]; STAssertEquals(num, 6, @”Not all names were loaded”); } - (void)testPickerSelection { NSArray *names = [[self pList] objectForKey:@”names”]; UIPickerView *pickerView = [viewController pickerView]; [pickerView selectRow:1 inComponent:0 animated:NO]; [pickerView selectRow:2 inComponent:1 animated:NO]; NSString *name = [[names objectAtIndex:[pickerView selectedRowInComponent:0]] objectForKey:@”name”]; NSString *age = [[names objectAtIndex:[pickerView selectedRowInComponent:1]] objectForKey:@”age”]; STAssertEqualObjects(name, @”Bill”, @”Incorrect name returned from pickerView”); STAssertEqualObjects(age, @”19”, @”Incorrect age returned from pickerView”); } @end
Unit testing your application Now that all the tests have been written, you must generate the development provisioning profi le mentioned earlier.
The Bundle Identifi er you enter is the same unique id that you declared in the “Unit Test Creation Steps: A Simple Unit Test” section. Do not use the com .ezeprod.UnitTests that I used. The steps to do that are as follows:
1 .
Sign into your development account at https://developer.apple.com/iphone/: ➤➤
Choose iPhone Provisioning Portal.
➤➤
Click App IDs.
➤➤
Click New App ID.
Unit Test Creation Steps: A Simple Unit Test
2.
➤➤
Enter the description: UnitTest Application.
➤➤
Enter the Bundle Identifier: (your unique id declared earlier)
➤➤
Click Submit.
❘ 451
Choose Provisioning from the menu on the left of the web page in the iPhone provisioning portal: ➤➤
Choose New Profile.
➤➤
Enter the Profile Name: UnitTests Development Provisioning Profile.
➤➤
Click the certificate to be used. If you have not already created a developer certificate, follow the instructions at the page: https://developer.apple.com/iphone/manage/certificates/team/index.action
➤➤
Click the device that runs the test.
➤➤
Click Submit.
➤➤
You will see a status of Pending. Wait a few seconds and then refresh the page. When the status is Active, you can download the profile.
3.
From Xcode, choose Window ➪ Organizer. Select and drag the profile into the Provisioning list and release the mouse (see Figure 12-23).
4.
From Xcode, set the active target to UnitTestsTesting, targeting the device, not the simulator (see Figure 12-24) Then click Allow codesign access.
Figure 12-23
Figure 12-24
Registering your Device In order for you to run the unit test on your device, your device has to be registered with Apple. The detailed procedures can be found at the following address: https://developer.apple.com/iphone/manage/devices/index.action
452
❘ Chapter 12 Unit Testing
Unit Testing Your Application Choose UnitTests ➪ Debugger Console and click Build and Run. Figure 12-25 indicates that the four tests ran successfully.
Figure 12-25
Summary In this chapter, the parts of an iOS 4 unit test case were identified and, with the sample application, demonstrated. As unit testing becomes increasingly integrated in the application development process, it is important that the testing effort does not outweigh the entire effort to develop the application itself. Xcode has integrated the unit test framework into the application’s project so that test cases can be created with reasonable effort quickly, and the main focus can remain of the application.
13
Performance Tuning and optimization What’s in this chaPter? ➤➤
Improving your application’s performance on iOS 4
➤➤
Using the Instruments tool to improve performance
➤➤
Identifying and correcting two memory leaks
One of the most important things to remember about applications running in iOS 4 on a device is that you do not have unlimited memory or swap fi les to simulate memory as you do on a desktop computer. If your application is not efficient and does not release memory, its performance will be sluggish and eventually terminated by iOS 4. Although ostensibly your application may seem to be the only application occupying the device, many system processes are running in background — for example, the process to detect and handle a phone call. As you tune the performance of your application, you should focus on the following three issues: ➤➤
Speed of the application
➤➤
Overall responsiveness
➤➤
Battery life
Apple has provided developers with a vast array of tools that will help you tune your application. This chapter presents a brief example that demonstrates two approachs you can use to identify and improve your application’s performance: ➤➤
Xcode’s Build ➪ Build and Analyze
➤➤
Xcode’s Run ➪ Run with Performance Tool ➪ Leaks
454
❘
chaPter 13 perforMaNce tuNiNg aNd optiMizatioN
ProfilinG The process of profi ling is to use the tools, such as Instruments or to choose Build ➪ Build and Analyze from the main menu. Xcode’s Instruments tool gathers useful data about your application and then provides the results using the following: ➤➤
Trace document
➤➤
Track view
➤➤
Detail views, which consist of the following: ➤➤
Table view
➤➤
Outline view
➤➤
Diagram view
The choice of Build ➪ Build and Analyze from the main menu will build your application as expected, but also analyzes the application to identify and annotate possible memory issues.
Using the simulator While you are able to run your app using Instruments for profi ling in the Simulator, it is not suggested you do so to gather profi ling metrics. Profi ling in the Simulator can give you the fi rst pass of potential problems.
You must keep in mind that the iPhone and iPad have a limited amount of memory. The low memory warnings are not properly refl ected while running in the Simulator due to the fact that the memory source for the Simulator is the actual computer on which the Simulator is running. One of the most frequent uses of Instruments in the Simulator is checking for leaks. This process is demonstrated in the example application later in this chapter.
Using the device Profi ling on the device is a critical step in your application development, because unchecked memory leaks continually acquire more and more memory that is needed for overall operation of the device. Because the app does not release the unused memory, iOS 4 fi nally reaches a point when it determines that it can no longer supply any more memory, and will terminate the offending application. Note the following differences that exist on the device vs. the Simulator: ➤➤
ARM processor
➤➤
Limited memory
➤➤
No swapping
If you are doing time profi ling for your application’s speed, it is important to do this on the actual device, as the Simulator will not produce any reliable statistics.
Benefits of Profiling When an application gradually acquires the free memory from the device, the operating system has to make adjustments to keep the device operating. With little memory available, the operation will be sluggish; the
A Simple Memory Leak Test
❘ 455
scrolling of table views will not be smooth, for example, applications will take longer to launch, and web pages will take longer to display. This extra work also will cause the device to work harder, thus expending more power reducing the battery life between charges. The application that will be created in the next section has two memory leaks. Through the use of the Instruments tool and build option of analyzing your source code for leaks, you will analyze and identify these two leaks and learn how to correct them.
A Simple Memory Leak Test In this section you create an application that accepts the entry of a name and an age; then, with Instruments profiling the application, you tap the Add button, at least 25 times (see Figure 13-1). As you are tapping the Add button, monitor the Instruments track view trace and observe the memory leaks being recorded (see Figure 13-2). From within Instruments, choose View ➪ Extended Detail, select the Leaks trace, and double-click on the DataViewController addDataToDictionary listing to see the source of the leak (see Figure 13-3). From within Xcode, choose Build ➪ Clean All Targets, and then choose Build ➪ Build and Analyze. In the lower-right corner, next to Succeeded, you will see an icon with the number 2 (see Figure 13-4). The icon is blue onscreen. The first item is as follows: ➤➤
Potential leak of an object allocated on line 27
This line allocates a new DataItem, initializes it with the values from the two text fields, but never releases it (see Figure 13-5). The second item is as follows: ➤➤
Potential leak of an object allocated on line 33
This line allocates a new NSString str, initializes it with the count from the array of items, but never releases it (see Figure 13-6).
Figure 13-1
To fix the potential leaks that were identified on lines 27 and 33, each object (item and str) must release its memory. By adding the following you will correct the leak issue: [[self dataArray] addObject:item]; [item release];
[totalCountLabel setText:str]; [str release];
Now within Xcode, choose Build ➪ Clean All Targets, and then choose Build ➪ Build and Analyze. Now you will see only the status, Succeeded, in the lower-right corner, as shown in Figure 13-7).
456
❘ Chapter 13 Performance Tuning and Optimization
Figure 13-2
Figure 13-3
A Simple Memory Leak Test
Figure 13-4
Figure 13-5
❘ 457
458
❘ Chapter 13 Performance Tuning and Optimization
Figure 13-6
Figure 13-7
A Simple Memory Leak Test
❘ 459
The performance profiling cycle is now complete. You have identified, analyzed, and resolved two memory leaks that were adversely affecting the overall performance of your application. The steps to create the project follow in the next section.
Development Steps: A Memory Leak Test To create the application with two instances of memory leaks that will be used for profiling by the Instruments tool, execute the following steps:
1.
Start Xcode and create a Window-based application for the iPhone and name it PerformanceTests. If you need to see this step, please see Appendix A for the steps to begin a Window-based application.
2.
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it DataViewController.
3.
With the Classes group still selected, choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it DataItem.
4.
Double-click the MainWindow.xib file to launch Interface Builder (see Figure 13-8).
Figure 13-8
460
❘ Chapter 13 Performance Tuning and Optimization
5.
From the Interface Builder Library (Tools ➪ Library), select and drag the following to the View window (your interface should now look like Figure 13-9): ➤➤
Two UILabels.
➤➤
6.
1. 2.
7.
8.
Double-click the label just below the top label and type Age:.
The top text field is located to the right of the Name label. The lower text field is located to the right of the Age label.
➤➤
One UILabel in the middle, right below the text fields you just created.
➤➤
One UIButton in the middle, right below the label you just created. Double-click the button and enter Add.
Back in the Interface Builder Library, choose an NSObject (it is labeled Object), and drag it right below the First Responder icon (see Figure 13-10).
Figure 13-9
Double-click the top label and type Name:.
Two UITextFields.
1. 2.
Figure 13-10
With the NSObject selected, choose Tools ➪ Identity Inspector, and select DataViewController for the class as shown in Figure 13-11. Back in the Interface Builder Library, click Classes at the top and scroll to and select your DataViewController class. At the bottom, now choose the Outlets button. Click the + and add the
following outlet, as shown in Figure 13-12: ➤➤
ageTextField (as a UITextField instead of id type)
➤➤
nameTextField (as a UITextField instead of id type)
➤➤
totalCountLabel (as a UILabel instead of id type)
A Simple Memory Leak Test
9.
At the bottom, now choose the Actions button. Click the + and add the following outlet, as shown in Figure 13-13: ➤➤
Figure 13-11
10.
11.
❘ 461
addDataToDictionary
Figure 13-12
Figure 13-13
From the main menu of Interface Builder, choose File ➪ Write Class Files, now from the two pop-up windows that appear, select Save from the first pop-up and then Merge from the second pop-up. A window now appears with your new additions to DataViewController.m on the left and the original template on the right (see Figure 13-14). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge and close the window.
A window will appear with your new additions to DataViewController.h on the left and the original template on the right (see Figure 13-15). ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Finally, choose File ➪ Save Merge and close the window.
462
❘ Chapter 13 Performance Tuning and Optimization
Figure 13-14
Figure 13-15
A Simple Memory Leak Test
❘ 463
You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to:
12. 13.
➤➤
Identify the UITextField as ageTextField.
➤➤
Identify the UITextField as nameTextField.
➤➤
Identify the UILabel as totalCountLabel.
To make the connection to identify the UITextField as ageTextField, control-click on the Data View Controller icon to bring up the Inspector as shown in Figure 13-16. From the right of the Data View Controller Inspector, control-drag from the circle next to the ageTextField to the UITextField ageTextField until it is highlighted, then release the
mouse. The circle will be filled, indicating that the connection has been made (see Figure 13-17). Dismiss the File’s Owner Inspector. 14. 15.
Repeat step 14 for the nameTextField to the UITextField nameTextField, and the totalCountLabel to the UILabel totalCountLabel (see Figure 13-18). Control-drag from the Add button to the Data View Controller icon and select the addDataTo Dictionary action (see Figure 13-19). Select File ➪ Save to save the files.
Figure 13-16
Figure 13-17
Figure 13-18
Figure 13-19
464
❘ Chapter 13 Performance Tuning and Optimization
16.
Control-drag from the nameTextField to the Data View Controller icon, select the delegate outlet, and repeat for the ageTextField to the Data View Controller icon, selecting the delegate outlet. Select File ➪ Save to save the files.
Now it is time to enter your logic.
Source Code Listings for a Memory Leak Test For this application, the PerformanceTestsAppDelegate.h and PerformanceTestsAppDelegate.m files are not modified and are used as generated.
DataViewController.h Modifications to the Template You declared three outlets in Interface Builder: totalCountLabel, nameTextField, and ageTextField. You must now define the properties for these variables in order to get and set their values (see Listing 13-1). The IBOutlet was moved to the property declaration. There is also an instance variable dataArray of type NSArray. Listing 13-1: The complete DataViewController.h file (Chapter13/PerformanceTests/Classes/
DataViewController.h)
#import
@interface DataViewController : NSObject { UITextField *ageTextField; UITextField *nameTextField; UILabel *totalCountLabel; NSMutableArray *dataArray; } @property (nonatomic, retain) IBOutlet UITextField *ageTextField; @property (nonatomic, retain) IBOutlet UITextField *nameTextField; @property (nonatomic, retain) IBOutlet UILabel *totalCountLabel; @property (nonatomic, retain) NSMutableArray *dataArray; - (IBAction)addDataToDictionary:(id)sender; @end
DataViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the DataViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 13-2). Listing 13-2: Addition of @synthesize @implementation DataViewController @synthesize @synthesize @synthesize @synthesize
ageTextField; nameTextField; totalCountLabel; dataArray;
A Simple Memory Leak Test
❘ 465
When the DataViewController initializes, the dataArray that will hold the names and ages is initialized and stored in the object (see Listing 13-3). Listing 13-3: The init method #pragma mark #pragma mark Initialization - init { [self setDataArray:[NSMutableArray array]]; return self; }
When the user taps the Add button, the values are taken from the text fields and stored in a dictionary and added to the array, as shown in Listing 13-4. Listing 13-4: The addDataToDictionary Data Source methods #pragma mark #pragma mark array modifier - (IBAction)addDataToDictionary:(id)sender { NSString *str = nil; DataItem *item = [[DataItem alloc] init]; [item setAge:[ageTextField text]]; [item setName:[nameTextField text]]; [[self dataArray] addObject:item]; [item release]; str = [[NSString alloc] initWithFormat:@”Current count = %d”, [[self dataArray] count]]; [totalCountLabel setText:str]; [str release]; }
To dismiss the keyboard when the user taps Return, the delegate method textFieldShouldReturn should be implemented (see Listing 13-5). Listing 13-5: The textFieldShouldReturn: delegate method #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; }
The DataViewController.m file is now complete. Listing 13-6 shows the complete implementation. Listing 13-6: The complete DataViewController.m file (Chapter13/PerformanceTests/Classes/
DataViewController.m)
#import “DataViewController.h” #import “DataItem.h”
@implementation DataViewController
466
❘ Chapter 13 Performance Tuning and Optimization
@synthesize @synthesize @synthesize @synthesize
ageTextField; nameTextField; totalCountLabel; dataArray;
- init { [self setDataArray:[NSMutableArray array]]; return self; } - (IBAction)addDataToDictionary:(id)sender { NSString *str = nil; DataItem *item = [[DataItem alloc] init]; [item setAge:[ageTextField text]]; [item setName:[nameTextField text]]; [[self dataArray] addObject:item]; str = [[NSString alloc] initWithFormat:@”Current count = %d”, [[self dataArray] count]]; [totalCountLabel setText:str]; } #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } - (void)dealloc { [super dealloc]; [ageTextField release]; [nameTextField release]; [totalCountLabel release]; [dataArray release]; } @end
Finally, the items that are stored in the array are DataItem objects and have to be defined (see Listings 13-7 and 13-8). Listing 13-7: The complete DataItem.h file (Chapter13/PerformanceTests/Classes/DataItem.h) #import
@interface DataItem : NSObject { NSString *name; NSString *age; } @property (nonatomic, retain) NSString *name; @property (nonatomic, retain) NSString *age; @end
A Simple Memory Leak Test
❘ 467
Listing 13-8: The complete DataItem.h file (Chapter13/PerformanceTests/Classes/DataItem.h) #import “DataItem.h”
@implementation DataItem @synthesize name; @synthesize age; @end
Development Steps Continued: Using the Instruments Application Launch Instruments from within Xcode by choosing the following, targeting the Simulator instead of targeting the device to run this test: ➤➤
Run ➪ Run with Performance Tool ➪ Leaks
When the application launches, enter any name and an age. Click the Add button at least 25 times. As described in the beginning of this section, you will now enter the name and age, then proceed to tap the Save button 25 times and monitor the Instruments display. In the Instruments track view, you will see the activity over time, and any memory leaks detected.
Development Steps Continued: Using Build and Analyze from the Main Menu The DataViewController.m file is now corrected for the memory leaks discovered in the analysis. These additions are in bold. Listing 13-9 shows the complete implementation. Listing 13-9: The complete, corrected DataViewController.m file (Chapter13/
PerformanceTests/Classes/DataViewController.m) #import “DataViewController.h” #import “DataItem.h”
@implementation DataViewController @synthesize @synthesize @synthesize @synthesize
ageTextField; nameTextField; totalCountLabel; dataArray;
- init { [self setDataArray:[NSMutableArray array]]; return self; } - (IBAction)addDataToDictionary:(id)sender { NSString *str = nil; DataItem *item = [[DataItem alloc] init]; [item setAge:[ageTextField text]]; [item setName:[nameTextField text]]; [[self dataArray] addObject:item]; [item release]; str = [[NSString alloc] initWithFormat:@”Current count = %d”,
468
❘ Chapter 13 Performance Tuning and Optimization
[[self dataArray] count]]; [totalCountLabel setText:str]; [str release]; } #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } - (void)dealloc { [super dealloc]; [ageTextField release]; [nameTextField release]; [totalCountLabel release]; [dataArray release]; } @end
Summary In this chapter, the importance of tuning your application was illustrated with an application that had two memory leaks. Two techniques were used to identify the leaks: ➤➤
Running the application with Instruments profiling the session
➤➤
Choosing Build and Analyze from within Xcode
Memory leaks are one of the most common, but avoidable, sources of poor performance in the iOS 4 environment. This chapter has demonstrated two techniques that are to be used to identify memory leaks and in the Build and Analyze option, suggestions that aid the developer to correct them.
14
integrating iads What’s in this chaPter? ➤➤
What the iAd Network offers
➤➤
Steps to join the iAd Network
➤➤
How to incorporate iAds into your application
When Apple introduced the iTunes App Store for purchasing music, television shows, and movies, it set the standard for others to follow. The entire shopping experience is a relatively easy process. Now, with the introduction of iOS 4, Apple has once again streamlined another process: implementing advertising within your own applications. Using the iAd Network, it takes only a few short steps to provide a space for banner ads in your application. In addition, you inherit a complete marketing network that supplies ads, as well as a robust infrastructure that can create an additional source of revenue for you. The iAd Network web page is located at https://developer.apple.com/iphone/appstore/iad/. After you join and integrate iAds into your application, you can follow your revenue stream from https://iad.apple.com/itcportal/. In this chapter, you will build an application that connects to the iAd Network and displays the result of the connection by displaying a test banner view and the status of the network connection.
You can test this application without joining the iAd Network, but you must be a registered member of the iPhone Developer Program and have joined the iAd Network for ads to operate properly in your application submitted to the iTunes App Store.
joininG the iad netWork Joining the iAd program is a three-step process:
1 . 2 . 3 .
Setting up banking information Enabling your applications to link into the iAd Network Configuring your preferences to indicate what ads you will receive
470
❘
chaPter 14 iNtegratiNg iads
setting Up Banking information If you already have a paid application in the iTunes App Store, then your tax and banking information are already set up; if not, you have to enter this information. In addition to the fi nancial information, you must agree to the Developer Advertising Services Agreement.
enabling your application for iads After setting up the banking information and confi rming the agreement, you must enable the iAd Network option for your application in the Manage Your Applications section of iTunes Connect.
configuring your iad Preferences Finally, with the fi rst two steps complete, you can set preferences to exclude ads from competitors or advertisers through the use of keywords or URLs.
PreParinG yoUr aPPlication to Use the iad netWork Preparing your application to incorporate the iAd network is a three-step process:
1 . 2 . 3 .
Implementing the ADBannerView Including and implementing the iAd framework Testing your application for proper iAd implementation
implementing the adBannerview You have to dedicate a section within your application that will contain the ADBannerView class. Apple suggests either the top or bottom of the device’s view. The AdBannerView class is a dedicated class that knows how to communicate with the Apple iAd network and accept the ads that are sent to it. You do not have to write any of that communication code. All you have to do is handle the display of the banner view.
Through implementation of the banner view delegates, you must handle the bannerView:didFailToReceiveAdWithError method, ensuring that you do not display an empty banner view or banner without any advertisement in it. If you do not handle this message, the App Review Team will not accept your application for inclusion into the iTunes App Store.
integrating the iad framework The iAd framework has to be added to the project. In the Groups & Files section of Xcode, click on the Frameworks and control-click and choose Add ➪ Existing Frameworks. Select iAd.framework and click Add.
A Simple Application Using iAds
❘ 471
Now with the iAd.framework in your iOS 4 project, you need to implement in your application the following iAd delegate methods, which process the messages from the iAd Network: ➤➤
bannerViewActionShouldBegin:willLeaveApplication:
➤➤
bannerViewDidLoadAd:
➤➤
bannerView:didFailToReceiveAdWithError:
A Simple Application Using iAds It is now time to create your application that will integrate with the iAd Network. When this application launches, it checks the version of the device’s operating system; if it is iOS 4 or later, it attempts to initialize the iAd network and display an ad in an ADBannerView. The status of the attempt to connect to the iAd network is displayed in a UITextView. During development, actual commercial ads will not be loaded; however, upon successful connection to the iAd network, a test ad banner is displayed, as shown in Figure 14-1. Tapping the test ad banner will display a test advertisement, as shown in Figure 14-2. If no ads are available, an error is returned from the iAd Network. When an error is encountered for any reason, Apple requires that an empty ad banner is not displayed. If your application displays an empty ad banner, the Apple review team will reject your application (see Figure 14-3).
Figure 14-1
Figure 14-2
Figure 14-3
472
❘
chaPter 14 iNtegratiNg iads
If your application displays an empty banner when an error is encountered from the iAd Network, the Apple App Review Team will reject your application during the review process. The steps for including the iAd Network in your applications are found in the next section.
development steps: a simple application Using iads To create this application that integrates the iAd Network into your application, execute the following steps:
1 .
Start Xcode and create a View-based application for the iPhone and name it iAdApp. If you need to see this step, please refer to Appendix A for the steps to begin a View-based application.
2 .
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File, choose Cocoa Touch Class and select the UIViewController subclass, and make sure “With XIB for user interface” is checked. Name this new class AdViewController.
3 .
Double-click the AdViewController.xib file to launch Interface Builder (see Figure 14-4).
fiGUre 14-4
A Simple Application Using iAds
4.
5. 6.
7. 8. 9. 10. 11.
From the Interface Builder main menu, do the following: ➤➤
Select Tools ➪ Attributes Inspector, and click the View window and set the Status Bar option to Unspecified.
➤➤
Select Tools ➪ Size Inspector, and set the height to 50.
➤➤
Select Tools ➪ Identity Inspector, and set the class to ADBannerView (see Figure 14-5).
Choose File ➪ Save. From the Xcode project’s Groups & Files window in the NIB Files group, double-click the iAdAppViewController.xib file to bring up the application’s main view in Interface Builder.
Select the main view and set the Background option to Group Table View Background Color.
From the main menu, select Tools ➪ Library, and choose the Objects tab.
12.
13.
❘ 473
Select a UIView and drag it over the main view, and release the mouse. Select Tools ➪ Size Inspector and set X:0, Y:0, W:320, H:50. Select a UITextView and drag it just under the previous view and release the mouse. Double-click on the sample text in the UITextView to select it all and press the Delete key on your keyboard to delete it. Select Tools ➪ Size Inspector, and set X:20, Y:137, W:280, H:250. Select a UILabel and drag it just above the UITextView you just added, set the text color to Dark Gray Color, and double-click it and enter Ad Banner Status. Your main view should look like Figure 14-6.
Figure 14-5
Figure 14-6
474
❘ Chapter 14 Integrating iAds
14.
15.
Select Tools ➪ Library, choose Classes at the top, and scroll to and select your iAdAppViewController class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure 14-7: ➤➤
adView (as a UIView instead of id type)
➤➤
textView (as a UITextView instead of id type)
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then Merge from the second pop-up. A window will appear with your new additions to iAdAppViewController.h on the left and the original template on the right (see Figure 14-8): ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
There aren’t any changes to the file, so just close it. You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to identify the UIView as adView and to identify the UITextView as textView. 16.
To make the connection to identify the UIView as adView, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 14-9.
Figure 14-8
Figure 14-7
A Simple Application Using iAds
17.
❘ 475
From the right of the File’s Owner Inspector, control-drag from the circle next to the adView to the UIView adView until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the textView to UITextView (see Figure 14-10). Select File ➪ Save to save the file.
18.
In Xcode’s Groups & Files window, control-click on the Resources group and choose Add ➪ Existing Frameworks, select the iAd.framework, and click Add.
Figure 14-9
Figure 14-10
You have completed all the user interface additions and connections that are needed for this application. Now it is time to enter your logic.
Source Code Listings for A Simple Application Using iAds For this application, the iAdAppAppDelegate.h and iAdAppAppDelegate.m files are not modified and are used as generated.
iAdAppViewController.h Modifications to the Template You declared two outlets in Interface Builder: adView and textView. You must now define the properties for these variables in order to get and set their values (see Listing 14-1). The IBOutlet was moved to the property declaration. A BOOL bannerVisible was declared to identify when the banner view is being displayed. Because you are processing AdBannerView delegate messages, you must declare the ADBannerViewDelegate protocol. To use the iAd framework, you also have to import the iAd/iAd.h header file. Listing 14-1 shows the complete iAdAppViewController.h file. Listing 14-1: The complete iAdAppViewController.h file (Chapter14/iAdApp/Classes/ iAdAppViewController.h) #import #import
@interface iAdAppViewController : UIViewController { UIView *adView; UITextView *textView; BOOL bannerVisible; } @property (nonatomic, retain) IBOutlet UIView *adView; @property (nonatomic, retain) IBOutlet UITextView *textView; @property (nonatomic, assign, getter=isBannerVisible) BOOL bannerVisible; -
(void)loadAdView; (BOOL)canLoadiAd; (void)hideAdBanner:(UIView *)banner; (void)showAdBanner:(UIView *)banner;
@end
476
❘ Chapter 14 Integrating iAds
iAdAppViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the iAdAppViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 14-2). Listing 14-2: Addition of @synthesize #import “iAdAppViewController.h” #import “AdViewController.h” @implementation iAdAppViewController @synthesize adView; @synthesize textView; @synthesize bannerVisible;
When the iAdAppViewController view initializes in the viewDidLoad method, a loadAdView method is called, which then calls the canLoadiAd method to determine whether the operating system is iOS 4 or later. If it is, then the AdViewController class will load; otherwise, it will not be loaded (see Listing 14-3). Listing 14-3: The viewDidLoad, loadAdView and canLoadiAd methods - (void)viewDidLoad { [super viewDidLoad]; [self loadAdView]; } #pragma mark #pragma mark - iAd related - (void)loadAdView { if([self canLoadiAd]) { UIView *aView = (UIView *)adView; aView.frame = CGRectOffset(aView.frame, 0, -50); AdViewController *adBannerView = [[[AdViewController alloc] initWithNibName:@”AdViewController” bundle:nil] autorelease]; id bView = [adBannerView view]; [bView setDelegate:self]; [adView addSubview:bView]; } } - (BOOL)canLoadiAd { NSArray *array = [[[UIDevice currentDevice] systemVersion] componentsSeparatedByString:@”.” ]; NSString *majorVersion = [array objectAtIndex:0]; int ver = [majorVersion intValue]; if(ver < 4) { return NO; } else { return YES; } }
A Simple Application Using iAds
❘ 477
Remember that if any error occurs with the iAd Network, the hideAdBanner method must be called to ensure that the banner will not be displayed. However, if everything is working properly, the method showAdBanner displays the banner view. Listing 14-4 shows these two methods. Listing 14-4: The hideAdBanner and showAdBanner methods #pragma mark #pragma mark iAd Banner display methods - (void)hideAdBanner:(UIView *)banner { [UIView beginAnimations:@”animateBanner” context:NULL]; // assumes the banner view is at the top of the screen. banner.frame = CGRectOffset(banner.frame, 0, -50); [UIView commitAnimations]; [self setBannerVisible:NO]; } - (void)showAdBanner:(UIView *)banner { [UIView beginAnimations:@”animateBanner” context:NULL]; // assumes the banner view is at the top of the screen. banner.frame = CGRectOffset(banner.frame, 0, 50); [UIView commitAnimations]; [self setBannerVisible:YES]; }
The iAdAppViewController.m class is now complete. Listing 14-5 shows the complete implementation. Listing 14-5: The complete iAdAppViewController.m file (Chapter14/iAdApp/Classes/ iAdAppViewController.m) #import “iAdAppViewController.h” #import “AdViewController.h” @implementation iAdAppViewController @synthesize adView; @synthesize textView; @synthesize bannerVisible;
#pragma mark #pragma mark View Initialization - (void)viewDidLoad { [super viewDidLoad]; [self loadAdView]; } #pragma mark #pragma mark - iAd related - (void)loadAdView { if([self canLoadiAd]) { UIView *aView = (UIView *)adView; aView.frame = CGRectOffset(aView.frame, 0, -50); AdViewController *adBannerView = [[[AdViewController alloc] initWithNibName:@”AdViewController” bundle:nil] autorelease];
478
❘ Chapter 14 Integrating iAds
id bView = [adBannerView view]; [bView setDelegate:self]; [adView addSubview:bView]; } } - (BOOL)canLoadiAd { NSArray *array = [[[UIDevice currentDevice] systemVersion] componentsSeparatedByString:@”.” ]; NSString *majorVersion = [array objectAtIndex:0]; int ver = [majorVersion intValue]; if(ver < 4) { return NO; } else { return YES; } } #pragma mark #pragma mark iAd Banner display methods - (void)hideAdBanner:(UIView *)banner { [UIView beginAnimations:@”animateBanner” context:NULL]; // assumes the banner view is at the top of the screen. banner.frame = CGRectOffset(banner.frame, 0, -50); [UIView commitAnimations]; [self setBannerVisible:NO]; } - (void)showAdBanner:(UIView *)banner { [UIView beginAnimations:@”animateBanner” context:NULL]; // assumes the banner view is at the top of the screen. banner.frame = CGRectOffset(banner.frame, 0, 50); [UIView commitAnimations]; [self setBannerVisible:YES]; } #pragma mark #pragma mark - iAd delegate methods - (BOOL)bannerViewActionShouldBegin:(ADBannerView *)banner willLeaveApplication:(BOOL)willLeave { BOOL shouldExecuteAction = [self canLoadiAd]; if (!willLeave && shouldExecuteAction) { // insert code here to suspend any services that might conflict // with the advertisement [[self textView] setText:@”Banner view is beginning an ad action”]; } return shouldExecuteAction; } - (void)bannerViewDidLoadAd:(ADBannerView *)banner { if (![self isBannerVisible]) { [self showAdBanner:[self adView]]; [self setBannerVisible:YES]; [[self textView] setText:@”Ad banner did load ad”]; } } - (void)bannerView:(ADBannerView *)banner
Summary
❘ 479
didFailToReceiveAdWithError:(NSError *)error { if ([self isBannerVisible]) { [self hideAdBanner:[self adView]]; [self setBannerVisible:NO]; } [[self textView] setText:[NSString stringWithFormat:@”Ad banner did not load = Error: %@”, [error localizedDescription]]]; } #pragma mark #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setAdView:nil]; [self setTextView:nil]; } - (void)dealloc { [adView release]; [textView release]; [super dealloc]; } @end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the section “A Simple Application Using iAds.” One thing you may experience is a delay of the test ad coming up within the simulator. This is normal, as there are a number of connection procedures that have to be completed internally with the simulator and network. You may also receive a message that there are no ads available. At the time of this writing, this is also normal, as the iAd network is in its early stages as are the number of available ads.
Summary Apple’s iAd Network provides an easy way to incorporate advertisements into your application, offering both a sophisticated framework and uncomplicated implementation. If you are a member of the iPhone Developer Program, have supplied your financial information, and agree to the terms of the iAd Network, you can integrate advertising into your application with relative ease, thereby creating a new source of income.
15
Multitasking What’s in this chaPter? ➤➤
How to set up an AVAudioSession for background operation
➤➤
How to modify your Xcode project to enable background audio processing
➤➤
Enabling remote events when the application is running in the background
Multitasking is a major feature of iOS 4. It not only adds the capabilities of newly developed applications, it enhances the operation of existing applications by enabling them to remain idle in the background, rather than terminate. This enhances the user experience, as the application’s launch time and previous state are automatically preserved. It does not mean that all applications are running all the time. In pre-iOS 4, an application would launch, perform its tasks, and then terminate, ultimately left in a nonrunning state. With iOS 4, an additional background state enables an application to be suspended until it is called upon to run again. Of course, when a multitasking application is in the background, it should consume minimal resources, which are needed by other active applications. Therefore, system resources should always be considered as you develop your applications. This chapter demonstrates how to write an audio application that will run in background, where it can still be controlled remotely.
mUltitaskinG services Apple’s iOS 4 multitasking features include the following: ➤➤
Fast app switching
➤➤
Push and Local notifications
➤➤
Background audio
➤➤
Location tracking
➤➤
Voice over IP
The fast app switching is provided by default, but the audio, location, and VoIP capabilities require additional settings in the Required background modes option, which are located in the application’s
482
❘ Chapter 15 Multitasking
Info.plist file. The setting for the Required background modes option in this application is App plays audio,
because audio is going to be running in the background.
UIApplication Delegate Messages Multitasking not only introduces new features, it also introduces new delegate messages in addition to the existing messages, identifying the following new control states. For application launching: ➤➤
applicationDidBecomeActive — Notification that your application has moved from the inactive to
active state. Use this method to restart any tasks that were paused while the application was inactive. ➤➤
applicationWillResignActive — Notification that your application is about to move from the active to inactive state. Use this method to pause ongoing tasks, disable timers and do as minimal amount of work as possible.
➤➤
application:didFinishLaunchingWithOptions — Notification that your application has launched and is about to move from the inactive state to active state. Use this method to initialize your application and prepare it for running.
➤➤
applicationWillTerminate — Notification that your application is about to be terminated and removed from memory. Use this method to perform any final clean-up tasks for your application.
For application switching: ➤➤
applicationDidEnterBackground — Notification that the application is now in the background. This method is called instead of the applicationWillTerminate: in iPhone OS 4.0 and later.
➤➤
applicationWillEnterForeground — Notification that the application is about to enter the foreground. This method is called when the application transitions from the background to the inactive state and is followed by an applicationDidBecomeActive notification.
Multitasking Responsibilities When your application recedes into the background, certain functional operations should cease to operate, as they are not being used, yet they still consume system resources. For example, when network listening, GPU, and shared data access are in the background, they should be suspended until the application reenters the foreground. Because memory is always an issue on small devices, it is suggested that you implement the applicationDidEnterBackground method to save user data and application state information. When this method is called, it is time to flush the cache, release any retained images, and suspend any user interface activity not needed in background processing. You should suspend all location services, remembering to close listening sockets beforehand; they can be reopened upon awakening. In general, it is recommended that you handle the following when your application enters the background: ➤➤
Save the application state.
➤➤
Flush caches.
➤➤
Suspend UI activity.
➤➤
Close any service sockets.
While task completion, uploading photos, or downloading content is supported by iOS 4, the execution time allowed is limited to avoid excessive battery drain. The typical block to perform a background task is as follows: bgTask = [app beginBackgroundTaskWithExpirationHandler:... ] [your active methods]; [app endBackgroundTask:self.bgTask];
An Application That Multitasks Audio
❘ 483
You should not design your applications to do heavy work in the background, as execution time is closely monitored. If iOS 4 determines that your application is consuming too many system resources while in the background, it will attempt to scale back your application; and if that doesn’t work, the application may be terminated.
Xcode Simulator Multitasking Limitations Although the Simulator is a frequently used tool that is critical to the development process, as it integrates well with Xcode, it should not be the only way you test your application. Make sure that you always test it on an iPhone, iPod touch or iPad. The iOS 4 features supported by the Simulator are fast app switching, task completion, local notifications, and multitasking that does not require iPod library access and gps location access. Services not supported by the iOS 4 Simulator include background audio, location and VoIP, GPS location changing, and push notification.
Device Support of Multitasking The iPhone 3G is an example of a device that does not support multitasking but can run iOS 4. If your application can run properly without the need for background processing, the following code should be included to check for multitasking support: UIDevice *device = [UIDevice currentDevice]; BOOL bgSupport = NO; if ([device respondsToSelector:@selector(isMultitaskingSupported)]) { bgSupport = device.multitaskingSupported; }
The following application demonstrates how to play audio in the background while enabling remote execution through the generic remote media player. The application also has an iPodLibrary object that gets audio from the iPod music library.
An Application That Multitasks Audio When this application launches, a Table view appears showing the status of loading the iPod music library, as shown in Figure 15-1. When the loading of the song information is complete, all the songs in the iPod library are displayed, as shown in Figure 15-2.
Figure 15-1
Figure 15-2
484
❘
chaPter 15 MultitaskiNg
When the user taps a song title, the modal view appears with the title of the song selected displayed as well as that song playing, as shown in Figure 15-3. The user can tap Done to dismiss the modal view, and can also tap the device’s home key to exit the app, and the music will continue playing (see Figure 15-4). Double-tapping the home button and swiping to the right will display the Music Player, and the icon indicates the source of the music (see Figure 15-5).
fiGUre 15-3
fiGUre 15-4
fiGUre 15-5
Now it is time to create your own multitasking iPod music player.
Access to the iPod music library is not available through the Simulator. This application has to be run on a real device. This means you have to be a registered member of the iPhone Developer Program and have the required certificates and provisioning profiles to install on devices. See Appendix D for the iTunes Connect Developer Guide.
development steps: an application that multitasks audio To create this music player application, execute the following steps:
1 .
Start Xcode and create a Navigation-based application for the iPad and name it MusicPlayer. If you need to see this step, please see Appendix A for the steps to begin a Navigation-based application.
2 .
In the Groups & Files section of Xcode, click the Classes group. Choose File ➪ New File, choose Cocoa Touch Class, and select UIViewController subclass class. Ensure that only “With XIB for user interface” is checked and name the file MusicPlayerViewController. This displays the list of songs that you selected from the iPod music library.
An Application That Multitasks Audio
3. 4.
❘ 485
Choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it iPodLibrary.
Double-click the MusicPlayerViewController.xib file in the group NIB Files to launch Interface Builder (see Figure 15-6).
Figure 15-6
5.
Select Tools ➪ Attributes Inspector, select the main view, and set the Background option to Group Table View Background.
6. 7.
From the main menu, select Tools ➪ Library, and choose the Objects tab.
8.
Select a UINavigationBar and drag it over the main view, place it at the bottom of the view, and release the mouse. Select a UIBarButtonItem and drag it over the navigation bar, place it at the left of the bar, and release the mouse. Select Pause for the Identifier.
486
❘ Chapter 15 Multitasking
9.
10.
11.
Select a UIBarButtonItem and drag it over the navigation bar, place it at the left of the bar, and release the mouse. Select Done for the Identifier. Select a UILabel and drag it over the main view, place it in the middle of the view, and release the mouse. Stretch it out so W:280. Your completed interface should look like Figure 15-7. Select Tools ➪ Library, choose Classes at the top, and scroll to and select your MusicPlayerView Controller class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure 15-8:
12.
➤➤
navigationBar (as a UINavigationBar instead of id type)
➤➤
titleLabel (as a UILabel instead of id type)
At the bottom, now choose the Actions button. Click the + and add the following actions, as shown in Figure 15-9: ➤➤
changePlaybackState
➤➤
done
Figure 15-7
Figure 15-8
Figure 15-9
An Application That Multitasks Audio
13.
❘ 487
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first popup, and then select Merge from the second pop-up. A window will appear with your new additions to MusicPlayerViewController.h on the left and the original template on the right (see Figure 15-10): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
Figure 15-10
14.
The next file that merges your new additions is MusicPlayerViewController.m, which displays the additions on the left and the original template on the right (see Figure 15-11): ➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
From the main menu, select Find ➪ Go to Next ➪ Difference.
➤➤
In the lower-right corner, choose Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
488
❘ Chapter 15 Multitasking
Figure 15-11
You now have an Objective-C template that will hold your application’s logic. Now you have to make all the connections to: ➤➤
Identify the UINavigationBar as navigationBar.
➤➤
Identify the UILabel as titleLabel.
15.
To make the connection to identify the UINavigationBar as navigationBar, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure 15-12.
16.
From the right of the File’s Owner Inspector, control-drag from the circle next to the navigationBar to the UINavigationBar navigationBar until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the titleLabel you declared above.
17.
From the right of the File’s Owner Inspector, control-drag from the circle next to changePlaybackState to the Pause button on the left until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for the Done button to the done action (see Figure 15-13), and then choose File ➪ Save.
An Application That Multitasks Audio
Figure 15-12
18.
❘ 489
Figure 15-13
Finally, you have to import the following frameworks into your project in order for it to successfully build: ➤➤
AVFoundation.framework
➤➤
MediaPlayer.framework
To import the frameworks, control-click the Frameworks group and select Add ➪ Existing Frameworks. Select the preceding frameworks and click Add. You have completed all the user interface additions and connections that are needed for this application. Now it is time to enter your logic.
Source Code Listings for an Application That Multitasks Audio For this application, the MusicPlayerAppDelegate.h and MusicPlayerAppDelegate.m files are not modified and are used as generated.
RootViewController.h Modifications to the Template There are several additions to the RootViewController.h file. First, you need to create the AVPlayer. Next, you need to create a process to read the iPod library titles and store the results in a NSDictionary, with the key being the title of the songs. This key will also be the value displayed on the Table view cell. After the initial view appears, the notification beginReceivingRemoteControlEvents is dispatched, indicating that this application will receive remote control events. Because loading a music library takes time, an activity indicator, in the form of a spinning wheel, is displayed; and the first Table view cell displays a message indicating that the file is busy reading data. When the entire library is read in, the music titles will populate the Table view cells. When the user taps a cell, the MusicPlayerViewController is called and the music begins playing. Listing 15-1 shows the complete RootViewController.h file. Listing 15-1: The complete RootViewController.h file (/Chapter15/MusicPlayer/Classes/ RootViewController.h) #import #import @class AVPlayer; @interface RootViewController : UITableViewController { NSDictionary *audioList;
490
❘ Chapter 15 Multitasking
AVPlayer* avPlayer; UIActivityIndicatorView *activityIndicator; NSIndexPath *selectedTrack; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
NSDictionary *audioList; AVPlayer* avPlayer; UIActivityIndicatorView *activityIndicator; NSIndexPath *selectedTrack;
-(void)initAudioSession; - (void)startIndicator; - (void)replaceTrack:(int)track; @end
RootViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the RootViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 15-2). Listing 15-2: Addition of @synthesize #import “RootViewController.h” #import “iPodLibrary.h” #import “MusicPlayerViewController.h”
@implementation RootViewController @synthesize @synthesize @synthesize @synthesize
audioList; avPlayer; activityIndicator; selectedTrack;
When the RootViewController view initializes in the viewDidLoad method, the activity indicator is started, the AVPlayer is created, and the process of loading the iPod library begins. The initialization of the AVAudioSession also begins, and for multitasking purposes the category AVAudioSessionCategoryPlayback is set (see Listing 15-3). Listing 15-3: The viewDidLoad method #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self startIndicator]; [self setAvPlayer:[[AVPlayer allocWithZone:[self zone]] init]]; [NSThread detachNewThreadSelector:@selector(loadIpodLibrary) toTarget:self withObject:nil]; } - (void)viewDidAppear:(BOOL)animated {
An Application That Multitasks Audio
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; [self becomeFirstResponder]; } #pragma mark #pragma mark Audio Related methods - (void)loadIpodLibrary { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; [self initAudioSession]; [self setAudioList:[iPodLibrary musicList]]; [self performSelectorOnMainThread:@selector(iPodLibraryLoaded:) withObject:[self audioList] waitUntilDone:NO]; [pool release]; } - (void)iPodLibraryLoaded:(NSArray *)list { [[self activityIndicator] stopAnimating]; [[self tableView] reloadData]; } -(void)initAudioSession { NSError *error; AVAudioSession *audioSession = [AVAudioSession sharedInstance]; [audioSession setCategory:AVAudioSessionCategoryPlayback error:&error]; [audioSession setDelegate:self]; } #pragma mark #pragma mark Activity Status indicators methods - (void)startIndicator { UIActivityIndicatorView *myIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [myIndicator setCenter:CGPointMake(160, 175)]; [myIndicator setHidesWhenStopped:YES]; [[self view] addSubview:myIndicator]; [myIndicator startAnimating]; [self setActivityIndicator:myIndicator]; }
When the user taps a song title in the list, the current song will be replaced by the new selection, which begins playing. Listing 15-4 shows the replaceTrack method. Listing 15-4: The replaceTrack: method #pragma mark #pragma mark Audio track processing - (void)replaceTrack:(int)track { NSIndexPath *idxPath = [NSIndexPath indexPathForRow:track inSection:0]; UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:idxPath]; NSURL *aURL = [[self audioList] objectForKey:[[cell textLabel] text]]; [self setSelectedTrack:idxPath]; [[self avPlayer] replaceCurrentItemWithPlayerItem: [AVPlayerItem playerItemWithURL:aURL]]; }
❘ 491
492
❘ Chapter 15 Multitasking
Because this is just a single list of related items, that being songs, there is only one section; how ever, the number of rows depends on how many songs are contained in the library. The methods numberOfSectionsInTableView and tableView:numberOfRowsInSection determine the output of the Table view, as shown in Listing 15-5. Listing 15-5: The numberOfSectionsInTableView and tableView:numberOfRowsInSection
methods
#pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int count = [[[self audioList] allKeys] count]; if(count == 0) { count = 7; } return count; }
There are two possibilities for displaying data in the Table view. First, if the library is still being loaded, seven cells are allocated and “Loading Library…” is displayed. When the library data is returned, the Table view is repopulated with the query (see Listing 15-6). Listing 15-6: The tableView:cellForRowAtIndex: method // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self audioList] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; int songCount = [keys count]; int row = [indexPath row]; static NSString *CellIdentifier = @”Cell”; static NSString *LoadingCellIdentifier = @”LoadingCell”; if((songCount == 0) && (row == 0)) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LoadingCellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:LoadingCellIdentifier] autorelease]; [[cell detailTextLabel] setTextAlignment:UITextAlignmentCenter]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } [[cell textLabel] setText:@”Loading Library…”]; return cell; } else { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
An Application That Multitasks Audio
❘ 493
NSString *cellText = [keys objectAtIndex:row]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } if(songCount > 0) { [[cell textLabel] setText:cellText]; } return cell; } }
When the song is selected from the list, a modal view appears and the music begins playing (see Listing 15-7). Listing 15-7: The Table view delegate methods #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; NSURL *aURL = [[self audioList] objectForKey:[[cell textLabel] text]]; [self setSelectedTrack:indexPath]; MusicPlayerViewController* controller = [[MusicPlayerViewController allocWithZone:[self zone]] init]; [controller setAvPlayer:[self avPlayer]]; [controller setMusicURL:aURL]; [controller setMusicTitle:[[cell textLabel] text]]; [controller setModalTransitionStyle:UIModalTransitionStylePartialCurl]; [self presentModalViewController:controller animated:YES]; [tableView deselectRowAtIndexPath: indexPath animated: YES]; [controller release]; }
Finally, when the music is playing and the application is running in the background, the user can doubletap the home button and slide the bottom icon over, revealing an internal media player that has controls to manipulate the music outside the application itself. The method that receives the remote message is remoteControlReceivedWithEvent (see Listing 15-8). Listing 15-8: The canBecomeFirstResponder and remoteControlReceivedWithEvent methods #pragma mark #pragma mark Run in background - (BOOL)canBecomeFirstResponder { return YES; } - (void)remoteControlReceivedWithEvent:(UIEvent *)event { //NSLog(@”remoteControlReceivedWithEvent = %@”, event); switch (event.subtype) { case UIEventSubtypeRemoteControlTogglePlayPause: if([[self avPlayer] rate] == 0.0) {
494
❘ Chapter 15 Multitasking
[[self avPlayer] play]; } else { [[self avPlayer] pause]; } break; case UIEventSubtypeRemoteControlNextTrack: [self replaceTrack:[[self selectedTrack] row] + 1]; break; case UIEventSubtypeRemoteControlPreviousTrack: [self replaceTrack:[[self selectedTrack] row] - 1]; break; default: break; } }
The RootViewController.m class is now complete. Listing 15-9 shows the complete implementation. Listing 15-9: The complete RootViewController.m file (/Chapter15/MusicPlayer/Classes/ RootViewController.m) #import “RootViewController.h” #import “iPodLibrary.h” #import “MusicPlayerViewController.h”
@implementation RootViewController @synthesize @synthesize @synthesize @synthesize
audioList; avPlayer; activityIndicator; selectedTrack;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self startIndicator]; [self setAvPlayer:[[AVPlayer allocWithZone:[self zone]] init]]; [NSThread detachNewThreadSelector:@selector(loadIpodLibrary) toTarget:self withObject:nil]; } - (void)viewDidAppear:(BOOL)animated { [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; [self becomeFirstResponder]; } #pragma mark #pragma mark Audio Related methods - (void)loadIpodLibrary { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; [self initAudioSession]; [self setAudioList:[iPodLibrary musicList]]; [self performSelectorOnMainThread:@selector(iPodLibraryLoaded:) withObject:[self audioList] waitUntilDone:NO];
An Application That Multitasks Audio
[pool release]; } - (void)iPodLibraryLoaded:(NSArray *)list { [[self activityIndicator] stopAnimating]; [[self tableView] reloadData]; } -(void)initAudioSession { NSError *error; AVAudioSession *audioSession = [AVAudioSession sharedInstance]; [audioSession setCategory:AVAudioSessionCategoryPlayback error:&error]; [audioSession setDelegate:self]; } #pragma mark #pragma mark Activity Status indicators methods - (void)startIndicator { UIActivityIndicatorView *myIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [myIndicator setCenter:CGPointMake(160, 175)]; [myIndicator setHidesWhenStopped:YES]; [[self view] addSubview:myIndicator]; [myIndicator startAnimating]; [self setActivityIndicator:myIndicator]; } #pragma mark #pragma mark Audio track processing - (void)replaceTrack:(int)track { NSIndexPath *idxPath = [NSIndexPath indexPathForRow:track inSection:0]; UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:idxPath]; NSURL *aURL = [[self audioList] objectForKey:[[cell textLabel] text]]; [self setSelectedTrack:idxPath]; [[self avPlayer] replaceCurrentItemWithPlayerItem: [AVPlayerItem playerItemWithURL:aURL]]; }
#pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int count = [[[self audioList] allKeys] count]; if(count == 0) { count = 7; } return count; }
❘ 495
496
❘ Chapter 15 Multitasking
// Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self audioList] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; int songCount = [keys count]; int row = [indexPath row]; static NSString *CellIdentifier = @”Cell”; static NSString *LoadingCellIdentifier = @”LoadingCell”; if((songCount == 0) && (row == 0)) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LoadingCellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:LoadingCellIdentifier] autorelease]; [[cell detailTextLabel] setTextAlignment:UITextAlignmentCenter]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } [[cell textLabel] setText:@”Loading Library…”]; return cell; } else { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; NSString *cellText = [keys objectAtIndex:row]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } if(songCount > 0) { [[cell textLabel] setText:cellText]; } return cell; } } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; NSURL *aURL = [[self audioList] objectForKey:[[cell textLabel] text]]; [self setSelectedTrack:indexPath]; MusicPlayerViewController* controller = [[MusicPlayerViewController allocWithZone:[self zone]] init]; [controller setAvPlayer:[self avPlayer]]; [controller setMusicURL:aURL]; [controller setMusicTitle:[[cell textLabel] text]]; [controller setModalTransitionStyle:UIModalTransitionStylePartialCurl]; [self presentModalViewController:controller animated:YES]; [tableView deselectRowAtIndexPath: indexPath animated: YES]; [controller release];
An Application That Multitasks Audio
} #pragma mark #pragma mark Run in background - (BOOL)canBecomeFirstResponder { return YES; } - (void)remoteControlReceivedWithEvent:(UIEvent *)event { switch (event.subtype) { case UIEventSubtypeRemoteControlTogglePlayPause: if([[self avPlayer] rate] == 0.0) { [[self avPlayer] play]; } else { [[self avPlayer] pause]; } break; case UIEventSubtypeRemoteControlNextTrack: [self replaceTrack:[[self selectedTrack] row] + 1]; break; case UIEventSubtypeRemoteControlPreviousTrack: [self replaceTrack:[[self selectedTrack] row] - 1]; break; default: break; } } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setAvPlayer:nil]; [self setAudioList:nil]; } - (void)dealloc { [audioList release]; [avPlayer release]; [[UIApplication sharedApplication] endReceivingRemoteControlEvents]; [super dealloc]; } @end
MusicPlayerViewController.h Modifications to the Template You declared two outlets in Interface Builder: navigatorBar and titleLabel. You must now define the properties for these variables in order to get and set their values. The IBOutlet was moved to the property declaration.
❘ 497
498
❘ Chapter 15 Multitasking
Two buttons were added to indicate the play or pause state of the music. The class also has an instance of AVPlayer that controls the play or pause action of the music. There are also two actions: changePlaybackState for the play or pause state of the music, and done for the Done button. Listing 15-10 shows the complete MusicPlayerViewController.h file. Listing 15-10: The complete MusicPlayerViewController.h file (/Chapter8/iPodLibraryPlayer/
Classes/MusicPlayerViewController.h) #import @class AVPlayer;
@interface MusicPlayerViewController : UIViewController { UINavigationBar *navigationBar; UILabel *titleLabel; UIBarButtonItem *pauseButton; UIBarButtonItem *playButton; NSURL *musicURL; NSString *musicTitle; AVPlayer* avPlayer; } @property (nonatomic, retain) IBOutlet UINavigationBar *navigationBar; @property (nonatomic, retain) IBOutlet UILabel *titleLabel; @property (nonatomic, retain) NSURL *musicURL; @property (nonatomic, retain) NSString *musicTitle; @property (nonatomic, retain) AVPlayer* avPlayer; @property (nonatomic, retain) UIBarButtonItem *pauseButton; @property (nonatomic, retain) UIBarButtonItem *playButton; - (void)createPlayPauseButtons; - (IBAction)changePlaybackState:(id)sender; - (IBAction)done:(id)sender; @end
MusicPlayerViewController.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the MusicPlayerViewController.m template. For each of the view properties that were declared, you must match them with @synthesize (see Listing 15-11). Listing 15-11: Addition of @synthesize #import “MusicPlayerViewController.h” #import
@implementation MusicPlayerViewController @synthesize musicURL; @synthesize musicTitle;
An Application That Multitasks Audio
@synthesize @synthesize @synthesize @synthesize @synthesize
❘ 499
avPlayer; pauseButton; playButton; navigationBar; titleLabel;
When the MusicPlayerViewController view initializes, it receives the newly selected song and replaces the current song with this new one, and creates the play-pause and Done buttons (see Listing 15-12). Listing 15-12: The viewDidLoad. viewWillAppear and createPlayPauseButtons methods #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [[self avPlayer] replaceCurrentItemWithPlayerItem: [AVPlayerItem playerItemWithURL:[self musicURL]]]; [self createPlayPauseButtons]; [self changePlaybackState:playButton]; [[navigationBar topItem] setTitle:@”Music Player”]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [titleLabel setText:[self musicTitle]]; } - (void)createPlayPauseButtons { UIBarButtonItem *play = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:@selector(changePlaybackState:)]; UIBarButtonItem *pause = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause target:self action:@selector(changePlaybackState:)]; [self setPlayButton:play]; [self setPauseButton:pause]; [play release]; [pause release]; }
Two actions are possible from this view. Users can choose to pause and then resume the playing of music or they can choose Done to dismiss the player page and return to the list of songs (see Listing 15-13). Listing 15-13: The button action handler methods #pragma mark #pragma mark Button Action handlers - (IBAction)changePlaybackState:(id)sender { if(sender == pauseButton) { [avPlayer pause]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; } else { [avPlayer play]; [[navigationBar topItem] setLeftBarButtonItem:pauseButton];
500
❘ Chapter 15 Multitasking
} } - (IBAction)done:(id)sender { [[self parentViewController] dismissModalViewControllerAnimated:YES]; }
The MusicPlayerViewController.m class is now complete. Listing 15-14 shows the complete implementation. Listing 15-14: The complete MusicPlayerViewController.m file (/Chapter8/iPodLibraryPlayer/ Classes/MusicPlayerViewController.m) #import “MusicPlayerViewController.h” #import
@implementation MusicPlayerViewController @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize @synthesize
musicURL; musicTitle; avPlayer; pauseButton; playButton; navigationBar; titleLabel;
#pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [[self avPlayer] replaceCurrentItemWithPlayerItem: [AVPlayerItem playerItemWithURL:[self musicURL]]]; [self createPlayPauseButtons]; [self changePlaybackState:playButton]; [[navigationBar topItem] setTitle:@”Music Player”]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [titleLabel setText:[self musicTitle]]; } - (void)createPlayPauseButtons { UIBarButtonItem *play = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:@selector(changePlaybackState:)]; UIBarButtonItem *pause = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause target:self action:@selector(changePlaybackState:)]; [self setPlayButton:play]; [self setPauseButton:pause]; [play release]; [pause release];
An Application That Multitasks Audio
} #pragma mark #pragma mark Button Action handlers - (IBAction)changePlaybackState:(id)sender { if(sender == pauseButton) { [avPlayer pause]; [[navigationBar topItem] setLeftBarButtonItem:playButton]; } else { [avPlayer play]; [[navigationBar topItem] setLeftBarButtonItem:pauseButton]; } } - (IBAction)done:(id)sender { [[self parentViewController] dismissModalViewControllerAnimated:YES]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [super viewDidUnload]; [self setMusicURL:nil]; [self setMusicTitle:nil]; [self setAvPlayer:nil]; [self setPauseButton:nil]; [self setPlayButton:nil]; [self setNavigationBar:nil]; [self setTitleLabel:nil]; } - (void)dealloc { [musicURL release]; [musicTitle release]; [avPlayer release]; [pauseButton release]; [playButton release]; [navigationBar release]; [titleLabel release]; [super dealloc]; }
@end
iPodLibrary.h Modifications to the Template The iPodLibrary class is the interface to the iPod music library; it contains two factory methods — one that returns the movie list and one that returns the title of the movie for a specific URL. Listing 15-15 shows the complete iPodLibrary.h file.
❘ 501
502
❘ Chapter 15 Multitasking
Listing 15-15: The complete iPodLibrary.h file (/Chapter15/MusicPlayer/Classes/iPodLibrary.h) #import
@interface iPodLibrary : NSObject { } + (NSMutableDictionary *)musicList; @end
iPodLibrary.m Modifications to the Template Now that the header file has been updated to define the additions to the template, it is time to modify the iPodLibrary.m template. In order to get the list of music that is in the iPod music library, you have to create a MPMediaQuery. To have only music returned, you have to set a query filter of the type MPMediaTypeMusic. What is returned is a list of MPMediaItems. The two items that you will be storing in your NSMutableDictionary musicList are the MPMediaItemPropertyTitle, the song title, and the MPMediaItemPropertyAssetURL, the location of the song. Listing 15-16 shows the complete iPodLibrary.m file. Listing 15-16: The complete iPodLibrary.m file (/Chapter15/MusicPlayer/Classes/iPodLibrary.m) #import “iPodLibrary.h” #import <MediaPlayer/MediaPlayer.h> #import
@implementation iPodLibrary + (NSMutableDictionary *)musicList { NSMutableDictionary *musicList = [NSMutableDictionary dictionary]; MPMediaQuery *query = [[MPMediaQuery alloc] init]; [query addFilterPredicate:[MPMediaPropertyPredicate predicateWithValue:[NSNumber numberWithInteger:(MPMediaTypeMusic)] forProperty:MPMediaItemPropertyMediaType]]; for (MPMediaItem* item in [query items]) { [musicList setObject:[item valueForProperty:MPMediaItemPropertyAssetURL] forKey:[item valueForProperty:MPMediaItemPropertyTitle]]; } [query release]; return musicList; }
@end
Now that you have everything completed, there is one more step to enable audio to be played in the background while you tend to other tasks. In Xcode’s Groups & Files section, click MusicPlayer-Info.plist.
Summary
❘ 503
Click the last entry in the list and on the right of the highlighted line, click the plus tab and select Required background modes. On the triangle, click to reveal Item 0; and in the Value column, choose App plays audio (see Figure 15-14). Choose File ➪ Save.
Figure 15-14
Test Your Application To test your application, choose the device from the Xcode panel, make sure your device is connected, and click Run and Build to try out your app. Remember that the iPod music library is not accessible from within the Simulator. It should give you the results described at the beginning of the section “An Application That Multitasks Audio.”
Summary This chapter described an important feature of iOS 4: enabling background processing of your own applications. Currently, this capability is available only with iOS 4. The process that was demonstrated for audio involved two critical steps: first, initializing an AVAudio Session and setting the category of AVAudioSessionCategoryPlayback, and second, including Required background modes as App plays audio in the MusicPlayer-Info.plist. Finally, the application hooked into the remote control feature, enabling the application to play in the background yet be controlled from another control set.
A
Your initial app — first steps What’s in this aPPendiX ➤➤
How to create the initial framework for your application development
➤➤
Identifying the user interface that will accompany each application type
While the applications you can create for the iPhone, iPod touch, and iPad are virtually unlimited, they all have similar origins. This appendix serves as a resource to illustrate the initial steps you should follow as you begin your application development.
Xcode Project BUilder To begin your application development, start the Xcode application. It is located at /Developer/ Applications/Xcode.app (see Figure A-1).
fiGUre a-1
506
❘ Appendix A Your Initial App — First Steps
Xcode is the complete developer toolset for creating Mac OS X and iPhone OS applications. This package installs the Xcode IDE, performance analysis tools, iPhone Simulator, and OS framework bundles in the form of Mac SDKs and iPhone SDKs.
Available Application Types After Xcode launches, you can choose from the three options shown in Figure A-2: ➤➤
Create a new Xcode project
➤➤
Getting started with Xcode
➤➤
Apple Developer Connection
Choosing Create a new Xcode project will display the New Project window shown in Figure A-3. From the New Project window, the application types described in Table A-1 are available. Table A-1: New Application Project Types Project Type
Device
Description
Navigation-based
iPhone, iPod touch
This template provides a starting point for an application that uses a navigation controller. It provides a user interface configured with a navigation controller to display a list of items.
OpenGL ES
iPhone, iPod touch, iPad
This template provides a starting point for an application that uses an OpenGL ES-based view. It provides a view into which you render your OpenGL ES scene, and a timer that enables you to animate the view.
Split View–based
iPad
This template provides a starting point for an application that uses a split view controller. It provides a user interface configured with a split view controller and two view controllers to manage a master-detail-style display.
Tab Bar
iPhone, iPod touch, iPad
This template provides a starting point for an application that uses a tab bar. It provides a user interface configured with a tab bar controller, and a view controller for the first tab bar item.
Utility
iPhone, iPod touch
This template provides a starting point for a utility application that has a main view and a flipside view. It sets up an Info button to flip the main view to the flipside, and a navigation bar with a Done item to flip back to the main view.
View-based
iPhone, iPod touch, iPad
This template provides a starting point for an application that uses a single view. It provides a view controller to manage the view, and a nib file that contains the view.
Window-based
iPhone, iPod touch, iPad, Universal
This template provides a starting point for any application. It provides only an application delegate and a window.
Xcode Project Builder
Figure A-2
Figure A-3
❘ 507
508
❘ Appendix A Your Initial App — First Steps
The Project Window From the project window, you can manage all aspects of your application development. Each application type offers template code and user interfaces applicable to the application’s functionality.
Navigation-based Navigation-based applications provide a view controller that represents a data source that populates a Table view. Upon selection of a single row of data, the next view controller in the sequence of stacked view controllers is created, thus navigating a hierarchy of data. Your starting template is shown in Figure A-4.
Figure A-4
OpenGL ES The Open Graphics Library (OpenGL) is a cross-platform C-based interface used to create 2D and 3D content on desktop systems. Game developers or anyone needing to perform drawing with high frame rates typically use Open GL. Your starting template is shown in Figure A-5.
Split View–based Split view–based applications can be used in master-detail interfaces or wherever you want to display two different types of information side by side. When the device is in a landscape orientation, the split view shows both panes. In portrait orientation, the split view displays only the second pane, which grows to fill the available space. Your starting template is shown in Figure A-6.
Xcode Project Builder
Figure A-5
Figure A-6
❘ 509
510
❘ Appendix A Your Initial App — First Steps
Tab Bar–based Tab Bar–based applications provide different perspectives on the same set of data, or different subtasks related to the overall function of the application. The tab bar appears at the bottom edge of the screen. Your starting template is shown in Figure A-7.
Figure A-7
Utility-based Utility applications perform a simple task that requires a minimum of user input. People open a utility application to see a quick summary of information, check the current status of something, or perform a simple task on a limited number of objects. Your starting template is shown in Figure A-8.
View-based View-based applications use a single view to implement the user interface. Your starting template is shown in Figure A-9.
Window-based Window-based applications describe any applications that contain an application delegate and a window. Use this template when you want to implement your own view hierarchy.
Xcode Project Builder
Figure A-8
Figure A-9
❘ 511
512
❘ Appendix A Your Initial App — First Steps
Build and Run in the Simulator In the project window, when the Build and Run icon is clicked (see Figure A-10), the iPhone Simulator application is launched on your computer. You use the simulation environment to do the following: ➤➤
Find and fix major problems in your application during design and early testing.
➤➤
Lay out and test your application’s user interface.
➤➤
Measure your application’s memory usage before carrying out detailed performance analysis on iPhone OS–based devices.
Figure A-10
Figure A-11 shows my DoubleMatch application running in the SDK 4.0 iPhone Simulator. Figure A-12 shows my DoubleMatch application running in the SDK 3.2 iPhone Simulator, dedicated for the iPad.
Figure A-11
Figure A-12
Interface Builder Interface Builder is the visual design environment used to create nib files. Nib files contain a combination of standard objects (such as the windows and views provided by the UIKit framework) and custom objects from your Xcode projects. Creating view hierarchies within Interface Builder is a simple matter of dragging
interface Builder
❘ 513
and dropping views in place. You can also configure the attributes of each object using the inspector window and create connections between objects to defi ne their runtime relationships. At runtime, the nib fi les are loaded into your application when you need the objects they contain. If your application uses view controllers, the view controller handles the nib loading process for you automatically; but nib fi les can also be loaded programmatically by using the methods of the NSBundle class. When you create a new project, the template code is generated, along with a template nib fi le. The application type determines how many nib fi les will be generated. For example a Navigation-based application has two nib fi les generated (MainWindow.xib and RootViewController.xib), while a Window-based application has only one generated (the MainWindow.xib). Keep in mind that these are just the starting fi les for your project. You are free to add as many Objective-C source fi les and nibs as your design requires.
You often hear nib and xib used interchangeably. In the days of NeXTSTEP, Interface Builder generated SomeFile.nib fi les, wherein nib stood for NeXT Interface Builder. Today we have Xcode so it is an Xcode Interface Builder, or XIB fi le. To avoid the confusion completely, just call them Interface Builder fi les.
creating an interface Builder document From your Xcode project view, there is a folder named Nib Files. For example, if you have a View-based application called ViewBased, double-click on ViewBasedViewController.nib. This will launch Interface Builder if it is not already running. After Interface Builder launches, you should see four windows: ➤➤
The document window
➤➤
The initial view
➤➤
The inspector
➤➤
The Library
the document Window Each Interface Builder document stores information about one or more objects you want to create at runtime in your application. The document window lists these objects. Most of these objects correspond to elements displayed on the screen, such as windows, views, controls, and menus. Some objects do not correspond to displayed elements, such as the controller objects your application uses to manage its windows and views. Figure A-13 shows the Interface Builder document window.
the library Window
fiGUre a-13
The Library window, shown in Figure A-14, contains the objects and resources you add to your Interface Builder documents. The Library window contains a large variety of elements, such as the View, the Navigation Bar, the Toolbar and the Bar Button Item. You can drag any of these objects onto a design surface such as a window or view.
514
❘ Appendix A Your Initial App — First Steps
The Inspector Window The inspector window makes it easy to display and adjust the settings for the currently selected objects. You use the mode icons along the top of the window to select a pane and display the associated settings (see Figure A-15).
Figure A-14
Figure A-15
The Connections Panel A connection enables you to associate interface elements with source code. The connections panel, shown in Figure A-16, provides a quick way to create and manage the connections for your outlets and actions.
Summary
Figure A-16
Despite the virtually infinite number of different applications you can develop for the iPhone and iPad, with Xcode, their beginnings are somewhat similar. Through the Xcode and Interface Builder, built-in templates are provided to aid you in the development of your application. This appendix should serve as a starting point not only for the applications in this book, but also for your own designs.
B
iPhone Developer Center What’s in this chaPter? ➤➤
Examining developer resources
➤➤
Using the iPhone developer program
➤➤
Understanding the relationship of iTunes Connect and the App Store
The only requirement for learning about and experimenting with the tools and techniques for developing applications for the iPhone, iPod touch, and iPad is registering with Apple in the iPhone Developer Center: http://developer.apple.com/iphone. Accessing all the tools and documentation is free of charge. The only cost is if you develop a device-ready application that you wish to sell in the iTunes App Store. Even then, for an individual it is only $99. If you write a reasonably decent app, sales should generate at least that much in a year. For those developers who like to live on the edge of technology, Apple sometimes offers a separate registration that enables you to participate in beta releases of their emerging products. In addition to registration, you have to sign a nondisclosure agreement (NDA), but you will have a head start in the race to check out what’s new. This appendix presents a brief tour of available resources for developers who are interested in creating applications for these revolutionary devices.
resoUrces for the ios 4 sdk The iPhone Developer Center is organized to help developers new to iPhone development fi nd their way, offering numerous Getting Started videos, documents, sample code, and coding how-tos. It also provides experienced developers with complete Reference guides. You can also get valuable information directly from other developers, including Apple engineers, through the Apple Developer Forums. This section lists the fundamental resources that are freely available to you.
516
❘ Appendix B iPhone Developer Center
Downloads The basic download in the iPhone Developer Center is the basic SDK. At the time of this writing, the current SDK is version 4, or iOS 4. iOS 4 is the latest release for the iPhone, while included in this release is SDK 3.2 for the iPad. Also included in the SDK download is the Xcode IDE, developer documentation, the iPhone Simulator, and other tools that will aid your development efforts for all of the Apple devices — i Phone, iPod touch, and iPad.
iOS 4 Reference Library The iOS 4 Reference Library, found at https://developer.apple.com/iphone/library/navigation/ index.html, is organized by resource type: ➤➤
Coding How-Tos
➤➤
Getting Started
➤➤
Guides
➤➤
Reference
➤➤
Release Notes
➤➤
Sample Code
➤➤
Technical Notes
➤➤
Technical Q&As
Coding How-Tos Organized using a format of frequently asked questions, the coding how-tos enable developers to quickly find and examine a topic of interest. The concise answer includes links to further documentation. Topics include the following: ➤➤
User Experience
➤➤
Networking & Internet
➤➤
Audio & Video
➤➤
Security
➤➤
Tools
➤➤
Data Management
➤➤
Graphics & Animation
Getting Started Videos More than 150 free videos are currently available, many also including the slides, and they are organized as follows: ➤➤
Essential
➤➤
Advanced
➤➤
Foundation
➤➤
In-House App Development Essential
Resources for the iOS 4 SDK
❘ 517
In addition to the free videos, you can purchase sessions from previous Worldwide Developer Conferences. Although you must pay for these session videos, you get source code examples from the sessions in addition to the video and slides.
Getting Started Documents The Getting Started section of the Developer Center contains a series of documents that offer insight into a wide range of topics including, but not limited to, the following: ➤➤
Data Management
➤➤
Networking & Internet
➤➤
Audio & Video
➤➤
Graphics & Animation
➤➤
Developer Tools
➤➤
Performance
➤➤
Security
➤➤
User Experience
Sample Code One of the most valued resources is the Sample Code section, which includes nearly 100 free, fully-working applications that cover the major topics identified in the Topics section of the iOS 4 Reference Library. The code examples constantly evolve as APIs change and are improved. It is truly a great resource. These code samples are also embedded as links within the reference guides, so as you are reading the topic in the reference, you can click the link in the guide and be taken to the associated sample code page for immediate project download. It is also organized by topic: ➤➤
Audio & Video
➤➤
Tools & Languages
➤➤
Data Management
➤➤
General
➤➤
Graphics & Animation
➤➤
Networking & Internet
➤➤
Performance
➤➤
Security
➤➤
User Experience
Finally, from a more technical approach, the Reference Library has a section dedicated to the iOS 4 frameworks. The frameworks are organized as follows: ➤➤
Cocoa Touch Layer
➤➤
Media Layer
➤➤
Core Services Layer
➤➤
Core OS Layer
Each of these layers is further subdivided, enabling you to find the necessary document as quickly as possible.
518
❘
aPPendiX B iphoNe developer ceNter
iPhone develoPer ProGram When you join the developer program by paying your annual membership fee, you open the door that enables you to start marketing your apps on the iTunes App Store.
To find more detail about provisioning, registering your device, creating App IDs, and preparing your application for sale, consult the official guide: iPhone Developer Program: Standard Program User Guide, which can be found at the following url: http://adcdownload.apple.com/iphone/iphone_developer_program_user_ guides/iphone_developer_program_user_guide__standard_program_v2.6_ final_3410.pdf.
iPhone Provisioning Portal The iPhone Provisional Portal is the place where you complete the steps necessary to prepare your applications for distribution. There are well-documented guides to aid you in the process, but here is a brief overview: ➤➤
Obtain your iPhone development certificate — In order for an application to run on any Apple device, the application must be signed by a valid certificate. The steps are as follows:
1 . 2 . 3 . 4 .
Generating a certificate signing request (CSR) Submitting a CSR for approval Receiving approval for the CSR Downloading and installing the CSR
➤➤
Register your device — In order to install and test your app on a device, the device must be registered.
➤➤
Set the App ID — After registering your device, you must create an App ID. This unique generated id is used by the iTunes App Store to identify your application.
1 . 2 . ➤➤
Generating an App ID Registering an App ID for In App Purchase
Add and install a provisioning profile. If you are in development and want to run your application on your device, you need the development provisioning profile. If you are ready to submit your application to the iTunes App Store, then you need the distribution provisioning profile. ➤➤
Development — This profile provides a link between the application, the developer, and the device. The profile must be generated, downloaded, and installed into Xcode for installation of an associated app onto a developer’s device.
➤➤
Distribution — Along with your iPhone CSR, you need to generate, download, and install into Xcode your distribution provisioning profile. Instead of installing this on your device, you build and create a zip package of your app bundle for App Store distribution. Submission to the App Store is accomplished through iTunes Connect.
apple developer forums Outside of the sample code, some of the greatest resources available to developers are the iPhone developer forums. Access to the forums is free; however, you have to register with Apple Developer programs located at https://developer.apple.com/.
iPhone Developer Program
❘ 519
Topics include the following: ➤➤
Getting Started
➤➤
Application Frameworks
➤➤
Graphics and Media Technologies
➤➤
Multitasking
➤➤
Core OS
➤➤
Web Technologies
➤➤
Apple Push Notification
➤➤
In App Purchase
➤➤
Developer Tools
➤➤
Performance Analysis and Debugging
➤➤
Distribution
These forums include the active participation of Apple engineers, who ensure that the technology is understood and implemented properly.
Developer Support Center The support center is located at https://developer.apple.com/support/iphone/. For the iPhone, the Developer Program offers the following support: ➤➤
Program Enrollment
➤➤
Identity Verification
➤➤
Purchase and Next Steps
➤➤
Account Manager
➤➤
Program Renewals
➤➤
iPhone Enterprise Program
➤➤
iPhone University Program
App Store Resource Center In addition to the technical aspects of your application development, Apple also has a resource center to aid you in the App Store process: https://developer.apple.com/iphone/appstore/. The resources are organized in four sections: ➤➤
➤➤
Prepare for App Submission ➤➤
Getting started with iTunes Connect
➤➤
Setting up user accounts
➤➤
Complete banking info and contracts
➤➤
Checklist for app submission
App Store Approval Process ➤➤
Fully explains the entire App Store approval process
520
❘ Appendix B iPhone Developer Center
➤➤
➤➤
Managing Apps on the App Store ➤➤
Explains the concepts of your app’s metadata
➤➤
Explains the process of interpreting crash logs
➤➤
Explains how to interpret sales reports
Marketing Resources
iTunes Connect After you have prepared your application for App Store submission through the portal, the actual submission and complete management will occur in iTunes Connect, which can be found at https://itunesconnect .apple.com/. This suite of tools for managing the distribution and management of your application includes the following: ➤➤
Sales and Trends — Includes reports for sales and payment information.
➤➤
Contracts, Tax & Bank Information — Lists your App Store contract and financial information, so you receive payments for your apps.
➤➤
Financial Reports — Organize, access, and download your monthly financial reports here.
➤➤
Manage Users — This is where you can manage your iTunes Connect User and In App Purchase Test User accounts.
➤➤
Manage Your Applications — Submit your new, or update your existing, app here for the App Store. Once your app is submitted, it goes through the approval process. When approved, it is live in the App Store.
➤➤
Manage Your In App Purchases — Submit your new, or update your existing, In App Purchases to the App Store.
➤➤
Request Promotional Codes — For marketing purposes, you can request up to 50 promotional codes, per release, that can be redeemed by users at the App Store for a four-week trial of your application.
News & Announcements This is the source for the latest news concerning the entire Apple Developer Network, found at http:// developer.apple.com/iphone/news/
RSS Feed Subscription If you would like the latest developer news sent to you automatically, subscribe to Apple’s developer RSS feed at feed://developer.apple.com/rss/iphonedevnews.rss
Summary This appendix described the resource infrastructure that Apple has created for developers. The Developer Center is your complete source for information on iPhone development, whether you are a new developer or a seasoned professional. With these continually updated resources, from Getting Started, coding how-tos, sample xode, and provisioning, to submission to the App Store and beyond, Apple has streamlined the process of application development, for all developers with an idea and the will to succeed.
C
Cocoa Touch static libraries What’s in this aPPendiX? ➤➤
How to create a static library
➤➤
Designing the static library to be an efficient component
➤➤
How to include a static library in an Xcode application project
I am a big fan of reusability. I only like to write code once. Using Xcode, you can build a static library of useful routines so you don’t have to write the same code repeatedly, or, worse yet, cut and paste the same code all over the place. While it may seem harmless and easy to just cut and paste code wherever you need it, imagine you have several applications in the iTunes App Store already and you have just discovered a major bug in your code that causes the apps to crash. After spending hours tracking down the bug, you now have to fi x it in all of the other applications — a tedious, time-consuming task. Needless to say, this is not fun; and there is a solution, which is what this appendix is all about. In Chapter 10, “Data Storage,” you worked through a contacts application using both property lists and Core Data. This appendix uses the property list application from Chapter 10, separating the data storage component and making it into a static library. That way, the contacts application focuses on user interaction, and the static library can be used for storing the contact information.
Xcode Project temPlate Creating a static library is similar to creating an application; you merely choose a different template. The important thing to keep in mind about a static library is that you do not build and run your project, you just Build it. The purpose of designing this contact static library is to separate the data storage responsibility from the user interaction.
522
❘ Appendix C Cocoa Touch Static Libraries
Choose Cocoa Static Library To create the static library, start Xcode and select iPhone Library, choose Cocoa Touch Static Library, and name the project DataSource (see Figure C-1).
Figure C-1
The responsibility of this library is to store Person contact data in a property list. Any application that uses this library must have the following methods available to store and retrieve contact information: ➤➤
initWithName: — For initialization
➤➤
addPerson:forKey: — For adding contacts
➤➤
personForKey: — For retrieving a particular contact
➤➤
removePersonForKey: — For removing a contact
Adding Classes to the Library Before writing the classes that will be included in this library, template stubs need to be created. The data storage object that handles the reading and writing of the data from and to property lists is a DataSource object. To create this class, choose File ➪ New File, choose Cocoa Touch Class, and select Objective-C class as a subclass of NSObject and name it DataSource. The data that is going to be stored is a Person object. To create this class, choose File ➪ New File and select Objective-C class as a subclass of NSObject and name it Person.
Xcode Project Template
❘ 523
Source Code Listings for the DataSource Static Library This section lists the code that provides the storage of contact information to a property list. The DataSource class stores the contact information in an NSDictionary, and keeps the filename of the plist.
DataSource.h Modifications to the Template The DataSource class has the responsibility of reading and writing data that is stored on disc. This class also has the responsibility of maintaining the data in memory and makes it accessible to the application that will be developed in the next section, “An Xcode Application Project.” Listing C-1 shows the complete interface listing of the DataSource.h file. Listing C-1: The complete DataSource.h file (/AppendixC/DataSource /Classes/DataSource.h) #import @class Person; @interface DataSource : NSObject { NSDictionary *data; NSString *fileName; } @property (nonatomic, retain) NSDictionary *data; @property (nonatomic, retain) NSString *fileName; -
initWithName:(NSString *)dataName; (id)personForKey:(NSString *)aKey; (void)addPerson:(Person *)aPerson forKey:(NSString *)aKey; (void)removePersonForKey:(NSString *)aKey;
- (NSDictionary *)dictionaryFromPropertyList:(NSString *)filename; - (BOOL)writePropertyListFromDictionary:(NSString *)filename dictionary:(NSDictionary *)plistDict; @end
DataSource.m Modifications to the Template The data that is stored is a plist file. In this format, it is easy to read the data into an NSDictionary object. By using the NSDictionary class, the values are retrieved by the use of a key and through the method PersonForKey:. In addition to the PersonForKey: method, there are methods for adding a Person to the dictionary, as well as for removing a Person from the dictionary, as addPerson:forKey: and removePersonForKey: respectively. Listing C-2 shows the complete interface listing of the DataSource.m file. Listing C-2: The complete DataSource.m file (/AppendixC/ DataSource /Classes/DataSource.m) #import “DataSource.h” #import “Person.h”
@implementation DataSource @synthesize data; @synthesize fileName;
524
❘ Appendix C Cocoa Touch Static Libraries
#pragma mark #pragma mark Initialization - initWithName:(NSString *)dataName { NSDictionary *aDict = [self dictionaryFromPropertyList:dataName]; if(aDict == nil) { aDict = [NSDictionary dictionary]; } [self setData:aDict]; [self setFileName:dataName]; return self; } #pragma mark #pragma mark Data methods - (id)personForKey:(NSString *)aKey { return [data objectForKey:aKey]; } - (void)addPerson:(Person *)aPerson forKey:(NSString *)aKey { NSMutableDictionary *aDict = [data mutableCopy]; [aDict setObject:aPerson forKey:aKey]; [self writePropertyListFromDictionary: [self fileName] dictionary:aDict]; [self setData:aDict]; [aDict release]; } - (void)removePersonForKey:(NSString *)aKey { NSMutableDictionary *aDict = [data mutableCopy]; [aDict removeObjectForKey:aKey]; [self writePropertyListFromDictionary:[self fileName] dictionary:aDict]; [self setData:aDict]; [aDict release]; } #pragma mark #pragma mark PropertyList methods - (NSDictionary *)dictionaryFromPropertyList:(NSString *)aFileName { NSDictionary *result = nil; NSString *fname = [NSString stringWithFormat:@”%@.plist”, aFileName]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *aData = [NSData dataWithContentsOfFile:bundlePath]; if(aData != nil) { result = [NSKeyedUnarchiver unarchiveObjectWithData:aData]; } return result; }
Xcode Project Template
❘ 525
- (BOOL)writePropertyListFromDictionary:(NSString *)filename dictionary:(NSDictionary *)aDict { NSString *fname = [NSString stringWithFormat:@”%@.plist”, filename]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *aData = [NSKeyedArchiver archivedDataWithRootObject:aDict]; return [aData writeToFile:bundlePath atomically:YES]; } #pragma mark #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self data] forKey:@”data”]; [coder encodeObject:[self fileName] forKey:@”fileName”]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setData:[coder decodeObjectForKey:@”data”]]; [self setFileName:[coder decodeObjectForKey:@”fileName”]]; } return self; } #pragma mark #pragma mark Memory methods - (void)dealloc { [data release]; [fileName release]; [super dealloc]; } @end #import
Person.h Modifications to the Template The Person class is used only to hold a person’s contact information. One thing to note is the NSCoding protocol. This allows for the archival and retrieval to disc to be accomplished. Listing C-3 shows the complete interface listing of the Person.h file. Listing C-3: The complete Person.h file (/AppendixC/DataSource/Classes/Person.h) #import
@interface Person : NSObject { NSString *firstName; NSString *lastName; NSString *phone;
526
❘ Appendix C Cocoa Touch Static Libraries
} @property (nonatomic, retain) NSString *firstName; @property (nonatomic, retain) NSString *lastName; @property (nonatomic, retain) NSString *phone; @end
Person.m Modifications to the Template Because the NSCoding protocol is being implemented, the encodeWithCoder: and initWithCoder are required for writing and reading the Person object correctly. Listing C-4 shows the complete interface listing of the Person.m file. Listing C-4: The complete Person.m file (/AppendixC/DataSource/Classes/Person.m) #import “Person.h”
@implementation Person @synthesize firstName; @synthesize lastName; @synthesize phone;
#pragma mark #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self firstName] forKey:@”firstName”]; [coder encodeObject:[self lastName] forKey:@”lastName”]; [coder encodeObject:[self phone] forKey:@”phone”]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setLastName:[coder decodeObjectForKey:@”firstName”]]; [self setFirstName:[coder decodeObjectForKey:@”lastName”]]; [self setPhone:[coder decodeObjectForKey:@”phone”]]; } return self; } #pragma mark #pragma mark Memory methods - (void)dealloc { [firstName release]; [lastName release]; [phone release]; [super dealloc]; } @end
An Xcode Application Project
❘ 527
Building the Project Because this is not an application, you cannot Build and Run. Choose Build ➪ Build, and your target libDataSource.a is now ready to be included in an application, as shown in Figure C-2.
Figure C-2
An Xcode Application Project The application in this appendix is like any other application with the exception that the data storage component is not directly part of it. That responsibility is handled by the external libDataSource.a library that is added to the application project. When this application launches for the first time, an empty Table view appears. It is empty because no contacts have been added yet. In addition to the empty Table view, there is an Edit button on the left, which enables the deletion of contacts in the list, and a plus button on the right, which enables the addition of new contacts (see Figure C-3). Tapping the plus button to add a new contact brings up an entry screen for first name, last name, and contact phone number. After entering the contact information, the user taps the Save or Cancel button. If Save is selected, the new entry is displayed in the list, as shown in Figure C-4. Only the last name is displayed in the list. To see contact details, the user taps the name entry and the details are displayed, as shown in Figure C-5.
528
❘ Appendix C Cocoa Touch Static Libraries
Figure C-3
Figure C-4
Figure C-5
To delete an entry from the list, the Edit button is tapped, and a red minus is displayed to the left of the last name. If the red minus is tapped, the minus rotates to a vertical position and a red Delete button appears to the right of the last name. Tapping the Delete button will remove the entry from both the list and storage. Tapping Done exits edit mode and the application is ready to accept more additions. The steps to create this application for storing contact information to a property list are outlined in the following sections.
Creating Your New Application In Xcode, create a new Navigation-based application for the iPhone. Do not select “Use Core Data for storage.” Name the project PListStorage. If you need to see this step, please see Appendix A for the steps to begin a Navigation-based application.
Adding the Library to the New Project To add the library project to the application’s Xcode project, click and drag the blue DataSource project from the Groups & Files window of the library project into the PListStorage Groups & Files window as shown in Figure C-6. Click Add in response to the pop-up asking if you want to add the file to the project. When you release the mouse you will see that the library project is included in the application’s project, as shown in Figure C-7.
An Xcode Application Project
Figure C-6
Figure C-7
❘ 529
530
❘ Appendix C Cocoa Touch Static Libraries
Select the libDataSource.a in the project window and select the check box on the right to ensure that the library is included in the build process of the application (see Figure C-8).
Figure C-8
Adding Library Headers to the App Project To add the DataSource.h file to the application’s Xcode project, drag it from the Groups & Files window of the library project into the PListStorage Groups & Files window as shown in Figure C-9. Click Add in response to the pop-up asking if you want to add the file to the project. When you release the mouse, you will see that the DataSource.h file is included in the application’s project, as shown in Figure C-10. Repeat the same process for the Person.h class.
Designing the App User Interface To enable the entering of contact information, the user interface has to be designed and the view controllers have to be written. The following steps will create the user interface and the associated view controllers.
1.
In order to add a new Person who will be stored in the property list, you need to create a new view controller. From Xcode, choose File ➪ New File and select UIViewController subclass, making sure that “With XIB for user interface” is the only item checked. Name this new class PersonAddViewController.
2.
To display the details of a selected person, you need to create a new view controller. Choose File ➪ New File and select UIViewController subclass, this time making sure that “UITableViewController subclass” and “With XIB for user interface” are checked. Name this new class PersonDetailViewController.
3.
Double-click the PersonAddViewController.xib file to launch Interface Builder (see Figure C-11).
An Xcode Application Project
Figure C-9
Figure C-10
❘ 531
532
❘ Appendix C Cocoa Touch Static Libraries
Figure C-11
4.
From Interface Builder’s main menu, select Tools ➪ Attributes Inspector, click the View window, and set the Background option to Group Table View Background Color.
From the main menu, select Tools ➪ Library, and choose the Objects tab.
5. 6.
7.
Select a UITextField and drag it over the main view near the top and release the mouse. Repeat this two more times so you have three stacked text fields (see Figure C-12). Select the first text field and do the following: ➤➤
Enter First Name for the placeholder.
➤➤
Set the font to size 17.
➤➤
Set Capitalize to Words.
8.
Repeat the previous steps for the next two fields using Last Name for the placeholder for the second text field, and Phone for the placeholder for the third text field, as shown in Figure C-13.
9.
Select Tools ➪ Library, choose Classes at the top, and scroll to and select your PersonAddViewController class. At the bottom, choose the Outlets button. Click the + and add the following outlets, as shown in Figure C-14: ➤➤
firstNameTextField (as a UITextField instead of id type)
➤➤
lastNameTextField (as a UITextField instead of id type)
➤➤
phoneTextField (as a UITextField instead of id type)
An Xcode Application Project
Figure C-12
10.
Figure C-13
❘ 533
Figure C-14
From the main menu of Interface Builder, choose File ➪ Write Class Files, select Save from the first pop-up, and then select Merge from the second pop-up. A window will appear with your new additions to PersonAddViewController.h on the left and the original template on the right (see Figure C-15): ➤➤
In the lower-right corner, select Actions ➪ Choose Left.
➤➤
Choose File ➪ Save Merge and close the window.
There aren’t any changes to the file, so just close it. You now have an Objective-C template that will hold your application’s logic. Before you begin actually programming, there is one more task to complete in Interface Builder. You have to make all the connections to: ➤➤
Identify the UITextField as firstNameTextField.
➤➤
Identify the UITextField as lastNameTextField.
➤➤
Identify the UITextField as phoneTextField.
534
❘ Appendix C Cocoa Touch Static Libraries
Figure C-15
11.
12.
To make the connection to identify the UITextField as firstNameTextField, control-click on the File’s Owner icon to bring up the Inspector, as shown in Figure C-16. From the right of the File’s Owner Inspector, control-drag from the circle next to firstNameTextField to UITextField firstNameTextField until it is highlighted, then release the mouse. The circle will be filled, indicating that the connection has been made. Repeat this for lastNameTextField and phoneTextField (see Figure C-17).
Figure C-16
13.
Figure C-17
To set the PersonAddViewController as the delegate of the three text fields, control-drag from firstNameTextField to the File’s Owner Icon and select delegate. Repeat this for lastNameTextField and phoneTextField. By doing this, when the user taps the Done button on the keyboard, the keyboard will be dismissed. Select File ➪ Save when you are done.
An Xcode Application Project
❘ 535
Adding Source Code Using the Library Classes The advantage to having the storage functionality in a separate library is that now the application only has to be concerned about the view controllers and how they handle the application interface navigation. When it comes time for the data interaction, the library classes will be used through the DataSource class.
RootVewController.h Modifications to the Template Listing C-5 shows the complete interface listing of the RootViewController.h file. Listing C-5: The complete RootViewController.h file (/AppendixC/PListStorage/Classes/ RootViewController.h) #import @class PersonAddViewController; @class DataSource; @interface RootViewController : UITableViewController { DataSource *contacts; } @property (nonatomic, retain) DataSource *contacts; - (void)addPerson:(id)sender; - (void)savePerson:(PersonAddViewController *)sender;
@end
RootVewController.m Modifications to the Template Listing C-6 shows the complete interface listing of the RootViewController.m file. Listing C-6: The complete RootViewController.m file (/AppendixC/PListStorage/Classes/ RootViewController.m) #import #import #import #import #import
“RootViewController.h” “PersonAddViewController.h” “PersonDetailViewController.h” “Person.h” “DataSource.h”
@implementation RootViewController @synthesize contacts; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setContacts:[[DataSource alloc] initWithName:@”Contacts”]]; [self setTitle:@”Contacts”]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
536
❘ Appendix C Cocoa Touch Static Libraries
target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; } #pragma mark #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; } #pragma mark #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { Person *person = [[Person alloc] init]; [person setFirstName:[[sender firstNameTextField] text]]; [person setLastName:[[sender lastNameTextField] text]]; [person setPhone:[[sender phoneTextField] text]]; [[self contacts] addPerson:person forKey:[[sender lastNameTextField] text]]; [person release]; [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } #pragma mark #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (contacts == nil) { [self setContacts:[NSDictionary dictionary]]; } return [[[self contacts] data] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView
An Xcode Application Project
cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = [indexPath row]; NSArray *keys = [[[[self contacts] data] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *cellText = [keys objectAtIndex:row]; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [[cell textLabel] setText:cellText]; return cell; } #pragma mark #pragma mark Table view editing // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; if (editingStyle == UITableViewCellEditingStyleDelete) { [[self contacts] removePersonForKey:[[cell textLabel] text]]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; Person *person = [[self contacts] personForKey:[[cell textLabel] text]]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning {
❘ 537
538
❘ Appendix C Cocoa Touch Static Libraries
[super didReceiveMemoryWarning]; } - (void)viewDidUnload { } - (void)dealloc { [contacts release]; [super dealloc]; } @end
PersonAddViewController.h Modifications to the Template Listing C-7 shows the complete interface listing of the PersonAddViewController.h file. Listing C-7: The complete PersonAddViewController.h file (/AppendixC/PListStorage/ Classes/PersonAddViewController.h) #import
@interface PersonAddViewController : UIViewController { UITextField *firstNameTextField; UITextField *lastNameTextField; UITextField *phoneTextField; id delegate; } @property @property @property @property
(nonatomic, (nonatomic, (nonatomic, (nonatomic,
retain) retain) retain) retain)
IBOutlet UITextField *firstNameTextField; IBOutlet UITextField *lastNameTextField; IBOutlet UITextField *phoneTextField; id delegate;
@end
PersonAddViewController.m Modifications to the Template Listing C-8 shows the complete interface listing of the PersonAddViewController.m file. Listing C-8: The complete PersonAddViewController.m file (/AppendixC/PListStorage/ Classes/PersonAddViewController.m) #import “PersonAddViewController.h” #import “RootViewController.h”
@implementation PersonAddViewController @synthesize @synthesize @synthesize @synthesize
firstNameTextField; lastNameTextField; phoneTextField; delegate;
#pragma mark #pragma mark View lifecycle // Implement viewDidLoad to do additional setup after
An Xcode Application Project
// loading the view, typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @”Add Contact”; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Cancel” style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@”Save” style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; } #pragma mark #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } #pragma mark #pragma mark Action methods - (void)save { [[self delegate] savePerson:self]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setFirstNameTextField:nil]; [self setLastNameTextField:nil]; [self setPhoneTextField:nil]; [super viewDidUnload]; }
- (void)dealloc { [super dealloc]; }
@end
❘ 539
540
❘ Appendix C Cocoa Touch Static Libraries
PersonDetailViewController.h Modifications to the Template Listing C-9 shows the complete interface listing of the PersonDetailViewController.h file. Listing C-9: The complete PersonDetailViewController.h file (/AppendixC/PListStorage/
Classes/PersonDetailViewController.h) #import @class Person;
@interface PersonDetailViewController : UITableViewController { Person *contact; } @property (nonatomic, retain) Person *contact; @end
PersonDetailViewController.m Modifications to the Template Listing C-10 shows the complete interface listing of the PersonDetailViewController.m file. Listing C-10: The complete PersonDetailViewController.m file (/AppendixC/PListStorage/ Classes/PersonDetailViewController.m) #import “PersonDetailViewController.h” #import “Person.h”
@implementation PersonDetailViewController @synthesize contact; #pragma mark #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@”Contact Detail”]; } #pragma mark #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; } else { rows = 2; } return rows;
An Xcode Application Project
} // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @”Last Name”; break; case 1: sectionTitle = @”Details”; break; default: break; } return sectionTitle; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @”Cell”; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell;
❘ 541
542
❘ Appendix C Cocoa Touch Static Libraries
} #pragma mark #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } #pragma mark #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContact:nil]; } - (void)dealloc { [contact release]; [super dealloc]; }
@end
Test Your Application Now that you have everything completed, choose the Simulator from the Xcode panel and click Run and Build to try out your app. It should give you the results described at the beginning of the “Xcode Application Project” section.
Summary The functionality of the application developed in this appendix isn’t any different from the application that was developed for property lists in Chapter 10, but the architecture is completely different. While code reuse and distribution has been around for a very long time, it is not used as much as it really should be. The application demonstrated here is a simple one, and one you have seen in a few other implementations, but I hope that you can see the power of reusable components and static libraries in your own iOS 4 projects. I have used a static library in three of the applications I have released to the iTunes App Store. Using a static library made application enhancements quite elegant as I changed the code to enhance the functionality in my library, and the changes were reflected in all three applications. It was a huge time saver for me, and I hope you have the same experience.
D
apple Developer resources What’s in this aPPendiX? ➤➤
How to find the appropriate information to begin developing iPhone applications
➤➤
Where to find more information about submitting your application to the iPhone App Store
This book illustrated the many frameworks that are available to you for your own applications; however, the most complete and up-to-date documentation is on the Apple site itself. This appendix provides references to the development centers and development guides that are published by Apple for you. The development tools and documentation are free after registering with the Developer Program, but if you want to deploy to your device or sell your application on the iTunes App Store, you have to be a paying member of the iPhone developer program. Regarding hardware and system software requirements, you must have an Intel processor–based Mac running Snow Leopard. This appendix lists important documents and developer resources on the Apple website to guide you through the entire application development process — from concept to distribution.
iPhone develoPer ProGram The main link to the developer program is http://developer.apple.com/programs/iphone.
developer centers The following links provide access to centers for a specific area of the iPhone developer program: ➤➤
The Apple iPhone Dev Center: https://developer.apple.com/iphone
➤➤
Developer Member Center: https://developer.apple.com/membercenter/index.action
➤➤
Developer Resource Center: http://developer.apple.com/resources
➤➤
Developer Technical Videos: http://developer.apple.com/videos
➤➤
WWDC 2010 Videos: http://developer.apple.com/videos/wwdc/2010
544
❘ Appendix D Apple Developer Resources
iOS Resources The following links provide access to a wide variety of information covering all aspects of the iOS: ➤➤
iOS Video Center: http://developer.apple.com/videos/iphone
➤➤
iOS Reference Library: https://developer.apple.com/iphone/library/navigation/index.html
➤➤
iOS Sample Code: https://developer.apple.com/iphone/library/navigation/index .html#section=Resource%20Types&topic=Sample%20Code
iOS Developer Resources The following links provide access to high-level information about the iOS, its architecture, and how your publications access it: ➤➤
iOS Xcode: http://developer.apple.com/technologies/xcode.html
➤➤
iOS Video Center: http://developer.apple.com/videos/iphone/
➤➤
iOS Developer Guides: https://developer.apple.com/iphone/library/navigation/index .html#section=Resource%20Types&topic=Guides
➤➤
iOS Reference Guides: https://developer.apple.com/iphone/library/navigation/index .html#section=Resource%20Types&topic=Reference
➤➤
iOS Tools for Development: https://developer.apple.com/iphone/library/referencelibrary/ GettingStarted/URL_Tools_for_iPhone_OS_Development/index.html#//apple_ref/doc/ uid/TP40007593
iOS Developer Guides The following guides are critical to understanding the fundamental iOS architecture and user experience with applications that are already on the web: ➤➤
iOS Development Guide: https://developer.apple.com/iphone/library/documentation/ Xcode/Conceptual/iphone_development/000-Introduction/introduction.html
➤➤
iOS Application Programming Guide: https://developer.apple.com/iphone/library/ documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Introduction/ Introduction.html#//apple_ref/doc/uid/TP40007072
➤➤
iPad Programming Guide: https://developer.apple.com/iphone/library/documentation/ General/Conceptual/iPadProgrammingGuide/Introduction/Introduction.html#// apple_ref/doc/uid/TP40009370
➤➤
iPhone Human Interface Guidelines: https://developer.apple.com/iphone/library/
➤➤
iPad Human Interface Guidelines: https://developer.apple.com/iphone/library/
documentation/UserExperience/Conceptual/MobileHIG/Introduction/Introduction.html documentation/General/Conceptual/iPadHIG/Introduction/Introduction.html
iTunes App Store After joining the developer program, you have access to numerous documents illustrating how to properly implement the requirements so your application can be approved in the iTunes App Store: ➤➤
App Store Resource Center: https://developer.apple.com/iphone/appstore
➤➤
Application Preparation: https://developer.apple.com/iphone/appstore/submission.html
➤➤
App Store Application Management: https://developer.apple.com/iphone/appstore/
➤➤
Provisioning Portal: https://developer.apple.com/iphone/manage/overview/index.action
managing.html
iTunes Connect Developer Guide
❘ 545
In the Provisioning portal, consider using the Development Provisioning Assistant, which guides you through the steps to create and install your Development Provisioning Profile and iPhone Development Certificate.
aPPlication distriBUtion ProcedUres The following resources identify the procedures to follow if you would like to publish your application: ➤➤
Application Distribution Principals: https://developer.apple.com/iphone/manage/ distribution/index.action
➤➤
CSR Generation Information: https://developer.apple.com/iphone/manage/certificates/ team/howto.action
➤➤
Adding Devices: https://developer.apple.com/iphone/manage/devices/index.action
➤➤
App ID Generation: https://developer.apple.com/iphone/manage/bundles/howto.action
➤➤
Development Provisioning Profile Generation: https://developer.apple.com/iphone/manage/
➤➤
iAd Network: https://developer.apple.com/iphone/appstore/iad
provisioningprofiles/howto.action
itUnes connect develoPer GUide Apple has produced a multi-page document describing development and distribution topics and related requirements called the iTunes Connect Developer Guide, which can be found at https:// itunesconnect.apple.com/docs/iTunesConnect_DeveloperGuide.pdf.
index
Index
A action methods Playing Audio from iPod Library application, 278–279, 288 Playing Video from iPod Library application, 307 Action Sheet application (iPad), 80–94 actionSheet:clickedButtonAtIndex: method, 91 DetailViewController.h interface file, 89–90 DetailViewController.h template, modifications to, 88–89 DetailViewController.m implementation file, 91–93 flipTheImage method, 91 loadImageFromName method, 90–91 overview, 80 RootViewController.h template code with modifications, 84 RootViewController.m complete modified file, 87–88 RootViewController.m dealloc method, 86 RootViewController.m tableView:cellForR owAtIndexPath: method, 86 RootViewController.m tableView:didSelect RowAtIndexPath: method, 86
RootViewController.m tableView:numberOfRowsInSection method, 85 RootViewController.m viewDidLoad method, 85 RootViewController.m viewDidUnload method, 86 steps for connecting outlets and actions, 93–94 steps for creating action sheet, 81–84 testing, 93–94 view initialization methods, 90 Action Sheet application (iPhone/iPod Touch), 67–80 actionSheet:clickedButtonAtIndex: method, 77 addBarButtonItem method, 76 createImageView method, 76 DetailViewController.h interface file, 75–76 DetailViewController.h template, modifications to, 75 DetailViewController.m implementation file, 77–79 flipTheImage method, 77 overview, 67–68 RootViewController.h template code with modifications, 70–71 RootViewController.m file (complete), 73–74
549
Action Sheet application (iPhone/iPod Touch) (continued) – application preferences
Action Sheet application (iPhone/iPod Touch) (continued) RootViewController.m tableView:cellForRo wAtIndexPath: method, 72 RootViewController.m tableView:didSelect RowAtIndexPath: method, 72–73 RootViewController.m tableView:numberOfRowsInSection method, 72 RootViewController.m Template, modifications to, 71 RootViewController.m viewdealloc method, 73 RootViewController.m viewDidLoad method, 71 RootViewController.m viewDidUnload method, 73 steps for creating action sheet, 68–70 testing, 80 viewDidLoad method, 75–76 action sheets defined, 66 design of, 66–67 UIActionSheetDelegate protocol, 67 actionSheet delegate method Modal View application (iPad), 114 Modal View application (iPhone/iPod Touch), 101 actionSheet:clickedButtonAtIndex: method Action Sheet application (iPad), 91 Action Sheet application (iPhone/iPod Touch), 77 ADBannerView (iAd Network app), 470 addBarButtonItem method (Action Sheet app), 76 addDataToDictionary data source methods (Memory Leak Test app), 465
550
addObserver:selector:name:object: method, 200 addPerson method Core Data application, 375–376 Property Lists application, 353 afterDelay argument, 413, 423 Alert View application, 54–66 alertView:clicksButtonAtIndex method, 62 AlertViewController.h file (complete), 61 AlertViewController.h template file, 60 AlertViewController.m file (complete), 64–66 AlertViewController.m template file, 60 clearDisplay action method, 63 displayFile method, 62–63 loading files into UITITextView, 55–59 modifications to AlertViewController.h template, 60–61 modifications to AlertViewController.m template, 61–62 openFile:alertTitle: method, 63 overview, 54–55 runChoice action method, 63 showAlert:buttons:alertTitle: method, 64 testing, 66 alerts alert view design, 54 UIAlertViewDelegate protocol, 54 uses for, 53 App Store resource center, 519–520 Apple Developer Network, 520 Apple developer resources, 543–545 application preferences accessing, 315 application configuration, 313–315 guidelines for, 314 overview, 313 preference element types, 314
applications – AVAudioRecorder class
preference hierarchies, 315 Setting Preferences application. See Setting Preferences application applications accessing preferences, 315 applicationDidEnterBackground method (Notifications app), 207 application-testing targets (unit testing), 429–430 applicationWillTerminate method (Notifications app), 207 distribution procedures for, 545 enabling for iAds, 470 initial framework for development of, 505–514 methods for launching (multitasking), 482 methods for switching (multitasking), 482 preparing for iAd Network, 470 types available in Xcode project builder, 506–507 UIApplication delegate messages (Audio Multitasking app), 483 audio Audio Hardware Service, 264 Audio Session Services, 264 Audio Toolbox, 264 Audio Unit framework, 264 frameworks for, 263–264 Playing Audio from iPod Library application. See Playing Audio from iPod Library application Audio Multitasking application, 483–504 button action handler methods, 499–500 canBecomeFirstResponder method, 493–494 createPlayPauseButtons method, 499 development steps, 484–489 iPodLibrary.h file (complete), 502
iPodLibrary.h modifications to template, 501 iPodLibrary.m file (complete), 502–503 iPodLibrary.m modifications to template, 502 MusicPlayerViewController.h file complete, 498 MusicPlayerViewController.h modifications to template, 497–498 MusicPlayerViewController.m file (complete), 500–501 MusicPlayerViewController.m modifications to template, 498 numberOfSectionsInTableView method, 492 overview, 483–484 remoteControlReceivedWithEvent method, 493–494 replaceTrack: method, 491 RootViewController.h file (complete), 489–490 RootViewController.h modifications to template, 489 RootViewController.m file (complete), 494–497 RootViewController.m modifications to template, 490 @synthesize, addition of, 490, 498–499 Table view delegate methods, 493 tableView:cellForRowAtIndex: method, 492–493 tableView:numberOfRowsInSection method, 492 testing, 503 viewDidLoad method, 490–491, 499 viewWillAppear method, 499 AV Foundation framework, 264 AVAudioPlayer class, 264 AVAudioRecorder class, 264
551
AVAudioSession class – connections panel (Interface Builder)
AVAudioSession class, 264, 490 AVPlayer, 265, 490 awakeFromNib method Gesture Recognizer application, 184–185 Touch Handler application, 168–169
B bannerView:didFailToReceiveAdWithError method (iAd Network app), 470 browseForPeers method (Peer-to-Peer app), 254–255 Build and Analyze (Memory Leak Test app), 467–468 Build and Run in Simulator (Xcode project builder), 512 button action handler methods (Audio Multitasking app), 499–500 buttonAction method (Notifications app), 209
C canBecomeFirstResponder method Audio Multitasking application, 493–494 Custom Menus for iPad application, 424 Cutting/Pasting Images application, 413–414 Cutting/Pasting Text application, 402 cancel delegate method Core Data application, 376 Property Lists application, 353 canLoadiAd method (iAd Network app), 476 canPerformAction:withSender: method Custom Menus for iPad application, 424 Cutting/Pasting Images application, 413–414
552
Cutting/Pasting Text application, 402–403 canPerformAction:withSender: method (editing menu), 397 categories (Objective-C), 441 cells, Table view, 122 center, notification, 200 Child Pane Preference Hierarchy application, 325–338 AppPreferences.h file (complete), 333 AppPreferences.m file (complete), 334–335 development steps, 326–332 overview, 325–326 RootViewController.h file (complete), 335–336 RootViewController.m file, 336–337 source code overview, 332–333 testing, 338 viewDidLoad method, 336 clearDisplay action method (Alert View app), 63 closeModalView method Modal View application (iPad), 117–118 Modal View application (iPhone/iPod Touch), 104 Cocoa Touch Class, 142 Cocoa Touch Static Library, 522 code (iOS 4 Reference Library) coding how-tos, 516 Sample Code section, 517 configuring applications, 313–315 configureCell:atIndexPath: method (Core Data app), 378 iAd preferences, 470 navigation bars, 2 connecting outlets and actions (Action Sheet iPad app), 93–94 connections panel (Interface Builder), 514
Contacts.plist property list file (Custom Table View app) – Custom Menus for iPad application
Contacts.plist property list file (Custom Table View app), 133–135 copy method (Cutting/Pasting Text app), 403 copy/cut operations, 397 Core Audio, 264 Core Data application, 367–394 addPerson method, 375–376 cancel delegate method, 376 configureCell:atIndexPath: method, 378 CoreDataStorage.xcdatamodel modifications to template, 391 data model, creating custom, 391–394 development steps, 367–370 fetchedResultsController method, 377–378 numberOfSectionsInTableView, 377 numberOfSectionsInTableView method, 387 PersonAddViewController.h file (complete), 370 PersonAddViewController.h modifications to template, 370 PersonAddViewController.m file (complete), 372–373 PersonAddViewController.m modifications to template, 370 PersonDetailViewController.h file (complete), 386 PersonDetailViewController.h modifications to template, 386 PersonDetailViewController.m file (complete), 388–390 PersonDetailViewController.m modifications to template, 386 Person.h file (complete), 391 Person.m file (complete), 391 Person.m modifications to template, 391 RootViewController.h file (complete), 374 RootViewController.h modifications to template, 373
RootViewController.m file (complete), 380–385 RootViewController.m Modifications to template, 374 save and cancel methods, 372 savePerson method, 376 @synthesize, addition of, 371, 374–375, 386 tableView:cellForRowAtIndexPath: method, 378, 379, 387–388 tableView:didSelectRowAtIndexPath method, 379 tableView:numberOfRowsInSection method, 377, 387 tableView:titleForHeaderInSection method, 387 testing, 394 textFieldShouldReturn method, 371–372 viewDidLoad method, 371, 386 viewDidLoad/viewWillAppear methods, 375 Core Data framework, 341–343 createImageView method (Action Sheet app), 76 createPlayPauseButtons method (Audio Multitasking app), 499 Custom Menus for iPad application, 416–427 canBecomeFirstResponder method, 424 canPerformAction:withSender: method, 424 CopyPasteImage_iPadViewController.h modifications to template, 422 CopyPasteImage_iPadViewController.h template, 421 CopyPasteImage_iPadViewController.m file (complete), 425–427 CopyPasteImage_iPadViewController.m Modifications to template, 422 CopyPasteImage_iPadViewController.m template, 421–422 cut/copy/paste methods, 424–425
553
Custom Menus for iPad application (continued) – Cutting/Pasting Text application
Custom Menus for iPad application (continued) development steps, 418–421 overview, 416–417 placeImageOnPasteboard method, 424–425 restoreView method, 424–425 showMenu: method, 423 @synthesize, addition of, 423 testing, 427 touchesBegan:withEvent: method, 423 Custom Table View application, 122–135 Contacts.plist property list file, 133–135 CustomTableViewCell.h file (complete), 132–133 CustomTableViewCell.m file (complete), 133 data source parameters, characteristics of, 128 development steps, 122–126 overview, 122 PropertyList.h file (complete), 131 PropertyList.m file (complete), 131–132 RootViewController.h file (complete), 127 RootViewController.h Modifications to template, 126 RootViewController.m file (complete), 129–131 @synthesize, addition of, 127 tableView delegate methods, 129 tableView:cellForRowAtIndexPath: method, 128 testing, 135 viewDidLoad method, 127–128 customizing gesture recognizers, 162 tab bars, 26 cut/copy/paste methods Custom Menus for iPad application, 424–425 Cutting/Pasting Images application, 414
554
Cutting/Pasting Images application, 407–416 canBecomeFirstResponder method, 413–414 canPerformAction:withSender: method, 413–414 CopyPasteImage_iPhoneViewController.h file (complete), 412 CopyPasteImage_iPhoneViewController.h modifications to template, 412 CopyPasteImage_iPhoneViewController.h template, 411–412 CopyPasteImage_iPhoneViewController.m file (complete), 414–416 CopyPasteImage_iPhoneViewController.m template, 412 cut/copy/paste methods, 414 development steps, 409–411 overview, 407–408 placeImageOnPasteboard method, 414 showMenu: method, 413 @synthesize, addition of, 412 testing, 416 touchesBegan:withEvent: method, 413 Cutting/Pasting Text application, 398–407 canBecomeFirstResponder method, 402 canPerformAction:withSender: method, 402–403 copy method, 403 CopyTableViewCell.h file (complete), 401–402 CopyTableViewCell.m file (complete), 403–404 development steps, 398–400 overview, 398 readFromPasteboard method, 405 RootViewController.h file (complete), 401 RootViewController.m file (complete), 405–407 setHighlighted:animated: method, 402
Data Storage– event action methods (gesture recognizer)
showMenu method, 405 tableView:cellForRowAtIndexPath: method, 404 tableView:didSelectRowAtIndexPath: method, 405 tableView:numberOfRowsInSection method, 401 testing, 407 viewDidLoad method, 401
D data storage common premise for, 343–344 Core Data application. See Core Data application Core Data framework, 341–343 data element types, 340 overview, 339 property lists, 339–341 Property Lists application. See Property Lists application Data.plist data source (Unit Test app), 442–444 DataSource Static Library, 522–527 building project, 527 Cocoa Touch Static Library, 522 creating template stubs, 522 DataSource.h file (complete), 523 DataSource.h Modifications to template, 523 DataSource.m file, 523–525 DataSource.m Modifications to template, 523 Person.h file (complete), 525–526 Person.h modifications to template, 525 Person.m file (complete), 526 Person.m modifications to template, 526
DataSource.plist property list file (Split Views app), 145–146 dataWithContentsOfFile method, 341 delegate processing method (Audio from iPod Library app), 287–288 deleting managed objects, 343 desktop server, building (network browser), 230–233 DetailViewController (Split Views app), 137 developers Developer Center, iPhone, 515–517 Development Provisioning Assistant, 545 iPhone Developer Program: Standard Program User Guide, 518 support center, 519 devices, profiling on, 454 dictionaryFromPropertyList method (Unit Test app), 441 disconnect method (Peer-to-Peer app), 254–255 displayFile method (Alert View app), 62–63 document window (Interface Builder), 513 done method (Audio from iPod Library app), 280–281 downloading iOS 4 SDK, 516
E editing menu (pasteboards), 397–398 element types, preference, 314 encodeWithCoder: method property lists and, 340 Property Lists application, 364 Tab Bar application, 48 entities, database, 342 error codes (NSNetService), 230 event action methods (gesture recognizer), 186–188
555
fetchedResultsController method (Core Data app) – iAds application
F fetchedResultsController method (Core Data app), 377–378 fetching managed objects, 343 filePath variable, 62 File’s Owner Inspector, 23, 31 flipTheImage method Action Sheet application (iPad), 91 Action Sheet application (iPhone/iPod Touch), 77
G gesture event processing methods, 185–187 Gesture Recognizer application, 174–197 awakeFromNib method, 184–185 development steps, 175–182 event action methods, 186–188 gesture event processing methods, 185–187 loadShakeSound method, 184–185 motion event processing methods, 189–190 overview, 174–175 SimpleGesturesView.h file (complete), 183–184 SimpleGesturesView.m file (complete), 190–197 SimpleTouchView.h modifications to template, 182–183 SimpleTouchView.m modifications to template, 184 @synthesize, addition of, 184 testing, 197 updateDisplayValuesWithPhase method, 184–185 gesture recognizer logic (Split Views app), 152–153 gestures and swipes, 161–162
556
Getting Started section (Developer Center), 517 GKPeerPickerControllerDelegate methods (Peer-to-Peer app), 253–254 GKSessionDelegate methods (Peer-to-Peer app), 252–253 GKVoiceChatClient., 244 GKVoiceChatClient/participantID delegate methods (Peer-to-Peer app), 254
H hideAdBanner/showAdBanner methods (iAd Network app), 477 hierarchies, preference, 315
I iAd Network ADBannerView, 470 iAds application, creating. See iAds application integrating iAd framework, 470–471 joining, 469–470 overview, 469 iAds application, 471–479 development steps, 472–475 hideAdBanner/showAdBanner methods, 477 iAdAppViewController.h file complete, 475 iAdAppViewController.m file complete, 477–479 iAdAppViewController.m modifications to template, 476 overview, 471–472 @synthesize, addition of, 476 testing, 479 viewDidLoad/loadAdView/canLoadiAd methods, 476
images – iTunes
images cutting/pasting. See Cutting/Pasting Images application imageWithData method, 414 UIImageView, 201 init method Memory Leak Test app, 465 Unit Test app, 438 initWithCoder: method property lists and, 340 Property Lists application, 364 Tab Bar application, 48–49 inspector window (Interface Builder), 514 Instruments application (Memory Leak Test app), 467 Instruments tool, 455 Interface Builder fundamentals, 512–514 Library, 18, 20–21, 32, 98 iOS 4 application performance on, 453–454 developer resources/guides, 544 SDK, resources for, 515–517 unit test bundle target, 429 iPad action sheets on. See Action Sheet application (iPad) alert view application for. See Alert View application creating custom menus for. See Custom Menus for iPad application modal view for. See Modal View application (iPad) navigation on. See navigation split view application for. See Split View application iPad Touch action sheets on. See Action Sheet application (iPhone/iPod Touch) alert view application for. See Alert View application
iPhone action sheets on. See Action Sheet application (iPhone/iPod Touch) alert view application for. See Alert View application Developer Center, 515–517 Developer Program, 266, 469, 518–520, 543–544 modal view for. See Modal View application (iPhone/iPod Touch) Provisional Portal, 518 Table views for. See Custom Table View application iPhone Client application, 237–244 development steps, 237 domain and service searches, 239 NSNetServiceBrowserDelegate methods, 240 NSNetServiceDelegate methods, 239 RootViewController.h file (complete), 238 RootViewController.h modifications to template, 238 RootViewController.m file (complete), 240–244 RootViewController.m modifications to template, 238 @synthesize, addition of, 238 testing, 244 iPod Touch iPodLibrary objects, 483 modal view for. See Modal View application (iPhone/iPod Touch) Table views for. See Custom Table View application iTunes App Store, 544 Connect, 520 iTunes Connect Developer Guide, 545
557
keyboardWillHide method (Notifications app) – Local Named Notification application
K keyboardWillHide method (Notifications app), 220 keyboardWillShow method (Notifications app), 200, 219–220
L launching applications, methods for (multitasking), 482 leaks, memory. See Memory Leak Test application libraries IOS 4 SDK Reference Library, 516 Library Inspector, 96 Library window (Interface Builder), 513 loadAdView method (iAd Network app), 476 loadImageFromName method (Action Sheet app), 90–91 loadShakeSound method gesture recognizer application, 184–185 touch handler application, 168–169 loadSound method (Peer-to-Peer app), 256 Local Keyboard Notification application, 211–225 development steps, 212–217 keyboardWillHide method, 220 keyboardWillShow method, 219–220 LocalNamedNotification-iPhone, 202 LocalNotificationKeyboard_ iPhoneAppDelegate.h modifications to template, 217 LocalNotificationKeyboard_ iPhoneAppDelegate.m modifications to template, 218 LocalNotificationKeyboard_ iPhoneViewController.h file (complete), 218
558
LocalNotificationKeyboard_ iPhoneViewController.m file (complete), 222–224 LocalNotificationKeyboard-iPhone, 212 next/previous/done methods, 221 overview, 211 registerToReceiveKeyboardNotifications method, 219 @synthesize, addition of, 218 testing, 225 textFieldShouldBeginEditing method, 220–221 textFieldShouldReturn method, 220–221 UIKeyboardWillShowNotification event, 200 viewDidAppear method, 219 viewDidLoad method, 219 viewDidUnload method, 221 Local Named Notification application, 201–211 applicationDidEnterBackground method, 207 applicationWillTerminate method, 207 buttonAction method, 209 development steps, 202–206 LocalNamedNotification_ iPhoneAppDelegate.m file (complete), 207–208 LocalNamedNotification_ iPhoneAppDelegate.m file modifications, 206 LocalNamedNotification_ iPhoneViewController .h file, 209 LocalNamedNotification_ iPhoneViewController .m file (complete), 210 LocalNamedNotification_ iPhoneViewController.m modifications to template, 209
local notifications – Modal View application (iPhone/iPod Touch)
overview, 201 setImageBackgroundBlue method, 207 setImageBackgroundRed method, 207 @synthesize, addition of, 209 testing, 211 viewDidLoad method, 210 local notifications, 200
M managed objects, 341–343 Media Item Picker delegate methods (Audio from iPod Library app), 279 Media Player framework, 263 mediaPickerDidCancel: method (Audio from iPod Library app), 288 mediaPicker:didPickMediaItems: method (Audio from iPod Library app), 288 Memory Leak Test application, 455–468 addDataToDictionary data source methods, 465 Build and Analyze from main menu, 467–468 DataItem.h file, 466–467 DataViewController.h file complete, 464 DataViewController.h modifications to template, 464 DataViewController.m file complete, 465–466 DataViewController.m file complete (corrected), 467–468 DataViewController.m modifications to template, 464 development steps, 459–464, 467–468 init method, 465 Instruments application, using, 467 overview, 455–459 @synthesize, addition of, 464
textFieldShouldReturn: delegate method, 465 menus creating custom for iPad. See Custom Menus for iPad application editing (pasteboards), 397 UIMenuController class, 395 message packet definition (Peer-to-Peer app), 261 MobileMe (Apple), 339 Modal View application (iPad), 106–119 actionSheet delegate method, 114 closeModalView method, 117–118 ModalView_iPadViewController implementation file, 115–116 ModalView_iPadViewController.h template code with modifications file, 113 ModalView_iPadViewController.m template, modifications to, 113 NewModalViewController implementation file, 118 NewModalViewController.h template code with modifications file, 116–117 NewModalViewController.m template, modifications to, 117 overview, 106–107 showModalView method, 113–114 steps for creating modal view, 108–112 testing, 119 viewDidLoad method, 117 Modal View application (iPhone/iPod Touch), 95–105 actionSheet delegate method, 101 closeModalView method, 104 ModalView_iPhoneViewController implementation file, 102–103 ModalView_iPhoneViewController.h template code with modifications, 100
559
Modal View application (iPhone/iPod Touch) (continued) – Navigation Bar application
Modal View application (iPhone/iPod Touch) (continued) NewModalViewController implementation file, 105 NewModalViewController.h template code with modifications file, 103 NewModalViewController.m template, modifications to, 104 overview, 95–96 showModalView method, 100–101 steps for creating modal view, 96–99 testing, 105 viewDidLoad method, 104 modal views modal presentation styles (iPad), 95 presenting/dismissing, 95 transition styles, 95 motion event processing methods gesture recognizer application, 189–190 touch handler application, 170–171 MPMediaPredicate class, 263 MPMoviePlayerController class, 265 multitasking (iOS 4) Audio Multitasking application. See Audio Multitasking application device support of, 483 overview, 481 responsibilities, 482–483 services, 481–482 UIApplication delegate messages, 482 Xcode Simulator multitasking limitations, 483 multi-touch events, 160
N named pasteboards, 396
560
navigation navigation-based application template (Xcode), 506, 508 overview, 1 stack based process, 2 tab bar. See Tab Bar application toolbar. See Toolbar application navigation bar configuring, 2 defined, 2 pushing/popping items, 3 UINavigationBarDelegate protocol, 2 Navigation Bar application, 3–17 DetailViewController.h modifications to template, 15 DetailViewController.m file (complete), 16–17 DetailViewController.m modifications to template, 15 development steps, 4 initialization of table view, 11 initialization of view, 5–6 overview, 3–4 RootDetailViewController.h file (complete), 10 RootDetailViewController.h modifications to template, 10 RootDetailViewController.m file (complete), 13–14 RootDetailViewController.m modifications to template, 10 RootViewController.h file, 5 RootViewController.m file (complete), 8–10 RootViewController.m modifications to template, 5 setText method, 15–16 @synthesize, addition of, 5, 11, 15 TableView cell display, 7, 12
Network Browser application – openFile:alertTitle: method (Alert View app)
TableView cell selected, 7, 12 TableView display definition, 6, 12 testing, 17 Network Browser application, 230–244 desktop server, building, 230–233 initialization of view/service start/stop, 234–235 iPhone client, creating. See iPhone Client application NSNetServiceDelegate methods, 235–236 overview, 230 ServerView.h file (complete), 234 ServerView.h Modifications to template, 233 ServerView.m file (complete), 236–237 @synthesize, addition of, 234 testing, 244 networks browser application. See Network Browser application communication over, 228–230 per-to-peer device communication. See Peer-to-Peer Device Communication application News and Announcements (Apple Developer Network), 520 next/previous/done methods (Notifications app), 221 nib files, 512–513 notification processing local keyboard notification application. See Local Keyboard Notification application local named notification application. See Local Named Notification application NSNotification concepts, 200–201 overview, 199–200 NSBundle class, 513 NSCoding protocol, 340, 526 NSDictionary, 341, 441
NSDictionary class, 523 NSEntityDescriptions, 342 NSFetchRequest., 343 NSFetchResultsController, 343 NSKeyArchiver object, 341 NSManagedObject class, 341 NSManagedObjectContext class, 341 NSManagedObjectModel class, 342 NSNetService class, 228 NSNetServiceBrowserDelegate methods (iPhone client), 240 NSNetServiceDelegate delegate methods (Network Browser app), 228, 239 NSNetServiceDelegate methods (Network Browser app), 235–236 NSNotification object, 199 NSNotificationCenter, 200 NSPersistentStoreCoordinator, 342 NSPredicate, 343 NSServiceBrowser class, 228 NSServicesErrorCodes, 230 NSSortDescriptor, 343 NSString, 103 numberOfSectionsInTableView method Audio Multitasking application, 492 Core Data application, 387 defined, 121 Property Lists application, 354, 360 Split View application, 148 Tab Bar application, 43–44
O OpenAL framework, 264 openFile:alertTitle: method (Alert View app), 63
561
OpenGL ES application template (Xcode) – Playing Audio from iPod Library application
OpenGL ES application template (Xcode), 506, 508–509
P participantID delegate method (Peer-to-Peer app), 254 pasteboards basic concepts, 396 cutting selections, 397–398 Cutting/Pasting ext application. See Cutting/Pasting Text application Cutting/Pasting Images application. See Cutting/Pasting Images application editing menu, 397–398 named, 396 overview, 395–396 pasting items, 398 persistence and, 396–397 UIPasteboard class, 395–396 pasting items, 398 Peer-to-Peer Device Communication application, 244–262 browseForPeers method, 254–255 development steps, 245–250 disconnect method, 254–255 GKPeerPickerControllerDelegate methods, 253–254 GKSessionDelegate methods, 252–253 GKVoiceChatClient method, 254 initialization of view controller/search for peers/view unload, 252 loadSound method, 256 message packet definition, 261 MessagePacket.h file (complete), 261 MessagePacket.m file (complete), 262 overview, 244–245
562
participantID delegate method, 254 peer picker object, 228 PeerCommunication_ iPhoneViewController.h file (complete), 251 PeerCommunication_ iPhoneViewController.h modifications to template, 250 PeerCommunication_ iPhoneViewController.m file (complete), 256–261 PeerCommunication_ iPhoneViewController.m modifications to template, 251 receiveData method, 255–256 sendMessage method, 254–255 sendPacket method, 255–256 @synthesize, addition of, 251–252 testing, 262 performance tuning/optimization Memory Leak Test application. See Memory Leak Test application overview, 453 profiling, 454–455 persistence pasteboard persistent applications, 396–397 persistent store coordinator (Core Data), 342 pickerView data source methods (Unit Test app), 438–439 pickerView:didSelectRow:inComponent: delegate method (Unit Test app), 439 placeImageOnPasteboard method Custom Menus for iPad application, 424–425 Cutting/Pasting Images application, 414 Playing Audio from iPod Library application, 265–291 action methods, 278–279, 288
Playing Video from iPod Library application – popoverControllerDidDismissPopover method
delegate processing method, 287–288 development steps, 266–274 done method, 280–281 iPodLibraryPlayerViewController.h file (complete), 275–276 iPodLibraryPlayerViewController.h modifications to template, 275 iPodLibraryPlayerViewController.m file (complete), 281–286 iPodLibraryPlayerViewController.m modifications to template, 276 Media Item Picker delegate methods, 279 MediaItemsViewController.h file (complete), 286 MediaItemsViewController.h modifications to template, 286 MediaItemsViewController.m file (complete), 289–290 MediaItemsViewController.m modifications to template, 287 mediaPickerDidCancel: method, 288 mediaPicker:didPickMediaItems: method, 288 overview, 265–266 refreshAlbumInfo method, 280–281 registerForNotifications method, 277–278 @synthesize, addition of, 276, 287 table view data source method, 287–288 testing, 291 unregisterForNotifications method, 277–278 updateMediaCollection method, 279–280 updatePlayerDisplay method, 280–281 viewDidLoad method, 276–277 Playing Video from iPod Library application, 291–312 action methods, 307 development steps, 292–298 iPodLibrary.h file (complete), 311
iPodLibrary.h modifications to template, 311 iPodLibrary.m file (complete), 311–312 iPodLibrary.m modifications to template, 311 iPodLibraryMoviePlayerAppDelegate.m file (complete), 299–300 iPodLibraryMoviePlayerAppDelegate.m modifications to template, 299 iPodLibraryMoviePlayerViewController.h file (complete), 301 iPodLibraryMoviePlayerViewController.h modifications to template, 300 iPodLibraryMoviePlayerViewController.m file (complete), 303–305 iPodLibraryMoviePlayerViewController.m modifications to template, 301 MediaPlaybackViewController.h file (complete), 305–306 MediaPlaybackViewController.h modifications to template, 305 MediaPlaybackViewController.m file (complete), 308–310 MediaPlaybackViewController.m modifications to template, 306 MediaPlaybackView.h file (complete), 310 MediaPlaybackView.h modifications to template, 310 MediaPlaybackView.m file (complete), 310 overview, 291–292 @synthesize, addition of, 301, 306 table view data source methods, 302 tableView:didSelectRowAtIndexPath method, 302–303 testing, 312 view initialization methods, 306–307 viewDidLoad method, 301–302 .plist extension, 339. See also property lists popoverControllerDidDismissPopover method, 138
563
popoverControllerShouldDismissPopover method – Property Lists application
popoverControllerShouldDismissPopover method, 138 portrait orientation (iPad), 139 posting local notifications, 200 postNotification method, 200 preferences application. See application preferences applications, accessing, 315 configuring iAd, 470 presenting/dismissing modal views, 95 private pasteboards, 396 profiles, provisioning, 446 profiling, 454–455 project window (Xcode project builder), 508–511 properties, navigation bar, 2 property lists application. See Property Lists application basics, 339–341 PropertyList class (Tab Bar app), 49–50 PropertyList.h category interface (Unit Test app), 439–440 Property Lists application, 343–367 addPerson method, 353 cancel delegate method, 353 development steps, 344–347 encodeWithCoder/initWithCoder methods, 364 numberOfSectionsInTableView method, 354, 360 overview, 343–344 PersonAddViewController.h file (complete), 348 PersonAddViewController.h modifications to template, 348 PersonAddViewController.m file (complete), 350–351
564
PersonAddViewController.m modifications to template, 348 PersonDetailViewController.h file (complete), 359 PersonDetailViewController.h modifications to template, 359 PersonDetailViewController.m file (complete), 361–363 PersonDetailViewController.m modifications to template, 359 Person.h file (complete), 364 Person.h modifications to template, 363–364 Person.m file (complete), 365 PropertyList.h file (complete), 366 PropertyList.h/PropertyList.m template modifications, 365 PropertyList.m file (complete), 366 RootViewController.h file (complete), 351–352 RootViewController.h modifications to template, 351 RootViewController.m file (complete), 356–359 RootViewController.m modifications to template, 352 save and cancel methods, 349–350 savePerson method, 353 @synthesize, addition of, 348, 352, 359, 364 tableView:cellForRowAtIndexPath method, 354, 361 tableView:commitEditingStyle:forRowAtInd exPath method, 355 tableView:didSelectRowAtIndexPath method, 355 tableView:numberOfRowsInSection method, 354, 360
provisioning profiles – runChoice action method (Alert View app)
tableView:titleForHeaderInSection method, 360 testing, 367 textFieldShouldReturn method, 349 viewDidLoad method, 349, 352, 359–360 provisioning profiles, 446 public pasteboards, 396 pushing/popping items (Navigation Bar app), 3
R readFromArchive method (Custom Table View app), 131 readFromPasteboard method (Cutting/Pasting Text app), 405 receiveData method (Peer-to-Peer app), 255–256 recognizers, gesture, 161–162 Reference Library, IOS 4 SDK, 516 refreshAlbumInfo method (Audio from iPod Library app), 280–281 registering devices (Unit Test app), 451 for local notifications, 200 registerForNotifications method (Audio from iPod Library app), 277–278 registerToReceiveKeyboardNotifications method (Notifications app), 219 remoteControlReceivedWithEvent method (Audio Multitasking app), 493–494 replaceTrack: method (Audio Multitasking app), 491 Required background modes, 481, 503 resources for iOS 4 SDK, 515–517 restoreView method (Cutting/Pasting Text app), 424–425
Root View Controller defined, 2–3 hiding/showing (Split Views app), 152 RootViewController (Split Views app), 137 RootViewController.m dealloc method (Action Sheet app), 86 RootViewController.m viewdealloc method (Action Sheet app), 73 RootViewController.m tableView:cellForRow AtIndexPath: method Action Sheet application (iPad), 86 Action Sheet application (iPhone/iPod Touch), 72 RootViewController.m tableView:didSelectRo wAtIndexPath: method Action Sheet application (iPad), 86 Action Sheet application (iPhone/iPod Touch), 72–73 RootViewController.m tableView:numberOfRowsInSection method Action Sheet application (iPad), 85 Action Sheet application (iPhone/iPod Touch), 72 RootViewController.m viewDidLoad method Action Sheet application (iPad), 85 Action Sheet application (iPhone/iPod Touch), 71 RootViewController.m viewDidUnload method Action Sheet application (iPad), 86 Action Sheet application (iPhone/iPod Touch), 73 rotateView method (Toolbar app), 24 RSS feed subscription (Apple developers), 520 runChoice action method (Alert View app), 63
565
Sample Code section (iOS 4 Reference Library) – Split View application
S Sample Code section (iOS 4 Reference Library), 517 save and cancel methods Core Data application, 372 Property Lists application, 349–350 savePerson method Core Data application, 376 Property Lists application, 353 saving property lists, 340–341 searchForBrowsableDomains method, 228 searchForServicesOfType:inDomain: method, 229 selections, cutting (pasteboards), 397–398 sendMessage method (Peer-to-Peer app), 254–255 sendPacket method (Peer-to-Peer app), 255–256 services, multitasking (iOS 4), 481–482 setDetailItem: method (Split Views app), 151–152 setHighlighted:animated: method (Cutting/ Pasting Text app), 402 setImageBackgroundBlue method (Notifications app), 207 setImageBackgroundRed method (Notifications app), 207 setText method (Navigation Bar app), 15–16 Setting Preferences application, 315–325 AppPreferences.h file (complete), 321 AppPreferences.m file (complete), 321–323 development steps, 315–319 overview, 315 RootViewController.h file (complete), 323 RootViewController.m file (complete), 323–325 setting title of view with contact preference, 315
566
source code overview, 320–321 testing, 325 viewDidLoad method, 323 setVisiblePage: method (Split Views app), 153–154 showAlert:buttons:alertTitle: method (Alert View app), 64 showMenu: method Custom Menus for iPad application, 423 Cutting/Pasting Images application, 413 Cutting/Pasting Text application, 405 showModalView method modal view application (iPad), 113–114 modal view application (iPhone/iPod Touch), 100–101 Simulator Build and Run in (Xcode project builder), 512 profiling in, 454 Xcode Simulator multitasking limitations, 483 single touch events, 160 Split View application, 138–158 DataSource.plist property list file, creating, 145–146 DetailViewController.h modifications to template, 151 DetailViewController.m file, 143–144 DetailViewController.m file (complete), 154–157 DetailViewController.m modifications to template, 151 development steps for iPad, 141–144 gesture recognizer logic, 152–153 numberOfSectionsInTableView method, 148 overview, 138–141 PropertyList.h file (complete), 157 PropertyList.m file (complete), 157–158
split views – Tab Bar application
Root View Controller, hiding/showing, 152 RootViewController.h file, 147 RootViewController.m file (complete), 149–150 RootViewController.m modifications to template, 147 setDetailItem: method, 151–152 setVisiblePage: method, 153–154 split views, defined, 137 @synthesize, addition of, 147, 151 tableView:cellForRowAtIndexPath: method, 148–149 tableView:didSelectRowAtIndexPath method, 149 tableView:numberOfRowsInSection: method, 148 testing, 158 UIPopoverControllerDelegate protocol, 138 UISplitViewControllerDelegate protocol, 138 viewDidLoad method, 147–148, 151 split views split view-based application template (Xcode), 506, 508–509 UISplitView (iPad), 62, 85, 90 UISplitViewController class, 137 UISplitViewControllerDelegate protocol, 138 stack, navigation, 2 static libraries DataSource Static Library application. See DataSource Static Library overview, 1 Xcode application project. See Xcode application project Xcode project template for creating, 521–527 support center, developers, 519
swipes and gestures, 161–162 switching applications, methods for (multitasking), 482 System Sound Services, 264
T Tab Bar application, 26–51 customizing tab bars, 26 development steps, 27–30 encodeWithCoder method, 48 FirstViewController.h file (complete), 37 FirstViewController.h modifications to template, 37 FirstViewController.m file (complete), 38 FirstViewController.m Modifications to template, 37 initWithCoder method, 48–49 numberOfSectionsInTableView method, 43–44 overview, 26–27 PropertyList.h file (complete), 49–50 PropertyList.h modifications to template, 49 PropertyList.m file (complete), 50–51 PropertyList.m modifications to template, 50 saveTransaction method, 40–41 SecondViewController.h file (complete), 39 SecondViewController.h modifications to template, 39 SecondViewController.m file (complete), 41–42 SecondViewController.m modifications to template, 39 @synthesize, addition of, 37, 39–40, 43, 48, 50 tableView:cellForRowAtIndexPath method, 44–45
567
Tab Bar application (continued) – TableView:numberOfRowsInSection: method
Tab Bar application (continued) tableView:numberOfRowsInSection method, 43–44 tableView:titleForHeaderInSection method, 44 testing, 51 textFieldShouldReturn method, 41 ThirdViewController.h file (complete), 42 ThirdViewController.h modifications to template, 42 ThirdViewController.m file (complete), 45–47 ThirdViewController.m modifications to template, 42 Transaction.h file (complete), 48 Transaction.h modifications to template, 48 Transaction.m file (complete), 49 Transaction.m modifications to template, 48 UITabBarDelegate protocol, 26 view controllers, designing, 30–36 viewWillAppear method, 37–38, 40, 43 tab bars Tab Bar-based application template (Xcode), 506, 510 UITabBarDelegate protocol, 26 Table view data source methods Playing Audio from iPod Library app, 287–288 Playing Video from iPod Library app, 302 Table views cells, 122 custom application. See Custom Table View application overview, 121–122 Table view delegate methods (Audio Multitasking app), 493
568
TableView cell display (Navigation Bar app), 7, 12 TableView cell selected (Navigation Bar app), 7, 12 TableView delegate methods, 129 TableView display definition (Navigation Bar app), 6, 12 TableView:cellForRowAtIndex: method (Audio Multitasking app), 492–493 TableView:commitEditingStyle:forRowAt IndexPath method (Property Lists app), 355 UITableView, 325 UITableViewDataSource protocol, 122 UITableViewDelegate protocol, 122 TableView:cellForRowAtIndexPath: method Core Data application, 378–379, 387–388 Custom Table View application, 128 Cutting/Pasting Text application, 404 Property Lists application, 354, 361 Split View application, 148–149 Tab Bar application, 44–45 TableView:didSelectRowAtIndexPath: method Audio from iPod Library application, 302–303 Core Data application, 379 Cutting/Pasting Text application, 405 Property Lists application, 355 Split View application, 149 TableView:numberOfRowsInSection: method Audio Multitasking application, 492 Core Data application, 377, 387 Cutting/Pasting Text application, 401 defined, 121 Property Lists application, 354, 360 Split View application, 148
TableView:numberOfRowsInSection method (Tab Bar app) – Toolbar application
TableView:numberOfRowsInSection method (Tab Bar app), 43–44 TableView:titleForHeaderInSection method Core Data application, 387 Property Lists application, 360 Tab Bar app, 44 tap events, 160–161 templates, application (Xcode project builder), 506–511 testing Action Sheet application (iPad), 93–94 Action Sheet application (iPhone/iPod Touch), 80 Alert View application, 66 Audio Multitasking application, 503 Child Pane Preference Hierarchy application, 338 Core Data application, 394 Custom Menus for iPad application, 427 Custom Table view application, 135 Cutting/Pasting Images application, 416 Cutting/Pasting Text application, 407 Gesture Recognizer application, 197 iAds application, 479 iPhone client, 244 Local Keyboard Notification application, 225 Local Named Notification application, 211 Memory Leak Test application. See Memory Leak Test application Modal View application (iPad), 119 Modal View application (iPhone/iPod Touch), 105 Navigation Bar application, 17 Peer-to-Peer Device Communication application, 262
Playing Audio from iPod Library (application), 291 Playing Video from iPod Library (application), 312 Property Lists application, 367 Setting Preferences application, 325 Split View application, 158 Tab Bar application, 51 test case methods, 430 Toolbar application, 26 Touch Handler application, 174 unit testing. See unit testing Xcode application project, 542 text cutting/pasting, 398. See also Cutting/ Pasting Images application textFieldShouldBeginEditing method (Notifications app), 220–221 textFieldShouldReturn: delegate method (Memory Leak Test app), 465 UITextFields, 211 UITextView, 54–59 textFieldShouldReturn method Core Data application, 371–372 Local Keyboard Notification application, 220–221 Property Lists application, 349 Tab Bar application, 41 Toolbar application, 17–26 development steps, 18–23 overview, 17–18 rotateView method, 24 SimpleToolbar_iPhoneViewController.h file (complete), 23 SimpleToolbar_iPhoneViewController.h modifications to template, 23
569
Toolbar application (continued) – UITouch sequence
Toolbar application (continued) SimpleToolbar_iPhoneViewController.m file (complete), 25–26 SimpleToolbar_iPhoneViewController.m modifications to Template, 24 @synthesize, addition of, 24 testing, 26 viewDidLoad method, 24 touch events. See also Touch Handler application multi-touch, 160 overview, 159–160 processing methods, 169–170 single touch, 160 swipes and gestures, 161–162 taps, 160–161 UITouch object, 159 UITouch sequence, 159 Touch Handler application, 162–174 awakeFromNib method, 168–169 development steps, 163–167 loadShakeSound method, 168–169 motion event processing methods, 170–171 overview, 162 SimpleTouchView.h file (complete), 167–168 SimpleTouchView.h modifications to template, 167 SimpleTouchView.m file (complete), 171–173 SimpleTouchView.m modifications to template, 168 @synthesize, addition of, 168 testing, 174 touch event processing methods, 169–170 updateDisplayValuesWithPhase method, 168–169
570
touchesBegan:withEvent: method Custom Menus for iPad application, 423 Cutting/Pasting Images application, 413 transition styles (Modal View app), 95
U UIActionSheetDelegate protocol, 67 UIAlertViewDelegate protocol (alerts), 54 UIApplication delegate messages (Audio Multitasking app), 482 UIBarButtonItem, 18 UIGestureRecognizer class, 175 UIImageView, 201 UIKeyboardWillShowNotification event, 200 UIKit framework, 395, 512 UIMenuController class, 395 UINavigationBarDelegate protocol, 2 UIPasteboard class, 395–396 UIPickerView, 430 UIPopoverControllerDelegate protocol (Split Views app), 138 UIResponder class, 395 UIResponderStandardEditActions class, 395 UISplitView (iPad), 62, 85, 90 UISplitViewController class, 137 UISplitViewControllerDelegate protocol, 138 UITabBarDelegate protocol, 26 UITableView, 325 UITableViewDataSource protocol, 122 UITableViewDelegate protocol, 122 UITextFields, 211 UITextView, 54–59 UITouch object, 159 UITouch sequence, 159
UIWindow class reference – view-based application template (Xcode)
UIWindow class reference, 199 unit testing application, steps for, 450–451 application-testing targets, 429–430 executing unit test, 452 registering device, 451 setting up environment for, 429 Unit Test application. See Unit Test application unit test bundle, 430 unit test cases, creating. See unit test cases Unit Test application, 430–445 Data.plist data source, creating, 442–444 Data.plist file (complete), 445 development steps, 431–435 init method, 438 overview, 430 pickerView data source methods, 438–439 PickerViewController class, connecting to UnitTestsAppDelegate, 437 PickerViewController.h file (complete), 437 PickerViewController.m modifications to template, 437 PickerViewControllerm.m file (complete), 439–440 pickerView:didSelectRow:inComponent: delegate method, 439 PropertyList.h category interface, 439–440 PropertyList.h file (complete), 441 PropertyList.m file (complete), 441 @synthesize, addition of, 438 testing, 445 UnitTestsAppDelegate.h file (complete), 435–436 UnitTestsAppDelegate.m file (complete), 436–437
unit test cases AppTests.h file (complete), 449 AppTests.m file (complete), 449–450 steps for creating, 446–449 unregisterForNotifications method (Audio from iPod Library app), 277–278 unregistering observers (Notifications app), 201 updateDisplayValuesWithPhase method Gesture Recognizer application, 184–185 Touch Handler application, 168–169 updateMediaCollection method (Audio from iPod Library app), 279–280 updatePlayerDisplay method (Audio from iPod Library app), 280–281 User Interface Guidelines (Apple), 54 utility-based application template (Xcode), 506, 510–511
V video frameworks for, 265 Playing from iPod Library application. See Playing Video from iPod Library application view controllers designing (Tab Bar app), 30–36 presenting modally, 95 root, 2–3 view initialization methods Action Sheet application (iPad), 90 Audio from iPod Library application, 306–307 view-based application template (Xcode), 506, 510–511
571
viewDidAppear method (Notifications app)– Xcode application project
viewDidAppear method (Notifications app), 219 viewDidLoad method Action Sheet application (iPhone/iPod Touch), 75–76 Audio Multitasking application, 490–491, 499 Child Pane Preference Hierarchy application, 336 Core Data application, 371 Cutting/Pasting Text application, 401 iAds application, 476 Local Keyboard Notification application, 219 Modal View application (iPad), 117 Modal View application (iPhone/iPod Touch), 104 Notifications application, 210 Playing Audio from iPod Library application, 276–277 Playing Video from iPod Library (application), 301–302 Property Lists application, 349, 352, 359–360 Setting Preferences application, 323 Split View application, 147–148, 151 Toolbar application, 24 viewDidLoad/viewWillAppear methods (Core Data app), 375 viewDidUnload method (Notifications app), 221 views, modal. See Modal View application (iPad) viewWillAppear method Audio Multitasking application, 499 Tab Bar application, 37–38, 40, 43
572
W web sites, for further information App Store resource center, 519–520 Apple Developer Network, 520 application distribution procedures, 545 iAd Network, 469 iPhone developer program, 543–544 iPhone Developer Program: Standard Program User Guide, 518 iTunes App Store, 544 iTunes Connect, 520 iTunes Connect Developer Guide, 545 OpenAL framework, 264 registering devices, 451 RSS feed subscription (Apple developers), 520 window-based application template (Xcode), 506, 510 Worldwide Developer Conference videos, 517
X Xcode defined, 506 modeling tool, 342–343 project template (static libraries), 521–527 Xcode Simulator multitasking limitations, 483 Xcode application project, 527–542 adding DataSource library, 528–530 adding library headers, 530 adding source code using library classes, 535–542 creating new application, 528 designing user interface, 530–534 overview, 527–528
Xcode project builder – XIB files
PersonAddViewController.h file (complete), 538 PersonAddViewController.m file (complete), 538–539 PersonDetailViewController.h file (complete), 540 PersonDetailViewController.m file (complete), 540–542 RootVewController.m file (complete), 535–538
RootViewController.h file (complete), 535–542 testing, 542 Xcode project builder available application types, 506–507 Build and Run in Simulator, 512 overview, 505–506 project window, 508–511 XIB files, 513
573