Mobile Test

Before the release of iOS 10, the only way to share CloudKit records between users was to store those records in a public database. With the introduction of CloudKit sharing, individual app users can now share private database records with other users.

This chapter aims to provide an overview of CloudKit sharing and the classes used to implement sharing within an iOS app. The techniques outlined in this chapter will be put to practical use in the An iOS CloudKit Sharing Tutorial chapter.

<s>let share = CKShare(rootRecord: myRecord)</s>
share[CKShare.SystemFieldKey.title] = "My First Share" as CKRecordValue
share.publicPermission = .readOnlyCode language: HTML, XML (xml)

Understanding CloudKit Sharing

CloudKit sharing provides a way for records within a private database to be shared with other app users, entirely at the discretion of the database owner. When a user decides to share CloudKit data, a share link in the form of a URL is sent to the person with whom the data is to be shared. This link can be sent in various ways, including text messages, email, Facebook, or Twitter. When the recipient taps on the share link, the app (if installed) will be launched and provided with the shared record information ready to be displayed.

The level of access to a shared record may also be defined to control whether a recipient can view and modify the record. It is important to be aware that when a share recipient accepts a share, they are receiving a reference to the original record in the owner’s private database. Therefore, a modification performed on a share will be reflected in the original private database.

Preparing for CloudKit Sharing

Before an app can take advantage of CloudKit sharing, the CKSharingSupported key needs to be added to the project Info.plist file with a Boolean true value. Also, a CloudKit record may only be shared if it is stored in a private database and is a member of a record zone other than the default zone.

The CKShare Class

CloudKit sharing is made possible primarily by the CKShare class. This class is initialized with the root CKRecord instance that is to be shared with other users together with the permission setting. The CKShare object may also be configured with title and icon information to be included in the share link message. The CKShare and associated CKRecord objects are then saved to the private database. The following code, for example, creates a CKShare object containing the record to be shared and configured for read-only access:

Once the share has been created, it is saved to the private database using a CKModifyRecordsOperation object. Note the recordsToSave: argument is declared as an array containing both the share and record objects:

let modifyRecordsOperation = CKModifyRecordsOperation(
    recordsToSave: [myRecord, share], recordIDsToDelete: nil)Code language: JavaScript (javascript)

Next, a CKConfiguration instance needs to be created, configured with optional settings, and assigned to the operation:

let configuration = CKOperation.Configuration()
        
configuration.timeoutIntervalForResource = 10
configuration.timeoutIntervalForRequest = 10Code language: JavaScript (javascript)

Next, a lambda must be assigned to the modifyRecordsResultBlock property of the modifyRecordsOperation object. The code in this lambda is called when the operation completes to let your app know whether the share was successfully saved:

modifyRecordsOperation.modifyRecordsResultBlock = { result in
    switch result {
    case .success:
        // Handle completion
    case .failure(let error):
        print(error.localizedDescription)
    }
}Code language: JavaScript (javascript)

Finally, the operation is added to the database to begin execution:

self.privateDatabase?.add(modifyRecordsOperation)Code language: CSS (css)

The UICloudSharingController Class

To send a share link to another user, CloudKit needs to know both the identity of the recipient and the method by which the share link is to be transmitted. One option is to manually create CKShareParticipant objects for each participant and add them to the CKShare object. Alternatively, the CloudKit framework includes a view controller specifically for this purpose. When presented to the user (Figure 51-1), the UICloudSharingController class provides the user with a variety of options for sending the share link to another user:

Figure 51-1

The app is responsible for creating and presenting the controller to the user, the template code for which is outlined below:

let controller = UICloudSharingController { 
	controller, prepareCompletionHandler in

	// Code here to create the CKShare and save it to the database
}

controller.availablePermissions = 
        [.allowPublic, .allowReadOnly, .allowReadWrite, .allowPrivate]

controller.popoverPresentationController?.barButtonItem =
    sender as? UIBarButtonItem

present(controller, animated: true)Code language: JavaScript (javascript)

Note that the above code fragment also specifies the permissions to be provided as options within the controller user interface. These options are accessed and modified by tapping the link in the Collaboration section of the sharing controller (in Figure 51-1 above, the link reads “Only invited people can edit”). Figure 51-2 shows an example share options settings screen:

Figure 51-2

Once the user selects a method of communication from the cloud-sharing controller, the completion handler assigned to the controller will be called. As outlined in the previous section, the CKShare object must be created and saved within this handler. After the share has been saved to the database, the cloud-sharing controller must be notified that the share is ready to be sent. This is achieved by a call to the prepareCompletionHandler method that was passed to the completion handler in the above code. When prepareCompletionHandler is called, it must be passed the share object and a reference to the app’s CloudKit container. Bringing these requirements together gives us the following code:

