Learning Core Data for iOS with Swift: A Hands-On Guide to Building Core Data Applications [Kindle Edition]

2,163 174 37MB

English Pages 480 [806] Year 2015

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Learning Core Data for iOS with Swift: A Hands-On Guide to Building Core Data Applications [Kindle Edition]

Citation preview

About This E-Book EPUB is an open, industry-standard format for e-books. However, support for EPUB and its many features varies across reading devices and applications. Use your device or app settings to customize the presentation to your liking. Settings that you can customize often include font, font size, single or double column, landscape or portrait mode, and figures that you can click or tap to enlarge. For additional information about the settings and features on your reading device or app, visit the device manufacturer’s Web site. Many titles include programming code or configuration examples. To optimize the presentation of these elements, view the e-book in single-column, landscape mode and adjust the font size to the smallest setting. In addition to presenting code and configurations in the reflowable text format, we have included images of the code that mimic the presentation found in the print book; therefore, where the reflowable format may compromise the presentation of the code listing, you will see a “Click here to view code image” link. Click the link to view the print-fidelity code image. To return to the previous page viewed, click the Back button on your device or app.

Learning Core Data for iOS with Swift Tim Roadley Boston • Columbus • Indianapolis • New York • San Francisco • Amsterdam • Cape Town Dubai • London • Madrid • Milan • Munich • Paris • Montreal • Toronto • Delhi Mexico City • São Paulo • Sidney • Hong Kong • Seoul • Singapore • Taipei • Tokyo

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals. Learning Core Data for iOS with Swift is an independent publication and has not been authorized, sponsored, or otherwise approved by Apple Inc. AirPlay, AirPort, AirPrint, AirTunes, App Store, Apple, the Apple logo, Apple TV, Aqua, Bonjour, the Bonjour logo, Cocoa, Cocoa Touch, Cover Flow, Dashcode, Finder, FireWire, iMac, Instruments, Interface Builder, iOS, iPad, iPhone, iPod, iPod touch, iTunes, the iTunes logo, Leopard, Mac, Mac logo, Macintosh, Multi-Touch, Objective-C, Quartz, QuickTime, QuickTime logo, Safari, Snow Leopard, Spotlight, and Xcode are trademarks of Apple, Inc., registered in the United States and other countries. OpenGL and the logo are registered trademarks of Silicon Graphics, Inc. The YouTube logo is a trademark of Google, Inc. Intel, Intel Core, and Xeon are trademarks of Intel Corp. in the United States and other countries. The author and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein. The publisher offers excellent discounts on this book when ordered in quantity for bulk purchases or special sales, which may include electronic versions and/or custom covers and content particular to your business, training goals, marketing focus, and branding interests. For more information, please contact: U.S. Corporate and Government Sales (800) 382-3419 [email protected] For sales outside the United States, please contact: International Sales [email protected] Library of Congress Control Number: 2013946325 Visit us on the Web: informit.com/aw Copyright © 2016 Pearson Education, Inc. All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. To obtain permission to use material from this work, please submit a written request to Pearson Education, Inc., Permissions Department, One Lake Street, Upper Saddle River, New Jersey 07458, or you may fax your request to (201) 236-3290. ISBN-13: 978-0-134-12003-5 ISBN-10: 0-134-12003-4

Text printed in the United States on recycled paper at RR Donnelley in Crawfordsville, Indiana. First printing: December 2015 Editor-in-Chief Mark Taub Senior Acquisitions Editor Trina MacDonald Senior Development Editor Chris Zahn Development Editor Michael Thurston Managing Editor Kristy Hart Project Editor Elaine Wiley Copy Editor Geneil Breeze Proofreader Leslie Joseph Technical Reviewers Carl Brown Niklas Saers Ash Furrow Publishing Coordinator Olivia Basegio Cover Designer Chuti Prasertsith Compositor Nonie Ratcliff

The more I learn, the more I learn I need to learn more. I dedicate this book to my wonderful wife, Tracey, who has given up many nights and weekends with me to help make this book a reality. Finally, we can sit back and relax together again! I’m sure Tyler & Taliah will let us….

Contents at a Glance Preface 1 Your First Core Data Application 2 Managed Object Model Basics 3 Managed Object Model Migration 4 Managed Object Model Expansion 5 Table Views 6 Views 7 Picker Views 8 Preloading Data 9 Deep Copy 10 Performance 11 Background Processing 12 Search 13 iCloud 14 Taming iCloud 15 CloudKit Sync: Uploading Objects 16 CloudKit Sync: Downloading Changes and Handling Deletions A Preparing the Groceries Application B Finalizing the Groceries Application

Table of Contents Preface 1 Your First Core Data Application What Is Core Data? Persistent Store Coordinator Managed Object Model Managed Object Context When to Use Core Data Introducing the Groceries Application Adding Core Data to an Existing Application Implementing the Core Data Helper Adding Paths Adding the Core Data Stack Adding the Setup Section Adding the Saving Section Adding a Managed Object Model File Summary Exercises 2 Managed Object Model Basics What Is a Managed Object Model? Entities Attributes Integer 16/32/64 Float and Double Decimal String Boolean Date Binary Data Transformable Attribute Settings

Subclassing NSManagedObject Scalar Properties for Primitive Data Types Introducing the Demo Function Creating a Managed Object Backend SQL Visibility Fetching Managed Objects Sorting Fetch Requests Filtering Fetch Requests Fetch Request Templates Deleting Managed Objects Summary Exercises 3 Managed Object Model Migration Changing a Managed Object Model Adding a Model Version Lightweight Migration Default Migration Migration Manager Introducing MigrationVC.swift (Migration View Controller Code) Introducing CDMigration.swift (Core Data Migration Code) Summary Exercises 4 Managed Object Model Expansion Relationships Delete Rules Introducing CDOperation.swift (Core Data Operation) Delete Rule Testing Entity Inheritance Summary Exercises 5 Table Views Table View Basics

Core Data Table Views Introducing CDTableViewController Expanding CDTableViewController DATASOURCE: UITableView DELEGATE: NSFetchedResultsController CDTableViewController Subclasses Enhancing PrepareTVC Preparing Test Data Deletion and Cell Selection for PrepareTVC Interaction for PrepareTVC Introducing ShopTVC Cell Selection for ShopTVC INTERACTION for ShopTVC Summary Exercises 6 Views View Basics The Target View Hierarchy Introducing ItemVC Keeping Reference to a Selected Item Passing a Selected Item to ItemVC Configuring the Scroll View and Text Fields Implementing ItemVC DELEGATE: UITextField The VIEW Section Adding Functionality to NSManagedObject Subclasses Units, Home Locations, and Shop Locations Adding and Editing Units Implementing UnitsTVC Implementing UnitVC Segueing from UnitsTVC to UnitVC Adding and Editing Home or Shop Locations

Configuring the Home Location Views Configuring the Shop Location Views Summary Exercises 7 Picker Views Picker View Basics Introducing CDPickerTextField Introducing UnitPickerTF Creating the Unit Picker Connecting the Unit Picker Configuring ItemVC for the Unit Picker Introducing LocationAtHomePickerTF Introducing LocationAtShopPickerTF Using the Location Pickers Connecting the Location Pickers Configuring ItemVC for the Location Pickers Picker-Avoiding Text Field Summary Exercises 8 Preloading Data Including Default Data Indicating Whether an Import Is Required Importing from XML Creating an Import Context Preventing Duplicate Default Data Triggering a Default Data Import Finding or Creating Managed Objects Mapping XML Data to Entity Attributes Importing from a Persistent Store Using the Default Data Store as the Initial Store Summary Exercises

9 Deep Copy The Deep Copy Process Configuring a Source Stack Configuring the Source Coordinator Configuring the Source Context Configuring the Source Store Enhancing CDImporter Identifying Unique Attributes Object Info Copying a Unique Object Establishing a To-One Relationship Establishing a To-Many Relationship Establishing an Ordered To-Many Relationship Copying Relationships Deep Copy Entities Triggering a Deep Copy Summary Exercises 10 Performance Identifying Performance Issues Implementing the Camera Implementing the Image Picker Controller Delegates Generating Test Data Merge Policies Measuring Performance with SQLDebug Measuring Performance with Instruments Improving Performance Model Optimization Handling Large Objects Cleaning Up Summary Exercises

11 Background Processing Implementing Background Save Configuring an Import Context Parent Faulting Objects Generating Thumbnails Summary Exercises 12 Search Updating CDTableViewController Updating PrepareTVC Search Optimization Summary Exercises 13 iCloud iCloud Basics Enabling iCloud Updating CDHelper for iCloud Adding an iCloud Store Handling iCloud Notifications The Debug Navigator Summary Exercises 14 Taming iCloud De-Duplication Identifying Duplicates Deleting Duplicates Triggering De-Duplication Seeding Preparing Seed Variables Adding Seed Helper Functions Developing with a Clean Slate Configurations

Summary Exercises 15 CloudKit Sync: Uploading Objects Introducing CloudKit CloudKit Database Synchronization Limitations Introducing CDCloudSync Public Data Groups Cache Status CloudKit Building Blocks Uploading New Records and Relationships Adding Synchronization Logic Preparing the Managed Object Model for Sync Summary Exercises 16 CloudKit Sync: Downloading Changes and Handling Deletions CloudKit Building Blocks (Continued) Change Synchronization Deletion Synchronization Quality Assurance Testing the Network Updating Synchronization Logic Adding CDCloudSync to Your Own Applications Summary Exercises Thank You! A Preparing the Groceries Application New Xcode Project Storyboard Design App Icons and Launch Images Introducing GenericVC B Finalizing the Groceries Application New Features

Photo Library and Photo Deletion Support Favorites Icon Badge Location De-Duplication Logic

Acknowledgments A resounding thank-you first goes out to Trina MacDonald for giving me the opportunity to write this book originally and again for Swift. Her guidance throughout the whole process has been invaluable, as has the assistance of Michael Thurston and the technical reviews from Ash Furrow, Carl Brown, and Niklas Saers. You guys saved this book from a few bugs that crept through on those late nights and also provided some great insight and coding technique suggestions with Swift.

About the Author Tim Roadley is a family man with strong technical focus in career and personal goals. From managing IT infrastructure to developing apps and writing books, if it’s complex, he’s in his element. He currently works for TKH Group, primarily focused on system integration software used by large clients such as Westfield. Prior roles include implementations of various IT systems at government departments and major banks, most notably the implementation of a payments switch that drives the RediATM network in Australia. In his downtime, he enjoys spending time with his wonderful wife, Tracey (when he can pull her away from her Thermomix), and two lovely children, Tyler and Taliah.

Preface Every day, millions of Apple devices run applications, or apps, that rely on Core Data. This has led to a mature, stable, and incredibly fast platform for apps to access their data. Core Data itself is not a database. In fact, Core Data is a framework that, among other things, automates how you interact with a database. Instead of writing SQL code, you work with managed objects. All the associated SQL you would otherwise have to write yourself is generated automatically. This leaves you with all the benefits of a relational database without the headache of writing, testing, and optimizing SQL queries within your code. The SQL code generated automatically “under the hood” is the product of years of refinement and optimization by Apple’s masterful engineers. Using Core Data not only speeds up your own application development time, it also significantly reduces the amount of code you have to write. Here are some notable features of Core Data: Change management (undo and redo) Relationships Data model versioning and migration Efficient fetching (through batching and faulting) Efficient filtering (through predicates) Data consistency and validation With this book, you are introduced to Core Data features and best practices. As you progress through the chapters, you also build a fully functional Core Data iPhone app from scratch. Each key piece of information is explained in succinct detail so you can apply what you’ve learned straight away. The sample application built throughout this book has been especially designed to demonstrate as many aspects of Core Data as possible. At the same time it is a completely real-world application available on the App Store today. This should make it easier to absorb concepts as you relate them to real-life scenarios. The arrival of iOS 9 offers major improvements in the speed, reliability, and simplicity of Core Data integration with iCloud and introduction of CloudKit. I encourage anyone who has previously given up on this technology to give it another go, because you will be pleasantly surprised. If you have feedback, bug fixes, corrections, or anything else you would like to contribute to a future edition, please contact me at [email protected]. Finally, thank you for taking an interest in this book. If you like it, please tweet or post to Facebook about it! I have put a lot of effort into meticulously crafting it, so I truly hope it helps you on your way to mastering this brilliant technology. —Tim Roadley (@TimRoadley), September 2015

Who Is This Book For? This book is aimed at Swift programmers who want to learn how to efficiently manage data in their iOS apps. Prior experience with databases may help you pick up some topics faster, yet is not essential knowledge. As old habits die hard, some SQL programmers may find it more difficult to wrap their heads around some topics. Beyond the basics, the closing chapters explain how to build a CloudKit solution for sharing data with multiple users. This is something previously impossible with old iCloud technology. Whatever your scenario, don’t worry. Every step is explained and demonstrated clearly.

What You’ll Need As a Swift programmer, it is expected that you already have a reasonably modern Mac or MacBook running Xcode 7 or above. You should also be familiar with Xcode and have an iOS device to test with. This is particularly true once you reach Chapter 10, “Performance,” which is all about device performance. Swift is new to everyone, so it’s likely you’ll encounter code and techniques you haven’t seen before. Fortunately, you’ll be guided step-by-step through the entire process of creating a real-world Swift Core Data application for iOS, so your experience with Swift will grow throughout. The following resources are suggested as an accompaniment to this book: Swift, by Apple (search iBooks Store within the iBooks App) Learning Swift Programming, by Jacob Schatz (search amazon.com)

How This Book Is Organized This book takes you through the entire process of building the Groceries application, which is available from the App Store today. The Groceries implementation throughout the book demonstrates Core Data integration with iCloud and CloudKit. Each chapter in this book builds on the last, so you’re introduced to topics in the order you need to implement them. Along the way you build helper classes that simplify redeployment of what you’ve learned into your own applications. In fact, the exercises at the end of the iCloud and CloudKit chapters guide you through this redeployment of the helper classes into a brand new app. Using this as an example you can integrate Core Data, iCloud, and CloudKit into your own applications in no time! Here’s a brief summary of what you find in each chapter: Chapter 1, “Your First Core Data Application”—The groundwork is laid as the fundamental concepts of Core Data are introduced. You learn what Core Data is, and just as importantly, what it isn’t. In addition, Core Data integration with an existing application is demonstrated as the CDHelper class is implemented. Chapter 2, “Managed Object Model Basics”—Data models are introduced as parallels are drawn between traditional database schema design and Core Data. You learn how to configure a basic managed object model as entities and attributes are

discussed, along with accompanying advice on choosing the right data types. Inserting, fetching, filtering, sorting, and deleting managed objects are also covered and followed up with an introduction to fetch request templates. Chapter 3, “Managed Object Model Migration”—Experience lightweight migration, default migration, and using a migration manager to display migration progress. You learn how to make an informed decision when deciding between migration options for your own applications and become comfortable with the model-versioning capabilities of Core Data. Chapter 4, “Managed Object Model Expansion”—The true power of a relational data model is unlocked as different types of relationships are explained and added to Groceries. Other model features such as abstract and parent entities are also covered, along with techniques for dealing with data validation errors. Chapter 5, “Table Views”—The application really comes to life as Core Data is used to drive memory-efficient and highly performing table views with a fetched results controller. Of course, most of the generic legwork is put into a reusable table view controller subclass called CDTableViewController. By dropping this class into your own applications, you can easily deploy Core Data–driven table views yourself. Chapter 6, “Views”—Working with managed objects takes a front seat as you’re shown how to pass them around the application. Objects selected on a table view are passed to a second view, ready for editing. The editing interface is added to Groceries, demonstrating how to work with objects and then save them back to the persistent store. Chapter 7, “Picker Views”—As a nice touch, Core Data–driven picker views are added to the editing views. Picker views allow the user to quickly assign existing items to a unit of measurement, home location, or shop location. A special reusable text field subclass called CDPickerTextField is introduced, which replaces the keyboard with a Core Data picker view whenever an associated text field is tapped. Chapter 8, “Preloading Data”—Techniques for generating a persistent store full of default data from XML are explained and demonstrated in this chapter as the generic CDImporter helper class is introduced. Once you have a persistent store to include with a shipping application, you then see how to determine whether a default data import is required or even desired by the user. Chapter 9, “Deep Copy”—A highly flexible and fine-grained alternative to migratePersistentStore, deep copy enables you to copy objects and relationships from selected entities between persistent stores. In this chapter, the CDImporter helper class is enhanced with the deep copy capability. Chapter 10, “Performance”—Gain experience with Instruments as you identify and eliminate performance issues caused by the common pitfalls of a Core Data application. The camera functionality is introduced to highlight these issues and demonstrates just how important good model design is to a well-performing application.

Chapter 11, “Background Processing”—Top-notch performance requires intensive tasks be offloaded to a background thread. You learn just how easy it is to run processes in the background as the example of photo thumbnail generation is added with a generic helper class called CDThumbnailer. You also learn how to keep memory usage low with another helper class, called CDFaulter. Chapter 12, “Search”—This chapter shows you how to integrate a UISearchController with Core Data as you implement efficient search in CDTableViewController. Chapter 13, “iCloud”—Enjoy the easiest, most reliable Core Data integration with iCloud yet. You learn how to handle multiple accounts and varying preferences on using iCloud without missing a beat. Chapter 14, “Taming iCloud”—Take iCloud integration to the next level with entity-level seeding and unique object de-duplication. This chapter shows you how to emulate first-time iCloud use by resetting ubiquitous content globally, the right way. Chapter 15, “CloudKit Sync: Uploading Objects”—This chapter shows you how to leverage a public CloudKit database to keep a small data set synchronized across the devices of a group of iCloud users. Part 1 lays the foundation for synchronization as the capability to automatically upload new NSManagedObjects is added. Chapter 16, “CloudKit Sync: Downloading Changes and Handling Deletions”—The synchronization implementation is finalized as deletion support and the capability to download changed CloudKit records are added. Appendix A, “Preparing the Groceries Application”—Every (non–Core Data) step involved in preparing the starting-point application for Chapter 1 is documented here for completeness. Appendix B, “Finalizing the Groceries Application”—Every (non–Core Data) step involved in finalizing the application for the App Store is documented here for completeness.

Getting the Sample Code The sample code built throughout this book is available for download from timroadley.com. Links are given in each chapter, or you can use Table P.1 as a reference, which is arranged in the order of implementation.

Table P.1 Groceries Code

Conventions Used in This Book Code Breaks Occasionally lines of code in the chapters are too long to fit on the printed page. Where that occurs, a code-continuation arrow ( ) has been used to mark the continuation. For example: Click here to view code image let _localStore = self.coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: self.localStoreURL, options: options, error: &error)

Sample Code Updates When it’s time to apply what you’ve learned to the sample application, the instructions are preceded by the words “Update Groceries as follows”. You should apply the subsequent steps to the sample application as instructed. The instructions make heavy use of bold formatting to clarify the interface elements you can expect to interact with. For example: Update Groceries as follows to link to the Core Data Framework: 1. Select the Groceries Target, as shown in Figure 1.2. 2. Click the + found in the Linked Frameworks and Libraries section of the General tab and then link to the CoreData.framework, as shown in Figure 1.2.

We Want to Hear from You! As the reader of this book, you are our most important critic and commentator. We value your opinion and want to know what we’re doing right, what we could do better, what areas you’d like to see us publish in, and any other words of wisdom you’re willing to pass our way. You can email or write me directly to let me know what you did or didn’t like about this book—as well as what we can do to make our books stronger. Please note that I cannot help you with technical problems related to the topic of this book, and that due to the high volume of mail I receive, I might not be able to reply to every message. When you write, please be sure to include this book’s title and author as well as your name and phone number or email address. I will carefully review your comments and share them with the author and editors who worked on the book. Email: [email protected] Mail: Trina MacDonald Senior Acquisitions Editor Addison-Wesley/Pearson Education, Inc. 75 Arlington St., Ste. 300 Boston, MA 02116 Register your copy of Learning Core Data for iOS with Swift at informit.com for convenient access to downloads, updates, and corrections as they become available. To start the registration process, go to informit.com/register and log in or create an account. Enter the product ISBN 9780134120034 and click Submit. Once the process is complete, you will find any available bonus content under “Registered Products.”

1. Your First Core Data Application If you can’t explain it simply, you don’t understand it well enough. Albert Einstein Kinesthetic learning, or learning by doing, is one of the best ways to absorb and retain information. The topic of Core Data has been a great hurdle for many seasoned programmers, so it’s about time a book with a hands-on approach to Core Data was written. To avoid sidetracking into deep topics too early, this chapter has many pointers to later chapters. First things first: It gives you a Core Data essentials primer and then dives right in and shows how to add Core Data to the sample application. The sample application is expanded over the course of this book as increasingly advanced topics are introduced.

What Is Core Data? Core Data is a framework that enables you to work with your data as objects, regardless of how they’re persisted to disk. This is useful to you as a Swift programmer, because you should be comfortable using objects in code already. To provide data objects, known as managed objects, Core Data sits between your application and a persistent store, which is the generic term given to a data file such as an SQLite database, XML file (which can’t be used as a persistent store on iOS), or Binary (atomic) store. These files are called “persistent” because they can survive the underlying hardware being reset. Another (oddly named) persistent store option is the In-Memory store. Although it isn’t really “persistent,” an In-Memory store allows you to leverage all the functional benefits of Core Data to manage your data, such as change management and validation, not to mention performance. To map data to a persistent store from managed objects, Core Data uses a managed object model, where you configure your application’s data structure using an object graph. You can think of an object graph as a collection of cookie cutters used to make managed objects from. The “object” in object graph refers to something called an entity, which is used as a cookie cutter to make a customized managed object. Once you have managed objects, you’re then free to manipulate them natively in Swift without having to write any SQL code (assuming you’re using SQLite as the persistent store, which is the most common scenario). Core Data transparently maps those objects back to a persistent store when you save to disk. A managed object holds a copy of data from a persistent store. If you use a database as a persistent store, a managed object might represent data from a table row in that database. If you use an XML file as a persistent store (Mac only), a managed object would represent data found within certain data elements. A managed object can be an instance of NSManagedObject; however, it’s usually an instance of a subclass of NSManagedObject. This is discussed in detail in Chapter 2, “Managed Object Model Basics.” All managed objects exist in a managed object context. A managed object context exists

in high-speed volatile memory, also known as RAM. One reason a managed object context is required is the overhead involved with transferring data to and from disk. Disk is much slower than RAM, so you don’t want to use it more than necessary. A managed object context acts like a cache, because it enables quick access to data that has been previously retrieved from disk. The downside, however, is that you need to call save() on the managed object context periodically to write changes back to disk. The managed object context exists also to track changes to its objects to provide full undo and redo support. Note “If you can’t explain it simply, you don’t understand it well enough” is a famous quote from the late great Albert Einstein. Each chapter of this book is headed by a famous Albert Einstein quote. Core Data can be a difficult topic to learn; however, that doesn’t mean it cannot be broken down and explained in understandable chunks. Whenever I write technical tutorials or documentation, I remember this quote and strive for easy-to-read, highly informative material. To help visualize how the main pieces of Core Data fit together, examine Figure 1.1.

Figure 1.1 The components of Core Data, referred to as the Core Data Stack

Persistent Store Coordinator The left part of Figure 1.1 shows a persistent store coordinator containing a persistent store with table rows. When you set up a persistent store coordinator, you’ll commonly choose an SQLite database as the persistent store. Other options for the persistent store are Binary, XML, and In-Memory stores. The thing to note about Binary and XML stores is that they are atomic. This means that even if you want to change only a small amount of data, you still have to write out the whole file to disk when you save. Of course, the same issue applies when reading an atomic store into memory in the first place. This can become problematic if you have a lot of data because it consumes valuable memory. An SQLite database, on the other hand, is updated incrementally as change logs, also known as transaction logs, are committed. As a result, the SQLite database memory

footprint is comparably small. For these reasons, you’ll typically choose an SQLite database. Note You should not configure Core Data to use a database (persistent store) that it did not originally create. If you need to use existing data, you should import it. This topic is covered in Chapter 8, “Preloading Data.” A persistent store coordinator can have multiple persistent stores. One situation where this may be appropriate is when Core Data is integrated with iCloud. By putting data that doesn’t belong in iCloud into one store, and data that does in another, you save network bandwidth and iCloud storage space. Even though you would then have two persistent stores, you would not need two separate object graphs. Using Core Data model configurations allows you to use separate stores, yet still have the one object graph. When you set up a Core Data model configuration, you can select what parts of the object graph belong in what persistent store. If you do use separate persistent stores, you need to ensure there’s no requirement for a relationship between data in each store. Core Data configurations are discussed in Chapter 14, “Taming iCloud.” A persistent store can have multiple persistent store coordinators. This is an advanced topic to be considered if you have performance issues in your application. This approach typically involves a separate foreground and background stack with a common persistent store. A persistent store is created from an instance of NSPersistentStore, and a persistent store coordinator is created from an instance of NSPersistentStoreCoordinator.

Managed Object Model The middle part of Figure 1.1 shows a managed object model sitting between a persistent store coordinator and a managed object context. As its name suggests, a managed object model is the model or graphical representation of a data structure. It forms the basis on which managed objects are produced. This is similar to a database schema and is also referred to as an object graph. To create a managed object model, you use Xcode to configure entities and the relationships between them. An entity is similar to a table schema in a database. Entities don’t contain data; they only dictate the properties that managed objects that are based on them will have. Essentially they are cookie cutters. Similar to how a database table has fields, an entity has attributes. An attribute can have one of several data types, such as integer, string, or date. Chapter 2 and Chapter 4, “Managed Object Model Expansion,” cover these topics in more detail. A managed object model is created from an instance of NSManagedObjectModel.

Managed Object Context The right part of Figure 1.1 shows a managed object context with managed objects inside. A managed object context manages the lifecycle of objects within and provides powerful features such as faulting, change tracking, and validation. Faulting simply means that when you fetch data from a persistent store, only the parts you need are retrieved. Faulting is covered further in Chapter 10, “Performance.” Change tracking is used for undo and redo support. Validation is the enforcement of rules set in the managed object model. For example, a minimum or maximum value rule can be enforced at an attribute level on an entity. Validation is discussed in Chapter 2. Much like you can have multiple persistent stores, you may also have more than one managed object context. Typically you would use multiple contexts for background processing, such as saving to disk or importing data. When you call save() on a foreground context, you may notice user interface lag, especially when there are a lot of changes. An easy way to get around this issue is to simply call save() only when the application enters the background. Another more complicated yet flexible way is to use two managed object contexts. Remember that a managed object context is an area in highspeed memory. This means you can actually configure a managed object context to save to another managed object context. Once you save a foreground context to a background context, you may then save the background context to disk asynchronously. This staged approach ensures the writes to disk never interfere with the user interface responsiveness. The ability to configure a parent and child context hierarchy has been available since iOS 5. A child context treats its parent as a persistent store, when really the parent is another context that exists to process heavy workloads, such as saving in the background. This is discussed in further detail in Chapter 11, “Background Processing.” A managed object context is created from an instance of NSManagedObjectContext.

When to Use Core Data Once your application outgrows trivial “settings” storage, such as NSUserDefaults and property lists, you’re going to run into memory or complexity issues. The solution is to use a database either directly or indirectly with Core Data. If you choose Core Data, you’ll save time otherwise spent coding a database interface. You could also enjoy big performance gains, as well as some functional benefits such as undo and validation. The time you would have spent developing and testing, you’ll instead free up to focus on more important areas of your application. Now you might be thinking, “I just want to save lots of stuff to disk, so why does it have to be so complicated?” Well, it’s not that difficult once a few key points are understood. Sure, you could write your own database interfaces and they would probably work great for a while. What happens, though, when your requirements change or you want to add, say, data synchronization between devices? How are your skills at building multithreaded data-import routines that don’t impact the user interface? Would your code also support undo and validation yet still be fast and memory efficient on an old iPhone? The good news for you is that all the hard work has already been done and is wrapped up

in the tried and tested Core Data Framework. Even if your application’s data requirements are minimal, it’s still worth using Core Data to ensure your application is as scalable as possible without compromising performance. Once you start using Core Data, you’ll appreciate how robust and optimized it really is. The millions of people worldwide using Core Data applications every day has led to a mature feature set with performance to match. In short, you’ll save more time learning Core Data than throwing it in the too-hard basket and writing your own database interfaces. You’ll also benefit from loads of additional functionality for free. Note Before you continue, you should have at least Xcode 7 installed on your Mac. The code used in this book is targeted at iOS 9, so it won’t work in lower versions of Xcode. It is also recommended that you become a member of the iOS Developer Program, so you can run the sample application on your device as required. Browse the Apple Developer website for further information on becoming a member.

Introducing the Groceries Application Groceries is the name of the sample iPhone application you’ll create over the course of this book. As the features and best practices of Core Data are introduced, you can apply what you’ve learned to Groceries. By the end of the book, you’ll have created a fast and fully functional Core Data application that integrates seamlessly with iCloud. If you want to see the end result upfront, head over to the App Store and download Groceries now. Without further ado, it’s time to begin! Have you ever stood in front of the fridge, pantry, cupboard, or some other location at home wondering what you’re forgetting to put on your shopping list? Then, when you get to the store, you can’t find something because you have no idea what aisle it’s in? To top it off, after zigzagging all the way from aisle 8 (and finally finding what you’re looking for in aisle 2), you discover the next item you need is back in aisle 8! Here’s what Groceries will do for you: Remind you what you might need by sorting potential items by their storage location in your house. Help you locate items at the grocery store by showing what aisle they’re in. Group your list by aisle so you need to visit each aisle only once, and in order. Sync between your devices with iCloud. Sync with other people’s devices with CloudKit. Help you learn Core Data!

Note Appendix A, “Preparing the Groceries Application,” shows the steps required to create the master project “Groceries” from scratch. You may run through those steps manually or alternatively download the starting point project from http://www.timroadley.com/LCDwS/Groceries-AfterAppendixA.zip. Once you have downloaded the project, open it in Xcode 7 or above. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name.

Adding Core Data to an Existing Application When you create an iOS Application project in Xcode, you can choose from various starting-point templates. Using Core Data in your project is as easy as checking the Use Core Data check box during creation of most template-based projects. Adding Core Data manually is more educational, so the Groceries project was created based on the Single View Application template, which doesn’t include Core Data. To use the Core Data Framework, you need to link it to the project. Update Groceries as follows to link to the Core Data Framework: 1. Select the Groceries Target, as shown in Figure 1.2.

Figure 1.2 Linking the Core Data Framework 2. Click the + found in the Linked Frameworks and Libraries section of the General tab and then link to the CoreData.framework, as shown in Figure 1.2.

Implementing the Core Data Helper If you’ve ever examined the built-in Core Data-enabled templates, you may have noticed a lot of Core Data setup is done in the application delegate. So that you may easily apply the approach used in this book to your own projects, Core Data should instead be set up using a helper class called CDHelper.swift. This keeps the Core Data components modular and portable. The application delegate will only be used to create an instance of CDHelper.swift. Creating an instance of this class triggers the following:

Initializes a managed object model Initializes a persistent store coordinator with a persistent store based on the managed object model Initializes a managed object context based on the persistent store coordinator Update Groceries as follows to create the CDHelper class in a new Xcode group: 1. Right-click the Groceries group in Xcode and then create a new group called Generic Core Data Classes, as shown in Figure 1.3.

Figure 1.3 Xcode group for generic Core Data classes 2. Select the Generic Core Data Classes group. 3. Click File > New > File…. 4. Create a new iOS > Source > Swift File and then click Next. 5. Set the filename to CDHelper and ensure the Groceries target is checked. 6. Ensure the Groceries project directory is open and then click Create. Listing 1.1 shows new code intended for the Core Data Helper class. Listing 1.1 Core Data Helper (CDHelper.swift) Click here to view code image import Foundation import CoreData private let _sharedCDHelper = CDHelper() class CDHelper : NSObject { // MARK: - SHARED INSTANCE class var shared : CDHelper {

return _sharedCDHelper } }

The CDHelper.swift class starts out by importing the CoreData Framework so that it has access to the Core Data classes it will leverage. It also supports a shared instance, which makes using this class easier and reduces code. It will soon be expanded to manage a Core Data Stack and provide convenience functions for easy setup and context saves. Note The singleton pattern used in Listing 1.1 is a contentious issue among developers. This pattern should only be used when it makes sense that there only be one of the object in question. This pattern has been adopted for the Core Data Helper to simplify its use and to centralize data management. It is the same approach used by Apple for file management with NSFileManager.defaultManager(). Update Groceries as follows to configure the starting point of CDHelper.swift: 1. Replace all code in CDHelper.swift with the code from Listing 1.1 Note As you paste listing code, consider using Edit > Paste and Preserve Formatting. This preserves the code indentation, which at the time of writing was lost when pasting. Once individual functions have been pasted you may need to highlight the pasted code and then right-click and select Structure > Shift Right. The helper class will soon be updated with new sections for PATHS, CONTEXT, MODEL, COORDINATOR, STORE, SETUP, and SAVING. For easy navigation and readability, these areas are separated with a MARK. As shown in Figure 1.4, the mark feature of Xcode automatically provides a convenient navigation menu.

Figure 1.4 Mark generated menu

Adding Paths For Core Data to function correctly it needs to know where the model and store files are located. The paths to these files should be provided by a modelURL and localStoreURL variable. Another variable called storesDirectory should also be added, which makes up part of the localStoreURL and is reused later for other stores. Listing 1.2 shows the code involved, which uses lazy variables whose values aren’t calculated until they’re needed. Listing 1.2 Core Data Helper PATHS (CDHelper.swift) Click here to view code image // MARK: - PATHS lazy var storesDirectory: NSURL? = { let fm = NSFileManager.defaultManager() let urls = fm.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) return urls[urls.count-1] }() lazy var localStoreURL: NSURL? = { if let url = self.storesDirectory?.URLByAppendingPathComponent(“LocalStore.sqlite”) { print(“localStoreURL = \(url)”) return url } return nil }()

lazy var modelURL: NSURL = { let bundle = NSBundle.mainBundle() if let url = bundle.URLForResource(“Model”, withExtension: “momd”) { return url } print(“CRITICAL - Managed Object Model file not found”) abort() }()

Update Groceries as follows to add the PATHS section: 1. Add the code from Listing 1.2 to the bottom of CDHelper.swift before the last curly brace.

Adding the Core Data Stack With the paths in place the Core Data Stack can now be set up. The Core Data Stack is made up of the following components: The Managed Object Context, also known as the Context The Managed Object Model, also known as the Model The Persistent Store Coordinator, also known as the Coordinator The Persistent Store, also known as the Store Each part of the Core Data Stack is provided through the variables context, model, coordinator, and localStore. Listing 1.3 shows the code involved. Listing 1.3 The Core Data Stack (CDHelper.swift) Click here to view code image // MARK: - CONTEXT lazy var context: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.MainQueueConcurrencyType) moc.persistentStoreCoordinator = self.coordinator return moc }() // MARK: - MODEL lazy var model: NSManagedObjectModel = { return NSManagedObjectModel(contentsOfURL:self.modelURL)! }() // MARK: - COORDINATOR lazy var coordinator: NSPersistentStoreCoordinator = { return NSPersistentStoreCoordinator(managedObjectModel:self.model) }() // MARK: - STORE lazy var localStore: NSPersistentStore? = { let options:[NSObject:AnyObject] = [NSSQLitePragmasOption: [“journal_mode”:“DELETE”]] var _localStore:NSPersistentStore? do { _localStore = try self.coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration:

nil, URL: self.localStoreURL, options: options) return _localStore } catch { return nil } }()

The context variable returns an instance of NSManagedObjectContext attached to the coordinator. It is initialized with an instance of MainQueueConcurrencyType that tells it to run on a “main thread” queue. You must have at least one context running on the main thread when you have a data-driven user interface. Background (private queue) concurrency types are discussed later in the book. The model variable returns an instance of NSManagedObjectModel. It is initialized based on the path to the model file. If a model file is not found, the application terminates. The coordinator variable returns an instance of NSPersistentStoreCoordinator. It is initialized based on the model. The localStore variable optionally returns an instance of NSPersistentStore. Before a persistent store can be returned it must be added to the coordinator. A call to addPersistentStoreWithType is made based on the URL to the store file and an options dictionary with a special journal_mode of DELETE. This journal mode is used throughout the book so you can inspect the contents of the underlying database. There is otherwise no need to use this journal mode. If the call to addPersistentStoreWithType throws an error, a do-catch statement catches this and a nil store is returned. This should occur only in cases where a manual model migration is required. Model migrations are discussed later in Chapter 3, “Managed Object Model Migration.” Update Groceries as follows to add the Core Data Stack: 1. Add the code from Listing 1.3 to the bottom of CDHelper.swift before the last curly brace.

Adding the Setup Section With the Core Data Stack ready to go it’s time to implement the functions responsible for its initial setup. Listing 1.4 shows the code involved. Listing 1.4 Core Data Helper SETUP (CDHelper.swift) // MARK: - SETUP required override init() { super.init() self.setupCoreData() } func setupCoreData() { _ = self.localStore }

The init function runs when an instance of CDHelper is created. It calls

setupCoreData, which creates a constant pointing to self.localStore. Creating this constant starts the chain of events that instantiates the Core Data Stack. The code in the setupCoreData function is expanded on later in the book and is currently only required for demonstration purposes. Update Groceries as follows to implement the SETUP section: 1. Add the code from Listing 1.4 to the bottom of CDHelper.swift before the last curly brace.

Adding the Saving Section To prevent data loss, you’ll eventually want to save the contents of the context to its persistent store. This is as easy as sending the context a save() message, as shown in Listing 1.5. Listing 1.5 Core Data Helper SAVING (CDHelper.swift) Click here to view code image // MARK: - SAVING class func save(moc:NSManagedObjectContext) { moc.performBlockAndWait { if moc.hasChanges { do { try moc.save() print(“SAVED context \(moc.description)”) } catch { print(“ERROR saving context \(moc.description) - \(error)”) } } else { print(“SKIPPED saving context \(moc.description) because there are no changes”) } if let parentContext = moc.parentContext { save(parentContext) } } } class func saveSharedContext() { save(shared.context) }

Everything is executed within performBlockAndWait to ensure that the contexts are saved on an appropriate thread and in order. The first thing the save class function does is check for unsaved changes. If they exist, the given context is saved. If the context has a parent context, it is saved too. Parent contexts are used for background saving and are discussed later in the book. The saveSharedContext class function exists only to make using CDHelper.swift easier and the code tidier. Update Groceries as follows to add the SAVING section:

1. Add the code from Listing 1.5 to the bottom of CDHelper.swift before the last curly brace. There’s no guarantee that the application won’t be terminated or put in the background at a moment’s notice. When this happens it’s a good idea to save the context to prevent data loss. Update Groceries as follows to ensure the context is saved when the application enters the background or terminates: 1. Add CDHelper.saveSharedContext() to the bottom of the applicationDidEnterBackground and applicationWillTerminate functions of AppDelegate.swift. If the application were run now, the new code would not be called. For testing purposes, a simple call to CDHelper.shared from the application delegate sets up the Core Data Stack. This extraneous code will be removed later. 1. Add CDHelper.shared to the bottom of the didFinishLaunchingWithOptions function of AppDelegate.swift before return true. There’s one final step before the application can be run, which is to add a managed object model file.

Adding a Managed Object Model File When the model variable is called, it expects that a model file exists at the location given by modelURL. Currently this is not the case. If you called this variable now, the application would terminate because there is no model file. Update Groceries as follows to add a Data Model file: 1. Right-click the existing Groceries group and then select New Group. 2. Set the new group name to Data Model. 3. Select the Data Model group. 4. Click File > New > File…. 5. Select iOS > Core Data > Data Model and then click Next. 6. Ensure the Groceries target is selected, leave the default filename as Model, and then click Create. 7. Select Model.xcdatamodeld, as shown in Figure 1.5.

Figure 1.5 The Xcode Data Model Designer Figure 1.5 shows the Xcode Data Model Designer, which is used to configure a data model and is discussed in the next chapter. For now an empty model file is enough to allow the application to run. Run Groceries on the iOS Simulator and examine the debug log window. The location of the localStoreURL should be visible in the console log as shown in Figure 1.6. Knowing the location will be useful later when it comes time to examine the contents of this persistent store.

Figure 1.6 The debug log window showing the location of the local store Press the home button (Shift+ +H or Hardware > Home). This triggers save() as the application enters the background. The save is skipped because there are no changes, which is mentioned in the console log.

Summary You’ve now been introduced to the key components of Core Data. The sample application Groceries was updated to include an SQLite persistent store, persistent store coordinator, managed object model, and managed object context. The data model has not been configured, so the application isn’t very interesting yet. Chapter 2 is where the real fun begins with the introduction of data models. If you’re still unclear on the role some parts of Core Data play, don’t worry too much at this stage. As you use each component more, it becomes easier to understand how they fit together.

Exercises Why not build on what you’ve learned by experimenting? 1. Examine the console log to compare the different locations persistent store files are saved to when running the application on a device, versus running on the iOS

Simulator. This is useful information when it comes time to open the persistent store for troubleshooting. 2. Change the persistent store type in the localStore variable of CDHelper.swift from NSSQLiteStoreType to NSXMLStoreType and try running the application. You won’t be able to run the application because this store type is not available on iOS, so revert this change when you’re done testing.

2. Managed Object Model Basics The only source of knowledge is experience. Albert Einstein In Chapter 1, “Your First Core Data Application,” the fundamental Core Data building blocks were added to the Groceries sample application. You configured a persistent store, coordinator, model, and context; however, the object graph remains empty. This means that although all the ingredients are ready to make cookies, you’re missing the cookie cutters! This chapter explains managed object model basics and takes you through the process of configuring an object graph for the sample application.

What Is a Managed Object Model? A managed object model represents a data structure. The terms data structure, schema, object graph, data model, and managed object model may all be used interchangeably because they all mean more or less the same thing. If you were designing a new database without Core Data, you might configure a database schema and refer to it as a “data model.” With Core Data, the focus is (managed) objects. Therefore, instead of calling the schema a data model, you may refer to it as a managed object model. That said, it’s still perfectly appropriate to call it a data model, object graph, schema, or data structure, too! Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter01.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. In Chapter 1, you created a managed object model file called Model.xcdatamodeld. Figure 2.1 shows this file open in the Xcode Data Model Designer, which is used to configure a data model. The three areas of a managed object model are Entities, Fetch Requests, and Configurations. Entities and fetch requests are discussed in this chapter. Configurations are discussed in Chapter 14, “Taming iCloud.”

Figure 2.1 The Xcode Data Model Designer

Entities A managed object model is made up of a collection of entity description objects called entities. Entities are the foundation of a managed object model because they are used to logically group data that belongs together. You can think of an entity as an individual cookie cutter used to create managed objects. Once you have managed objects, you can work with data natively in code. A managed object model can have one or more entities, which varies between applications. Before you can produce managed objects, you first need to design each cookie cutter (entity) appropriately. Designing an entity is similar to traditional database table design. When designing a database table, you do the following: 1. Configure a table name. 2. Configure fields and set a data type for each one. When designing an entity, you do this: 1. Configure an entity name. 2. Configure attributes and set a data type for each one. 3. Configure an NSManagedObject subclass based on the entity (optional). Whereas a table in a database has fields, an entity has attributes. An attribute must have a data type specified (for example, a string or integer). When it comes time to produce a managed object from an entity, you usually create an NSManagedObject subclass based on the entity; however, this is not mandatory. Using an NSManagedObject subclass does have some advantages, though, such as enabling dot notation for your managed objects, which makes your code easier to read and prevents typos. From an NSManagedObject or an NSManagedObject subclass, you create instances to begin working with data as managed objects. In database terms, managed object instances are like a row in a database table. The name of the entity and the name of the NSManagedObject subclass you create from it are usually the same. Attributes you configure in an entity become variables of the managed objects created from them. Figure 2.2 shows how entities are used to map between a persistent store’s database and managed objects.

Figure 2.2 Core Data entity mapping One of the most important things to think about when designing a managed object model is the name you give each entity. The name you choose for an entity needs to describe (in a word or two) the data that the entity represents. For Groceries, there’s a requirement to put things on a list. When you’re out shopping, you may want to put apples and oranges on the list. With this in mind, perhaps “fruit” is a good name for the entity representing things you might put on a list. When you configure an entity, be careful to use a name that is generic enough to be flexible, yet specific enough that it’s obvious what it represents. Sometimes it’s easy to work out; sometimes it’s a delicate balancing act that forces you to think hard about your application’s purpose—now and in future releases. A shopping list is prone to containing more than just fruit, so perhaps “Item” is a better name for the entity representing things that could be put on a shopping list. Update Groceries as follows to add an Item entity: 1. Select Model.xcdatamodeld. 2. Click Add Entity. 3. Double-click the new entity and rename it Item. More detail about this entity can be seen using the Data Model Inspector (Option+ +3) as shown on the right of Figure 2.3.

Figure 2.3 The Item entity

Attributes An attribute is a property of an entity. In the sample application, the Item entity represents things you can put on a shopping list. To work out what attributes are appropriate for the Item entity, you should think about things all shopping list items have in common. As a reasonable starting point, you might come up with a list similar to this: Item name Item quantity Attribute names must begin with a lowercase letter. They should also not have a name that is reserved by NSObject or NSManagedObject. Xcode won’t let you break this rule and warns you if, for example, you create an illegal entity attribute called “description.” When an NSManagedObject subclass is created from the Item entity, it has variables with the same name as the equivalent entity attributes. As with other objects in Swift, you may refer to the variables of an NSManagedObject subclass using dot notation. This makes code easier to read as you get variable values directly via item.name and item.quantity. Update Groceries as follows to add two new attributes: 1. Add two attributes to the Item entity by clicking Add Attribute twice. 2. Rename one attribute to name and the other to quantity. The expected result is shown in Figure 2.4.

Figure 2.4 Attributes with undefined types When you add an attribute to an entity, you must specify the type of data it represents. The

default attribute type is Undefined. The data type you choose for each attribute varies and requires some forward thinking. There are several attribute types to choose from and as a Swift programmer, you may already be familiar with some of them.

Integer 16/32/64 Each Integer attribute data type represents a whole number, without a decimal point. The only difference between these similar types is how big or small that number can be. Core Data uses signed integers, so their range starts in the negative instead of with zero: Integer 16 ranges from –32768 to 32767. Integer 32 ranges from –2147483648 to 2147483647. Integer 64 ranges from –9223372036854775808 to 9223372036854775807. The bigger a number, the more memory is needed to hold it. When choosing between the three options for integers, you need to think about the lowest and highest value that may ever be needed by this attribute. If you’re not quite sure which size to choose, it’s generally a safe bet to go with Integer 32. Just beware that if you get it wrong and it turns out that you need a bigger number, you will need to upgrade the attribute to Integer 64. This type of change requires an upgrade to the managed object model, which is discussed in Chapter 3, “Managed Object Model Migration.” Integers use the base-2 number system, better known as binary. Calculations using integers are faster than those with floating-point numbers. This is because there’s no need to handle the remainder left over after a calculation. For example, if you divide 10 by 3, the result is 3 and the remainder of 1 is lost. The term for this loss is low precision. If you choose to use integers to represent money, it is highly recommended that 1 = 1 cent. This way, there are no rounding errors if you need to do financial calculations. When an NSManagedObject subclass is created from an entity containing an Integer 16, Integer 32, or Integer 64 attribute, the resulting variable is an NSNumber.

Float and Double Float and double attribute data types can be thought of as non-integers with a decimal point. They are both used to represent real numbers; however, they have some limitations in doing so. Floats and doubles use the base-2 (binary) number system, which is a CPU native number system prone to rounding errors. Consider the fraction 1/5. In decimal this can be represented exactly as 0.2. In binary it can only ever be represented by an approximation. The more digits you have after the decimal point, the higher the precision and more accurate the approximation is. Higher precision comes at the cost of more memory to store that precision. Compared to a float, a double is just that—double the amount of bits. A float takes 32 bits to store and a double takes 64 bits. They are both stored in scientific notation. That is, there is a number (called a mantissa) and an exponent (power of) that are put together to form a floating-point number. Having 64 bits means a double has a larger range of values and higher precision than a float.

The biggest possible float on iOS is 340282346638528859811704183484516925440.000000. Floats and doubles are also stored with a sign bit, so this means that the lowest possible float on iOS is – 340282346638528859811704183484516925440.000000. The biggest double is much larger than the biggest float. At the end of this chapter, the exercises provide code to display the minimum and maximum values for each of the numerical data types. When deciding between float and double, you should consider the characteristics of the attribute you are configuring. What are the smallest and largest values you need? Do you really need more than the ~7 digits of precision float offers? Although it may seem inconsequential for a handful of variables to all be doubles, be aware that databases amplify the storage requirements because they potentially have thousands of rows. Given the power and capacity of modern devices these days, you could get away with using a double for most scenarios. On the other hand, if you need to increase the speed of floatingpoint calculations and precision isn’t too critical, a float may be more appropriate. You shouldn’t use a float or double to represent dollars and cents for financial calculations because rounding errors might cause money to go missing! When an NSManagedObject subclass is created from an entity containing either a float or double attribute, the resulting variable is an NSNumber.

Decimal Decimal is the recommended attribute data type for working with numbers where base-10 arithmetic is appropriate. Base-10 is not a CPU native number system like base-2 (binary). This means substantial processor overhead is incurred when working with decimals. Much like floats and doubles, decimals are made up of an integer mantissa, exponent, and sign. At the cost of extra memory and processing time, decimal provides excellent calculation accuracy. With this system, numbers such as 0.1 can be represented exactly. In essence, the decimal type stores this example as 1 / 10 ^ 1. The largest decimal isn’t as big as the largest double; however, the precision is much higher and in some cases perfect. At the end of this chapter, the exercises provide code to display an example of the precision achievable by each of the numerical data types. When an NSManagedObject subclass is created from an entity containing a decimal attribute, the resulting variable is an NSDecimalNumber. When you perform calculations with an NSDecimalNumber, it is imperative you only use its built-in functions to ensure you retain the precision.

String The string attribute data type is used to store an array of characters, or plain old text. Because you’re a Swift programmer, odds are you’re familiar with strings already. When an NSManagedObject subclass is created from an entity containing a string attribute, the resulting variable is a String.

Boolean The Boolean attribute data type is used to store a true or false value. When an NSManagedObject subclass is created from an entity containing a Boolean attribute, the resulting variable is an NSNumber. To get the Boolean value back out of the NSNumber, simply send a boolValue message to the NSNumber instance. When setting a Boolean value in an NSNumber, use the numberWithBool function.

Date The date attribute data type is self-explanatory. It is used to store a date and time. When an NSManagedObject subclass is created from an entity containing a date attribute, the resulting variable is an NSDate.

Binary Data If you need to store photos, audio, or some other contiguous BLOB of zeros and ones, you should use the binary data attribute type. When an NSManagedObject subclass is created from an entity containing a binary data attribute, the resulting variable type is NSData. Depending on the data you’re storing, the approach in converting your data to and from NSData differs. One common scenario is storing photos. To store a photo, you convert a UIImage to NSData using UIImagePNGRepresentation() or UIImageJPEGRepresentation(). To retrieve a photo, you convert the NSData to UIImage using the UIImage class function imageWithData. Binary Data is a good choice for large files because they can be seamlessly stored outside the database using the Allows External Storage attribute setting. With this setting enabled, Core Data decides whether it is more efficient to store the file inside or outside the database.

Transformable The transformable attribute data type is used to store a Swift object. This attribute type is a flexible option that allows you to store an instance of any class. An example of a transformable attribute is an instance of UIColor. When an NSManagedObject subclass is created from an entity containing a transformable attribute, the resulting variable type is AnyObject. For the AnyObject object to make it into the store (and back again), you need to use an instance of NSValueTransformer or an instance of a subclass of NSValueTransformer. This class helps transparently convert the attribute to NSData and back again. This is a reasonably simple process, especially when the class you want to store implements the NSCoding protocol. If it does, the system provides a default transformer that already knows how to archive and unarchive specific objects. Update Groceries as follows to configure its attributes:

1. Set the name attribute type to String. 2. Set the quantity attribute type to Float. Using a floating-point number allows a user to put, for example, 1.5 Kg of something on his or her list. 3. Add an attribute called photoData to the Item entity and set its type to Binary Data. This attribute stores the image data of an item’s photo. (Note that the Allows External Storage attribute setting won’t be enabled until later in the book.) 4. Add an attribute called listed to the Item entity and set its type to Boolean. This attribute is used to indicate whether an item is on the shopping list. 5. Add an attribute called collected to the Item entity and set its type to Boolean. This attribute is used to indicate whether an item has been collected and therefore checked off the shopping list. The data model should now match Figure 2.5, as each attribute now has a data type configured.

Figure 2.5 Groceries’ data model in Table editor style In preparation for an increasingly complicated data model, it is good to be aware you can switch to a graphical view. To change to graph editor mode, simply toggle the Editor Style button shown near the middle at the bottom of Figure 2.6. The center of Figure 2.6 shows what the editor looks like in Graph style.

Figure 2.6 Groceries’ data model in Graph editor style

Attribute Settings On the right of Figure 2.6 the Data Model Inspector is shown, which allows configuration of attribute settings beyond their type. You can access this area by pressing Option+ +3 when an attribute is selected. The options presented vary depending on the selected attribute’s type. Not all options are available to all attributes: Transient properties are never written to the persistent store. Although it may seem odd to have a property that is never persisted, there are scenarios where you need a property only in the managed object context. For example, you may want to calculate a value on the fly and then store the result in a transient property. Being in a context allows those properties to benefit from features such as undo and redo. Optional properties aren’t required to have a value. All properties are originally created as optional. When a property is not optional, you won’t be able to save the managed object back to the store until the non-optional properties have a valid value. Indexed properties are optimized for search efficiency at the cost of additional storage space in the underlying persistent store. This additional space required for the index ranges in size depending on how much data needs indexing. If you’re not going to search on a particular attribute, you can save space by not indexing the attribute. Validation can be used to ensure illogical data never enters the persistent store. Each of the numerical attribute types has the same validation options, which are minimum and maximum values. Similarly, you can set constraints around string lengths and date ranges, too. It is perfectly fine to have invalid values in a managed object context; as long as these are resolved before save is called. It’s generally a good practice to validate data as soon as a user tries to take focus off an input element, such as a UITextfield. Reg Ex is short for Regular Expression and goes beyond validating a minimum or maximum string length. Although it can be used to enforce length, it is primarily used to test that an attribute’s string value matches a certain pattern. When Reg Ex validation is configured for an attribute, any related managed object variable values must match the pattern; otherwise, they cannot be written to a persistent store. The pattern match configuration options must conform to the ICU Reg Ex specification. You can find further information about this specification, including valid configuration options, at http://userguide.icu-project.org/strings/regexp. Default values can be set for all attributes types except transformable and binary data. They are a starting point value used when no other value has been specified. It is best practice to have a default value set on numerical attributes because of the way the backend SQLite database handles null values. Setting an appropriate default value for a string attribute is situational and therefore depends on your requirements. For the date attribute type, you unfortunately cannot configure a default date of “now” in the model editor. Allows External Storage is used to permit large binary data attribute values to be

stored outside the persistent store. It is recommended that you enable this option when storing large media such as photos, audio, or video. Core Data automatically stores large attribute values outside an SQLite persistent store when this option is enabled. This option has no effect when the underlying persistent store is XML (remember, this type of store isn’t supported on iOS). Index in Spotlight doesn’t do anything for an iOS application. It is used to integrate a Core Data-based Mac application with Spotlight. Spotlight is the search facility available via the magnifying glass in the top-right corner of the screen on a Mac. When a Mac application’s Core Data attribute is indexed in Spotlight, its values can appear in the results of a Spotlight search. When performing the search index, Spotlight looks for the hidden, zero-length files Core Data created that represent records from the persistent store. As indexed attribute values change in the persistent store, the equivalent files outside the store are updated automatically. Store in External Record File duplicates data from the persistent store into an XML representation outside the store. When used in conjunction with Index in Spotlight, this setting causes the index files created for Spotlight to be populated with values. It’s not recommended that you use this option unless you have a specific need to, such as for debugging purposes. If you choose to use external records to feed data to another application, note that the directory structure containing the records is subject to change. Name properties (of transformable attributes) are used to specify the name of the specific NSValueTransformer subclass that knows how to convert from any class into NSData and back again. Update Groceries as follows to configure indexing and defaults: 1. Set the name attribute to Indexed. 2. Set the name attribute Default Value to New Item. 3. Set the quantity attribute Default Value to 1. 4. Set the listed attribute Default Value to YES. This ensures newly created items show up on the shopping list. 5. Set the collected attribute Default Value to NO. This ensures newly created items aren’t checked off the shopping list as soon as they’re created.

Subclassing NSManagedObject With a basic managed object model in place, it’s time to create an NSManagedObject subclass based on the Item entity. These subclass files let you work with data objects directly in code using dot notation. Whenever the model is updated in the future, you need to regenerate these files again using the procedure you’re about to follow. Although it is possible to add functions to these generated files, you should avoid doing so directly because changes you’ve made will be lost if they’re ever regenerated. If you need to add custom functions, you’re better off subclassing or creating a category for the generated files.

Update Groceries as follows to generate NSManagedObject subclass files: 1. Select the Item entity. 2. Click Editor > Create NSManagedObject Subclass…. 3. Ensure the Model is selected and then click Next. 4. Ensure the Item entity is selected and then click Next. 5. Ensure the language is Swift. 6. Ensure that Use scalar properties for primitive data types is not checked. 7. Ensure the group is set to Data Model. 8. Ensure the Groceries target is checked. 9. Ensure the files will be saved into the Groceries project directory and then click Create. There are now two new files in the Xcode project. The Item.swift file is reserved for any item-specific code you want to add. The Item+CoreDataProperties.swift file is an extension of Item.swift and was generated based on the Item entity, as shown in Listing 2.1. Listing 2.1 Managed Object Subclass (Item+CoreDataProperties.swift) Click here to view code image import Foundation import CoreData extension Item { @NSManaged var name: String? @NSManaged var quantity: NSNumber? @NSManaged var photoData: NSData? @NSManaged var listed: NSNumber? @NSManaged var collected: NSNumber? }

Each of the attributes from the Item entity is listed in the NSMangedObject subclass as an optional @NSManaged variable. The order that they’re listed in may vary and is not important. Notice the slight differences between the entity attribute types and the resulting variable types. Here’s a summary of how entity attributes translate to @NSManaged variables: A Date attribute becomes an NSDate variable. A String attribute becomes a String variable. A Decimal attribute becomes an NSDecimalNumber variable, and all other numerical data types and Boolean become NSNumber variables. A Binary Data attribute becomes an NSData variable. A Transformable attribute becomes an AnyObject variable.

Scalar Properties for Primitive Data Types When you created the NSManagedObject subclass for the Item entity, you came across the option Use scalar properties for primitive data types. This option allows the resulting NSManagedObject subclass to use object variables only when it has no other recourse. Here’s a summary of how entity attributes translate to managed object variables when this option is selected: A Date attribute becomes an NSTimeInterval. A Double attribute becomes a Double. A Float attribute becomes a Float. An Integer 16/32/64 attribute becomes an Int16, Int32, or Int64, respectively. A Boolean attribute becomes a Bool. This option has no effect on String, Decimal, Binary Data, or Transformable attributes, so their resulting property is still an object pointer.

Introducing the Demo Function Throughout this book there’s code that demonstrates a point and yet isn’t required in the final application. Listing 2.2 shows a new demo function to be used for testing purposes, which is called from the existing applicationDidBecomeActive function in AppDelegate.swift. This code doesn’t do anything yet as it’s only the foundation for later steps in the chapter. Listing 2.2 Demo Preparation (AppDelegate.swift: demo) Click here to view code image func demo () { // Reserved for upcoming code }

Update Groceries as follows to implement the demo function: 1. Add the empty demo function from Listing 2.2 to the top of AppDelegate.swift on the line after the window variable is declared. 2. Add demo() to the applicationDidBecomeActive function of AppDelegate.swift.

Creating a Managed Object Everything is now in place to create some new managed objects. New objects are based off the NSEntityDescription class of a particular entity, specified by name. In addition to specifying the entity to base an object on, you also need to provide a pointer to a managed object context where the new managed object goes. Access to a shared context is now available from anywhere in the application through the CDHelper.shared.context variable. The code in Listing 2.3 demonstrates how to insert a new managed object based on an entity into a context. Inserting a new managed object is as simple as calling the insertNewObjectForEntityForName function of the NSEntityDescription class and then passing it an appropriate entity name and context pointer. Once a new Item-based managed object is created, you can then manipulate its values directly in code. The print command at the end of Listing 2.3 demonstrates this when item.name is passed in as a string variable. The dot notation shown is a particularly clean way of working with objects because it makes your code easier to read. Listing 2.3 Inserting (AppDelegate.swift demo) Click here to view code image func demo () { let itemNames = [“Apples”, “Milk”, “Bread”, “Cheese”, “Sausages”, “Butter”, “Orange Juice”, “Cereal”, “Coffee”, “Eggs”, “Tomatoes”, “Fish”] for itemName in itemNames { if let item:Item = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: CDHelper.shared.context) as? Item { item.name = itemName print(“Inserted New Managed Object for ‘\(item.name!)’”) } } CDHelper.saveSharedContext() }

Update Groceries as follows to practice inserting managed objects: 1. Add import CoreData to the top of AppDelegate.swift. This ensures you have access to NSEntityDescription. 2. Replace the demo function in AppDelegate.swift with the code from Listing 2.3. Run the application, and you should see managed object names listed in the console. Figure 2.7 shows the expected result. The table view on the device will remain blank.

Figure 2.7 Inserting managed objects Note If you launched the application previously you receive an error stating “The model used to open the store is incompatible with the one used to create the store.” If you get this error, you need to delete the application from the device and rerun it from Xcode to install a fresh copy. If that doesn’t fix it, you should also click Product > Clean. Graceful model upgrades are discussed and implemented in Chapter 3.

Backend SQL Visibility At face value, examining the console logs for Core Data results is rather underwhelming. How do you know what’s really going on under the covers? What is Core Data actually doing to get your data into the persistent store? Is it doing its job properly? What SQL queries are being generated to provide the seamless Core Data experience? Are duplicate objects being inserted every time you run the app in the simulator? These questions can be answered with an extremely verbose debug option that provides plenty of information regarding what’s going on under the hood. The debug option exposes the auto-generated SQL queries and gives some great insight to the workings of Core Data. Update Groceries as follows to enable SQL Debug mode: 1. Click Product > Scheme > Edit Scheme…. 2. Ensure Run Debug and the Arguments tab are selected as shown in Figure 2.8.

Figure 2.8 Arguments Passed On Launch 3. Add a new argument by clicking + in the Arguments Passed On Launch section. 4. Enter -com.apple.CoreData.SQLDebug 3 as a new argument and then click Close. Now that you’ve enabled SQL Debug mode level 3, run the application again and reexamine the logs. Look at all those INSERT statements you didn’t have to write! Figure 2.9 shows a fraction of the expected results.

Figure 2.9 Core Data-generated SQL queries Each of the SQLite binds shown in Figure 2.9 is a variable used to form an INSERT statement. The purpose of the statement is to insert managed object variable values into a row of the ZITEM table found in the persistent store. The ZITEM table is associated with the Item entity. You can tell which database fields the entity attributes relate to by their name. The Z prefix is just a Core Data standard naming convention. Note that the insert statements were only triggered because the context was saved. To verify managed objects are being persisted to an SQLite persistent store, you can actually look inside using a third-party utility. Be aware that altering a database directly isn’t recommended. You can quickly jump to the iOS Simulator working directory containing the LocalStore.sqlite with these steps: 1. Copy the path to the store from the console as shown by the highlighted text in Figure 2.10.

Figure 2.10 The iOS Simulator’s Groceries SQLite database 2. Outside Xcode, right-click the Finder icon and click Go to Folder…. 3. Paste the path to the store from step 1 into the Go to the Folder window and then click Go. This should take you straight to the folder containing the LocalStore.sqlite file. To open an SQLite database file, you can use one of many freely available SQLite database browser utilities found by searching Google. There’s a good one on SourceForge called SQLite Database Browser you may want to try, so take a moment now to download and install it. This application isn’t signed, so you need to set Allow Applications Downloaded from Anywhere in System Preferences > Security & Privacy > General. If you’re not comfortable allowing this, search the Mac App Store for extension:sqlite to find another suitable signed application that can open .sqlite files. Even though browsing the contents of a database is great for debugging purposes, you should not code your application to rely on the internal, private schema of a Core Datamanaged database because Apple could change it without notice. Note Given that the Library folder is hidden, it’s easier to open LocalStore.sqlite by right-clicking it, selecting Open With, and then selecting SQLite Database Browser. Be careful not to have the SQLite file already open with the database browser while Xcode is running Groceries. If you do, the application may time out when attempting to open the database. Figure 2.11 shows that the managed object variables have been persisted to the store.

Figure 2.11 Browsing the Groceries SQLite persistent store You most probably have duplicate entries in the database now because the demo function is saving these items to disk whenever the application is launched. Don’t worry about these duplicates; you’ll soon learn how to delete managed objects. Note You must add a persistent store with the DELETE journal mode to see its complete contents. Keep this in mind if you use the technique from this chapter to examine the contents of your own persistent stores. To ensure you’re getting the best available performance, remember to remove the DELETE journal mode before shipping to the App Store. Update Groceries as follows to avoid further data duplication: 1. Remove everything from within the demo function of AppDelegate.swift

Fetching Managed Objects To work with existing data from a managed object context, you first need to fetch it. If the data isn’t already in a context when fetched, it is retrieved from the underlying persistent store transparently. To fetch, you need an instance of NSFetchRequest, which returns an array of managed objects. When the fetch is executed, every managed object for the specified entity is returned in the resulting array. In SQL database terms, a fetch is similar to a SELECT statement. The code involved is shown in Listing 2.4. Listing 2.4 Fetch Request (AppDelegate.swift demo)

Click here to view code image let request = NSFetchRequest(entityName: “Item”) do { try CDHelper.shared.context.executeFetchRequest(request) } catch { print(“ERROR executing a fetch request: \(error)”) }

Update Groceries as follows to fetch all the Item entity instances: 1. Add the code from Listing 2.4 to the demo function of AppDelegate.swift. Run the application to examine the logs and you should see several similar lines showing each managed object that has been retrieved from the database. This number varies depending on the amount of duplicated data. Figure 2.12 shows the expected result. At this stage, don’t worry about the occurrences of ; these are expected.

Figure 2.12 Fetched managed objects To see the names of each item printed in the console, you can use a for loop to iterate through the fetched array. You need to specify that the resulting array is an array of items using as? [Item]. The code involved is shown in Listing 2.5. Listing 2.5 Fetching (AppDelegate.swift demo) Click here to view code image func demo () { let request = NSFetchRequest(entityName: “Item”) do { if let items = try CDHelper.shared.context.executeFetchRequest(request) as? [Item] { for item in items { if let name = item.name { print(“Fetched Managed Object = ‘\(name)’”) } } } } catch { print(“ERROR executing a fetch request: \(error)”) } }

Update Groceries as follows to configure item names to display in the console log:

1. Update the demo function of AppDelegate.swift to match Listing 2.5. Run the application again and you should see each of the item names listed in the console log. Figure 2.13 shows the expected result. Don’t worry about duplicates.

Figure 2.13 Fetched managed object names

Sorting Fetch Requests An NSFetchRequest returns an array, which by nature supports being sorted. As such, you may optionally configure an NSFetchRequest with a sort descriptor configured to order managed objects in a certain way. Sort descriptors are passed to an NSFetchRequest as instance(s) of NSSortDescriptor in an array. An array is used so you can sort by multiple attributes. In SQL database terms, a sort descriptor is similar to an ORDER BY statement. Listing 2.6 shows the code involved. Listing 2.6 Sorting (AppDelegate.swift demo) Click here to view code image func demo () { let request = NSFetchRequest(entityName: “Item”) let sort = NSSortDescriptor(key: “name”, ascending: true) request.sortDescriptors = [sort] do { if let items = try CDHelper.shared.context.executeFetchRequest(request) as? [Item] { for item in items { if let name = item.name { print(“Fetched Managed Object = ‘\(name)’”) } } } } catch { print(“ERROR executing a fetch request: \(error)”) } }

Update Groceries as follows to ensure the fetch is sorted: 1. Insert the bold code from Listing 2.6 into the equivalent place within the demo function of AppDelegate.swift. Run the application again to examine the logs, and you should see the item names are now sorted in alphabetical order. Figure 2.14 shows the expected result, which may vary

slightly for your application depending on the number of duplicate objects.

Figure 2.14 Fetched managed object names (sorted)

Filtering Fetch Requests When you don’t want to fetch every object for an entity, you can filter fetches using a predicate. A predicate is defined for a fetch request using an instance of NSPredicate and then passing that to an instance of NSFetchRequest. Using a predicate limits the number of objects in the fetch results based on the criteria specified. Predicates are persistent store agnostic, so you can use the same predicates regardless of the backend store. That said, there are some corner cases where particular predicates won’t work with certain stores. For example, the matches operator works with in-memory filtering; however, it does not work with an SQLite store. In SQL database terms, a predicate is similar to a WHERE clause. A predicate is evaluated against each potential managed object as a part of fetch execution. The predicate evaluation result is a Boolean value. If true, the predicate criteria are satisfied and the managed object is part of the fetched results. If false, the predicate criteria are not satisfied and the managed object is not part of the fetched results. In Groceries, let’s say you wanted to exclude items matching the name “Coffee.” When you create the NSPredicate that will be passed to the NSFetchrequest, you would specify that name is not equal to the string "Coffee". In code, this predicate is written as name != "Coffee". Because predicates support variable substitution, a string could also be passed to the predicate at runtime, as shown in bold in Listing 2.7. Building predicates can be a complicated topic, depending on your requirements. For further information, search the Apple developer website for the Predicate Programming Guide. Listing 2.7 Filtering (AppDelegate.swift demo) Click here to view code image func demo () { let request = NSFetchRequest(entityName: “Item”) let sort = NSSortDescriptor(key: “name”, ascending: true) request.sortDescriptors = [sort] let filter = NSPredicate(format: “name != %@”,“Coffee”) request.predicate = filter do { if let items = try CDHelper.shared.context.executeFetchRequest(request) as? [Item] {

for item in items { if let name = item.name { print(“Fetched Managed Object = ‘\(name)’”) } } } } catch { print(“ERROR executing a fetch request: \(error)”) } }

Update Groceries as follows to add a predicate to filter the fetch: 1. Add the bold code from Listing 2.7 into the equivalent place within the demo function of AppDelegate.swift. Run the application again to examine the logs and you should see the list of item names now excludes Coffee. Figure 2.15 shows the expected results.

Figure 2.15 Fetched managed object names (sorted and filtered)

Fetch Request Templates Determining the correct predicate format to use for every fetch can become laborious. Thankfully, the Xcode Data Model Designer supports predefining fetch requests. These reusable templates are easier to configure than predicates and they reduce repeated code. Fetch request templates are configured using a series of drop-down boxes and fields specific to the application’s model. Unfortunately, given their simplicity they aren’t as powerful as predicates. If you need features such as custom AND/OR combinations, you have to revert to predicate programming. Update Groceries as follows to create a fetch request template: 1. Select Model.xcdatamodeld. 2. Click Editor > Add Fetch Request. 3. Set the name of the fetch request template to Test. 4. Click the + on the far right side; then configure the Test fetch request template to fetch all Item objects that have a name attribute that contains the letter “e”, as shown in Figure 2.16.

Figure 2.16 Fetch request template configuration To use a fetch request template, you need to send a fetchRequestTemplateForName message to the managed object model, telling it the name of the template to use. This gives you an NSFetchRequest to work with. Listing 2.8 shows the code involved. Listing 2.8 Fetch Request Template (AppDelegate.swift demo) Click here to view code image func demo () { let model = CDHelper.shared.model if let template = model.fetchRequestTemplateForName(“Test”), let request = template.copy() as? NSFetchRequest { let sort = NSSortDescriptor(key: “name”, ascending: true) request.sortDescriptors = [sort] do { if let items = try CDHelper.shared.context.executeFetchRequest(request) as? [Item] { for item in items { if let name = item.name { print(“Fetched Managed Object = ‘\(name)’”) } } } } catch {print(“ERROR executing a fetch request: \(error)”)} } else {print(“FAILED to prepare template”)} }

Because the fetch request has been created from a template, there’s no longer a need to pass in a predicate for filtering. To modify the fetch request in any way (for example, to sort it), you must first copy() the fetch request template. This is because a fetch request template comes from an immutable (unchangeable) model. Update Groceries as follows to use the Test fetch request template: 1. Replace the demo function in AppDelegate.swift with the code from Listing 2.8. Run the application again to examine the logs, and you should see the sorted managed object names all contain a lowercase letter e, as configured in the fetch request template. Figure 2.17 shows the expected results.

Figure 2.17 Fetched managed object names (filtered via template) Did you also notice the SQL statement generated for this fetch? You can see all the elements required to retrieve (SELECT) a filtered (WHERE) and sorted (ORDER BY) set of results. Figure 2.18 shows the expected results.

Figure 2.18 Automatically generated SQL

Deleting Managed Objects Deleting a managed object is as easy as calling deleteObject or deleteObjects on a containing context. Note that deletion isn’t permanent until you save the context. The code involved is shown in Listing 2.9. Listing 2.9 Deleting (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context let request = NSFetchRequest(entityName: “Item”) do { if let items = try CDHelper.shared.context.executeFetchRequest(request) as? [Item] { for item in items { if let name = item.name { print(“Deleting Item ‘\(name)’”) context.deleteObject(item) } } } } catch { print(“ERROR executing a fetch request: \(error)”) } }

Update Groceries as follows to delete all the objects: 1. Replace the demo function with the code from Listing 2.9. 2. Run the application. 3. Press the home button (Shift+ +H or Hardware > Home) to save the changes to the context. Did you notice all the SQL statements calling DELETE that were logged to the console once you pressed the home button to trigger a save?

Summary By now you should have a reasonable level of comfort that Core Data is taking care of the backend SQL for you. You’ve been introduced to the steps required to configure a basic managed object model. Entities and attributes have been discussed, along with some advice on choosing the right data types. Inserting, fetching, filtering, sorting, and deleting managed objects have all been covered, followed up with an introduction to fetch request templates. The covers have also been lifted off Core Data to give you some insight into what it’s doing for you under the hood.

Exercises Why not build on what you’ve learned by experimenting? 1. Insert some new managed objects and set their name and quantity. 2. Alter the Test fetch request template and experiment with other filtering options. 3. Open LocalStore.sqlite in SQLite Database Browser and then click the Database Structure button. Notice the existence of ZITEM_ZNAME_INDEX? That’s where the indexing for the name attribute is stored since you checked the Indexed check box. 4. Replace the code in the demo function of AppDelegate.swift with the code from Listing 2.10. Run the application again and examine the range limitations of the numerical data types in the console log. Once you’re finished, remove the code from within the demo function and turn off SQLDebug. Listing 2.10 Numerical Attribute Ranges Click here to view code image print(“Integer16 ranges from \(Int16.min) to \(Int16.max)”) print(“Integer32 ranges from \(Int32.min) to \(Int32.max)”) print(“Integer64 ranges from \(Int64.min) to \(Int64.max)”) print(“Float ranges from -\(FLT_MAX) to \(FLT_MAX)”) print(“Double ranges from -\(DBL_MAX) to \(DBL_MAX)”) print(“Decimal ranges from \(NSDecimalNumber.minimumDecimalNumber()) to \(NSDecimalNumber.maximumDecimalNumber())”) print(“One third as a float is \(NSNumber(float: 1.0/3))”) print(“One third as a double is \(NSNumber(double: 1.0/3))”)

print(“One third as a decimal is \(NSDecimalNumber.one().decimalNumberByDividingBy(NSDecimalNumber(float: 3)))”)

3. Managed Object Model Migration Anyone who has never made a mistake has never tried anything new. Albert Einstein In Chapter 2, “Managed Object Model Basics,” the fundamentals of managed object models were introduced, yet you were constrained to just one entity and a few attributes. The next logical step is to add to the model; however, this requires a number of preliminary steps to prevent errors caused by these changes. This chapter shows how to add model versions and model mappings, and it demonstrates different migration techniques you can choose when upgrading a model.

Changing a Managed Object Model As an application evolves, its managed object model probably needs to change, too. Simple changes, such as attribute defaults, validation rules, and fetch request templates can be modified without consequence. Other more structural changes require that persistent stores be migrated to new model versions. If a persistent store doesn’t have the appropriate mappings and settings required to migrate data from one version to the next, the application throws a “store is incompatible” error. Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter02.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. It is recommended that you use the iOS Simulator when following this chapter so you can inspect the contents of the SQLite database files easily. Update Groceries as follows to generate a “store is incompatible” error: 1. Run Groceries once to ensure the existing model has been used to create the persistent store. You should see the file system location of the store printed to the console log. 2. Select Model.xcdatamodeld in Xcode. 3. Add a new entity and rename it to Measurement. 4. Select the Measurement entity and add an attribute called abc. Set its type to String. 5. Rerun the application and examine the console log. You should now have generated arguably one of the most common Core Data errors, as shown in Figure 3.1. If this error has not appeared, delete the application and then click Product > Clean and

retry from step 1.

Figure 3.1 Store incompatibility This error isn’t an issue when an application is in its initial development phase. To get past it, you can just delete the application from the device and run it again from Xcode. When the application is run for the first time after being deleted, the persistent store is created based on the latest model. This makes the store compatible with the model, so the application won’t throw the error anymore. However, it won’t have any old data in it. As such, this scenario is unacceptable for any application already available on the App Store. There are a few approaches to migrating existing persistent stores, and the migration path you choose is driven by the complexity of the changes and whether you’re using iCloud. Whatever you do, you first need to become familiar with model versioning. Update Groceries as follows to revert to the original model: 1. Select Model.xcdatamodeld. 2. Delete the Measurement entity. 3. Rerun the application, which now should not throw an error.

Adding a Model Version To avoid the error shown in Figure 3.1, you need to create a new model version before making changes to the model. Ongoing, you should not remove old versions of a model. Old model versions are needed to help migrate incompatible persistent stores to the current model version. If there are no existing persistent stores on customer devices, you can ignore model versioning until your application is on the App Store. Update Groceries as follows to add a model version: 1. Select Model.xcdatamodeld. 2. Click Editor > Add Model Version…. 3. Click Finish to accept Model 2 as the version name. You should now have two model versions, as shown in Figure 3.2.

Figure 3.2 Multiple model versions The new model Model 2.xcdatamodel starts out as a replica of Model.xcdatamodel. This unfortunately makes it easy to modify the wrong version unintentionally. Before you edit a model, you should triple-check you have selected the correct one. You may want to get into the habit of taking a snapshot, committing to source control, or even backing up the whole project prior to editing a model. Note that the check mark in the green circle represents the current model, which is the model version used at runtime. Update Groceries as follows to reintroduce the Measurement entity: 1. Optionally take a snapshot or back up the Groceries project. 2. Select Model 2.xcdatamodel. 3. Add a new entity and rename it to Measurement. 4. Select the Measurement entity and add an attribute called abc. Set its type to String. After you add the new model version, you still need to set it as the current version before it is used by the application. Update Groceries as follows to change the current model version: 1. Select Model.xcdatamodeld (not Model.xcdatamodel). 2. Click View > Utilities > Show File Inspector (or press Option+ +1). 3. Set the Current Model Version to Model 2, as shown at the bottom of Figure 3.3.

Figure 3.3 Setting the current model Before you can successfully launch the application, you need to configure migration options to tell Core Data how to migrate. Feel free to launch it again to generate the incompatible store error in the meantime.

Lightweight Migration Whenever a new model is set as the current version, existing persistent stores must be migrated to use them. This is because the persistent store coordinator tries to use the current model to open the existing store, which fails if the store was created using a previous version of the model. The process of store migration can be handled automatically by passing an options dictionary to a persistent store coordinator when a store is added: When the NSMigratePersistentStoresAutomaticallyOption is true (1) and passed to a persistent store coordinator, Core Data automatically attempts to migrate incompatible persistent stores to the current model. When the NSInferMappingModelAutomaticallyOption is true (1) and passed to a persistent store coordinator, Core Data automatically attempts to infer a best guess at what attributes from the source model entities should end up as attributes in the destination model entities. Using those persistent store coordinator options together is called lightweight migration and is demonstrated in bold in Listing 3.1. These options are set in an updated localStore variable of CDHelper.swift. Note that if you’re using iCloud, this is your only choice for migration. Listing 3.1 The Local Store (CDHelper.swift localStore) Click here to view code image

// MARK: - STORE lazy var localStore: NSPersistentStore? = { let options:[NSObject:AnyObject] = [NSSQLitePragmasOption: [“journal_mode”:“DELETE”], NSMigratePersistentStoresAutomaticallyOption:

NSInferMappingModelAutomaticallyOption:1] var _localStore:NSPersistentStore? do { _localStore = try self.coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: self.localStoreURL, options: options) return _localStore } catch { return nil } }()

Update Groceries as follows to enable lightweight migration: 1. Replace the existing localStore variable code in CDHelper.swift with the code from Listing 3.1. The key change to be aware of is the introduction of the bold code. 2. Rerun the application, which should not throw an error. From now on, any time you set a new model as the current version and lightweight migration is enabled, the migration should occur transparently. Before other migration types can be demonstrated, some test data needs to be generated. Listing 3.2 contains code that generates managed objects based on the Measurement entity. You may notice that this code blocks the user interface until it finishes because the context runs on the main thread. More appropriate ways to insert test data in the background are demonstrated later in the book. Listing 3.2 Inserting Test Measurement Data (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context for i in 0…50000 { if let newMeasurement = NSEntityDescription.insertNewObjectForEntityForName(“Measurement”, inManagedObjectContext: context) as? Measurement { newMeasurement.abc = “—>> LOTS OF TEST DATA x\(i)” print(“Inserted \(newMeasurement.abc!)”) } } CDHelper.saveSharedContext() }

Update Groceries as follows to generate test data: 1. Create an NSManagedObject subclass of the Measurement entity. As discussed in Chapter 2, this is achieved by first selecting the entity and then clicking Editor > Create NSManagedObject Subclass… and following the prompts. When it comes time to save the class file, don’t forget to save the file in the Data Model group and check the Groceries target. 2. Replace the demo function in AppDelegate.swift with the code from Listing 3.2.

3. Run the application once. This inserts a lot of test data into the persistent store, which you can monitor by examining the console log. This may take a little while, depending on the speed of your machine. It’s important to have a fair amount of data in the persistent store to demonstrate the speed of migrations later. Note that the table view still remains blank because it has not yet been configured to display anything. The next step is to reconfigure the demo function to show some of what’s in the persistent store. The code shown in Listing 3.3 fetches a small sample of Measurement data. Notice that a new option is included that limits fetched results to 50. This is great for limiting how many results are fetched from large data sets, and it is even more powerful when mixed with sorting to generate a Top-50, for example. Listing 3.3 Fetching Test Measurement Data (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context let request = NSFetchRequest(entityName: “Measurement”) request.fetchLimit = 50 do { if let measurements = try context.executeFetchRequest(request) as? [Measurement] { for measurement in measurements { print(“Fetched Measurement Object \(measurement.abc!)”) } } } catch { print(“ERROR executing a fetch request: \(error)”) } }

Update Groceries as follows to print a sample of the store contents to the console log: 1. Replace the demo function in AppDelegate.swift with the code from Listing 3.3. 2. Run the application. The console log should show 50 rows of seemingly random measurement objects. 3. Examine the contents of the LocalStore.sqlite file using SQLite Database Browser, as explained previously in Chapter 2. Figure 3.4 shows the expected results when viewing the ZMEASUREMENT table, which is the data for the Measurement entity.

Figure 3.4 Test data ready for the next parts of this chapter Close the SQLite Database Browser before continuing.

Default Migration Sometimes, you need more control than what lightweight migration offers. Let’s say, for instance, you want to replace the Measurement entity with another entity called Amount. You also want the abc attribute from the Measurement entity to end up as an xyz attribute in the Amount entity. Any existing abc data should also be migrated to the xyz attribute. To achieve these requirements, you need to create a model mapping to manually specify what maps to where. When the persistent store option NSInferMappingModelAutomaticallyOption is true (1), Core Data still checks to see whether there are any model-mapping files it should use before trying to infer automatically. It is recommended that you disable this setting while you’re testing a mapping model. This way, you can be certain that the mapping model is being used and is functioning correctly. Update Groceries as follows to disable automatic model mapping: 1. Set the NSInferMappingModelAutomaticallyOption option in the localStore variable of CDHelper.swift to false by changing the 1 to a 0. Update Groceries as follows to add a new model in preparation for the migration from the Measurement entity to the Amount entity: 1. Optionally take a snapshot or back up the project. 2. Add a new model version called Model 3 based on Model 2 (Editor > Add Model Version…).

3. Set Model 3 as the current model version. 4. Select Model 3.xcdatamodel. 5. Delete the Measurement entity. 6. Add a new entity called Amount with a String attribute called xyz. 7. Create an NSManagedObject subclass of the Amount entity. When it comes time to save the class file, don’t forget to save the file in the Data Model group and check the Groceries target. 8. Replace the demo function of AppDelegate.swift with the code from Listing 3.4. Similar to the code being replaced, this code simply fetches a small sample of Amount data instead of Measurement data. 9. Run the application, which should throw the “Can’t find mapping model for migration” error shown in Figure 3.5.

Figure 3.5 A mapping model is required when mapping is not inferred Listing 3.4 Fetching Test Amount Data (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context let request = NSFetchRequest(entityName: “Amount”) request.fetchLimit = 50 do { if let amounts = try context.executeFetchRequest(request) as? [Amount] { for amount in amounts { print(“Fetched Amount Object \(amount.xyz!)”) } } } catch { print(“ERROR executing a fetch request: \(error)”) } }

To resolve the error shown in Figure 3.5, you need to create a mapping model that shows where attributes map. Specifically, the requirement is to map the old Measurement abc attribute to the new Amount xyz attribute. Update Groceries as follows to add a new mapping model: 1. Ensure the Data Model group is selected.

2. Click File > New > File…. 3. Select iOS > Core Data > Mapping Model and then click Next. 4. Select Model 2.xcdatamodel as the Source Data Model and then click Next. 5. Select Model 3.xcdatamodel as the Target Data Model and then click Next. 6. Set the mapping model name to save as Model2toModel3. 7. Ensure the Groceries target is checked and then click Create. 8. Select Model2toModel3.xcmappingmodel. You should now be presented with the model-mapping editor, as shown in Figure 3.6.

Figure 3.6 The model-mapping editor The mappings you’re presented with are a best guess based on what Core Data can infer on its own. On the left you should see Entity Mappings, showing what source entities map to what destination entities. You should also see in Figure 3.6 how the source Item entity has already inferred that it should map to the destination Item entity, which is a fair assumption. The naming standard of an entity mapping is SourceToDestination. With this in mind, notice the Amount entity doesn’t seem to have a source entity because it never existed in the source model. Update Groceries as follows to map the old Measurement entity to the new Amount entity: 1. Select the Amount entity mapping. 2. Click View > Utilities > Show Mapping Model Inspector (if that’s not visible in the menu system, press Option+ +3). You need to be able to see the pane shown on the right in Figure 3.7.

Figure 3.7 Custom entity mapping of MeasurementToAmount 3. Set the Source of the Amount entity mapping to Measurement. The expected result is shown in Figure 3.7. Because Measurement was selected as the source entity for the Amount destination entity, the Entity Mapping Name was automatically renamed to MeasurementToAmount. In addition, the mapping type changed from Add to Transform. For more complex implementations, you can specify a custom policy in the form of an NSEntityMigrationPolicy subclass. By overriding createDestinationInstancesForSourceInstance in the subclass, you can manipulate the data that’s migrated. For example, you could intercept the values of the abc attribute, set them all to title case, and then migrate them to the xyz attribute. The Source Fetch option shown at the bottom right of Figure 3.7 allows you to limit the migrated data to the results of a predicated (filtered) fetch. This is useful if you only want a subset of the existing data to be migrated. The predicate format you use here is the same as the format you would use when normally configuring a predicate, except you use $source variables. An example of a predicate that would filter out nil source data from the abc attribute is $source.abc != nil. Select the ItemToItem entity mapping shown previously in Figure 3.6 and examine its attribute mappings. Notice how each destination attribute has a Value Expression set. Now examine the MeasurementToAmount entity mapping. Notice there’s no value expression for the xyz destination attribute. This means that the xyz attribute has no source attribute, and you need to set one using the same format used in the ItemToItem entity mapping. The original requirement was to map the abc attribute to the xyz attribute, so that’s what needs configuring here. Update Groceries as follows to set an appropriate value expression for the xyz destination attribute: 1. Set the Value Expression for the xyz destination attribute of the MeasurementToAmount entity mapping to $source.abc. 2. Run the application. So long as the migration has been successful, you should see the expected result in the console log, as shown in Figure 3.8.

Figure 3.8 Results of a successfully mapped model To verify the migration has persisted to the store, examine the contents of the LocalStore.sqlite file using the techniques discussed in Chapter 2. The expected result is shown in Figure 3.9, which illustrates the new ZAMOUNT table (that is, Amount entity) with the data from the old Measurement entity.

Figure 3.9 A successfully mapped model Close the SQLite Database Browser before continuing.

Migration Manager Instead of letting a persistent store coordinator perform store migrations, you may want to manually migrate stores using an instance of NSMigrationManager. Using a migration manager still uses a mapping model; however, the difference is you have total control over the migration and the ability to report progress. To be certain that migration is being handled manually, automatic migration should be disabled. Update Groceries as follows to disable automatic migration: 1. Set the NSMigratePersistentStoresAutomaticallyOption option in

the localStore variable of CDHelper.swift to false by changing the 1 to a 0. Reporting on the progress of a migration is useful for keeping the user informed (and less annoyed) about a slow launch. Although most migrations should be fast, some large databases requiring complex changes can take a while to migrate. To keep the user interface responsive, the migration must be performed on a background thread. At the same time, the user interface has to be responsive to provide updates to the user. The challenge is to prevent the user from attempting to use the application during the migration. This is because the data won’t be ready yet, so you don’t want the user staring at a blank screen wondering what’s going on. This is where a migration progress view comes into play. Update Groceries as follows to configure a migration View Controller: 1. Select Main.storyboard. 2. Drag a new View Controller onto the storyboard, placing it above the existing Navigation Controller. 3. Drag a new Label and Progress View onto the new View Controller. 4. Position the Progress View directly in the center of the View Controller and then position the Label above it in the center. 5. Widen the Label and Progress View to the width of the View Controller margins, as shown in the center of Figure 3.10.

Figure 3.10 Migration View Controller 6. Configure the Label with Centered text that reads Migration Progress 0%, as shown in the center of Figure 3.10. 7. Configure the Progress View progress to 0. 8. Select the View Controller and set its Storyboard ID to migration using Identity Inspector (Option+ +3). 9. Optionally configure the following layout constraints by holding down the control key and dragging from the progress bar toward the applicable margin. You may skip this step if you’re uncomfortable with constraints as it is not critical. Leading Space to Container Margin

Trailing Space to Container Margin Center Vertically In Container 10. Optionally configure the following layout constraints by holding down the control key and dragging from the progress label toward the applicable margin. You may skip this step if you’re uncomfortable with constraints as it is not critical. Leading Space to Container Margin Trailing Space to Container Margin Vertical Spacing from the progress bar

Introducing MigrationVC.swift (Migration View Controller Code) The new migration View Controller has UILabel and UIProgressView interface elements that need updating during a migration. This means that a way to refer to these interface elements in code is required. A new UIViewController subclass called MigrationVC should be created for this purpose. Update Groceries as follows to add a MigrationVC file to a new group: 1. Right-click the existing Groceries group and then select New Group. 2. Set the new group name to View Controllers. This group will contain all of the view controllers. As a side note, feel free to move ViewController.swift to the trash because it is no longer required. 3. Select the View Controllers group. 4. Click File > New > File…. 5. Create a new iOS > Source > Cocoa Touch Class and then click Next. 6. Set the subclass to UIViewController and the filename to MigrationVC. 7. Ensure the language is Swift and then click Next. 8. Ensure the Groceries target is checked and that the new file will be saved in the Groceries project directory; then click Create. 9. Select Main.storyboard. 10. Set the Custom Class of the new migration View Controller to MigrationVC using Identity Inspector (Option+ +3) while the View Controller is selected. This is in the same place as where the Storyboard ID was set. 11. Show the Assistant Editor by clicking View > Assistant Editor > Show Assistant Editor (or pressing Option+ +Return). 12. Ensure the Assistant Editor is automatically showing MigrationVC.swift. The top-right of Figure 3.11 shows what this looks like. If you need to, just click Manual or Automatic while the migration View Controller is selected and select MigrationVC.swift.

Figure 3.11 Creating storyboard-linked properties to MigrationVC.swift 13. Hold down the control key while dragging a line from the migration progress label to the code in MigrationVC.swift on the line before the viewDidLoad function. When you let go of the mouse button, a pop-up appears. In the pop-up, set the Name to label and ensure the Storage is set to Strong before clicking Connect. Figure 3.11 shows the intended configuration. 14. Repeat the technique in step 13 to create a linked UIProgressView variable from the progress view called progressView. There should now be an @IBOutlet called label and an @IBOutlet called progressView in MigrationVC.swift. You may now switch back to the Standard Editor ( + return). When a migration occurs, notifications that communicate progress need to be sent. For the progress bar to reflect the progress, a new function is required in MigrationVC.swift. In addition, this function needs to be called every time a progress update is observed. Listing 3.5 shows the new code involved in bold. Listing 3.5 Migration View Controller (MigrationVC.swift) Click here to view code image import UIKit class MigrationVC: UIViewController { @IBOutlet var label: UILabel! @IBOutlet var progressView: UIProgressView! // MARK: - MIGRATION func progressChanged (note:AnyObject?) { if let _note = note as? NSNotification { if let progress = _note.object as? NSNumber { let progressFloat:Float = round(progress.floatValue * 100) let text = “Migration Progress: \(progressFloat)%” print(text) dispatch_async(dispatch_get_main_queue(), { self.label.text = text self.progressView.progress = progress.floatValue })

} else {print(“\(__FUNCTION__) FAILED to get progress”)} } else {print(“\(__FUNCTION__) FAILED to get note”)} } override func viewDidLoad() { super.viewDidLoad() NSNotificationCenter.defaultCenter().addObserver(self, selector: “progressChanged:”, name: “migrationProgress”, object: nil) } deinit { NSNotificationCenter.defaultCenter().removeObserver(self, name: “migrationProgress”, object: nil) } }

The progressChanged function simply unwraps the progress notification and constructs a string with the migration completion percentage. It then updates the user interface with this information. Of course, none of this can happen without first adding an observer of the migrationProgress variable in the viewDidLoad function. When the view deinitializes, it is unregistered as an observer of the migrationProgress variable. Update Groceries as follows to ensure migration progress is reported to the user: 1. Replace all code in MigrationVC.swift with the code from Listing 3.5. The user interface is now positioned to report migration progress to the user. The next step is to implement the code required to perform a manual migration.

Introducing CDMigration.swift (Core Data Migration Code) To keep CDHelper.swift small, the code required to perform a managed migration is put in a new class called CDMigration.swift. The starting point to this class is shown in Listing 3.6. Listing 3.6 Migration View Controller Shared Instance (CDMigration.swift shared) Click here to view code image import UIKit import CoreData private let _sharedCDMigration = CDMigration() class CDMigration: NSObject { // MARK: - SHARED INSTANCE class var shared : CDMigration { return _sharedCDMigration } }

Just like CDHelper.swift, CDMigration.swift has a shared function that makes it easy to use because you can call it from anywhere in the project via

CDMigration.shared. Update Groceries as follows to implement CDMigration.swift: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the filename to CDMigration and ensure the Groceries target is checked. 5. Ensure the Groceries project directory is open and then click Create. 6. Replace the contents of CDMigration.swift with the code from Listing 3.6. To handle migrations manually, three supporting functions are required. One function checks that a given store exists and another checks that it needs migrating. A successful migration generates a separate compatible store, so as soon as migration completes, this new store needs to replace the incompatible one. The final supporting function does exactly that—it replaces the incompatible store with the migrated store. Listing 3.7 shows the code involved with these three supporting functions. Listing 3.7 Migration View Controller Supporting Functions (CDMigration.swift storeExistsAtPath, store, replaceStore) Click here to view code image // MARK: - SUPPORTING FUNCTIONS func storeExistsAtPath(storeURL:NSURL) -> Bool { if let _storePath = storeURL.path { if NSFileManager.defaultManager().fileExistsAtPath(_storePath) { return true } } else {print(“\(__FUNCTION__) FAILED to get store path”)} return false } func store(storeURL:NSURL, isCompatibleWithModel model:NSManagedObjectModel) -> Bool { if self.storeExistsAtPath(storeURL) == false { return true // prevent migration of a store that does not exist } do { var _metadata:[String : AnyObject]? _metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(NSSQLiteStoreType, URL: storeURL, options: nil) if let metadata = _metadata { if model.isConfiguration(nil, compatibleWithStoreMetadata: metadata) { print(“The store is compatible with the current version of the model”) return true } } else {print(“\(__FUNCTION__) FAILED to get metadata”)} } catch { print(“ERROR getting metadata from \(storeURL) \(error)”)

} print(“The store is NOT compatible with the current version of the model”) return false } func replaceStore(oldStore:NSURL, newStore:NSURL) throws { let manager = NSFileManager.defaultManager() do { try manager.removeItemAtURL(oldStore) try manager.moveItemAtURL(newStore, toURL: oldStore) } }

The storeExistsAtPath function uses NSFileManager to determine whether a store exists at the given URL. It returns a Bool indicating the result. The store:isCompatibleWithModel function first checks that a store exists at the given path. If there is no store, true is returned because this prevents a migration from being attempted. If a store exists at the given URL, it is checked for model compatibility against the given model. To do this, the model used to create the store is drawn from the store’s metadata and then compared to the given model via its isConfiguration:compatibleWithStoreMetadata function. The replaceStore function uses NSFileManager to remove the incompatible store from the file system and then replaces it with the compatible store. Update Groceries as follows to implement a new SUPPORTING FUNCTIONS section: 1. Add the code from Listing 3.7 to the bottom of CDMigration.swift before the last curly brace. When a migration is in progress, the value of the migration manager’s migrationProgress variable is constantly updated. This is information that the user needs to see, so a function is required to react whenever the migrationProgress value changes. Listing 3.8 shows a new function that posts a notification whenever this value changes. Listing 3.8 Migration View Controller Progress Reporting (CDMigration.swift observeValueForKeyPath) Click here to view code image // MARK: - PROGRESS REPORTING override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) { if object is NSMigrationManager, let manager = object as? NSMigrationManager { if let notification = keyPath { NSNotificationCenter.defaultCenter().postNotificationName(notification, object: NSNumber(float: manager.migrationProgress)) }

} else {print(“observeValueForKeyPath did not receive a NSMigrationManager class”)} }

Update Groceries as follows to implement a new PROGRESS REPORTING section: 1. Add the code from Listing 3.8 to the bottom of CDMigration.swift before the last curly brace. The next function is where the actual migration happens. Most of this function is used to gather all the pieces required to perform a migration. Listing 3.9 shows the code involved. Listing 3.9 Migration (CDMigration.swift migrateStore) Click here to view code image // MARK: - MIGRATION func migrateStore(store:NSURL, sourceModel:NSManagedObjectModel, destinationModel:NSManagedObjectModel) { if let tempdir = store.URLByDeletingLastPathComponent { let tempStore = tempdir.URLByAppendingPathComponent(“Temp.sqlite”) let mappingModel = NSMappingModel(fromBundles: nil, forSourceModel: sourceModel, destinationModel: destinationModel) let migrationManager = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel) migrationManager.addObserver(self, forKeyPath: “migrationProgress”, options: NSKeyValueObservingOptions.New, context: nil) do { try migrationManager.migrateStoreFromURL(store, type: NSSQLiteStoreType, options: nil,withMappingModel: mappingModel, toDestinationURL: tempStore, destinationType: NSSQLiteStoreType, destinationOptions: nil) try replaceStore(store, newStore: tempStore) print(“SUCCESSFULLY MIGRATED \(store) to the Current Model”) } catch { print(“FAILED MIGRATION: \(error)”) } migrationManager.removeObserver(self, forKeyPath: “migrationProgress”) } else {print(“\(__FUNCTION__) FAILED to prepare temporary directory”)} }

The migrateStore function needs to be given a store to migrate, a source model to migrate from, and destination model to migrate to. The source model could have been taken from the given store’s metadata; however, seeing as this step is performed first in another function, this approach saves repeated code. The first thing migrateStore does is prepare four variables: The tempdir variable holds the URL to the given store and is used to build a URL to a temporary store used for migration. The tempStore variable holds the URL to the temporary store used for migration.

The mappingModel variable holds an instance of NSMappingModel specific to the models being migrated from and to. The migration will fail without a mapping model. The migrationManager variable holds an instance of NSMigrationManager based on the source and destination models. An observer is added for the migrationProgress variable so that the observeValueForKeyPath function is called whenever the migrationProgress variable changes. All these variables are then used to make a call to the migrateStoreFromURL function, which is responsible for migrating the given store to be compatible with the destination model. Once this is complete, the old incompatible store is removed and the new compatible store is put in its place. Update Groceries as follows to implement a new MIGRATION section: 1. Add the code from Listing 3.9 to the bottom of CDMigration.swift before the final closing curly brace. The migration code that has just been implemented needs to be called from a background thread so that the user interface can be updated without freezing. This, along with the instantiation of the progress view that the user sees, is shown in Listing 3.10. Listing 3.10 Migration Progress (CDMigration.swift migrateStoreWithProgressUI) Click here to view code image func migrateStoreWithProgressUI(store:NSURL, sourceModel:NSManagedObjectModel, destinationModel:NSManagedObjectModel) { // Show migration progress view preventing the user from using the app let storyboard = UIStoryboard(name: “Main”, bundle: nil) if let initialVC = UIApplication.sharedApplication().keyWindow?.rootViewController as? UINavigationController { if let migrationVC = storyboard.instantiateViewControllerWithIdentifier(“migration”) as? MigrationVC {

initialVC.presentViewController(migrationVC, animated: false, completion: { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BAC 0), { print(“BACKGROUND Migration started…”) self.migrateStore(store, sourceModel: sourceModel, destinationModel: destinationModel) dispatch_async(dispatch_get_main_queue(), { // trigger the stack setup again, this time with the upgraded store let _ = CDHelper.shared.localStore dispatch_after(2, dispatch_get_main_queue(), { migrationVC.dismissViewControllerAnimated(false,

completion: nil) }) }) }) }) } else {print(“FAILED to find a view controller with a story board id of ‘migration’”)} } else {print(“FAILED to find the root view controller, which is supposed to be a navigation controller”)} }

The migrateStoreWithProgressUI function uses a storyboard identifier to instantiate and present the migration view. Once the view is blocking user interaction the migration can begin. The migrateStore function is called on a background thread. Once migration is complete, the localStore is loaded as usual, the migration view is dismissed, and normal use of the application can resume. Update Groceries as follows to implement the migrateStoreWithProgressUI function: 1. Add the code from Listing 3.10 to the MIGRATION section at the bottom of CDMigration.swift before the last curly brace. The final piece of code required in CDMigration.swift is used to migrate the store if necessary. This function is called from the setupCoreData function of CDHelper.swift, which is run as a part of initialization. Listing 3.11 shows the code involved. Listing 3.11 Migration (CDMigration.swift migrateStoreIfNecessary) Click here to view code image func migrateStoreIfNecessary (storeURL:NSURL, destinationModel:NSManagedObjectModel) { if storeExistsAtPath(storeURL) == false { return } if store(storeURL, isCompatibleWithModel: destinationModel) { return } do { var _metadata:[String : AnyObject]? _metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(NSSQLiteStoreType, URL: storeURL, options: nil) if let metadata = _metadata, let sourceModel = NSManagedObjectModel.mergedModelFromBundles([NSBundle.mainBundle()], forStoreMetadata: metadata) { self.migrateStoreWithProgressUI(storeURL, sourceModel: sourceModel, destinationModel: destinationModel) } } catch { print(“\(__FUNCTION__) FAILED to get metadata \(error)”) } }

Once it’s established that the given store exists, a model compatibility check is performed and the store is migrated if necessary. The model used to create the given store is drawn from the store’s metadata. This is then given to the migrateStoreWithProgressUI function. Update Groceries as follows to implement the migrateStoreIfNecessary function: 1. Add the code from Listing 3.11 to MIGRATION section at the bottom of CDMigration.swift before the last curly brace. When CDHelper.swift initializes, a call is made to setupCoreData. This is an ideal time to check that the localStore is compatible with the current model, before it’s needed. The new code required in the setupCoreData is shown in bold in Listing 3.12. Listing 3.12 Migration During Setup (CDHelper.swift setupCoreData) Click here to view code image func setupCoreData() { // Model Migration if let _localStoreURL = self.localStoreURL { CDMigration.shared.migrateStoreIfNecessary(_localStoreURL, destinationModel: self.model) } // Load Local Store _ = self.localStore }

Update Groceries as follows to ensure a manual migration is triggered as required: 1. Replace the setupCoreData function of CDHelper.swift with the code from Listing 3.12. Currently the localStore variable of CDHelper.swift always tries to return a store. If it tried to return a store that wasn’t compatible with the current model, the application would throw an error. To prevent this, a check is needed to see whether the store needs migrating before it is loaded. This check is needed only when migration is handled manually, so the bold code in Listing 3.13 wouldn’t be required otherwise. Listing 3.13 Triggering Migration Manager (CDHelper.swift localStore) Click here to view code image lazy var localStore: NSPersistentStore? = { let useMigrationManager = true if let _localStoreURL = self.localStoreURL { if useMigrationManager == true && CDMigration.shared.storeExistsAtPath(_localStoreURL) && CDMigration.shared.store(_localStoreURL, isCompatibleWithModel: self.model) == false { return nil // Don’t return a store if it’s not compatible with

the model } }

let options:[NSObject:AnyObject] = [NSSQLitePragmasOption: [“journal_mode”:“DELETE”], NSMigratePersistentStoresAutomaticallyOption: NSInferMappingModelAutomaticallyOption:0] var _localStore:NSPersistentStore? do { _localStore = try self.coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: self.localStoreURL, options: options) return _localStore } catch { return nil } }()

Update Groceries as follows to ensure a localStore is not returned when the migration manager is used and a manual migration is required: 1. Replace the localStore variable in CDHelper.swift with the code from Listing 3.13. For progress to be shown to the user, the interface needs to be ready before a migration is triggered. This means that the first call to anything Core Data related should be made from an existing view after it has loaded. To demonstrate the migration process, a small amount of code needs to be applied to the existing table view. The only table view in the application so far is the Prepare table view, where the user adds items to the shopping list. Listing 3.14 shows the minimal code involved that triggers a store model migration. Note that the table view won’t be configured to display anything until later in the book. Listing 3.14 The Prepare Table View Controller (PrepareTVC.swift) Click here to view code image import UIKit class PrepareTVC: UITableViewController { override func viewDidLoad() { super.viewDidLoad() } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) // Trigger Demo Code if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate { appDelegate.demo() } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 0 }

override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 0 } }

The PrepareTVC.swift code is the bare minimum code required to show a table view. You might notice that the view is configured to return no rows or sections, as the intent for the moment is just to show model migration progress. Update Groceries as follows to implement PrepareTVC.swift: 1. Right-click the existing Groceries group and then select New Group. 2. Set the new group name to Table View Controllers. 3. Select the Table View Controllers group. 4. Click File > New > File…. 5. Create a new iOS > Source > Swift File and then click Next. 6. Set the filename to PrepareTVC and ensure the Groceries target is checked. 7. Ensure the Groceries project directory is open and then click Create. 8. Replace the contents of PrepareTVC.swift with the code from Listing 3.14. 9. Select Main.storyboard. 10. Set the Custom Class of the Table View Controller to PrepareTVC using Identity Inspector (Option+ +3) while the Table View Controller is selected. 11. Remove the call to CDHelper.shared from the application:didFinishLaunchingWithOptions function of AppDelegate.swift. This code would otherwise trigger a migration before the user interface was ready. 12. Remove the call to demo() from the application:applicationDidBecomeActive function of AppDelegate.swift. This code would otherwise trigger a migration before the user interface was ready. Almost everything is in place to perform a manual migration; however, a new managed object model and mapping model are required to show what attributes map to where. Update Groceries as follows to prepare the new model: 1. Add a model version called Model 4 based on Model 3. 2. Set Model 4 as the current model. 3. Select Model 4.xcdatamodel. 4. Delete the Amount entity. 5. Add a new entity called Unit with a String attribute called name.

6. Set the default value of the name attribute to New Unit. 7. Create an NSManagedObject subclass of the Unit entity. When it comes time to save the class file, don’t forget to check the Groceries target and ensure that the Data Model group is selected. 8. Create a new mapping model with Model 3 as the source and Model 4 as the target. When it comes time to save the mapping model file, don’t forget to check the Groceries target and save the mapping model as Model3toModel4. 9. Select Model3toModel4.xcmappingmodel. 10. Select the Unit entity mapping. 11. Set the Source of the Unit entity to Amount and the Value Expression of the name destination attribute to $source.xyz. You should see the Unit entity mapping automatically renamed to AmountToUnit, as shown in Figure 3.12.

Figure 3.12 Mapping model for AmountToUnit You’re almost ready to perform a migration; however, the fetch request in the demo function still refers to the old Amount entity. Listing 3.15 shows an updated version of this function. Listing 3.15 Fetching Test Unit Data (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context let request = NSFetchRequest(entityName: “Unit”) request.fetchLimit = 50 do { if let units = try context.executeFetchRequest(request) as? [Unit] { for unit in units { print(“Fetched Unit Object \(unit.name!)”) } } } catch { print(“ERROR executing a fetch request: \(error)”) } }

Update Groceries as follows to refer to the Unit entity instead of the Amount entity:

1. Replace the demo function of AppDelegate.swift with the code shown in Listing 3.15. This code just fetches 50 Unit objects from the persistent store. The migration manager is finally ready! Run the application and pay close attention! You should see the migration manager flash before your eyes, alerting you to the progress of the migration. The progress is also shown in the console log (see Figure 3.13).

Figure 3.13 Visible migration progress Examine the contents of the ZUNIT table in the LocalStore.sqlite file using the techniques discussed in Chapter 2. The expected result is shown in Figure 3.14.

Figure 3.14 Successful use of migration manager If you reproduced the results shown in Figure 3.14, give yourself a pat on the back because you successfully implemented three types of model migration! The rest of the

book uses lightweight migrations, so it needs to be re-enabled. Before you continue, close the SQLite Database Browser. Update Groceries as follows to re-enable lightweight migration: 1. Set useMigrationManager to false in the localStore variable of CDHelper.swift. 2. Set the NSMigratePersistentStoresAutomaticallyOption option in the localStore variable of CDHelper.swift to true by changing the 0 to a 1. 3. Set the NSInferMappingModelAutomaticallyOption option in the localStore variable of CDHelper.swift to true by changing the 0 to a 1. 4. Comment out the call to migrateStoreIfNecessary from the setupCoreData function of CDHelper.swift. 5. Replace the code in the demo function of AppDelegate.swift with a call to CDHelper.shared. This ensures that the Core Data stack is set up without a reliance on particular entities. The old mapping models and NSManagedObject subclasses of entities that don’t exist anymore are no longer needed. Although you could remove them, leave them in the project for reference sake.

Summary You’ve now experienced lightweight migration, default migration, and using a migration manager to display progress. You should now be able to make an informed decision when determining between migration options for your own applications. Don’t forget that the only migration option for iCloud-enabled Core Data applications is lightweight migration. Adding model versions should now be a familiar procedure because the model has changed several times already.

Exercises Why not build on what you’ve learned by experimenting? 1. Set the current model version to Model 3 and run the application. It should not throw an error because the downgrade of data is inferred automatically. Note that this is only because NSInferMappingModelAutomaticallyOption has been re-enabled. In reality, you would need a Model4toModel3 mapping model to map attributes properly. 2. Examine the contents of the ZAMOUNT table in LocalStore.sqlite and you’ll notice something critical: Where has all the data gone? There was no mapping model, so all the ZUNIT data was lost during the downgrade! 3. Set the current model to Model 4 and then run the application to trigger an automatic lightweight migration. This should be fast because there is no data in the store.

4. Managed Object Model Expansion Black holes are where God divided by zero. Albert Einstein In Chapter 3, “Managed Object Model Migration,” you learned how to manage a changing model with versioning, mappings, and migration techniques. This chapter exercises your newfound migration knowledge as it further introduces changes to the model. The topics expand beyond one or two entities with the introduction of relationships, and at the end of the chapter, entity inheritance. The implications of delete rules are discussed, along with the impact some of them can have on data validation.

Relationships Relationships link entities. Using relationships in the managed object model introduces a powerful means to connect logical areas of data represented by entities. Using relationships can significantly reduce the capacity requirements of a database. Instead of duplicating the same data in multiple entities, a relationship can be put in place as a pointer to a piece of data so it only needs to be stored once. Although de-duplication is one advantage, the true power of relationships is their capability to allow connections between complex data types. Consider the existing Item and Unit entities found in Model 4.xcdatamodel. Instances of the Item entity represent things you can put on a shopping list, such as the following: Chocolate Potatoes Milk Instances of the Unit entity represent units of measurement (g, Kg, and ml) appended to an item quantity. For example: 250g chocolate 4Kg potatoes 500ml milk If you really wanted to, you could simply have a string attribute in the Item entity called unit. For each item object, you could then populate the string representing the unit with something like g, Kg, or ml. The problem with that approach is that you’re wasting space in the database because you repeatedly duplicate the data for every Item’s unit. In addition to wasting all that space, you’re going to have a hard time updating a unit for all items. If you wanted to change Kg to kilogram, for example, you would have to iterate through all items with the string Kg and rename it to kilogram, item by item. This is highly inefficient and would make for a slow application. The solution is to instead use a relationship to a Unit entity, thereby just using a pointer to a single Kg object. This not only reduces the amount of storage space required, it also means you only have to update the unit name in one place to change all the units.

Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter03.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. It is recommended that you use the iOS Simulator when following this chapter so you can inspect the contents of the SQLite database files easily. Update Groceries as follows to prepare for this chapter: 1. Delete the application from the iOS Simulator. If the iOS Simulator is not open, just run the application once and then stop and delete it. 2. Click Product > Clean to ensure there’s no residual cache from previous incarnations of the project. 3. Run the application once on the iOS Simulator to generate an empty persistent store. Update Groceries as follows to create a relationship: 1. Optionally take a snapshot or back up the project. 2. Add a model version named Model 5 based on Model 4 using the techniques from Chapter 3. 3. Set Model 5 as the Current Model. 4. Select Model 5.xcdatamodel. 5. Change the Editor Style to Graph, as shown in the bottom right-hand corner of Figure 4.1. If the Item and Unit entities are sitting on top of each other, drag them apart.

Figure 4.1 A new relationship 6. Hold down Control and drag a line from the Item entity to the Unit entity. The expected result is shown in Figure 4.1. By creating a relationship between two entities using the Graph Editor Style, you’ve created an inverse relationship, which means two relationships in opposite directions between two entities. In this case, one relationship is from Item to Unit and the other is from Unit to Item. If instead you created a relationship using the Table Editor Style, the result would only be a one-way relationship. You need to manually add inverse relationships when using the Table Editor Style, provided that is what you want. With an inverse relationship in place, you can now do the following: Associate Item managed objects to Unit managed objects. Associate Unit managed objects to Item managed objects. This newfound association allows access to the attributes of the related entity through the relationship (for example, item.newRelationship.name). You next need to consider whether each direction of the relationship is To-Many or To-One. Determining this should also help you rename the relationship appropriately. A To-Many relationship allows a potentially unlimited number of destination objects, whereas the alternative limits the allowable number of destination objects to one. Consider the Item-to-Unit relationship: Configuring a To-Many relationship from the Item entity to the Unit entity would allow an item to have potentially unlimited units of measurement. This is not ideal because items on a shopping list only need one unit of measurement (such as Kg or pound). Note also that it is possible to configure a maximum number of related objects allowable through a To-Many relationship. Configuring a To-One relationship from the Item entity to the Unit entity would mean only one unit of measurement can be assigned to an item. This is ideal because

items on a shopping list only need one unit of measurement. A good name for the relationship in this direction would therefore be unit. With this new relationship name, you could then use item.unit.name to reference the related unit’s name through the item object. When you create an inverse relationship, you need to think about both directions. Again, consider the Unit-to-Item relationship: Configuring a To-One relationship from the Unit entity to the Item entity would prevent a unit from being used by more than one item. This is not ideal because many items on a shopping list should be able to use the same unit of measurement (such as 2Kg of onions and 1Kg of corn). Configuring a To-Many relationship from the Unit entity to the Item entity would allow a unit to be used by potentially unlimited items. This is ideal because many items on a shopping list need to use the same unit of measurement. A good name for the relationship in this direction would therefore be items. If you wanted to list all items related to a particular unit, it’s as easy as fetching unit.items to retrieve an NSSet of pointers to each related item. Update Groceries as follows to configure the relationships: 1. Rename the newRelationship in the Item entity to unit. 2. Rename the newRelationship in the Unit entity to items. 3. Change the items relationship Type to To-Many, as shown in Figure 4.2.

Figure 4.2 The unit and items relationships As you can see, the name of a relationship should reflect something in line with what it provides access to. With the unit and items relationships now configured, you can access relationship destination variables in code via item.unit and unit.items. That said, you first need new NSManagedObject subclasses of these entities before you can use this dot notation. Update Groceries as follows to refresh the existing NSManagedObect subclasses: 1. Ensure Model 5.xcdatamodel is selected.

2. Create a Swift NSManagedObect subclass of the Item and Unit entities using techniques discussed in the previous chapters. As you do so, ensure both the Item and Unit entities are checked on the Select the Entities You Would Like to Manage page. When it comes time to save the class file, don’t forget to check the Groceries target and ensure the Data Model group is selected. Examine Unit+CoreDataProperties.swift and look for the new items variable. The items variable represents a To-Many relationship, so the “many” is provided as an NSSet. The thing to know about an NSSet is that the objects within are not ordered, as opposed to an NSOrderedSet, NSArray, or [AnyObject] Swift array. The ordering of fetched objects is usually set with a sort descriptor passed to a fetch request, which, in contrast, returns an array. If you check the Ordered relationship setting available with a To-Many relationship, the resulting NSManagedObject variable type is an NSOrderedSet. Another difference between an NSSet and arrays to keep in mind is that an NSSet cannot contain duplicate objects. Examine Item+CoreDataProperties.swift, and notice there is a new unit variable of type Unit. The unit variable represents a To-One relationship, so the “one” is simply the class type of the target entity. Inserting a couple of items and setting both their unit attributes to the same thing is now a clear-cut exercise, as shown in Listing 4.1. For brevity this code unwraps the optional variables; however, in a shipping application you should use the if-let pattern. Listing 4.1 Relationship Creation (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context if let kg = NSEntityDescription.insertNewObjectForEntityForName(“Unit”, inManagedObjectContext: context) as? Unit, let oranges = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: context) as? Item, let bananas = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: context) as? Item { kg.name = “Kg” oranges.name = “Oranges” bananas.name = “Bananas” oranges.quantity = NSNumber(float: 1) bananas.quantity = NSNumber(float: 4) oranges.listed = NSNumber(bool: true) bananas.listed = NSNumber(bool: true) oranges.unit = kg bananas.unit = kg

print(“Inserted \(oranges.quantity!)\(oranges.unit!.name!) of \ (oranges.name!)”) print(“Inserted \(bananas.quantity!)\(bananas.unit!.name!) of \ (bananas.name!)”) CDHelper.saveSharedContext() } }

Update Groceries as follows to insert new items with a common unit: 1. Replace the demo function in AppDelegate.swift with the code from Listing 4.1. 2. Run the application once. The expected console log results are shown in Figure 4.3.

Figure 4.3 Successful insertion of items with a relationship to the same unit 3. Delete all code within the demo function of AppDelegate.swift. To prove there is only one row in the database representing the Kg object, examine the contents of the LocalStore.sqlite file using the techniques discussed in Chapter 2, “Managed Object Model Basics.” The expected result is shown in Figure 4.4.

Figure 4.4 The Unit entity in the SQLite database as ZUNIT Close the SQLite Database Browser before continuing.

Delete Rules An important relationship setting to be aware of is the delete rule. When an object is deleted, this setting determines what happens to related objects, as detailed here: The Nullify delete rule is a good default for most situations. When an object is deleted and this rule is in place, related objects nil out their relationship to the deleted object. For example, assume a unit object named Kg is related to some item objects. If a Nullify delete rule was set on the items relationship and the Kg unit object was deleted, the related item objects would set their unit variable to nil. The Cascade delete rule propagates deletions through the relationship. For example, assume a unit object named Kg is related to some item objects. If a Cascade delete rule was set on the items relationship and the Kg unit object was deleted, all the related item objects would be deleted, too. The Deny delete rule prevents the deletion of an object if it still has related objects. For example, assume a unit object named Kg is related to some item objects. If a Deny delete rule was set on the items relationship and the Kg unit object was

deleted, the existence of related item objects would cause a validation error when the context was saved. When you use a Deny delete rule, you need to ensure that there are no related objects at the relationship destination before deleting the source object. The No Action delete rule is a strange one that actually leaves your object graph in an inconsistent state. If you use this delete rule, it is up to you to manually set the inverse relationship to something valid. Only corner-case situations call for this delete rule.

Introducing CDOperation.swift (Core Data Operation) To test the results of object deletion, a new function is required that displays a count of objects specific to an entity within a context. This function will be added to a new class called CDOperation.swift. Throughout the course of the book other various useful and reusable functions are added to this class, too. Listing 4.2 shows the code involved in the starting point for CDOperation.swift. Listing 4.2 Core Data Operation (CDOperation.swift objectCountForEntity) Click here to view code image import Foundation import CoreData class CDOperation { class func objectCountForEntity (entityName:String, context:NSManagedObjectContext) -> Int { let request = NSFetchRequest(entityName: entityName) var error:NSError? let count = context.countForFetchRequest(request, error: &error) if let _error = error { print(“\(__FUNCTION__) Error: \(_error.localizedDescription)”) } else { print(“There are \(count) \(entityName) object(s) in \(context)”) } return count } }

The CDOperation.swift class starts out with a single class function called objectCountForEntity. This class function simply prints the number of objects found for a given entity and context in the console log. Update Groceries as follows to implement the CDOperation.swift class: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next.

4. Set the filename to CDOperation and ensure the Groceries target is checked. 5. Ensure the Groceries project directory is open and then click Create. 6. Replace the contents of CDOperation.swift with the code from Listing 4.2. The new objectCountForEntity function is called twice from the demo function of AppDelegate.swift, once for each entity. The code involved is shown in Listing 4.3. Listing 4.3 Delete Rule Test Preparation (AppDelegate.swift demo) Click here to view code image func demo () { CDOperation.objectCountForEntity(“Item”, context: CDHelper.shared.context) CDOperation.objectCountForEntity(“Unit”, context: CDHelper.shared.context) }

Update Groceries as follows to prepare for delete rule testing: 1. Replace the demo function of AppDelegate.swift with the code from Listing 4.3. 2. Run the application. The expected console log results are shown in Figure 4.5.

Figure 4.5 A count of item(s) and unit(s) in the persistent store The expected result shows there are two item objects and one unit object in the context. The two item objects are oranges and bananas, which were inserted as a part of implementing Listing 4.1. The only unit object is Kg, which both items are related to. It’s time to see what happens when the Kg unit is deleted when a Deny delete rule is in play. Update Groceries as follows to configure a Deny delete rule: 1. Select Model 5.xcdatamodel. 2. Select the items relationship in the Unit entity. 3. Set the items relationship Delete Rule to Deny using Data Model Inspector (Option+ +3). To delete an object you must specify what object to delete. This means that you first need to fetch it with a fetch request. Listing 4.4 shows reusable code that fetches objects for a given entity and context. It also supports optional filters and sorting for greater flexibility. Listing 4.4 Objects for Entity Class Function (CDOperation.swift objectsForEntity) Click here to view code image class func objectsForEntity(entityName:String, context:NSManagedObjectContext, filter:NSPredicate?, sort:

[NSSortDescriptor]?) -> [AnyObject]? { let request = NSFetchRequest(entityName:entityName) request.predicate = filter request.sortDescriptors = sort do { return try context.executeFetchRequest(request) } catch { print(“\(__FUNCTION__) FAILED to fetch objects for \(entityName) entity”) return nil } }

As described in Chapter 2, you need an NSFetchRequest to fetch existing objects. Once you have a fetch request you can apply (among other things) filtering and sorting. It’s not guaranteed that you will get any objects back from a fetch request, so the returned variable is optional. Note For clarity, the functions in CDOperation.swift do not surround the code with performBlock or performBlockAndWait. If the given context runs on a private queue, you need to wrap the function call with performBlock or performBlockAndWait. Don’t worry if you’re unfamiliar with these terms, they’re discussed in further detail in Chapter 8, “Preloading Data.” Update Groceries as follows to implement reusable code to fetch objects: 1. Add the code from Listing 4.4 to the bottom of CDOperation.swift before the last curly brace.

Delete Rule Testing With the supporting code required to fetch an object ready, the Deny delete rule can now be tested. The code to delete a unit object named Kg is shown in Listing 4.5. Listing 4.5 Unit Deletion (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context print(“Before deletion attempt:”) CDOperation.objectCountForEntity(“Item”, context:context) CDOperation.objectCountForEntity(“Unit”, context:context) let filter = NSPredicate(format: “name == %@”,“Kg”) if let units = CDOperation.objectsForEntity(“Unit”, context: context, filter: filter, sort: nil) as? [Unit] { for unit in units {

print(“Deleting Unit ‘\(unit.name!)’…”) context.deleteObject(unit) } } print(“After deletion attempt:”) CDOperation.objectCountForEntity(“Item”, context:context) CDOperation.objectCountForEntity(“Unit”, context:context) }

To find the unit object with the name “Kg”, a predicate was used to filter out any other Unit objects. The deletion is then invoked with a call to deleteObject. Update Groceries as follows to implement the code to delete a “Kg” unit object: 1. Replace the demo function in AppDelegate.swift with the code from Listing 4.5. 2. Run the application. The expected console log results are shown in Figure 4.6.

Figure 4.6 Deletion attempt Upon examining the console log, it appears that the Deny delete rule hasn’t worked. What’s going on here? Why are there no units anymore? Shouldn’t the Deny rule have prevented the Kg unit object from being deleted because oranges and bananas are related to it? They’re all good questions; however, the key point here is that the delete rule is only enforced when it comes time to save the context. Update Groceries as follows to save the context after the deletion: 1. Add CDHelper.saveSharedContext() to the demo function of AppDelegate.swift on the line after context.deleteObject(unit). 2. Rerun the application, which fails to save the context as shown in Figure 4.7.

Figure 4.7 The Deny delete rule working The delete rule only comes into effect when a context save is attempted. Even though the save fails, the context still reports that the unit object has been deleted and the delete rule

generates an NSCocoaErrorDomain error 1600. This error is an NSValidationRelationshipDeniedDeleteError. To get around it, you need to check that the unit object can be safely deleted before deleting it. If it is not safe to delete the object, you could do either of the following: Tell the user the deletion is denied and skip deletion. Nil out all unit.items first and then delete the unit object. In reality this type of issue would probably come up after a user tries to delete a unit from a table view, at which point you could inform the user of the issue. When a Deny delete rule is in place, you should use a function available from the super class NSManagedObject called validateForDelete. If a call to this function does not throw an error, it’s safe to delete the object in question. Listing 4.6 shows a call to this function wrapped in reusable code that prints out any errors when validating an object for deletion. Listing 4.6 Validate for Delete (CDOperation.swift objectName, objectDeletionIsValid) Click here to view code image class func objectName(object:NSManagedObject) -> String { if let name = object.valueForKey(“name”) as? String { return name } return object.description } class func objectDeletionIsValid(object:NSManagedObject) -> Bool { do { try object.validateForDelete() return true // object can be deleted } catch let error as NSError { print(”’\(objectName(object))’ can’t be deleted. \ (error.localizedDescription)”) return false // object can’t be deleted } }

The objectName function is used to print out the name of an object, so long as it has a string attribute called name. If no name attribute exists, the built-in description variable of the managed object is used instead. It’s harder to distinguish between managed objects when the built-in description variable is used; however, it’s a good enough fallback when the name attribute doesn’t exist. The objectDeletionIsValid function returns true when it is safe to delete an object. If it’s not safe, false is returned and the reason printed to the console log. This function should be used to check whether an object can be deleted before you try to delete it. This gives you a chance to resolve errors beforehand, likely by asking the user for input. Update Groceries as follows to enhance CDOperation.swift with deletion validation

functions: 1. Add the code from Listing 4.6 to the bottom of CDOperation.swift before the last curly brace. It’s now easy to check that an object can be deleted before deleting it. Listing 4.7 shows the new demo code in bold. Listing 4.7 Validate for Delete in Practice (AppDelegate.swift demo) Click here to view code image func demo () { let context = CDHelper.shared.context print(“Before deletion attempt:”) CDOperation.objectCountForEntity(“Item”, context:context) CDOperation.objectCountForEntity(“Unit”, context:context) let filter = NSPredicate(format: “name == %@”,“Kg”) if let units = CDOperation.objectsForEntity(“Unit”, context: context, filter: filter, sort: nil) as? [Unit] { for unit in units { if CDOperation.objectDeletionIsValid(unit) { print(“Deleting Unit ‘\(unit.name!)’…”) context.deleteObject(unit) } CDHelper.saveSharedContext() } } print(“After deletion attempt:”) CDOperation.objectCountForEntity(“Item”, context:context) CDOperation.objectCountForEntity(“Unit”, context:context) }

Update Groceries as follows to test object deletion validation: 1. Replace the demo function in AppDelegate.swift with the code from Listing 4.7. 2. Run the application again. The expected result is that the Kg unit is not deleted, which is shown in Figure 4.8.

Figure 4.8 Validating that objects are safe to delete before deleting them 3. Remove all code from within the demo function of AppDelegate.swift.

4. Change the delete rule on the items relationship of the Unit entity back to Nullify.

Entity Inheritance Similar to classes, entities have the capability to inherit from a parent. This useful feature allows you to simplify the data model. Child entities automatically inherit the attributes of their parent entity. The sample application is centered on the understanding that grocery items could live in one of two locations: the shop or your house. This means entity inheritance could be leveraged to allow attributes common to a shop location and a home location to be inherited from a parent location. For example, let’s say an entity called Location has an attribute called summary. If another entity such as LocationAtHome or LocationAtShop inherits from Location, it automatically has the summary attribute. This behavior is similar to class inheritance. To prevent the Location entity from ever being instantiated, you have the option of making it abstract. You would do this only if it doesn’t make sense to have instances of the Location entity in your code. Update Groceries as follows to configure entities with inheritance: 1. Add a new model version named Model 6 based on Model 5. 2. Set Model 6 as the Current Model. 3. Select Model 6.xcdatamodel. 4. Add a new entity called Location with a String attribute called summary. 5. Select the Location entity and open up the Data Model Inspector in the Utilities pane, as shown on the right in Figure 4.9.

Figure 4.9 Location as an abstract, parent entity 6. Set the Location entity as an Abstract Entity. This triggers a warning that the Location entity has no children. 7. Add a new entity called LocationAtHome with a String attribute called storedIn.

8. Set the parent entity of the LocationAtHome entity to Location. 9. Add a new entity called LocationAtShop with a String attribute called aisle. 10. Set the parent entity of the LocationAtShop entity to Location. 11. Change the Editor Style to Graph (if it isn’t already) and then arrange the entities as shown in Figure 4.9. If you can’t see all the entities properly, you might need to drag them apart. With the new parent and child entities in place, it’s time to link them to the Item entity so items can be related to a home or shop location. Update Groceries as follows to configure the location at home relationships: 1. Hold down Control while dragging a line from the LocationAtHome entity to the Item entity. 2. Rename newRelationship in the LocationAtHome entity to items. 3. Set the items relationship in the LocationAtHome entity to To-Many. 4. Rename newRelationship in the Item entity to locationAtHome. Update Groceries as follows to configure the location at shop relationships: 1. Hold down Control while dragging a line from the LocationAtShop entity to the Item entity. 2. Rename newRelationship in the LocationAtShop entity to items. 3. Set the items relationship in the LocationAtShop entity to To-Many. 4. Rename newRelationship in the Item entity to locationAtShop. The model should now match the one shown in Figure 4.10. Notice how the doubleheaded arrows indicate a To-Many relationship.

Figure 4.10 The new model with support for item locations Update Groceries as follows to ensure dot notation can be used with the new entities in code: 1. Create an NSManagedObject subclass of all entities in Model 6. Remember to select the Groceries target and the Data Model group when you save.

Summary You’ve now seen how to create and configure relationships between entities. As the benefits of using relationships were discussed, the key relationship settings such as ToMany, To-One, and delete rules were covered too. As the model was expanded, new location-centric entities were introduced that demonstrated abstract entities as parents in a new entity inheritance hierarchy. The foundation has been laid, so it’s time to bring the application to life with the introduction of table views, discussed in the next chapter.

Exercises Why not build on what you’ve learned by experimenting? 1. Insert a new Item, LocationAtHome, and LocationAtShop object. (Hint: Use Listing 4.1 as a cheat sheet.) 2. Set item.locationAtHome to be the LocationAtHome object and item.locationAtShop to be the LocationAtShop object. 3. Browse the contents of the ZLOCATION table in LocalStore.sqlite to see how the location entities translate to database tables. You should observe that the

storedIn and aisle attributes from the Location child entities are in the same table. 4. Set the delete rule on the items relationship of the Unit entity to Cascade instead of Deny. Insert and then delete a unit. What happens to the related items after you save the context? (Hint: Use Listing 4.1 and Listing 4.5 as a cheat sheet for inserting, relating, and deleting.) Change the delete rule on the items relationship of the Unit entity back to Nullify before the next chapter. You don’t need to worry about model versioning for this small change because it does not impact the data structure.

5. Table Views If we knew what it was we were doing, it would not be called research, would it? Albert Einstein In Chapter 4, “Managed Object Model Expansion,” you tapped into the flexibility that relationships and entity inheritance add to a managed object model. Up until now, the demonstrations were constrained to the console log. It’s now time to move closer to the end user experience as you’re shown how to efficiently present Core Data-fetched results in a table view. This chapter begins with a brief refresher on table views and then dives right in to constructing a Core Data-driven Table View Controller subclass. This reusable subclass is leveraged to populate two new table views—one for preparing a shopping list and one for shopping with.

Table View Basics Arguably one of the most common iOS interface elements is the table view. Based on UIScrollView, this powerful part of UIKit offers a highly customizable way to display a list of information in a single column. Even if you’re new to Core Data, you may already be familiar with creating table views populated using an array. In case you’re unfamiliar, this section outlines the basics of table views. Figure 5.1 shows the main components of a table view, where the section header title and table view cell (row) locations should be apparent. The section index is the small, vertical text shown down the right-hand side. Tapping or dragging the section index allows you to jump quickly between sections within the table view.

Figure 5.1 Fundamental table view components To populate a table view, you usually create a UITableViewController subclass adopting the UITableViewDataSource protocol. You then assign the subclass to a Table View Controller on a storyboard. The UITableViewDataSource protocol requires that a couple of mandatory functions be implemented to allow the table to be populated with data: The numberOfRowsInSection function is where you specify how many rows each table view section has. For example, you might configure this function to return someArray.count, so the number of objects in the data source array matches the number of rows in the table view. The cellForRowAtIndexPath function is where you specify what is displayed in each cell. It’s common to heavily customize this function. If you use the built-in cell styles, there are some default variables available with a standard UITableViewCell to be aware of. For example, the text shown in each row of Figure 5.1 appears because the textLabel.text variable has been set. For a complete list of variables, you can jump to the definition of UITableViewCell in Xcode. Other optional functions are available by adopting the UITableViewDataSource protocol. These functions may be used to configure editing, reordering, deleting, headers,

footers, the index, and more. Most of them are covered later in this chapter.

Core Data Table Views As previously mentioned, if you weren’t using Core Data, you might populate a table view using an array as the data source. A key problem with this approach, which you may have experienced firsthand, is that performance can suffer when the array is too big as it consumes too much memory. Up until now, you’ve performed Core Data fetches using an NSFetchRequest, which returns an array when executed. Although you could use this array directly to populate a table view, there is a better way. To populate a table view, you still fetch with an NSFetchRequest; however, this time you configure additional options such as the fetch batch size to stagger the fetch. This small option can have a huge impact on the memory footprint and consequently improve overall performance. The batch size you set should be a bit larger than the number of rows visible on the screen at any one time. The best way to efficiently manage fetched data between Core Data and a table view is with an NSFetchedResultsController. If you were to otherwise use the array returned by a fetch request directly without a fetched results controller, there’s a chance that when the underlying data changes, the objects in the array could become invalid. This could lead to a crash. When a table view is a delegate of a fetched results controller, you can implement change tracking to update the table view automatically as fetched objects change in the underlying context. The performance of a fetched results controller-backed table view may also be increased by setting a cache, which is as easy as specifying a unique name for the cache. Using a cache minimizes unnecessary repeated fetches. As well as the performance and change-tracking benefits, a fetched results controller has a number of convenient variables that make it easy to implement a Core Data table view. Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter04.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. Also, delete any existing copy of Groceries from your device or the iOS Simulator.

Introducing CDTableViewController The Core Data Table View Controller CDTableViewController is a reusable subclass that underpins all Core Data table views in Groceries. It is also generic enough to reuse in your own applications. To create CDTableViewController, you create a UITableViewController subclass with an NSFetchedResultsController instance variable.

Update Groceries as follows to create CDTableViewController.swift: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the filename to CDTableViewController and ensure the Groceries target is checked. 5. Ensure the Groceries project directory is open and then click Create. Figure 5.2 shows the expected results.

Figure 5.2 Core Data Table View Controller class The CDTableViewController class is a UITableViewController subclass that adopts the NSFetchedResultsControllerDelegate protocol. It also has a lazy NSFetchedResultsController variable. Listing 5.1 shows the starting point code for this class. Listing 5.1 Core Data Table View Controller (CDTableViewController.swift) Click here to view code image import UIKit import CoreData class CDTableViewController: UITableViewController, NSFetchedResultsControllerDelegate { // MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } // MARK: - CELL CONFIGURATION func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) {

// Use self.frc.objectAtIndexPath(indexPath) to get an object specific to a cell in the subclasses print(“Please override configureCell in \(__FUNCTION__)!”) } // Override var entity = “MyEntity” var sort = [NSSortDescriptor(key: “myAttribute”, ascending: true)] // Optionally Override var context = CDHelper.shared.context var filter:NSPredicate? = nil var cacheName:String? = nil var sectionNameKeyPath:String? = nil var fetchBatchSize = 0 // 0 = No Limit var cellIdentifier = “Cell” // MARK: - FETCHED RESULTS CONTROLLER lazy var frc: NSFetchedResultsController = { let request = NSFetchRequest(entityName:self.entity) request.sortDescriptors = self.sort request.fetchBatchSize = self.fetchBatchSize if let _filter = self.filter {request.predicate = _filter} let newFRC = NSFetchedResultsController( fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: self.sectionNameKeyPath, cacheName: self.cacheName) newFRC.delegate = self return newFRC }() }

To use CDTableViewController.swift you need to subclass it and override the entity and sort variables. These variables are shown in Listing 5.1 beneath the Override comment and are critical to the creation of the fetched results controller. The other variables beneath the Optionally Override comment also assist in the creation of the fetched results controller. To create the fetched results controller, you need four things: An instance of NSFetchRequest, which uses the mandatory entity and sort variables in addition to the optional fetchBatchSize and filter variables. These variables should be set in CDTableViewController subclasses. An instance of NSManagedObjectContext. In this case, CDHelper.shared.context is used by default. In CDTableViewController subclasses you can set this via self.context. A string representing a sectionNameKeyPath. This string value represents an entity attribute key and is used to group the table view into sections. For example, you might want to group the table view into the aisles that items are located in at the grocery store. It’s important to note that the attribute specified here must also be the

first sort descriptor in the fetch request. In CDTableViewController subclasses you can set this via self.sectionNameKeyPath. A string representing a cache name. Although it’s not provided in this case, if it were it should be a string that’s unique across the application. In CDTableViewController subclasses you can set this via self.cacheName. The cellIdentifier variable should match the identifier of the relevant prototype cell in the Storyboard, which you can usually just set to Cell. Update Groceries as follows to configure CDTableViewController.swift: 1. Replace all code in CDTableViewController.swift with the code from Listing 5.1.

Expanding CDTableViewController The CDTableViewController class requires several new sections. For easy navigation and readability in Xcode, they’re separated by marks in code: FETCHING VIEW DEALLOCATION DATA SOURCE: UITableView DELEGATE: NSFetchedResultsController Listing 5.2 shows the FETCHING, VIEW, and DEALLOCATION sections of CDTableViewController.swift. Listing 5.2 Fetching, View, and Deallocation Sections (CDTableViewController.swift) Click here to view code image // MARK: - FETCHING func performFetch () { self.frc.managedObjectContext.performBlock ({ do { try self.frc.performFetch() } catch { print(“\(__FUNCTION__) FAILED : \(error)”) } self.tableView.reloadData() }) } // MARK: - VIEW override func viewDidLoad() { super.viewDidLoad() // Force fetch when notified of significant data changes NSNotificationCenter.defaultCenter().addObserver(self, selector: “performFetch”, name: “SomethingChanged”, object: nil)

} override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) } // MARK: - DEALLOCATION deinit { NSNotificationCenter.defaultCenter().removeObserver(self, name: “performFetch”, object: nil) }

The performFetch function is responsible for fetching data. If errors occur during the fetch, they are logged to the console. The viewDidLoad function is configured to listen for notifications that the table view should refetch all its data. When underlying data changes for an entity other than the one the table view is displaying, the fetched results controller won’t tell the table view to refresh. For example, when a unit name is updated, the items table view won’t be updated even if it relies on item.unit.name. In this case you need to post a SomethingChanged notification manually. The deinit function exists to balance the observer code in viewDidLoad. Update Groceries as follows to configure the CDTableViewController for fetching: 1. Add the code from Listing 5.2 to the bottom of CDTableViewController.swift before the last curly brace.

DATASOURCE: UITableView The CDTableViewController class inherits from UITableViewController, so it adopts the UITableViewDataSource protocol by default. By adopting this protocol, CDTableViewController or one of its subclasses is responsible for implementing the delegate functions required for a table view to work. Listing 5.3 shows the code involved. Listing 5.3 Data Source: UITableView Section (CDTableViewController.swift) Click here to view code image // MARK: - DATA SOURCE: UITableView override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return self.frc.sections?.count ?? 0 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.frc.sections![section].numberOfObjects ?? 0 } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell:UITableViewCell? = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier) if cell == nil { cell = UITableViewCell(style: UITableViewCellStyle.Value1,

reuseIdentifier: self.cellIdentifier) } cell!.selectionStyle = UITableViewCellSelectionStyle.None cell!.accessoryType = UITableViewCellAccessoryType.DetailButton self.configureCell(cell!, atIndexPath: indexPath) return cell! } override func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int { return self.frc.sectionForSectionIndexTitle(title, atIndex: index) } override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return self.frc.sections![section].name ?? ”” } override func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? { return self.frc.sectionIndexTitles }

Most of UITableViewDataSource protocol functions are generic enough to be implemented in CDTableViewController. They are as follows: The numberOfSectionsInTableView function indicates how many sections the table view has. The default value is 1 if this function isn’t implemented. When an instance of a fetched results controller is created, you have an opportunity to configure a sectionNameKeyPath too, which groups the table into sections. To handle all cases of fetched result sections appropriately, you simply need to configure this function to return self.frc.sections?.count. This ensures that when multiple sections are required by the fetched results controller, that the Table View Controller can automatically handle it. If there are no sections, 0 is returned. The numberOfRowsInSection function indicates the number of rows for each section. This is drawn from the NSFetchedResultsSectionInfo of the fetched results controller. The cellForRowAtIndexPath function builds each cell for each row in each section, given its indexPath. It in turn calls self.configureCell, which is in a separate function that you need to override to customize the appearance of the cells in the table view. The sectionForSectionIndexTitle function indicates what section a particular section title belongs to. A fetched results controller has a function specifically to help provide this. This means returning self.frc.sectionForSectionIndexTitle(title, atIndex: index) is all that’s required here. The titleForHeaderInSection function indicates what text title should be shown in a particular section. Returning name from the NSFetchedResultsSectionInfo of the fetched results controller provides section title information with respect to any sectionNameKeyPath you may have configured.

The sectionIndexTitlesForTableView function indicates the text title of each index that should be shown in the table view. A fetched results controller has a variable specifically to help populate this table view data source function. This means returning self.frc.sectionIndexTitles is all that’s required here. As shown in Listing 5.3, the code to populate a table view using the UITableViewDataSource protocol functions is trivial. You may want to override functions such as the header title later on in a CDTableViewController subclass; however, this is a great starting point for now. Update Groceries as follows to enhance the CDTableViewController implementation: 1. Add the code from Listing 5.3 to the bottom of CDTableViewController.swift before the last curly brace.

DELEGATE: NSFetchedResultsController Whenever you need to make a change to the data presented in a table view, you need to tell the table view to beginUpdates, and when you’re done, endUpdates. When you’re using a fetched results controller, you need to call these functions from controllerWillChangeContent and controllerDidChangeContent, respectively, as shown in Listing 5.4. Listing 5.4 Delegate: NSFetchedResultsController Section (CDTableViewController.swift) Click here to view code image // MARK: - DELEGATE: NSFetchedResultsController func controllerWillChangeContent(controller: NSFetchedResultsController) { self.tableView.beginUpdates() } func controllerDidChangeContent(controller: NSFetchedResultsController) { self.tableView.endUpdates() }

Update Groceries as follows to update the CDTableViewController implementation: 1. Add the code from Listing 5.4 to the bottom of CDTableViewController.swift before the last curly brace. The next two fetched results controller delegate protocol functions to be implemented are controller:didChangeSection and controller:didChangeObject. They handle table view cell moves, deletes, updates, and insertions. Listing 5.5 shows the code involved. Listing 5.5 Delegate: NSFetchedResultsController Section - Continued (CDTableViewController.swift) Click here to view code image

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { switch type { case .Insert: self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) case .Delete: self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) default: return } } func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { switch type { case .Insert: self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) case .Delete: self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) case .Update: self.tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: .None) case .Move: self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade) self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade) } }

The code in Listing 5.5 implements the fetched results controller delegate functions to the relevant table view functions. Update Groceries as follows to implement the remaining fetched results controller functions: 1. Add the code from Listing 5.5 to the bottom of CDTableViewController.swift before the last curly brace.

CDTableViewController Subclasses CDTableViewController is now ready to be subclassed. There will be five table views in the Groceries application, so five separate CDTableViewController subclasses are required as follows: PrepareTVC lists items that can be put on the shopping list. ShopTVC lists items that are on the shopping list. UnitsTVC lists units that items are measured by (for example, Kg, g, or liters). This class will be implemented in the next chapter. LocationsAtHomeTVC lists the possible locations items can be stored in at

home. This class will be implemented in the next chapter. LocationsAtShopTVC lists the possible aisle locations items can be in at a shop. This class will be implemented in the next chapter. The PrepareTVC and ShopTVC table views will be the Groceries application’s primary views. Groceries needs a Tab Bar Controller to allow switching between them. Update Groceries as follows to add a Tab Bar Controller: 1. Select Main.storyboard. 2. Drag a Tab Bar Controller onto the storyboard to the left of the existing Navigation Controller. 3. Delete the two default view controllers connected to the Tab Bar Controller. 4. Hold down Control and drag a line from the Tab Bar Controller to the existing Navigation Controller and then select Relationship Segue > view controllers. 5. Set the Tab Bar Controller as the initial view controller using Attributes Inspector (Option+ +4), as shown in Figure 5.3.

Figure 5.3 Setting a Tab Bar Controller to toggle table views A Tab Bar Controller isn’t any good with just one tab, so another needs to be added for the upcoming ShopTVC table view. Before that can happen, the ShopTVC table view itself needs to be created. Update Groceries as follows to add a new Table View Controller connected to the tab bar: 1. Select Main.storyboard. 2. Drag a new Table View Controller onto the storyboard beneath the existing Navigation Controller. 3. Ensure that the new Table View Controller is selected and then click Editor > Embed In > Navigation Controller.

4. Set the Navigation Item Title of the new Table View Controller to Groceries using Attributes Inspector (Option+ +4). 5. Click the Prototype Table View Cell of the new Table View Controller and set the Reuse Identifier to Cell. This identifier is referred to in the ShopTVC’s cellForRowAtIndexPath function as it populates these cells with data. If you were to set this to something other than Cell, you would need to set self.cellIdentifier to match it in the init function of the CDTableViewController subclass. 6. Hold down Control and drag a line from the Tab Bar Controller to the new Navigation Controller; then select Relationship Segue > view controllers. 7. Arrange the Tab Bar Controller so it lines up nicely, as shown in Figure 5.4.

Figure 5.4 The Tab Bar Controller with two tabs The Tab Bar Controller now has two tabs that will be used to cycle between the PrepareTVC and ShopTVC table views. Before those subclasses are prepared and assigned to the table views, final touches are needed to help identify the tabs properly. Update Groceries as follows to add tab bar icons: 1. Download and extract the tab bar icons from the following URL: http://www.timroadley.com/LCDwS/TabBarIcons.zip. 2. Select the Assets.xcassets asset catalog. 3. Drag the tab bar icons into the asset catalog beneath LaunchScreen, as shown in Figure 5.5.

Figure 5.5 The Tab Bar Controller icons Update Groceries as follows to configure the tabs: 1. Select Main.storyboard. 2. Select the Tab Bar Item on the Navigation Controller next to the top table view associated with the PrepareTVC class. 3. Set the Bar Item Title to Prepare and Bar Item Image to prepare, as shown in Figure 5.6.

Figure 5.6 The Prepare tab 4. Select the Tab Bar Item on the Navigation Controller next to the other table view. 5. Set the Bar Item Title to Shop and Bar Item Image to shop using the technique from step 3. 6. Run the application. You should be able to switch between table views, as shown in Figure 5.7. There won’t be any data in the tables just yet. If the Prepare and Shop tabs are in the wrong order, you can drag them to the correct order using the Tab Bar Controller on the storyboard.

Figure 5.7 Table view toggles in the Groceries Tab Bar Controller

Enhancing PrepareTVC To put an item on the shopping list (i.e., the Shop tab), a user will simply tap an item on the Prepare tab. To completely clear the shopping list, a Clear button will be implemented on the Prepare tab. To prevent the shopping list from being accidentally cleared, an action sheet is needed to confirm that a shopping list should in fact be cleared. The PrepareTVC class now needs to be updated as a subclass of CDTableViewController. A valid entity and sort descriptor also need to be provided, along with some table view cell customization. Listing 5.6 shows the code involved in the updated class. Listing 5.6 Prepare Table View Controller (PrepareTVC.swift) Click here to view code image import UIKit import CoreData class PrepareTVC: CDTableViewController { // MARK: - CELL CONFIGURATION override func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { if let item = frc.objectAtIndexPath(indexPath) as? Item { var itemName = ”” if let name = item.name {itemName = ” \(name)”} // Prefix quantity and unit if let unit = item.unit?.name {itemName = “\(unit)\(itemName)”} if let quantity = item.quantity {itemName = “\(quantity)\ (itemName)”} if let textLabel = cell.textLabel, listed = item.listed { textLabel.text = itemName cell.accessoryType = UITableViewCellAccessoryType.DetailButton // Color items according to the listed attribute if listed.boolValue { textLabel.font = UIFont(name: “Helvetica Neue”, size: 18) textLabel.textColor = UIColor(red: 1, green: 0.2, blue:

0.2, alpha: 1) } else { textLabel.font = UIFont(name: “Helvetica Neue”, size: 16) textLabel.textColor = UIColor.grayColor() } } else {print(“ERROR getting textLabel in \(__FUNCTION__)”)} } else {print(“ERROR getting item in \(__FUNCTION__)”)} } // MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // CDTableViewController subclass customization self.entity = “Item” self.sort = [NSSortDescriptor(key: “locationAtHome.storedIn”, ascending: true),NSSortDescriptor(key: “name”, ascending: true)] self.sectionNameKeyPath = “locationAtHome.storedIn” self.fetchBatchSize = 50 } // MARK: - VIEW override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) self.performFetch() } }

The configureCell function sets the text of the cell’s textLabel. The grocery item specific to the cell has the same index in the fetched results controller. This means that the indexPath of the cell can be used to gain reference to the appropriate managed object in frc. The function continues on to color-code items depending on whether they’re listed on the Shop tab. The init function is where entity and sort descriptor are set prior to fetching the data. This is the place you could also set any of the other optional variables from CDTableViewController. For example, the self.sectionNameKeyPath variable is set so that the shopping list is divided into sections. The viewDidAppear function is used to trigger a fetch each time the view appears. Update Groceries as follows to configure PrepareTVC: 1. Replace all code in PrepareTVC.swift with the code from Listing 5.6. If you get an error regarding optional types, make sure that the relationships in Item+CoreDataProperties.swift are all optional variables by suffixing them with a question mark.

Preparing Test Data The PrepareTVC is now in a position to display data, yet there’s nothing in the persistent store to show because the application was deleted at the beginning of this chapter. Listing 5.7 shows the code required to add test data to the persistent store. Listing 5.7 Demo Data (AppDelegate.swift demo)

Click here to view code image func demo () { if CDOperation.objectCountForEntity(“Item”, context: CDHelper.shared.context) == 0 { let context = CDHelper.shared.context let homeLocations = [“Fruit Bowl”,“Pantry”,“Nursery”,“Bathroom”,“Fridge”] let shopLocations = [“Produce”,“Aisle 1”,“Aisle 2”,“Aisle 3”,“Deli”] let unitNames = [“g”,“pkt”,“box”,“ml”,“kg”] let itemNames = [“Grapes”,“Biscuits”,“Nappies”,“Shampoo”,“Sausages”] var i = 0 for itemName in itemNames { print(“Inserting ‘\(itemName)’”) if let locationAtHome = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtHome”, inManagedObjectContext: context) as? LocationAtHome , let locationAtShop = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtShop”, inManagedObjectContext: context) as? LocationAtShop , let unit = NSEntityDescription.insertNewObjectForEntityForName(“Unit”, inManagedObjectContext: context) as? Unit , let item = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: context) as? Item { locationAtHome.storedIn = homeLocations[i] locationAtShop.aisle = shopLocations[i] unit.name = unitNames[i] item.name = itemNames[i] item.locationAtHome = locationAtHome item.locationAtShop = locationAtShop item.unit = unit i++ } else {“ERROR preparing items in \(__FUNCTION__)”} } print(“Test data was inserted”) CDHelper.saveSharedContext() } }

Update Groceries as follows to insert some test data: 1. Replace the demo function of AppDelegate.swift with the code shown in Listing 5.7. This code inserts test data using techniques discussed in the previous chapters. 2. Add demo() to the applicationDidBecomeActive function of AppDelegate.swift. 3. Run the application to insert the test data, which works only if there’s no existing data. The expected result is shown in Figure 5.8.

Figure 5.8 PrepareTVC table view with test data From now on the code in the demo function of AppDelegate.swift automatically inserts test objects if it detects that there are no existing Item objects. Congratulations! The fundamental components of a Core Data–driven table view are now in place.

Deletion and Cell Selection for PrepareTVC The next steps for PrepareTVC involve adding the basic features a user would expect from this application, such as the ability to delete items or add them to the Shop tab when they’re selected. Listing 5.8 shows the two new functions required to achieve this. Listing 5.8 Prepare Table View Controller Data Source (PrepareTVC.swift) Click here to view code image // MARK: - DATA SOURCE: UITableView override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if (editingStyle == UITableViewCellEditingStyle.Delete) { if let object = self.frc.objectAtIndexPath(indexPath) as? NSManagedObject { self.frc.managedObjectContext.deleteObject(object) } CDHelper.saveSharedContext() } } override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

// Add/Remove items from the shopping list if let item = self.frc.objectAtIndexPath(indexPath) as? Item, listed = item.listed { if listed.boolValue { item.listed = NSNumber(bool: false) } else { item.listed = NSNumber(bool: true) item.collected = NSNumber(bool: false) } CDHelper.saveSharedContext() } }

The commitEditingStyle function is responsible for handling item deletion, which happens when a user swipes a table view cell. The didSelectRowAtIndexPath function is responsible for toggling whether an item is listed and thereby shown on the Shop tab. It also ensures items freshly set as listed are not set as collected. When an item is marked as collected, it will be “checked off” yet still visible in the Shop tab. When a user taps Clear on the Shop tab, any item marked as collected is removed from the Shop tab. Update Groceries as follows to add to the DATA SOURCE: UITableView section: 1. Add the code from Listing 5.8 to the bottom of PrepareTVC.swift, before the last curly brace. 2. Select Main.storyboard. 3. Select the Prototype Table View Cell of the Prepare Table View Controller (the one at the top) and then open Attributes Inspector (Option+ +4), as shown in Figure 5.9.

Figure 5.9 The Items table view prototype cell 4. Set the Table View Cell Selection to None, as shown in Figure 5.9. 5. Set the Table View Cell Accessory to Detail Disclosure, as shown in Figure 5.9. Run the application again to see the new behaviors. First, when an item is selected on the Prepare tab, it toggles red or gray. Once the ShopTVC table view is configured later in this chapter, red items on the Prepare tab will be visible on the Shop tab. This is how an

item is added to the shopping list. The second new behavior is the ability to delete items by swiping them. Test this out by deleting the biscuits. Whenever an item is changed or deleted the context is saved. This is in preparation for later chapters where this becomes important when syncing devices.

Interaction for PrepareTVC The ability to remove all items from the shopping list will now be added, which involves adding a new Clear button to the Prepare tab. This button sets all listed items to false. To support this functionality, a new fetch request template is required that fetches all items where listed is true. Update Groceries as follows to create the shopping list fetch request template: 1. Select Model 6.xcdatamodel. 2. Rename the existing Test fetch request template to ShoppingList and configure it as shown in Figure 5.10. This fetch request only fetches items flagged as listed. As per usual for Boolean attributes, 1 is equal to true.

Figure 5.10 ShoppingList fetching listed items A new INTERACTION section in PrepareTVC uses the fetch request template to implement a Clear button. Because this button could be pressed accidentally, an alert controller presents an action sheet to confirm the action with the user. Listing 5.9 shows these new functions required in the INTERACTION section. Listing 5.9 Prepare Table View Controller Interaction (PrepareTVC.swift) Click here to view code image // MARK: - INTERACTION @IBAction func clear (sender: AnyObject) { if let request = CDHelper.shared.model.fetchRequestTemplateForName(“ShoppingList”) {

let context = frc.managedObjectContext var error: NSError? let listedItemCount = context.countForFetchRequest(request, error: &error) if let _error = error {print(“ERROR getting ‘ShoppingList’ fetch request template: \(_error.localizedDescription)”)} if listedItemCount > 0 { // ACTION CONFIRMATION let sheet = UIAlertController(title: “Clear Entire Shopping List?”, message:nil, preferredStyle:.ActionSheet) let confirm = UIAlertAction(title: “Clear”, style: .Destructive, handler: { (action) -> Void in self.clearList() }) let cancel = UIAlertAction(title: “Cancel”, style: .Cancel, handler: { (action) -> Void in // do nothing }) sheet.addAction(confirm) sheet.addAction(cancel) self.presentViewController(sheet, animated: true, completion: nil) } else { // ALERT NOTIFICATION let alert = UIAlertController(title: “Nothing to Clear”, message: “Add items to the Shop tab by tapping them on the Prepare tab. Remove all items from the Shop tab by clicking Clear on the Prepare tab”, preferredStyle: .Alert) let ok = UIAlertAction(title: “Ok”, style:.Default, handler: { (action) -> Void in // Do nothing }) alert.addAction(ok) self.presentViewController(alert, animated: true, completion: nil) } } else { print(“ERROR getting ‘ShoppingList’ fetch request”) } CDHelper.saveSharedContext() } func clearList () { if let request = CDHelper.shared.model.fetchRequestTemplateForName(“ShoppingList”) { let context = frc.managedObjectContext context.performBlock { do { if let items = try context.executeFetchRequest(request) as? [Item] { for item in items { item.listed = NSNumber(bool: false) } } else {print(“\(__FUNCTION__) FAILED to fetch objects”)} } catch {print(“\(__FUNCTION__) FAILED to fetch objects”)}

CDHelper.saveSharedContext() } } else {print(“ERROR getting ‘ShoppingList’ fetch request”)} }

The clear function is an interface builder action linked to the Clear button on the Prepare table view. Pressing this button presents a “Clear entire shopping list?” action sheet, provided there is at least one listed item. The clearList function iterates through all listed items, marking them as unlisted. This has the effect of removing the items from the Shop tab. Update Groceries as follows to implement the INTERACTION section: 1. Add the code from Listing 5.9 to the bottom of PrepareTVC.swift before the last curly brace and then save the class file (press +S). 2. Select Main.storyboard. 3. Drag a Bar Button Item to the top left of the Prepare table view. 4. Set the new Bar Item Title to Clear using Attributes Inspector (Option+ +4), as shown in Figure 5.11.

Figure 5.11 The Clear button for removing items from the shopping list 5. Hold down Control and drag a line from the Clear button to the yellow circle at the top of the Prepare table view. Then select Sent Actions > clear: from the pop-up menu. This links the Clear button to the clear function. Run the application again to see the new application behaviors. If you press the Clear button and nothing is selected, a notification tells you to select items before pressing Clear. If items are selected (red) and Clear is pressed, you have a chance to confirm or cancel that action. If you confirm the action, all red items return to gray. Throughout all this, the Shop tab remains empty because it hasn’t yet been configured.

Introducing ShopTVC The purpose of the ShopTVC table view is to show a shopping list sorted by aisle. ShopTVC is a CDTableViewController subclass similar to PrepareTVC. The starting point code for ShopTVC.swift is shown in Listing 5.10. Listing 5.10 Shop Table View Controller (ShopTVC.swift)

Click here to view code image import UIKit import CoreData class ShopTVC: CDTableViewController { // MARK: - CELL CONFIGURATION override func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { if let item = frc.objectAtIndexPath(indexPath) as? Item { var itemName = ”” if let name = item.name {itemName = ” \(name)”} // Prefix quantity and unit if let unit = item.unit?.name {itemName = “\(unit)\(itemName)”} if let quantity = item.quantity {itemName = “\(quantity)\ (itemName)”} if let textLabel = cell.textLabel, collected = item.collected { textLabel.text = itemName cell.accessoryType = UITableViewCellAccessoryType.DetailButton // Color items depending to show if they’re collected if collected.boolValue { cell.accessoryType = UITableViewCellAccessoryType.Checkmark textLabel.font = UIFont(name: “Helvetica Neue”, size: 16) textLabel.textColor = UIColor(red:0.37, green:0.74, blue:0.35, alpha:1) textLabel.attributedText = NSAttributedString(string: itemName, attributes: [NSStrikethroughStyleAttributeName : “1”]) } else { cell.accessoryType = UITableViewCellAccessoryType.DetailButton textLabel.font = UIFont(name: “Helvetica Neue”, size: 18) textLabel.textColor = UIColor(red: 1, green: 0.2, blue: 0.2, alpha: 1) textLabel.attributedText = NSAttributedString(string: itemName, attributes: [NSStrikethroughStyleAttributeName : “0”]) } } else {print(“ERROR getting textLabel in \(__FUNCTION__)”)} } else {print(“ERROR getting item in \(__FUNCTION__)”)} } // MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // CDTableViewController subclass customization self.entity = “Item” self.sort = [NSSortDescriptor(key: “locationAtShop.aisle”, ascending: true),NSSortDescriptor(key: “name”, ascending: true)] self.sectionNameKeyPath = “locationAtShop.aisle” self.fetchBatchSize = 50 if let template = CDHelper.shared.model.fetchRequestTemplateForName(“ShoppingList”) {

let request = template as NSFetchRequest self.filter = request.predicate } else {print(“Could not find a fetch request template called ShoppingList”)} } // MARK: - VIEW override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) self.performFetch() } }

The functions in ShopTVC have a similar purpose to the equivalent functions in PrepareTVC. The configureCell function sets the text and the color of the textLabel depending on whether the relevant item has been collected. The init function is where the entity and sort descriptor are set prior to fetching the data. The self.sectionNameKeyPath variable is set so that the shopping list is divided into sections according to shop aisles. Most importantly self.filter is configured using the ShoppingList fetch request template, so that only items flagged as listed are visible in this view. The viewDidAppear function is used to trigger a fetch each time the view appears. Update Groceries as follows to implement the starting point of ShopTVC: 1. Ensure the Table View Controllers group is selected. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the filename to ShopTVC, ensure the Groceries target is selected, and then click Create. 5. Replace all code in ShopTVC.swift with the code from Listing 5.10.

Cell Selection for ShopTVC When the user taps an item on the Shop tab, it should go green and a check mark is displayed. The configureCell function is already configured to present items in this way when an item’s collected attribute is true. The next step is to implement didSelectRowAtIndexPath to toggle tapped items as collected. The code involved is shown in Listing 5.11. Listing 5.11 Shop Table View Controller Data Source (ShopTVC.swift) Click here to view code image // MARK: - DATA SOURCE: UITableView override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

// Check items off the shopping list if let item = self.frc.objectAtIndexPath(indexPath) as? Item, collected = item.collected { if collected.boolValue { item.collected = NSNumber(bool: false) } else { item.collected = NSNumber(bool: true) } CDHelper.saveSharedContext() } }

Update Groceries as follows to ensure items can be checked off the shopping list: 1. Add the code from Listing 5.11 to the bottom of ShopTVC.swift before the last curly brace.

INTERACTION for ShopTVC The INTERACTION section of ShopTVC is used to handle another Clear button. The difference between the Clear buttons on each tab is that the one on the Prepare tab clears the whole shopping list, whereas the one on the Shop tab clears only collected items. Again, a clear function is executed when the Clear button is tapped. As shown in Listing 5.12, this function either clears collected items or alerts the user that there’s nothing to clear. Listing 5.12 Shop Table View Controller Interaction (ShopTVC.swift) Click here to view code image // MARK: - INTERACTION @IBAction func clear (sender: AnyObject) { if let items = self.frc.fetchedObjects as? [Item] { var nothingCleared = true // Alert if there are no listed items if items.count == 0 { // ALERT NOTIFICATION let alert = UIAlertController(title: “Nothing to Clear”, message: “Add items using the Prepare tab”, preferredStyle: .Alert) let ok = UIAlertAction(title: “Ok”, style:.Default, handler: { (action) -> Void in // Do nothing }) alert.addAction(ok) self.presentViewController(alert, animated: true, completion: nil) return } // Clear collected items for item in items { if let collected = item.collected where collected.boolValue { item.listed = NSNumber(bool: false) item.collected = NSNumber(bool: false)

nothingCleared = false } } // Alert if there are no collected items if nothingCleared { // ALERT NOTIFICATION let alert = UIAlertController(title: “Nothing to Clear”, message: “Select items to be removed from the list before pressing Clear”, preferredStyle: .Alert) let ok = UIAlertAction(title: “Ok”, style:.Default, handler: { (action) -> Void in // Do nothing }) alert.addAction(ok) self.presentViewController(alert, animated: true, completion: nil) } } CDHelper.saveSharedContext() }

Update Groceries as follows to implement the INTERACTION section: 1. Add the code from Listing 5.12 to the bottom of ShopTVC.swift before the last curly brace and then save the class file (press +S). 2. Select Main.storyboard. 3. Set the class of the Shop table view (the one at the bottom) to ShopTVC using Identity Inspector (option + + 3). 4. Drag a Bar Button Item to the top left of the Shop table view (the table view at the bottom) and then set its Bar Item Title to Clear using the same approach used with the Clear button on the Prepare table view. 5. Hold down Control and drag a line from the new Clear button to the yellow circle at the top of the Shop table view. Then select Sent Actions > clear: from the pop-up menu. This links the Clear button to the clear function. 6. Select the Prototype Table View Cell of the Shop Table View Controller. 7. Set the Table View Cell Style to Basic. 8. Set the Table View Cell Selection to None. 9. Set the Table View Cell Accessory to Detail Disclosure. Run the application and ensure some items on the Prepare tab are red. Change to the Shop tab and notice these items have appeared sorted by aisle! Tap items on the Shop tab and they will be crossed off, green, and checked. Press the Clear button on the Shop tab to remove the checked items from the Shop tab. Return to the Prepare tab and notice the bought items (collected and cleared) aren’t red anymore.

Summary The application has really started to take shape as core functionality has been implemented. Already you can see the Prepare and Shop tabs listing potential items and shopping list items, respectively. To get to this point, certain repeatable design patterns were followed as you’ve seen with the implementation of the PrepareTVC and ShopTVC classes. By modifying the init function in CDTableViewController subclasses, you can leverage these design patterns in your own projects.

Exercises Why not build on what you’ve learned by experimenting? 1. Change the self.sectionNameKeyPath variable in the init function of PrepareTVC.swift to name. Also remove locationAtHome.storedIn as the first sort descriptor. Run the application and you should see the items grouped into sections by name. 2. Change the self.sort variable in the init function of PrepareTVC.swift to sort descending, by setting ascending to false. Run the application and you should see the items sorted in reverse order. 3. Change the self.entity variable in the init function of PrepareTVC.swift to Unit. Run the application and there will be errors in the console log. This highlights the importance of updating the configureCell function whenever you change the self.entity variable. Prevent the issue by updating configureCell to display units instead of items. If you’re unsure how to do this, replace the configureCell function of PrepareTVC.swift with the code from Listing 5.13 and run the application again, which should not generate any errors in the console log. Reverse any changes you’ve made to the project during the exercises before continuing to the next chapter. Listing 5.13 Exercise Testing (PrepareTVC.swift) Click here to view code image override func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { if let unit = self.frc.objectAtIndexPath(indexPath) as? Unit { if let textLabel = cell.textLabel { textLabel.text = unit.name } } }

6. Views A man should look for what is, and not for what he thinks should be. Albert Einstein Chapter 5, “Table Views,” demonstrated how to create Core Data-backed table views. During this process, the benefits of a fetched results controller were explained as one was implemented in CDTableViewController. This helpful UITableViewController subclass was itself subclassed as you customized the Prepare and Shop tabs to display different information. This chapter shows how to pass managed objects from a table view to a detail view. This detail view is configured to edit the selected managed object attribute values using a UITextField.

View Basics The most common standard iOS interface element is the UIView. Based on UIResponder, this powerful part of UIKit offers a highly customizable way to display things onscreen. Interface elements such as UIPickerView, UITextField, UIButton, and UIScrollView are all derived from UIView. Although the UITableView is great for displaying or deleting data, a UIView in a UIViewController offers a better starting point for a data-editing interface. By adding a UITextField to a view controller’s view, the user can easily modify managed object attribute values. In Groceries, a customized view controller supports the front end for editing a managed object’s attributes. When designing an object editing view, think about what the user might be doing outside the application when performing edits. In the case of Groceries, the user will probably be pushing a grocery cart, collecting an item, or rummaging through the house determining what he or she needs to buy. This means a user only has one hand free to interact with the application. By favoring interface elements that minimize user interaction, you produce an application that’s easy to use. An example of such an element might be a picker-viewenabled UITextField instead of a UITextField on its own. Even the choice of keyboard displayed when a UITextField is tapped can go a long way to improve the user experience. Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter05.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name.

The Target View Hierarchy Currently, the view hierarchy has a Tab Bar Controller at the root, with table views at the next level. Deeper into the view hierarchy is a blank Item view controller, which will become central to the editing of an item. Figure 6.1 shows a high-level overview of the target view hierarchy that will be in place by the end of this chapter.

Figure 6.1 A high-level overview of the target view hierarchy You should already be familiar with the Prepare and Shop tabs, which display the PrepareTVC and ShopTVC table views. When the + in the top-right corner of the PrepareTVC table view is tapped, a segue to a view controller occurs. In this segue, a new managed object needs to be created and then passed to the view controller. Additionally, when an accessory detail button is tapped on the Prepare tab, the existing managed object needs to be passed to the Item View Controller.

Introducing ItemVC A managed object from the Item entity has several attribute values that the user needs to edit, such as item name, quantity, and so on. To enable this functionality, a new GenericVC subclass named ItemVC is created. This new class is connected to various interface elements and enables the user to edit an item. Update Groceries as follows to add a new GenericVC subclass called ItemVC: 1. Select the View Controllers group. 2. Click File > New > File…. 3. Create a new iOS > Source > Cocoa Touch Class and then click Next. 4. Set Subclass of to GenericVC and Class name to ItemVC. Ensure the Language is Swift before clicking Next. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. The expected result is shown in Figure 6.2.

Figure 6.2 View Controller and Table View Controller subclasses

Keeping Reference to a Selected Item To edit an existing item, users select the accessory detail button of an item on the PrepareTVC or ShopTVC table view. When they do this, a reference to that item needs to be passed to the item detail view controller. This same pattern will be followed later with other object types, so a generic managed object will be added to GenericVC. The updated code is shown in bold in Listing 6.1. Listing 6.1 Generic View Controller (GenericVC.swift) Click here to view code image import UIKit import CoreData class GenericVC: UIViewController { var segueObject:NSManagedObject? var selectedObject:NSManagedObject? { if let object = self.segueObject { return object } self.done() return nil } func done () { self.navigationController?.popViewControllerAnimated(true) } func hideKeyboard () { self.view.endEditing(true) } func hideKeyboardWhenBackgroundIsTapped () { let tgr = UITapGestureRecognizer(target: self, action:Selector(“hideKeyboard”)) self.view.addGestureRecognizer(tgr) } }

When segueing from a table view to a view, the managed object should be passed through

the segueObject variable. From the view, this managed object is then accessed through the selectedObject variable. The selectedObject variable is set up in such a way that if the managed object disappears, for example, it is deleted on another device, then the view is dismissed and the table view displayed instead. Update Groceries as follows so references to selected objects can be held in GenericVC subclasses: 1. Replace contents of GenericVC.swift with the code from Listing 6.1.

Passing a Selected Item to ItemVC As mentioned previously, an item is passed to ItemVC in Groceries in one of two ways. For new items, the prepareForSegue function in PrepareTVC is used. For existing items, the accessoryButtonTappedForRowWithIndexPath function of PrepareTVC and ShopTVC is used. Both functions are shown in Listing 6.2. Listing 6.2 Segue Section (PrepareTVC.swift and ShopTVC.swift) Click here to view code image // MARK: - SEGUE override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if let itemVC = segue.destinationViewController as? ItemVC { if segue.identifier == “Add Item Segue” { let object = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: CDHelper.shared.context) itemVC.segueObject = object } else { print(“Unidentified Segue Attempted!”) } } } override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) { if let itemVC = self.storyboard?.instantiateViewControllerWithIdentifier(“ItemVC”) as? ItemVC { if let object = self.frc.objectAtIndexPath(indexPath) as? NSManagedObject { itemVC.segueObject = object } self.navigationController?.pushViewController(itemVC, animated: true) } }

Update Groceries as follows to configure the PrepareTVC and ShopTVC implementations: 1. Add the code from Listing 6.2 to the bottom of PrepareTVC.swift before the last curly brace.

2. Add the code from Listing 6.2 to the bottom of ShopTVC.swift before the last curly brace. 3. Delete the prepareForSegue function from ShopTVC.swift. It’s not required in this class because new items aren’t created on the Shop tab.

Configuring the Scroll View and Text Fields At first, the focus is on the UITextField objects used to edit the name and quantity item attribute values. In the next chapter, custom UITextField objects with the capability to present a UIPickerView are added. Those special UITextField objects are used to set an item’s unit.name, locationAtHome.storedIn, and locationAtShop.aisle from a picker. The target layout of the Item view for this chapter is shown in Figure 6.3. All the fields shown are contained within a Scroll View to allow scrolling within the view. The buttons lead to other views used to add and edit units, home locations, and shop locations.

Figure 6.3 The target Item View Controller The easiest way to link interface elements to your code is to create them on the storyboard and then drag them into an appropriate class file. Update Groceries as follows to prepare the item view: 1. Select Main.storyboard. 2. Select the Item View Controller (to the right of the PrepareTVC table view) and un-check Extend Edges Under Top Bars using Attributes Inspector (Option+ +4). 3. Drag a Scroll View onto the existing Item view and then resize it so that it takes up

the whole view. 4. While the Scroll View is still selected, click Editor > Resolve Auto Layout Issues > All Views in View Controller > Add Missing Constraints. This ensures the Scroll View stretches when the device is rotated. 5. If it’s hidden, show the Document Outline by clicking Editor > Show Document Outline. 6. While the Scroll View is selected, click Editor > Reveal in Document Outline, and then check constraints were applied to the Scroll View, as shown in Figure 6.4. Don’t worry if they don’t match exactly, so long as the scroll view always fills the window.

Figure 6.4 A new Scroll View using Auto Layout Update Groceries as follows to add the Name and Quantity text fields: 1. Drag a Text Field anywhere onto the new Scroll View and then configure it as follows using Attributes Inspector (Option+ +4): Set Font to System Bold 17.0. Set Text Alignment to Center. Set Placeholder Text to Name. Set Border Style to the Line (represented by a rectangle). Set Capitalization to Sentences. Set Background to Other > Pencils > Mercury (the second lightest gray pencil). 2. Configure the Height of the text field to 48 using Size Inspector (Option+ +5). 3. Duplicate the text field by holding Option while dragging downward. 4. Configure the duplicated text field as follows using Attributes Inspector (Option+ +4): Set Placeholder Text to Qty. Check Clear when editing begins. Set Keyboard Type to Decimal Pad. 5. Configure the Width of the Qty text field to 60 using Size Inspector (Option+

+5). 6. Widen the Name text field to the edge guides and then arrange both text fields, as shown in Figure 6.5.

Figure 6.5 The Name and Qty text field arrangement 7. Select both text fields at once ( +click) and then click Editor > Resolve Auto Layout Issues > Selected Views > Add Missing Constraints. This ensures the text fields resize when the device is rotated. The expected result is shown in Figure 6.5. To connect the scroll view and text fields to the code base, the Custom Class of the Item View Controller needs to be set. After that’s done, you can drag these user interface elements straight into ItemVC using the Assistant Editor. Update Groceries as follows to configure the Item view: 1. Select the Item View Controller. 2. Set both the Custom Class and Storyboard ID of the Item View Controller to ItemVC using Identity Inspector (Option+ +3). The Storyboard ID is used to reference this view when the detail disclosure is tapped on the Prepare table view. The expected result is shown in Figure 6.6.

Figure 6.6 The Item View Controller uses the ItemVC class Update Groceries as follows to create properties for the Name and Qty text fields: 1. With the Item View Controller selected, show the Assistant Editor (Option+

+Return). 2. If required, set the Assistant Editor to Automatic > ItemVC.swift, as shown at the top right of Figure 6.7.

Figure 6.7 Creating properties from storyboard elements 3. If it’s hidden, show the Document Outline by clicking Editor > Show Document Outline. 4. Expand ItemVC Scene, as shown in Figure 6.7 on the left. 5. Delete the contents of the ItemVC.swift class, so that it matches Figure 6.7. 6. Hold down Control and drag a line from the Scroll View to ItemVC.swift before the last curly brace; then set the Name to scrollView, as shown in the middle of Figure 6.7. Double-check the Type is UIScrollView and the Storage is Strong. 7. Hold down Control and drag a line from the Name text field to ItemVC.swift beneath the scrollView variable. Set the new variable Name to nameTextField. Double-check the Type is UITextField and the Storage is Strong before clicking Connect. 8. Hold down Control and drag a line from the Qty text field to ItemVC.swift beneath the nameTextField variable. Set the new Name to quantityTextField. Double-check the Type is UITextField and the Storage is Strong before clicking Connect. 9. Show the Standard Editor ( +Return). There should now be a scrollView, nameTextField, and quantityTextField variable in ItemVC.swift.

Implementing ItemVC The ItemVC class is categorized into sections, similar to PrepareTVC and ShopTVC. ItemVC initially has the following sections: DELEGATE: UITextField contains functions available to classes adopting the UITextFieldDelegate protocol. They are leveraged to set an item name according to the contents of the text field and to ensure zero-length names aren’t

accepted. VIEW contains functions responsible for ensuring each interface element is refreshed with appropriate data from the persistent store.

DELEGATE: UITextField The code for the DELEGATE: UITextField section implements the textFieldDidBeginEditing and textFieldDidEndEditing functions, which are optional UITextFieldDelegate protocol functions. These functions are called whenever a text field gains or loses focus. When a text field gains focus, new items have their name set to a zero-length string. When a text field loses focus, it is an opportune time to update the selected managed object with the contents of the text field that just lost focus. Listing 6.3 shows the code involved in an updated ItemVC.swift. Listing 6.3 Item View Controller (ItemVC.swift) Click here to view code image import UIKit import Foundation class ItemVC: GenericVC, UITextFieldDelegate { @IBOutlet var scrollView: UIScrollView! @IBOutlet var nameTextField: UITextField! @IBOutlet var quantityTextField: UITextField! // MARK: - DELEGATE: UITextField func textFieldDidBeginEditing(textField: UITextField) { switch (textField) { case self.nameTextField: if self.nameTextField.text == “New Item” { self.nameTextField.text = ”” } break; default:break; } } func textFieldDidEndEditing(textField: UITextField) { if let item = self.selectedObject as? Item { switch (textField) { case self.nameTextField: if self.nameTextField.text == ”” { self.nameTextField.text = “New Item” } item.name = self.nameTextField.text break; case self.quantityTextField: if let text = self.quantityTextField.text, floatValue = Float(text) { item.quantity = NSNumber(float: floatValue) } break;

default:break; } } else {self.done()} } }

The textFieldDidBeginEditing function clears the text field when the item name is exactly “New Item”. This saves the user from having to backspace all the text before entering the item name. The textFieldDidEndEditing function sets the item to “New Item” if there’s no text in the nameTextField. This is required so that there are no items with blank names on the Prepare and Shop table views. For the quantityTextField, the item quantity is set according to the number in this text field. Update Groceries as follows to implement the DELEGATE: UITextField section: 1. Replace all code in ItemVC.swift with the code from Listing 6.3.

The VIEW Section The VIEW section has four functions: The refreshInterface function refreshes the interface using the values of the selected managed object, provided there is one. The viewDidLoad function configures the view as a text field delegate. It also calls a superclass function responsible for hiding the keyboard when the view background is tapped and displays a Done button. The viewWillAppear function calls refreshInterface whenever the view is about to appear, which ensures fresh data is visible. It also shows the keyboard immediately when a new item is being created. The viewDidDisappear function saves the context whenever the view disappears. Although it’s not strictly necessary to save the context every time a view disappears, it’s a good idea to persist data regularly in case the device loses power. Saving becomes more important when iCloud is introduced because it ensures changes are updated outside the application. Listing 6.4 shows the code involved. Listing 6.4 View Section (ItemVC.swift) Click here to view code image // MARK: - VIEW func refreshInterface () { if let item = self.selectedObject as? Item { self.nameTextField.text = item.name self.quantityTextField.text = item.quantity?.stringValue } else {self.done()} } override func viewDidLoad() { super.viewDidLoad()

self.hideKeyboardWhenBackgroundIsTapped() self.nameTextField.delegate = self self.quantityTextField.delegate = self let doneButton = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: “done”) self.navigationItem.rightBarButtonItem = doneButton } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) self.refreshInterface() if self.nameTextField.text == “New Item” { self.nameTextField.text = ”” self.nameTextField.becomeFirstResponder() } } override func viewDidDisappear(animated: Bool) { super.viewDidDisappear(animated) CDHelper.saveSharedContext() }

Update Groceries as follows to implement the VIEW section: 1. Add the code from Listing 6.4 to the bottom of ItemVC.swift before the last curly brace. 2. Run the application and then click + on the Prepare tab to create a new item. 3. Set the item Name to Coffee and the Quantity to 2 and then press Done. As you’re returned to the Prepare table view, you either won’t be able to see the new item or it will be in an unnamed section. This is because it isn’t in an appropriate table view section. This issue occurs because there’s no home location set. This issue also occurs on the Shop tab, in this case because there’s no shop location set. Home and shop locations are critical to the section layout of each table view. This means that when a user hasn’t specified a home or shop location the item should be assigned to a default. The default location is called “? Unknown”. The leading question mark ensures that this location is displayed at the top of the table view, due to the sort descriptor of the fetch. A supporting function now needs to be added to CDOperation.swift that returns the first object it finds matching a given attribute and value. This function is used to find or create the unknown location object. Listing 6.5 shows the code involved. Listing 6.5 Object with Attribute Value For Entity (CDOperation.swift objectWithAttributeValueForEntity) Click here to view code image class func objectWithAttributeValueForEntity(entityName:String, context:NSManagedObjectContext, attribute:String, value:String) -> NSManagedObject? { let predicate = NSPredicate(format: “%K == %@”, attribute, value) let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicate, sort: nil) if let object = objects?.first as? NSManagedObject { return object

} return nil }

The objectWithAttributeValueForEntity function first creates a predicate filter from the given attribute and value. This is then used to get an array of objects matching the predicate. The first object matching the predicate is returned from the function. Update Groceries as follows to enhance CDOperation.swift: 1. Add the code from Listing 6.5 to the bottom of CDOperation.swift before the last curly brace.

Adding Functionality to NSManagedObject Subclasses To keep PrepareTVC and ShopTVC as small as possible, additional functionality that prevents items from having no location are added to Item.swift. This file was created as a part of NSManagedObject subclass generation; however, its contents won’t be overwritten if you regenerate these files again. Listing 6.6 shows the code involved. Listing 6.6 Additional NSManagedObject Subclass Functionality (Item.swift) Click here to view code image import Foundation import CoreData class Item: NSManagedObject { class func ensureLocationsAreNotNilForAllItems () { let predicate = NSPredicate(format: “locationAtHome == nil OR locationAtShop == nil”) let lostItems = CDOperation.objectsForEntity(“Item”, context: CDHelper.shared.context, filter: predicate, sort: nil) if let items = lostItems as? [Item] { for item in items { // Assign orphaned items to unknown home/shop location ensureHomeLocationIsNotNil(item) ensureShopLocationIsNotNil(item) } } } class func ensureHomeLocationIsNotNil(item:Item) { if item.locationAtHome != nil {return} let context = item.managedObjectContext! let entity = “LocationAtHome” let attribute = “storedIn” let value = “? Unknown Location” if let unknown = CDOperation.objectWithAttributeValueForEntity(entity, context:context, attribute:attribute, value:value) as? LocationAtHome {

item.locationAtHome = unknown } else { if let newLocation = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtHome”, inManagedObjectContext:context) as? LocationAtHome { newLocation.storedIn = value item.locationAtHome = newLocation } } } class func ensureShopLocationIsNotNil(item:Item) { if item.locationAtShop != nil {return} let context = item.managedObjectContext! let entity = “LocationAtShop” let attribute = “aisle” let value = “? Unknown Location” if let unknown = CDOperation.objectWithAttributeValueForEntity(entity, context:context, attribute:attribute, value:value) as? LocationAtShop { item.locationAtShop = unknown } else { if let newLocation = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtShop”, inManagedObjectContext:context) as? LocationAtShop { newLocation.aisle = value item.locationAtShop = newLocation } } } }

The Item.swift class has three functions used to ensure that an item has a home and shop location: The ensureHomeLocationIsNotNil function does nothing if the given item already has a home location. If the item does not have a home location, the given item is assigned to the existing “? Unknown” home location. If the “? Unknown” home location does not exist, it is created and assigned. The ensureShopLocationIsNotNil function does nothing if the given item already has a shop location. If the item does not have a shop location, the given item is assigned to the existing “? Unknown” shop location. If the “? Unknown” shop location does not exist, it is created and assigned. The ensureLocationsAreNotNilForAllItems function leverages the other two functions to detect and remediate orphaned items in cases where a home or shop location is deleted. Update Groceries as follows to enhance the Item.swift class: 1. Replace the contents of Item.swift with the code from Listing 6.6. This file should be in the Data Model group. 2. Add Item.ensureLocationsAreNotNilForAllItems() to the

viewDidAppear function of PrepareTVC.swift and ShopTVC.swift on the line before self.performFetch(). The new functions can now be leveraged by ItemVC to ensure that items always have appropriate locations. Calls to this new code require that a reference to the selected item be given, so this must first be unwrapped to ensure that a nil item has not been given. If the item has been deleted, for example, on another device, the ItemVC view should be closed. Listing 6.7 shows the code involved. Listing 6.7 Validate Item (ItemVC.swift validateItem) Click here to view code image // MARK: - DATA func validateItem () { if let item = self.selectedObject as? Item { Item.ensureHomeLocationIsNotNil(item) Item.ensureShopLocationIsNotNil(item) } else {self.done()} }

Update Groceries as follows to ensure items always have locations: 1. Add the code from Listing 6.7 to the bottom of ItemVC.swift before the last curly brace. 2. Add self.validateItem() to the viewDidDisappear function of ItemVC.swift on the line before CDHelper.saveSharedContext(). 3. Add self.validateItem() to the viewWillAppear function of ItemVC.swift on the line before self.refreshInterface(). 4. Run the application again, tap the Accessory Detail Button next to the Coffee item, and then go back to the table view. The expected result is shown in Figure 6.8. If you receive an error regarding an attempt to delete and reload the same index path or if the Coffee item disappears, just run the application again.

Figure 6.8 The ? Unknown Location with its own section

Units, Home Locations, and Shop Locations Users must be able to add and edit units, home locations, and shop locations. The technique to achieve this goal is similar to the technique used to add and edit items. This means there will be a table view with a custom CDTableViewController subclass and an additional view for editing each of these object types. Update Groceries as follows to add the generic table view and an additional view: 1. Select Main.storyboard. 2. Drag a new Table View Controller onto the storyboard to the right of the existing Item view. 3. Select the Prototype Cell of the new Table View Controller and set its Reuse Identifier to Cell using Attributes Inspector (Option+ +4). 4. With the new Table View Controller selected, click Editor > Embed In > Navigation Controller. 5. Drag a Bar Button Item onto the top left of the new Table View Controller. 6. Set the new Bar Button Item System Item to Done. 7. Drag a Bar Button Item onto the top right of the new Table View Controller. 8. Set the new Bar Button Item System Item to Add. 9. Drag a new View Controller onto the storyboard to the right of the new Table View Controller. 10. Hold down Control and drag a line from the Prototype Cell of the new Table View Controller to the new View Controller and then select Selection Segue > show. 11. Set the Storyboard Segue Identifier of the new segue to Edit Object Segue. 12. Hold down Control and drag a line from the new Table View Controller’s Add (+) button to the new View Controller and then select Action Segue > show. 13. Set the Storyboard Segue Identifier of the new segue to Add Object Segue. 14. Select new View Controller and un-check Extend Edges Under Top Bars. 15. Drag a Text Field anywhere onto the new View Controller and then configure it as follows using Attributes Inspector (Option+ +4): Set Font to System Bold 17.0. Set Text Alignment to Center. Set Placeholder Text to Name. Set Border Style to the Line (represented by a rectangle). Set Capitalization to Sentences. Set Background to Other > Pencils > Mercury (the second lightest gray pencil).

16. Configure the Height of the text field to 48 using Size Inspector (Option+ +5). 17. Widen and position the Name text field to the edge guides, as shown on the right of Figure 6.9.

Figure 6.9 Generic controllers The generic controllers are ready, so it’s time to use them as the basis of the add/edit controllers for units, home locations, and shop locations. Update Groceries as follows to replicate the generic controllers: 1. Select together the generic Navigation Controller, Table View Controller, and View Controller built in the previous steps and then click Edit > Duplicate. Zooming to 50% may make this easier. 2. Drag the duplicated controllers above the original controllers. 3. Repeat the duplication procedure and arrange the controllers as shown in Figure 6.9. You should have nine new controllers, as shown in Figure 6.10. The top three are for units, the middle three for home locations, and the bottom three for shop locations. The controllers will be reached via three new buttons positioned on the Item view. Until the buttons are implemented, there will be a warning in Xcode regarding how these new controllers are unreachable, which you can safely ignore for the time being.

Figure 6.10 Generic controllers, in triplicate

Adding and Editing Units To reach the new controllers, new buttons are required in the Item view. The first button required is the Add Units button. This button segues to a Units table view, which is embedded in a Navigation Controller. The Units table view behaves like the Prepare table view in that it has an associated view for editing the selected object (in this case, a unit). Update Groceries as follows to add the button icons: 1. Download and extract the button icons from http://www.timroadley.com/LCDwS/Icons_ItemVC.zip. 2. Select the Assets.xcassets asset catalog. 3. Drag the new icons into the asset catalog, as shown in Figure 6.11.

Figure 6.11 New button icons Update Groceries as follows to add the Add Unit button: 1. Select Main.storyboard. 2. Drag a Button anywhere onto the existing Scroll View of the ItemVC view and then configure it as follows using Attributes Inspector (Option+ +4): Set the Type to Custom. Delete the text that reads “Button.” Set the Image to add_units. 3. Configure the Button as follows using Size Inspector (Option+ +5): Ensure Width is 48. Ensure Height is 48. 4. Hold down Control and drag a line from the new button to the Navigation Controller that leads to the topmost of the three generic table view controllers built in the previous steps and then select Action Segue > present modally. 5. Drag, center, and widen to fit a new Text View beneath the existing Name text field on the Unit view controller (topmost on the right), configuring it as follows: Set Text content to Enter a unit of measurement, E.g. ‘ml’, ‘pkt’ or ‘items’. Set Color to Light Gray Color. Set Font to System Bold 16.0. Set Alignment to Centered. Un-check Editable and Selectable. 6. Create a Height constraint for the Name text field by holding down Control and drawing a vertical line within the Name text field. 7. Create a Height constraint for the Text View by holding down Control and drawing a vertical line within the Text View. 8. Click Editor > Resolve Auto Layout Issues > All Views in View Controller > Add Missing Constraints. If you run the application and make your way to the Item view, you can test out the new Add Unit button. There’s no code behind any of these views yet, so it’s time to create a

CDTableViewController subclass so these views function correctly. Without this code, editing the unit name has no effect, nor does pressing the Done button. This means you’ll currently get stuck in the modal popover. If you were unable to see the Add Unit button, it’s probably due to constraints that haven’t been implemented yet, so try rotating the view or repositioning the button on the Storyboard.

Implementing UnitsTVC The Units table view displays a list of available units. When the view loads, the table should be populated with all unit objects found in the persistent store, sorted by name. When a unit is swiped, it should be deleted. When the Done button is pressed, the units view should be dismissed. UnitsTVC is a CDTableViewController subclass and implements this required functionality, as shown in Listing 6.8. All code found in this listing should be familiar because it has been similarly implemented previously. Listing 6.8 Units Table View Controller (UnitsTVC.swift) Click here to view code image import UIKit import CoreData class UnitsTVC: CDTableViewController { // MARK: - CELL CONFIGURATION override func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { cell.accessoryType = UITableViewCellAccessoryType.None if let unit = self.frc.objectAtIndexPath(indexPath) as? Unit { if let textLabel = cell.textLabel { textLabel.text = unit.name } } } // MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // CDTableViewController subclass customization self.entity = “Unit” self.sort = [NSSortDescriptor(key: “name”, ascending: true)] self.fetchBatchSize = 50 } // MARK: - VIEW override func viewDidLoad() { super.viewDidLoad() self.navigationItem.title = “Units” self.performFetch() } // MARK: - DATA SOURCE: UITableView override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {

if let object = self.frc.objectAtIndexPath(indexPath) as? NSManagedObject { self.frc.managedObjectContext.deleteObject(object) } CDHelper.saveSharedContext() } // MARK: - INTERACTION @IBAction func done (sender: AnyObject) { if let parent = self.parentViewController { parent.dismissViewControllerAnimated(true, completion: nil) } } }

Update Groceries as follows to implement UnitsTVC: 1. Select the Table View Controllers group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the class name to save as UnitsTVC. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in UnitsTVC.swift with the code from Listing 6.8 and then save the class file (press +S). 7. Select Main.storyboard. 8. Set the Custom Class of the Units Table View Controller to UnitsTVC using Identity Inspector (Option+ +3). 9. Hold down Control and drag a line from the Done button on the Units Table View Controller to the yellow circle at the top of the same view and then select Sent Actions > done. If you can’t see this option close and reopen the Xcode project and try again. The Units table view is now ready. The next step is to implement the code required for the Unit view controller, including the relevant segue to get to it.

Implementing UnitVC The Unit view allows editing of a unit’s name using a text field. When the view loads, the text field is populated with the name of the unit selected on the table view. When the Done button is pressed, the view should be dismissed. UnitVC is a GenericVC subclass and implements this required functionality. Listing 6.9 shows the code involved. Listing 6.9 Unit View Controller (UnitVC.swift) Click here to view code image

import UIKit class UnitVC: GenericVC, UITextFieldDelegate { @IBOutlet var nameTextField: UITextField! // MARK: - DELEGATE: UITextField func textFieldDidBeginEditing(textField: UITextField) { switch (textField) { case self.nameTextField: if self.nameTextField.text == “New Unit” { self.nameTextField.text = ”” } break; default:break; } } func textFieldDidEndEditing(textField: UITextField) { if let unit = self.selectedObject as? Unit { switch (textField) { case self.nameTextField: if self.nameTextField.text == ”” { “New Unit” } unit.name = self.nameTextField.text break; default:break; } } else {self.done()} } // MARK: - VIEW func refreshInterface () { if let unit = self.selectedObject as? Unit { self.nameTextField.text = unit.name } else {self.done()} } override func viewDidLoad() { super.viewDidLoad() self.hideKeyboardWhenBackgroundIsTapped() self.nameTextField.delegate = self let doneButton = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: “done”) self.navigationItem.rightBarButtonItem = doneButton } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) self.refreshInterface() self.nameTextField.becomeFirstResponder() } override func viewDidDisappear(animated: Bool) { super.viewDidDisappear(animated)

CDHelper.saveSharedContext() NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChanged” object: nil)

} }

When editing begins, the textFieldDidBeginEditing function checks whether this is a new unit. If it is a new unit, nothing is shown in the text field as it is assumed that the user would just delete this text. Once editing finishes, the textFieldDidEndEditing function updates the selected unit.name attribute value to match what the user typed into the nameTextField. The refreshInterface function refreshes the nameTextField with the name of the selected unit from the unit.name attribute value. The viewDidLoad function implements the standard keyboard-hiding techniques used previously in this book. It also configures the nameTextField delegate and then sets up a Done button. The viewWillAppear function refreshes the interface and makes the name text field the first responder, so the keyboard is shown immediately. The viewDidDisappear function saves the shared context and ensures the observing table views are notified of a location change. This is needed because an NSFetchResultsController doesn’t track related objects. Update Groceries as follows to create the UnitVC class: 1. Select the View Controllers group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the class name to save as UnitVC. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in UnitVC.swift with the code from Listing 6.9. 7. Select Main.storyboard. 8. Set the Custom Class of the Unit View Controller to UnitVC using Identity Inspector (Option+ +3). 9. Show the Assistant Editor (Option+ +Return). 10. Set the Assistant Editor to Automatic > UnitVC.swift, as shown at the top right of Figure 6.12.

Figure 6.12 Connecting the unit name text field to code 11. Drag a line from the nameTextField variable connector found in UnitVC.swift to the Name text field in the Unit view as shown in Figure 6.12. 12. Show the Standard Editor ( +Return).

Segueing from UnitsTVC to UnitVC A segue is required to reach UnitVC from UnitsTVC. This segue passes the selected unit to UnitVC. UnitVC uses this object to refresh its user interface appropriately. Listing 6.10 shows the code involved. Listing 6.10 Units Table View Controller Segue Section (UnitsTVC.swift prepareForSegue) Click here to view code image // MARK: - SEGUE override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if let unitVC = segue.destinationViewController as? UnitVC { if segue.identifier == “Add Object Segue” { let object = NSEntityDescription.insertNewObjectForEntityForName(“Unit”, inManagedObjectContext: CDHelper.shared.context) unitVC.segueObject = object } else if segue.identifier == “Edit Object Segue” { if let indexPath = self.tableView.indexPathForSelectedRow { if let object = self.frc.objectAtIndexPath(indexPath) as? NSManagedObject { unitVC.segueObject = object } } } else {print(“Unidentified Segue Attempted!”)} } }

Update Groceries as follows to implement the segue from UnitsTVC to UnitVC: 1. Add the code from Listing 6.10 to the bottom of UnitsTVC.swift before the last curly brace. Run the application and try adding some units via the new unit views. You won’t be able to assign a unit to an item yet—that’s a goal of the next chapter.

Adding and Editing Home or Shop Locations The home and shop location table views and views are a near match to UnitsTVC and UnitVC. The only difference is they support home or shop location objects instead of unit objects. To avoid a large exercise in copy and paste, premade location class files are available for download. Update Groceries as follows to add the premade location class files: 1. Download and extract the location classes from http://www.timroadley.com/LCDwS/LocationClasses.zip. 2. Drag the LocationsAtHomeTVC and LocationsAtShopTVC class files into the Groceries Table View Controllers group. Ensure Copy items if needed and the Groceries target are checked before clicking Finish. 3. Drag the LocationAtHomeVC and LocationAtShopVC class files into the Groceries View Controllers group. Ensure Copy items into destination group’s folder and the Groceries target are checked before clicking Finish.

Configuring the Home Location Views To use these new classes, a similar exercise to the one used to configure the unit views is now required. These steps configure buttons, connections, and text fields associated to the home location views. Update Groceries as follows: 1. Select Main.storyboard. 2. Drag a Button anywhere into the existing Scroll View of the Item view, and then configure it as follows using Attributes Inspector (Option+ +4): Set the Type to Custom. Delete the text that reads “Button.” Set the Image to add_homelocations. 3. Configure the button as follows using Size Inspector (Option+ +5): Ensure Width is 48. Ensure Height is 48. 4. Hold down Control and drag a line from the new button to the Navigation Controller to the right that is beneath the Units Navigation Controller, which leads to the middle three generic controllers from previous steps. Then select Action Segue > present modally. 5. Copy the Text View from the Unit view to the same position in the Home Location view and then change the text content to Enter the location an item is expected to be found at home. E.g. ‘Pantry’ or ‘Bathroom’. 6. Create a Height constraint for the Name text field by holding down Control and drawing a vertical line within the Name text field.

7. Click Editor > Resolve Auto Layout Issues > All Views in View Controller > Add Missing Constraints. 8. Set the Custom Class of the Home Location view controller to LocationAtHomeVC using Identity Inspector (Option+ +3). 9. Set the Custom Class of the Home Locations Table View Controller to LocationsAtHomeTVC using Identity Inspector (Option+ +3). 10. Hold down Control and drag a line from the Done button on the Home Locations table view controller to the yellow circle at the top of the same view, and then select Sent Actions > done. 11. Show the Assistant Editor (Option+ +Return) and select the LocationAtHomeVC view. 12. Set the Assistant Editor to Automatic > LocationAtHomeVC.swift using the approach demonstrated previously in Figure 6.12. 13. Connect the existing nameTextField variable found in LocationAtHomeVC.swift to the text field on the home location view. Use the approach demonstrated previously in Figure 6.12.

Configuring the Shop Location Views To use the new shop location classes, another similar exercise is now required. These steps configure the buttons, connections, and text fields associated to the shop location views. Update Groceries as follows: 1. Select Main.storyboard and show the Standard Editor ( +Return). 2. Drag a Button anywhere into the existing Scroll View of the Item view and then configure it as follows using Attributes Inspector (Option+ +4): Set the Type to Custom. Delete the text that reads “Button.” Set the Image to add_shoplocations. 3. Configure the button as follows using Size Inspector (Option+ +5): Ensure Width is 48. Ensure Height is 48. 4. Hold down Control and drag a line from the new button to the Navigation Controller beneath the Home Locations Navigation Controller, which leads to the bottom three generic controllers from previous steps. Then select Action Segue > present modally. 5. Copy the Text View from the Home Location view to the same position in the Shop Location view and then change the text content to Enter the location an item is expected to be found at the shop. E.g. ‘Produce Section’ or ‘Deli’.

6. Create a Height constraint for the Name text field by holding down Control and drawing a vertical line within the Name text field. 7. Click Editor > Resolve Auto Layout Issues > All Views in View Controller > Add Missing Constraints. 8. Set the Custom Class of the Shop Location view controller to LocationAtShopVC using Identity Inspector (Option+ +3). 9. Set the Custom Class of the Shop Locations Table View Controller to LocationsAtShopTVC using Identity Inspector (Option+ +3). 10. Hold down Control and drag a line from the Done button found on the Shop Locations table view controller to the yellow circle at the top of the same view and then select Sent Actions > done. 11. Show the Assistant Editor (Option+ +Return) and select the LocationAtShopVC view. 12. Set the Assistant Editor to Automatic > LocationAtShopVC.swift using the approach demonstrated previously in Figure 6.12. 13. Connect the existing nameTextField variable found in LocationAtShopVC.swift to the text field on the shop location view. Use the approach demonstrated previously in Figure 6.12. 14. Show the Standard Editor ( +Return). 15. Select the ItemVC view and align the icons as per Figure 6.13.

Figure 6.13 Icon alignment 16. Click Editor > Resolve Auto Layout Issues > All Views in ItemVC > Reset to Suggested Constraints.

The unit, home location, and shop location views are now in place. Run the application to examine your handiwork!

Summary The application is really starting to take shape, as the capability to edit items, units, home locations, and shop locations has been implemented. You learned how to pass an object between views and how to ensure the user interface is refreshed with the latest information available. In addition, you also learned how to perform a data-integrity check as a part of transitioning from a view, as demonstrated with the ensureHomeLocationIsNotNil and ensureShopLocationIsNotNil functions.

Exercises Why not build on what you’ve learned by experimenting? 1. Select some items on the Prepare tab so they go red. Change to the Shop tab and examine the section they’re placed in. This feature is the crux of Groceries—all you have to do is tap an item and it’s already organized by its location in the Shop tab. 2. Test out the Clear feature on the Prepare and Shop tabs. 3. Add some units and home and shop locations. Delete them and observe the impact on the Prepare and Shop tabs.

7. Picker Views In the middle of difficulty lies opportunity. Albert Einstein In Chapter 6, “Views,” you started building the Item view as the concept of passing managed objects around the application was demonstrated. Its primary focus introduced text fields as a means to edit managed objects. This chapter explains how to create special text fields that have a Core Data-driven UIPickerView as the input view. The purpose of a picker text field is to make selecting from predefined values, such as a shop aisle, as easy and fast as possible.

Picker View Basics Picker views make it easy for a user to relate managed objects to each other. In Groceries, this makes picker views ideal for setting the item relationships unit.name, locationAtHome.storedIn, and locationAtShop.aisle. As an example, Figure 7.1 shows how a unit can be selected for an item with a picker view. To make the picker view appear, a special UITextField subclass is created. This subclass presents the picker view as an input view, which is the same type of view that the keyboard is usually shown in.

Figure 7.1 A picker view

Note To continue building the sample application, you need to add the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter06.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name.

Introducing CDPickerTextField To provide a picker view as an input view, a UITextField subclass called CDPickerTextField is created. This new class adopts the NSFetchedResultsControllerDelegate, UIPickerViewDelegate, and UIPickerViewDataSource protocols. Just as CDTableViewController underpins PrepareTVC and ShopTVC, so too does CDPickerTextField underpin other custom subclasses. These custom subclasses are assigned to new text fields used to set an item’s unit, home location, or shop location with a picker. The initial code for CDPickerTextField.swift is shown in Listing 7.1 Listing 7.1 Core Data Picker Text Field (CDPickerTextField.swift) Click here to view code image import UIKit import CoreData protocol CDPickerTextFieldDelegate { func selectedObject(object:NSManagedObject, changedForPickerTF pickerTF:CDPickerTextField) } class CDPickerTextField: UITextField, NSFetchedResultsControllerDelegate, UIPickerViewDelegate, UIPickerViewDataSource { // Override var entity = “MyEntity” var sort = [NSSortDescriptor(key: “myAttribute”, ascending: true)] // Optionally Override var context = CDHelper.shared.context var filter:NSPredicate? = nil var cacheName:String? = nil var sectionNameKeyPath:String? = nil var fetchBatchSize = 0 // 0 = No Limit // Supporting Variables var toolbar:UIToolbar? var picker:UIPickerView? var pickerDelegate:CDPickerTextFieldDelegate? var selectedTitle:String? var selectedIndex:NSIndexPath? // MARK: - FETCHED RESULTS CONTROLLER

lazy var frc: NSFetchedResultsController = { let request = NSFetchRequest(entityName:self.entity) request.sortDescriptors = self.sort request.fetchBatchSize = self.fetchBatchSize if let _filter = self.filter {request.predicate = _filter} let newFRC = NSFetchedResultsController( fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: self.sectionNameKeyPath, cacheName: self.cacheName) newFRC.delegate = self return newFRC }() // MARK: - DELEGATE & DATASOURCE: UIPickerView func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { return self.frc.sections?.count ?? 0 } func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return self.frc.sections?[component].numberOfObjects ?? 0 } func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return “Override titleForRow:forComponent” } func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { // Pass the selected object back to the picker delegate if let _pickerDelegate = self.pickerDelegate { let indexPath = NSIndexPath(forRow: row, inSection: component) if let object = self.frc.objectAtIndexPath(indexPath) as? NSManagedObject { _pickerDelegate.selectedObject(object, changedForPickerTF: self) } } } }

Several key sections make up CDPickerTextField, as shown in Listing 7.1. To start with, there is a protocol declaration at the top defining the selectedObject:changedForPickerTF function. This function is implemented in CDPickerTextFieldDelegate classes containing a picker text field, so they can receive a reference to the selected object when a user selects it. Following that, is a set of variables that support the picker view. You may recognize some of these from CDTableViewController, and they serve the same purpose here. The new variables found in the Supporting Variables section serve the following purpose: toolbar is a reference to the toolbar found directly above the picker view. The toolbar has a Done button that closes the picker view. picker is a reference to the picker view.

pickerDelegate is a reference to anything set as a delegate of CDPickerTextField. Messages are sent to delegates when a row is selected on the picker, telling them what object was selected. selectedTitle is a reference to the string name of the currently selected object. For example, if an item had a unit name of Kg, the selectedTitle would be “Kg”. This string is used later to help identify the index of the object matching this name. selectedIndex is a reference to a selected managed object and is calculated by a special function implemented in a CDPickerTextField subclass. The next section is FETCHED RESULTS CONTROLLER, which has been copied from the CDTableViewController class implemented in Chapter 5, “Table Views.” The final section is DELEGATE & DATASOURCE: UIPickerView, which is similar to the equivalent section in CDTableViewController except it supports a picker view instead of a table view. It implements the following UIPickerViewDataSource and UIPickerViewDelegate protocol functions: The numberOfComponentsInPickerView function is used to specify how many columns the picker view has. In a picker view, a column is referred to as a component. The number of components in the picker view is drawn from the number of sections in the fetched results controller via self.frc.sections?.count. If there are no sections in the fetched results, there will be no components in the picker. The numberOfRowsInComponent function is used to specify the total number of rows that each component in the picker view has. Again the fetched results controller is used to return an appropriate value for each component by leveraging the section info. The titleForRow function is used to specify what’s displayed in each row of each component. This function is similar to the cellForRowAtIndexPath function a table view uses to populate each row and should be overridden in CDPickerTextField subclasses. The didSelectRow function is used to handle a row in a component being selected. The CDPickerTextField by default sends a message to any delegates, notifying them that the selected object has changed. All CDPickerTextField subclasses should override this function. Update Groceries as follows to configure CDPickerTextField: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the file to save as CDPickerTextField.

5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in CDPickerTextField.swift with the code from Listing 7.1. The next step is to add the functions for fetching and responding to data content changes. Listing 7.2 shows the code involved in the new FETCHING and DELEGATE: NSFetchedResultsController sections. Listing 7.2 Fetching and Content Changes (CDPickerTextField.swift performFetch, controllerDidChangeContent) Click here to view code image // MARK: - FETCHING func performFetch () { self.frc.managedObjectContext.performBlock ({ do { try self.frc.performFetch() } catch { print(“\(__FUNCTION__) FAILED : \(error)”) } if let _picker = self.picker {_picker.reloadAllComponents()} }) } // MARK: - DELEGATE: NSFetchedResultsController func controllerDidChangeContent(controller: NSFetchedResultsController) { if let _picker = self.picker {_picker.reloadAllComponents()} }

The FETCHING section was copied from the CDTableViewController class and only one line of code changed. The updated code refreshes a picker view instead of a table view. The DELEGATE: NSFetchedResultsController section implements one function to reload the picker when the underlying data changes. Update Groceries as follows to configure CDPickerTextField: 1. Add the code from Listing 7.2 to the bottom of CDPickerTextField.swift before the last curly brace. The final section to be added to CDPickerTextField is INTERACTION. This section contains functions responsible for creating the picker view in the place of the keyboard and an accessory view above it for a Done button. It also contains a function that dismisses the input view containing the picker. Listing 7.3 shows the code involved. Listing 7.3 Interaction Section (CDPickerTextField.swift) Click here to view code image // MARK: - INTERACTION func done () {

self.resignFirstResponder() } func createInputView () -> UIView? { let newPicker = UIPickerView(frame: CGRectZero) newPicker.showsSelectionIndicator = true newPicker.dataSource = self newPicker.delegate = self self.picker = newPicker return self.picker } func createInputAccessoryView () -> UIView? { let newToolbar = UIToolbar(frame: CGRectZero) var frame = newToolbar.frame frame.size.height = 44 newToolbar.frame = frame let space = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FlexibleSpace, target: nil, action: nil) let doneButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action: “done”) newToolbar.setItems([space,doneButton], animated: false) self.toolbar = newToolbar return self.toolbar }

The done function is self-explanatory because all it does is hide the picker by resigning as first responder. The createInputView function is responsible for instantiating and returning a new picker view. This function is called by CDPickerTextField subclasses so they present a picker instead of a keyboard. The createInputAccessoryView function is responsible for instantiating and returning a new UIToolbar view. This function is called by CDPickerTextField subclasses so they present a toolbar above the picker to display a Done button. Update Groceries as follows to implement the INTERACTION section: 1. Add the code from Listing 7.3 to the bottom of CDPickerTextField.swift before the last curly brace.

Introducing UnitPickerTF To choose an existing unit of measurement for an item, a picker is required. To present a picker, a text field with a custom CDPickerTextField subclass is required. Listing 7.4 shows the code involved in a new CDPickerTextField subclass called UnitPickerTF.swift, which is specific to unit selection. Listing 7.4 Unit Picker Text Field (UnitPickerTF.swift) Click here to view code image import UIKit import CoreData class UnitPickerTF: CDPickerTextField {

// MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // CDPickerTextField subclass customization self.entity = “Unit” self.sort = [NSSortDescriptor(key: “name”, ascending: true)] self.fetchBatchSize = 10 self.performFetch() self.inputView = self.createInputView() self.inputAccessoryView = self.createInputAccessoryView() } // MARK: - ROW SELECTION func setSelectedRowForComponent (component:Int) { switch (component) { case 0: switch (picker, selectedTitle, self.frc.fetchedObjects as? [Unit]) { case (.Some(let _picker), .Some(let selectedTitle), .Some(let units)): // Find the row with text matching the selected title var row = 0 for (unit) in units { if selectedTitle == unit.name { _picker.selectRow(row, inComponent: component, animated: false) break } row++ } default: print(“_picker or selectedTitle is nil or fetchedObjects not [Unit]”) } break; // Add more cases to support additional picker components default: break; } } // MARK: - DATASOURCE: UIPickerView override func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { let indexPath = NSIndexPath(forRow: row, inSection: component) if let unit = self.frc.objectAtIndexPath(indexPath) as? Unit { return unit.name } return ”” } }

This CDPickerTextField subclass UnitPickerTF implements three functions:

The init function is responsible for preparing the variables needed by the fetch results controller, most importantly the entity and sort descriptor. It also triggers the fetch and sets the input view to display a picker instead of a keyboard using the functions implemented in CDPickerTextField. The setSelectedRowForComponent function is configured to select a default row on the picker. Any existing association an item may have to the objects available on the picker determines this default row selection. For example, if a Bananas item object is already associated with a Kg unit object, the default picker selection is Kg. Fast enumeration is used to find the appropriate row to set as the default based on the fetched objects. The titleForRow function customizes each row of the picker to show the name of the unit that the row represents. Update Groceries as follows to create the UnitPickerTF class: 1. Right-click the existing Groceries group and then select New Group. 2. Set the new group name to Picker Text Fields. 3. Click File > New > File…. 4. Create a new iOS > Source > Swift File and then click Next. 5. Set the file to save as UnitPickerTF. 6. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 7. Replace all code in UnitPickerTF.swift with the code from Listing 7.4.

Creating the Unit Picker The UnitPickerTF class is ready to use, so a new text field is required on ItemVC. Update Groceries as follows to create the unit picker text field: 1. Select Main.storyboard. 2. Drag a Text Field onto the Scroll View of the Item view to the right of the Qty text field; then configure it as follows using Attributes Inspector (Option+ +4): Set the Font to System Bold 17.0. Set the Text Alignment to Center. Set the Placeholder Text to Unit. Set the Border Style to the Line (represented by a rectangle). Set Background to Other > Pencils > Mercury (the second lightest gray pencil). 3. Configure the Height of the Text Field to 48 using Size Inspector (Option+ +5). 4. Arrange the Unit text field to the guides as shown in Figure 7.2 and then set its Custom Class to UnitPickerTF using Identity Inspector (Option+ +3).

Figure 7.2 The unit picker text field 5. Add a Vertical Spacing constraint between the Name text field and the Unit text field by holding down Control and dragging a line between them. When you release the mouse button, select Vertical Spacing. 6. Add a Horizontal Spacing constraint between the Qty and Unit text fields by holding down Control and dragging a line between them. When you release the mouse button, select Horizontal Spacing. 7. Add a Horizontal Spacing constraint between the Unit text field and the Unit button by holding down Control and dragging a line between them. When you release the mouse button, select Horizontal Spacing. 8. Add a Height constraint to the Unit text field by holding down Control and dragging a small vertical line over it. When you release the mouse button, select Height. 9. Add a Width constraint to the Unit button by holding down Control and dragging a small horizontal line across it. When you release the mouse button, select Width.

Connecting the Unit Picker To reference the new picker text field in code, the outlets need to be connected. The Assistant Editor is used to achieve this. Update Groceries as follows to connect the unit picker text field to ItemVC.swift: 1. Select Main.storyboard. 2. Ensure the Item View Controller is selected and then show the Assistant Editor (Option+ +Return). 3. Set the Assistant Editor to Automatic > ItemVC.swift if it isn’t set to this already. 4. Hold down Control and drag a line from the Unit Text Field to ItemVC.swift beneath the existing quantityTextField. Set the Name of the new property to unitPickerTextField, as shown in Figure 7.3. Double-check that the Type is UnitPickerTF and that the Storage is Strong before clicking Connect.

Figure 7.3 Connecting the unit picker text field 5. Show the Standard Editor ( +Return).

Configuring ItemVC for the Unit Picker For pickers to send selected managed objects back to their related text field, the ItemVC needs to adopt the CDPickerTextFieldDelegate protocol and import the CoreData framework. Listing 7.5 shows the new code in bold. Listing 7.5 Core Data Picker Text Field Delegate Protocol Adoption (ItemVC.swift) Click here to view code image import UIKit import Foundation import CoreData class ItemVC: GenericVC, UITextFieldDelegate, CDPickerTextFieldDelegate { // Existing code }

Update Groceries as follows to adopt the CDPickerTextFieldDelegate protocol: 1. Update ItemVC.swift with the additional code shown in bold in Listing 7.5. Xcode should now warn you that ItemVC does not conform to the CDPickerTextFieldDelegate protocol, which you can safely ignore. The CDPickerTextFieldDelegate protocol function required to suppress the warning is shown in Listing 7.6. Listing 7.6 Unit Picker Text Field Delegate Code (ItemVC.swift) Click here to view code image // MARK: - DELEGATE: CDPickerTextFieldDelegate func selectedObject(object: NSManagedObject, changedForPickerTF pickerTF: CDPickerTextField) {

if let item = self.selectedObject as? Item { switch (pickerTF) { case self.unitPickerTextField: if let unit = object as? Unit { item.unit = unit } break; default: break; } } else {self.done()} self.refreshInterface() }

The selectedObject:changedForPickerTF function is responsible for setting the text of a given text field using the given managed object. Currently it handles the unit picker text field; however, additional cases will be added to support other text fields later. Update Groceries as follows to implement the CDPickerTextFieldDelegate protocol function: 1. Add the code from Listing 7.6 to the bottom of ItemVC.swift before the last curly brace. When the user taps the unit picker text field, the default row of the picker needs to be set. This is achieved by updating the existing textFieldDidBeginEditing method as shown in bold in Listing 7.7. Listing 7.7 Text Field Delegate (ItemVC.swift) Click here to view code image // MARK: - DELEGATE: UITextField func textFieldDidBeginEditing(textField: UITextField) { switch (textField) { case self.nameTextField: if self.nameTextField.text == “New Item” { self.nameTextField.text = ”” } break; case self.unitPickerTextField: self.unitPickerTextField.performFetch() self.unitPickerTextField.setSelectedRowForComponent(0) break; default:break; } }

Update Groceries as follows to configure the unit picker text field to set the selected row when editing begins: 1. Update the textFieldDidBeginEditing function of ItemVC.swift with the bold code shown in Listing 7.7. Because the ItemVC class now adopts both the CDPickerTextFieldDelegate and

UITextFieldDelegate protocols, it needs to be set as a delegate of each. Being a delegate of the former ensures selected values on the unit picker can be reflected in the unit text field. Being a delegate of the latter ensures the unit text field moves into view when obscured by the unit picker. To achieve this functionality, the UITextFieldDelegate functions set the selected text field as the activeField later in the chapter. Listing 7.8 shows the code involved in setting the delegates. Listing 7.8 Unit Text Field and Picker Delegates (ItemVC.swift viewDidLoad) Click here to view code image self.unitPickerTextField.delegate = self self.unitPickerTextField.pickerDelegate = self

Update Groceries as follows to configure the unit picker text field delegates: 1. Add the code from Listing 7.8 to the bottom of the viewDidLoad function of ItemVC.swift. The final touch required to the unit picker text field is to ensure that its text value is populated with the current unit name whenever the view appears. The viewWillAppear function already has a call to refreshInterface, so this is an ideal place to set the unit picker text field text value. Listing 7.9 shows the new code in bold. Listing 7.9 Interface Refresh for Units (ItemVC.swift refreshInterface) Click here to view code image func refreshInterface () { if let item = self.selectedObject as? Item { self.nameTextField.text = item.name self.quantityTextField.text = item.quantity?.stringValue self.unitPickerTextField.text = item.unit?.name ?? ”” self.unitPickerTextField.selectedTitle = item.unit?.name ?? ”” } else {self.done()} }

Update Groceries as follows to ensure the unitPickerTextField displays the appropriate unit name: 1. Add the bold code from Listing 7.9 to the refreshInterface function of ItemVC.swift. 2. Run the application and test setting an item’s unit using the picker. The expected result is shown in Figure 7.4.

Figure 7.4 The unit picker text field in action The unit picker is now completely operational, so it’s time to perform a similar process to implement the home and shop location pickers.

Introducing LocationAtHomePickerTF A picker is required to set the location an item is stored in at home. The technique used to create the unit picker is also used to create the home location picker. Again the init, setSelectedRowForComponent, and titleForRow functions are implemented in a CDPickerTextField subclass. Listing 7.10 shows the code involved. Listing 7.10 Location At Home Picker Text Field (LocationAtHomePickerTF.swift) Click here to view code image import UIKit import CoreData class LocationAtHomePickerTF: CDPickerTextField { // MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // CDPickerTextField subclass customization self.entity = “LocationAtHome” self.sort = [NSSortDescriptor(key: “storedIn”, ascending: true)] self.fetchBatchSize = 10 self.performFetch() self.inputView = self.createInputView() self.inputAccessoryView = self.createInputAccessoryView() } // MARK: - ROW SELECTION func setSelectedRowForComponent (component:Int) { switch (component) { case 0: switch (picker, selectedTitle, self.frc.fetchedObjects as? [LocationAtHome]) { case (.Some(let _picker), .Some(let selectedTitle), .Some(let homeLocations)): // Find the row with text matching the selected title var row = 0 for locationAtHome in homeLocations { if selectedTitle == locationAtHome.storedIn { _picker.selectRow(row, inComponent: component, animated: false) break } row++ } default: print(“_picker or selectedTitle is nil or fetchedObjects not [LocationAtHome]”) } break;

// Add more cases to support additional picker components default: break; } } // MARK: - DATASOURCE: UIPickerView override func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { let indexPath = NSIndexPath(forRow: row, inSection: component) if let locationAtHome = self.frc.objectAtIndexPath(indexPath) as? LocationAtHome { return locationAtHome.storedIn } return ”” } }

Update Groceries as follows to create the LocationAtHomePickerTF class: 1. Select the Picker Text Fields group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the file to save as LocationAtHomePickerTF. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in LocationAtHomePickerTF.swift with the code from Listing 7.10. Because the code structure is so similar, it makes sense to create the equivalent code for the shop location picker now too.

Introducing LocationAtShopPickerTF A picker is required to set the location an item is stored in at the shop. The technique used to create the unit picker is also used to create the shop location picker. Again the init, setSelectedRowForComponent, and titleForRow functions are implemented in a CDPickerTextField subclass. Listing 7.11 shows the code involved. Listing 7.11 Location At Shop Picker Text Field (LocationAtShopPickerTF.swift) Click here to view code image import UIKit import CoreData class LocationAtShopPickerTF: CDPickerTextField { // MARK: - INITIALIZATION

required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // CDPickerTextField subclass customization self.entity = “LocationAtShop” self.sort = [NSSortDescriptor(key: “aisle”, ascending: true)] self.fetchBatchSize = 10 self.performFetch() self.inputView = self.createInputView() self.inputAccessoryView = self.createInputAccessoryView() } // MARK: - ROW SELECTION func setSelectedRowForComponent (component:Int) { switch (component) { case 0: switch (picker, selectedTitle, self.frc.fetchedObjects as? [LocationAtShop]) { case (.Some(let _picker), .Some(let selectedTitle), .Some(let shopLocations)): // Find the row with text matching the selected title var row = 0 for locationAtShop in shopLocations { if selectedTitle == locationAtShop.aisle { _picker.selectRow(row, inComponent: component, animated: false) break } row++ } default: print(“_picker or selectedTitle is nil or fetchedObjects not [LocationAtShop]”) } break; // Add more cases to support additional picker components default: break; } } // MARK: - DATASOURCE: UIPickerView override func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { let indexPath = NSIndexPath(forRow: row, inSection: component) if let locationAtShop = self.frc.objectAtIndexPath(indexPath) as? LocationAtShop { return locationAtShop.aisle } return ”” } }

Update Groceries as follows to create the LocationAtShopPickerTF class: 1. Select the Picker Text Fields group.

2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the file to save as LocationAtShopPickerTF. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in LocationAtShopPickerTF.swift with the code from Listing 7.11.

Using the Location Pickers The LocationAtHomePickerTF and LocationAtShopPickerTF classes are ready to use, so two new text fields are required on ItemVC. Update Groceries as follows to create the home and shop location picker text fields: 1. Select Main.storyboard. 2. Drag two Text Fields anywhere onto the Scroll View of the Item view and configure them as follows using Attributes Inspector (Option+ +4): Set the Font of both new text fields to System Bold 17.0. Set the Text Alignment of both new text fields to Center. Set the Border Style of both new text fields to Line (represented by a rectangle). Set Background to Other > Pencils > Mercury (the second lightest gray pencil). Set the Placeholder Text of one of the text fields to Location at Home and the other to Location at Shop. 3. Set the Custom Class of the Location at Home text field to LocationAtHomePickerTF and the Custom Class of the Location at Shop text field to LocationAtShopPickerTF using Identity Inspector (Option+ +3). 4. Configure the Height of both new text fields to 48 using Size Inspector (Option+ +5). 5. Arrange the text fields to the guides as shown in Figure 7.5. Then click Editor > Resolve Auto Layout Issues > All Views in ItemVC > Add Missing Constraints.

Figure 7.5 The home location and shop location picker text fields

Connecting the Location Pickers To reference the new picker text fields in code, the outlets need to be connected. The Assistant Editor is used to achieve this. Update Groceries as follows to connect the home and shop location picker text fields to ItemVC.swift: 1. Select Main.storyboard. 2. Ensure the Item View Controller is selected. 3. Show the Assistant Editor (Option+ +Return). 4. Set the Assistant Editor to Automatic > ItemVC.swift if it isn’t set to this already. 5. Hold down Control and drag a line from the Location at Home text field to ItemVC.swift beneath the existing unitPickerTextField variable. Set the Name of the new property to homeLocationPickerTextField. Doublecheck the Type is LocationAtHomePickerTF and the Storage is Strong before clicking Connect. 6. Hold down Control and drag a line from the Location at Shop text field to ItemVC.swift beneath the existing homeLocationPickerTextField variable. Set the Name of the new property to shopLocationPickerTextField. Double-check the Type is LocationAtShopPickerTF and the Storage is Strong before clicking Connect. 7. Show the Standard Editor ( +Return). When you examine ItemVC.swift, there should now be filled-in circles next to each of the connected picker text fields. The expected result is shown in Figure 7.6.

Figure 7.6 Connected picker text fields

Configuring ItemVC for the Location Pickers To ensure the home and shop location pickers behave like the unit picker, some existing ItemVC.swift functions need to be updated. The first function to be updated is textFieldDidBeginEditing, which is responsible for setting the initial row of the picker. Listing 7.12 shows this new code in bold. Listing 7.12 Setting the Selected Picker Row (ItemVC.swift textFieldDidBeginEditing) Click here to view code image func textFieldDidBeginEditing(textField: UITextField) { switch (textField) { case self.nameTextField: if self.nameTextField.text == “New Item” { self.nameTextField.text = ”” } break; case self.unitPickerTextField: self.unitPickerTextField.performFetch() self.unitPickerTextField.setSelectedRowForComponent(0) break; case self.homeLocationPickerTextField: self.homeLocationPickerTextField.performFetch() self.homeLocationPickerTextField.setSelectedRowForComponent(0) break; case self.shopLocationPickerTextField: self.shopLocationPickerTextField.performFetch() self.shopLocationPickerTextField.setSelectedRowForComponent(0) break; default:break; } }

Update Groceries as follows to ensure the home and show location pickers have an appropriate initial row selected: 1. Replace the textFieldDidBeginEditing function in ItemVC.swift with the code from Listing 7.12. The ItemVC class also needs to be set as a delegate of homeLocationPickerTextField and shopLocationPickerTextField in the same way as with unitPickerTextField. Listing 7.13 shows the code involved

in configuring these delegates. Listing 7.13 Location Text Field and Picker Delegates (ItemVC.swift viewDidLoad) Click here to view code image self.homeLocationPickerTextField.delegate = self self.homeLocationPickerTextField.pickerDelegate = self self.shopLocationPickerTextField.delegate = self self.shopLocationPickerTextField.pickerDelegate = self

Update Groceries as follows to configure the home and shop location picker and text field delegates: 1. Add the code from Listing 7.13 to the bottom of the viewDidLoad function of ItemVC.swift. In addition, the refreshInterface function needs to be updated to cater for the home and shop location picker text fields. Listing 7.14 shows the new code in bold. Listing 7.14 Interface Refresh for Locations (ItemVC.swift refreshInterface) Click here to view code image func refreshInterface () { if let item = self.selectedObject as? Item { self.nameTextField.text = item.name self.quantityTextField.text = item.quantity?.stringValue self.unitPickerTextField.text = item.unit?.name ?? ”” self.unitPickerTextField.selectedTitle = item.unit?.name ?? ”” self.homeLocationPickerTextField.text = item.locationAtHome?.storedIn ?? ”” self.homeLocationPickerTextField.selectedTitle = item.locationAtHome?.storedIn ?? ”” self.shopLocationPickerTextField.text = item.locationAtShop?.aisle ?? ”” self.shopLocationPickerTextField.selectedTitle = item.locationAtShop?.aisle ?? ”” } else {self.done()} }

Update Groceries as follows to ensure the home and shop location text fields display the appropriate information: 1. Update the refreshInterface function of ItemVC.swift with the bold code from Listing 7.14. The final touch involves updating the CDPickerTextField delegate function in ItemVC.swift to cater for the home and shop location pickers. This function has an existing switch statement used to react to the appropriate picker when a delegate function is called. Listing 7.15 shows the new code in bold. Listing 7.15 Location Picker Text Field Delegate Code (ItemVC.swift)

Click here to view code image func selectedObject(object: NSManagedObject, changedForPickerTF pickerTF: CDPickerTextField) { if let item = self.selectedObject as? Item { switch (pickerTF) { case self.unitPickerTextField: if let unit = object as? Unit { item.unit = unit } break; case self.homeLocationPickerTextField: if let locationAtHome = object as? LocationAtHome { item.locationAtHome = locationAtHome } break; case self.shopLocationPickerTextField: if let locationAtShop = object as? LocationAtShop { item.locationAtShop = locationAtShop } break; default: break; } } else {self.done()} self.refreshInterface() }

Update Groceries as follows to enhance the CDPickerTextField delegate function: 1. Replace the selectedObject:changedForPickerTF function in ItemVC.swift with the code from Listing 7.15. All picker views are now completely configured. Run the application and then create and assign some units, home locations, and shop locations.

Picker-Avoiding Text Field When a picker is shown, there’s less room to display its related text field. This means that on a small iPhone 4S screen, the related text field (referred to as the active text field) will likely become hidden behind its picker. To keep the active text field visible on the Item view, the scroll view needs to be resized in response to the keyboard being shown or hidden. Once the scroll view is resized, the active text field can be made visible using the scrollRectToVisible function of UIScrollView. Listing 7.16 shows the code involved. Listing 7.16 Interaction Section (ItemVC.swift) Click here to view code image // MARK: - INTERACTION var activeField:UITextField? func keyboardDidShow (note:NSNotification) { if let info = note.userInfo, keyboardFrameEndUserInfoKey = info[UIKeyboardFrameEndUserInfoKey] as? NSValue {

// Find top of keyboard input view (i.e. picker) var keyboardRect: CGRect = keyboardFrameEndUserInfoKey.CGRectValue() keyboardRect = self.view.convertRect(keyboardRect, fromView: nil) let keyboardTop = keyboardRect.origin.y // Resize scroll view var newScrollViewFrame = CGRectMake(0, 0, self.view.bounds.size.width, keyboardTop) newScrollViewFrame.size.height = keyboardTop self.view.bounds.origin.y self.scrollView.frame = newScrollViewFrame // Scroll to the active Text-Field if let _activeField = self.activeField { self.scrollView.scrollRectToVisible(_activeField.frame, animated: true) } } } func keyboardWillHide () { // Reset Scrollview to the same size as the containing view let defaultFrame = CGRectMake(self.scrollView.frame.origin.x, self.scrollView.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height) self.scrollView.frame = defaultFrame // Scroll to the top again let topFrame = CGRectMake(0, 0, 1, 1) self.scrollView.scrollRectToVisible(topFrame, animated: true) }

As well as a supporting activeField variable, two functions are required as shown in Listing 7.16. The keyboardDidShow function finds the top of the keyboard input view where the picker will be located and resizes the scrollView. The offset differs depending on the current orientation. Once the scrollView frame size matches the remaining visible area, the active text field can be brought into view. The keyboardWillHide function does the same thing as keyboardWillShow; it just makes the scrollView bigger instead of smaller. It scrolls to the top of the parent scrollView too. Update Groceries as follows to add to the INTERACTION section: 1. Add the code from Listing 7.16 to the bottom of ItemVC.swift before the last curly brace. 2. Add self.activeField = textField to the top of the textFieldDidBeginEditing function of ItemVC.swift. 3. Add self.activeField = nil to the top of the textFieldDidEndEditing function of ItemVC.swift. The next step ensures the functions from Listing 7.16 are called when the keyboard is

shown or hidden. This is achieved by observing UIKeyboardDidShowNotification and UIKeyboardWillHideNotification. The code involved is shown in Listing 7.17. Listing 7.17 Observing Keyboard Changes (ItemVC.swift viewWillAppear) Click here to view code image // Register for keyboard notifications while the view is visible. NSNotificationCenter.defaultCenter().addObserver(self, selector: “keyboardDidShow:”, name: UIKeyboardDidShowNotification, object: self.view.window) NSNotificationCenter.defaultCenter().addObserver(self, selector: “keyboardWillHide”, name: UIKeyboardWillHideNotification, object: self.view.window)

Update Groceries as follows to observe and respond to keyboard notifications: 1. Add the code from Listing 7.17 to the top of the viewWillAppear function of ItemVC.swift on the line after super.viewWillAppear(animated). There’s no need to continue observing keyboard notifications when the Item view is not onscreen. The viewDidDisappear function is an ideal place to remove these observers. Listing 7.18 shows the code involved. Listing 7.18 Disabling Keyboard Observation (ItemVC.swift viewDidDisappear) Click here to view code image // Unregister for keyboard notifications while the view is not visible. NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardDidShowNotification, object: self.view.window) NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: self.view.window)

Update Groceries as follows to stop observing keyboard notifications: 1. Add the code from Listing 7.18 to the bottom of the viewDidDisappear function of ItemVC.swift. Run the application again and ensure that there are several shop locations in the persistent store. On the Item view, tap the Shop Location picker text field and change the shop location of an item. You should notice that the shop location picker text field comes into view automatically when using a small screen, such as an iPhone 4S.

Summary In this chapter, you learned how to display Core Data fetched results in a picker and then present that picker in an inputView triggered from a custom text field. The picker text fields for Groceries were fully implemented in the process, so configuring an item should now be fast. This becomes even more apparent once there are several units, home locations, and shop locations added to the persistent store.

Exercises Why not build on what you’ve learned by experimenting? 1. Create a multicomponent picker by adding self.sectionNameKeyPath = "name" to UnitPickerTF.swift on the line before self.performFetch(). 2. Run the application and select the unit picker text field to see the results. Figure 7.7 shows the expected results of the exercises. Reverse these steps before moving to the next chapter.

Figure 7.7 Multicomponent Picker

8. Preloading Data Never memorize something you can look up. Albert Einstein Chapter 7, “Picker Views,” focused on configuring user interface elements backed by Core Data objects. As a result, the main functionality of Groceries is now in place. This chapter dives back into the data model as preloading default data is explained and demonstrated. There are several approaches to providing default data to an application. In some cases, it makes sense to just import data directly in code, as you experienced in previous chapters. A more advanced technique is to generate a persistent store from an XML file. The resulting persistent store can then be shipped with the application and inserted as the initial persistent store before Core Data is set up for the first time.

Including Default Data When your Core Data application is released, you may want to ship it with some default data. In some cases, default data only serves as an example of how to use an application. In other cases, the application is useless without it. When no default data is included, it may not be immediately apparent how Groceries is supposed to be used. This is especially true when the user is confronted with empty picker views on the Item view. If default data is included with an application, it becomes easier to learn to use. The easier a program is to use, the more likely it is that people will continue to use it. The longer people use an application, the more chance that word about the application will spread, thus increasing sales potential. Before an application imports default data, it’s prudent to check that the import is required and, optionally, that the user wants the default data imported. The import source for default data can vary greatly from case to case. Whatever the source, it can help to dump the data raw into a spreadsheet, generate XML using the upcoming techniques, and then generate a persistent store to ship with the application. The key to remember is that you must let Core Data generate any persistent store that you want Core Data to use. Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter07.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. This chapter covers importing default data from sources other than code, so there’s no longer a need for demo() to be called from the applicationDidBecomeActive

function of AppDelegate.swift. Update Groceries as follows to stop default data from being imported with code: 1. Remove demo() from the applicationDidBecomeActive function of AppDelegate.swift.

Indicating Whether an Import Is Required To indicate whether an import is required, a value can be set in a persistent store’s metadata. Each time the application runs, this value can be checked to verify whether an import is required. This technique acts as a safety switch against importing duplicate default data. The first time the application is launched, this value won’t exist and the default data is free to import. The code required to support this functionality is added to a new class specific to importing data into Core Data, which exists to minimize the code in CDHelper.swift. The starting point for this class is shown in Listing 8.1. Listing 8.1 Core Data Importer (CDImporter.swift) Click here to view code image import Foundation import CoreData import UIKit private let _sharedCDImporter = CDImporter() class CDImporter : NSObject, NSXMLParserDelegate { // MARK: - SHARED INSTANCE class var shared : CDImporter { return _sharedCDImporter } }

The CDImporter.swift class starts out with a similar structure to CDHelper.swift. It has a shared instance that conveniently handles instantiation so you just need to call CDImporter.shared to use it. The NSXMLParserDelegate is used to receive the contents of the XML file in small chunks that can be turned into managed objects. Update Groceries as follows to stop default data from being imported with code: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the file to save as CDImporter. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in CDImporter.swift with the code from Listing 8.1. When Groceries is launched and the stores have been set up, it’s a good time to check

whether a default data import is required. Listing 8.2 shows the first function in a new DATA IMPORT section to be implemented in CDImporter.swift. Listing 8.2 Data Import (CDImporter.swift) Click here to view code image // MARK: - DATA IMPORT class func isDefaultDataAlreadyImportedForStoreWithURL (url:NSURL, type:String) -> Bool { do { var metadata:[String : AnyObject]? metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(NSSQLiteStoreType, URL: url, options: nil) if let dictionary = metadata { if let defaultDataAlreadyImported = dictionary[“DefaultDataImported”] as? NSNumber { if defaultDataAlreadyImported.boolValue == false { print(“Default Data has not been imported yet”) return false } else { print(“Default Data import is not required”) return true } } else { print(“Default Data has not been imported yet”) return false } } else {print(“\(__FUNCTION__) FAILED to get metadata”)} } catch { print(“ERROR getting metadata from \(url) \(error)”) } return true // default to true to prevent a default data import when an error occurs }

The isDefaultDataAlreadyImportedForStoreWithURL function has the job of returning true or false when asked whether default data has already been imported for the store at the given NSURL. It works this out by looking for an existing metadata value for the DefaultDataImported key. If this key doesn’t exist or exists with a false value, the default data import is assumed to be required. Note The key name DefaultDataImported is an arbitrary (random) name. The key name itself is not important. What is important is that it matches the key name set in the upcoming setDefaultDataAsImportedForStoreWithURL function, which is responsible for marking a store as imported. Update Groceries to add the DATA IMPORT section: 1. Add the code from Listing 8.2 to the bottom of CDImporter.swift before the

last curly brace. The next function required is checkIfDefaultDataNeedsImporting. This function calls isDefaultDataAlreadyImportedForStoreWithURL as it checks whether an import is required. If an import is required, an alert is shown to the user to double-check that she wants the import to occur. If an import is not required, nothing happens. Listing 8.3 shows the code involved. Listing 8.3 Import Check (CDImporter.swift) Click here to view code image func checkIfDefaultDataNeedsImporting (url:NSURL, type:String) { if CDImporter.isDefaultDataAlreadyImportedForStoreWithURL(url, type: type) == false { let alert = UIAlertController(title: “Import Default Data?”, message: “If you’ve never used this application before then some default data might help you understand how to use it. Tap ‘Import’ to import default data. Tap ‘Cancel’ to skip the import, especially if you’ve done this before on your other devices.”, preferredStyle: .Alert) let importButton = UIAlertAction(title: “Import”, style: .Destructive, handler: { (action) -> Void in // Reserved for import code }) let skipButton = UIAlertAction(title: “Skip”, style: .Default, handler: { (action) -> Void in // Do nothing }) alert.addAction(importButton) alert.addAction(skipButton) // PRESENT dispatch_async(dispatch_get_main_queue(), { () -> Void in if let initialVC = UIApplication.sharedApplication().keyWindow?.rootViewController { initialVC.presentViewController(alert, animated: true, completion: nil) } else {NSLog(“ERROR getting the initial view controller in %@”,__FUNCTION__)} }) } }

Update Groceries as follows to implement code to check whether an import is required: 1. Add the code from Listing 8.3 to the bottom of the DATA IMPORT section of CDImporter.swift before the last curly brace. 2. Add the following code to the bottom of the setupCoreData function of CDHelper.swift before the last curly brace. This triggers a test of the localStore to see whether it needs to import default data. Click here to view code image // Import Default Data

if let _localStoreURL = self.localStoreURL { CDImporter.shared.checkIfDefaultDataNeedsImporting(_localStoreURL, type: NSSQLiteStoreType) } else {print(“ERROR getting localStoreURL in \(__FUNCTION__)”)}

After you make those changes, run the application. The expected result is shown in Figure 8.1. For the moment, this prompt displays every time the application is launched because the data import code hasn’t been implemented yet. Once you close the application, delete it from the device in case there’s data in it from previous chapters.

Figure 8.1 Checking whether the user wants to load default data

Importing from XML Importing data from XML is a technique you can use to generate a persistent store containing default data. Once you have a “default data” persistent store, you can then ship it with your application bundle without the XML file. The advantage with this approach is that the default data will be ready to go instantly because no XML import process is required. Many good XML parsers are available that can be used to create the default data store. Although some of those parsers would give better performance, the NSXMLParser included in the iOS SDK is fit for this purpose. The process to create the default data store isn’t something the user will have to sit through, so performance isn’t an issue. NSXMLParser is a streaming event-driven parser. This means that once parse is called on an instance of NSXMLParser its delegate is informed of what was found in the XML file as it is found. Note XML isn’t your only choice for an import source. For example, you could import data using a JSON file with NSJSONSerialization or you could use a property list instead.

A new function called importFromXML will now be implemented that is responsible for configuring the CDImporter instance as an NSXMLParser delegate and then triggering the XML file parse. Once the parse is complete, a notification is sent to ensure that the table views are refreshed with the latest data. There would be no need for this notification if context were a parent of an import context. Parent context hierarchies are discussed in Chapter 11, “Background Processing.” The importFromXML code is shown in Listing 8.4. Listing 8.4 XML Parser (CDImporter.swift) Click here to view code image // MARK: - XML PARSER var parser:NSXMLParser? func importFromXML (url:NSURL) { self.parser = NSXMLParser(contentsOfURL: url) if let _parser = self.parser { _parser.delegate = self

NSLog(“START PARSE OF %@”,url) _parser.parse() NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChanged” object: nil) NSLog(“END PARSE OF %@”,url) } }

Update Groceries as follows to implement the XML Parser trigger code: 1. Add the code from Listing 8.4 to the bottom of CDImporter.swift before the last curly brace. Before anything can be imported, you need an XML file to import. The XML format that you need for Groceries is shown in Listing 8.5. Listing 8.5 Default Data XML Example Format Click here to view code image



Creating an XML file in this format is easy using a spreadsheet application such as Numbers or Excel. After pasting existing data into a spreadsheet, you can then insert the appropriate part of the XML string you’re trying to create between the columns, as shown in Figure 8.2. The advantage in using Numbers or Excel is that you can fill down the repeated XML tags. Alternatively, you can use any editor you’re comfortable with to produce the XML file, so long as the format is consistent.

Figure 8.2 Creating an XML file using Excel or Numbers The columns in Figure 8.2 are shaded to indicate what is data and what is part of the XML elements. Once imported, each row becomes a managed object based on the Item entity. When the spreadsheet is ready, it’s just a matter of saving it as a plain text file with an XML extension. After you remove any stray tab stops or spaces that may come from converting a spreadsheet to text, the XML is then ready to use. An XML file containing a big list of ~400 items has been premade for you so you can maintain focus on building Groceries. Update Groceries as follows to add the Default Data XML file: 1. Extract the premade DefaultData.xml from http://www.timroadley.com/LCDwS/DefaultData.xml.zip. 2. Drag DefaultData.xml into the Data Model group. Ensure Copy items if needed and the Groceries target are checked before clicking Finish. The expected result after you click the XML file is shown in Figure 8.3.

Figure 8.3 Default Data XML, ready for import

Creating an Import Context When importing data, you should use a managed object context that does not run on the main thread. It is recommended that you use a context existing solely for importing data. This ensures there’s no chance it will block the queue of another context—most importantly, the main queue context. An import context can use the same persistent store coordinator as another context. The import context is implemented in the same way as the foreground context; however, it will have a different concurrency type. You have three concurrency types to choose from when configuring an NSManagedObjectContext: MainQueueConcurrencyType should be used when you want the context to

operate on the main thread. Any heavy work performed in this queue may slow down or even freeze the user interface. You need at least one context working in the foreground to update user interface elements. PrivateQueueConcurrencyType should be used when you don’t want the context to operate on the main thread. This is an ideal concurrency type for potentially heavy work, such as saving or importing data. Any context with a PrivateQueueConcurrencyType should only be sent messages using performBlock or performBlockAndWait. Use performBlockAndWait if you need the block to return before continuing; otherwise, use performBlock. If you call performBlockAndWait on a context running on the main thread, it blocks the main thread. ConfinementConcurrencyType is a deprecated option, which you should avoid. This legacy option required you to have a separate context for every thread and a managed object context could only be used on the thread or queue that created it. The code to configure importContext to run on a private (background) queue is shown in Listing 8.6. Listing 8.6 Import Context (CDHelper.swift) Click here to view code image lazy var importContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.coordinator return moc }()

Update Groceries as follows to implement the import context: 1. Add the code from Listing 8.6 to the bottom of the CONTEXT section of CDHelper.swift beneath the existing context variable code.

Preventing Duplicate Default Data To prevent default data from being imported more than once, a DefaultDataImported metadata key set to true needs to be applied to the persistent store. To do this, the existing metadata dictionary is first taken from the store and the DefaultDataImported key added to it. This process occurs in the setDefaultDataAsImportedForStore function, which can be used for any persistent store and is shown in Listing 8.7. Listing 8.7 Set Default Data As Imported (CDImporter.swift) Click here to view code image func setDefaultDataAsImportedForStore (store:NSPersistentStore) { if let coordinator = store.persistentStoreCoordinator {

var metadata = store.metadata metadata[“DefaultDataImported”] = NSNumber(bool: true) coordinator.setMetadata(metadata, forPersistentStore: store) print(“Store metadata after setDefaultDataAsImportedForStore \ (store.metadata)”) } }

Update Groceries as follows to add support for setting default data as imported: 1. Add the code from Listing 8.7 to the bottom of the DATA IMPORT section of CDImporter.swift.

Triggering a Default Data Import When the import alert is displayed, the user can tap Skip to bypass the import or tap Import to proceed with loading default data. The code required to trigger an import with the Import button can now be added to the checkIfDefaultDataNeedsImporting function. Listing 8.8 shows the code involved. Listing 8.8 Import Check—Updated (CDImporter.swift) Click here to view code image func checkIfDefaultDataNeedsImporting (url:NSURL, type:String) { if CDImporter.isDefaultDataAlreadyImportedForStoreWithURL(url, type: type) == false { let alert = UIAlertController(title: “Import Default Data?”, message: “If you’ve never used this application before then some default data might help you understand how to use it. Tap ‘Import’ to import default data. Tap ‘Cancel’ to skip the import, especially if you’ve done this before on your other devices.”, preferredStyle: .Alert) let importButton = UIAlertAction(title: “Import”, style: .Destructive, handler: { (action) -> Void in // Import data if let url = NSBundle.mainBundle().URLForResource(“DefaultData”, withExtension: “xml”) { CDHelper.shared.importContext.performBlock { print(“Attempting DefaultData.xml Import…”) self.importFromXML(url) } } else {print(“DefaultData.xml not found”)} // Set the data as imported if let store = CDHelper.shared.localStore { self.setDefaultDataAsImportedForStore(store) } }) let skipButton = UIAlertAction(title: “Skip”, style: .Default, handler: { (action) -> Void in // Set the data as imported

if let store = CDHelper.shared.localStore { self.setDefaultDataAsImportedForStore(store) } }) alert.addAction(importButton) alert.addAction(skipButton) // PRESENT dispatch_async(dispatch_get_main_queue(), { () -> Void in if let initialVC = UIApplication.sharedApplication().keyWindow?.rootViewController { initialVC.presentViewController(alert, animated: true, completion: nil) } else {NSLog(“ERROR getting the initial view controller in %@”,__FUNCTION__)} }) } }

When the Import button is tapped, the previously implemented importFromXML function is called. Note that the import is performed using performBlock without AndWait. This means that the block of code gets back to the application whenever it is ready, without adversely impacting the user experience. Regardless of what is chosen by the user, the setDefaultDataAsImportedForStore function is called to prevent the alert from harassing the user every time the application launches. If the user selects the wrong option, there is currently no recourse to reverse the decision. For brevity, this issue will remain out of scope to maintain focus on the primary objectives of this chapter. Update Groceries as follows to implement code to trigger a default data import: 1. Replace the checkIfDefaultDataNeedsImporting function of CDImporter.swift with the code from Listing 8.8. Run the application and tap Import to begin the data import. The expected result is shown in Figure 8.4. The lines in the console log may have a different order for you because the parse isn’t executed on the main thread. Notice how after the function setDefaultDataAsImportedForStore is run that the store metadata includes a line that reads DefaultDataImported: 1.

Figure 8.4 DefaultDataImported metadata key

Although the parse is triggered as expected, no data is imported. This is because the NSXMLParser delegate functions have not yet been implemented. Before these functions are implemented some supporting functions are required to find or create unique managed objects.

Finding or Creating Managed Objects Before inserting a new object into the target context, a check needs to be performed to ensure the proposed object doesn’t already exist to prevent duplicates. Because the import is from XML, the only uniqueness indicator to match against is one or more of the target context entity attribute values. For Groceries, this is an easy selection because item name, unit name, location at home storedIn, and location at shop aisle fit the bill nicely. In other applications, email addresses or phone numbers might be more appropriate. In some cases, you may end up needing to add a uniqueness ID to your source and target data. Checking for an existing managed object with a specific set of attribute values centers around creating an appropriate predicate. Although it is overkill for Groceries, Listing 8.9 shows the code involved in building a compound predicate, which supports fetching managed objects with more than one unique value. In addition, it supports numerical and string attribute values. This additional functionality was included to provide you with greater support for fetching unique objects in your own applications. This code is placed in CDOperation.swift as it’s not strictly code related to importing data. Listing 8.9 Predicate for Attributes (CDOperation.swift) Click here to view code image class func predicateForAttributes (attributes:[NSObject:AnyObject], type:NSCompoundPredicateType ) -> NSPredicate? { // Create an array of predicates, which will be later combined into a compound predicate. var predicates:[NSPredicate]? // Iterate unique attributes in order to create a predicate for each for (attribute, value) in attributes { var predicate:NSPredicate? // If the value is a string, create the predicate based on a string value if let stringValue = value as? String { predicate = NSPredicate(format: “%K == %@”, attribute, stringValue) } // If the value is a number, create the predicate based on a numerical value if let numericalValue = value as? NSNumber { predicate = NSPredicate(format: “%K == %@”, attribute, numericalValue) } // Append new predicate to predicate array, or create it if it doesn’t exist yet if let newPredicate = predicate {

if var _predicates = predicates { _predicates.append(newPredicate) } else {predicates = [newPredicate]} } } // Combine all predicates into a compound predicate if let _predicates = predicates { return NSCompoundPredicate(type: type, subpredicates: _predicates) } return nil }

A compound predicate is a set of NSPredicate objects used together. When a compound predicate is created it needs to be of a certain type, either AND, OR, or NOT. The chosen type specifies how the predicates that make up the compound predicate should be used together. The predicateForAttributes function supports all compound predicate types. The following list explains how to create compound predicates: To create a compound AND predicate, pass .AndPredicateType as the type. This specifies that all the criteria of all subpredicates must be met. In SQL terms, this is similar to the WHERE clause in the statement SELECT * FROM MyTable WHERE FirstName = 'Tim' AND LastName = 'Roadley'. To create a compound OR predicate, pass .OrPredicateType as the type. This specifies that criteria of at least one of the subpredicates must be met. In SQL terms, this is similar to the WHERE clause in the statement SELECT * FROM MyTable WHERE FirstName = 'Tim' OR LastName = 'Roadley'. To create a compound NOT predicate, pass .NotPredicateType as the type. This specifies that none of the criteria of any subpredicates are allowed to be met. In SQL terms, this is similar to the WHERE clause in the statement SELECT * FROM MyTable WHERE FirstName NOT IN ('Tim') AND LastName NOT IN ('Roadley'). In Listing 8.9 the compound predicate is built from an array of predicates. The array of predicates is drawn from the given dictionary of attribute values. This dictionary is iterated and for each attribute found a predicate is created and put in the predicates array. Update Groceries as follows to add the predicateForAttributes function: 1. Add the code from Listing 8.9 to the bottom of CDOperation.swift before the last curly brace. Before inserting a managed object from XML, a check is needed against the target context to ensure that the proposed object does not already exist. To achieve this, a fetch is performed on the target context with a predicate specific to the unique attribute values. The code involved is shown in Listing 8.10. Listing 8.10 Unique Object with Attribute Values (CDOperation.swift) Click here to view code image

class func uniqueObjectWithAttributeValuesForEntity(entityName:String, context:NSManagedObjectContext, uniqueAttributes:[NSObject:AnyObject]) -> NSManagedObject? { let predicate = CDOperation.predicateForAttributes(uniqueAttributes, type: .AndPredicateType) let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicate, sort: nil) if objects?.count > 0 { if let object = objects?[0] as? NSManagedObject { return object } } return nil }

The uniqueObjectWithAttributeValuesForEntity function returns the first managed object it finds that matches the specified predicate. An existing objectsForEntity function of CDOperation.swift is leveraged to achieve this. If no matching objects are found, nothing is returned. Update Groceries as follows to implement the existingObjectInContext function: 1. Add the code from Listing 8.10 to the bottom of CDOperation.swift before the last curly brace. If the uniqueObjectWithAttributeValuesForEntity function returns nil, the object does not exist in the target context. When importing from XML, this result indicates that a new object with the given unique attribute values is required in the target context. To insert objects, a new function called insertUniqueObject is required, which is shown in Listing 8.11. Listing 8.11 Insert Unique Object (CDOperation.swift) Click here to view code image class func insertUniqueObject(entityName:String, context:NSManagedObjectContext, uniqueAttributes:[String:AnyObject], additionalAttributes:[String:AnyObject]?) -> NSManagedObject { // Return existing object after adding the additional attributes. if let existingObject = CDOperation.uniqueObjectWithAttributeValuesForEntity(entityName, context: context, uniqueAttributes: uniqueAttributes) { if let _additionalAttributes = additionalAttributes { existingObject.setValuesForKeysWithDictionary(_additionalAttributes) } return existingObject } // Create object with given attribute value let newObject = NSEntityDescription.insertNewObjectForEntityForName(entityName, inManagedObjectContext: context) newObject.setValuesForKeysWithDictionary(uniqueAttributes) if let _additionalAttributes = additionalAttributes { newObject.setValuesForKeysWithDictionary(_additionalAttributes)

} return newObject }

The insertUniqueObject function returns an NSManagedObject with its attributes populated from the dictionaries of attribute values given to the function. The insertNewObjectForEntityForName returns an AnyObject object, so the insertUniqueObject function needs to cast this to NSManagedObject. If this cast fails, an error is shown in the console log and a default NSManagedObject is returned. This is an extremely unlikely scenario; however, it must be included for the code to compile. Update Groceries as follows to implement the insertUniqueObject function: 1. Add the code from Listing 8.11 to the bottom of CDOperation.swift before the last curly brace.

Mapping XML Data to Entity Attributes The data import engine that CDImporter and CDOperation provide can now be leveraged by the parse results of an NSXMLParser. All that’s left to do is implement the appropriate delegate functions defined by the NSXMLParserDelegate protocol, which are as follows: The parseErrorOccurred function is used to log any errors that occur during the XML parse, usually from the NSXMLParserErrorDomain. If you receive errors, they probably are due to a formatting error or invalid character in the XML file. The didStartElement function is called every time the parser finds a new element in the given XML file. In the case of Groceries’ default data XML file this is the element. Every attribute and associated value found within this element is passed to the delegate function as a dictionary. This dictionary is perfect for creating managed objects. If you were to adapt this import technique to your own applications, the didStartElement function is where you need to customize according to your data and model. The code involved is shown in Listing 8.12. Listing 8.12 NSXML Parser Delegate (CDImporter.swift) Click here to view code image // MARK: - DELEGATE: NSXMLParser func parser(parser: NSXMLParser, parseErrorOccurred parseError: NSError) { NSLog(“ERROR PARSING: %@”,parseError.localizedDescription) } // NOTE: - The code in the didStartElement function is customized for ‘Groceries’ func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) { let importContext = CDHelper.shared.importContext

importContext.performBlockAndWait { // Process only the ‘Item’ element in the XML file if elementName == “Item” { // STEP 1a: Insert a unique ‘Item’ object var item:Item? if let itemName = attributeDict[“name”] { item = CDOperation.insertUniqueObject(“Item”, context: importContext, uniqueAttributes: [“name”:itemName], additionalAttributes: nil) as? Item if let _item = item {_item.name = itemName} } // STEP 1b: Insert a unique ‘Unit’ object var unit:Unit? if let unitName = attributeDict[“unit”] { unit = CDOperation.insertUniqueObject(“Unit”, context: importContext, uniqueAttributes: [“name”:unitName], additionalAttributes: nil) as? Unit if let _unit = unit {_unit.name = unitName} } // STEP 1c: Insert a unique ‘LocationAtHome’ object var locationAtHome:LocationAtHome? if let storedIn = attributeDict[“locationAtHome”] { locationAtHome = CDOperation.insertUniqueObject(“LocationAtHome”, context: importContext, uniqueAttributes: [“storedIn”:storedIn], additionalAttributes:nil) as? LocationAtHome if let _locationAtHome = locationAtHome {_locationAtHome.storedIn = storedIn} } // STEP 1d: Insert a unique ‘LocationAtShop’ object var locationAtShop:LocationAtShop? if let aisle = attributeDict[“locationAtShop”] { locationAtShop = CDOperation.insertUniqueObject(“LocationAtShop”, context: importContext, uniqueAttributes: [“aisle”:aisle], additionalAttributes: nil) as? LocationAtShop if let _locationAtShop = locationAtShop {_locationAtShop.aisle = aisle} } // STEP 2: Manually add extra attribute values. if let _item = item {_item.listed = NSNumber(bool: false)} // STEP 3: Create relationships if let _item = item { _item.unit = unit _item.locationAtHome = locationAtHome _item.locationAtShop = locationAtShop } // STEP 4: Save new objects to the persistent store. CDHelper.save(importContext) // STEP 5: Turn objects into faults to save memory if let _item = item { importContext.refreshObject(_item, mergeChanges: false)} if let _unit = unit { importContext.refreshObject(_unit,

mergeChanges: false)} if let _locationAtHome = locationAtHome { importContext.refreshObject(_locationAtHome, mergeChanges: false)} if let _locationAtShop = locationAtShop { importContext.refreshObject(_locationAtShop, mergeChanges: false)} } } }

The didStartElement delegate function is called every time the parser finds a new element in the XML file. If the XML element name is equal to “Item,” the import routine begins. First the managed objects are inserted using the insertUniqueObject function. Once inserted, additional attributes and relationships can be set as per steps 2 and 3 in Listing 8.12. Finally, the context is saved and objects turned into faults to save memory. This whole process is run within performBlockAndWait on the import context so that each item is completely processed before moving on to the next. The main thread is not blocked and the application remains usable during the import only because importContext runs on a private queue. Update Groceries as follows to implement the DELEGATE: NSXMLParser section: 1. Add the code from Listing 8.12 to the bottom of CDImporter.swift before the last curly brace. 2. Delete Groceries from the iOS Simulator so the persistent store does not have the DefaultDataImported key set to 1. 3. Comment out the SAVED context and SKIPPED saving context messages in the save class function of CDHelper.swift to prevent the console log filling with repeated messages. 4. Click Product > Clean and run Groceries on the iOS Simulator. Tap Import to begin the import. As the default data is imported, you can still use the application. The expected result is shown in Figure 8.5.

Figure 8.5 Preloaded default data

Importing from a Persistent Store If you want to ship an application with default data from a persistent store, you have a couple of options. Which option you choose depends on whether customer devices already have existing data for your application. Option 1: Use the default data persistent store as the initial persistent store. This is achieved by copying the default data store onto the device before Core Data is set up for the first time. This is by far the easiest and most efficient option. This approach cannot be used if you have already shipped your application without default data. If that’s the case, users will have already generated a persistent store with their own data, which you likely don’t want to overwrite. This option is added to Groceries in this chapter. Option 2: Deep copy unique data from the default data persistent store to an existing persistent store. This is achieved by copying the attribute values and relationships of each entity. The copy is referred to as “deep” because the relationships are all evaluated and objects are created as necessary. This find-orcreate technique is a processor-intensive task best performed in the background. This option is a complicated topic and is discussed in Chapter 9, “Deep Copy.” You should avoid this option when feasible. One alternative that is faster yet potentially creates duplicate objects is the migratePersistentStore function of NSPersistentStore.

Using the Default Data Store as the Initial Store A persistent store full of default data has been created by Core Data as a part of the XML import. Core Data created the database so the format is correct. To set a default store as the initial store, the database file needs to exist in the application bundle. When preparing a default database to ship with your application, you need to take into account the database journaling mode. Since iOS 7, a default journaling mode called Write-Ahead Logging (WAL) has been set for SQLite databases. This default increases performance and provides better concurrency support. As a side effect, there are now three files per database by default: The sqlite file is the database file, as per usual. The sqlite-wal file is the Write-Ahead Log file containing uncommitted database transactions. If you delete this file, you lose data. If this file does not exist, there are no pending transactions waiting to be committed. The sqlite-shm file is the Shared Memory file containing an index of the WAL file. This file can be regenerated automatically so you don’t need to worry about it. Figure 8.6 shows an example of Groceries SQLite WAL and SHM files.

Figure 8.6 SQLite database files in WAL journaling mode The journal_mode was intentionally set to DELETE in Chapter 2, “Managed Object Model Basics,” to ensure that -wal and -shm files aren’t present. This allows you to take a copy of the newly generated persistent store file without having to worry about extra files. Extract the Groceries persistent store as follows: 1. Run Groceries and copy the path to the Documents folder from the console log as per Figure 2.10 in Chapter 2. 2. Right-click Finder and then select Go to Folder…. 3. Paste the path to the Documents folder in the Go to the Folder dialog box. 4. You should see a LocalStore.sqlite file, as shown in Figure 8.7. The application identifier will vary. If you have a lot of applications installed in the simulator and need help locating the appropriate identifier, search the console log for

LocalStore.sqlite and examine its containing path.

Figure 8.7 SQLite database file in DELETE journaling mode 5. Ensure Groceries isn’t running. 6. Copy LocalStore.sqlite to the desktop and rename it to DefaultData.sqlite. Figure 8.7 shows an example of where the persistent store is located in the iOS Simulator application sandbox. Note If you are unable to find LocalStore.sqlite, you may download a copy from http://www.timroadley.com/LCDwS/DefaultData.sqlite.zip. Now that you have a DefaultData.sqlite store, you can include it in the application bundle and reenable WAL mode. Update Groceries as follows: 1. Reenable WAL journaling by commenting out the NSSQLitePragmasOption option in the localStore function of CDHelper.swift. 2. Drag DefaultData.sqlite from your desktop into the Data Model group in Xcode. Ensure Copy items if needed and the Groceries target are checked before clicking Finish. Now that the DefaultData.sqlite file exists in the application bundle, a new function called setDefaultDataStoreAsInitialStore can be implemented. This function is called first by setupCoreData, so the default store can be put in place before it is required, as long as it doesn’t already exist. There are a couple of ways to move the default store into place. You could use the NSPersistentStoreCoordinator function migratePersistentStore, which, it should be noted, can transparently handle store type conversions. The other option is the more basic NSFileManager function copyItemAtURL, which Groceries will use. The code involved is shown in Listing 8.13.

Listing 8.13 Set Default Store as Initial Store (CDHelper.swift) Click here to view code image // MARK: - DEFAULT STORE func setDefaultDataStoreAsInitialStore () { if let url = self.localStoreURL, path = url.path { if NSFileManager.defaultManager().fileExistsAtPath(path) == false { if let defaultDataURL = NSBundle.mainBundle().URLForResource(“DefaultData”, withExtension: “sqlite”) { do { try NSFileManager.defaultManager().copyItemAtURL(defaultDataURL, toURL: url) print(“A copy of DefaultData.sqlite was set as the initial store for \(url)”) } catch { print(“\(__FUNCTION__) ERROR setting DefaultData.sqlite as the initial store: : \(error)”) } } else {print(“\(__FUNCTION__) ERROR: Could not find DefaultData.sqlite inthe application bundle.”)} } } else {print(“\(__FUNCTION__) ERROR: Failed to prepare URL in \ (__FUNCTION__)”)} }

The setDefaultDataStoreAsInitialStore function first checks whether there is already a persistent store at the target location. If there isn’t, it proceeds to copy the default store into place from the main bundle using copyItemAtURL. Update Groceries as follows to implement setDefaultDataStoreAsInitialStore: 1. Add the code from Listing 8.13 to the bottom of CDHelper.swift before the last curly brace. 2. Add self.setDefaultDataStoreAsInitialStore() to the setupCoreData function of CDHelper.swift on the line before _ = self.localStore. 3. Delete Groceries from your device or simulator. 4. Click Product > Clean to clear any residual cache. 5. Run the application, which should open prepopulated with default data without prompting you. The expected result is shown in Figure 8.8.

Figure 8.8 Preloaded default data

Summary This chapter showed how to configure multiple managed object contexts to allow a data import to occur in the background. Tips for preparing an XML file from raw data were given as the basic functionality of CDImporter was implemented. This new class has enabled a model-agnostic creation of unique managed objects in a target context, based on an XML file. Don’t forget that the size of any imported XML file should be kept to a minimum because the entire file needs to be stored in memory while the import occurs. This isn’t too much of a problem when you’re running an import on the iOS Simulator to create a default store. Where possible, it is recommended that you ship a prepopulated persistent store with your applications so it may be used as the initial store. If a persistent store already exists on customer devices, you may have to deep copy or migrate data from a preloaded persistent store. This topic is covered in the next chapter.

Exercises Why not build on what you’ve learned by experimenting? 1. Add a quantity attribute with a random number to some of the items in the DefaultData.xml file. 2. Reactivate XML import by commenting out self.setDefaultDataStoreAsInitialStore() in the setupCoreData function of CDHelper.swift. 3. Replace the STEP 2 section of the parser:didStartElement function of CDImporter.swift with the code from Listing 8.14. Delete and rerun the application and then trigger an import. The quantities added to DefaultData.xml should have applied to the appropriate objects imported.

4. Temporarily disable WAL journaling mode by enabling the DELETE journaling mode and then rerun the application. Stop the application and edit the name of an item in the persistent store using the SQLite Database Browser. Editing an item is achieved by double-clicking it in the Browse Data mode and then clicking the Save button. Run the application again and see that the change appears in the iOS Simulator. Be careful not to have the SQLite file open by both the browser and simulator at the same time. Listing 8.14 Did Start Element (CDImporter.swift) Click here to view code image // STEP 2: Manually add extra attribute values. if let _item = item { _item.listed = NSNumber(bool: false) let f = NSNumberFormatter() if let quantityString = attributeDict[“quantity”] { if let quantity = f.numberFromString(quantityString) { _item.quantity = quantity } } }

Reverse any changes made during the exercises before moving to the next chapter.

9. Deep Copy The only sure way to avoid making mistakes is to have no new ideas. Albert Einstein In Chapter 8, “Preloading Data,” importing default data from an XML file was demonstrated. This import method is suitable only when the XML source file is small enough to fit into memory. Another option is to use a prepopulated persistent store as the initial persistent store. If customers already have existing data and you want to add a lot more to it, this option isn’t suitable either. When you find yourself in this position, you may need to perform a “deep copy” of managed objects from a source persistent store to an existing persistent store. This option provides de-duplication and more granularity than the migratePersistentStore function of NSPersistentStore, although it isn’t as fast.

The Deep Copy Process A deep copy involves copying managed objects and their relationships from one persistent store to another. Once an object has been copied, the relationships from its source object are evaluated to find related source objects. Those related source objects are then copied to the target store as required. Relationships in the source store are then replicated in the target store between the copied objects. As this cycle continues for each object, every relationship in all directions is eventually copied into the target persistent store. Needless to say, this is an intensive task and should only be run in the background. Depending on an application’s data model, it may be more efficient to copy all objects in one pass and then reestablish the relationships later. In fact, this should be the preference. Unfortunately for Groceries, this approach cannot be used. This is because the items shown in the Prepare and Shop tabs have a reliance on a related object. If items are imported without a relationship to a home or shop location, the tables won’t be divided into sections properly during the import process. This wouldn’t look right to the user, who may get the impression that there is a bug in the application. Deep copy works only when the source and target stores have the same managed object model. However, a separate coordinator and context are needed for the source and target stores. The contexts used for the source and target stores should also be separate from the main queue context. Figure 9.1 shows a high-level overview of the Core Data components that deep copy requires.

Figure 9.1 Deep copy The existing importContext is reused as the target context for this demonstration of deep copy. Copying an object to another context isn’t as straightforward as a copy-andpaste command. To copy an object, you actually need to create a new object in the target context and then copy all the attribute values from the source object to the new object. That leaves the relationships, which can’t be copied in the same way. If you were to copy a relationship the same way you copied an attribute value, you would end up with an illegal cross-store relationship between the copied object and object(s) in the source store. Instead of copying a relationship, a deep copy needs to identify related copied objects in the target context and then establish a relationship to them from the copied object. Figure 9.2 illustrates a To-One relationship copy.

Figure 9.2 Copying a To-One relationship Before a relationship can be copied, checks are needed to ensure that objects required as a part of a copied relationship already exist in the target context. Objects that are missing in the target context are created on demand based on their equivalent source object. To-Many relationships present an interesting challenge and are a big part of the reason that a deep copy is a resource intensive task. A deep copy needs to iteratively check every object and all of its relationships, so this process can take a long time. The other factor to account for is ordered and unordered relationships. An ordered relationship is, under the

covers, an NSMutableOrderedSet of related objects. An unordered relationship is an NSMutableSet of related objects, so the deep copy code needs to reflect this. Figure 9.3 illustrates this more complicated relationship copy.

Figure 9.3 Copying a To-Many relationship To deep copy objects, several functions from the previous chapter are used. The deep copy process relies on unique attributes being chosen for each entity up front. This allows a uniqueness check to take place prior to an object being copied, which prevents duplicated data. The following shows the high-level process of a deep copy: An array of entity names to deep copy is given to CDImporter, which copies all objects for each entity as required. You only need to specify the entities you want copied. Related objects of entities earmarked for copy are copied regardless, however. If equivalent objects from the source context don’t already exist in the target context, new managed objects are inserted. New objects are given the attribute values of the source object. If a source object has a relationship, it is walked to find the related object(s). Related object(s) are copied to the target context as required. Once all objects involved in a relationship exist in the target context, the relationship itself is reformed from the copied object to the related copied object(s) as previously illustrated in Figure 9.2 and Figure 9.3.

Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter08.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. The deep copy process is demonstrated as another approach to importing default data, as opposed to an approach to adding data to an existing persistent store. At present, the Groceries application checks to see whether a persistent store exists at launch. If it doesn’t, a default data persistent store is configured as the initial store. This behavior needs to be disabled so that deep copy may be demonstrated. Update Groceries as follows to prevent the default store from replacing the initial store: 1. Comment out self.setDefaultDataStoreAsInitialStore() in the setupCoreData function of CDHelper.swift. 2. Delete Groceries from your device or the iOS Simulator, whichever you’re using. 3. Click Product > Clean.

Configuring a Source Stack Core Data stack is a term referring to the combination of persistent store, persistent store coordinator, managed object model, and managed object context. To perform a deep copy from a source store, you need a separate Core Data stack from the one that already exists. This has the effect of providing a source and target context, which is where the copies will be performed. The only commonality between the two stacks is that they use the same managed object model.

Configuring the Source Coordinator The code required for the source coordinator is shown in Listing 9.1 and is similar to the existing coordinator variable. Listing 9.1 Source Coordinator (CDHelper.swift) Click here to view code image lazy var sourceCoordinator:NSPersistentStoreCoordinator = { return NSPersistentStoreCoordinator(managedObjectModel:self.model) }()

Update Groceries as follows to add a source coordinator: 1. Add the code from Listing 9.1 to the COORDINATOR section of CDHelper.swift beneath the existing declaration of coordinator.

Configuring the Source Context The code required for the source context is shown in Listing 9.2 and is similar to the existing importContext variable. Listing 9.2 Source Context (CDHelper.swift) Click here to view code image lazy var sourceContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.sourceCoordinator return moc }()

Update Groceries as follows to introduce a source context: 1. Add the code from Listing 9.2 to the CONTEXT section of CDHelper.swift beneath the existing declaration of importContext.

Configuring the Source Store The same approach used to configure the existing store is used again to configure the source store. This means a variable that returns a URL to the source store is first required. Listing 9.3 shows the new code involved, which specifies the source store filename. The existing DefaultData.sqlite store is used for this demonstration of deep copy. Listing 9.3 Source Store URL (CDHelper.swift) Click here to view code image lazy var sourceStoreURL: NSURL? = { if let url = NSBundle.mainBundle().URLForResource(“DefaultData”, withExtension: “sqlite”) { print(“sourceStoreURL = \(url)”) return url } return nil }()

Update Groceries as follows to configure the sourceStoreURL variable: 1. Add the code from Listing 9.3 to the PATHS section of CDHelper.swift beneath the existing declaration of localStoreURL. The next step is to implement the sourceStore variable. This variable is responsible for adding the source store to the source coordinator. Because the source store lives in the application bundle, it must be loaded as read-only. This means that if the model is upgraded in the future, you need to ship a pre-upgraded version of DefaultData.sqlite with it. Listing 9.4 shows the code involved. Listing 9.4 Source Store (CDHelper.swift) Click here to view code image

lazy var sourceStore: NSPersistentStore? = { let options:[NSObject:AnyObject] = [NSReadOnlyPersistentStoreOption:1] var _sourceStore:NSPersistentStore? do { _sourceStore = try self.sourceCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: self.sourceStoreURL, options: options) return _sourceStore } catch { return nil } }()

Update Groceries as follows to complete the configuration of the Core Data stack initialization code: 1. Add the code from Listing 9.4 to the STORE section of CDHelper.swift beneath the existing declaration of localStore. 2. Add _ = self.sourceStore on the line before _ = self.localStore in the setupCoreData function of CDHelper.swift. If you ran the application now it would still import default data from XML. This is because the call to import from XML needs to be replaced with a call to import from a persistent store. Before that change can be made, the CDImporter class needs updating to support deep copy from a persistent store.

Enhancing CDImporter To enable deep copy, the CDImporter class is enhanced to allow the complicated procedure of copying a managed object. The complexity comes from relationship copies, as each relationship must be evaluated to find related objects. The three relationship types (To-One, To-Many, and Ordered To-Many) must also be supported. As complicated as this process can be, by breaking it down into understandable chunks, it should become easier to understand. This breakdown is the reason many functions are required to perform a deep copy.

Identifying Unique Attributes To prevent the creation of duplicate objects, you need to predefine a list of attributes that should be considered unique for an entity. This is achieved using the function shown in Listing 9.5. When you apply this approach to your own applications, you need to carefully consider what about each object makes it unique. Examples of unique identifiers are email addresses, product codes, object IDs, and so on. Listing 9.5 Selecting Unique Attributes (CDImporter.swift) Click here to view code image // MARK: - DEEP COPY class func selectedUniqueAttributesForEntity (entityName:String) -> [String]? {

// Return an array of attribute names to be considered unique for an entity. // Multiple unique attributes are supported. // Only use attributes whose values are alphanumeric. switch (entityName) { case “Item” :return [“name”] case “Unit” :return [“name”] case “LocationAtHome”:return [“storedIn”] case “LocationAtShop”:return [“aisle”] default: break; } return nil }

The selectedUniqueAttributesForEntity function optionally returns an array of strings. Each string represents an attribute considered unique for the given entity. If multiple unique attributes are specified for an entity, both must be matched for an object to be considered unique. Update Groceries to implement the selectedUniqueAttributesForEntity function: 1. Add the code from Listing 9.5 to the bottom of CDImporter.swift before the last curly brace.

Object Info A new objectInfo function is used to cut down repetitive code otherwise required in most of the upcoming deep copy functions. By passing a managed object to this function, you get back a string containing the object’s entity name, unique attribute, and unique attribute value information. Listing 9.6 shows the code involved. Listing 9.6 Managed Object Info (CDImporter.swift) Click here to view code image class func objectInfo (object:NSManagedObject) -> String { if let entityName = object.entity.name { var attributes:NSString = ”” if let uniqueAttributes = CDImporter.selectedUniqueAttributesForEntity(entityName) { for attribute in uniqueAttributes { if let valueForKey = object.valueForKey(attribute) as? NSObject { attributes = “\(attributes)\(attribute) \(valueForKey) “ } } // trim trailing space attributes = attributes.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())

return “\(entityName) with \(attributes)” } else {print(“ERROR: \(__FUNCTION__) could not find any uniqueAttributes”)} } else {print(“ERROR: \(__FUNCTION__) could not find an entityName”)} return ”” }

Update Groceries to implement the objectInfo function: 1. Add the code from Listing 9.6 to the bottom of CDImporter.swift before the last curly brace.

Copying a Unique Object The next function required is copyUniqueObject. This function is responsible for ensuring a unique copy of an object exists in the target context. Technically, this function does not copy a managed object. Instead, it creates a new object in the target context and then copies the attribute values from the source object to the new object. As discussed in the previous chapter, the insertUniqueObject function is used to ensure only unique objects are inserted. If the object already exists, this function just returns the existing object. Note that relationships are not copied in this function because they need to be copied in another way. Listing 9.7 shows the code involved. Listing 9.7 Copy Unique Object (CDImporter.swift) Click here to view code image class func copyUniqueObject (sourceObject:NSManagedObject, targetContext:NSManagedObjectContext) -> NSManagedObject? { if let entityName = sourceObject.entity.name { if let uniqueAttributes = CDImporter.selectedUniqueAttributesForEntity(entityName) { // PREPARE unique attributes to copy var uniqueAttributesFromSource:[String:AnyObject] = [:] for uniqueAttribute in uniqueAttributes { uniqueAttributesFromSource[uniqueAttribute] = sourceObject.valueForKey(uniqueAttribute) } // PREPARE additional attributes to copy var additionalAttributesFromSource:[String:AnyObject] = [:] if let attributesByName:[String:AnyObject] = sourceObject.entity.attributesByName { for (additionalAttribute, _) in attributesByName { additionalAttributesFromSource[additionalAttribute] = sourceObject.valueForKey(additionalAttribute) } } // COPY attributes to new object let copiedObject = CDOperation.insertUniqueObject(entityName, context: targetContext, uniqueAttributes: uniqueAttributesFromSource, additionalAttributes: additionalAttributesFromSource)

return copiedObject } else {print(“ERROR: \(__FUNCTION__) could not find any selected unique attributes for the ‘\(entityName)’ entity”)} } else {print(“ERROR: \(__FUNCTION__) could not find an entity name for the given object ‘\(sourceObject)’”)} return nil }

Update Groceries as follows to implement the copyUniqueObject function: 1. Add the code from Listing 9.7 to the bottom of CDImporter.swift before the last curly brace.

Establishing a To-One Relationship The next function required is establishToOneRelationship. This function is responsible for establishing a To-One relationship by name, from one object to another. If the relationship already exists, the relationship creation is skipped. Listing 9.8 shows the code involved. Listing 9.8 Establish a To-One Relationship (CDImporter.swift) Click here to view code image class func establishToOneRelationship (relationshipName:String,from object:NSManagedObject, to relatedObject:NSManagedObject) { // SKIP establishing an existing relationship if object.valueForKey(relationshipName) != nil { print(“SKIPPED \(__FUNCTION__) because the relationship already exists”) return } if let targetContext = object.managedObjectContext { // ESTABLISH the relationship object.setValue(relatedObject, forKey: relationshipName) print(“ A copy of \(CDImporter.objectInfo(object)) is related via To-One \(relationshipName) relationship to \ (CDImporter.objectInfo(relatedObject))”) // REMOVE the relationship from memory after it is committed to disk CDHelper.save(targetContext) targetContext.refreshObject(object, mergeChanges: false) targetContext.refreshObject(relatedObject, mergeChanges: false) } else {print(“ERROR: \(__FUNCTION__) could not get a targetContext”)} }

Establishing a To-One relationship requires a single line of code. It is established by setting the value of the relationship’s key-value pair on an object. The relationship name is the key, and the related object is the value. You only need to set a relationship in one direction; the inverse is implied by the managed object model. The final part of the establishToOneRelationship function is the important cleanup task that removes references to the specified objects from each context. By calling refreshObject for each object after a context save, the managed objects are faulted.

This removes the objects from memory, thus breaking strong reference cycles that would otherwise keep unneeded objects around wasting resources. Without this step, importing from a persistent store would be no better than importing from XML, as all the source data would be loaded in memory. Although it can be resource intensive to call save so frequently, it keeps the memory overhead low. In addition, the process occurs in the background, so it won’t impact the user interface negatively. It will, however, refresh the visible table view cells onscreen. The general overview of this concept was shown previously in Figure 9.2. Update Groceries as follows to implement the establishToOneRelationship function: 1. Add the code from Listing 9.8 to the bottom of CDImporter.swift before the last curly brace.

Establishing a To-Many Relationship The next function required is establishToManyRelationship, which is responsible for establishing a To-Many relationship from an object. It is expected that the object passed to this function is from the deep copy target context. The given NSMutableSet should contain objects from the source context. The function creates missing objects required as a part of the new relationship in the target context. A To-Many relationship is established by adding an object to another object’s NSMutableSet that represents a particular relationship. The NSMutableSet is accessed through the object’s key-value pair. The relationship name is the key, and the NSMutableSet is the value. An NSMutableSet can only contain unique objects, so there is no chance of accidentally duplicating a relationship from the same object. The general overview of this concept was shown previously in Figure 9.3 and is demonstrated in code in Listing 9.9. Listing 9.9 Establish a To-Many Relationship (CDImporter.swift) Click here to view code image class func establishToManyRelationship (relationshipName:String,from object:NSManagedObject, sourceSet:NSMutableSet) { // SKIP establishing an existing relationship if object.valueForKey(relationshipName) != nil { print(“SKIPPED \(__FUNCTION__) because the relationship already exists”) return } if let targetContext = object.managedObjectContext { let targetSet = object.mutableSetValueForKey(relationshipName) targetSet.enumerateObjectsUsingBlock({ (relatedObject, stop) -> Void in if let theRelatedObject = relatedObject as? NSManagedObject {

if let copiedRelatedObject = CDImporter.copyUniqueObject(theRelatedObject, targetContext: targetContext) { targetSet.addObject(copiedRelatedObject) print(“ A copy of \(CDImporter.objectInfo(object)) is related via To-Many \(relationshipName) relationship to \ (CDImporter.objectInfo(copiedRelatedObject))”) // REMOVE the relationship from memory after it is committed to disk CDHelper.save(targetContext) targetContext.refreshObject(object, mergeChanges: false) targetContext.refreshObject(theRelatedObject, mergeChanges: false) } else {print(“ERROR: \(__FUNCTION__) could not get a copiedRelatedObject”)} } else {print(“ERROR: \(__FUNCTION__) could not get theRelatedObject”)} }) } else {print(“ERROR: \(__FUNCTION__) could not get a targetContext”)} }

Update Groceries as follows to implement the establishToManyRelationship function: 1. Add the code from Listing 9.9 to the bottom of CDImporter.swift before the last curly brace.

Establishing an Ordered To-Many Relationship The next function required is establishOrderedToManyRelationship, which is responsible for establishing an Ordered To-Many relationship from an object. It is expected that the object passed to this function is from the deep copy target context. The given NSMutableOrderedSet should contain objects from the source context. The function creates missing objects required as a part of the new relationship in the target context. An Ordered To-Many relationship is established by adding one object to another object’s NSMutableOrderedSet that represents a particular relationship. The NSMutableOrderedSet is accessed through the object’s key-value pair. The relationship name is the key, and the NSMutableOrderedSet is the value. An NSMutableOrderedSet can only contain unique objects, so there is no chance of accidentally duplicating a relationship from the same object. The order of the set in the target context needs to match the order of the set from the source context. The order of the source set is maintained as the equivalent objects are added to the target object’s ordered set in the order they are found. The general overview of this concept was shown previously in Figure 9.3 and is demonstrated in code in Listing 9.10. Listing 9.10 Establish an Ordered To-Many Relationship (CDImporter.swift) Click here to view code image class func establishOrderedToManyRelationship (relationshipName:String,from object:NSManagedObject, sourceSet:NSMutableOrderedSet) {

// SKIP establishing an existing relationship if object.valueForKey(relationshipName) != nil { print(“SKIPPED \(__FUNCTION__) because the relationship already exists”) return } if let targetContext = object.managedObjectContext { let targetSet = object.mutableOrderedSetValueForKey(relationshipName) targetSet.enumerateObjectsUsingBlock { (relatedObject, index, stop) > Void in if let theRelatedObject = relatedObject as? NSManagedObject { if let copiedRelatedObject = CDImporter.copyUniqueObject(theRelatedObject, targetContext: targetContext) { targetSet.addObject(copiedRelatedObject) print(“ A copy of \(CDImporter.objectInfo(object)) is related via Ordered To-Many \(relationshipName) relationship to \ (CDImporter.objectInfo(copiedRelatedObject))’”) // REMOVE the relationship from memory after it is committed to disk CDHelper.save(targetContext) targetContext.refreshObject(object, mergeChanges: false) targetContext.refreshObject(theRelatedObject, mergeChanges: false) } else {print(“ERROR: \(__FUNCTION__) could not get a copiedRelatedObject”)} } else {print(“ERROR: \(__FUNCTION__) could not get theRelatedObject”)} } } else {print(“ERROR: \(__FUNCTION__) could not get a targetContext”)} }

Update Groceries as follows to implement the establishOrderedToManyRelationship function: 1. Add the code from Listing 9.10 to the bottom of CDImporter.swift before the last curly brace. There are no ordered relationships in Groceries; however, this function is included in case you want to use CDImporter in your own projects.

Copying Relationships The next function required is copyRelationshipsFromObject, which is responsible for copying all relationships from an object in the source context to an equivalent object in the target context. This function is what the other functions implemented so far have been building up to. The first task this function performs is to ensure there is an equivalent object in the target context. Referred to as the copiedObject, this object is created as required using the previously implemented copyUniqueObject function. If it still doesn’t exist after a

copy is attempted, this function returns prematurely. To copy relationships, the function works out what relationships exist on the source object using sourceObject.entity.relationshipsByName. This dictionary is then iterated to find valid relationships. Provided the relationship exists, the equivalent relationship is recreated from the copiedObject. Before copying a relationship, its type is first determined. For To-Many or Ordered To-Many relationships, the appropriate source set is passed to the appropriate “copy To-Many” function. For a To-One relationship, the object to be related is copied to the target context before the appropriate function is called to establish the relationship. Listing 9.11 shows the code involved. Listing 9.11 Copy Relationships from Object (CDImporter.swift) Click here to view code image class func copyRelationshipsFromObject(sourceObject:NSManagedObject, to targetContext:NSManagedObjectContext) { if let copiedObject = CDImporter.copyUniqueObject(sourceObject, targetContext: targetContext) { let relationships = sourceObject.entity.relationshipsByName // [String : NSRelationshipDescription] for (_, relationship) in relationships { if relationship.toMany && relationship.ordered { // COPY To-Many Ordered Relationship let sourceSet = sourceObject.mutableOrderedSetValueForKey(relationship.name) CDImporter.establishOrderedToManyRelationship(relationship.name, from: copiedObject, sourceSet: sourceSet) } else if relationship.toMany && relationship.ordered == false { // COPY To-Many Relationship let sourceSet = sourceObject.mutableSetValueForKey(relationship.name) CDImporter.establishToManyRelationship(relationship.name, from: copiedObject, sourceSet: sourceSet) } else { // COPY To-One Relationship if let relatedSourceObject = sourceObject.valueForKey(relationship.name) as? NSManagedObject { if let relatedCopiedObject = CDImporter.copyUniqueObject(relatedSourceObject, targetContext: targetContext) { CDImporter.establishToOneRelationship(relationship.name, from:copiedObject, to: relatedCopiedObject) } else {print(“ERROR: \(__FUNCTION__) could not get a relatedCopiedObject”)} } else {print(“ERROR: \(__FUNCTION__) could not get a relatedSourceObject”)}

} } } else {print(“ERROR: \(__FUNCTION__) could not find or create an object to copy relationships to.”)} }

Update Groceries as follows to implement the copyRelationshipsFromObject function: 1. Add the code from Listing 9.11 to the bottom of CDImporter.swift before the last curly brace.

Deep Copy Entities The next function required is deepCopyEntities, which is responsible for copying all objects from the specified entities in one context to another context. There are several ways this function could be implemented, and the user experience would differ with each option. If you search the Internet for “core data programming guide: efficiently importing data,” you should find an Apple guide that discusses techniques for importing data. It says that when possible it is more efficient to copy all the objects in a single pass and then fix up relationships later. Depending on the application, this may not be feasible. An import can take a long time, and if the relationships are missing even for a few seconds, the user might assume the application has a bug. The options open to you to combat this issue are as follows (your selection varies depending on the nature of your application): Prevent the user from using the application, partially or wholly. During the import, you could display a progress indicator. If the import takes a long time, this may annoy the user. Depending on the application, you may instead only disable partial functionality, until the data is ready. Import all objects first and then establish relationships. The user might see halfimported data with little or no established relationships. Depending on the data model, this may or may not be acceptable. Consider prioritizing the order the entities are imported to make this option more palatable. Import objects and relationships together. Although this is certainly not as efficient as the other options, the entire deep copy process is run in the background so the user impact is minimal to nonexistent. This is a more resource intensive task than the alternative; however, the application remains usable. CDImporter is configured to import objects and relationships together and a notification is sent each time an entity finishes processing. This ensures that table views relying on relationships for their sections are updated appropriately. Listing 9.12 shows the code involved. Listing 9.12 Deep Copy Entities (CDImporter.swift) Click here to view code image class func deepCopyEntities(entities:[String], from sourceContext:NSManagedObjectContext, to targetContext:NSManagedObjectContext) {

for entityName in entities { print(“DEEP COPYING ‘\(entityName)’ objects to target context…”) if let sourceObjects = CDOperation.objectsForEntity(entityName, context: sourceContext, filter: nil, sort: nil) as? [NSManagedObject] {

for sourceObject in sourceObjects { print(“DEEP COPYING OBJECT: \ (CDImporter.objectInfo(sourceObject))”) CDImporter.copyUniqueObject(sourceObject, targetContext: targetContext) CDImporter.copyRelationshipsFromObject(sourceObject, to: targetContext) } } else {print(“ERROR: \(__FUNCTION__) could not find any sourceObjects”)} NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChanged” object: nil) } }

Update Groceries as follows to implement the deepCopyEntities function: 1. Add the code from Listing 9.12 to the bottom of CDImporter.swift before the last curly brace. The required code is now in place to support a deep copy, and the only thing left to do is to trigger it.

Triggering a Deep Copy As mentioned at the beginning of this chapter, a deep copy is demonstrated using the existing default data store. The default data store DefaultData.sqlite was previously configured as the initial store during setupCoreData via setDefaultDataStoreAsInitialStore. This function call has since been commented out, so on new installations an import from XML would be triggered instead. This is due to the call to checkIfDefaultDataNeedsImporting in the setupCoreData function that triggers an alert giving the option to import with importFromXML. To trigger a deep copy from a persistent store instead, a new function called triggerDeepCopy is required in CDImporter.swift. Listing 9.13 shows the code involved. Listing 9.13 Triggering a Deep Copy (CDImporter.swift) Click here to view code image class func triggerDeepCopy (sourceContext:NSManagedObjectContext, targetContext:NSManagedObjectContext, mainContext:NSManagedObjectContext) { sourceContext.performBlock { CDImporter.deepCopyEntities([“Item”,“Unit”,“LocationAtHome”, “LocationAtShop”], from: sourceContext, to: targetContext)

mainContext.performBlock { // Trigger interface refresh NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChan

object: nil) } print(“*** FINISHED DEEP COPY FROM DEFAULT DATA PERSISTENT STORE ***”) } }

The triggerDeepCopy function calls deepCopyEntities using performBlock. This has the effect of performing the deep copy in the background because sourceContext has a private queue concurrency type. When the copy is finished a final interface refresh is triggered via a notification. Update Groceries as follows to implement the triggerDeepCopy function: 1. Add the code from Listing 9.13 to the bottom of CDImporter.swift before the last curly brace. Finally, the triggerDeepCopy function needs to be called instead of importFromXML when the user taps the Import button. This means the checkIfDefaultDataNeedsImporting function needs updating, as shown in bold in Listing 9.14. Listing 9.14 Triggering DefaultData.sqlite Import (CDImporter.swift) Click here to view code image func checkIfDefaultDataNeedsImporting (url:NSURL, type:String) { if CDImporter.isDefaultDataAlreadyImportedForStoreWithURL(url, type: type) == false { let alert = UIAlertController(title: “Import Default Data?”, message: “If you’ve never used this application before then some default data might help you understand how to use it. Tap ‘Import’ to import default data. Tap ‘Cancel’ to skip the import, especially if you’ve done this before on your other devices.”, preferredStyle: .Alert) let importButton = UIAlertAction(title: “Import”, style: .Destructive, handler: { (action) -> Void in // Import data //if let url = NSBundle.mainBundle().URLForResource(“DefaultData”, withExtension: “xml”) { CDHelper.shared.importContext.performBlock { //print(“Attempting DefaultData.xml Import…”) //self.importFromXML(url) print(“Attempting DefaultData.sqlite Import…”) CDImporter.triggerDeepCopy(CDHelper.shared.sourceContext, targetContext: CDHelper.shared.importContext, mainContext: CDHelper.shared.context) } //} else {print(“DefaultData.xml not found”)} // Set the data as imported if let store = CDHelper.shared.localStore { self.setDefaultDataAsImportedForStore(store) } })

let skipButton = UIAlertAction(title: “Skip”, style: .Default, handler: { (action) -> Void in // Set the data as imported if let store = CDHelper.shared.localStore { self.setDefaultDataAsImportedForStore(store) } }) alert.addAction(importButton) alert.addAction(skipButton) // PRESENT dispatch_async(dispatch_get_main_queue(), { () -> Void in if let initialVC = UIApplication.sharedApplication().keyWindow?.rootViewController { initialVC.presentViewController(alert, animated: true, completion: nil) } else {NSLog(“ERROR getting the initial view controller in %@”,__FUNCTION__)} }) } }

Update Groceries as follows to configure the checkIfDefaultDataNeedsImporting function to trigger deep copy: 1. Replace the existing checkIfDefaultDataNeedsImporting function found in CDImporter.swift with the function from Listing 9.14. 2. Delete Groceries from your device or the iOS Simulator and click Product > Clean. 3. Run the application and import default data. As the import occurs, notice that you can still use the application. Figure 9.4 shows the expected result after the import has completed.

Figure 9.4 Data imported from a persistent store via deep copy The interface should be updated with imported data seconds after it is imported without noticeable impact to the user experience.

Summary You’ve now experienced the complicated topic of deep copy. If you do find yourself in a position where you need to populate unique data into an existing persistent store, this option is now open to you. The CDImporter classes are model agnostic, so you may use them freely in your own applications. Remember that to prevent unnecessary imports, you need to set a metadata key-value on the existing persistent store. This technique was demonstrated in the previous chapter, so there was no need to repeat it here. When deciding on how you’ll migrate data from one store to another, ensure you evaluate the migratePersistentStore function of NSPersistentStoreCoordinator. This function allows you to migrate the entire contents of a persistent store into another persistent store. Unfortunately, there are no deduplication options, nor is there the capability to be selective about what entities you migrate. That said, migratePersistentStore is much faster than deep copy.

Exercises Why not build on what you’ve learned by experimenting? 1. Delete Groceries from the device and then quickly press the home button (Shift+ +H) during another import. The import should continue when the application is in the background. 2. Test the migratePersistentStore function of NSPersistentStoreCoordinator by adding the code shown in Listing 9.15 to the bottom of the setupCoreData function of CDHelper.swift. Run the application again, and you should notice that duplicate data is inserted almost instantly. Comment out the code added in Exercise 2 and then uncomment self.setDefaultDataStoreAsInitialStore() in the setupCoreData function of CDHelper.swift before continuing on to the next chapter. Listing 9.15 Setup Core Data (CDHelper.swift) Click here to view code image // The code below demonstrates how to migrate one persistent store to another. sourceContext.performBlock { if let _localStoreURL = self.localStoreURL, let _sourceStore = self.sourceStore { print(“Attempting to migrate the source store to the local store with migratePersistentStore…”) do { try CDHelper.shared.sourceCoordinator.migratePersistentStore(_sourceStore, toURL: _localStoreURL, options: nil, withType: NSSQLiteStoreType) print(“The source store has been successfully migrated to the

local store with migratePersistentStore!”) self.context.performBlock { // Trigger interface refresh NSNotificationCenter.defaultCenter().postNotificationName(“Something object: nil) } } catch { print(“\(__FUNCTION__) FAILED to migrate persistent store - \ (error)”) } } }

10. Performance Insanity is doing the same thing, over and over again, but expecting different results. Albert Einstein In Chapter 9, “Deep Copy,” techniques were demonstrated that populate a persistent store with data. As persistent stores grow, it’s important to ensure that the application remains responsive. The fetched results controllers in Groceries have already been configured for improved performance using batched fetch requests. What may not be apparent, however, is that the managed object model design plays a key role in producing better performance. This chapter takes you through the process of identifying and eliminating performance issues.

Identifying Performance Issues As an application nears the end of its development cycle, it’s important to iron out performance issues. Without suitable performance testing, it may not be apparent that an application has a performance issue until it’s too late. The worst-case scenario is that customers use an application for some time and the application slows as their data grows. To prevent this, it’s recommended that you test on the slowest possible device with a data set larger than you would expect any customer to have. For iOS 9, this means testing on an iPhone 4S. Depending on the nature of an application, performance issues reveal themselves in different ways. That said, there are some common things to look for in all iOS applications that indicate good performance: The application should load quickly. Table views should scroll smoothly. Views should transition quickly. The user interface should remain responsive at all times. With a large test data set, performance issues should become more obvious. The more obvious performance issues are, the easier it is to track down the root cause. In addition to having a large data set, using large objects such as photos is a common cause of application performance issues. In this chapter, the camera functionality is added to Groceries, which opens the door to large objects making their way into the application. Later in this chapter, you learn tips for how the model can be optimized for large objects such as photos.

Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter09.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. Before you begin, also remove any existing copies of Groceries from your device to ensure default data is loaded.

Implementing the Camera When you’re preparing a shopping list, it would be great if you could take a photo of that obscure brand of coffee you love so much. When you’re at the store and can’t remember what it is called, you could then simply refer to the photo. The ability to take a photo is added to the existing item editing view. When an item has a photo, it is shown both on the item editing view and in the table views of the Prepare and Shop tabs. To take a photo, a new button is required on the Item view, and an Image view is needed to display it. Update Groceries as follows to implement the camera button and Image view: 1. Download and extract the “add photo” icons from the following URL: http://www.timroadley.com/LCDwS/Icons_add_photo.zip. 2. Select the Assets.xcassets asset catalog and then drag the new add_photo icons into it. 3. Select Main.storyboard. 4. Drag an Image View into the existing Scroll View of the Item edit view. 5. Ensure Attributes Inspector is visible (Option+ +4). 6. Set the Background color of the Image View to Other > Pencils > Mercury (the second lightest gray pencil). 7. Drag a Button onto the existing Scroll View of the Item View and configure it as follows using Attributes Inspector (Option+ +4): Set Type to Custom. Delete the “Button” title text. Set Image to add_photo. 8. Ensure both the Height and Width of the new button are 48 using Size Inspector (Option+ +5). 9. Reposition and resize the Image View to have an equal width to the Location at Shop text field, as shown in Figure 10.1.

Figure 10.1 Add_photo button and Image view 10. Hold down Control while dragging a horizontal line over the add_photo button to add a Width constraint of 48. 11. Hold down Control while dragging a vertical line over the add_photo button to add a Height constraint of 48. 12. Hold down Control while dragging a line between the add_photo button and the add_shoplocations button to add a Trailing and Vertical Spacing constraint. 13. Hold down Control while dragging a line between the Image View and the Location at Shop text field to add a Leading, Trailing, and Vertical Spacing constraint. 14. Hold down Control while dragging a vertical line over the Image View to add a Height constraint. Set this constraint constant to 300. The outlets to the new user interface elements must now be connected so they can be referenced in code. The Assistant Editor is used to achieve this. Update Groceries as follows to connect the camera button and Image view to ItemVC.swift: 1. Ensure Main.storyboard is selected. 2. Ensure the ItemVC view controller is selected. 3. Show the Assistant Editor (Option+ +Return).

4. Set the Assistant Editor to Automatic > ItemVC.swift if it isn’t set to this already, as shown at the top of Figure 10.2.

Figure 10.2 Connected camera variables 5. Hold down Control and drag a line from the new Image View to the top of ItemVC.swift beneath the existing shopLocationPickerTextField variable. Set the name of the new variable to photoImageView. Double-check that Type is UIImageView and that Storage is Strong; then click Connect. 6. Hold down Control and drag a line from the camera button to the top of ItemVC.swift beneath the existing photoImageView variable. Set the name of the new variable to cameraButton. Double-check that Type is UIButton and that Storage is Strong; then click Connect. Once it’s connected, if you mouse over the circle to the left of the cameraButton variable, you should see the camera button highlighted. 7. Show the Standard Editor ( +Return).

Implementing the Image Picker Controller Delegates So that photos can be taken, the next step is to configure the appropriate protocol and delegate. The ItemVC class adopts the UIImagePickerControllerDelegate protocol so it receives messages containing the captured photo data. In addition, the ItemVC class adopts the UINavigationControllerDelegate protocol so it can present the camera interface. Finally, a new variable is required to hold the UIImagePickerController instance, known as the camera variable. Update Groceries as follows to configure the ItemVC header for the camera: 1. Add two protocol adoptions (shown in bold) to the top of ItemVC.swift as follows: Click here to view code image class ItemVC: GenericVC, UITextFieldDelegate, CDPickerTextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate

2. Add the following variable to the top of ItemVC.swift beneath the existing cameraButton variable: Click here to view code image var camera:UIImagePickerController?

To implement the camera, which in reality is a UIImagePickerController instance, four new functions are required: The checkCamera function ensures the camera is available and toggles the camera button appropriately. This function is called each time the Item view appears. The showCamera function is connected to the cameraButton. When this button is pressed, the camera interface appears. The imagePickerController:didFinishPickingImage function is an image picker delegate function. This delegate function is called when a photo has been taken. In this function, the photo data is saved into the photoData attribute value for the currently selected item. The photo is saved at reduced size and quality to save storage capacity. The imagePickerControllerDidCancel function is another image picker delegate function. It is called when Camera view is cancelled. The code in this function dismisses the Camera view. Listing 10.1 shows the code involved. Listing 10.1 Camera (ItemVC.swift) Click here to view code image

// MARK: - CAMERA func checkCamera () { self.cameraButton.enabled = UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Came } @IBAction func showCamera () {

if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Came == true { self.camera = UIImagePickerController() if let _camera = self.camera {

_camera.sourceType = UIImagePickerControllerSourceType.Camera _camera.mediaTypes = UIImagePickerController.availableMediaTypesForSourceType(UIImagePickerControllerSour _camera.allowsEditing = true _camera.delegate = self if let nc = self.navigationController { nc.presentViewController(_camera, animated: true, completion: nil) } else {print(“Failed to prepare the camera (navigation controller issue)”)} } else {print(“Failed to prepare the camera”)} } else {print(“Camera is not available”)}

} func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { if let item = self.selectedObject as? Item { if let photoData = UIImageJPEGRepresentation(image, 0.7) { print(“Captured a \(image.size.height) x \(image.size.width) photo”) item.photoData = photoData self.photoImageView.image = UIImage(data: photoData) } else {print(“\(__FUNCTION__) failed to capture and convert an image”)} } else {self.done()} picker.dismissViewControllerAnimated(true, completion: nil) } func imagePickerControllerDidCancel(picker: UIImagePickerController) { picker.dismissViewControllerAnimated(true, completion: nil) }

Update Groceries to implement the camera: 1. Add the code from Listing 10.1 to the bottom of ItemVC.swift before the last curly brace. 2. Add the following code to the refreshInterface function of ItemVC.swift. It should go at the bottom within the if let item statement: Click here to view code image if let photoData = item.photoData {self.photoImageView.image = UIImage(data: photoData)} self.checkCamera()

3. Select Main.storyboard. 4. Hold down Control and drag a line from the camera button to the yellow circle at the top of the Item edit view; then select Sent Events > showCamera. If you cannot see the Sent Events > showCamera option, you may need to save ItemVC.swift and try again. 5. Test the new camera functionality on a physical device. You should be able to take photos of items. Note that the camera does not work on the iOS Simulator. Now that photos can be taken, the table views on the Prepare and Shop tabs are updated to display photos. Update Groceries as follows to prepare the table view cells to display photos: 1. Add the following code to the configureCell functions of PrepareTVC.swift and ShopTVC.swift on the line after if let item: Click here to view code image // Display item photo if let imageView = cell.imageView { if let photoData = item.photoData { imageView.image = UIImage(data: photoData) } else { imageView.image = nil }

}

Run the application again. Photos you’ve taken should now appear in the table views. Figure 10.3 shows the expected results.

Figure 10.3 The “Prepare” table view photo support

Generating Test Data Creating a large test data set is easy with some for loops and a little patience. If you need more data, just keep looping through object creation until you have enough for your needs. Techniques to generate test data were touched on early in the book and are used again in this chapter, this time with the inclusion of a large image. A copy of the image intentionally is used separately for each test item. This simulates the effect of having a unique photo for each test item. The test image is 2000×2000, which is big enough to emulate a sizable photo taken from the camera. Listing 10.2 shows the code involved in generating test data. Listing 10.2 Test Data Import (CDHelper.swift) Click here to view code image func demo () { if CDOperation.objectCountForEntity(“Item”, context: CDHelper.shared.context) == 0 { let moc = CDHelper.shared.importContext

moc.performBlock { if let locationAtHome = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtHome”, inManagedObjectContext: moc) as? LocationAtHome , let locationAtShop = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtShop”, inManagedObjectContext: moc) as? LocationAtShop , let unit = NSEntityDescription.insertNewObjectForEntityForName(“Unit”, inManagedObjectContext: moc) as? Unit { locationAtHome.storedIn = “Test Home Location” locationAtShop.aisle = “Test Shop Location” unit.name = ”” for i in 1…100 { if let item = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: moc) as? Item { item.locationAtHome = locationAtHome item.locationAtShop = locationAtShop item.unit = unit item.name = “Test Item \(i)” if let photo = UIImage(named: “BigGroceries.png”), photoData = UIImagePNGRepresentation(photo) { item.photoData = photoData }

// Save to disk, free up memory and trigger interface refresh CDHelper.save(moc) moc.refreshObject(item, mergeChanges: false) } CDHelper.shared.context.performBlock { NSNotificationCenter.defaultCenter().postNotificationName(“S object: nil) } } } } } }

Update Groceries as follows to add test data import functionality: 1. Replace the code in the demo function of AppDelegate.swift with the code from Listing 10.2. 2. Add demo() to the bottom of the applicationDidBecomeActive function of AppDelegate.swift. 3. Download, extract, and add the following large image to Assets.xcassets: http://www.timroadley.com/LCDwS/BigGroceries.png.zip. The data import is now handled by the demo function, so the existing default data import code needs to be commented out as shown in Listing 10.3.

Listing 10.3 Setup Core Data (CDHelper.swift) Click here to view code image func setupCoreData() { /*// Model Migration if let _localStoreURL = self.localStoreURL { CDMigration.shared.migrateStoreIfNecessary(_localStoreURL, destinationModel: self.model) } */ // Load Local Store // self.setDefaultDataStoreAsInitialStore() _ = self.sourceStore _ = self.localStore // Import Default Data /* if let _localStoreURL = self.localStoreURL { CDImporter.shared.checkIfDefaultDataNeedsImporting(_localStoreURL, type: NSSQLiteStoreType) } else {print(“ERROR getting localStoreURL in \(__FUNCTION__)”)}*/ }

Update Groceries as follows to disable default data import: 1. Update the setupCoreData function in CDHelper.swift to match the code from Listing 10.3.

Merge Policies If the user modifies items during the import process, there is the potential that the same object could be modified in two contexts at the same time. When one of those contexts is then saved, a merge conflict could result. To handle merge conflicts, a merge policy should be configured in advance. The merge policy decides who wins when these conflicts arise. There are five options: NSErrorMergePolicy is the default policy. Merge conflicts prevent the context from being saved. NSMergeByPropertyObjectTrumpMergePolicy resolves merge conflicts using object variable values from the context to overwrite those in the persistent store. NSMergeByPropertyStoreTrumpMergePolicy resolves merge conflicts using object variable values from the persistent store to overwrite those in the context. NSOverwriteMergePolicy resolves merge conflicts using entire objects from the context to overwrite those in the persistent store. NSRollbackMergePolicy resolves merge conflicts using entire objects from the persistent store to overwrite those in the context. The merge policies boil down to a decision on whether the context or persistent store should win, at either an object or variable level. For Groceries,

NSMergeByPropertyObjectTrumpMergePolicy is used so that objects the user modifies overwrite those in the persistent store. The persistent store could have been updated, for example, by an import process or iCloud change. Update Groceries as follows to enable a merge policy: 1. Replace the CONTEXT section of CDHelper.swift with the one shown in Listing 10.4. The updated code introduces merge policies to each context and is shown in bold. Listing 10.4 Context Merge Policies (CDHelper.swift) Click here to view code image // MARK: - CONTEXT lazy var context: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.MainQueueConcurrencyType) moc.persistentStoreCoordinator = self.coordinator moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc }() lazy var importContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.coordinator moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc }() lazy var sourceContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.sourceCoordinator moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc }()

Delete Groceries from the iOS Simulator, click Product > Clean, and then run Groceries to install it again. After a slow import, the expected result is shown in Figure 10.4.

Figure 10.4 Test data Once the test data has been imported, try scrolling through the items. You should notice scrolling isn’t smooth unless you have a very fast Mac. If you were to run Groceries on a device, you would receive memory warnings and most likely experience a crash.

Measuring Performance with SQLDebug Assuming the underlying persistent store is SQLite, one technique for troubleshooting Core Data performance is to use SQLDebug. You can use SQLDebug to gain visibility of how long the automatically generated SQL queries are taking to execute. Debug level 1 is enough to show query execution time. As discussed in Chapter 2, “Managed Object Model Basics,” SQLDebug is enabled by editing the scheme of the application. Update Groceries as follows to enable SQL Debug level 1: 1. Click Product > Scheme > Edit Scheme…. 2. Ensure Run (next to the play symbol on the left) and the Arguments tabs are selected, as shown in Chapter 2 Figure 2.8. 3. Add a new argument by clicking + in the Arguments Passed On Launch section. 4. Enter -com.apple.CoreData.SQLDebug 1 as a new argument and then click Close. 5. Run Groceries on the iOS Simulator again and examine the console log. The expected result is shown in Figure 10.5.

Figure 10.5 Measuring query execution time Figure 10.5 shows the automatically generated SQL query that fetches items for the prepare table view. Although 100 test items exist in the persistent store, only 50 rows have been retrieved due to the self.fetchBatchSize = 50 configuration in the init function of PrepareTVC.swift. Even if there were thousands of items in the persistent store, this code would still protect the application from loading them all into memory at once. A good rule of thumb when choosing a batch fetch size is to think about how many objects you need displayed at the same time. A table view using the default row height on a 5.5inch iPhone 6 Plus Retina display shows around 15 rows at once. If the user flicks through the table quickly, this could end up being many more rows required in quick succession. A batch size of 25 is set for standard table views. The picker views are left with their existing fetch batch size of 10. Update Groceries as follows to alter the fetch request batch sizes: 1. Search the Xcode project for self.fetchBatchSize = 50. 2. Replace self.fetchBatchSize = 50 with self.fetchBatchSize = 25 for every class in the project. 3. Run Groceries on the iOS Simulator and examine the console log again. The expected result is shown in Figure 10.6.

Figure 10.6 Improved query execution time The query execution time is faster because fewer rows are fetched. Although setting an appropriate batch size is a good starting point, the scrolling still isn’t smooth, so more investigation is required.

Measuring Performance with Instruments Ideally, to measure an application’s performance, you would profile it with Instruments. Unfortunately, the option to profile Core Data is not available for physical iOS devices. This means you must profile Core Data on the iOS Simulator instead. Although this is not ideal, it still provides good insight into key Core Data–specific metrics such as fetches, cache misses, and saves. You can, however, still profile system resources such as memory allocations and CPU on a physical device. To profile an application using Instruments, you simply run the application in a different way. Instead of just pressing the Run button, press and hold the Run button to select Profile instead. Try this now and notice how the button changes as Instruments loads. The expected result is shown in Figure 10.7.

Figure 10.7 Profiling an application Once Instruments loads, you are prompted to select a profiling template, as shown in Figure 10.8. Core Data is in the Standard category.

Figure 10.8 Profiling an application using the Core Data template Select the Core Data template and click Choose. It is recommended that Time Profiler and Allocations be used in conjunction with the Core Data template, so they will be added now. Configure Instruments as follows to add Time Profiler and Allocations:

1. Press +L to make the Library visible if it isn’t already. 2. Drag Time Profiler and Allocations from the Library onto the main Instruments window above the Core Data instruments. 3. Optionally, save this template by clicking File > Save As Template… and then fill in the appropriate information. This ensures you don’t have to repeat this setup each time you begin profiling. 4. Press Record to begin profiling and test the scrolling speed of the Prepare table view using the iOS Simulator. Figure 10.9 shows the expected result.

Figure 10.9 Profiling Groceries As the application launches, you should see a flurry of CPU Usage activity in Time Profiler. You should also see the Allocations increase as the table view scrolls, with Core Data fetches lining up with these increases. If you click either Time Profiler or Allocations and use the Call Tree options, shown in Figure 10.10, you reveal the functions responsible for the heaviest resource utilization.

Figure 10.10 Groceries resource utilization Clearly the configureCell and cellForRowAtIndexPath functions of PrepareTVC are responsible for the majority of the memory footprint. This is no surprise because the table view cell was configured to display a test image, which happens to be 2000×2000. To see how long the fetches took, select Fetches: Fetch duration and examine the Event List, as shown in Figure 10.11.

Figure 10.11 Measuring fetch duration Fetch durations are displayed in microseconds, and depending on the type of Mac you’re using, your fetch duration may vary from Figure 10.11. If you need to know what class and function made these calls, click Event List and change to Call Tree. Of course, you want to set the appropriate options to hide system libraries and invert the call tree. The same technique applies for viewing Core Data saves and Core Data cache misses. A cache miss is a trip to the persistent store, required because an object was not already in memory.

Improving Performance A reduced fetched results size helps improve performance. Beyond setting self.fetchBatchSize, there are other NSFetchRequest options you can use to minimize the result set: Use fetchLimit and fetchOffset to reduce fetch results to a specific number of rows starting from a specific point. This may, for example, be appropriate for an application using a Page View Controller to display information. Each page could have a different fetch offset and possibly the same fetch limit. Use predicate to filter the result set according to the supplied predicate. If you’re supplying a compound predicate (that is, two or more things to filter by), ensure the predicate that will cut out the most results is put first. Performance gains can also be achieved if a predicate filters against numerical values before textual values. For example, someNumber > 0 && someText LIKE ‘whatever’ is more efficient than someText LIKE ‘whatever’ && someNumber > 0. Use propertiesToGroupBy and resultType together to emulate the GROUP BY operator otherwise available with raw SQL. When used in conjunction with havingPredicate, you can restrict results even further. As an example, you could use this option to produce a count of items in a particular home location. Use includesPropertyValues set to false to force the fetch request to exclude variable values in cases where you’re only interested in fetching the objectID for each object. Use relationshipKeyPathsForPrefetching to indicate what relationships should be prefetched. Prefetching a relationship brings related objects into the context to avoid the inefficiency of fault firing. Use this option when you know you need to access those related objects soon. None of the additional fetch request options are appropriate for Groceries, and yet the performance is still bad. Regardless, support for these extra fetch request options is added to CDTableViewController and CDPickerTextField, as they may be required in your own applications. Listing 10.5 shows the updated variables and fetched results controller code required in bold. Listing 10.5 Fetched Results Controller (CDTableViewController.swift) Click here to view code image // Optionally Override var context = CDHelper.shared.context var filter:NSPredicate? = nil var cacheName:String? = nil var sectionNameKeyPath:String? = nil var fetchBatchSize = 0 // 0 = No Limit var cellIdentifier = “Cell” var fetchLimit = 0 var fetchOffset = 0 var resultType:NSFetchRequestResultType = NSFetchRequestResultType.ManagedObjectResultType

var propertiesToGroupBy:[AnyObject]? = nil var havingPredicate:NSPredicate? = nil var includesPropertyValues = true var relationshipKeyPathsForPrefetching:[AnyObject]? = nil // MARK: - FETCHED RESULTS CONTROLLER lazy var frc: NSFetchedResultsController = { let request = NSFetchRequest(entityName:self.entity) request.sortDescriptors = self.sort request.fetchBatchSize = self.fetchBatchSize if let _filter = self.filter {request.predicate = _filter} request.fetchLimit = self.fetchLimit request.fetchOffset = self.fetchOffset request.resultType = self.resultType request.includesPropertyValues = self.includesPropertyValues if let _propertiesToGroupBy = self.propertiesToGroupBy {request.propertiesToGroupBy = _propertiesToGroupBy} if let _havingPredicate = self.havingPredicate {request.havingPredicate = _havingPredicate} if let _relationshipKeyPathsForPrefetching = self.relationshipKeyPathsForPrefetching as? [String] { request.relationshipKeyPathsForPrefetching = _relationshipKeyPathsForPrefetching} let newFRC = NSFetchedResultsController( fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: self.sectionNameKeyPath, cacheName: self.cacheName) newFRC.delegate = self return newFRC }()

Update Groceries as follows to enhance CDTableViewController and CDPickerTextField: 1. Update the variables and fetched results controller code in CDTableViewController.swift with the bold code from Listing 10.5 2. Update the variables and fetched results controller code in CDPickerTextField.swift with the bold code from Listing 10.5

Model Optimization Showing such a large image on a table view is the main contributor to the performance issues, given that configureCell is reported to be the busiest function. Perhaps using a prescaled thumbnail image would be a better idea. Although thumbnail generation won’t be added until the next chapter, the groundwork can be laid now. Update Groceries as follows to implement thumbnails: 1. Select Model.xcdatamodeld. 2. Click Editor > Add Model Version…. 3. Click Finish to accept the default model name of Model 7.

4. Select Model.xcdatamodeld and set the Current Model Version to Model 7 using File Inspector (Option+ +1). 5. Select Model 7.xcdatamodel. 6. Add a Binary Data attribute called thumbnail to the Item entity. 7. Add @NSManaged var thumbnail: NSData? to Item+CoreDataProperties.swift. 8. Update the configureCell function in PrepareTVC.swift and ShopTVC.swift to set the row image to display a thumbnail instead of the full photoData, replacing the existing “Display item photo” code as follows: Click here to view code image // Display item photo if let imageView = cell.imageView { if let thumbnail = item.thumbnail { imageView.image = UIImage(data: thumbnail) } else { imageView.image = nil } }

Profile Groceries as you scroll through the prepare table view again. No images are displayed because there’s no data in the thumbnail attribute yet. Despite the lack of images, the memory footprint is still large. Figure 10.12 shows the expected result.

Figure 10.12 A large memory footprint The key point here is that when an object is fetched from the persistent store, all its attributes are fetched into memory, including the large image. To work around this model constraint, any attribute expected to hold a large value should be moved to another entity. A relationship can then be created to that entity so the data is still accessible via the original entity, as required. This faulting technique ensures that the large object is only loaded into memory when it is needed. Once you’re finished with objects, remember to turn them back into a fault by calling refreshObject with the mergeChanges = false option. If changes have been made to the object, you should save it before turning it into a fault.

Handling Large Objects In addition to faulting, it is recommended that large objects be allowed to reside outside the persistent store. An Allows External Storage option is available for Binary Data attributes underpinned by an SQLite persistent store. This option ensures large objects (as determined heuristically by Core Data) are automatically stored outside the SQLite database. This approach is a more efficient way to store large objects. Figure 10.13 shows an example of large objects stored externally to the sqlite file within the application sandbox. Assuming an application with this setting has been run on a physical device already, the sandbox is viewable in Xcode by clicking Window > Devices and then selecting Groceries from the Installed Apps section of the appropriate connected device. You need to click the Cog icon and select Show Container. If you download the container from the device and show its package contents, you won’t be able to see the external data unless you can see hidden files on your Mac.

Figure 10.13 Storing objects externally To apply the improved model design principles to Groceries, the photoData attribute is migrated to a new Item_Photo entity and renamed to data. The Item_Photo entity is accessible via a new To-One relationship called photo. The inverse To-One relationship is called item. What was once accessed via photoData can now be accessed via photo.data. Update Groceries as follows to migrate photoData: 1. Ensure Model.xcdatamodeld is selected. 2. Click Editor > Add Model Version…. 3. Click Finish to accept the default model name of Model 8. 4. Select Model.xcdatamodeld and set the Current Model Version to Model 8 using File Inspector (Option+ +1). 5. Select Model 8.xcdatamodel. 6. Delete the photoData attribute from the Item entity. 7. Add a new entity and rename it to Item_Photo.

8. Select the new Item_Photo entity. 9. Create a new attribute called data in the Item_Photo entity and set its type to Binary Data. 10. Configure the data attribute to enable the Allows External Storage option using Data Model Inspector (Option+ +3). 11. Create a To-One relationship from the Item entity to the Item_Photo entity called photo. Also, set the inverse relationship name to item. It’s easiest to change the editor style to Graph to create the relationship. To create a relationship, just hold down Control and drag a line from one entity to another. 12. Set the Delete Rule of the photo relationship to Cascade using Data Model Inspector (Option+ +3). This ensures that when an item is deleted, its photo data is deleted too. 13. Update the selectedUniqueAttributesForEntity function of CDImporter.swift to cater to the new Item_Photo entity by adding the following case after the Item case: Click here to view code image case “Item_Photo” :return [“data”]

The resulting Model 8 data model is shown in Figure 10.14.

Figure 10.14 Groceries Model 8 This type of model change cannot be completely inferred by Core Data unless you don’t mind losing the existing values of photoData. In reality, you would not want to lose

existing customer photos, so a model-mapping file is required. Whenever automatic store migration with an inferred mapping is triggered, Core Data checks for a model-mapping file prior to guessing what maps to where. The model-mapping file needs to do the following: Map the old photoData attribute from the Item entity to the new data attribute in the Item_Photo entity. Map the new photo relationship from the Item entity to the Item_Photo entity. Map the new item relationship from the Item_Photo entity to the Item entity. Although it may have been obvious that the photoData attribute needed to be mapped to the data attribute, it is less obvious that a mapping is required for the new photo and item relationships. Without this relationship mapping, existing photos from photoData will not be correctly related to the new data attribute. This mapping is created using a Value Expression similar to those inferred for other relationships in the model-mapping file. Update Groceries as follows to manually map Model 7 to Model 8: 1. Select the Data Model group and then click File > New > File…. 2. Click iOS > Core Data > Mapping Model > Next. 3. Select Model 7.xcdatamodel as the source data model and click Next. 4. Select Model 8.xcdatamodel as the target data model and click Next. 5. Save the mapping model as Model7toModel8, ensure the Groceries target is selected, and then click Create. 6. Select the Item_Photo entity mapping within Model7toModel8.xcmappingmodel. 7. Change the Source of the Item_Photo entity mapping to the Item using the Mapping Model Inspector (Option+ +3). This automatically renames the entity mapping to ItemToItem_Photo. 8. Set the Value Expression of the Destination Attribute called data to $source.photoData, which ensures objects from the old photoData attribute are migrated to the new data attribute. 9. Likewise, set the Value Expression of the Destination Relationship called item to the following: Click here to view code image FUNCTION($manager, “destinationInstancesForEntityMappingNamed:sourceInstances:” ,“ItemToItem”, $source)

10. Select the ItemToItem entity mapping. 11. Set the Value Expression of the Destination Relationship called photo to the following: Click here to view code image FUNCTION($manager, “destinationInstancesForEntityMappingNamed:sourceInstances:” ,“ItemToItem_Photo”, $source)

12. Remove @NSManaged var photoData: NSData from Item+CoreDataProperties.swift. 13. Generate an NSManagedObject subclass for the Item_Photo entity in the Data Model group. 14. Add @NSManaged var photo: Item_Photo? to Item+CoreDataProperties.swift. Once the model is upgraded, it is important to remember that the existing sourceStore used to populate default data becomes incompatible with the data model. If you tried to load the existing sourceStore, the application would crash. For your convenience, a pre-upgraded source store has been created to include with the project bundle. In reality, you would have had to generate this source store again by first importing default data from Model 6 and then upgrading it to Model 7 and then Model 8. Attempt to run the application again, and you are prompted to resolve references to the now nonexistent photoData attribute. Listing 10.6 shows the code involved in an updated demo function, which caters to the new data model. Listing 10.6 CDHelper.swift: demo Click here to view code image func demo () { if CDOperation.objectCountForEntity(“Item”, context: CDHelper.shared.context) == 0 { let moc = CDHelper.shared.importContext moc.performBlock { if let locationAtHome = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtHome”, inManagedObjectContext: moc) as? LocationAtHome , let locationAtShop = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtShop”, inManagedObjectContext: moc) as? LocationAtShop , let unit = NSEntityDescription.insertNewObjectForEntityForName(“Unit”, inManagedObjectContext: moc) as? Unit { locationAtHome.storedIn = “Test Home Location” locationAtShop.aisle = “Test Shop Location” unit.name = ”” for i in 1…100 { if let item = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: moc) as? Item { item.locationAtHome = locationAtHome item.locationAtShop = locationAtShop item.unit = unit item.name = “Test Item \(i)” if let photo = UIImage(named: “BigGroceries.png”), photoData = UIImagePNGRepresentation(photo) {

if let itemPhoto = NSEntityDescription.insertNewObjectForEntityForName(“Item_Photo”, inManagedObjectContext: moc) as? Item_Photo { itemPhoto.data = photoData item.photo = itemPhoto } }

// Save to disk, free up memory and trigger interface refresh CDHelper.save(moc) moc.refreshObject(item, mergeChanges: false) } CDHelper.shared.context.performBlock { NSNotificationCenter.defaultCenter().postNotificationName(“S object: nil) } } } } } }

Update Groceries as follows to use the new attributes and an upgraded default store: 1. Replace the demo function of AppDelegate.swift with the code from Listing 10.6. 2. Update the refreshInterface function of ItemVC.swift to set the photo image view as follows: Click here to view code image if let photoData = item.photo?.data, let image = UIImage(data: photoData) { self.photoImageView.image = image } else { self.photoImageView.image = nil }

3. Replace the didFinishPickingImage function of ItemVC.swift with the code from Listing 10.7 to store captured photos using the new data attribute. 4. Delete DefaultData.sqlite and move it to trash. 5. Download the source store from the following URL and drag it into Groceries: http://www.timroadley.com/LCDwS/Model_8_DefaultData.sqlite.zip. Ensure Copy items if needed and the Groceries target are checked. This persistent store is the Model 8 version of DefaultData.sqlite. Listing 10.7 Image Capture (ItemVC.swift) Click here to view code image func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { if let item = self.selectedObject as? Item { // REMOVE OLD PHOTO if let existingPhoto = item.photo {

CDHelper.shared.context.deleteObject(existingPhoto) } item.photo = nil item.thumbnail = nil // RESIZE PHOTO let newsize = CGSizeMake(300, 300) UIGraphicsBeginImageContextWithOptions(newsize, true, 1.0) image.drawInRect(CGRectMake(0, 0, newsize.width, newsize.height)) let scaledImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() // ADD NEW PHOTO if let photoData = UIImageJPEGRepresentation(scaledImage, 0.7) { print(“Captured a \(scaledImage.size.height) x \ (scaledImage.size.width) photo with size \(photoData.length)”) if let itemPhoto = NSEntityDescription.insertNewObjectForEntityForName(“Item_Photo”, inManagedObjectContext: CDHelper.shared.context) as? Item_Photo { itemPhoto.data = photoData item.photo = itemPhoto CDHelper.saveSharedContext() } else {print(“\(__FUNCTION__) failed to insert new Item_Photo object”)} } else {print(“\(__FUNCTION__) failed to capture and convert an image”)} } else {self.done()} picker.dismissViewControllerAnimated(true, completion: nil) }

Profile Groceries again and scroll through the prepare table view. Compare the results shown previously in Figure 10.12 with those shown in Figure 10.15.

Figure 10.15 A minimized memory footprint You should notice a significant difference in the memory usage, as ~35MB used by the configureCell function has been reduced to less than 300KB. By moving the large objects to the Item_Photo entity, the photo data remains a fault. This means the photo data is not drawn into memory whenever Item objects are fetched.

Cleaning Up When you’re finished with managed objects, it’s important to free up memory by removing them from the context. You can achieve this by turning them into a fault. Whenever the item view disappears, it’s a good opportunity to turn the previously selected item and photo into a fault. Update Groceries as follows to turn previously selected items into a fault: 1. Add the code shown in Listing 10.8 to the bottom of the viewDidDisappear function of ItemVC.swift. Listing 10.8 Fault Photo When View Disappears (ItemVC.swift) Click here to view code image // Turn item & item photo into a fault if let item = self.selectedObject as? Item { if let itemPhoto = item.photo { CDHelper.shared.context.refreshObject(itemPhoto, mergeChanges: false) } CDHelper.shared.context.refreshObject(item, mergeChanges: false) }

Summary This chapter showed what to look for when using Instruments to measure Core Data performance. The various options of NSFetchRequest were explained and their merits demonstrated. It also showed how model design plays a key role in the performance of an application, particularly when large objects are involved. Remember that it is important to execute your testing on the slowest possible device with the largest data set your customers are likely to use. If an application works well on a slow device with a large data set, it will perform even better on the latest devices.

Exercises Why not build on what you’ve learned by experimenting? 1. Comment out self.fetchBatchSize = 25 in the init function of PrepareTVC.swift. Check the query execution time and investigate the performance using Instruments. 2. Set the request fetch limit and offset in the init function of PrepareTVC.swift using the code from Listing 10.9. Examine the query execution time and investigate the performance using Instruments. Notice the effect this change has on the items shown in the Prepare table view. Disable the SQLDebug launch argument and reverse the changes made during the exercises before moving to the next chapter. Listing 10.9 PrepareTVC.swift: init self.fetchLimit = 20

self.fetchOffset = 50

11. Background Processing Everything must be made as simple as possible, but not simpler. Albert Einstein Chapter 10, “Performance,” gave recommendations on how to configure a managed object model for optimal performance. Measuring performance with Instruments was also demonstrated. Full-size photos were removed from the Prepare and Shop table view cells, and yet there are no thumbnails in their place. The process to create thumbnails from photos is intensive, so it cannot be performed in the foreground. Thumbnail creation aside, even the simple act of saving a context has the potential to impact the user interface if there are many changes to commit. This chapter uses the example of thumbnail generation to demonstrate how to perform an intensive task using a private queue context. In addition, background save is implemented in this chapter by introducing a parent context between the persistent store and existing main queue context.

Implementing Background Save Since the excessive memory usage issues were resolved in Chapter 10, the Prepare and Shop table views don’t display thumbnails. The process to create thumbnail images dynamically is intensive, so it should be performed in the background to prevent impact on the user interface. To support the process of thumbnail creation, background save is implemented first. The easiest way to achieve background save is to use two contexts in a parent and child hierarchy. The background context, referred to as the parentContext, is configured to use a private queue by setting its concurrency type to NSPrivateQueueConcurrencyType. The existing foreground context that underpins the user interface is referred to as the context. It is already configured to use the main queue, because its concurrency type is NSMainQueueConcurrencyType. Both contexts exist in memory, so intercommunication between them is fast. A child context has no persistent store. Instead, its parent context acts as the persistent store. When a child context is saved, changes go to its parent. For those changes to be persisted, a save must then be performed on the parent context too. As the parent context runs on a private queue, the save does not impact the user interface. Figure 11.1 shows an overview of this process.

Figure 11.1 Background save with parent and child contexts Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter10.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. When the context hierarchy is configured, at least one context will be configured on a private queue. The updated code involved in configuring a context hierarchy is shown in Listing 11.1 in bold. Listing 11.1 Parent Context (CDHelper.swift) Click here to view code image // MARK: - CONTEXT lazy var parentContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.coordinator moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc }() lazy var context: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.MainQueueConcurrencyType) // moc.persistentStoreCoordinator = self.coordinator moc.parentContext = self.parentContext moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc }() lazy var importContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.coordinator

moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc }() lazy var sourceContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.sourceCoordinator moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc }()

Update Groceries as follows to implement a parent and child context hierarchy: 1. Replace the CONTEXT section of CDHelper.swift with the code from Listing 11.1. It should be noted at this point that the save function of CDHelper.swift already has support for saving a context hierarchy. Listing 11.2 shows in bold the existing code that handles saving a parent context. The save to the parent context will be fast because it is performed within memory. It doesn’t matter if the save from the parent context to the persistent store takes a while because it occurs asynchronously on a private queue. Listing 11.2 Parent Context Save (CDHelper.swift) Click here to view code image class func save(moc:NSManagedObjectContext) { moc.performBlockAndWait { if moc.hasChanges { do { try moc.save() //print(“SAVED context \(moc.description)”) } catch { print(“ERROR saving context \(moc.description) - \(error)”) } } else { //print(“SKIPPED saving context \(moc.description) because there are no changes”) } if let parentContext = moc.parentContext { save(parentContext) } } }

The save function currently does not print to the console log when a context is saved. Update Groceries as follows to ensure saves are logged to the console and test the import process again: 1. Uncomment print("SAVED context \(moc.description)") in the save function of CDHelper.swift. 2. Delete Groceries from the iOS Simulator and click Product > Clean in Xcode.

3. Run Groceries on the iOS Simulator and wait for the test data import process to finish. As the data is imported, multiple saves of the import context are triggered. When each save triggers, you should see “SAVED context” and the memory address of the import context in the console log. 4. Once the import completes, trigger a save of the child and parent context by tapping any item on the Prepare tab. When this save triggers, you should see “SAVED context” and the memory addresses of the child and parent context, as shown in Figure 11.2.

Figure 11.2 Background save 5. To prevent further excessive logging, comment out print("SAVED context \(moc.description)") in the save function of CDHelper.swift. With background save implemented, you may be wondering how much benefit it really adds. Saving in the foreground in Groceries was fast to begin with, so the benefits are arguably small. The benefits of background save are more obvious when a larger amount of data needs to be saved.

Configuring an Import Context Parent With the introduction of a parent context, there are implications on the way data is imported. If the repeated SomethingChanged notifications were removed from the demo function of AppDelegate, you would never see the imported test items appear in the Prepare tab. The problem is that the fetched results controller of the Items table view has no idea that the new data is available in the persistent store. Although you could set up notification observers to trigger a merge, there’s no need to when you can instead configure context as the parent of importContext. Figure 11.3 illustrates this concept.

Figure 11.3 context as a parent of importContext Update Groceries as follows to configure importContext with a parent: 1. Comment out the following line of code in the importContext variable of CDHelper.swift: Click here to view code image moc.persistentStoreCoordinator = self.coordinator

2. Add the following code directly beneath the newly commented code: Click here to view code image moc.parentContext = self.context

Although it won’t be used in this chapter, the sourceContext should also be updated with context as its parent. Update Groceries as follows to configure sourceContext with a parent: 1. Update the code within the sourceContext variable to match the code within the importContext variable in CDHelper.swift. Now that importContext is a child of context, imported data will be immediately visible on table views watching context after importContext is saved without the need to call SomethingChanged. This is a benefit of using a fetched results controller– backed table view. If you have an exceptionally large amount of data to import and you don’t mind whether the user can see the importing data, consider importing into the persistent store from a separate Core Data Stack.

Faulting Objects The process of generating thumbnails impacts a potentially large number of objects. Any time a large number of objects are processed, care should be taken to ensure memory bloating and interface lag are avoided. As explained in Chapter 10, you should turn objects into faults once you’re finished with them using refreshObject and mergeChanges = false after saving the containing context. With a parent and child context hierarchy, you need to do this for every context in the hierarchy. That means for every object imported, a save and fault are required in all three contexts. The process of saving a context and turning a particular object into a fault is a repeatable

one. The code needed to perform this task can be reused regardless of the context involved. As such, a new class called CDFaulter is implemented that can turn a given object into a fault. If there are pending changes, the object’s context is saved prior to the object being turned into a fault. CDFaulter automatically faults the object through the parent context hierarchy if the given context has a parent. Listing 11.3 shows the code involved. Listing 11.3 Core Data Faulter (CDFaulter.swift) Click here to view code image import UIKit import CoreData class CDFaulter: NSObject { class func faultObject (object:NSManagedObject, moc:NSManagedObjectContext) { moc.performBlockAndWait { if object.hasChanges { CDHelper.save(moc) } if object.fault == false { moc.refreshObject(object, mergeChanges: false) } if let parentMoc = moc.parentContext { CDFaulter.faultObject(object, moc:parentMoc) } } } }

Update Groceries as follows to add the CDFaulter class: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Cocoa Touch Class and then click Next. 4. Set Subclass of to NSObject and Class name to CDFaulter. Verify that the Language is Swift before clicking Next. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in CDFaulter.swift with the code from Listing 11.3. The CDFaulter class is first used by the existing demo function of AppDelegate.swift. The test data import process currently saves only objects in importContext. With the new context hierarchy in place, the import process now needs to save and fault new objects in context and parentContext too. Listing 11.4 shows an updated demo function with the changes emphasized in bold.

Listing 11.4 Demo Function (AppDelegate.swift) Click here to view code image func demo () { if CDOperation.objectCountForEntity(“Item”, context: CDHelper.shared.context) == 0 { let moc = CDHelper.shared.importContext moc.performBlock { if let locationAtHome = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtHome”, inManagedObjectContext: moc) as? LocationAtHome , let locationAtShop = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtShop”, inManagedObjectContext: moc) as? LocationAtShop , let unit = NSEntityDescription.insertNewObjectForEntityForName(“Unit”, inManagedObjectContext: moc) as? Unit { locationAtHome.storedIn = “Test Home Location” locationAtShop.aisle = “Test Shop Location” unit.name = ”” for i in 1…100 { if let item = NSEntityDescription.insertNewObjectForEntityForName(“Item”, inManagedObjectContext: moc) as? Item { item.locationAtHome = locationAtHome item.locationAtShop = locationAtShop item.unit = unit item.name = “Test Item \(i)” if let photo = UIImage(named: “BigGroceries.png”), photoData = UIImagePNGRepresentation(photo) { if let itemPhoto = NSEntityDescription.insertNewObjectForEntityForName(“Item_Photo”, inManagedObjectContext: moc) as? Item_Photo { itemPhoto.data = photoData item.photo = itemPhoto CDFaulter.faultObject(itemPhoto, moc: moc) } }

// Save to disk, free up memory and trigger interface refresh CDFaulter.faultObject(item, moc: moc) } CDHelper.shared.context.performBlock { NSNotificationCenter.defaultCenter().postNotificationName(“S object: nil) } } } } } }

Update Groceries as follows to enhance the test data import function: 1. Replace the existing demo function in AppDelegate.swift with the function from Listing 11.4. 2. Delete Groceries from the iOS Simulator and click Product > Clean in Xcode. 3. Run Groceries on the iOS Simulator and wait for the test data import process to finish. You should see items appear immediately as they are imported; however, they still won’t have thumbnails. The CDFaulter class will now be used by a new class responsible for creating thumbnails.

Generating Thumbnails CDThumbnailer is the class used to generate photo thumbnails in the background. This class assumes the context hierarchy is configured as per Figure 11.3. To remain generic enough for reuse in other applications, this class takes the following variables: entityName is the string name of the entity you want to create thumbnails for. thumbnailAttribute is the string name of the binary data attribute where the proposed thumbnail should be created. This attribute is assumed to exist in the entity specified by entityName. photoRelationship is the string name of the relationship that starts from the entity specified by entityName and leads to another entity that contains the photo data. photoAttribute is the string name of the binary data attribute containing the photo. This attribute is assumed to exist at the destination of the relationship specified in photoRelationship. sort is an optional variable used to sort objects with missing thumbnails. The sorting determines the order thumbnails are created in. It will look better to the user if thumbnails are generated in the same order as the table view where the thumbnails are displayed. This means that self.frc.fetchRequest.sortDescriptors should be passed here. size is a CGSize variable used to set the dimensions of the proposed thumbnails. You usually pass a size equal width and height dimensions. Each dimension should be equal to or less than the row height of the cell where the thumbnail will be displayed. moc should be a context running on a private queue. The thumbnails are created inside this context. Typically, this context should be a child of a main queue context, so that thumbnails appear in the interface immediately. Listing 11.5 shows the CDThumbnailer class. Listing 11.5 Core Data Thumbnailer (CDThumbnailer.swift) Click here to view code image

import UIKit import CoreData class CDThumbnailer: NSObject { class func createMissingThumbnails(entityName:String, thumbnailAttribute:String, photoRelationship:String, photoAttribute:String, sort:[NSSortDescriptor]?, size:CGSize, moc:NSManagedObjectContext) { moc.performBlock { let filter = NSPredicate(format: “%K==nil && %K.%K!=nil”, thumbnailAttribute, photoRelationship, photoAttribute) if let objects = CDOperation.objectsForEntity(entityName, context: moc, filter: filter, sort: sort) as? [NSManagedObject] { for object in objects { if object.valueForKey(thumbnailAttribute) == nil { if let objectName = object.valueForKey(“name”) as? String, let photoObject = object.valueForKey(photoRelationship) as? NSManagedObject, let photoData = photoObject.valueForKey(photoAttribute) as? NSData, let photo = UIImage(data: photoData) { print(“Creating thumbnail for object with name ‘\ (objectName)’”) // Create thumbnail UIGraphicsBeginImageContextWithOptions(size, true, 0.0) photo.drawInRect(CGRectMake(0, 0, size.width, size.height)) let thumbnail = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() object.setValue(UIImageJPEGRepresentation(thumbnail, 0.5), forKey: thumbnailAttribute) // Fault objects out of memory CDFaulter.faultObject(photoObject, moc: moc) CDFaulter.faultObject(object, moc: moc) } else {print(“\(__FUNCTION__) failed to prepare the thumbnail attribute object”)} } } } } } }

The implementation code in CDThumbnailer is wrapped in a block. It is expected that the given import context is running on a private background queue. When it is called, the first thing CDThumbnailer does is find all objects without thumbnails using the given variables. This results in an array of pointers to objects with a photo and without a thumbnail. A thumbnail is generated for each of these objects. Once the thumbnail has been created, the now unneeded objects are turned into a fault to save memory.

Update Groceries as follows to create the CDThumbnailer class: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Cocoa Touch Class and then click Next. 4. Set Subclass of to NSObject and Class name to CDThumbnailer. Verify that the Language is Swift before clicking Next. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in CDThumbnailer.swift with the code from Listing 11.5. As table views containing items appear, they should use the CDThumbnailer class to generate missing thumbnails. This means the viewDidAppear function needs to be updated in the PrepareTVC and ShopTVC classes. The call to the class function of CDThumbnailer needs to be configured as shown in Listing 11.6. Listing 11.6 Creating Missing Thumbnails (PrepareTVC.swift and ShopTVC.swift) Click here to view code image override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) Item.ensureLocationsAreNotNilForAllItems() self.performFetch() // Create missing Thumbnails let sort = self.frc.fetchRequest.sortDescriptors let size = CGSizeMake(66, 66) let moc = CDHelper.shared.importContext CDThumbnailer.createMissingThumbnails(“Item”, thumbnailAttribute: “thumbnail”, photoRelationship: “photo”, photoAttribute: “data”, sort: sort, size: size, moc: moc) }

Update Groceries as follows to ensure missing thumbnails are created: 1. Update the viewDidAppear function of PrepareTVC.swift and ShopTVC.swift to match Listing 11.6. Run the application again, and you should see thumbnails appear as they’re automatically generated (see Figure 11.4). Scrolling should remain smooth at all times.

Figure 11.4 Photo thumbnails generated in the background If you run the application on a slow device and navigate to the item view, you may notice a slight lag as the item view loads. This is due to the large image being pulled from the store into memory. A performBlock can be used to offload intensive tasks such as this. Listing 11.7 shows in bold additional code used to load a photo on the item view using this approach. Listing 11.7 Refresh Interface (ItemVC.swift) Click here to view code image func refreshInterface () { if let item = self.selectedObject as? Item { self.nameTextField.text = item.name self.quantityTextField.text = item.quantity?.stringValue self.unitPickerTextField.text = item.unit?.name ?? ”” self.unitPickerTextField.selectedTitle = item.unit?.name ?? ”” self.homeLocationPickerTextField.text = item.locationAtHome?.storedIn ?? ”” self.homeLocationPickerTextField.selectedTitle = item.locationAtHome?.storedIn ?? ”” self.shopLocationPickerTextField.text = item.locationAtShop?.aisle ?? ”” self.shopLocationPickerTextField.selectedTitle = item.locationAtShop?.aisle ?? ”” if let photoData = item.photo?.data, let image = UIImage(data: photoData) { CDHelper.shared.context.performBlock {

self.photoImageView.image = image } } self.checkCamera() } else {self.done()} }

Update Groceries as follows to lazily load the photo on the item view: 1. Replace the refreshInterface function of ItemVC.swift with the one from Listing 11.7. Run the application again and navigate to the item view. The photo loads slightly after the item view interface loads. This likely won’t be noticeable on the iOS Simulator because it is has more resources than a physical iOS device.

Summary This chapter showed how to save and import data in the background. These techniques ensure you don’t impact the user interface negatively during intensive operations. At the same time, a useful class, CDFaulter, was introduced. This class helps reduce the additional code required to save and fault objects through a context hierarchy. You may want to adopt CDFaulter in your own projects where a context hierarchy is in play. Likewise, another new class CDThumbnailer was introduced that can be adapted into your own projects to assist in generating thumbnail images.

Exercises Why not build on what you’ve learned by experimenting? 1. If you have an iOS device, test that the import process and thumbnail creation do not impact the user interface. This can be tested by scrolling up and down during the import process. 2. Update the parser:didStartElement function of CDImporter.swift with CDFaulter support for the context hierarchy. This allows you to test XML import with multiple contexts. To do this, replace the code in STEP 5 of the parser:didStartElement function of CDImporter.swift with the code from Listing 11.8. Listing 11.8 Core Data Importer (CDImporter.swift) Click here to view code image if let _item = item { CDFaulter.faultObject(_item, moc: importContext)} if let _unit = unit { CDFaulter.faultObject(_unit, moc: importContext)} if let _locationAtHome = locationAtHome { CDFaulter.faultObject(_locationAtHome, moc: importContext)} if let _locationAtShop = locationAtShop { CDFaulter.faultObject(_locationAtShop, moc: importContext)}

3. Test XML import with CDFaulter as follows:

Comment out demo() from the applicationDidBecomeActive function of AppDelegate.swift. Uncomment the checkIfDefaultDataNeedsImporting code in the setupCoreData function of CDHelper.swift. Update the checkIfDefaultDataNeedsImporting function of CDImporter.swift with the code from Listing 11.9. This has been updated to import from XML instead of deep copy. Delete the application from the device or simulator and run it again from Xcode to test the XML import process with CDFaulter. Try adding some logging to the save function of CDHelper.swift and repeat the previous step to get greater visibility when a save is successful. Revert this change when you’re finished testing. Listing 11.9 Import from XML (CDImporter.swift) Click here to view code image func checkIfDefaultDataNeedsImporting (url:NSURL, type:String) { if CDImporter.isDefaultDataAlreadyImportedForStoreWithURL(url, type: type) == false { let alert = UIAlertController(title: “Import Default Data?”, message: “If you’ve never used this application before then some default data might help you understand how to use it. Tap ‘Import’ to import default data. Tap ‘Cancel’ to skip the import, especially if you’ve done this before on your other devices.”, preferredStyle: .Alert) let importButton = UIAlertAction(title: “Import”, style: .Destructive, handler: { (action) -> Void in // Import data if let url = NSBundle.mainBundle().URLForResource(“DefaultData”, withExtension: “xml”) { CDHelper.shared.importContext.performBlock { print(“Attempting DefaultData.xml Import…”) self.importFromXML(url) //print(“Attempting DefaultData.sqlite Import…”) //CDImporter.triggerDeepCopy(CDHelper.shared.sourceContext, targetContext: CDHelper.shared.importContext, mainContext: CDHelper.shared.context) } } else {print(“DefaultData.xml not found”)} // Set the data as imported if let store = CDHelper.shared.localStore { self.setDefaultDataAsImportedForStore(store) } }) let skipButton = UIAlertAction(title: “Skip”, style: .Default, handler: { (action) -> Void in // Set the data as imported if let store = CDHelper.shared.localStore {

self.setDefaultDataAsImportedForStore(store) } }) alert.addAction(importButton) alert.addAction(skipButton) // PRESENT dispatch_async(dispatch_get_main_queue(), { () -> Void in if let initialVC = UIApplication.sharedApplication().keyWindow?.rootViewController { initialVC.presentViewController(alert, animated: true, completion: nil) } else {NSLog(“ERROR getting the initial view controller in %@”,__FUNCTION__)} }) } }

Prepare for the next chapter by commenting out everything in the setupCoreData function of CDHelper.swift except for self.setDefaultDataStoreAsInitialStore() and _ = self.localStore. There’s no need to call checkIfDefaultDataNeedsImporting anymore.

12. Search The measure of intelligence is the ability to change. Albert Einstein In Chapter 11, “Background Processing,” the execution of background tasks such as save, import, and thumbnail creation was demonstrated. This brief chapter implements search on the PrepareTVC table view of the Prepare tab. Search results have the same sectioning and sort order as the PrepareTVC table view so that it looks like search results are filtered in place. This functionality is added in such a way that it is easy to enable search on any table view, with only minor updates required. This flexibility is achieved by implementing as much of the search functionality into the underlying CDTableViewController as possible. To add search to a table view, you need to use a UISearchController class. This class is used in conjunction with the current table view and fetched results controller to display search results. When a user taps in the search bar, a search controller delegate method fires, and the fetched results controller is updated with a predicate to filter the existing data. Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter11.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name.

Updating CDTableViewController By updating CDTableViewController to support search, all table views in Groceries inherit the capability to easily implement search. To add search support to CDTableViewController, it needs to be updated to adopt the UISearchBarDelegate, UISearchControllerDelegate, and UISearchResultsUpdating protocols. Listing 12.1 shows how CDTableViewController.swift looks with these changes. Listing 12.1 Search Delegates (CDTableViewController.swift) Click here to view code image import UIKit import CoreData class CDTableViewController: UITableViewController, NSFetchedResultsControllerDelegate, UISearchBarDelegate,

UISearchControllerDelegate, UISearchResultsUpdating { // existing code }

Update Groceries as follows to adopt the search protocols: 1. Update CDTableViewController.swift to inherit from UISearchBarDelegate, UISearchControllerDelegate, and UISearchResultsUpdating by adding the bold code from Listing 12.1 to the class definition at the top of CDTableViewController.swift. This likely generates an error stating that CDTableViewController does not conform to these protocols, which you can ignore for now. The next step is to add a new variable and function to CDTableViewController called searchController and reloadFRC, respectively. The variable is used in conjunction with the existing tableView and frc variables to efficiently manage search results. When a user types in search text the reloadFRC function is called to update the predicate and perform a new fetch. Listing 12.2 shows the code involved in a new section of CDTableViewController specific to search. Listing 12.2 Search Controller (CDTableViewController.swift) Click here to view code image // MARK: - SEARCH var searchController:UISearchController? = nil func reloadFRC (predicate:NSPredicate?) { self.filter = predicate self.frc.fetchRequest.predicate = predicate self.performFetch() }

Update Groceries as follows to add the new variable for search: 1. Add the code from Listing 12.2 to the bottom of CDTableViewController.swift before the last curly brace. Continue to ignore any Xcode errors for now. The next step is to add a UISearchController delegate function to CDTableViewController.swift. This function is called whenever the search bar comes into focus or the search text changes. In turn it uses the search text to create a predicate used to reload the fetched results controller. The code involved is shown in Listing 12.3. Listing 12.3 Updating the Search Controller (CDTableViewController.swift) Click here to view code image // MARK: - DELEGATE: UISearchController func updateSearchResultsForSearchController(searchController: UISearchController) { if let searchBarText = searchController.searchBar.text {

var predicate:NSPredicate? if searchBarText != ”” { predicate = NSPredicate(format: “name contains[cd] %@”, searchBarText) } self.reloadFRC(predicate) } }

Update Groceries as follows to implement the updateSearchResultsForSearchController function: 1. Add the code from Listing 12.3 to the bottom of CDTableViewController.swift before the last curly brace. There should now be no Xcode errors. The next step is to add a UISearchController delegate function to CDTableViewController.swift. This function is called whenever the user presses the (x) in the search bar to stop searching. In turn it reloads the fetched results controller with a nil predicate. The code involved is shown in Listing 12.4. Listing 12.4 Search Bar (CDTableViewController.swift) Click here to view code image // MARK: - DELEGATE: UISearchBar func searchBarTextDidEndEditing(searchBar: UISearchBar) { self.reloadFRC(nil) }

Update Groceries as follows to implement the updateSearchResultsForSearchController function: 1. Add the code from Listing 12.4 to the bottom of CDTableViewController.swift before the last curly brace. The final addition to CDTableViewController is a new function called configureSearch, which is used by CDTableViewController subclasses to enable search. This new function programmatically adds a UISearchBar to the header of the current table view. In addition, appropriate search bar delegate information is configured. Listing 12.5 shows the code involved. Listing 12.5 Search Configuration (CDTableViewController.swift) Click here to view code image func configureSearch () { self.searchController = UISearchController(searchResultsController: nil) if let _searchController = self.searchController { _searchController.delegate = self _searchController.searchResultsUpdater = self _searchController.dimsBackgroundDuringPresentation = false _searchController.searchBar.delegate = self

_searchController.searchBar.sizeToFit() self.tableView.tableHeaderView = _searchController.searchBar } else {print(“ERROR configuring _searchController in %@”, __FUNCTION__)} }

Update Groceries as follows to implement the configureSearch function: 1. Add the code from Listing 12.5 to the bottom of the SEARCH section of CDTableViewController.swift.

Updating PrepareTVC Being a CDTableViewController subclass, most of the legwork in configuring PrepareTVC for search has already been done. As shown in Listing 12.6, to add search to any table view that inherits from CDTableViewController all you need to do is add self.configureSearch() to the viewDidLoad function. In cases where you want to search based on an attribute other than name you need to override updateSearchResultsForSearchController too. Listing 12.6 Search Configuration (PrepareTVC.swift) override func viewDidLoad() { super.viewDidLoad() self.configureSearch() }

Update Groceries as follows to configure search in PrepareTVC: 1. Add the code from Listing 12.6 to the bottom of the VIEW section of PrepareTVC.swift.

Search Optimization The predicate configured in updateSearchResultsForSearchController is the heart of search. This predicate configuration is where you make the most changes when adapting search to your own applications. Here are some key pointers to keep in mind when configuring search: When using a compound predicate (two or more predicates together), put the predicate that filters out the most results first. The order in which the predicates are specified can have a big difference on query execution time. Also, take advantage of indexed attributes by putting them first. When using a compound predicate, put the predicate that filters out dates or numbers ahead of predicates filtering text. This is secondary to the previous point. When working with a large data set with thousands of rows, consider adding a preprocessed normalized string attribute for the entity being searched. A normalized string would be completely lowercase and contain no characters like “á.” This allows you to avoid the need for a case and diacritic insensitive predicate, which optimizes search because there are fewer possible characters to search for. Case and

diacritic insensitivity are specified in a predicate using [cd]. Delete and rerun the application; then search for “baby.” The expected result is shown in Figure 12.1.

Figure 12.1 Testing search

Summary This chapter showed how to add search to an application in such a way that it is easy to propagate it to any table view with ease. Take care when you implement search in your own applications and be sure to understand the expected size of the data sets where search will be required. When it is expected that a large data set will be searched, consider adding special search attributes to your entities with preprocessed normalized strings. This ensures search is seamless and responsive for your users. If you have advanced requirements for search, you might want to investigate Apple’s SearchKit framework.

Exercises Why not build on what you’ve learned by experimenting? 1. Alter the search predicate used in the updateSearchResultsForSearchController function of CDTableViewController.swift with the following options. Search for the letter “j” as you compare the search results between the different predicates: NSPredicate(format: "name beginswith[cd] %@ OR name endswith[cd]%@", searchBarText, searchBarText) NSPredicate(format: "name like[cd] %@", "*\ (searchBarText)*") NSPredicate(format: "name contains %@", searchBarText) 2. Update ShopTVC.swift to implement search using the same approach used to

add search to PrepareTVC. 3. Override the updateSearchResultsForSearchController function in ShopTVC.swift and modify the search predicate so that only listed items are searched. Undo the changes made in the exercises before moving to the next chapter.

13. iCloud You never fail until you stop trying. Albert Einstein This chapter shows how to achieve automatic data synchronization between an individual user’s devices using iCloud. Once Core Data is integrated with iCloud, changes made to application data on one user device are automatically reflected on the user’s other devices. If an application requires that multiple accounts have the ability to use the same data, CloudKit should be used instead. If an application requires ordered relationships, or needs a mapping model for migration in future releases, be aware that neither is supported with iCloud at the time of writing.

iCloud Basics iCloud is used to synchronize documents and data between devices belonging to one user. When Core Data is integrated with iCloud, its data is ubiquitous. The term ubiquitous means “available everywhere,” which is the fundamental intention of iCloud. For changes on one device to be reflected on another, each device needs to be signed in as the same iCloud user. The first device to use the application with iCloud forms a ubiquitous Core Data baseline for that application in iCloud. This baseline is the starting point used by other devices to build their own local copy of the application’s data from iCloud. This local copy of the application data is known as the iCloud Store. The iCloud Store is updated automatically according to change logs in the ubiquity container, which is maintained by Apple’s iCloud servers. Three key components are involved when integrating Core Data with iCloud: iCloud is the service allowing a user’s data to be synchronized across all the user’s devices. It is possible to see the contents of your iCloud container for debugging purposes at https://developer.icloud.com; however, at the time of writing it appears that this site is either deprecated or at least under revision. You can also perform metadata queries to inspect the contents of iCloud or use the iCloud Debug Navigator, which is shown later in the chapter. Each application using iCloud has its own directory at the root of iCloud, and applications from the same developer can be configured to share a ubiquity container within it. This is useful if you need to maintain separate free and paid versions of an application that need to access the same data. The Ubiquity Container is where iCloud documents and data specific to the authenticated iCloud user are found. Everything in this folder is synchronized automatically with the iCloud servers. If a directory with a .nosync suffix is stored in the ubiquity container, its contents are not synchronized. You may have seen implementations of iCloud where the iCloud Store was placed in a .nosync folder of the ubiquity container. This approach is not recommended since iOS 7 because it prevents Core Data from transparently managing a Fallback Store. A Fallback Store is used to provide seamless transitions between iCloud accounts and

reduce the time it takes for the iCloud Store to become usable for the first time. The Fallback Store is also used when the user is logged out of iCloud or has disabled iCloud Drive for the application. The Fallback Store should not be confused with LocalStore.sqlite. If users have data in LocalStore.sqlite it should be migrated into iCloud. The Application Documents Directory, also known as the application sandbox, is a local directory that currently holds the LocalStore.sqlite store. An iCloud.sqlite store is added to this folder in a subfolder specific to each iCloud user. The per-user subfolders are managed transparently by Core Data. The folder structure is shown at the end of the chapter in the “Exercises” section. Figure 13.1 shows an overview of the key components involved in Core Data and iCloud integration, along with the placement of stores and change logs. Note that LocalStore.sqlite is not necessary in this scenario.

Figure 13.1 Key components of iCloud Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter12.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. You also need to ensure that a valid developer team has been selected as the Code Signing Identity for iCloud to work. You can set this in the Identity section of the General tab available when the Groceries application target is selected.

Enabling iCloud Prior to iOS 7, enabling iCloud required you to configure an App ID and provisioning profile. Although this is still a requirement, it is handled for you automatically when you toggle the iCloud switch. This switch to enable iCloud is available on the Capabilities tab of the application target. Update Groceries as follows to enable the iCloud capability: 1. Select the Capabilities tab of the Groceries target and expand iCloud, as shown in Figure 13.2.

Figure 13.2 iCloud before being enabled 2. Ensure you’re connected to the Internet and then turn on iCloud and check iCloud Documents, as shown in Figure 13.3. Notice that a Groceries.entitlements file has been generated too.

Figure 13.3 iCloud Documents enabled Once iCloud has been enabled, you see an automatically generated ubiquity container name. You can customize this to whatever you want, as long as you don’t change it once users have data at this location. If you need data between free and paid versions of an application to remain consistent, you should ensure that the ubiquity container name in each application matches.

Updating CDHelper for iCloud To make it easy to reuse the sample code in your own projects, CDHelper is updated to include iCloud support. The first new function required is called iCloudAccountIsSignedIn and is used to check the iCloud account status. Only when an iCloud account has been authenticated does this function return true. If the user is signed out or has chosen to specifically disable iCloud Drive for the application, this function returns false. Listing 13.1 shows the code involved. Listing 13.1 Checking Authentication State (CDHelper.swift) Click here to view code image // MARK: - ICLOUD func iCloudAccountIsSignedIn() -> Bool { if let token = NSFileManager.defaultManager().ubiquityIdentityToken { print(“** This device is SIGNED IN to iCloud with token \(token) **”) return true } print(“\rThis application cannot use iCloud because it is either signed out or is disabled for this App.”) print(“If the device is signed in and you still get this error, verify the following:”) print(“1) iCloud Documents is ticked in Xcode (Application Target > Capabilities > iCloud.)”) print(“2) The App ID is enabled for iCloud in the Member Center (https://developer.apple.com/)”) print(“3) The App is enabled for iCloud on the Device (Settings > iCloud > iCloud Drive)”) return false }

Update Groceries as follows to enable iCloud account status checks: 1. Add the code from Listing 13.1 to the bottom of CDHelper.swift before the last curly brace. 2. Add CDHelper.shared.iCloudAccountIsSignedIn() to the bottom of the applicationDidBecomeActive function of AppDelegate.swift. 3. Ensure your test iOS device or iOS Simulator is signed in to iCloud. The only mandatory iCloud setting you need to ensure is enabled is iCloud Drive. You can check this via Settings > iCloud > iCloud Drive. 4. Run Groceries on your test device and examine the bottom of the console log. Figure 13.4 shows the expected results; the token will vary.

Figure 13.4 Signed in to iCloud

Adding an iCloud Store To set the location of the iCloud Store, a new variable called iCloudStoreURL is added. The iCloud Store filename is set to iCloud.sqlite; however, feel free to use any store name you think is appropriate in your own projects. Listing 13.2 shows the code involved. Listing 13.2 The iCloud Store URL (CDHelper.swift) Click here to view code image lazy var iCloudStoreURL: NSURL? = { if let url = self.storesDirectory?.URLByAppendingPathComponent(“iCloud.sqlite”) { print(“iCloudStoreURL = \(url)”) return url } return nil }()

Update Groceries as follows to implement the iCloudStoreURL variable: 1. Add the code from Listing 13.2 to the PATHS section of CDHelper.swift beneath the existing localStoreURL variable. The next step is to implement the iCloudStore variable itself, which leverages iCloudStoreURL. Listing 13.3 shows the code involved. Listing 13.3 The iCloud Store (CDHelper.swift) Click here to view code image lazy var iCloudStore: NSPersistentStore? = { // Change contentNameKey for your own applications let contentNameKey = “Groceries” print(“Using ‘\(contentNameKey)’ as the iCloud Ubiquitous Content Name Key”) let options:[NSObject:AnyObject] = [NSMigratePersistentStoresAutomaticallyOption:1, NSInferMappingModelAutomaticallyOption:1, NSPersistentStoreUbiquitousContentNameKey:contentNameKey //,NSPersistentStoreUbiquitousContentURLKey:“ChangeLogs” // Optional since iOS7 ] var _iCloudStore:NSPersistentStore? do { _iCloudStore = try self.coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: self.iCloudStoreURL,options: options) return _iCloudStore } catch { print(“\(__FUNCTION__) ERROR adding iCloud store : \(error)”) return nil } }()

Loading an iCloud store can be performed on the main thread. You still add a persistent store to the coordinator, similar to the approach used in the localStore variable. To use iCloud you pass an additional option key called NSPersistentStoreUbiquitousContentNameKey. This key is a mandatory part of configuring Core Data integration with iCloud. Its value can be any string you would like to represent the ubiquitous store, in this case “Groceries”. Since iOS 7, what was a mandatory key for specifying the location of the change logs is now optional. This key, called NSPersistentStoreUbiquitousContentURLKey, should be used only when you need to manually control where the ubiquitous change logs are located—for example, in cases where your users have existing iCloud application data created on iOS 6. Once all the persistent store options are set, they are given to the addPersistentStoreWithType function. This function returns immediately with a store the application can use. The store returned is actually the Fallback Store, which is used transparently in lieu of the real iCloud Store until it is ready. Update Groceries as follows to implement iCloudStore: 1. Add the code from Listing 13.3 beneath the existing localStore variable in CDHelper.swift.

Handling iCloud Notifications Once the iCloud Store is ready, a notification called NSPersistentStoreCoordinatorStoresWillChangeNotification is sent to indicate that the store is about to change. Once it has changed, another notification called NSPersistentStoreCoordinatorStoresDidChangeNotification is sent. Additionally, when a store imports ubiquitous content, a notification called NSPersistentStoreDidImportUbiquitousContentChangesNotification is sent. All these notifications must be observed to trigger an appropriate response, which is explained shortly. Listing 13.4 shows the code involved in a new function called listenForStoreChanges, which is used to configure the application to observe these critical notifications. Listing 13.4 Listening for Store Changes (CDHelper.swift) Click here to view code image func listenForStoreChanges () { let dc = NSNotificationCenter.defaultCenter() dc.addObserver(self, selector: “storesWillChange:”, name: NSPersistentStoreCoordinatorStoresWillChangeNotification, object: self.coordinator) dc.addObserver(self, selector: “storesDidChange:”, name: NSPersistentStoreCoordinatorStoresDidChangeNotification, object: self.coordinator) dc.addObserver(self, selector: “iCloudDataChanged:”, name: NSPersistentStoreDidImportUbiquitousContentChangesNotification, object:self.coordinator) }

Update Groceries as follows to listen for store changes: 1. Add the code from Listing 13.4 to the bottom of the ICLOUD section of CDHelper.swift. 2. Add self.listenForStoreChanges() to the bottom of the init function of CDHelper.swift. The next step is to implement each function called when the store change notifications mentioned in Listing 13.4 are received. The code involved is shown in Listing 13.5. Listing 13.5 Handling Store Changes (CDHelper.swift) Click here to view code image func storesWillChange (note:NSNotification) { self.sourceContext.performBlockAndWait { do { try self.sourceContext.save() self.sourceContext.reset() } catch {print(“ERROR saving sourceContext \ (self.sourceContext.description) - \(error)”)} } self.importContext.performBlockAndWait { do { try self.importContext.save() self.importContext.reset() } catch {print(“ERROR saving importContext \ (self.importContext.description) - \(error)”)} } self.context.performBlockAndWait { do { try self.context.save() self.context.reset() } catch {print(“ERROR saving context \(self.context.description) - \ (error)”)} } self.parentContext.performBlockAndWait { do { try self.parentContext.save() self.parentContext.reset() } catch {print(“ERROR saving parentContext \ (self.parentContext.description) - \(error)”)} } } func storesDidChange (note:NSNotification) { // Refresh UI NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChanged”, object: nil) } func iCloudDataChanged (note:NSNotification) { // Refresh UI Context self.context.mergeChangesFromContextDidSaveNotification(note) NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChanged”, object: nil) }

The three functions in Listing 13.5 are positioned to react to the store change notifications from Listing 13.4: The storesWillChange function is called just before the underlying persistent store is about to change. One scenario leading to this could be that the current iCloud account has changed. When this happens, the underlying store must change to another store containing the new user’s data. Because the old user might start using his account again in the future, this is an opportune time to save the user’s data and reset each context. Because Groceries has a context hierarchy, each context is synchronously saved and reset in order of child to parent. The calls in this function need to be synchronous because the old store is detached once the function returns. The storesDidChange function is called once the underlying store has changed. This is an opportune time to refresh the user interface. This is achieved by sending the SomethingChanged notification that views in Groceries already observe. The iCloudDataChanged function is used to bring the managed object context up to speed with changes from iCloud. Whenever the underlying iCloud Store is updated, the notification that triggers this function is used to update the context driving the interface, so changes are visible immediately. Update Groceries as follows to implement the functions used to respond to iCloud notifications: 1. Add the code from Listing 13.5 to the bottom of the ICLOUD section of CDHelper.swift. The setupCoreData function of CDHelper now needs to be updated to load the iCloud store instead of the local store. Listing 13.6 shows the code involved. Listing 13.6 Setup Core Data for iCloud (CDHelper.swift) Click here to view code image func setupCoreData() { /*// Model Migration if let _localStoreURL = self.localStoreURL { CDMigration.shared.migrateStoreIfNecessary(_localStoreURL, destinationModel: self.model) } */ // Load Local Store // self.setDefaultDataStoreAsInitialStore() //_ = self.sourceStore //_ = self.localStore // Load iCloud Store _ = self.iCloudStore // Import Default Data /* if let _localStoreURL = self.localStoreURL { CDImporter.shared.checkIfDefaultDataNeedsImporting(_localStoreURL, type: NSSQLiteStoreType) } else {print(“ERROR getting localStoreURL in \(__FUNCTION__)”)}*/ }

Update Groceries as follows to ensure the iCloud store is loaded: 1. Replace the setupCoreData function of CDHelper.swift with the code from Listing 13.6. 2. Run the application and examine the console log. Figure 13.5 shows the expected results.

Figure 13.5 Core Data and iCloud integration successfully configured Core Data has taken on the responsibility of maintaining a Fallback Store. The Fallback Store allows the user to instantly begin using the application with iCloud, even if the network is unavailable. You can now expect to see two new entries in the console log, as follows: Using local storage: 1 means that the Fallback Store is in use, so the user’s data won’t be available on other devices until the initial sync has completed. Using local storage: 0 means that the transition to the iCloud Store is complete and the Fallback Store isn’t being used anymore. This is the message to look for that indicates iCloud synchronization is up and running. At this point, you should start seeing changes reflected on other devices using the same application with iCloud enabled. You might not see the Using Local Storage: 0 message immediately; however, provided you have seen Using Local Storage: 1, then the persistent store is ready to work with. Later, when the background process responsible for completely bringing up the iCloud Store has completed, you should see Using Local Storage: 0 in the console log.

The Debug Navigator Using the Debug Navigator with a running iCloud application reveals capacity, uploads, downloads, and individual document-level statuses. This lets you track the progress of the change logs and gives a better sense of what is going on under the hood. Figure 13.6 shows the Debug Navigator focused on iCloud.

Figure 13.6 iCloud debugging If the Debug Navigator informs you that iCloud is not configured, try enabling iCloud Drive on your Mac in System Preferences > iCloud.

Summary This chapter integrated Core Data with iCloud and yet there’s still work to do to ensure it behaves in a production-like manner. You may find that the initial sync time varies, depending on the state of the change logs that have to be replayed and the speed of the network connection. Because iCloud provides background synchronization, changes made on one device may take a while to show up on other devices. One key point to remember is that unsaved changes in a context won’t appear on other devices. Frequent saves ensure that changes show up on other devices as fast as possible. The following chapter demonstrates techniques such as de-duplication and seeding, which ensure that the user experience is a good one. It is recommended that you complete the next chapter prior to performing any further rigorous testing with iCloud.

Exercises Why not build on what you’ve learned by experimenting? 1. Test iCloud debug logging as follows: Click Product > Scheme > Edit Scheme…. Ensure Run Debug and the Arguments tab are selected.

Click the + in the Arguments Passed On Launch section. Type in -com.apple.coredata.ubiquity.logLevel 3 and then click Close. Run the application again and examine the console log. Note This is incredibly verbose, so disable it when you’re finished testing it. 2. iCloud integration now maintains a file structure in the application sandbox to cater to multiple iCloud accounts. Examine the contents of the Groceries sandbox as follows: Click Window > Devices. Select Groceries in the Installed Apps section of the appropriate connected device. Click the Cog icon and select Show Container. The folder structure is shown in Figure 13.7.

Figure 13.7 Core Data’s application sandbox structure As per Figure 13.7, the folder structure leading to iCloud.sqlite is different from the one you specified in the iCloudStoreURL variable. The additional structure has been inserted by Core Data to allow seamless support for multiple iCloud accounts. The structure varies depending on whether you’re using local storage. If you have an additional iCloud account, try using it with Groceries to see the additional store and folders generated.

14. Taming iCloud Only those who attempt the absurd can achieve the impossible. Albert Einstein In Chapter 13, “iCloud,” the basic steps to integrate Core Data with iCloud were demonstrated. Still, several areas need to be addressed to ensure that the user’s iCloud experience is as seamless as possible. For example, when multiple iCloud-enabled devices are in play, there’s a risk that some data may be duplicated. Techniques for handling data duplication are discussed as they are implemented in Groceries. This chapter also shows how to seed iCloud from data found in a local “Non-iCloud” persistent store. This approach ensures that users don’t lose data when they start using iCloud for the first time, even if they had existing data on separate devices on prior versions of the application before iCloud was introduced.

De-Duplication When iCloud is used across multiple devices, there’s a risk that duplicate items may be created by the user or seeded to iCloud. In particular, when an item is created on an offline device, it won’t appear on other devices until Internet connectivity is restored. In the meantime, if an item with the same name is created on another device, duplicate items result. To deal with this scenario, a new CDDeduplicator class is used to selectively delete the oldest of the duplicate items. In your own applications, you may want to use different logic to determine the most appropriate duplicate to delete. For example, you may want to compare attribute values such as a NSUUID or even take into account relationships before choosing which duplicate to delete. Beyond that, you could even merge values with custom logic, if that makes sense for your application. Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter13.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. You also need to ensure that an appropriate profile has been selected as the Code Signing Identity in the Code Signing section of the Build Settings tab of the Groceries target. If you are using the provided sample code, toggle the iCloud Capability off and then on again to ensure that your own development team is used. The team in use can be seen on the General tab of the Groceries target. Update Groceries as follows to add the CDDeduplicator class:

1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Cocoa Touch Class and then click Next. 4. Set Subclass of to NSObject and Class name to CDDeduplicator and then click Next. 5. Ensure the Groceries target is checked and then create the class by clicking Create in the Groceries project directory.

Identifying Duplicates Before objects can be de-duplicated, they must first be identified as duplicates. To identify an object as a duplicate, you need to choose an attribute that can be used to determine uniqueness. In other words, choose an attribute whose value should always be unique. In the case of Groceries, the unique attributes for each entity where de-duplication is enabled are shown in Table 14.1.

Table 14.1 Selected Unique Attribute Names Once a unique attribute has been chosen, it can be used to create a list showing how many objects have the unique value. Ideally, only one object should have the same unique attribute value. If more than one object has the same unique attribute value, it is considered a duplicate. Listing 14.1 shows the code involved in retrieving an array of duplicate attribute values for a given entity. Listing 14.1 Core Data De-Duplicator (CDDeduplicator.swift) Click here to view code image import UIKit import CoreData class CDDeduplicator: NSObject { class func duplicatesForEntityWithName (entityName:String, uniqueAttributeName:String, moc:NSManagedObjectContext) -> [AnyObject]? { if let psc = moc.persistentStoreCoordinator { // GET UNIQUE ATTRIBUTE let allEntities = psc.managedObjectModel.entitiesByName if let uniqueAttribute = allEntities[entityName]?.propertiesByName[uniqueAttributeName] as? NSAttributeDescription {

// CREATE COUNT EXPRESSION let countExpression = NSExpressionDescription() countExpression.name = “count” countExpression.expression = NSExpression(format: “count: (%K)”, uniqueAttributeName) countExpression.expressionResultType = NSAttributeType.Integer64AttributeType // CREATE AN ARRAY OF *UNIQUE* ATTRIBUTE VALUES let request = NSFetchRequest(entityName:entityName) request.includesPendingChanges = false request.fetchBatchSize = 100 request.propertiesToFetch = [uniqueAttribute, countExpression] request.propertiesToGroupBy = [uniqueAttribute] request.resultType = NSFetchRequestResultType.DictionaryResultType // RETURN AN ARRAY OF *DUPLICATE* ATTRIBUTE VALUES do { let fetchResults = try moc.executeFetchRequest(request) var duplicates:[String] = [] if let duplicatesDictionaryArray = fetchResults as? [Dictionary] { for duplicatesDictionary:Dictionary in duplicatesDictionaryArray { if let instanceCount = duplicatesDictionary[“count”] as? Int { if instanceCount > 1 { if let instanceName = duplicatesDictionary[uniqueAttributeName] as? String { duplicates.append(instanceName) } else {print(“\(__FUNCTION__) FAILED to prepare instanceName”)} } } else {print(“\(__FUNCTION__) FAILED to prepare instanceCount”)} } } else {print(“\(__FUNCTION__) FAILED to prepare duplicatesDictionaryArray”)} if duplicates.count > 0 { print(“FOUND DUPLICATES: \(duplicates)”) return duplicates } } catch { print(“\(__FUNCTION__) FAILED to fetch objects: \ (error)”)} } else {print(“\(__FUNCTION__) FAILED to prepare uniqueAttribute”)} } else {print(“\(__FUNCTION__) FAILED to get persistentStoreCoordinator from moc”)} return nil } }

The duplicatesForEntityWithName function returns an array of attribute values identified as duplicates. This function requires that an entity name, unique attribute name, and context be given. To provide the duplicates array, a fetch request is created that returns each unique attribute value alongside a count of how many objects have these unique

values. Any unique attribute value that is seen more than once is considered a duplicate. Duplicate attribute values are added to an array, which is then returned from the function. Update Groceries as follows to implement duplicatesForEntityWithName: 1. Replace all code in CDDeduplicator.swift with the code from Listing 14.1.

Deleting Duplicates The next step is to add a new function called deDuplicateEntityWithName to CDDeduplicator. This new function is used to delete duplicated objects. The deduplication process should be performed in the import context, which runs in the background. This ensures the user interface isn’t impacted when a substantial amount of duplicates are being processed. The other benefit of using the import context is that the changes are immediately reflected in the main context once the import context is saved. This is due to the context hierarchy, which has been in place since Chapter 11, “Background Processing.” The first thing that the deDuplicateEntityWithName function does is use the existing duplicatesForEntityWithName function to obtain an array of duplicate attribute values. Those values are then used when creating a fetch request configured with an IN predicate. An IN predicate constrains the fetch request to return only objects that have a unique attribute value matching any value in the list of given duplicate values. Once the fetch is performed, an array of all duplicate objects is returned that is used to remove duplicate objects. Once de-duplication has occurred, the remaining objects are turned into a fault. Listing 14.2 shows the code involved. Listing 14.2 De-Duplicating an Entity (CDDeduplicator.swift) Click here to view code image class func deDuplicateEntityWithName (entityName:String, uniqueAttributeName:String, backgroundMoc moc:NSManagedObjectContext) { moc.performBlock { if let duplicates = CDDeduplicator.duplicatesForEntityWithName(entityName, uniqueAttributeName: uniqueAttributeName, moc: moc) as? [String] { // FETCH DUPLICATE OBJECTS let filter = NSPredicate(format:”%K IN (%@)”, uniqueAttributeName,duplicates) let sort = [NSSortDescriptor(key:uniqueAttributeName, ascending:true)] if let duplicateObjects = CDOperation.objectsForEntity(entityName, context: moc, filter: filter, sort: sort) as? [NSManagedObject] { var _lastObject:NSManagedObject? for object in duplicateObjects { // DELETE DUPLICATE OBJECTS if let lastObject = _lastObject { if object.valueForKey(uniqueAttributeName) as? String

== lastObject.valueForKey(uniqueAttributeName) as? String { // Add deletion logic here } } _lastObject = object } for object in duplicateObjects { CDFaulter.faultObject(object, moc: moc) } } } } }

Update Groceries as follows to implement deDuplicateEntityWithName: 1. Add the code from Listing 14.2 to the bottom of CDDeduplicator.swift before the last curly brace. When deciding which duplicate object to delete, it’s important to ensure that any device presented with the same duplicates will delete the same objects. To this end, the CDDeduplicator class is updated to compare a new date attribute called modified. This attribute indicates which object is the oldest and therefore should be deleted. As a second line of defense against duplicates, objects with the least attribute values are deleted if the modified dates match. Failing that, de-duplication is skipped. To add the modified attribute without causing existing installations to crash with incompatible models, a new model called Model 9 is added. Note If a user runs different versions of the same iCloud application with different data models, synchronization of this data between devices will not work. Users need to upgrade all their devices to the latest version for synchronization to resume. Be aware of this constraint during your testing too. Update Groceries to implement the modified attribute: 1. Select Model.xcdatamodeld. 2. Click Editor > Add Model Version…. 3. Click Finish to accept Model 9 as the version name. 4. Ensure Model 9.xcdatamodel is selected. 5. Add an attribute called modified to the Item entity and then set its type to Date. 6. With the new modified attribute selected, un-check Optional using the Data Model Inspector (Option+ +3). 7. Set the Default Value of the modified attribute to 01/01/1970, 12:00:00 AM.

8. Copy the modified attribute from the Item entity to the Item_Photo, Location, and Unit entities. There’s no need to create the modified attribute in the LocationAtHome and LocationAtShop entities because they inherit from the Location entity. 9. Add @NSManaged var modified: NSDate? to Item+CoreDataProperties.Swift, Item_Photo+CoreDataProperties.Swift, Location+CoreDataProperties.Swift, and Unit+CoreDataProperties.Swift. 10. Select Model.xcdatamodeld and then set the current model to Model 9 using File Inspector (Option+ +1), as shown on the right in Figure 14.1.

Figure 14.1 Model 9 as the current model With the new model in place, the editing views need to be configured to update the modified date whenever an object is accessed. The existing refreshInterface function found in each view is a reasonable place for this new code because it already has a pointer to the object being edited. The code differs slightly for each editing view, as shown in Table 14.2.

Table 14.2 Code to Update Object Modified Update Groceries as follows to ensure the modified attribute value is updated to the current date and time when objects are edited:

1. Add item.modified = NSDate() to the refreshInterface function of ItemVC.swift on the first line within the if let item closure. 2. Add unit.modified = NSDate() to the refreshInterface function of UnitVC.swift on the first line within the if let unit closure. 3. Add locationAtHome.modified = NSDate() to the refreshInterface function of LocationAtHomeVC.swift on the first line within the if let locationAtHome closure. If you receive an error informing you that LocationAtHome has no member called modified, ensure that LocationAtHome.swift inherits from Location instead of NSManagedObject. 4. Add locationAtShop.modified = NSDate() to the refreshInterface function of LocationAtShopVC.swift on the first line within the if let locationAtShop closure. If you receive an error informing you that LocationAtShop has no member called modified, ensure that LocationAtShop.swift inherits from Location instead of NSManagedObject. With the appropriate code in place to ensure objects have a modified date, the code to delete duplicated objects based on this attribute can now be implemented. Listing 14.3 shows the code involved. Listing 14.3 Object Deletion Logic (CDDeduplicator.swift) Click here to view code image // DELETION LOGIC if let date1 = object.valueForKey(“modified”) as? NSDate { if let date2 = lastObject.valueForKey(“modified”) as? NSDate { // DELETE OLDEST DUPLICATE… if date1.compare(date2) == NSComparisonResult.OrderedAscending { print(“De-duplicating \(entityName) ‘\ (object.valueForKey(uniqueAttributeName)!)’”) moc.deleteObject(object) } else if date1.compare(date2) == NSComparisonResult.OrderedDescending { print(“De-duplicating \(entityName) ‘\ (object.valueForKey(uniqueAttributeName)!)’”) moc.deleteObject(lastObject) } // ..or.. DELETE DUPLICATE WITH LESS ATTRIBUTE VALUES (if dates match) else if object.committedValuesForKeys(nil).count > lastObject.committedValuesForKeys(nil).count { print(“De-duplicating \(entityName) ‘\ (object.valueForKey(uniqueAttributeName)!)’”) moc.deleteObject(lastObject) } else if object.committedValuesForKeys(nil).count < lastObject.committedValuesForKeys(nil).count{ print(“De-duplicating \(entityName) ‘\ (object.valueForKey(uniqueAttributeName)!)’”) moc.deleteObject(object) } else { print(“Skipped De-duplication, dates and value counts match”)

} CDHelper.save(moc) } }

The logic to blindly delete the oldest object is a primitive way to achieve de-duplication. In some cases, the results of this hardline logic may annoy the users. For example, if a user created a new “apples” object not realizing that one already existed with a lovely photo of apples, that photo would be lost. In your own projects, consider adding further logic to merge old attribute values where a newer object has a nil value. For brevity, there is little value in adding this to Groceries. However you choose to handle your deduplication strategy, the fundamentals are now in place to support your desired logic. Update Groceries as follows to implement duplicate object deletion logic: 1. Add the code from Listing 14.3 to the deDuplicateEntityWithName function of CDDeduplicator.swift by replacing the comment //Add deletion logic here.

Triggering De-duplication The final step in implementing de-duplication is to call deDuplicateEntityWithName at an appropriate time and then double-check that the locations are not nil. Listing 14.4 shows the code involved. Listing 14.4 Triggering De-duplication(PrepareTVC.swift) Click here to view code image // Trigger Deduplication CDDeduplicator.deDuplicateEntityWithName(“Item”, uniqueAttributeName: “name”, backgroundMoc: CDHelper.shared.importContext) CDDeduplicator.deDuplicateEntityWithName(“Unit”, uniqueAttributeName: “name”, backgroundMoc: CDHelper.shared.importContext) CDDeduplicator.deDuplicateEntityWithName(“LocationAtHome”, uniqueAttributeName: “storedIn”, backgroundMoc: CDHelper.shared.importContext) CDDeduplicator.deDuplicateEntityWithName(“LocationAtShop”, uniqueAttributeName: “aisle”, backgroundMoc: CDHelper.shared.importContext) Item.ensureLocationsAreNotNilForAllItems()

Update Groceries as follows to enable de-duplication: 1. Add the code from Listing 14.4 to the bottom of the viewDidAppear function of PrepareTVC.swift. Run the application and create a new item with the same name as another item. One item should be deleted automatically. The expected result is shown in Figure 14.2. Don’t worry if your device isn’t signed in to iCloud; the results should be the same from a deduplication point of view.

Figure 14.2 De-duplication results in the console log

Seeding When users enable iCloud support in an application they already use, chances are they have data that needs to be migrated to iCloud. There are now several stores to consider, as shown in Figure 14.3.

Figure 14.3 Persistent stores in the Application Sandbox The persistent stores in Figure 14.3 are as follows: Store A is a local copy of iCloud data specific to one iCloud user. This data is kept in sync with the iCloud servers transparently. If a different iCloud user logs on to the same device, a separate store is created in another directory. Store B is a local store used when no one is logged in to iCloud or iCloud Drive is disabled for this application. Store C is a local store used before iCloud support was added to the application. This store is now unused and is a good example of a store that you might want to migrate into iCloud. The technique used to migrate Store C to iCloud is deep copy, which was implemented in

Chapter 9, “Deep Copy.” Although more resource intensive than the migratePersistentStore function of NSPersistentStoreCoordinator, this approach provides the granularity of per-entity migration and allows object values to be updated as they are migrated. If you don’t need this granularity in your own applications, it is recommended that you use migratePersistentStore. For Groceries, the ability to update attribute values as they are migrated is leveraged so the modified attribute can be set to a date long in the past. This ensures that the deduplication process is more effective when default data with the same date on multiple devices is migrated. Listing 14.5 shows the new CDImporter code required to force the modified attribute to another date during the migration. Listing 14.5 Customizing the Date (CDImporter.swift) Click here to view code image // Update modified date as appropriate for (additionalAttribute, _) in sourceObject.entity.attributesByName { if additionalAttribute == “modified” { copiedObject.setValue(NSDate(timeIntervalSinceNow: -999999999), forKey: “modified”) } }

Update Groceries as follows to ensure deep copy updates the modified date: 1. Add the code from Listing 14.5 to the copyUniqueObject function of CDImporter.swift on the line before return copiedObject.

Preparing Seed Variables New seedStoreURL, seedContext, and seedCoordinator variables are now added to CDHelper, which is used to seed the LocalStore.sqlite data into iCloud. Listing 14.6 shows these new variables. Listing 14.6 Variables Required for Seeding (CDHelper.swift) Click here to view code image lazy var seedStoreURL: NSURL? = { if let url = self.storesDirectory?.URLByAppendingPathComponent(“LocalStore.sqlite”) { print(“seedStoreURL = \(url)”) return url } return nil }() lazy var seedCoordinator:NSPersistentStoreCoordinator = { return NSPersistentStoreCoordinator(managedObjectModel:self.model) }() lazy var seedContext: NSManagedObjectContext = { let moc = NSManagedObjectContext(concurrencyType:.PrivateQueueConcurrencyType) moc.persistentStoreCoordinator = self.seedCoordinator moc.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return moc

}()

Update Groceries as follows to implement the new variables required for seeding: 1. Add the seedStoreURL variable from Listing 14.6 to CDHelper.swift beneath the existing sourceStoreURL variable. 2. Add the seedCoordinator variable from Listing 14.6 to CDHelper.swift beneath the existing sourceCoordinator variable. 3. Add the seedContext variable from Listing 14.6 to CDHelper.swift beneath the existing sourceContext variable. Another variable required for seeding is seedStore, which is similar to sourceStore. The seedStore variable is loaded as read-only because there’s no need to write to it. The code involved is shown in Listing 14.7. Listing 14.7 The Seed Store Variable (CDHelper.swift) Click here to view code image lazy var seedStore: NSPersistentStore? = { let options:[NSObject:AnyObject] = [NSReadOnlyPersistentStoreOption:1] var _seedStore:NSPersistentStore? do { _seedStore = try self.seedCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: self.seedStoreURL, options: options) return _seedStore } catch { return nil } }()

Update Groceries as follows to configure the seed properties: 1. Add the seedStore variable from Listing 14.7 to CDHelper.swift beneath the existing sourceStore variable.

Adding Seed Helper Functions When the seeding process finishes, the seed store should be deleted to save storage. Two new helper functions are now added to the STORE section of CDHelper.swift to facilitate this. The first function, unloadStore, is used to ensure that the given store is not attached to a coordinator. The second function, removeFileAtURL, is used to delete the seed store file once seeding is finished. For brevity, no check is performed to ensure that seeding was successful before deleting the file. In your own applications, you may want to prompt the user prior to deleting the old store or implement code to detect seeding status prior to deletion. The code involved with the new functions is shown in Listing 14.8.

Listing 14.8 Seed Helper Functions (CDHelper.swift) Click here to view code image func unloadStore (ps:NSPersistentStore) -> Bool { if let psc = ps.persistentStoreCoordinator { do { try psc.removePersistentStore(ps) return true // Unload complete } catch {print(“\(__FUNCTION__) ERROR removing persistent store : \ (error)”)} } else {print(“\(__FUNCTION__) ERROR removing persistent store : store \ (ps.description) has no coordinator”)} return false // Fail } func removeFileAtURL (url:NSURL) { do { try NSFileManager.defaultManager().removeItemAtURL(url) print(“Deleted \(url)”) } catch { print(“\(__FUNCTION__) ERROR deleting item at url ‘\(url)’ : \ (error)”) } }

Update Groceries as follows to add the two new functions: 1. Add the code from Listing 14.8 to the bottom of the existing STORE section of CDHelper.swift. The next step is to implement the function responsible for seeding data to iCloud. Everything is performed within a block using seedContext, which runs in the background. The entities to copy are specified by name and are given to the deepCopyEntities function of CDImporter. Once the deep copy completes, the old store and its related shm and wal files are removed. Listing 14.9 shows the code involved. Listing 14.9 Seeding Data to iCloud (CDHelper.swift) Click here to view code image func seedDataToiCloud () { self.seedContext.performBlock { print(“*** STARTED DEEP COPY FROM SEED STORE TO ICLOUD STORE ***”) _ = self.seedStore let entities = [“LocationAtHome”,“LocationAtShop”,“Unit”,“Item”] CDImporter.deepCopyEntities(entities, from: self.seedContext, to: self.importContext) self.context.performBlock {

NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChan object: nil)

print(“*** FINISHED DEEP COPY FROM SEED STORE TO ICLOUD STORE ***”) // Remove seed store if let _seedStoreURL = self.seedStoreURL { if let wal = _seedStoreURL.path?.stringByAppendingString(“wal”) { self.removeFileAtURL(NSURL(fileURLWithPath: wal)) } if let shm = _seedStoreURL.path?.stringByAppendingString(“shm”) { self.removeFileAtURL(NSURL(fileURLWithPath: shm)) } self.removeFileAtURL(_seedStoreURL) } } } }

Update Groceries as follows to implement the code that seeds data to iCloud: 1. Add the code from Listing 14.9 to the ICLOUD section at the bottom of CDHelper.swift before the last curly brace. Whenever the iCloud store is loaded, a check is performed to see whether there’s local data that needs to be migrated to iCloud. If there is, the user is asked whether she wants to merge it with iCloud. This requires a new function called confirmMergeWithiCloud, which is used to show an alert. Listing 14.10 shows the code involved. Listing 14.10 Seed Confirmation (CDHelper.swift) Click here to view code image func confirmMergeWithiCloud () { if let path = self.seedStoreURL?.path { if NSFileManager.defaultManager().fileExistsAtPath(path) { let alert = UIAlertController(title: “Merge with iCloud?”, message: “This will move your existing data into iCloud.”, preferredStyle: .Alert) let mergeButton = UIAlertAction(title: “Merge”, style: .Default, handler: { (action) -> Void in self.seedDataToiCloud() }) let dontMergeButton = UIAlertAction(title: “Don’t Merge”, style: .Default, handler: { (action) -> Void in // Don’t do anything. In your own applications, store this decision. }) alert.addAction(mergeButton) alert.addAction(dontMergeButton) // PRESENT

dispatch_async(dispatch_get_main_queue(), { () -> Void in if let initialVC = UIApplication.sharedApplication().keyWindow?.rootViewController { initialVC.presentViewController(alert, animated: true, completion:nil) } else {print(“%@ FAILED to prepare the initial view controller”,__FUNCTION__)} }) } else { print(“Skipped unnecessary migration of seed store to iCloud (there’s no store file).”) } } }

When the alert appears, the user is asked whether she wants to merge local data with iCloud. If the user chooses not to seed the data into iCloud, the user is asked again the next time confirmMergeWithiCloud is called. In your own applications it is recommended that you store the decision not to merge to avoid prompting the user unnecessarily. Update Groceries as follows to implement the code to confirm a merge with iCloud: 1. Replace import Foundation with import UIKit at the top of CDHelper.swift. 2. Add the code from Listing 14.10 to the bottom of the ICLOUD section of CDHelper.swift. The next step is to update the setupCoreData function to load the local store as the seed store. Listing 14.11 shows the code involved. Listing 14.11 Setup Core Data with Seeding (CDHelper.swift) Click here to view code image func setupCoreData() { /*// Model Migration if let _localStoreURL = self.localStoreURL { CDMigration.shared.migrateStoreIfNecessary(_localStoreURL, destinationModel: self.model) } */ // Load Local Store // self.setDefaultDataStoreAsInitialStore() //_ = self.sourceStore //_ = self.localStore // Load iCloud Store if let _ = self.iCloudStore { if let path = self.seedStoreURL?.path { // Merge existing data with iCloud if NSFileManager.defaultManager().fileExistsAtPath(path) { if let _ = self.seedStore { self.confirmMergeWithiCloud() } else {print(“Failed to instantiate seed store”)} } else {print(“Failed to find seed store at ‘\(path)’”)}

} else {print(“Failed to prepare seed store path”)} } else {print(“Failed to load iCloud store”)} // Import Default Data /* if let _localStoreURL = self.localStoreURL { CDImporter.shared.checkIfDefaultDataNeedsImporting(_localStoreURL, type: NSSQLiteStoreType) } else {print(“ERROR getting localStoreURL in \(__FUNCTION__)”)}*/ }

Update Groceries as follows to load a seed store during setup: 1. Replace the setupCoreData function of CDHelper.swift with the code from Listing 14.11. All code required to test seeding is now in place. Test seeding as follows: 1. Ensure there is a LocalStore.sqlite file on the device as follows: Delete Groceries from the device or the iOS Simulator. Comment out all code in the setupCoreData function of CDHelper.swift except for _ = self.localStore. Run Groceries on a device or the iOS Simulator. Create some test items that you will recognize as local data. Press Home (Shift+ +H) to trigger a save and then stop the application with Xcode. 2. Click Product > Clean. 3. Restore the setupCoreData function of the CDHelper.swift function so that it loads the iCloud store, which should match Listing 14.11. 4. Run Groceries on a device or the iOS Simulator. 5. Tap Merge as shown in Figure 14.4. If the merge prompt does not appear, retry from Step 1 and ensure the LocalStore.sqlite file exists.

Figure 14.4 Merging local data with iCloud The local data should be migrated into iCloud and the LocalStore.sqlite file removed once the migration completes. If you have additional devices, create some recognizable local data on them and seed it to iCloud. Upon initial import, there may be a lot of seeding activity and then subsequent de-duplication work. Search the console log for “Using local storage: 0,” which should appear within a few minutes of enabling iCloud, so long as your device has Internet connectivity and has a consistent ubiquity container.

Developing with a Clean Slate Throughout testing, you may want to revert iCloud to a clean slate. Usually this is so you can observe application behavior as if a user had just installed the application for the first time. Instead of deleting an application’s iCloud directory contents each time you want to clear iCloud, you can use the NSPersistentStoreCoordinator class function removeUbiquitousContentAndPersistentStoreAtURL. This function synchronously deletes everything in iCloud specific to the given persistent store, so it can take a while. It then propagates this deletion to all participating devices when they come online. When you call this function, you need to provide an options dictionary to help locate the files associated to the given persistent store, just as you would when adding a persistent store normally. It is important to ensure that there are no active persistent store coordinators in use when this function is called. Listing 14.12 shows the code involved in a new ICLOUD RESET section. Listing 14.12 Resetting iCloud (CDHelper.swift) Click here to view code image // MARK: - ICLOUD RESET (only for use during testing, not production) func destroyAlliCloudDataForThisApplication () { print(“Attempting to destroy all iCloud content for this application, which could take a while…”)

let persistentStoreCoordinators = [self.coordinator,self.seedCoordinator, self.sourceCoordinator] for persistentStoreCoordinator in persistentStoreCoordinators { for persistentStore in persistentStoreCoordinator.persistentStores { self.unloadStore(persistentStore) } } if let _iCloudStoreURL = self.iCloudStoreURL { do {

let options = [NSPersistentStoreUbiquitousContentNameKey:“Groceries”] try NSPersistentStoreCoordinator.removeUbiquitousContentAndPersistentStoreAtURL(_iCloudS options: options) print(“\n\n\n”) print(“* This application’s iCloud content has been destroyed. *”) print(“* On ALL devices, please delete any reference to this application from *”) print(“* Settings > iCloud > Storage *”) print(“* * print(“* The application is force closed to ensure iCloud data is wiped cleanly. *”) print(“\n\n\n”) abort() } catch {print(“\n\n FAILED to destroy iCloud content - \(error)”)} } else {print(“\n\n FAILED to destroy iCloud content because _iCloudStoreURL is nil.”)} }

The destroyAlliCloudDataForThisApplication function first checks that a store exists at the given path. So long as it does, all stores are removed from all coordinators and an iCloud options dictionary is created. This dictionary is given along with the iCloud URL to the removeUbiquitousContentAndPersistentStoreAtURL function. Upon success, you’re notified to wipe the application data from all devices. At this point, consider deleting the application entirely to more accurately simulate first time use, if that is your goal. Update Groceries as follows to implement iCloud reset: 1. Add the code from Listing 14.12 to the bottom of CDHelper.swift before the last curly brace. 2. Add self.destroyAlliCloudDataForThisApplication() to the setupCoreData function of CDHelper.swift within the if let _ = self.iCloudStore closure. This triggers a complete wipe of this application’s iCloud data on all devices. 3. Run the application and wait for the iCloud store to be loaded and destroyed. The application should terminate automatically once abort() is reached. Figure 14.5 shows the expected result.

Figure 14.5 iCloud reset 4. Once you receive confirmation that the iCloud content has been destroyed, stop the application and comment out self.destroyAlliCloudDataForThisApplication() in the setupCoreData function of CDHelper.swift. 5. Ensure there are no references to Groceries in Settings > iCloud > iCloud Drive on all devices that synchronize with this iCloud account. 6. Delete Groceries from all devices and the iOS Simulator. 7. In Xcode, click Product > Clean. Note If you have a device that is refusing to synchronize after you have reset its iCloud data, try resetting all settings via Settings > General > Reset > Reset All Settings. This approach won’t delete any application data, and it can resolve the “Error attempting to read ubiquity root url” error message.

Configurations Although inappropriate in Groceries, it’s good to be aware of Core Data configurations. A configuration allows you to assign different entities to different stores. This can be useful if you need to separate data that should be stored in iCloud from data that is best stored locally. For example, static or even rapidly changing device-specific data (such as a location) would be best placed in a non-iCloud store. So long as it is unnecessary to replicate that data to other devices, it should stay local. A configuration is created using the Model Editor, as shown in Figure 14.6.

Figure 14.6 Core Data configurations Configuration creation requires a similar approach to creating an Entity or Fetch Request template. The default configuration can’t be deleted, so if you want to divide your entities into iCloud and non-iCloud configurations, you need to create two new configurations. Once these are created, you can then drag each entity into an appropriate configuration. To use a configuration, pass the configuration name when adding a persistent store to a coordinator with addPersistentStoreWithType. For example, the iCloudStore variable currently passes nil as the configuration name, which just uses the default configuration. Instead of nil, pass the configuration name. A key point to be aware of when using configurations is that you are separating your data into separate stores. Relationships between objects in separate stores are not supported, so you need to work around this constraint.

Summary Additional features were implemented in this chapter that should make Core Data and iCloud more user friendly. From de-duplication to seeding, each of these techniques goes a step closer to providing the seamless experience users have come to expect.

Exercises Why not build on what you’ve learned by experimenting? These exercises involve adding iCloud and Core Data support to a brand new application. 1. Create a new iOS > Application > Single View Application based on the Swift language called EZiCloud. Ensure Use Core Data is not checked and that Devices is set to iPhone. 2. Link to the Core Data Framework in the General tab of the application target. 3. Download and extract the Generic Core Data Classes folder from the following URL: http://timroadley.com/LCDwS/Generic%20Core%20Data%20Classes.zip. 4. Drag the Generic Core Data Classes folder into the EZiCloud group in the EZiCloud project. Ensure that Copy items if needed, Create groups, and the

EZiCloud target are selected before clicking Finish. 5. Add a Data Model as follows: Click File > New > File… and create an iOS > Core Data > Data Model. Ensure the EZiCloud target and an appropriate Group are selected; then click Create to accept Model as the filename. 6. Configure Model.xcdatamodeld as follows: Add an entity called Test with three attributes: modified, device, and someValue. Set the modified attribute type to Date. Set the device and someValue attribute types to String. 7. Create an NSManagedObject subclass for the Test entity. Ensure the EZiCloud group and target are selected before clicking Create. 8. Click File > New > File…. 9. Create an iOS > Source > Cocoa Touch Class that is a CDTableViewController subclass called TestTVC and then replace the code in TestTVC.swift with the code from Listing 14.13. Listing 14.13 Test Table View Controller (TestTVC.swift) Click here to view code image import UIKit import CoreData class TestTVC: CDTableViewController { // MARK: - CELL CONFIGURATION override func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { if let test = self.frc.objectAtIndexPath(indexPath) as? Test { if let textLabel = cell.textLabel, device = test.device { textLabel.text = device } if let detailTextLabel = cell.detailTextLabel, modified = test.modified, someValue = test.someValue { detailTextLabel.text = “\(modified) \(someValue)” } } } // MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // CDTableViewController subclass customization self.entity = “Test” self.sort = [NSSortDescriptor(key: “modified”, ascending: false)]

self.fetchBatchSize = 25 } // MARK: - VIEW override func viewDidLoad() { super.viewDidLoad() self.navigationItem.title = “EZiCloud” self.performFetch() } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) CDDeduplicator.deDuplicateEntityWithName(“Test”, uniqueAttributeName: “someValue”, backgroundMoc: CDHelper.shared.importContext) } // MARK: - INTERACTION @IBAction func add (sender: AnyObject) { if let object = NSEntityDescription.insertNewObjectForEntityForName(“Test”, inManagedObjectContext: CDHelper.shared.context) as? Test { object.device = UIDevice().name object.modified = NSDate() object.someValue = “Test: \(NSUUID().UUIDString)” CDHelper.saveSharedContext() } } // MARK: - DATA SOURCE: UITableView override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if (editingStyle == UITableViewCellEditingStyle.Delete) { if let object = self.frc.objectAtIndexPath(indexPath) as? NSManagedObject { self.frc.managedObjectContext.deleteObject(object) } CDHelper.saveSharedContext() } } }

10. Replace the default view with a table view, as follows: Select Main.storyboard. Delete the existing View Controller. Drag a Table View Controller onto the storyboard and then click Editor > Embed In > Navigation Controller. Set the Navigation Controller as the Initial View Controller using Attributes Inspector (Option+ +4). Drag a Bar Button Item on to the top right of the Table View Controller. Set the System Item of the new Bar Button Item to Add using Attributes Inspector (Option+ +4).

Select the Prototype Cell and then set its Style to Subtitle and its Identifier to Cell. Select the Table View Controller and set its Custom Class to TestTVC using Identity Inspector (Option+ +3). Hold down Control and drag a line from the Add button to the yellow circle at the top of the Table View Controller. Then select Sent Actions > add:. 11. Turn on the iCloud capability using the approach discussed in Chapter 13. Don’t forget to check iCloud Documents. 12. Add CDHelper.saveSharedContext() to the applicationDidEnterBackground and applicationWillTerminate functions of AppDelegate.swift. 13. Set the iCloud contentNameKey from MyApp to EZiCloud within the iCloudStore variable and the destroyAlliCloudDataForThisApplication function of CDHelper.swift. 14. Comment out _ = self.localStore in the setupCoreData function of CDHelper.swift. 15. Uncomment the Load iCloud Store code in the setupCoreData function of CDHelper.swift. There’s no need to uncomment the seed code, however. 16. Run the application on two devices and tap the + button to create test objects on each device. Once Using local storage: 0 appears in the console log, you should see data on the other devices. The expected result is shown in Figure 14.7. If the simulator seems slow to sync, click Debug > Trigger iCloud Sync.

Figure 14.7 Easy iCloud For your convenience, the EZiCloud project is available for download from the following URL: http://timroadley.com/LCDwS/EZiCloud.zip.

15. CloudKit Sync: Uploading Objects Everybody is a genius. But if you judge a fish by its ability to climb a tree, it will live its whole life believing that it is stupid. Albert Einstein In Chapter 14, “Taming iCloud,” the final touches were added to an iCloud integration specific to one user and the user’s devices. This chapter and the next demonstrate how to leverage a public CloudKit database to keep a small data set synchronized across the devices of multiple iCloud users. This is a key distinction from the previous chapters because it expands iCloud’s scope beyond one person’s data. This topic is split across two chapters. This chapter lays the foundation for synchronization by adding the capability to upload new objects. The following chapter completes the circle by adding the capability to download server side changes and handle deletions.

Introducing CloudKit CloudKit is a service for moving data to and from iCloud. On its own, CloudKit has no means of caching iCloud data on a device. This means that a user won’t be able to access his iCloud data when Internet connectivity is unavailable. A generic Core Data class is created called CDCloudSync to fill this gap. It maintains an NSManagedObject for each CloudKit Record (CKRecord), as shown in Figure 15.1. A CKRecord is a fundamental object used to manage data with CloudKit.

Figure 15.1 A Core Data cache for iCloud data accessed with CloudKit Using Core Data as a cache for CloudKit data is different from the way Core Data was integrated with iCloud in the previous chapters. Instead of using self.iCloudStore, the CloudKit cache is stored in self.localStore. To this end, the setupCoreData function of CDHelper needs updating.

Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter14.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. Update Groceries as follows to ensure the local store is used for caching and to enable CloudKit: 1. Comment out all code except _ = self.localStore within the setupCoreData function of CDHelper.swift. 2. Ensure you are connected to the Internet. 3. Check CloudKit in the Capabilities tab of the application target, as shown in Figure 15.2.

Figure 15.2 Enabling CloudKit

CloudKit Database Synchronization Limitations Data stored in iCloud through CloudKit resides in either public or private databases. The public database is accessible to everyone, and private databases are specific to individuals. Within a database, records are stored within a record zone. For Groceries, the use case for CloudKit is to enable multiple people to use the same shopping list. This means that the public database must be used and some restriction placed over who can see what. At the time of writing the following limitations had to be worked around: A public CloudKit database only supports the default record zone. A default record zone does not support sync semantics. In other words, CKFetchRecordChangesOperation cannot be used to check for changed data on the server. The net result of these limitations is that CDCloudSync has to do all the legwork to detect changes on the server. Unfortunately this means that the solution won’t be very scalable. That said the solution suffices for scenarios where groups of people need to share a moderate amount of data. As an estimate, try to keep the total number of objects below a

few thousand.

Introducing CDCloudSync To maintain a local cache of CloudKit data, a new class is added called CDCloudSync. The starting point for this class is shown in Listing 15.1. Listing 15.1 Core Data CloudKit Sync (CDCloudSync.swift) Click here to view code image import UIKit import CoreData import CloudKit import SystemConfiguration // CACHE STATUS let NEW = 0, INSERTING = 1, RELATING = 2, CHANGED = 3, DELETING = 4, SYNCHRONIZED = 5 private let _sharedCDCloudSync = CDCloudSync() class CDCloudSync: NSObject { // MARK: - SHARED INSTANCE class var shared : CDCloudSync { return _sharedCDCloudSync } // MARK: - LEGIBILITY func friendlyObjectName (object:NSManagedObject) -> String { if let entityName = object.entity.name, moc = object.managedObjectContext { if let entity = NSEntityDescription.entityForName(entityName, inManagedObjectContext: moc) { for (attribute, _) in entity.attributesByName { if [“name”,“storedIn”,“aisle”].contains(attribute) { if let value = object.valueForKey(attribute) as? String { if let cacheID = object.valueForKey(“cacheID”) as? String { return “[\(entityName) object "\(value)" cacheID:"\(cacheID)"]” } else { return “[\(entityName) object "\(value)" cacheID:"nil")]” } } } } } return “[\(entityName) object]” } return “[????]” } func friendlyRecordName (record:CKRecord) -> String {

if let name = record.valueForKey(“name”) as? String { return “[\(record.recordType) record "\(name)" recordID:"\ (record.recordID.recordName)"]” } if let storedIn = record.valueForKey(“storedIn”) as? String { return “[\(record.recordType) record "\(storedIn)" recordID:"\ (record.recordID.recordName)"]” } if let aisle = record.valueForKey(“aisle”) as? String { return “[\(record.recordType) record "\(aisle)" recordID:"\ (record.recordID.recordName)"]” } return “[\(record.recordType) record recordID:"\ (record.recordID.recordName)"]” } func friendlyRecordSummary (record:CKRecord) -> String { if let name = record.valueForKey(“name”) as? String { return “\(name)” } if let storedIn = record.valueForKey(“storedIn”) as? String { return “\(storedIn)” } if let aisle = record.valueForKey(“aisle”) as? String { return “\(aisle)” } return “Photos” } }

After importing the relevant frameworks, CDCloudSync.swift first declares the possible cache statuses that an NSManagedObject can have. These statuses are used to determine what objects need uploading to iCloud. The CDCloudSync.swift class is also structured similarly to CDHelper, so that it can return a shared instance of itself. To make the console log easier to read, a LEGIBILITY section is responsible for returning a user-friendly readable string for the given NSManagedObject and CKRecord objects. If you adapt this code to your own applications, simply add any other identifying attributes to the friendlyObjectName and friendlyRecordName functions. Update Groceries as follows to implement CDCloudSync.swift: 1. Select the Generic Core Data Classes group. 2. Click File > New > File…. 3. Create a new iOS > Source > Swift File and then click Next. 4. Set the file to save as CDCloudSync. 5. Ensure the Groceries target is checked and then click Create to create the class in the Groceries project directory. 6. Replace all code in CDCloudSync.swift with the code from Listing 15.1.

Public Data Groups For groups of people to use the same shopping list, the public data must be compartmentalized into groups. To compartmentalize public data into groups, each CKRecord is configured with a group and groupKey attribute. All fetches from iCloud are filtered to return data only for a specific group and groupKey, essentially acting like a pseudo username and password. The group and groupKey are set in NSUserDefaults and the user informed of this during the first launch of the application. To support this, a settings bundle is now added to Groceries. Note Using a group and groupKey predicate is not a sufficient means to protect sensitive data. If your applications have sensitive data, it is recommended that you employ encryption to protect customer data. This topic is out of scope for this book. Update Groceries as follows to add a settings bundle: 1. Click File > New > File…. 2. Click iOS > Resource > Settings Bundle > Next. 3. Configure Settings Bundle to save in the Groceries group and then click Create. 4. Configure Settings.bundle > Root.plist as shown in Figure 15.3. Most critical are the two Text Fields with an Identifier of group and groupKey, respectively.

Figure 15.3 Configuring the Groceries settings bundle Run Groceries and press Home (Shift+ +H in the iOS Simulator). Open Settings > Groceries and you should see the Shopping List Name and Shopping List Password set to Public, as per Figure 15.4.

Figure 15.4 Groceries settings The key-value pairs set in Settings.bundle are accessible through NSUserDefaults. Retrieval of the current group and groupKey functions is achieved as shown in Listing 15.2. Listing 15.2 Group Configuration (CDCloudSync.swift) Click here to view code image // MARK: - GROUP func group () -> String { let defaults = NSUserDefaults.standardUserDefaults() if let object = defaults.objectForKey(“group”) as? String { return object } defaults.setObject(“Public”, forKey: “group”) return “Public” } func groupKey () -> String { let defaults = NSUserDefaults.standardUserDefaults() if let object = defaults.objectForKey(“groupKey”) as? String { return object } defaults.setObject(“Public”, forKey: “groupKey”) return “Public” } class func addGroupInfo(object:NSManagedObject) { object.setValue(CDCloudSync.shared.group(), forKey: “group”) object.setValue(CDCloudSync.shared.groupKey(), forKey: “groupKey”) }

In addition to the group and groupKey functions, there’s also a new addGroupInfo function. This function is used to minimize the code required to update managed objects with the group and groupKey values. Update Groceries as follows to implement the group and groupKey functions in

addition to the addGroupInfo function: 1. Add the code from Listing 15.2 to the bottom of CDCloudSync.swift before the last curly brace. When the application becomes active, the user should be encouraged not to use the default public shopping list. To achieve this, a UIAlertController is presented to the user whenever a group name of “Public” is found. The alert controller prompts the user to set a different shared shopping list and password as shown in Listing 15.3. Listing 15.3 Public Group Check (AppDelegate.swift) Click here to view code image func applicationDidBecomeActive(application: UIApplication) { if CDCloudSync.shared.group() == “Public” { if let window = self.window, view = window.rootViewController { let alert = UIAlertController(title: “You’re using a Public Shopping List.”,message: “Set the same Shopping List Name and Password on two or more devices to stay synchronized.”, preferredStyle: UIAlertControllerStyle.Alert) alert.addTextFieldWithConfigurationHandler({ (textField) -> Void in textField.placeholder = “Shopping List Name”}) alert.addTextFieldWithConfigurationHandler({ (textField) -> Void in textField.placeholder = “Password” textField.secureTextEntry = true}) let saveAction = UIAlertAction(title: “Save”, style: .Default, handler: { (action) -> Void in if let textFields = alert.textFields { if let shoppingListName = textFields[0].text, let password = textFields[1].text { // Save custom shopping list let defaults = NSUserDefaults.standardUserDefaults() defaults.setObject(shoppingListName, forKey: “group”) defaults.setObject(password, forKey: “groupKey”) defaults.synchronize() // Reset the predicate filter on all fetched results controllers NSNotificationCenter.defaultCenter().postNotificationName(“GroupChanged”, object: nil) } } else {print(“\(__FUNCTION__) FAILED to get textFields from alert controller”)} }) alert.addAction(saveAction) view.presentViewController(alert, animated: true, completion: nil) } else {“FAILED to get window.rootViewController”} } }

The applicationDidBecomeActive function first checks whether the public list is being used. If it is, the current view is located and used to present a UIAlertController. The user can set a new shopping list name and password, which is saved in NSUserDefaults. A GroupChanged notification is then sent, which is used to trigger a predicate update for all fetched results controllers. Update Groceries as follows to prompt the user in cases when the user is using the Public list: 1. Replace the applicationDidBecomeActive function in AppDelegate.swift with the code from Listing 15.3. Don’t worry about the code you’re replacing. 2. Add the code from Listing 15.4 to the bottom of PrepareTVC.swift, ShopTVC.swift, UnitsTVC.swift, LocationsAtHomeTVC.swift, and LocationsAtShopTVC.swift. This is the code that updates the fetched results controller predicates and contains some code in preparation for the next chapter. 3. Add AND listed == 1 to the end of the predicate format string in the groupChanged function of ShopTVC.swift. This ensures that only listed items are shown on the Shop tab after the group changes. Be sure to leave a space after the %i in the format string before AND listed == 1. 4. Add the function from Listing 15.4 to the bottom of UnitPickerTF.swift, LocationAtHomePickerTF.swift, and LocationAtShopPickerTF.swift. 5. Add NSNotificationCenter.defaultCenter().addObserver(self, selector: "groupChanged", name: "GroupChanged", object: nil) to the bottom of the init function of all classes from step 2 and step 4. Listing 15.4 Handling Group Changes (CDCloudSync.swift) Click here to view code image // MARK: - CLOUDSYNC func groupChanged () { let predicate = NSPredicate(format: “group == %@ AND groupKey == %@ AND cacheStatus != %i”, CDCloudSync.shared.group(), CDCloudSync.shared.groupKey(), DELETING) self.filter = predicate self.frc.fetchRequest.predicate = predicate self.performFetch() }

Cache Status The cacheStatus function of an NSManagedObject is fundamental to the detection of changes that should be uploaded to iCloud. The cache status definitions are as follows: The NEW cache status is set on objects that have just been created. This is the default

status. The INSERTING cache status is set on objects just before an attempt to upload them to iCloud. If the upload attempt fails, the object is downgraded to a NEW cache status again. The RELATING cache status is set on objects that have been successfully uploaded. This cache status indicates that the relationships from the source NSManagedObject need to be recreated as references from the associated CKRecord. The CHANGED cache status is set on objects that have been changed in any way. This cache status is used in the next chapter as support for existing object change detection is added. The DELETING cache status is set on objects that have been deleted. This cache status is used in the next chapter as support for object deletion is added. All table views and picker views are configured to filter out objects with this cache status. This gives the appearance of immediate deletion even when Internet connectivity is unavailable. The SYNCHRONIZED cache status is set on objects that have been fully uploaded or downloaded and haven’t been changed since. Mainly to support a readable console log, a new CACHE STATUS section is added to CDCloudSync.swift. The code involved is shown Listing 15.5. Listing 15.5 Cache Status (CDCloudSync.swift) Click here to view code image // MARK: - CACHE STATUS func cacheStatusName (cacheStatus:Int) -> String { switch (cacheStatus) { case NEW: return “NEW” case INSERTING: return “INSERTING” case RELATING: return “RELATING” case CHANGED: return “CHANGED” case DELETING: return “DELETING” case SYNCHRONIZED: return “SYNCHRONIZED” default: return “UNKNOWN” } } func setCacheStatusForObject (object:NSManagedObject, requestedStatus:Int) { if let currentStatus = object.valueForKey(“cacheStatus”) as? NSNumber { if currentStatus == NEW && requestedStatus == CHANGED { NSLog(“%@ cannot move directly from NEW to CHANGED”, self.friendlyObjectName(object)) return } if currentStatus == INSERTING && requestedStatus == CHANGED { NSLog(“%@ cannot move directly from INSERTING to CHANGED”,

self.friendlyObjectName(object)) return } let newStatus = NSNumber(integer: requestedStatus) object.setValue(newStatus, forKey: “cacheStatus”) object.setValue(NSDate(), forKey: “modified”) if let context = object.managedObjectContext {CDHelper.save(context)} } else {NSLog(“ERROR getting existing cacheStatus in %@”,__FUNCTION__)} } func cacheStatusForAllObjects (entities:[String], context:NSManagedObjectContext) { var new = [NSManagedObject]() var inserting = [NSManagedObject]() var relating = [NSManagedObject]() var changed = [NSManagedObject]() var deleting = [NSManagedObject]() var synchronized = [NSManagedObject]() let predicateNew = NSPredicate(format: “cacheStatus == %i”, NEW) let predicateInserting = NSPredicate(format: “cacheStatus == %i”, INSERTING) let predicateRelating = NSPredicate(format: “cacheStatus == %i”, RELATING) let predicateChanged = NSPredicate(format: “cacheStatus == %i”, CHANGED) let predicateDeleting = NSPredicate(format: “cacheStatus == %i”, DELETING) let predicateSynchronized = NSPredicate(format: “cacheStatus == %i”, SYNCHRONIZED) for entityName in entities { if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicateNew, sort: nil) as? [NSManagedObject] { for object in objects {new.append(object)} } if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicateInserting, sort: nil) as? [NSManagedObject] { for object in objects {inserting.append(object)} } if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicateRelating, sort: nil) as? [NSManagedObject] { for object in objects {relating.append(object)} } if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicateChanged, sort: nil) as? [NSManagedObject] { for object in objects {changed.append(object)} } if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicateDeleting, sort: nil) as? [NSManagedObject] { for object in objects {deleting.append(object)} } if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicateSynchronized, sort: nil) as? [NSManagedObject] { for object in objects {synchronized.append(object)}

} } if new.isEmpty == false { NSLog(“%i NEW:”, new.count) for object in new {NSLog(“ —> %@”, self.friendlyObjectName(object))} } if inserting.isEmpty == false { NSLog(“%i INSERTING:”, inserting.count) for object in inserting {NSLog(“ —> %@”, self.friendlyObjectName(object))} } if relating.isEmpty == false { NSLog(“%i RELATING:”, relating.count) for object in relating {NSLog(“ —> %@”, self.friendlyObjectName(object))} } if changed.isEmpty == false { NSLog(“%i CHANGED:”, changed.count) for object in changed {NSLog(“ —> %@”, self.friendlyObjectName(object))} } if deleting.isEmpty == false { NSLog(“%i DELETING:”, deleting.count) for object in deleting {NSLog(“ —> %@”, self.friendlyObjectName(object))} } if synchronized.isEmpty == false { NSLog(“%i SYNCHRONIZED:”, synchronized.count) for object in synchronized {NSLog(“ —> %@”, self.friendlyObjectName(object))} } }

The cacheStatusName function returns a string with a friendly cache status name. It is used only for clearer console logging purposes and a reduced codebase. The setCacheStatusForObject function is used to set the cache status for an object. This not only reduces repetitive code, it also supports rules to prevent specific changes. For example, the cache status of an object cannot move directly from NEW or INSERTING to CHANGED. In the future this will prevent attempts to update a CKRecord that doesn’t yet exist. The cacheStatusForAllObjects function is used purely for troubleshooting purposes. It writes a full list of all managed objects to the console log. Calls to this function should be commented out before an application ships to the App Store. Note All logging in CDCloudSync.swift uses NSLog() instead of print(). This ensures that console log entries are synchronous and have a timestamp. Using print() to log activity from background threads can produce unreadable logs as the text merges from separate threads, seemingly randomly.

Update Groceries as follows to add the CACHE STATUS section and ensure various areas of the application use these new statuses: 1. Add the code from Listing 15.5 to the bottom of CDCloudSync.swift before the last curly brace. 2. Add CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: CHANGED) to the bottom of the addGroupInfo function of CDCloudSync.swift. 3. Add CDCloudSync.shared.setCacheStatusForObject(item, requestedStatus: CHANGED) to the following locations: Before CDHelper.saveSharedContext() in the tableView:didSelectRowAtIndexPath function of PrepareTVC.swift. After item.listed = NSNumber(bool: false) in the clearList function of PrepareTVC.swift. After nothingCleared = false in the clear function of ShopTVC.swift. Before CDHelper.saveSharedContext() in the tableView:didSelectRowAtIndexPath function of ShopTVC.swift. 4. Update the imagePickerController:didFinishPickingImage function of ItemVC.swift by replacing CDHelper.shared.context.deleteObject(existingPhoto) with CDCloudSync.shared.setCacheStatusForObject(existingPhoto, requestedStatus: DELETING)

CloudKit Building Blocks Many of the base functions required to operate with CloudKit are put in a new GENERAL section. These functions form the foundations of interactivity with CloudKit. The first functions are used to create and update CloudKit records, as shown in Listing 15.6. In addition, a syncMessage function will be used to keep the user informed regarding synchronization progress. Listing 15.6 CloudKit Record Creation and Deletion (CDCloudSync.swift) Click here to view code image // MARK: - GENERAL func syncMessage (message:String) { dispatch_async(dispatch_get_main_queue(), { () -> Void in NSNotificationCenter.defaultCenter().postNotificationName(“syncMessage”, object: message) }) } func createRecordWithObject (object:NSManagedObject) -> CKRecord { let record = CKRecord(recordType: object.entity.name!)

return updateRecord(record, withObject: object) } func updateRecord (record:CKRecord, withObject object:NSManagedObject) -> CKRecord { if let entityName = object.entity.name { if record.recordType == entityName { for (attribute, description) in object.entity.attributesByName { if let type = description.attributeValueClassName { if attribute != “cacheID” && attribute != “cacheStatus” { switch (type) { case “NSNumber”: if let value = object.valueForKey(attribute) as? NSNumber { record.setObject(value, forKey: attribute) } case “NSDecimalNumber”: if let value = object.valueForKey(attribute) as? NSDecimalNumber { record.setObject(value, forKey: attribute) } case “NSString”: if let value = object.valueForKey(attribute) as? NSString { record.setObject(value, forKey: attribute) } case “NSDate”: if let value = object.valueForKey(attribute) as? NSDate { record.setObject(value, forKey: attribute) } case “NSData”: if let value = object.valueForKey(attribute) as? NSData { record.setObject(value, forKey: attribute) } default: NSLog(“UNHANDLED type ‘%@’ in %@”, type, __FUNCTION__) } } } } record.setObject(record.recordID.recordName, forKey: “cacheID”) record.setObject(self.group(), forKey: “group”) record.setObject(self.groupKey(), forKey: “groupKey”) } else {NSLog(“SKIPPED record update because the given object entity and record type don’t match in %@”, __FUNCTION__)} } else {NSLog(“SKIPPED record update because the entityName could not be determined in %@”, __FUNCTION__)} return record }

The syncMessage function simply posts a custom notification to all observers. This will be used in the future to update the user interface with the names of items that are currently being synchronized. The createRecordWithObject function creates and returns a new CKRecord object. Before it returns the new object it updates it using the new updateRecord function. The updateRecord function is at the crux of mapping an NSManagedObject to a CKRecord. Once the NSManagedObject entity name and CKRecord record type are confirmed to match, the mapping exercise can begin. Similar to an NSManagedObject, a CKRecord is a dictionary of key value pairs. The supported attribute types can therefore be mapped easily using setObject:forKey. The supported attribute types are NSNumber, NSDecimalNumber, NSString, NSDate, and NSData, which should be sufficient for most applications. Once the updateRecord function has mapped the NSManagedObject attribute values to the CKRecord, a cacheID, group, and groupKey attribute are then set on the NSManagedObject. The cacheID attribute is required to enable IN predicate searches with CloudKit and should always be equal to the record name of the CKRecord. The record name cannot be used directly for this purpose. The group and groupKey attributes ensure that separate shopping lists remain separate. Update Groceries as follows to add the GENERAL section: 1. Add the code from Listing 15.6 to the bottom of CDCloudSync.swift before the last curly brace. The next step is to configure references on the CKRecord as per the associated NSManagedObject relationships. A relationship from one CKRecord to another CKRecord is specified with a CKReference. The code involved is shown in Listing 15.7. Listing 15.7 Update Reference for Record (CDCloudSync.swift) Click here to view code image func updateReferenceForRecord (record:CKRecord, withObject object:NSManagedObject) -> CKRecord { for (relationshipName, relationship) in object.entity.relationshipsByName { if relationship.toMany { //NSLog(“SKIPPED unsupported To-Many ‘%@’ relationship for %@ (ManyToOne will be processed on the inverse ToOne relationship)”, relationshipName, self.friendlyObjectName(object)) } else if relationship.ordered { NSLog(“SKIPPED Ordered ‘%@’ relationship for %@ (Not supported)”, relationshipName, self.friendlyObjectName(object)) } else { // ToOne relationship determined, find the related object’s cacheID

if let relatedObject = object.valueForKey(relationshipName) as? NSManagedObject { if let relatedObjectCacheID = relatedObject.valueForKey(“cacheID”) as? String { let relatedObjectRecordID = CKRecordID(recordName: relatedObjectCacheID) let reference = CKReference(recordID: relatedObjectRecordID, action: .None) record.setObject(reference, forKey: relationshipName) NSLog(“ESTABLISHED a To-One ‘%@’ reference for %@ based on relationship from %@ to %@”, relationshipName, self.friendlyRecordName(record),self.friendlyObjectName(object), self.friendlyObjectName(relatedObject)) } else { NSLog(“CRITICAL ERROR getting cacheID from related object %@. ObjectID = %@”, self.friendlyObjectName(relatedObject), relatedObject.objectID.description) } } else { NSLog(“ESTABLISHED NIL %@ relationship for %@”, relationshipName, self.friendlyObjectName(object)) record.setObject(nil, forKey: relationshipName) } } } return record }

For each relationship found in the given NSManagedObject, the updateReferenceForRecord function attempts to create an equivalent CKReference in the given CKRecord. A reference is not created for relationships where the destination object is nil, or in cases where the relationship is ordered or tomany. Only many-to-one relationships are created. In the future, when NSManagedObject objects are created from CKRecord objects, Core Data will automatically reestablish one-to-many relationships when the inverse many-to-one relationships are created. There is no need to worry about delete rules because they are handled by Core Data already. Update Groceries as follows to add relationship creation support: 1. Add the code from Listing 15.7 to the bottom of the GENERAL section of CDCloudSync.swift. The next function to be added to the GENERAL section is saveMappedRecords, as shown in Listing 15.8. Listing 15.8 Save Mapped Records (CDCloudSync.swift) Click here to view code image func saveMappedRecords (mappedRecords:[CKRecord:NSManagedObject], context:NSManagedObjectContext, database:CKDatabase, successStatus:Int, fault:Bool, completion:(savedMappedRecords:[CKRecord:NSManagedObject]?) -> Void) { // Nothing to save

if mappedRecords.count == 0 {completion(savedMappedRecords: nil);return} // Split into batches to prevent “too many items” & “rate limited” errors let batchSize = 300 let batchCountFloat:Float = Float(mappedRecords.count) / Float(batchSize) var batchCountInt:Int = Int(round(batchCountFloat)) if batchCountFloat > Float(batchCountInt) {batchCountInt++} // Round Up if batchCountInt == 0 {completion(savedMappedRecords: nil);return} var batches = [[CKRecord]]() let allKeys:[CKRecord] = Array(mappedRecords.keys) var rangeStart = 0, rangeEnd = 0 NSLog(“SAVING %i record(s) in %i batch(es) with a size of %i”, mappedRecords.count, batchCountInt, batchSize) for batchNumber in 1…batchCountInt { rangeStart = rangeEnd rangeEnd = rangeStart + batchSize if rangeEnd > allKeys.count {rangeEnd = allKeys.count} // Pull a subset of mapped records into the “batch” sized dictionary. let batchKeys = allKeys[rangeStart…(rangeEnd-1)] var batch:[CKRecord] = [] for batchKey in batchKeys {batch.append(batchKey)} batches.append(batch) NSLog(“PROCESSING save batch %i range %i to %i”, batchNumber, rangeStart, rangeEnd - 1) } var allSavedRecords = [CKRecord]() var batchesRemaining = batches.count for batch in batches { let operation = CKModifyRecordsOperation(recordsToSave: batch, recordIDsToDelete: []) operation.savePolicy = CKRecordSavePolicy.AllKeys operation.qualityOfService = .UserInteractive operation.modifyRecordsCompletionBlock = {(savedRecords, deletedRecords, error) -> Void in if let _error = error { self.syncMessage(“Sync Error 1. Try Later”) NSLog(“ERROR saving records: %@”, _error) completion(savedMappedRecords: nil) } else if let _savedRecords = savedRecords { // Reconstitute batch allSavedRecords.appendContentsOf(_savedRecords) } batchesRemaining— NSLog(“%i save batch(es) remaining”, batchesRemaining) if batchesRemaining == 0 { var savedMappedRecords = [CKRecord:NSManagedObject]() for savedRecord in allSavedRecords { if let mappedObject = mappedRecords[savedRecord] { // Set cacheID if it’s missing if mappedObject.valueForKey(“cacheID”) == nil { mappedObject.setValue(savedRecord.recordID.recordName, forKey: “cacheID”) } self.syncMessage(“Synchronized \ (self.friendlyRecordSummary(savedRecord))”) NSLog(“SAVED record %@”, self.friendlyRecordName(savedRecord))

// Update Cache Status self.setCacheStatusForObject(mappedObject, requestedStatus: successStatus) savedMappedRecords[savedRecord] = mappedObject // Free up memory if fault {CDFaulter.faultObject(mappedObject, moc: context)} } } completion(savedMappedRecords: savedMappedRecords) } } database.addOperation(operation) } }

The saveMappedRecords function creates a batches array of arrays of records to save, based on the given dictionary of CKRecord and NSManagedObject objects. For each batch, a CKModifyRecordsOperation instance is then created, which batches the save operation. The operation quality of service is set to User Initiated because in this application the user triggers the synchronization. So long as the save is successful, the completion block returns an array of saved CKRecord objects. The saved records are then put in a new CKRecord and NSManagedObject dictionary before the cacheID and cacheStatus values are set. Finally, the NSManagedObject is optionally turned in to a fault to save memory and the completion block returns the saved CKRecord and NSManagedObject dictionary. If the save fails, for example due to a network failure, the cacheStatus of each NSManagedObject remains unchanged so the save can be reattempted later. Update Groceries as follows to add record save support: 1. Add the code from Listing 15.8 to the bottom of the GENERAL section of CDCloudSync.swift.

Uploading New Records and Relationships In preparation for synchronization logic, new functions are required that upload new records and their relationships. Listing 15.9 shows the code involved. Listing 15.9 Uploading Records (CDCloudSync.swift) Click here to view code image // MARK: - INSERTS func uploadNewRecordsForEntities(entities:[String], context:NSManagedObjectContext, database:CKDatabase, completion:() -> Void) { // Prepare new records to insert var recordsToInsert = [CKRecord:NSManagedObject]() let predicate = NSPredicate(format: “cacheStatus == %i OR cacheStatus == %i”, NEW, INSERTING) for entityName in entities { if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: predicate, sort: nil) as? [NSManagedObject] {

// Set Cache Status to INSERTING for given records for object in objects { self.setCacheStatusForObject(object, requestedStatus: INSERTING) let record = self.createRecordWithObject(object) recordsToInsert[record] = object } // Ensure objects have a permanentID, which ensures that references to these objects via relationships work properly do { try context.obtainPermanentIDsForObjects(objects) } catch { NSLog(“ERROR obtaining permanentIDsForObjects in %@”,__FUNCTION__) } CDHelper.save(context) } } if recordsToInsert.isEmpty {NSLog(“No new objects need uploading.”);completion();return} self.saveMappedRecords(recordsToInsert, context: context, database: database, successStatus: RELATING, fault: false) { (savedMappedRecords) -> Void in if let _savedMappedRecords = savedMappedRecords { self.uploadReferencesForRecords(_savedMappedRecords, context: context, database: database, completion: { () -> Void in completion() }) } else { NSLog(“FAILED to insert records, setting the cacheStatus back to New”) for (_, object) in recordsToInsert { self.setCacheStatusForObject(object, requestedStatus: NEW) } completion() } } } func uploadReferencesForRecords (records:[CKRecord:NSManagedObject], context:NSManagedObjectContext, database:CKDatabase, completion:() -> Void) { var recordsToSave = [CKRecord:NSManagedObject]() // Upload references for (record, object) in records { let updatedRecord = self.updateReferenceForRecord(record, withObject: object) recordsToSave[updatedRecord] = object } self.saveMappedRecords(recordsToSave, context: context, database: database, successStatus: SYNCHRONIZED, fault: true) { (savedMappedRecords) -> Void in CDHelper.save(context) completion()

} }

The uploadNewRecordsForEntities function iterates through each entity, looking for objects with a cacheStatus of NEW. Matching objects are put in a CKRecord and NSManagedObject dictionary. The first step to uploading a new object is to set its cache status to INSERTING. This prevents duplicate objects from being inserted in cases where synchronization is triggered in close succession to a previous synchronization. Objects are also ensured to have a permanent Core Data objectID to prevent issues with establishing relationships later on. If no new objects need to be inserted, the function returns before attempting to save anything to iCloud, because this would be unnecessary. When there are records to insert, the existing savedMappedRecords function is leveraged. Once this function completes, the saved records are passed on to the next step in the process, which is to establish relationships with uploadReferencesForRecords. If the save fails, the cacheStatus is downgraded to NEW again so it may be reattempted later. The uploadReferencesForRecords function iterates the given dictionary and calls updateReferenceForRecord for each record found within. When there are records to update, the existing savedMappedRecords function is leveraged again. The completion block is called at the end, which is used later to trigger the next step in the synchronization process. Update Groceries as follows to add record upload support: 1. Add the code from Listing 15.9 to the bottom of CDCloudSync.swift before the last curly brace.

Adding Synchronization Logic The first part of the synchronization logic is almost ready to be implemented. In preparation, a new TRACKING section is added to record the last successful synchronization time. This also is used to prevent frequent synchronization attempts, which could be triggered by the user once all the required code is in place. Listing 15.10 shows the code involved. Listing 15.10 Date Tracking (CDCloudSync.swift) Click here to view code image // MARK: - TRACKING func setDateKey(key:String, date:NSDate) { let defaults = NSUserDefaults.standardUserDefaults() defaults.setObject(date, forKey: key) NSLog(“%@ has been set to %@”, key, date) } func dateKey (key:String) -> NSDate { let defaults = NSUserDefaults.standardUserDefaults()

if let object = defaults.objectForKey(key) as? NSDate { return object } let object = NSDate(timeIntervalSince1970: 0) // Default defaults.setObject(object, forKey: key) return object }

The setDateKey function stores a given NSDate under the given key within the standard user defaults. This ensures that the last synchronization date is saved even if the application is terminated completely. Likewise, the dateKey facilitates the retrieval of a given NSDate key. In cases where no NSDate has been stored, the dateKey function returns a default date of 1970. Update Groceries as follows to add date tracking support: 1. Add the code from Listing 15.10 to the bottom of CDCloudSync.swift before the last curly brace. The foundation is now in place for the first part of the synchronization logic to be implemented. Listing 15.11 shows the code involved. Listing 15.11 Synchronization Logic (CDCloudSync.swift) Click here to view code image // MARK: - SYNC func syncEntities (entities:[String], database:CKDatabase, context:NSManagedObjectContext) { if CDHelper.shared.iCloudAccountIsSignedIn() == false { NSLog(“\n\nSKIPPED SYNC - iCloud Account is NOT signed in.”) self.syncMessage(“Groceries”) return } // Limit Sync calls to once per 10 seconds. let syncLockout = dateKey(“SYNC LOCKOUT”) if syncLockout.timeIntervalSince1970 > NSDate().timeIntervalSince1970 { NSLog(“\n\nSKIPPED SYNC - Sync was performed within the last 10 seconds”) self.syncMessage(“Groceries”) return } self.setDateKey(“SYNC LOCKOUT”, date: NSDate(timeIntervalSinceNow: 10)) // Sync context.performBlock { () -> Void in NSLog(“–––––-STARTED uploadNewRecordsForEntities–––––-“) self.syncMessage(“Uploading New Items”) self.uploadNewRecordsForEntities(entities, context: context, database: database, completion: { () -> Void in NSLog(“–––––-FINISHED uploadNewRecordsForEntities–––––-“) self.cacheStatusForAllObjects(entities, context: context) self.syncMessage(“Groceries”) }) } }

The syncMessage function is responsible for sending a notification, which is used to display synchronization messages on the PrepareTVC and ShopTVC table view navigation item titles. These messages are dispatched on the main thread to ensure that the message gets through to the user interface in a timely manner. The syncEntities function first checks that an iCloud account is signed in. So long as one is, the sync lockout logic takes note of the current time and ensures that another synchronization cannot be triggered for at least 10 seconds. Finally, the synchronization logic calls uploadNewRecordsForEntities within a block. Once this function completes, the cache status for all objects and entities is displayed and a syncMessage is sent. This function is updated in the next chapter to upload NSManagedObject changes, download CKRecord changes, and delete CKRecords based on deleted NSManagedObject objects. Update Groceries as follows to add the initial synchronization logic: 1. Add the code from Listing 15.11 to the bottom of CDCloudSync.swift before the last curly brace. The initial synchronization logic is now ready, so the existing views need to be updated to use it. Listing 15.12 shows the code involved. Listing 15.12 Synchronization Functions (PrepareTVC.swift and ShopTVC.swift) Click here to view code image func sync () { let defaultContainer = CKContainer.defaultContainer() let publicDatabase = defaultContainer.publicCloudDatabase CDCloudSync.shared.syncEntities([“Item”,“Item_Photo”,“LocationAtHome”, “LocationAtShop”,“Unit”], database: publicDatabase, context: CDHelper.shared.importContext) } func syncMessage (note:NSNotification) { if let title = note.object as? String { self.navigationItem.title = title if let refreshControl = self.refreshControl { refreshControl.endRefreshing() self.performFetch() } } }

The sync function specifies that the public database and the default container should be used. With this information it triggers the syncEntities function for all entities using the shared import context. It’s important to use the import context, which runs on a background thread, so that the user interface remains responsive at all times. The syncMessage function is positioned to change the navigation item title whenever a syncMessage is received. Of course, the table view needs to be configured to observe

these notifications. In addition, the standard refreshControl variable of the table view controller is used to facilitate a swipe-down-to-sync feature. The code involved in observing notifications and enabling the refreshControl variable is shown in Listing 15.13. Listing 15.13 Displaying Synchronization Progress (PrepareTVC.swift and ShopTVC.swift) Click here to view code image // CloudSync NSNotificationCenter.defaultCenter().addObserver(self, selector: “syncMessage:”, name: “syncMessage”, object: nil) self.refreshControl = UIRefreshControl() if let refreshControl = self.refreshControl { refreshControl.addTarget(self, action: Selector(“sync”), forControlEvents: UIControlEvents.ValueChanged) }

Update Groceries as follows to prepare for the initial synchronization logic: 1. Add import CloudKit to the top of PrepareTVC.swift and ShopTVC.swift. 2. Add the code from Listing 15.12 to the bottom of the CLOUDSYNC section of PrepareTVC.swift and ShopTVC.swift before the last curly brace. 3. Add the code from Listing 15.13 to the bottom of the viewDidLoad function of PrepareTVC.swift. 4. Copy the viewDidLoad function from PrepareTVC.swift to the equivalent place in ShopTVC.swift. 5. Remove self.configureSearch() from the viewDidLoad function of ShopTVC.swift.

Preparing the Managed Object Model for Sync The code is now ready and the Core Data model needs to be updated to support it. This involves adding a new model version and four new attributes to each entity. Update Groceries as follows to implement Model 10: 1. Select Model.xcdatamodeld. 2. Click Editor > Add Model Version… and create a new model called Model 10 based on Model 9. 3. Set Model 10 as the current model. 4. Ensure Model 10.xcdatamodel is selected. 5. Add the following attributes to the Item entity; then copy them to the Item_Photo, Unit, and Location entities (don’t copy the new attributes to LocationAtHome or LocationAtShop):

An Integer 16 attribute called cacheStatus String attributes called cacheID, group, and groupKey 6. Copy the variables from Listing 15.14 into Item+CoreDataProperties.swift, Item_Photo+CoreDataProperties.swift, Unit+CoreDataProperties.swift, and Location+CoreDataProperties.swift. Listing 15.14 Synchronization Variables (Item.swift+CoreDataProperties.swift, Item_Photo+CoreDataProperties.swift, Unit+CoreDataProperties.swift, and Location+CoreDataProperties.swift) Click here to view code image @NSManaged var group: String? @NSManaged var groupKey: String? @NSManaged var cacheID: String? @NSManaged var cacheStatus: NSNumber?

The application is just about ready yet needs some last minute updates before it can be tested. The console log can become crowded, so now is a good opportunity to reduce some of that code. The group and groupKey information must also be added to new objects so they are saved to the appropriate group. Update Groceries as follows to add the finishing touches before testing: 1. Add CDCloudSync.addGroupInfo(object) to the Add Item Segue closure of the prepareForSegue function of PrepareTVC.swift on the line before itemVC.segueObject = object. 2. Replace the code within the Add Object Segue closure of the prepareForSegue function of UnitsTVC.swift with the UnitsTVC code from Listing 15.15. 3. Replace the code within the Add Object Segue closure of the prepareForSegue function of LocationsAtHomeTVC.swift with the LocationsAtHomeTVC code from Listing 15.15. 4. Replace the code within the Add Object Segue closure of the prepareForSegue function of LocationsAtShopTVC.swift with the LocationsAtShopTVC code from Listing 15.15. 5. Update the imagePickerController:didFinishPickingImage function of ItemVC by adding the ItemVC code from Listing 15.15 to the line before itemPhoto.data = photoData. 6. Add CDCloudSync.addGroupInfo(newLocation) to the

ensureHomeLocationIsNotNil function of Item.swift on the line before newLocation.storedIn = value. 7. Add CDCloudSync.addGroupInfo(newLocation) to the ensureShopLocationIsNotNil function of Item.swift on the line before newLocation.aisle = value. Listing 15.15 Finishing Touches (UnitsTVC.swift, LocationsAtHome.swift, LocationsAtShop.swift, and ItemVC.swift) Click here to view code image // UnitsTVC let object = CDOperation.insertUniqueObject(“Unit”, context: CDHelper.shared.context, uniqueAttributes: [“name”:””], additionalAttributes: nil) CDCloudSync.addGroupInfo(object) unitVC.segueObject = object // LocationsAtHomeTVC let object = CDOperation.insertUniqueObject(“LocationAtHome”, context: CDHelper.shared.context, uniqueAttributes: [“storedIn”:””], additionalAttributes: nil) CDCloudSync.addGroupInfo(object) locationAtHomeVC.segueObject = object //LocationsAtShopTVC let object = CDOperation.insertUniqueObject(“LocationAtShop”, context: CDHelper.shared.context, uniqueAttributes: [“aisle”:””], additionalAttributes: nil) CDCloudSync.addGroupInfo(object) locationAtShopVC.segueObject = object // ItemVC itemPhoto.modified = NSDate() CDCloudSync.addGroupInfo(itemPhoto)

You can now run the application to install it. Once it launches set a Shopping List Name and Password; then tap Save. Create some test items and when you’re ready to upload to iCloud make sure you have a good Internet connection. Then swipe down on either the PrepareTVC or ShopTVC table views and examine the console log. Figure 15.5 shows a partially successful result, which you may encounter depending on Apple’s current defaults for metadata and object security. At the time of writing, changes to default metadata and security levels were required.

Figure 15.5 Uploading from Core Data to iCloud with CloudKit

While the creation of the new objects succeeds, you may encounter some errors due to restrictive schema permissions or because certain fields are not enabled for query. To resolve this, custom metadata settings and schema permissions need to be set in the CloudKit dashboard. Update the Groceries metadata settings and schema permissions as follows: 1. Click the CloudKit Dashboard button in the iCloud section of the Capabilities tab of the application target. If you can’t find this button, navigate to https://icloud.developer.apple.com/dashboard/. 2. Ensure that the Groceries application is selected in the top left corner of the CloudKit dashboard. 3. Click Record Types. This reveals a list of record types equivalent to the entities that have been uploaded. 4. Update the Security as shown on the right in Figure 15.6 for each Record Type except Users. Don’t forget to click Save after editing each record type.

Figure 15.6 Custom record type security 5. Expand the Metadata Indexes shown in the center of Figure 15.6 to ensure the Record ID field is enabled for query for each Record Type except Users. Don’t forget to click Save after editing each record type. 6. Click Public Data > Default Zone and Add ID Query Index for each record type. 7. Delete all objects for each record type. You can change the record type using the drop-down list located at the top left of the blue area. So far there are no measures in place to retry an upload for an object stuck in a RELATING state. The application and objects need to be deleted in preparation for retesting. Retest Groceries as follows: 1. Stop and delete the Groceries application from your device or the iOS Simulator and reinstall it by running it again from Xcode. 2. Run Groceries again from Xcode to reinstall it and create some test items after setting an appropriate shopping list name and password. 3. Swipe down on the PrepareTVC or ShopTVC table view to trigger synchronization. Figure 15.7 shows the expected result, where all new items have a SYNCHRONIZED

cache status. Note that it is normal to receive the “Object cannot move directly from NEW to CHANGED” warning as new items are created. This warning is just a safeguard in place to ensure that new items are created in iCloud instead of being updated.

Figure 15.7 Core Data objects uploaded to iCloud with CloudKit

Summary Congratulations, you’re now half way toward achieving synchronization of public data with CloudKit! Now that the upload piece is ready, all new NSManagedObject objects are uploaded whenever synchronization is triggered. This chapter showed how NSManagedObject objects are mapped to CKRecord objects, and how relationships become CKReference objects. It also showed how to batch saves using CKModifyRecordsOperation, which is the recommended approach to minimize network calls. The creation of the cloud schema was trivial due to its just-in-time nature. Make sure you run through the exercises before moving to the next chapter to ensure that the schema is fully prepared.

Exercises Why not build on what you’ve learned by experimenting? 1. Run Groceries on a device and create a new item with a new photo and a new unit. Return to the Prepare tab and swipe down to trigger synchronization. This should create a new Item_Photo record type in CloudKit. 2. Refresh the CloudKit Dashboard and then ensure the security of each record type except User is set according to Figure 15.6. 3. Click Public Data > Default Zone and examine the records for each record type, adding query indices where necessary. Take note of how they correspond to their equivalent NSManagedObject.

16. CloudKit Sync: Downloading Changes and Handling Deletions Information is not knowledge. Albert Einstein In Chapter 15, “CloudKit Sync: Uploading Objects,” the capability to upload Core Data managed objects into iCloud using CloudKit was added. This chapter completes the circle as the capability to download changed CloudKit records is added. Support for handling deleted objects is implemented too.

CloudKit Building Blocks (Continued) Similar to the previous chapter, there are several disparate functions intended for CDCloudSync.swift that won’t be brought together until the end of the chapter. To begin with, four base functions are added to the existing GENERAL section. The first function, fetchRecordWithCacheID, is shown in Listing 16.1. Listing 16.1 Fetch Record with CacheID (CDCloudSync.swift) Click here to view code image func fetchRecordWithCacheID(cacheID:String, database:CKDatabase, completion: (fetchedRecord: CKRecord?, recordNotFound:Bool) -> Void) { let recordID = CKRecordID(recordName: cacheID) database.fetchRecordWithID(recordID, completionHandler: { (record, error) -> Void in if let _error = error { if let reason = _error.userInfo[“ErrorDescription”] as? String { if reason == “Record not found” { completion(fetchedRecord: nil, recordNotFound: true) // Fail, record does not exist return } } NSLog(“RETRY fetching record ‘%@’ later %@”, recordID.recordName, _error) completion(fetchedRecord: nil, recordNotFound: false) // Fail, probably network related } else { completion(fetchedRecord: record, recordNotFound: false) // Success } }) }

The fetchRecordWithCacheID function is used to fetch a record from the given CloudKit database using the given cacheID. The cacheID is first turned into a CKRecordID so that a call to fetchRecordWithID can be made against the given database. The fetchRecordWithID completion handler returns an NSError or

CKRecord, which triggers one of the following responses: If the NSError is not nil, it is checked to see whether the required record was not found. If so, the completion block returns a nil record and a recordNotFound value of true. If the NSError returns any other reason, for example a network failure, the completion block returns a nil record and a recordNotFound value of false. If the NSError is nil, it is expected that a CKRecord was found. This is subsequently passed on through the completion block. Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter15.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name. Update Groceries as follows to implement fetchRecordWithCacheID: 1. Add the code from Listing 16.1 to the bottom of the GENERAL section of CDCloudSync.swift. The next base function, deleteRecordWithObject, is shown in Listing 16.2. Listing 16.2 Delete Record with Object (CDCloudSync.swift) Click here to view code image func deleteRecordWithObject (object:NSManagedObject, database:CKDatabase) { if let context = object.managedObjectContext { if let cacheID = object.valueForKey(“cacheID”) as? String { let recordID = CKRecordID(recordName: cacheID) database.deleteRecordWithID(recordID, completionHandler: { (deletedRecordID, error) -> Void in if let _error = error { if let reason = _error.userInfo[“ErrorDescription”] as? String { if reason == “Record not found” { NSLog(“DELETED object %@. An associated record was not found in iCloud”, self.friendlyObjectName(object)) context.deleteObject(object) CDHelper.save(context) return } } NSLog(“ERROR deleting record %@ - %@”,cacheID, _error) } else {

NSLog(“DELETED object %@ and the associated record in iCloud”, self.friendlyObjectName(object)) context.deleteObject(object) CDHelper.save(context) return } }) } else { NSLog(“DELETED object %@”, __FUNCTION__) context.deleteObject(object) CDHelper.save(context) } } else {NSLog(“ERROR getting context in %@”,__FUNCTION__)} }

The deleteRecordWithObject function is used to delete a CloudKit record based on a given managed object and CloudKit database. In preparation, the managed object context is first drawn from the managed object. If no context is found, an error is written to the console log. Provided a context is found, the next thing needed from the managed object is its cacheID. If the object has no cacheID, the object is assumed to not exist in iCloud and is therefore deleted immediately. Otherwise, the cacheID is used to build a CKRecordID. The CKRecordID is used to call deleteRecordWithID against the database, which attempts to delete the CloudKit record. If the record is or is not found in iCloud, the associated managed object is deleted. If the deletion fails for another reason, for example, if the Internet connection is offline, the associated managed object is not deleted. In this case the deletion can be reattempted during a future synchronization. Update Groceries as follows to implement deleteRecordWithObject: 1. Add the code from Listing 16.2 to the bottom of the GENERAL section of CDCloudSync.swift. The next base function, createRelationshipFromReference, is shown in Listing 16.3. Listing 16.3 Create Relationship from Reference (CDCloudSync.swift) Click here to view code image func createRelationshipFromReference (reference:CKReference, database:CKDatabase, object:NSManagedObject, relationshipName:String) { if let moc = object.managedObjectContext { let destinationCacheID = reference.recordID.recordName let uniqueAttributes = [“cacheID”:destinationCacheID] let relationshipDescriptions = object.entity.relationshipsByName // Get the appropriate record type or target entity for (relationship, description) in relationshipDescriptions { if let targetEntityName = description.destinationEntity?.name { if relationshipName == description.name { if let targetObject = CDOperation.uniqueObjectWithAttributeValuesForEntity(targetEntityName,

context: moc, uniqueAttributes: uniqueAttributes) { NSLog(“CONNECTED %@ relationship from %@ to %@”, relationship, friendlyObjectName(object), friendlyObjectName(targetObject)) object.setValue(targetObject, forKey: relationshipName) } else { NSLog(“WARNING: Can’t create a relationship to a targetObject that does not exist!”) } } } } } else {NSLog(“%@ failed to get moc”,__FUNCTION__)} }

The createRelationshipFromReference function is used to establish a relationship from a given managed object to another managed object, based on the given CKReference. In preparation, the managed object context and entity name are first drawn from the given managed object. In addition, the relationship destination object’s cacheID is drawn from the given CKReference. With this information, the next step is to iterate the relationships of the given managed object to find the intended relationship target object. Once found, a relationship is established to this object using setValue:forKey. If no relationship target object is found, the relationship will not be created. Update Groceries as follows to implement createRelationshipFromReference: 1. Add the code from Listing 16.3 to the bottom of the GENERAL section of CDCloudSync.swift. The next base function, createOrUpdateObjectWithRecord, is shown in Listing 16.4. Listing 16.4 Create or Update Object with Record (CDCloudSync.swift) Click here to view code image func createOrUpdateObjectWithRecord (record:CKRecord, database:CKDatabase, context:NSManagedObjectContext, attributes:Bool) -> NSManagedObject { let cacheID = record.recordID.recordName let entityName = record.recordType let uniqueAttributes = [“cacheID”:cacheID] // Find or insert unique object (insertUniqueObject handles both scenarios) let object = CDOperation.insertUniqueObject(entityName, context: context, uniqueAttributes: uniqueAttributes, additionalAttributes: nil) // Nil out relationships missing from record var expectedRelationships = Set() for (relationship, description) in object.entity.relationshipsByName { if description.toMany == false { expectedRelationships.insert(relationship) } }

for relationship in record.allKeys() { expectedRelationships.remove(relationship) } for relationship in expectedRelationships { // NSLog(“Setting the %@ relationship to nil for %@”, relationship, self.friendlyObjectName(object)) object.setValue(nil, forKey: relationship) } // Set object attributes & relationships from CKRecord for key in record.allKeys() { if let reference = record.valueForKey(key) as? CKReference { // Set relationship if attributes == false { self.createRelationshipFromReference(reference, database:database, object:object, relationshipName:key) } } else { // Set attributes if attributes == true { object.setValue(record.valueForKey(key), forKey: key) } } } self.setCacheStatusForObject(object, requestedStatus: SYNCHRONIZED) return object }

The createOrUpdateObjectWithRecord function finds or creates a managed object based on a given CloudKit record. The given record will have no entries for nil relationships, so these must be deleted and set to nil manually. The function then updates the managed object according to the given attributes flag. The attributes flag determines whether the attributes or relationships are updated. Once the attributes or relationships have been updated, the cache status for the object is set to SYNCHRONIZED. Update Groceries as follows to implement createOrUpdateObjectWithRecord: 1. Add the code from Listing 16.4 to the bottom of the GENERAL section of CDCloudSync.swift.

Change Synchronization This section introduces code to detect server side changes. At time of writing, public CloudKit databases do not support sync semantics via CKFetchRecordChangesOperation. This means that the upcoming code changes instead rely on the modified attribute to detect changes. Listing 16.5 shows the code involved in finding the cacheID of all changed CloudKit records for a specific database. Listing 16.5 Server Changes Since Last Download (CDCloudSync.swift) Click here to view code image // MARK: - CHANGES func serverChangesSinceLastDownload (entities:[String], database:CKDatabase, completion:(changedCacheIDs:[String]) -> Void) {

var changedCacheIDs:[String] = [] var entitiesToProcess = entities.count let lastDownload = dateKey(“LAST DOWNLOAD FOR GROUP "\(self.group())":"\ (self.groupKey())"”) for entityName in entities { NSLog(“PREPARING to download changes for Entity [%@]”, entityName) let predicate = NSPredicate(format: “modified > %@ AND group == %@ AND groupKey == %@ AND cacheID != ””, lastDownload, self.group(), self.groupKey()) let query = CKQuery(recordType: entityName, predicate: predicate) let operation = CKQueryOperation(query: query) operation.qualityOfService = .UserInteractive operatio// MARK: - CHANGES var serverChanges = [String:Set]() func serverChangesOperation(queryOperation: CKQueryOperation, entityName:String, database:CKDatabase, completion:() -> Void) { queryOperation.recordFetchedBlock = { (record : CKRecord) -> Void in if let cacheID = record.objectForKey(“cacheID”) as? String { if var entityCacheIDs = self.serverChanges[entityName] { entityCacheIDs.insert(cacheID) self.serverChanges[entityName] = entityCacheIDs } else {print(“ERROR in \(__FUNCTION__) getting entityCacheIDs”)} } else {print(“ERROR in \(__FUNCTION__) getting cacheID”)} } queryOperation.queryCompletionBlock = { (cursor: CKQueryCursor?, error: NSError?) -> Void in if let queryCursor = cursor { NSLog(“WAITING for more %@ data…”, entityName) self.syncMessage(“Looking For Changes”) let queryCursorOperation = CKQueryOperation(cursor: queryCursor) self.serverChangesOperation(queryCursorOperation, entityName: entityName, database: database, completion: { () -> Void in }) } else { if let _error = error { NSLog(“ERROR in queryCompletionBlock: -> %@”, _error) } completion() } } database.addOperation(queryOperation) } func serverChangesSinceLastDownload (entities:[String], database:CKDatabase, completion:() -> Void) { // Reset self.serverChanges = [String:Set]() for entityName in entities { self.serverChanges[entityName] = Set() } let lastDownload = dateKey(“LAST DOWNLOAD FOR GROUP "\(self.group())":"\ (self.groupKey())"”) var entitiesToProcess = entities.count for entityName in entities { let predicate = NSPredicate(format: “modified > %@ AND group == %@ AND groupKey == %@ AND cacheID != ””, lastDownload, self.group(),

self.groupKey()) let query = CKQuery(recordType: entityName, predicate: predicate) let operation = CKQueryOperation(query: query) operation.qualityOfService = .UserInteractive operation.desiredKeys = [“cacheID”] operation.resultsLimit = CKQueryOperationMaximumResults self.serverChangesOperation(operation, entityName: entityName, database: database, completion: { () -> Void in entitiesToProcess— if entitiesToProcess == 0 {completion()} }) } }

The serverChangesSinceLastDownload function builds a serverChanges dictionary with an array of cacheIDs for each entity, which represents all changed records in the given CloudKit database. To build this dictionary, it first resets and instantiates the serverChanges dictionary before iterating through each entity. Each iteration fires a CKQuery to retrieve the cacheIDs of the changed objects, which are appended to an entity specific string array as they’re found. The CKQueryOperation is split out in to a separate serverChangesOperation function, so that it can be reused in cases where a CKQueryCursor is received in the query completion block. As each entity is processed, the entitiesToProcess variable reduces by one, so that the completion block can return when all iterations are complete. Update Groceries as follows to implement the CHANGES section: 1. Add the code from Listing 16.5 to CDCloudSync.swift after the existing INSERTS section. With a means to detect changes in place, a new function is added to download these changes. Listing 16.6 shows the code involved. Listing 16.6 Download Changes for Entities (CDCloudSync.swift) Click here to view code image func downloadChangesForEntities (entities:[String], database:CKDatabase, func downloadChangesForEntities (entities:[String], database:CKDatabase, context:NSManagedObjectContext, completion:() -> Void) { self.serverChangesSinceLastDownload(entities, database: database) { () -> Void in // Build changed RecordID array var changedRecordIDs:[CKRecordID] = [] for entityName in entities { if let changedCacheIDs = self.serverChanges[entityName] { for cacheID in changedCacheIDs { changedRecordIDs.append(CKRecordID(recordName: cacheID)) } } else {NSLog(“ERROR in %@ getting changes for %@”, __FUNCTION__, entityName)} } // Split download into batches to prevent “too many items” & “rate

limited” errors let batchSize = 300 let batchCountFloat:Float = Float(changedRecordIDs.count) / Float(batchSize) var batchCountInt:Int = Int(round(batchCountFloat)) if batchCountFloat > Float(batchCountInt) {batchCountInt++} // Round Up if batchCountInt == 0 {completion();return} var batches = [[CKRecordID]]() var rangeStart = 0, rangeEnd = 0 NSLog(“DOWNLOADING %i record(s) in %i batch(es) with a size of %i”, changedRecordIDs.count, batchCountInt, batchSize) for batchNumber in 1…batchCountInt { rangeStart = rangeEnd rangeEnd = rangeStart + batchSize if rangeEnd > changedRecordIDs.count {rangeEnd = changedRecordIDs.count} // Pull a subset of mapped records into the “batch” sized dictionary. let batchKeys = changedRecordIDs[rangeStart…(rangeEnd-1)] var batch:[CKRecordID] = [] for batchKey in batchKeys {batch.append(batchKey)} batches.append(batch) NSLog(“PROCESSING download batch %i range %i to %i”, batchNumber, rangeStart, rangeEnd - 1) } // Download batch var downloadedRecords = [CKRecord]() var batchesRemaining = batches.count for batch in batches { let operation = CKFetchRecordsOperation(recordIDs: batch) operation.qualityOfService = .UserInteractive operation.fetchRecordsCompletionBlock = { (recordDictionary, error) -> Void in if let _error = error { NSLog(“ERROR in %@ - %@”, __FUNCTION__, _error) } else if let _recordDictionary = recordDictionary { // Reconstitute batch for (_, record) in _recordDictionary { downloadedRecords.append(record) } } else {NSLog(“ERROR in %@ getting _recordDictionary”, __FUNCTION__)} batchesRemaining— if batchesRemaining == 0 { // Create/update attributes for record in downloadedRecords { self.createOrUpdateObjectWithRecord(record, database: database, context: context, attributes: true) } // Create/update relationships for record in downloadedRecords {

self.createOrUpdateObjectWithRecord(record, database: database, context: context, attributes: false) } // Update Last Download date for this group NSLog(“All download batches have been processed.”) self.setDateKey(“LAST DOWNLOAD FOR GROUP "\ (self.group())":"\(self.groupKey())"”, date: NSDate()) completion() } else {NSLog(“%i download batch(es) remaining”, batchesRemaining)} } database.addOperation(operation) } } }

The downloadChangesForEntities function first prepares a changed CKRecordID array based on the serverChanges dictionary built by serverChangesSinceLastDownload. This array is then split in to download batches, which prevents errors in cases where there are a lot of records to download. A CKFetchRecordsOperation is then fired for each batch, which builds an array of downloaded records. Once the download is complete, these records are passed on to createOrUpdateObjectWithRecord to create or update equivalent Core Data managed objects. The attributes are all set before the relationships. Finally, the last download date is updated and the completion block is called. Update Groceries as follows to implement downloadChangesForEntities: 1. Add the code from Listing 16.6 to the bottom of the CHANGES section of CDCloudSync.swift. With changed server side records taken care of, the next step is to ensure that local object changes are uploaded to iCloud. Listing 16.7 shows the code involved. Listing 16.7 Upload Changes for Entities (CDCloudSync.swift) Click here to view code image var updatedRecords = [String:[CKRecord:NSManagedObject]]() func updatedRecordsOperation(queryOperation: CKQueryOperation, entityName:String, context:NSManagedObjectContext, database:CKDatabase, completion:() -> Void) { queryOperation.recordFetchedBlock = { (record : CKRecord) -> Void in if var updatedRecords = self.updatedRecords[entityName] { let cacheID = record.recordID.recordName if let object = CDOperation.objectWithAttributeValueForEntity(entityName, context: context, attribute: “cacheID”, value: cacheID) { var updatedRecord = self.updateRecord(record, withObject: object) updatedRecord = self.updateReferenceForRecord(updatedRecord, withObject: object) updatedRecords[record] = object self.updatedRecords[entityName] = updatedRecords } else {NSLog(“ERROR in %@ getting object”, __FUNCTION__)}

} else {NSLog(“ERROR in %@ getting entityRecords”, __FUNCTION__)} } queryOperation.queryCompletionBlock = { (cursor: CKQueryCursor?, error: NSError?) -> Void in if let queryCursor = cursor { NSLog(“WAITING for more %@ data…”, entityName) self.syncMessage(“Processing Changes…”) let queryCursorOperation = CKQueryOperation(cursor: queryCursor) self.updatedRecordsOperation(queryCursorOperation, entityName: entityName, context: context, database: database, completion: { () -> Void in }) } else { if let _error = error { NSLog(“ERROR in queryCompletionBlock: -> %@”, _error) } completion() } } database.addOperation(queryOperation) } func uploadChangesForEntities (entities:[String], database:CKDatabase, context:NSManagedObjectContext, completion:() -> Void) { // Reset self.updatedRecords = [String:[CKRecord:NSManagedObject]]() for entityName in entities { self.updatedRecords[entityName] = [CKRecord:NSManagedObject]() } var entitiesToProcess = entities.count for entityName in entities { let predicate = NSPredicate(format: “cacheStatus == %i AND group == %@ AND groupKey == %@”, CHANGED, self.group(), self.groupKey()) if let changedObjects = CDOperation.objectsForEntity(entityName, context: context, filter: predicate, sort: nil) as? [NSManagedObject] { // Create changed object cacheID array var cacheIDs = [String]() for changedObject in changedObjects { if let cacheID = changedObject.valueForKey(“cacheID”) as? String { // Prevent upload of more than 100 changes at once to work around server rejection of “IN” predicate “too many items”. Additional changes will need to be uploaded later. if cacheIDs.count < 100 {cacheIDs.append(cacheID)} } else {print(“ERROR in \(__FUNCTION__) getting cacheID”)} } // Log intended changes to console if changedObjects.count == 0 { NSLog(“No %@ changes need uploading.”, entityName) entitiesToProcess— if entitiesToProcess == 0 {completion()} } else { NSLog(“PREPARING to upload %i %@ changes”, changedObjects.count, entityName) // Update records let queryPredicate = NSPredicate(format: “cacheID IN %@”, cacheIDs) let query = CKQuery(recordType: entityName, predicate: queryPredicate)

let operation = CKQueryOperation(query: query) operation.qualityOfService = .UserInteractive operation.desiredKeys = [“cacheID”] operation.resultsLimit = CKQueryOperationMaximumResults self.updatedRecordsOperation(operation, entityName: entityName, context: context, database: database, completion: { () -> Void in if let updatedRecords = self.updatedRecords[entityName] { self.saveMappedRecords(updatedRecords, context: context, database: database, successStatus: SYNCHRONIZED, fault: false, completion: { (savedMappedRecords) -> Void in entitiesToProcess— NSLog(“There are %i entities left to process.”, entitiesToProcess) if entitiesToProcess == 0 {completion()} }) } else { NSLog(“ERROR in %@ getting updated %@ records”, __FUNCTION__, entityName) if entitiesToProcess == 0 {completion()} } }) } } else {NSLog(“ERROR in %@ getting changedObjects”,__FUNCTION__)} } }

The uploadChangesForEntities function detects changed managed objects by looking for a cache status of CHANGED. The cacheIDs of up to 100 changed objects are used to build a CKQueryOperation that fetches records requiring an update. It is necessary to limit the number of cacheIDs specified in the IN predicate, so that the query is not rejected by the server. This means synchronization needs to be run multiple times if more than 100 local changes need uploading. The CKQueryOperation is split out in to a separate updatedRecordsOperation function, so that it can be reused in cases where there are too many results and a CKQueryCursor is received in the query completion block. Each fetched record triggers the recordFetchedBlock, which adds the record to the updatedRecords dictionary after adding the local changes. Once all records are processed the updated records are saved back to the server using the existing saveMappedRecords function. Update Groceries as follows to ensure changed objects are uploaded: 1. Add the code from Listing 16.7 to the bottom of the CHANGES section of CDCloudSync.swift. 2. Add CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: CHANGED) as the first line within the selectedObject:changedForPickerTF function of ItemVC.swift. 3. Add CDCloudSync.shared.setCacheStatusForObject(item, requestedStatus: CHANGED) beneath item.modified = NSDate()

in the refreshInterface function of ItemVC.swift. 4. Add CDCloudSync.shared.setCacheStatusForObject(unit, requestedStatus: CHANGED) beneath unit.modified = NSDate() in the refreshInterface function of UnitVC.swift. 5. Add CDCloudSync.shared.setCacheStatusForObject(locationAtHome, requestedStatus: CHANGED) beneath locationAtHome.modified = NSDate() in the refreshInterface function of LocationAtHomeVC.swift. 6. Add CDCloudSync.shared.setCacheStatusForObject(locationAtShop, requestedStatus: CHANGED) beneath locationAtShop.modified = NSDate() in the refreshInterface function of LocationAtShopVC.swift.

Deletion Synchronization Now that insert and change tracking is in place, the next step is to handle deletions. When a user deletes an object from a device, the object receives a cacheStatus of DELETING. The table views and picker views are configured to filter out objects with this status. In addition, all objects with this status are processed for deletion as a part of synchronization. While it’s simple to delete objects from one device, the challenge comes with ensuring the same objects are deleted from all other devices. This can’t be picked up with the existing change tracking because the server side records do not exist after a deletion. The net effect is that there are orphaned objects on the other devices, which must be deleted by exception. Listing 16.8 shows the code involved. Listing 16.8 Delete Orphaned Cache Objects (CDCloudSync.swift) Click here to view code image // MARK: - DELETIONS var serverCacheIDs = [String:Set]() func existingServerCacheIDsOperation(queryOperation: CKQueryOperation, entityName:String, database:CKDatabase, completion:() -> Void) { queryOperation.recordFetchedBlock = { (record : CKRecord) -> Void in if let cacheID = record.objectForKey(“cacheID”) as? String { if var entityCacheIDs = self.serverCacheIDs[entityName] { entityCacheIDs.insert(cacheID) self.serverCacheIDs[entityName] = entityCacheIDs } else {print(“ERROR in \(__FUNCTION__) preparing entityCacheIDs”)} } else {print(“ERROR in \(__FUNCTION__) getting cacheID”)} } queryOperation.queryCompletionBlock = { (cursor: CKQueryCursor?, error: NSError?) -> Void in if let queryCursor = cursor {

NSLog(“WAITING for more %@ data…”, entityName) self.syncMessage(“Comparing Server Data”) let queryCursorOperation = CKQueryOperation(cursor: queryCursor) self.existingServerCacheIDsOperation(queryCursorOperation, entityName: entityName, database: database, completion: { () -> Void in }) } else { if let _error = error { NSLog(“ERROR in queryCompletionBlock: -> %@”, _error) } completion() } } database.addOperation(queryOperation) } func existingServerCacheIDs (entities:[String], database:CKDatabase, completion:() -> Void) { // Reset self.serverCacheIDs = [String:Set]() for entityName in entities { self.serverCacheIDs[entityName] = Set() } var entitiesToProcess = entities.count for entityName in entities { let predicate = NSPredicate(format: “group == %@ AND groupKey == %@”, self.group(), self.groupKey()) let query = CKQuery(recordType: entityName, predicate: predicate) let operation = CKQueryOperation(query: query) operation.qualityOfService = .UserInteractive operation.desiredKeys = [“cacheID”] operation.resultsLimit = CKQueryOperationMaximumResults self.existingServerCacheIDsOperation(operation, entityName: entityName, database: database, completion: { () -> Void in entitiesToProcess— if entitiesToProcess == 0 {completion()} }) } } func deleteOrphanedCacheObjects (entities:[String], database:CKDatabase, context:NSManagedObjectContext, completion:() -> Void) { self.existingServerCacheIDs(entities, database: database) { () -> Void in for entityName in entities { if let entityCacheIDs = self.serverCacheIDs[entityName] { let filter = NSPredicate(format: “(NOT(cacheID IN %@) AND (cacheStatus == %i OR cacheStatus == %i)) OR cacheStatus == %i OR group != %@ OR groupKey != %@”, entityCacheIDs, SYNCHRONIZED, CHANGED, DELETING, self.group(), self.groupKey()) if let objects = CDOperation.objectsForEntity(entityName, context: context, filter: filter, sort: nil) as? [NSManagedObject] { if objects.count > 0 { NSLog(“%i DELETING:”, objects.count) } else {

NSLog(“No orphaned %@ objects need deleting.”, entityName) } for object in objects { NSLog(“ —> %@”, self.friendlyObjectName(object)) self.deleteRecordWithObject(object, database: database) } CDHelper.save(context) } else {NSLog(“FAILED to get objects array”)} } else { NSLog(“ERROR in %@ getting entityCacheIDs”, __FUNCTION__)} } completion() } }

The deleteOrphanedCacheObjects function detects SYNCHRONIZED objects that have been deleted from the server by comparing the cacheIDs of all server objects with the cacheIDs of all local objects. Any cacheID found locally that is not found on the server is considered deleted from the server. To make this comparison the existingServerCacheIDs function downloads every cacheID for every object within the specified group. This download is one of the reasons why this solution is not scalable beyond a few thousand records. The CKQueryOperation in the existingServerCacheIDs function is split out in to a separate existingServerCacheIDsOperation function, so that it can be reused in cases where there are too many results and a CKQueryCursor is received in the query completion block. Update Groceries as follows to implement the DELETIONS section and ensure that items are deleted by changing their status: 1. Add the code from Listing 16.8 to CDCloudSync.swift after the existing CHANGES section. 2. Search the project for self.frc.managedObjectContext.deleteObject(object) and replace with CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: DELETING). 3. Search the project for moc.deleteObject(object) and replace with CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: DELETING). 4. Search the project for moc.deleteObject(lastObject) and replace with CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: DELETING). 5. Add the code from Listing 16.9 to the tableView:commitEditingStyle function of PrepareTVC.swift on the line after

CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: DELETING). This code ensures that photos are deleted from iCloud when a related item has been deleted. Listing 16.9 Set Cache Status (PrepareTVC.swift) Click here to view code image if let photoObject = object.valueForKey(“photo”) as? Item_Photo { CDCloudSync.shared.setCacheStatusForObject(photoObject, requestedStatus: DELETING) }

To hide objects tagged for deletion, all table views and picker views must be updated to filter out objects with a DELETING cache status. Listing 16.10 shows the code involved. Listing 16.10 Hide Deleted Objects (CDCloudSync.swift) Click here to view code image self.filter = NSPredicate(format: “group == %@ AND groupKey == %@ AND cacheStatus != %i”, CDCloudSync.shared.group(), CDCloudSync.shared.groupKey(), DELETING)

Update Groceries as follows to ensure deleted items are not visible on table views and picker views: 1. Add the code from Listing 16.10 beneath self.sort in the init function of PrepareTVC.swift, ShopTVC.swift, UnitsTVC.swift, LocationsAtHomeTVC.swift, and LocationsAtShopTVC.swift. 2. Add AND listed == 1 to the end of the self.filter predicate format string in the init function of ShopTVC.swift. This ensures that only listed items are shown on the Shop tab. (Be sure to leave a space after the %i before AND listed == 1.) 3. Comment out the if let template code near the bottom of the init function of ShopTVC.swift. The template does not have the appropriate predicate to filter out deleted items, so it won’t be used anymore. 4. Add the code from Listing 16.10 beneath self.sort in the init function of UnitPickerTF.swift, LocationAtHomePickerTF.swift, and LocationAtShopPickerTF.swift.

Quality Assurance The next section of code is required to ensure consistency between the data in iCloud and the data on the device. To achieve this, a comparison is made between cacheID’s in iCloud with those on the device. If any are missing on the device, then those records are downloaded. Listing 16.11 shows the code involved. Listing 16.11 Quality Assurance (CDCloudSync.swift)

Click here to view code image // MARK: - QUALITY ASSURANCE func existingLocalCacheIDs (entityName:String) -> Set { var _localCacheIDs = Set() let request = NSFetchRequest(entityName:entityName) request.includesPendingChanges = true request.propertiesToFetch = [“cacheID”] request.resultType = NSFetchRequestResultType.DictionaryResultType do { let fetchResults = try CDHelper.shared.parentContext.executeFetchRequest(request) if let dictionaryArray = fetchResults as? [Dictionary] { for dictionary:Dictionary in dictionaryArray { if let cacheID = dictionary[“cacheID”] as? String { _localCacheIDs.insert(cacheID) } else { NSLog(“ERROR in %@ getting cacheID”, __FUNCTION__)} } } else { NSLog(“ERROR in %@ getting dictionaryArray”, __FUNCTION__)} } catch {print(“\(__FUNCTION__) FAILED to fetch objects: \(error)”)} return _localCacheIDs } func missingLocalCacheIDs (entities:[String], database:CKDatabase, completion:(missingCacheIDs:Set) -> Void) { var allMissingCacheIDs = Set() var entitiesToProcess = entities.count for entityName in entities { if let entityCacheIDs = self.serverCacheIDs[entityName] { var missingEntityCacheIDs = entityCacheIDs for existingCacheID in self.existingLocalCacheIDs(entityName) { missingEntityCacheIDs.remove(existingCacheID) } for missingCacheID in missingEntityCacheIDs { allMissingCacheIDs.insert(missingCacheID) } } else { NSLog(“ERROR in %@ getting entityCacheIDs”, __FUNCTION__)} // Keep track of how many entities have been processed entitiesToProcess— if entitiesToProcess == 0 { if allMissingCacheIDs.count > 0 {NSLog(“FOUND cacheID’s in iCloud that are not on this device: %@”, allMissingCacheIDs.description)} completion(missingCacheIDs: allMissingCacheIDs) } } } func qualityCheckEntities (entities:[String], database:CKDatabase, context:NSManagedObjectContext, completion:() -> Void) { self.missingLocalCacheIDs(entities, database: database) { (missingCacheIDs) -> Void in NSLog(“There are %i objects on the server that are not on this device.”, missingCacheIDs.count) if missingCacheIDs.count > 0 {

// Build missing RecordID array var missingRecordIDs:[CKRecordID] = [] for cacheID in missingCacheIDs { missingRecordIDs.append(CKRecordID(recordName: cacheID)) } // Split download into batches to prevent “too many items” & “rate limited” errors let batchSize = 300 let batchCountFloat:Float = Float(missingRecordIDs.count) / Float(batchSize) var batchCountInt:Int = Int(round(batchCountFloat)) if batchCountFloat > Float(batchCountInt) {batchCountInt++} // Round Up if batchCountInt == 0 {completion();return} var batches = [[CKRecordID]]() var rangeStart = 0, rangeEnd = 0 NSLog(“DOWNLOADING %i missing record(s) in %i batch(es) with a size of %i”, missingRecordIDs.count, batchCountInt, batchSize) for batchNumber in 1…batchCountInt { rangeStart = rangeEnd rangeEnd = rangeStart + batchSize if rangeEnd > missingRecordIDs.count {rangeEnd = missingRecordIDs.count} // Pull a subset of mapped records into the “batch” sized dictionary. let batchKeys = missingRecordIDs[rangeStart…(rangeEnd-1)] var batch:[CKRecordID] = [] for batchKey in batchKeys {batch.append(batchKey)} batches.append(batch) NSLog(“PROCESSING download batch %i range %i to %i”, batchNumber, rangeStart, rangeEnd - 1) } // Download batch var downloadedRecords = [CKRecord]() var batchesRemaining = batches.count for batch in batches { let operation = CKFetchRecordsOperation(recordIDs: batch) operation.qualityOfService = .UserInteractive operation.fetchRecordsCompletionBlock = { (recordDictionary, error) ->Void in if let _error = error { NSLog(“ERROR in %@ - %@”, __FUNCTION__, _error) } else if let _recordDictionary = recordDictionary { // Reconstitute batch for (_, record) in _recordDictionary { downloadedRecords.append(record) } } else {NSLog(“ERROR in %@ getting _recordDictionary”, __FUNCTION__)} batchesRemaining— if batchesRemaining == 0 { // Create/update attributes for record in downloadedRecords {

self.createOrUpdateObjectWithRecord(record, database: database, context: context, attributes: true) } // Create/update relationships for record in downloadedRecords { self.createOrUpdateObjectWithRecord(record, database: database, context: context, attributes: false) } // Update Last Download date for this group NSLog(“All missing record download batches have been processed.”) completion() } else {NSLog(“There are %i download batch(es) remaining.”, batchesRemaining)} } database.addOperation(operation) } } else {completion()} } }

The existingLocalCacheIDs function fetches a string set of cacheID’s that are present on the local device. The missingLocalCacheIDs function leverages the existingLocalCacheIDs function and previously downloaded serverCacheIDs to build a string array of cacheID’s that are not present on the local device. When building this array, consideration is also given to the current group and groupKey, so that only data relevant to this user is checked. The qualityCheckEntities function leverages the missingLocalCacheIDs function to determine if any missing records need to be downloaded. If there are missing records, a CKFetchRecordsOperation batch is triggered to fetch these specific records. Update Groceries as follows to implement quality assurance: 1. Add the QUALITY ASSURANCE code from Listing 16.11 to CDCloudSync.swift just before the TRACKING section.

Testing the Network Before a sync is triggered, it’s a good idea to check that the network is available. This gives you a chance to inform the user that his or her data won’t be synchronized and to try again later. Listing 16.12 shows the code involved. Listing 16.12 Network Connection (CDCloudSync.swift) Click here to view code image // MARK: - NETWORK CONNECTION func connectedToNetwork () -> Bool { let connected = checkNetwork()

if connected == false { self.syncMessage(“Sync Skipped (Check Network)”) } return connected } func checkNetwork() -> Bool { var zeroAddress = sockaddr_in() zeroAddress.sin_len = UInt8(sizeofValue(zeroAddress)) zeroAddress.sin_family = sa_family_t(AF_INET) guard let defaultRouteReachability = withUnsafePointer(&zeroAddress, { SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0)) }) else { return false } var flags : SCNetworkReachabilityFlags = [] if SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) == false { return false } let isReachable = flags.contains(.Reachable) let needsConnection = flags.contains(.ConnectionRequired) return (isReachable && !needsConnection) }

The connectedToNetwork function is the first function call from syncEntities. If connectedToNetwork returns false, the synchronization is skipped and the user interface updated with a message to check the Internet connection. The checkNetwork function leverages the SystemConfiguration framework to test for connectivity. In your own projects this function could be updated to test for access to a specific URL. Update Groceries as follows to test for connectivity prior to synchronization: 1. Add the NETWORK CONNECTION section code from Listing 16.12 to CDCloudSync.swift just before the TRACKING section.

Updating Synchronization Logic The prerequisite code is now in place, so the synchronization logic is ready to be updated. The high level process of synchronization is as follows: 1. Check that synchronization is not already running. 2. Check that network connectivity is available. 3. Check that the iCloud account is signed in. 4. Check that a synchronization has not been triggered within the last 10 seconds. 5. Reset the background context used for synchronization. 6. Delete local objects with a synchronized or changed status that weren’t found on the server.

7. Upload new records. 8. Upload changes to existing records. 9. Download server side changes. 10. Perform a quality check to download objects missing from the device. 11. Report synchronization status of each object to the console log. 12. Perform clean up tasks such as de-duplication, creating thumbnails, and nil location checks. The code involved is shown in Listing 16.13. Listing 16.13 Sync Entities (CDCloudSync.swift) Click here to view code image // MARK: - SYNC var syncRunning = false func syncEntities (entities:[String], database:CKDatabase, context:NSManagedObjectContext) { // Check a sync is not already running if self.syncRunning { NSLog(“\n\nSKIPPED SYNC - Sync is already running”) return } // Check network before sync attempt if connectedToNetwork() == false { print(“CloudKit Sync skipped (network error)”) return } // Check iCloud ACcount if CDHelper.shared.iCloudAccountIsSignedIn() == false { NSLog(“\n\nSKIPPED SYNC - iCloud Account is NOT signed in.”) self.syncMessage(“Sync Skipped (Enable iCloud)”) return } // Limit Sync calls to once per 10 seconds. let syncLockout = dateKey(“SYNC LOCKOUT”) if syncLockout.timeIntervalSince1970 > NSDate().timeIntervalSince1970 { NSLog(“\n\nSKIPPED SYNC - Sync was performed within the last 10 seconds”) self.syncMessage(“Groceries”) return } self.setDateKey(“SYNC LOCKOUT”, date: NSDate(timeIntervalSinceNow: 10)) // Sync context.performBlock { context.reset() self.syncRunning = true NSLog(“– SYNC STARTED – “) self.syncMessage(“Starting Sync”) self.deleteOrphanedCacheObjects(entities, database: database, context: context, completion: {

self.syncMessage(“Uploading New Items”) self.uploadNewRecordsForEntities(entities, context: context, database: database, completion: { self.syncMessage(“Uploading Changes”) self.uploadChangesForEntities(entities, database: database, context: context, completion: { self.syncMessage(“Downloading Changes”) self.downloadChangesForEntities(entities, database: database, context: context, completion: { self.syncMessage(“Quality Checking”) self.qualityCheckEntities(entities, database: database, context: context, completion: { () -> Void in

NSLog(“– SYNC FINISHED –”) self.cacheStatusForAllObjects(entities, context: context) self.syncMessage(“Groceries”) self.syncRunning = false Item.ensureLocationsAreNotNilForAllItems() CDHelper.save(CDHelper.shared.importContext) CDDeduplicator.deDuplicateEntityWithName(“Item”, uniqueAttributeName: “name”, backgroundMoc: CDHelper.shared.importContext) CDDeduplicator.deDuplicateEntityWithName(“Unit”, uniqueAttributeName: “name”, backgroundMoc: CDHelper.shared.importContext) CDDeduplicator.deDuplicateEntityWithName(“LocationAtHome uniqueAttributeName: “storedIn”, backgroundMoc: CDHelper.shared.importContext) CDDeduplicator.deDuplicateEntityWithName(“LocationAtShop uniqueAttributeName: “aisle”, backgroundMoc: CDHelper.shared.importContext) CDThumbnailer.createMissingThumbnails(“Item”, thumbnailAttribute: “thumbnail”, photoRelationship: “photo”, photoAttribute: “data”, sort: nil, size:CGSizeMake(66, 66), moc: CDHelper.shared.importContext) }) }) }) }) }) } }

Each step of the updated syncEntities code makes use of completion blocks, which is due to the asynchronous nature of working with network data. This ensures a previous synchronization step is complete before beginning the next step. A sync message is sent as each step completes, so that the user can see the synchronization status in real time. Update Groceries as follows to finalize the synchronization logic: 1. Replace the SYNC section in CDCloudSync.swift with the code from Listing 16.13. 2. Run the application on a device and create some new items, units, locations, and photos. 3. Swipe down on the Prepare tab to trigger synchronization. Figure 16.1 shows the expected result.

Figure 16.1 Core Data objects synchronized to iCloud with CloudKit

Adding CDCloudSync to Your Own Applications The following procedure is an example of how to add CDCloudsync to your own applications. If you purchased this book, you are entitled to use this code in your own applications for free. 1. Create a new iOS > Application > Single View Application based on the Swift language called EZCloudKit. Ensure Use Core Data is not checked and that Devices is set to iPhone. 2. Link to the System Configuration Framework, the Core Data Framework, and the CloudKit Framework in the General tab of the application target. 3. Download and extract the Generic Core Data Classes folder from the following URL: http://timroadley.com/LCDwS/Generic%20Core%20Data%20Classes.zip. 4. Drag the Generic Core Data Classes folder into the EZCloudKit group in the EZCloudKit project. Ensure that Copy items if needed, Create groups, and the EZCloudKit target are selected before clicking Finish. 5. Add a Data Model as follows: Click File > New > File… and create an iOS > Core Data > Data Model. Ensure the EZCloudKit target and an appropriate Group are selected; then click Create to accept Model as the filename. 6. Configure Model.xcdatamodeld as follows: Add an entity called Test with the following attributes: modified date device string someValue string cacheID string group string groupKey string cacheStatus Integer 16 (with a default value of 0, which means NEW) 7. Create an NSManagedObject subclass for the Test entity. Ensure the EZCloudKit group and target are selected before clicking Create. 8. Click File > New > File….

9. Create an iOS > Source > Cocoa Touch Class that is a CDTableViewController subclass called TestTVC and then replace the code in TestTVC.swift with the code from Listing 16.14. Listing 16.14 Test Table View Controller (TestTVC.swift) Click here to view code image import UIKit import CoreData import CloudKit class TestTVC: CDTableViewController { // MARK: - CELL CONFIGURATION override func configureCell(cell: UITableViewCell, atIndexPath indexPath: NSIndexPath) { if let test = self.frc.objectAtIndexPath(indexPath) as? Test { if let textLabel = cell.textLabel, device = test.device { textLabel.text = device } if let detailTextLabel = cell.detailTextLabel, modified = test.modified, someValue = test.someValue { detailTextLabel.text = “\(modified) \(someValue)” } } } // MARK: - INITIALIZATION required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) // Set group and groupKey. It’s up to you how you achieve this in your application. A settings.bundle is a good approach. let defaults = NSUserDefaults.standardUserDefaults() defaults.setObject(“Testing”, forKey: “group”) defaults.setObject(“Testing”, forKey: “groupKey”) // CDTableViewController subclass customization self.entity = “Test” self.sort = [NSSortDescriptor(key: “someValue”, ascending: true)] self.filter = NSPredicate(format: “group == %@ AND groupKey == %@ AND cacheStatus !=%i”, CDCloudSync.shared.group(), CDCloudSync.shared.groupKey(), DELETING) self.fetchBatchSize = 25 } // MARK: - VIEW override func viewDidLoad() { super.viewDidLoad() self.navigationItem.title = “EZCloudKit” self.performFetch() // Enable CloudSync Swipe Down to Refresh NSNotificationCenter.defaultCenter().addObserver(self, selector: “syncMessage:”,name: “syncMessage”, object: nil)

self.refreshControl = UIRefreshControl() if let refreshControl = self.refreshControl { refreshControl.addTarget(self, action: Selector(“sync”), forControlEvents: UIControlEvents.ValueChanged) } } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) CDDeduplicator.deDuplicateEntityWithName(“Test”, uniqueAttributeName: “someValue”, backgroundMoc: CDHelper.shared.importContext) } // MARK: - INTERACTION @IBAction func add (sender: AnyObject) { if let object = NSEntityDescription.insertNewObjectForEntityForName(“Test”, inManagedObjectContext: CDHelper.shared.context) as? Test { object.device = UIDevice().name object.modified = NSDate() object.someValue = “Test: \(NSUUID().UUIDString)” object.group = “Testing” object.groupKey = “Testing” CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: NEW) CDHelper.saveSharedContext() } } // MARK: - DATA SOURCE: UITableView override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if (editingStyle == UITableViewCellEditingStyle.Delete) { if let object = self.frc.objectAtIndexPath(indexPath) as? NSManagedObject { CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: DELETING) CDHelper.saveSharedContext() } } } // MARK: - CLOUDSYNC func groupChanged () { let predicate = NSPredicate(format: “group == %@ AND groupKey == %@ AND cacheStatus != %i”, CDCloudSync.shared.group(), CDCloudSync.shared.groupKey(), DELETING) self.filter = predicate self.frc.fetchRequest.predicate = predicate self.performFetch() } func sync () { let defaultContainer = CKContainer.defaultContainer() let publicDatabase = defaultContainer.publicCloudDatabase CDCloudSync.shared.syncEntities([“Test”], database: publicDatabase, context: CDHelper.shared.importContext) } func syncMessage (note:NSNotification) { if let title = note.object as? String {

self.navigationItem.title = title if let refreshControl = self.refreshControl { refreshControl.endRefreshing() self.performFetch() } } } }

10. Replace the default view with a table view, as follows: Select Main.storyboard. Delete the existing View Controller. Drag a Table View Controller onto the storyboard and then click Editor > Embed In > Navigation Controller. Set the Navigation Controller as the Initial View Controller using Attributes Inspector (Option+ +4). Drag a Bar Button Item on to the top right of the Table View Controller. Set the System Item of the new Bar Button Item to Add using Attributes Inspector (Option+ +4). Select the Prototype Cell and then set its Style to Subtitle and its Identifier to Cell. Select the Table View Controller and set its Custom Class to TestTVC using Identity Inspector (Option+ +3). Hold down Control and drag a line from the Add button to the yellow circle at the top of the Table View Controller. Then select Sent Actions > add:. 11. Turn on the iCloud capability using the approach discussed in Chapter 15. Don’t forget to check iCloud Documents and CloudKit. 12. Add CDHelper.saveSharedContext() to bottom of the applicationDidEnterBackground and applicationWillTerminate functions of AppDelegate.swift. 13. Run the application and tap the + button to create test objects. Once you’re ready, swipe down to trigger synchronization. The expected result is shown in Figure 16.2.

Figure 16.2 Core Data objects from a sample application synchronized to iCloud with CloudKit For your convenience, the EZCloudKit project is available for download from the following URL: http://timroadley.com/LCDwS/EZCloudKit.zip. Here are some points to keep in mind when using CDCloudSync in your own applications: Trigger CloudKit sync with CDCloudSync.shared.syncEntities. Log in to the CloudKit dashboard and set appropriate security for your data: https://icloud.developer.apple.com/dashboard/ Whenever you change an NSManagedObject, call the following: CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: CHANGED) Whenever you delete an NSManagedObject, call the following: CDCloudSync.shared.setCacheStatusForObject(object, requestedStatus: DELETING) Filter out objects with a cacheStatus of DELETING. Filter in objects with the current group and groupKey. Refetch data if the current group or groupKey changes.

Summary This chapter showed how to synchronize public data with CloudKit. This has been quite a journey with a lot of prerequisite and utility code, all building up to the final synchronization logic. The code now detects server side changes and deletions, even without the use of CKFetchRecordChangesOperation, which at the time of writing cannot be used with public CloudKit databases. To learn how to add this functionality to your own applications, continue on to the exercises.

Exercises Why not build on what you’ve learned by experimenting? 1. Test synchronization between multiple devices. 2. Examine the contents of CloudKit as you continue to test synchronization between multiple devices. 3. Reset the development environment in the CloudKit dashboard via Admin > Deployment > Reset Development Environment.

Thank You! Thank you for taking the time to read my book! If you enjoyed it, consider posting some nice feedback on Twitter (@timroadley), Facebook, or the website where you purchased it. Positive feedback keeps me motivated to continue producing material like this. If you find a bug, please contact me at [email protected], or leave a forum post at timroadley.com. For the sake of completion, Appendix B, “Finalizing the Groceries Application,” covers the steps to implement the final App Store code. In addition to implementing several new features, it also resolves an interesting CDCloudSync de-duplication issue specific to Groceries. Cheers! —Tim Roadley

A. Preparing the Groceries Application This appendix details the steps required to build the Groceries application from scratch. Groceries is developed across the course of this book, and as each chapter introduces topics on Core Data, you expand Groceries to practice what you’ve learned. These instructions require at least Xcode 7 and iOS 9.

New Xcode Project The creation of the base Xcode project isn’t related to Core Data, so the instructions to build it are tucked away in this appendix. If you prefer, you may skip the creation of this project and begin Chapter 1, “Your First Core Data Application.” A link to the Xcode project created as a result of this appendix is available in Chapter 1. Create the initial Groceries Xcode project as follows: 1. Open Xcode and click Create a new Xcode project. 2. Choose the iOS > Application > Single View Application template and click Next. 3. Select the Swift and iPhone project options shown in Figure A.1, substituting Tim Roadley with your own developer name. Ensure that Use Core Data, Include Unit Tests, and Include UI Tests are not checked before clicking Next. 4. Select an appropriate directory for the new project. Ensure Create git repository is not checked before clicking Create.

Figure A.1 Project options

Storyboard Design The initial storyboard is a simple configuration made up of a View Controller and Table View Controller embedded in a Navigation Controller. Update Groceries as follows to create the initial storyboard: 1. Select Main.storyboard, which is located in the Xcode Group called Groceries. 2. Drag a Table View Controller from the Utilities pane onto the storyboard, placing it to the left of the existing View Controller. You can toggle the Utilities pane by pressing Option+ +0 together. 3. Ensure the new Table View Controller is selected. To get a high level overview of the storyboard you can zoom out by right-clicking the storyboard and selecting Zoom to 50%. 4. Check Is Initial View Controller, as shown in Figure A.2 near the bottom right. If you can’t see these settings, make sure the Utilities pane is open at the Attributes Inspector tab. You can do this by selecting View > Utilities > Show Attributes Inspector or by pressing Option+ +4 together.

Figure A.2 Setting the initial View Controller 5. Ensure the Table View Controller is selected and then click Editor > Embed In > Navigation Controller. 6. Drag a Bar Button Item on to the top right of the Table View Controller. You have to be zoomed to 100% for this to work. 7. Select the new Bar Button Item. 8. Set the System Item to Add in the Bar Button Item section of Attributes Inspector, as shown in Figure A.3.

Figure A.3 Setting a Bar Button Item identifier 9. Hold down Control and drag a line from the Add button to the center of the View Controller to the right and then select Action Segue > show. 10. Select the new segue and then set its Identifier to Add Item Segue using the Attributes Inspector, as shown in Figure A.4.

Figure A.4 Adding an Item Segue 11. Set the Title of the Table View Controller to Groceries by double-clicking the title area. 12. Select the Prototype Cell found in the Table View. 13. Set the Style of the Table View Cell to Basic and the Identifier to Cell using the Attributes Inspector, as shown in Figure A.5.

Figure A.5 Setting the Item Cell 14. Zoom to 50% and arrange the storyboard as shown in Figure A.6.

Figure A.6 The initial storyboard

App Icons and Launch Images Like any iOS app, Groceries needs icons and images to make it look unique. Update Groceries as follows to configure the appropriate app icons: 1. Download and extract http://www.timroadley.com/LCDwS/Icons_Images.zip. 2. Select Assets.xcassets, which is an Asset Catalog that contains the application images. 3. Select AppIcon. 4. Drag AppIcon_29_2x.png into the 2x 29pt placeholder. 5. Drag AppIcon_29_3x.png into the 3x 29pt placeholder. 6. Drag AppIcon_40_2x.png into the 2x 40pt placeholder. 7. Drag AppIcon_40_3x.png into the 3x 40pt placeholder. 8. Drag AppIcon_60_2x.png into the 2x 60pt placeholder. 9. Drag AppIcon_60_3x.png into the 3x 60pt placeholder. Figure A.7 shows the expected results.

Figure A.7 App icons Update Groceries as follows to configure the launch screen: 1. Drag the six LaunchScreen images from Finder into Assets.xcassets, beneath the AppIcon Image Set. 2. Select LaunchScreen.storyboard. 3. Drag an Image View onto the existing View Controller and then resize it to fill the entire view. 4. Select the View Controller and click Editor > Resolve Auto Layout Issues > All Views > Reset to Suggested Constraints. 5. Set the Image of the Image View to LaunchScreen-Background. 6. Drag another Image View onto the existing Image View and resize it to the same size as the background. 7. Set the Image of the new Image View to LaunchScreen. 8. Set the View Mode of the new Image View to Aspect Fit. 9. Select the View Controller and click Editor > Resolve Auto Layout Issues > All Views > Reset to Suggested Constraints. Figure A.8 shows the expected results.

Figure A.8 Launch Screen

Introducing GenericVC The Groceries application has several views that all need common functionality. For example, when the user touches the background the keyboard should be hidden. The GenericVC.swift class is the superclass to provide this common functionality. Listing A.1 shows code intended for GenericVC.swift. Listing A.1 Generic View Controller (GenericVC.swift) Click here to view code image import UIKit class GenericVC: UIViewController { func hideKeyboard () { self.view.endEditing(true) } func hideKeyboardWhenBackgroundIsTapped () { let tgr = UITapGestureRecognizer(target: self, action:Selector(“hideKeyboard”)) self.view.addGestureRecognizer(tgr) } }

Update Groceries as follows to create the GenericVC.swift class in a new Xcode group: 1. Right-click the Groceries group in Xcode and then create a new group called Generic Classes, as shown in Figure A.9.

Figure A.9 Xcode group for generic classes

2. Select the new Generic Classes group. 3. Click File > New > File…. 4. Create a new iOS > Source > Swift File and then click Next. 5. Set the filename to GenericVC and ensure the Groceries target is checked. 6. Ensure the Groceries project directory is open and then click Create. 7. Replace the contents of GenericVC.swift with the code from Listing A.1 The sample project is now ready for Chapter 1. Feel free to run Groceries on the iOS Simulator or an iOS device to examine your handiwork! Note that the deployment target is iOS 9 and previous versions of iOS aren’t supported with Groceries.

B. Finalizing the Groceries Application This appendix details the steps required to finalize the Groceries sample application so it matches the version on the App Store. The finalization of Groceries isn’t related to Core Data, so the instructions to build it are tucked away in this appendix.

New Features Currently users have limited options in relation to item photos. They cannot delete an existing photo, nor can they choose a new photo from the photo library. This appendix enhances the camera button on the item edit view so that it displays an action sheet from an alert controller with options to delete, take, or choose a photo. Another new feature allows you to set items as favorites. All favorite items can be added to the shopping list at the touch of a button. This gives a kick-start to the shopping list each week. The final new feature is an application badge icon. This is updated to show how many items are on the shopping list when the application enters the background. Thanks to my wife, Tracey, for suggesting and testing these improvements! Note To continue building the sample application, you need to have added the previous chapter’s code to Groceries. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LCDwS/Groceries-AfterChapter16.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to hold down option and click Product > Clean Build Folder. This practice ensures there’s no residual cache from previous projects using the same name.

Photo Library and Photo Deletion Support The code required to enhance the options regarding item photos is shown in Listing B.1. Listing B.1 Updated Camera Functionality (ItemVC.swift) Click here to view code image @IBAction func showCamera () { let sheet = UIAlertController(title:nil, message:nil, preferredStyle:.ActionSheet)

// TAKE BUTTON if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Came { let takePhoto = UIAlertAction(title: “Take”, style: .Default, handler: { (action) -> Void in

self.camera = UIImagePickerController() if let _camera = self.camera {

_camera.sourceType = UIImagePickerControllerSourceType.Camera _camera.mediaTypes = UIImagePickerController.availableMediaTypesForSourceType(UIImagePickerControllerSour _camera.allowsEditing = true _camera.delegate = self if let nc = self.navigationController { nc.presentViewController(_camera, animated: true, completion: nil) } else {print(“Failed to prepare the camera (navigation controller issue)”)} } else {print(“Failed to prepare the camera”)} }) sheet.addAction(takePhoto) } // CHOOSE BUTTON let choosePhoto = UIAlertAction(title: “Choose”, style: .Default) { (action) -> Void in

if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Phot == true { self.camera = UIImagePickerController() if let _camera = self.camera {

_camera.sourceType = UIImagePickerControllerSourceType.PhotoLibrary if let mediaTypes = UIImagePickerController.availableMediaTypesForSourceType(UIImagePickerControllerSour { _camera.mediaTypes = mediaTypes } _camera.allowsEditing = true _camera.delegate = self if let nc = self.navigationController { nc.presentViewController(_camera, animated: true, completion: nil) } else {print(“Failed to prepare the Photo Library (navigation controller issue)”)} } else {print(“Failed to prepare the Photo Library”)} } else {print(“Photo Library is not available”)} } sheet.addAction(choosePhoto) // DELETE BUTTON let deletePhoto = UIAlertAction(title: “Delete”, style: .Destructive) { (action) -> Void in if let item = self.selectedObject as? Item { if let itemPhoto = item.photo { CDCloudSync.shared.setCacheStatusForObject(itemPhoto, requestedStatus:DELETING) } CDCloudSync.shared.setCacheStatusForObject(item, requestedStatus: CHANGED) item.photo = nil

item.thumbnail = nil self.photoImageView.image = nil self.refreshInterface() } else {self.done()} } sheet.addAction(deletePhoto) // CANCEL BUTTON let cancel = UIAlertAction(title: “Cancel”, style: UIAlertActionStyle.Cancel) { (action) -> Void in // Do nothing } sheet.addAction(cancel) self.presentViewController(sheet, animated: true, completion: nil) }

Update Groceries as follows to enable enhanced photo support: 1. Replace the existing showCamera function in ItemVC.swift with the code from Listing B.1. 2. Delete self.checkCamera() from the refreshInterface function of ItemVC.swift. 3. Delete the checkCamera function from ItemVC.swift. Run the application and navigate to the item edit view. You should be able to choose a photo from the Photo Library and delete photos too. If you’re using a device with a camera, you should see an additional option to take a photo.

Favorites The ability to tag an item as a favorite and the ability to put all favorites on the shopping list will now be implemented. This requires new icons, a managed object model update, new code, and new user interface elements. Update Groceries as follows to add the new icons: 1. Download the new icons from http://www.timroadley.com/LCDwS/Icons_ItemVC_AppB.zip. 2. Extract the new icons and drag them in to Assets.xcassets as shown in Figure B.1.

Figure B.1 New icons Update Groceries as follows to implement favorites: 1. Select Model.xcdatamodeld. 2. Click Editor > Add Model Version… and create a new model called Model 11 based on Model 10. 3. Set Model 11 as the current model. 4. Select Model 11.xcdatamodel. 5. Add an attribute to the Item entity called favorite and set its type to Boolean. 6. Set the Default Value of the favorite attribute to NO. 7. Add @NSManaged var favorite: NSNumber? to Item+CoreDataProperties.swift. The code shown in Listing B.2 is used to toggle an item as a favorite. Listing B.2 Toggle Favorite (ItemVC.swift) Click here to view code image // MARK: - FAVORITE @IBAction func toggleFavorite () { if let item = self.selectedObject as? Item { if let favorite = item.favorite { if favorite.boolValue { item.favorite = NSNumber(bool: false) } else { item.favorite = NSNumber(bool: true) } } else { item.favorite = NSNumber(bool: false) } self.refreshInterface() } else {self.done()} }

Update Groceries as follows to implement the favorite toggling code and a button to trigger it: 1. Add the code from Listing B.2 to the bottom of ItemVC.swift before the last curly brace. 2. Select Main.storyboard. 3. Drag a new Button on to the item edit view beneath the existing camera button. 4. Set the image of the new Button to fav_off, remove the button text of “Button,” and then ensure that the width and height of the button are 48. 5. Position the button as shown in Figure B.2; then click Editor > Resolve Auto Layout Issues > Selected Views > Add Missing Constraints. 6. Hold down Control and drag a line from the new favorite button to the yellow circle at the top of the same view; then choose toggleFavorite. 7. Double-click ItemVC.swift to open it in a new window. 8. Hold down Control while dragging a line from the new favorite button to the top of ItemVC.swift beneath the camera variable. When the window pops up, set the Name of the button to favoriteButton and ensure that the Storage is Strong before clicking Connect.

Figure B.2 New favorite button With the favorite button now accessible through code, it can be set with an appropriate image when the item edit view refreshes. Listing B.3 shows the code involved, along with an additional check that ensures the most up-to-date object is being used. Listing B.3 Set Favorite Button Image (ItemVC.swift) Click here to view code image // Favorite if let favorite = item.favorite {

if favorite.boolValue { self.favoriteButton.setImage(UIImage(named: “fav_on”), forState: .Normal) } else { self.favoriteButton.setImage(UIImage(named: “fav_off”), forState: .Normal) } } else { self.favoriteButton.setImage(UIImage(named: “fav_off”), forState: .Normal) } // Make sure you’re using the most up to date object if let moc = item.managedObjectContext { moc.refreshAllObjects() }

Update Groceries as follows to implement the additional refreshInterface code: 1. Add the code from Listing B.3 to the refreshInterface function of ItemVC.swift on line before item.modified = NSDate(). The next step is to implement the ability to add all favorites to the shopping list. Listing B.4 shows the code involved. Listing B.4 Add Favorites (ShopTVC.swift) Click here to view code image // MARK: - FAVORITE @IBAction func addFavorites () { let sheet = UIAlertController(title:“Add Favorite Items to the List?”, message:nil, preferredStyle:.ActionSheet) // ADD BUTTON let add = UIAlertAction(title: “Add”, style: .Destructive) { (action) -> Void in let filter = NSPredicate(format: “favorite == 1 AND listed == 0”) if let favoriteItems = CDOperation.objectsForEntity(“Item”, context: CDHelper.shared.context, filter: filter, sort: nil) as? [Item] { for item in favoriteItems { item.listed = NSNumber(bool: true) item.collected = NSNumber(bool: false) item.modified = NSDate() CDCloudSync.shared.setCacheStatusForObject(item, requestedStatus: CHANGED) } CDHelper.saveSharedContext() } } sheet.addAction(add) // CANCEL BUTTON let cancel = UIAlertAction(title: “Cancel”, style: UIAlertActionStyle.Cancel) { (action) -> Void in // Do nothing

} sheet.addAction(cancel) self.presentViewController(sheet, animated: true, completion: nil) }

Update Groceries as follows to implement the ability to add all favorite items to the shopping list: 1. Add the code from Listing B.4 to the bottom of ShopTVC.swift before the last curly brace. 2. Select Main.storyboard. 3. Drag a new Bar Button Item on to top right of the ShopTVC table view. This is the table view visible from the Shop tab and has a custom class of ShopTVC. 4. Set the image of the new Bar Button Item to fav_mini. 5. Hold down Control and drag a line from the new Bar Button Item to the yellow circle at the top of the same view and choose addFavorites.

Icon Badge The code in Listing B.5 updates the application badge with the number of items on the shopping list. Listing B.5 Set the Application Icon Badge (AppDelegate.swift) Click here to view code image func applicationWillResignActive(application: UIApplication) { // Show the number of listed items as a badge let filter = NSPredicate(format: “group == %@ AND groupKey == %@ AND cacheStatus != %i AND listed == 1 AND collected == 0”, CDCloudSync.shared.group(), CDCloudSync.shared.groupKey(), DELETING) if let items = CDOperation.objectsForEntity(“Item”, context: CDHelper.shared.context, filter: filter, sort: nil) as? [Item] { application.applicationIconBadgeNumber = items.count } else { application.applicationIconBadgeNumber = 0 } }

Update Groceries as follows to enable badge support: 1. Replace the applicationWillResignActive function in AppDelegate.swift with the code from Listing B.5. 2. Replace the didFinishLaunchingWithOptions function in AppDelegate.swift with the code from Listing B.6. This code ensures that the application has permission to display badges. Listing B.6 Register for User Notification Permission (AppDelegate.swift) Click here to view code image

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { let settings = UIUserNotificationSettings(forTypes: .Badge, categories: nil) application.registerUserNotificationSettings(settings) return true }

Location De-Duplication Logic Items must always have a home location and a shop location because the Prepare and Shop table views are grouped by these attributes. Currently, the combination of CDCloudSync synchronization and de-duplication can result in an endless loop of “? Unknown Location” creation and deletion. Likewise, the same issue occurs when two users of the same shopping list each create a location with the same name. The deduplication logic needs to be updated to move related items from a location that would be deleted to the location surviving de-duplication. Listing B.7 shows the code involved in de-duplicating home location objects. Listing B.7 Location At Home Object De-Duplication (LocationAtHome.swift) Click here to view code image import Foundation import CoreData import CloudKit class LocationAtHome: Location { class func printItems (locationAtHome:LocationAtHome) { if let storedIn = locationAtHome.storedIn, cacheID = locationAtHome.cacheID { if let items = locationAtHome.items { NSLog(“The ‘%@’ home location [%@] is related to %i items”,storedIn,cacheID,items.count) for item in items { if let name = item.valueForKey(“name”) as? String { NSLog(“ - %@”, name) } else {NSLog(“Failed to retrieve the item name value from item”)} } } else {NSLog(“Failed to retrieve the items value from home location”)} } else {NSLog(“Failed to retrieve the storedIn value from home location”)} } class func migrateItemsAndDeleteSource (source:LocationAtHome, target:LocationAtHome) { // Move item relationship from source location to target location if let sourceItems = source.items { for sourceItem in sourceItems { if let _sourceItem = sourceItem as? Item {

_sourceItem.locationAtHome = target CDCloudSync.shared.setCacheStatusForObject(_sourceItem, requestedStatus:CHANGED) } } } source.storedIn = source.cacheID // Prevent subsequent deduplication triggers source.items = nil LocationAtHome.printItems(target) CDCloudSync.shared.setCacheStatusForObject(target, requestedStatus: CHANGED) LocationAtHome.printItems(source) CDCloudSync.shared.setCacheStatusForObject(source, requestedStatus: DELETING)

// Refresh UI. // This is required because NSFetchedResultsController doesn’t track related objects. // The prepare and home tables are grouped by related objects. NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChanged” object:nil) } class func deduplicate (objectA objectA:NSManagedObject, objectB:NSManagedObject) -> Bool { if let homeA = objectA as? LocationAtHome, homeB = objectB as? LocationAtHome, storedIn = homeA.storedIn { if let homeAcacheStatus = homeA.cacheStatus { if homeAcacheStatus == DELETING { NSLog(“SKIPPED ‘%@‘de-duplication because one of the duplicates has already been deleted”,storedIn) return true // deduplication attempted } } if let homeBcacheStatus = homeB.cacheStatus { if homeBcacheStatus == DELETING { NSLog(“SKIPPED ‘%@‘de-duplication because one of the duplicates has already been deleted”,storedIn) return true // deduplication attempted } } NSLog(“De-duplicating the ‘%@’ Home Location”, storedIn) if let homeAcacheID = homeA.cacheID, homeBcacheID = homeB.cacheID { if let first = [homeAcacheID,homeBcacheID].sort().first {

if first == homeAcacheID { NSLog(“Removing %@ and leaving %@ in place”,CDCloudSync.shared.friendlyObjectName(homeA),CDCloudSync.shared.friendlyObjec LocationAtHome.migrateItemsAndDeleteSource(homeA, target: homeB) } else { NSLog(“Removing %@ and leaving %@ in place”,CDCloudSync.shared.friendlyObjectName(homeB),CDCloudSync.shared.friendlyObjec LocationAtHome.migrateItemsAndDeleteSource(homeB, target: homeA)

} } } else {NSLog(“SKIPPED de-duplication because a home location’s cacheID is nil”)} return true // deduplication attempted } return false // logic skipped } }

Update Groceries as follows to enhance home location de-duplication: 1. Replace the contents of LocationAtHome.swift with the code from Listing B.7. Listing B.8 shows the code involved in de-duplicating shop location objects. Listing B.8 Location At Shop Object De-Duplication (LocationAtShop.swift) Click here to view code image import Foundation import CoreData import CloudKit class LocationAtShop: Location { class func printItems (locationAtShop:LocationAtShop) { if let aisle = locationAtShop.aisle, cacheID = locationAtShop.cacheID { if let items = locationAtShop.items { NSLog(“The ‘%@’ shop location [%@] is related to %i items”,aisle,cacheID,items.count) for item in items { if let name = item.valueForKey(“name”) as? String { NSLog(“ - %@”, name) } else {NSLog(“Failed to retrieve the item name value from item”)} } } else {NSLog(“Failed to retrieve the items value from shop location”)} } else {NSLog(“Failed to retrieve the aisle value from shop location”)} } class func migrateItemsAndDeleteSource (source:LocationAtShop, target:LocationAtShop) { // Move item relationship from source location to target location if let sourceItems = source.items { for sourceItem in sourceItems { if let _sourceItem = sourceItem as? Item { _sourceItem.locationAtShop = target CDCloudSync.shared.setCacheStatusForObject(_sourceItem, requestedStatus: CHANGED) } } }

source.aisle = source.cacheID // Prevent subsequent deduplication triggers source.items = nil LocationAtShop.printItems(target) CDCloudSync.shared.setCacheStatusForObject(target, requestedStatus: CHANGED) LocationAtShop.printItems(source) CDCloudSync.shared.setCacheStatusForObject(source, requestedStatus: DELETING)

// Refresh UI. // This is required because NSFetchedResultsController doesn’t track related objects. // The prepare and shop tables are grouped by related objects. NSNotificationCenter.defaultCenter().postNotificationName(“SomethingChanged” object: nil) } class func deduplicate (objectA objectA:NSManagedObject, objectB:NSManagedObject) -> Bool { if let shopA = objectA as? LocationAtShop, shopB = objectB as? LocationAtShop, aisle = shopA.aisle { if let shopAcacheStatus = shopA.cacheStatus { if shopAcacheStatus == DELETING { NSLog(“SKIPPED ‘%@‘de-duplication because one of the duplicates has already been deleted”,aisle) return true // deduplication attempted } } if let shopBcacheStatus = shopB.cacheStatus { if shopBcacheStatus == DELETING { NSLog(“SKIPPED ‘%@‘de-duplication because one of the duplicates has already been deleted”,aisle) return true // deduplication attempted } } NSLog(“De-duplicating the ‘%@’ Shop Location”, aisle) if let shopAcacheID = shopA.cacheID, shopBcacheID = shopB.cacheID { if let first = [shopAcacheID,shopBcacheID].sort().first {

if first == shopAcacheID { NSLog(“Removing %@ and leaving %@ in place”,CDCloudSync.shared.friendlyObjectName(shopA),CDCloudSync.shared.friendlyObjec LocationAtShop.migrateItemsAndDeleteSource(shopA, target: shopB) } else { NSLog(“Removing %@ and leaving %@ in place”,CDCloudSync.shared.friendlyObjectName(shopB),CDCloudSync.shared.friendlyObjec LocationAtShop.migrateItemsAndDeleteSource(shopB, target: shopA) } } } else {NSLog(“SKIPPED de-duplication because a shop location’s cacheID is nil”)} return true // deduplication attempted } return false // logic skipped

} }

Update Groceries as follows to enhance shop location de-duplication: 1. Replace the contents of LocationAtShop.swift with the code from Listing B.8. Listing B.9 shows the updated code that ensures items have a location. This new code has more context saves and cache status changes. Listing B.9 Nil Location Check (Item.swift) Click here to view code image import Foundation import CoreData class Item: NSManagedObject { class func ensureLocationsAreNotNilForAllItems () { let predicate = NSPredicate(format: “locationAtHome == nil OR locationAtShop == nil”) let lostItems = CDOperation.objectsForEntity(“Item”, context: CDHelper.shared.context, filter: predicate, sort: nil) if let items = lostItems as? [Item] { for item in items { // Assign orphaned items to unknown home/shop location ensureHomeLocationIsNotNil(item) ensureShopLocationIsNotNil(item) // Update item status & save if let currentStatus = item.valueForKey(“cacheStatus”) as? NSNumber { if currentStatus == SYNCHRONIZED { CDCloudSync.shared.setCacheStatusForObject(item, requestedStatus:CHANGED) } else { if let context = item.managedObjectContext {CDHelper.save(context)} } } else { print(“ERROR getting cacheStatus in \(__FUNCTION__)”) } } } } class func ensureHomeLocationIsNotNil(item:Item) { if item.locationAtHome != nil {return} item.modified = NSDate() let context = item.managedObjectContext! let entity = “LocationAtHome” let attribute = “storedIn” let value = “? Unknown Location”

if let unknown = CDOperation.objectWithAttributeValueForEntity(entity, context:context, attribute:attribute, value:value) as? LocationAtHome { item.locationAtHome = unknown } else { if let newLocation = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtHome”, inManagedObjectContext:context) as? LocationAtHome { print(“CREATED ? Unknown Location for HOME”) CDCloudSync.addGroupInfo(newLocation) newLocation.storedIn = value item.locationAtHome = newLocation newLocation.modified = NSDate() if let context = newLocation.managedObjectContext {CDHelper.save(context)} } } } class func ensureShopLocationIsNotNil(item:Item) { if item.locationAtShop != nil {return} item.modified = NSDate() let context = item.managedObjectContext! let entity = “LocationAtShop” let attribute = “aisle” let value = “? Unknown Location” if let unknown = CDOperation.objectWithAttributeValueForEntity(entity, context:context, attribute:attribute, value:value) as? LocationAtShop { item.locationAtShop = unknown } else { if let newLocation = NSEntityDescription.insertNewObjectForEntityForName(“LocationAtShop”, inManagedObjectContext:context) as? LocationAtShop { print(“CREATED ? Unknown Location for SHOP”) CDCloudSync.addGroupInfo(newLocation) newLocation.aisle = value item.locationAtShop = newLocation newLocation.modified = NSDate() if let context = newLocation.managedObjectContext {CDHelper.save(context)} } } CDCloudSync.shared.setCacheStatusForObject(item, requestedStatus: CHANGED) if let context = item.managedObjectContext {CDHelper.save(context)} } }

Update Groceries as follows to introduce more context saves and cache status changes to Item.swift: 1. Replace the contents of Item.swift with the code from Listing B.9. The new de-duplication code must be triggered as shown in Listing B.10.

Listing B.10 Location De-Duplication (CDDeduplicator.swift) Click here to view code image // DE-DUPLICATE LOCATIONS if LocationAtHome.deduplicate(objectA:object, objectB: lastObject) {return} if LocationAtShop.deduplicate(objectA:object, objectB: lastObject) {return}

Update Groceries as follows to trigger location de-duplication: 1. Add the code from Listing B.10 to the deDuplicateEntityWithName function of CDDeduplicator.swift on the line before the DELETION LOGIC comment. The sample project is now complete and matches the version on the App Store. Note that it is normal for the application to warn “cannot move directly from NEW to CHANGED” when new objects are created. Feel free to comment out this warning in the setCacheStatusForObject function of CDCloudSync. Run the application now to test it out and don’t forget to tweet some nice feedback if you liked this book!

Code Snippets