let controller = UICloudSharingController { controller,
    prepareCompletionHandler in

let share = CKShare(rootRecord: thisRecord)

        share[CKShare.SystemFieldKey.title]
                 = "An Amazing House" as CKRecordValue
        share.publicPermission = .readOnly

        // Create a CKModifyRecordsOperation object and configure it
        // to save the CKShare instance and the record to be shared.
        let modifyRecordsOperation = CKModifyRecordsOperation(
            recordsToSave: [myRecord, share],
            recordIDsToDelete: nil)

        // Create a CKOperation instance
        let configuration = CKOperation.Configuration()

        // Set configuration properties to provide timeout limits
        configuration.timeoutIntervalForResource = 10
        configuration.timeoutIntervalForRequest = 10

        // Apply the configuration options to the operation
        modifyRecordsOperation.configuration = configuration

        // Assign a completion block to the CKModifyRecordsOperation. This will
        // be called the modify records operation completes or fails. 
                     
        modifyRecordsOperation.modifyRecordsResultBlock = { result in
            switch result {
            case .success:
                // The share operation was successful. Call the completion
                // handler
                prepareCompletionHandler(share, CKContainer.default(), nil)
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
        
        // Start the operation by adding it to the database
        self.privateDatabase?.add(modifyRecordsOperation)
}Code language: JavaScript (javascript)

Once the prepareCompletionHandler method has been called, the app for the chosen form of communication (Messages, Mail, etc.) will launch preloaded with the share link. All the user needs to do at this point is enter the contact details for the intended share recipient and send the message. Figure 51-3, for example, shows a share link loaded into the Mail app ready to be sent:

Figure 51-3

Accepting a CloudKit Share

When the recipient user receives a share link and selects it, a dialog will appear, providing the option to accept the share and open it in the corresponding app. When the app opens, the userDidAcceptCloudKitShareWith method is called on the scene delegate class located in the project’s SceneDelegate.swift file:

func windowScene(_ windowScene: UIWindowScene,
    userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
}Code language: CSS (css)

When this method is called, it is passed a CKShare.Metadata object containing information about the share. Although the user has accepted the share, the app must also accept the share using a CKAcceptSharesOperation object. As the acceptance operation is performed, it will report the results of the process via two result blocks assigned to it. The following example shows how to create and configure a CKAcceptSharesOperation instance to accept a share:

let container = CKContainer(identifier: metadata.containerIdentifier)
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])     
var rootRecordID: CKRecord.ID!

operation.acceptSharesResultBlock = { result in
    switch result {
    case .success:
        // The share was accepted successfully. Call the completion handler.
        completion(.success(rootRecordID))
    case .failure(let error):
        completion(.failure(error))
    }
}

operation.perShareResultBlock = { metadata, result in
    switch result {
    case .success:
        // The shared record ID was successfully obtained from the metadata.
        // Save a local copy for later. 
        rootRecordID = metadata.hierarchicalRootRecordID

        // Display the appropriate view controller and use it to fetch, and 
        // display the shared record.
        DispatchQueue.main.async {
            let viewController: ViewController = 
                    self.window?.rootViewController as! ViewController
            viewController.fetchShare(metadata)
        }        
    case .failure(let error):
        print(error.localizedDescription)
    }
}Code language: JavaScript (javascript)

The final step in accepting the share is to add the configured CKAcceptSharesOperation object to the CKContainer instance to accept share the share:

container.add(operation) Code language: CSS (css)

Fetching a Shared Record

Once a share has been accepted by both the user and the app, the shared record needs to be fetched and presented to the user. This involves the creation of a CKFetchRecordsOperation object using the root record ID contained within a CKShare.Metadata instance that has been configured with result blocks to be called with the results of the fetch operation. It is essential to be aware that this fetch operation must be executed on the shared cloud database instance of the app instead of the recipient’s private database. The following code, for example, fetches the record associated with a CloudKit share:

let operation = CKFetchRecordsOperation(
                     recordIDs: [metadata.hierarchicalRootRecordID!])

operation.perRecordResultBlock = { recordId, result in
    switch result {
    case .success(let record):
        DispatchQueue.main.async() {
             // Shared record successfully fetched. Update user 
             // interface here to present to the user. 
        }
    case .failure(let error):
        print(error.localizedDescription)
    }
}

operation.fetchRecordsResultBlock = { result in
    switch result {
    case .success:
        break
    case .failure(let error):
        print(error.localizedDescription)
    }
}

CKContainer.default().sharedCloudDatabase.add(operation)Code language: JavaScript (javascript)

Once the record has been fetched, it can be presented to the user within the perRecordResultBlock code, taking the steps above to perform user interface updates asynchronously on the main thread.

Summary

CloudKit sharing allows records stored within a private CloudKit database to be shared with other app users at the discretion of the record owner. An app user could, for example, make one or more records accessible to other users so that they can view and, optionally, modify the record. When a record is shared, a share link is sent to the recipient user in the form of a URL. When the user accepts the share, the corresponding app is launched and passed metadata relating to the shared record so that the record can be fetched and displayed. CloudKit sharing involves the creation of CKShare objects initialized with the record to be shared. The UICloudSharingController class provides a pre-built view controller which handles much of the work involved in gathering the necessary information to send a share link to another user. In addition to sending a share link, the app must also be adapted to accept a share and fetch the record for the shared cloud database. This chapter has covered the basics of CloudKit sharing, a topic that will be covered further in a later chapter entitled An iOS CloudKit Sharing Tutorial.

Using Trait Variations to Design Adaptive iOS User Interfaces

In 2007 developers only had to design user interfaces for single screen size and resolution (that of the first generation iPhone). However, considering the range of iOS devices, screen sizes, and resolutions available today, designing a single user interface layout to target the full range of device configurations now seems a much more daunting task.

Although eased to some degree by the introduction of Auto Layout, designing a user interface layout that would work on both iPhone and iPad device families (otherwise known as an adaptive interface) typically involved creating and maintaining two storyboard files, one for each device type.

iOS 9 and Xcode 7 introduced the concepts of trait variations and size classes, explicitly intended to allow a user interface layout for multiple screen sizes and orientations to be designed within a single storyboard file. In this chapter of iOS 16 App Development Essentials, the concept of traits and size classes will be covered together with a sample app demonstrating how to use them to create an adaptive user interface.

Understanding Traits and Size Classes

Traits define the features of the environment an app is likely to encounter when running on an iOS device. Traits can be defined both by the device’s hardware and how the user configures the iOS environment. Examples of hardware-based traits include hardware features such as the range of colors supported by the device display (also referred to as the display gamut) and whether or not the device supports features such as 3D Touch. The traits of a device that are dictated by user configuration include the dynamic type size setting and whether the device is configured for left-to-right or right-to-left text direction.

Arguably the most powerful trait category, however, relates specifically to the size and orientation of the device screen. Therefore, these trait values are referred to as size classes.

Size classes categorize the screen areas an app user interface will likely encounter during execution. Rather than represent specific screen dimensions and orientations, size classes represent width (w) and height (h) in terms of being compact (C) or regular (R).

All iPhone models in portrait orientation are represented by the compact width and regular height size class (wC hR). However, when smaller models such as the iPhone SE are rotated to landscape orientation, they are considered to be of compact height and width (wC hC). On the other hand, larger devices such as iPhone Pro and Pro Max models in landscape orientation are categorized as compact height and regular width (wR hC).

Regarding size class categorization, all iPad devices (including the iPad Pro) are classified as regular height and width (wR hR) in both portrait and landscape orientation. In addition, a range of different size class settings are used when apps are displayed on the iPad using multitasking, a topic covered in detail in the A Guide to Multitasking in iOS 12 chapter of this book.

Size Classes in Interface Builder

Interface Builder in Xcode 14 allows different Auto Layout constraints to be configured for different size class settings within a single storyboard file. In other words, size classes allow a single user interface file to store multiple sets of layout data, with each data set targeting a particular size class. At runtime, the app will use the layout data set for the size class that matches the device and prevailing orientation on which it is executing, ensuring that the user interface appears correctly.

By default, any layout settings configured within the Interface Builder environment will apply to all size classes. Only when trait variations are specifically specified will the layout configuration settings differ between size classes.

Customizing a user interface for different size classes goes beyond the ability to configure different Auto Layout constraints for different size classes. Size classes may also be used to designate which views in a layout are visible within each class and which version of a particular image is displayed to the user. A smaller image may be used when an app is running on an iPhone SE, for example, or extra buttons might be made to appear to take advantage of the larger iPad screen.

Enabling Trait Variations

Xcode enables trait variations by default for new Xcode projects. This setting can be viewed and changed by opening the Main storyboard file, selecting a scene, and displaying the File inspector as illustrated in Figure 23-1:

Figure 23-1

Setting “Any” Defaults

When designing user interfaces using size classes, there will often be situations where particular Auto Layout constraints or view settings will be appropriate for all size classes. Rather than configure these same settings for each size class, these can be configured within the Any category. Such settings will be picked up by default by all other size classes unless specifically overridden within those classes.

Working with Trait Variations in Interface Builder

A key part of working with trait variations involves using the device configuration bar. Located along the bottom edge of the Interface Builder storyboard canvas is an indicator showing the currently selected device, marked A in Figure 23-2:

Figure 23-2

Clicking on the current setting (A) in the status bar will display the device configuration menu shown in Figure 23-3, allowing different iOS device types to be selected:

Figure 23-3

Note that when an iPad model is selected, the button marked B is enabled so that you can test layout behavior for multitasking split-screen and slide-over modes. In addition, the device’s orientation can be switched between portrait and landscape using the button (C).

When you make device and orientation selections from the configuration bar, the scenes within the storyboard canvas will resize to match precisely the screen size corresponding to the selected device and orientation. This provides a quick way to test the adaptivity of layouts within the storyboard without compiling and running the app on different devices or emulators.

Attributes Inspector Trait Variations

Regardless of the current selection in the device configuration panel, any changes made to layouts or views within the current storyboard will, by default, apply to all devices and size classes (essentially the “Any” size class). Trait variations (in other words, configuration settings that apply to specific size classes) are configured in two ways.

The first option relates to setting size-specific properties such as fonts and colors within the Attributes Inspector panel. Consider, for example, a Label view contained within a storyboard scene. This label is required to display text at a 17pt font size for all size classes except for regular width and height, where a 30pt font is needed. Reviewing the Attributes Inspector panel shows that the 17pt font setting is already configured, as shown in Figure 23-4:

Figure 23-4

The absence of any size class information next to the attribute setting tells us that the font setting corresponds to the any width, any height, any gamut size class and will be used on all devices and orientations unless overridden. To the left of the attribute field is a + button, as indicated in Figure 23-5:

Figure 23-5

This is the Add Variation button which, when selected, displays a panel of menus allowing size class selections to be made for a trait variation. To configure a different font size for regular height, regular width, and any gamut, for example, the menu selections shown in Figure 23-6 would need to be made:

Figure 23-6

Once the trait configuration has been added, it appears within the Attributes Inspector panel labeled as wR hR and may be used to configure a larger font for regular width and height size class devices:

Figure 23-7

Using Constraint Variations

Layout size and constraint variations are managed using the Size inspector, which is accessed using the button marked A in Figure 23-8:

Figure 23-8

The Size inspector will update to reflect the currently selected object or constraint within the design layout. Constraints are selected either by clicking on the constraint line in the layout canvas or by making selections within the Document outline panel. In Figure 23-9, for example, a centerX constraint applied to a Label is selected within Interface Builder:

Figure 23-9

The area marked B in Figure 23-8 above contains a list of variations supported by the currently selected constraint (in this example, only the default constraint is installed, which will apply to all size classes). As demonstrated later in this chapter, we can add additional variations by clicking on the + button (C) and selecting the required class settings as outlined above when working with fonts. Once the new variation has been set up, the default variation must be disabled so that only the custom variation is active. In Figure 23-10, for example, a Label width constraint variation has been configured for the wR hR size class:

Figure 23-10

If we needed a different width for compact width, regular height devices, we would add a second width constraint to our Label and configure it with a wC hR variation containing the required width value. Then, the system will select the appropriate width constraint variation for the current device when the app runs.

An Adaptive User Interface Tutorial

The remainder of this chapter will create an adaptive user interface example. This tutorial aims to create a straightforward adaptive layout that demonstrates each of the key features of trait variations, including setting individual attributes, using the vary for traits process, and implementing image asset catalogs in the context of size classes. Begin by creating a new Xcode iOS app project named AdaptiveDemo with the language option set to Swift.

Designing the Initial Layout

The iPhone 14 in portrait orientation will serve as the base layout configuration for the user interface, so begin by selecting the Main.storyboard file and ensuring that the device configuration bar currently has the iPhone 14 device selected.

Drag and drop a Label view so that it is positioned in the horizontal center of the scene and slightly beneath the top edge of the safe area of the view controller scene. With the Label still selected, use the Align menu to add a constraint to center the view horizontally within the container. Using the Add New Constraints menu, set a Spacing to nearest neighbor constraint on the top edge of the Label using the current value and with the Constrain to Margins option enabled.

Double-click on the Label and change the text to read “A Sunset Photo”. Next, drag and drop an Image View object from the Library panel to be positioned beneath the Label and centered horizontally within the scene.

Resize the Image View so that the layout resembles that illustrated in Figure 23-11:

Figure 23-11

With the Image View selected, display the Align menu and enable the option to center the view horizontally within the container, then use the Add New Constraints menu to add a nearest neighbor constraint on the top edge of the Image View with the Constrain to Margins option disabled and to set a Width constraint of 320:

Figure 23-12

Ctrl-click and drag diagonally across the Image View, as shown in Figure 23-13. On releasing the line, select Aspect Ratio from the resulting menu.

Figure 23-13

If necessary, click the Update Frames button in the status bar to reset the views to match the constraints.

Adding Universal Image Assets

The tutorial’s next step is adding some image assets to the project. The images can be found in the adaptive_ images directory of the code samples download, which can be obtained from:

https://www.ebookfrenzy.com/retail/ios12/

Within the project navigator panel, locate and click on the Assets entry and click on the + button located in the panel’s bottom left-hand corner and select the Image Set option. Next, double-click on the new image set, which will have been named Image, and rename it to Sunset.

Locate the [email protected] file in a Finder window and drag and drop it onto the 2x image box as illustrated below:

Figure 23-14

Return to the Main.storyboard file, select the Image View and display the Attributes Inspector panel. Next, click the down arrow to the right of the Image field and select Sunset from the resulting menu. The Image View in the view controller canvas will update to display the sunset image from the asset catalog.

Switch between device types and note that the same image is used for all size classes. The next step is to add a different image to be displayed in regular-width size class configurations.

Once again, select Assets from the project navigator panel and, with the Sunset image set selected, display the Attributes Inspector and change the Width Class attribute to Any & Compact. An additional row of image options will appear within the Sunset image set. Once again, locate the images in the sample source code download, this time dragging and dropping the [email protected] file onto the 2x image well in the compact row, as shown in Figure 23-15:

Figure 23-15

Return to the Main.storyboard file and verify that different sunset images appear when switching between compact and regular width size class configurations.

Increasing Font Size for iPad Devices

The next step in the tutorial is to add some trait variations that will apply when the app runs in regular width and regular height size class device configurations (in other words, when running on an iPad). The first variation is to use a larger font on the Label object.

Begin by selecting the Label object in the scene. Next, display the Attributes Inspector panel and click on the + button next to the font property, as illustrated in Figure 23-6 above. Select the Regular Width | Regular Height menu settings from the resulting menu and click on the Add Variation button. Within the new wR hR font attribute field, increase the font size to 35pt. Using the device configuration bar, test that the new font applies only to iPad device configurations.

Adding Width Constraint Variations

The next step is to increase the Image View size when the app encounters an iPad device size class. Since the Image View has a constraint that preserves the aspect ratio, only the width constraint needs to be modified to achieve this goal.

Earlier in the tutorial, we added a width constraint to the Image view that is currently being used for all size classes. The next step, therefore, is to adjust this constraint so that it only applies to compact width devices. To do this, select the width constraint for the Image view in the Document outline as shown below:

Figure 23-16

With the constraint selected, display the Size inspector, click on the button marked C in Figure 23-8 above and add a new compact width, regular height variation. Once the variation has been added, disable the default variation so that only the new variation is installed:

Figure 23-17

Now that we have a width constraint for compact width devices, we need to add a second width constraint to the Image view for regular width size classes. Select the Image view in the layout and use the Add New Constraints menu to add a width constraint with a value of 600:

Figure 23-18

Locate this new width constraint in the Document outline and select it as outlined in Figure 23-19:

Figure 23-19

With the constraint selected, display the Size inspector, add a regular width, regular height variation and disable the default variation. If the Relation field is set to Greater Than or Equal to, change it to Equal:

Figure 23-20

Testing the Adaptivity

Use the device configuration bar to test a range of size class permutations. Assuming that the adaptivity settings are working, a larger font and a larger, different image should appear when previewing iPad configurations. Use the device configuration bar to test a range of size class permutations. Assuming that the adaptivity settings are working, a larger font and a larger, different image should appear when previewing iPad configurations.

Before continuing to the next chapter, take some time to experiment by adding other variations, for example, to adjust the image size for smaller iPhone devices in landscape orientation (wC hC).

Summary

The range of iOS device screen sizes and resolutions is much more diverse than when the original iPhone was introduced in 2007. Today, developers need to be able to target an increasingly wide range of display sizes when designing user interface layouts for iOS apps. With size classes and trait variations, it is possible to target all screen sizes, resolutions, and orientations from within a single storyboard. This chapter has outlined the basics of size classes and trait variations and worked through a simple example user interface layout designed for both the iPad and iPhone device families.