Chapter 3. The User Interface

Apple has added quite a few things to UIKit in iOS 9 worth knowing about. One of my favorites is stacked views. We’ll check them out soon. We will also have a look at content sizes, unwind segues, layout guides, and more.

3.1 Arranging Your Components Horizontally or Vertically

Problem

You have vertical or horizontal view hierarchies that you find cumbersome to manage with constraints.

Solution

Stacked views are the solution.

Discussion

Imagine that you want to create a view that looks like Figure 3-1.

Figure 3-1. Vertical and horizontal views

Prior to Xcode 7 and its stacked views, we had to set up massive amounts of constraints just to achieve a simple layout like Figure 3-1. Well, no more. Let’s head to IB and drop an image view, three labels arranged vertically, and three arranged horizontally, like the previous figure. Our image and labels look initially like Figure 3-2.

Figure 3-2. Stacked images

Grab the top three labels and press the little Stack button at the bottom of IB as shown in Figure 3-3.

Figure 3-3. The stack button is the leftmost button

Now you will notice that your components are aligned as you wanted them. Now select the top stack (your vertical components). Then, from the Attributes inspector, under Spacing, choose 20. Then select your horizontal group and do the same. Bring your horizontal group up and align it to the bottom of the image view to end up with something like Figure 3-1.

See Also

Recipe 3.3Recipe 3.2, and Recipe 3.7

3.2 Customizing Stack Views for Different Screen Sizes

Problem

You want to customize the way your stack views appear on the screen, based on the screen size they are running on.

Solution

Use size class customization features of Xcode, right in the Attributes inspector.

Discussion

You might have noticed tiny + buttons in various places inside IB. But what are they? Have you used them before? If not, you are missing out on a lot and I’m going to show you how to take advantage of them.

Size classes are encapsulated information about the dimensions of the current screen: possible values are regular, compact, and any. These sizes have been defined to stop us from thinking in terms of pixels. You either have a regular size or compact size.

Imagine your iPhone 6+ in portrait mode. The screen width is compact, and the screen height is regular. Once you go to landscape mode, your screen width is regular and your height is compact. Now imagine an iPad in portrait mode. Your screen width is regular and so is your height. Landscape, ditto.

Let’s work on a project to get the idea more clearly. I want us to achieve the effect shown in Figure 3-4 when running our app on iPhone in portrait mode.

Figure 3-4. In portrait, our views have no spacing between them

And when we go to landscape, I want us to have 10 points spacing between the items, but only when the height of the screen is compact (Figure 3-5).

Figure 3-5. With compact screen height, we want spacing to be applied between our views

We get started by creating three colorful views on our main storyboard. I leave the colors to you to decide. Select all your views and then press the little stack button (Figure 3-3) in IB to group your views horizontally. Then place your stacked view on the top left of the view with proper top and left margin spacing (see Figure 3-6).

Figure 3-6. The IB guidelines appear when the view is on top left of the super view

Once done, make sure your stacked view is the selected view and then press the Resolve Auto Layout issues button (the rightmost button in Figure 3-3). Under Selected Views, choose “Reset to Suggested Constraints.”

Now choose your stack view. In the Attributes inspector, under the Spacing section, find the little + button and press it. In the popup, choose Any Width and then under that choose Compact Height. This will give you an additional text field to write the desired spacing value for any screen width while the height of the screen is compact. Put the value of 10 in this box (see Figure 3-7).

Figure 3-7. Place the value of 10 in the new text box

If you run your app on an iPhone 6+ and then switch to landscape, you won’t see any spacing between the items—so what happened? The problem is that in landscape mode, we are not increasing the width of our stack view. It doesn’t currently have extra width to show the spaces between the views. To account for this, let’s first add a normal width constraint to our stack view. You can do that by selecting the stack view in the list of views that you have, holding down the Control button on your keyboard, and dragging from the stack view to the stack view itself. You will now get a popup. Choose Width in this popup (see Figure 3-8).

Figure 3-8. Choose the Width option in the popup to add a width constraint to the stack view

While your stack view is selected, go to the Size inspector and double-click the Width constraint that we just created. This will allow you to edit this constraint with size classes. How awesome is that? Next to the Constant text box, I can see the value of 300. You might see a different value based on the width of the views you placed in your stack view. My views were each 100 points wide, hence x3 comes to 300 points. I can also see a little + button next to the Constant box. Press that button and add a new constant for “Any Width and Compact Height” and set the value to N+20, where N is the value of your current constant. For me N is 300, so I’ll enter the value of 320 in the new box (see Figure 3-9).

Figure 3-9. Add a new width constant class to the stack view

There is one more thing that we need to tell the stack view in order for it to stack our views correctly when its width changes. Select the stack view and, in attributes inspector, under the Distribution section, change the default value to Equal Spacing. Now run your app and enjoy the awesomeness that you just created. Rotate from portrait to landscape under any iPhone simulator (not iPad).

See Also

Recipe 3.1

3.3 Creating Anchored Constraints in Code

Problem

You want your code to use the same layout anchors that IB uses.

Solution

Start using the new anchor properties on UIView, such as leadingAnchor and trailingAnchor.

Discussion

Layout anchors are very useful for arranging your components on the screen. Let’s say that you have two buttons on your view, arranged horizontally, and you want the second button to be placed 10 points to the right of the first button.

First create two buttons on your view using IB and then place them next to each other, horizontally. The horizontal space between them does not matter so much right now. Then select both of them and in the Resolve Auto Layout issues button (rightmost button in Figure 3-3), under the Selected Views, choose the Add Missing Constraints option (see Figure 3-10).

Figure 3-10. Adding the missing constraints to our buttons

Then select the second button (on the right). Under the Size inspector, find the “Leading Space to” constraint, double-click it, and choose the “Remove at build time” option (see Figure 3-11). This will make sure that the leading constraint, which we are going to create in code, will be present in IB while checking things out, but that during the project run the constraint will be removed, giving us the ability to replace it.

Figure 3-11. Removing the leading constraint at build time will give us a window to replace it at runtime

Now link your buttons into your code with names such as btn1 and btn2. In the viewDidLoad method of your view controller, write the following code:

  override func viewDidLoad() {
    super.viewDidLoad()

    btn2.leadingAnchor.constraintEqualToAnchor(btn1.trailingAnchor,
      constant: 10).active = true

  }

Now run your app and see how your second button is trailing your first button horizontally with a 10-point space between them. You can use the following anchors in your views:

  • bottomAnchor
  • centerXAnchor
  • centerYAnchor
  • firstBaselineAnchor
  • heightAnchor
  • lastBaselineAnchor
  • leadingAnchor
  • leftAnchor
  • rightAnchor
  • topAnchor
  • trailingAnchor
  • widthAnchor
Note

All of these anchors are direct or indirect subclasses of the NSLayoutAnchor class. The horizontal anchors specifically are subclasses of the NSLayoutXAxisAnchor class and the vertical ones are subclasses of NSLayoutYAxisAnchor.

Now, just to play with some more anchors, let’s create a view hierarchy like the one in Figure 3-12. We are going to place a red view under the first button and set the width of this view to the width of the button in our code.

Figure 3-12. Two buttons and a view

In IB, drag and drop a view onto your main view and set the background color of it to red so that you can see it better. Drag and drop it so that it is aligned under the two buttons with proper left and top margins (see Figure 3-13).

Figure 3-13. Align the red view like so

Anchor the views as follows:

  1. Select the red view.
  2. In IB, choose the Resolve Auto Layout issues button.
  3. Under the Selected View section, choose Add Missing Constraints.
  4. Go to the Size inspector. For the red view, find the “Trailing Space to” constraint and delete it by selecting it and pressing the delete button.
  5. Select the red button in the view hierarchy, hold down the Control button on your keyboard, and drag and drop the button into itself.
  6. A menu will appear. In the menu, choose Width to create a width constraint. Then find the new width constraint in the Size inspector, double-click it, and choose the “Remove at build time” option (see Figure 3-14).
Figure 3-14. Remove the automatically built width constraint at build time so that we can replace it in code

Now create an outlet for this red view in your code (I’ve named mine “v”) and add the following code to your viewDidLoad()method:

    v.widthAnchor.constraintEqualToAnchor(btn2.widthAnchor,
      constant:0).active = true

See Also

Recipe 3.4

3.4 Allowing Users to Enter Text in Response to Local and Remote Notifications

Problem

You want to allow your users to enter some text in response to local or push notifications that you display. And you would additionally like to be able to read this text in your app and take action on it.

Solution

Set the new behavior property of the UIUserNotificationAction class to .TextInput (with a leading period).

Discussion

Let’s say that we want our app to register for local notifications and then ask the user for her name once the app has been sent to the background. The user enters her name and then we come to the foreground and take action on that name.

We start by writing a method that allows us to register for local notifications:

  func registerForNotifications(){

    let enterInfo = UIMutableUserNotificationAction()
    enterInfo.identifier = "enter"
    enterInfo.title = "Enter your name"
    enterInfo.behavior = .TextInput //this is the key to this example
    enterInfo.activationMode = .Foreground

    let cancel = UIMutableUserNotificationAction()
    cancel.identifier = "cancel"
    cancel.title = "Cancel"

    let category = UIMutableUserNotificationCategory()
    category.identifier = "texted"
    category.setActions([enterInfo, cancel], forContext: .Default)

    let settings = UIUserNotificationSettings(
      forTypes: .Alert, categories: [category])

    UIApplication.sharedApplication()
      .registerUserNotificationSettings(settings)

  }

We set the behavior property on the UIMutableUserNotificationAction instance to .TextInput to allow this particular action to receive text input from the user. Now we will move on to calling this method when our app is launched:

  func application(application: UIApplication,
    didFinishLaunchingWithOptions
    launchOptions: [NSObject : AnyObject]?) -> Bool {

      registerForNotifications()

      return true
  }

We also need a method to schedule a local notification whenever asked for:

  func scheduleNotification(){

    let n = UILocalNotification()
    let c = NSCalendar.autoupdatingCurrentCalendar()
    let comp = c.componentsInTimeZone(c.timeZone, fromDate: NSDate())
    comp.second += 3
    let date = c.dateFromComponents(comp)
    n.fireDate = date

    n.alertBody = "Please enter your name now"
    n.alertAction = "Enter"
    n.category = "texted"
    UIApplication.sharedApplication().scheduleLocalNotification(n)

  }

And we’ll call this method when our app is sent to the background:

  func applicationDidEnterBackground(application: UIApplication) {
    scheduleNotification()
  }

Once that is done, we will read the text that the user has entered and do our work with it (I’ll leave this to you):

  func application(application: UIApplication,
    handleActionWithIdentifier identifier: String?,
    forLocalNotification notification: UILocalNotification,
    withResponseInfo responseInfo: [NSObject : AnyObject],
    completionHandler: () -> Void) {

      if let text = responseInfo[UIUserNotificationActionResponseTypedTextKey]
        as? String{

          print(text)
          //TODO: now you have access to this text

      }

      completionHandler()

  }

Let’s run it and then send the app to the background and see what happens (see Figure 3-15).

Figure 3-15. A local notification is shown on the screen

Then take that little bar at the bottom of the notification and drag it down to show the actions that are possible on the notification (see Figure 3-16).

Figure 3-16. Possible actions on our local notification

Now if the user just taps the Enter button, she will see a text field and can then enter her information. Upon submitting the text, she will be redirected to our app where we will receive the text (see Figure 3-17).

Figure 3-17. Entering text in a local notification

See Also

Recipe 3.7

3.5 Dealing with Stacked Views in Code

Problem

You want to programmatically manipulate the contents of stack views.

Solution

Use an instance of the UIStackView.

Discussion

For whatever reason, you might want to construct your stack views programmatically. I do not recommend this way of working with stack views because IB already can handle most of the situations where you would want to use stack views, and then some. But if you absolutely have to use stack views in your app, simply instantiate UIStackView and pass it your arranged views.

You can also then set the axis property to either Vertical or Horizontal. Remember to set the distribution property as well, of type UIStackViewDistribution. Some of the values of this type are Fill, FillEqually , and EqualSpacing. I also like to set the spacing property of the stack view manually so that I know how much space there is between my items.

Let’s say that we want to create a stack view like Figure 3-18. The stack view is tucked to the right side of the screen and every time we press the button, a new label will be appended to the stack view.

Figure 3-18. This is the stack view that we want to create

First define a stack view in your view controller:

  var rightStack: UIStackView!

Then a few handy methods for creating labels and a button:

  func lblWithIndex(idx: Int) -> UILabel{
    let label = UILabel()
    label.text = "Item (idx)"
    label.sizeToFit()
    return label
  }

  func newButton() -> UIButton{
    let btn = UIButton(type: .System)
    btn.setTitle("Add new items...", forState: .Normal)
    btn.addTarget(self, action: "addNewItem",
      forControlEvents: .TouchUpInside)
    return btn
  }

  func addNewItem(){
    let n = rightStack.arrangedSubviews.count
    let v = lblWithIndex(n)
    rightStack.insertArrangedSubview(v, atIndex: n - 1)
  }
Note

The addNewItem function will be called when the button is pressed.

When our view is loaded on the screen, we will create the stack view and fill it with the three initial labels and the button. Then we will set up its axis, spacing, and distribution. Once done, we’ll create its constraints:

  override func viewDidLoad() {
    super.viewDidLoad()

    rightStack = UIStackView(arrangedSubviews:
      [lblWithIndex(1), lblWithIndex(2), lblWithIndex(3), newButton()])

    view.addSubview(rightStack)

    rightStack.translatesAutoresizingMaskIntoConstraints = false

    rightStack.axis = .Vertical
    rightStack.distribution = .EqualSpacing
    rightStack.spacing = 5

    rightStack.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor,
      constant: -20).active = true
    rightStack.topAnchor.constraintEqualToAnchor(
      topLayoutGuide.bottomAnchor).active = true

  }

See Also

Recipe 3.2 and Recipe 3.3

3.6 Showing Web Content in Safari View Controller

Problem

You want to take advantage such awesome Safari functionalities as Reader Mode in your own apps.

Solution

Use the SFSafariViewController class in the SafariServices.framework. This view controller can easily be initialized with a URL and then displayed on the screen.

Discussion

Let’s go ahead and build the UI. For this recipe, I am aiming for a UI like Figure 3-19.

Figure 3-19. Create a UI that looks similar to this in your own storyboard

Then hook up the text field and button to your code. Once the button is tapped, the code that runs is:

  @IBAction func openInSafari() {

    guard let t = textField.text where t.characters.count > 0,
      let u = NSURL(string: t)  else{
      //the url is missing, you can further code this method if you want
      return
    }

    let controller = SFSafariViewController(URL: u,
      entersReaderIfAvailable: true)
    controller.delegate = self
    presentViewController(controller, animated: true, completion: nil)

  }

Now make your view controller conform to the SFSafariViewControllerDelegate protocol. Program the safariViewControllerDidFinish(_:) method to ensure that, when the user closes the Safari view controller, the view disappears:

  func safariViewControllerDidFinish(controller: SFSafariViewController) {
    dismissViewControllerAnimated(true, completion: nil)
  }

In the initializer of the Safari controller, I also specified that I would like to take advantage of the Reader Mode if it is available.

See Also

Recipe 11.1 and Recipe 5.1

3.7 Laying Out Text-Based Content on Your Views

Problem

You want to show text-based content to your users and want to lay it out on the screen in the optimal position.

Solution

Use the readableContentGuide property of UIView.

Discussion

The readableContentGuide property of UIView gives you the margins that you need to place your text content on the screen properly. On a typical iPhone 6 screen, this margin is around 20 points on both the left and the right. The top and bottom margins on the same device are usually set near 0. But don’t take these numbers at face value. They might change and you should never think about them as hardcoded values. That is why we should use the readableContentGuide property to place our components correctly on the screen.

There isn’t really much more to it than that, so let’s just see an example. In this code, I will create a label and stretch it horizontally and vertically to fill the readable section of my view. I will also make sure the top and left positioning of the label is according to the readable section’s guides:

    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.backgroundColor = UIColor.greenColor()
    label.text = "Hello, World"
    label.sizeToFit()
    view.addSubview(label)

    label.leadingAnchor.constraintEqualToAnchor(
      view.readableContentGuide.leadingAnchor).active = true

    label.topAnchor.constraintEqualToAnchor(
      view.readableContentGuide.topAnchor).active = true

    label.trailingAnchor.constraintEqualToAnchor(
      view.readableContentGuide.trailingAnchor).active = true

    label.bottomAnchor.constraintEqualToAnchor(
      view.readableContentGuide.bottomAnchor).active = true

See Also

Recipe 3.4

3.8 Improving Touch Rates for Smoother UI Interactions

Problem

You want to be able to improve the interaction of the user with your app by decreasing the interval required between touch events.

Solution

Use the coalescedTouchesForTouch(_:) and the predictedTouchesForTouch(_:) methods of the UIEvent class. The former method allows you to receive coalesced touches inside an event, while the latter allows you to receive predicted touch events based on iOS’s internal algorithms.

Discussion

On selected devices such as iPad Air 2, the display refresh rate is 60Hz like other iOS devices, but the touch scan rate is 120Hz. This means that iOS on iPad Air 2 scans the screen for updated touch events twice as fast as the display’s refresh rate. These events obviously cannot be delivered to your app faster than the display refresh rate (60 times per second), so they are coalesced. At every touch event, you can ask for these coalesced touches and base your app’s reactions on them..

In this recipe, imagine that we are just going to draw a line based on where the user’s finger has been touching the screen. The user can move her finger over our view any way she wants and we just draw a line on that path.

Create a single-view app. In the same file as your view controller’s Swift source file, define a new class of type UIView and name it MyView:

class MyView : UIView{

}

In your storyboard, set your view controller’s view class to MyView (see Figure 3-20).

Figure 3-20. Your view is inside the view controller now
Note

Make sure that you are running this code on a device at least as advanced as an iPad Air 2. iPhone 6 and 6+ do not have a 120Hz touch scan rate.

Then in your view, define an array of points and a method that can take a set of touches and an event object, read the coalesced touch points inside the event, and place them inside our array:

  var points = [CGPoint]()

  func drawForFirstTouchInSet(s: Set<UITouch>, event: UIEvent?){

    guard let touch = s.first, event = event,
      allTouches = event.coalescedTouchesForTouch(touch)
      where allTouches.count > 0 else{
        return
    }

    points += allTouches.map{$0.locationInView(self)}

    setNeedsDisplay()

  }

Now when the user starts touching our view, we start recording the touch points:

  override func touchesBegan(touches: Set<UITouch>,
    withEvent event: UIEvent?) {

      points.removeAll()
      drawForFirstTouchInSet(touches, event: event)

  }

Should we be told that the touch events sent to our app were by accident, and that the user really meant to touch another UI component on the screen, such as the notification center, we have to clear our display:

  override func touchesCancelled(touches: Set<UITouch>?,
    withEvent event: UIEvent?) {

      points.removeAll()
      setNeedsDisplayInRect(bounds)

  }

Every time the touch location moves, we move with it and record the location:

  override func touchesCancelled(touches: Set&lt;UITouch&gt;?,
    withEvent event: UIEvent?) {

      points.removeAll()
      setNeedsDisplayInRect(bounds)

  }

Once the touches end, we also ask iOS for any predicted touch events that might have been calculated, and we will draw them too:

  override func touchesEnded(touches: Set<UITouch>,
    withEvent event: UIEvent?) {

      guard let touch = touches.first, event = event,
        predictedTouches = event.predictedTouchesForTouch(touch)
        where predictedTouches.count > 0 else{
          return
      }

      points += predictedTouches.map{$0.locationInView(self)}
      setNeedsDisplay()

  }

Our drawing code is simple. It goes through all the points and draws lines between them:

  override func drawRect(rect: CGRect) {

    let con = UIGraphicsGetCurrentContext()

    //set background color
    CGContextSetFillColorWithColor(con, UIColor.blackColor().CGColor)
    CGContextFillRect(con, rect)

    CGContextSetFillColorWithColor(con, UIColor.redColor().CGColor)
    CGContextSetStrokeColorWithColor(con, UIColor.redColor().CGColor)

    for point in points{

      CGContextMoveToPoint(con, point.x, point.y)

      if let last = points.last where point != last{
        let next = points[points.indexOf(point)! + 1]
        CGContextAddLineToPoint(con, next.x, next.y)
      }

    }

    CGContextStrokePath(con)

  }

}

Now run this on an iPad Air 2 and compare the smoothness of the lines that you draw with those on an iPhone 6 or 6+, for instance.

See Also

Recipe 3.1

3.9 Supporting Right-to-Left Languages

Problem

You are internationalizing your app and, as part of this process, need to support right-to-left languages such as Persian or Arabic.

Solution

Use a combination of the following:

  • Use IB’s view properties to arrange your items with proper semantic properties.
  • Ensure that you create your constraints correctly, preferrably using IB.
  • Use UIView’s userInterfaceLayoutDirectionForSemanticContentAttribute(_:) class method to find the direction of the user interface based on the semantic attributes that are part of the UISemanticContentAttribute enum.
  • If arranging your items in code, use the semanticContentAttribute property of your views to set their semantic correctly.

Discussion

Let’s create an app that has a text view on top and four buttons arranged like the arrow keys on the keyboard: up, left, down, right. When each one of these buttons is pressed, we will display the corresponding word in the text field. The text field will be read-only, and when displaying right-to-left languages, it will of course show the text on the righthand side. Make sure that your UI looks (for now) something like Figure 3-21. There are one text field and four buttons.

Figure 3-21. Initial layout

Now select the left, down, and right buttons on the UI (exclude the up button for now) and stack them up together. In the new stack that was created, set the spacing to 20 (see Figure 3-22). Set the horizontal stack view’s spacing so that the buttons will be horizontally stacked with the proper distance from each other.

Then select the newly created stack and the up button on IB and stack those up together. This will create a vertical stack view for you. Set the spacing for this new stack view to 10. Place the main stack view at the center of the screen. Use IB’s “Resolve Auto Layout Issues” feature to add all missing constraints for all the components. Also make sure that you disable editing of the text field. Then hook up the text field to your code as an outlet and hook up the four buttons’ touch events to your view controller as well. Now your UI should look like Figure 3-23 on IB.

Figure 3-22. Horizontal spacing between buttons
Figure 3-23. Your UI should look like this at the moment

Now choose the main stack view in your UI. In IB, in the Semantic section under Attributes inspector, choose Playback. This will ensure that the views inside this stack view will not be mirrored right to left when the language changes to a right-to-left language (see Figure 3-24).

Figure 3-24. Choosing the Playback view semantic

Now from Xcode, create a new strings file, name it Localizable.strings, and place your string keys in there:

"up" = "Up";
"down" = "Down";
"right" = "Right";
"left" = "Left";

Under your main project’s info page in Xcode, choose Localizations and add Arabic as a localization. Then move over to your newly created strings file and enable the Arabic language on it (see Figure 3-25).

Figure 3-25. Localize the strings file so that you have both English and Arabic in the list

You will now have two strings files. Go into the Arabic one and localize the file:

"up" = "Up in Arabic";
"down" = "Down in Arabic";
"right" = "Right in Arabic";
"left" = "Left in Arabic";

In your code now, we have to set the text field’s text direction based on the orientation that we get from UIView. That orientation itself depends on the semantics that we set on our text field before:

class ViewController: UIViewController {

  @IBOutlet var txtField: UITextField!

  @IBAction func up() {
    txtField.text = NSLocalizedString("up", comment: "")
  }

  @IBAction func left() {
    txtField.text = NSLocalizedString("left", comment: "")
  }

  @IBAction func down() {
    txtField.text = NSLocalizedString("down", comment: "")
  }

  @IBAction func right() {
    txtField.text = NSLocalizedString("right", comment: "")
  }

  override func viewDidAppear(animated: Bool) {

    let direction = UIView
      .userInterfaceLayoutDirectionForSemanticContentAttribute(
        txtField.semanticContentAttribute)

    switch direction{
    case .LeftToRight:
      txtField.textAlignment = .Left
    case .RightToLeft:
      txtField.textAlignment = .Right
    }

  }

}

Now run the app on an English device and you will see English content in the text field aligned from left to right. Run it on an Arabic localized device and you’ll see the text aligned on the right hand side.

3.10 Associating Keyboard Shortcuts with View Controllers

Problem

You want to allow your application to respond to complex key combinations that a user can press on an external keyboard, to give the user more ways to interact with your app.

Solution

Construct an instance of the UIKeyCommand class and add it to your view controllers using the addKeyCommand(_:) method. You can remove key commands with the removeKeyCommand(_:) method.

Discussion

Keyboard shortcuts are very useful for users with external keyboards. Why? Since you asked, it’s because they can use keyboard shortcuts. For instance, on a document editing iOS app, the user might expect to press Command-N to create a new document, whereas on an iOS device this may be achieved by the user pressing a button such as “New.”

Let’s say that we want to write a single-view app that allows users with an external keyboard to press Command-Alt-Control-N to see an alert controller. When our view is loaded, we will create the command and add it to our view controller:

  override func viewDidLoad() {
    super.viewDidLoad()

    let command = UIKeyCommand(input: "N",
      modifierFlags: .Command + .Alternate + .Control,
      action: "handleCommand:")

    addKeyCommand(command)

  }

As you can see, I am using the + operator between items of type UIKeyModifierFlags. This operator by default does not exist, so let’s write a generic operator method that enables this functionality for us:

func +<T: OptionSetType where T.RawValue : SignedIntegerType>
  (lhs: T, rhs: T) -> T{
  return T(rawValue: lhs.rawValue | rhs.rawValue)
}

When the command is issued, iOS will attempt to call the method that we have specified. In there, let’s show the alert:

  func handleCommand(cmd: UIKeyCommand){

    let c = UIAlertController(title: "Shortcut pressed",
      message: "You pressed the shortcut key", preferredStyle: .Alert)

    c.addAction(UIAlertAction(title: "Ok!", style: .Destructive, handler: nil))

    presentViewController(c, animated: true, completion: nil)

  }

Open this in the simulator. From the Hardware menu, select Keyboard, and then select the Connect Hardware Keyboard menu item (see Figure 3-26). While the focus is on the simulator, press the aforementioned key combinations and see the results for yourself.

Figure 3-26. You can enable a hardware keyboard even in the simulator. This is necessary to test the output of this recipe.

3.11 Recording the Screen and Sharing the Video

Problem

You want your user to be able to record their screen while in your app and then edit and save the results. This is really important for games providing replay functionality to gamers.

Solution

Follow these steps:

  1. Import ReplayKit.
  2. Get a recorder of type RPScreenRecorder using RPScreenRecorder.sharedRecorder().
  3. Call the available property of the recorder to see whether recording is available.
  4. Set the delegate property of the recorder to your code and conform to the RPScreenRecorderDelegate protocol.
  5. Call the startRecordingWithMicrophoneEnabled(_:handler:) method of the recorder.
  6. Wait until your handler method is called and then check for errors.
  7. If no error occurred, once you are done with recording, call the stopRecordingWithHandler(_:) on the same recorder object.
  8. Wait for your handler to be called. In your handler, you’ll get an instance of the RPPreviewViewController class.
  9. Set the previewControllerDelegate property of the preview controller to your code and conform to the RPPreviewViewControllerDelegate protocol.
  10. Preset your preview controller.

Discussion

When playing games, you might be given the option to record your screen for later playback or sharing with others. So let’s define our view controller:

import UIKit
import ReplayKit

class ViewController: UIViewController, RPScreenRecorderDelegate,
RPPreviewViewControllerDelegate {
  ...

Set up your UI as shown in Figure 3-27. The start and stop buttons are self-explanatory. The segmented control is there just so you can play with it while recording and then see the results after you’ve stopped the playback.

Figure 3-27. Initial layout

I hook up the buttons to my code:

  @IBOutlet var startBtn: UIButton!
  @IBOutlet var stopBtn: UIButton!

And here I’ll define my delegate methods:

  func previewControllerDidFinish(previewController: RPPreviewViewController) {
    print("Finished the preview")
    dismissViewControllerAnimated(true, completion: nil)
    startBtn.enabled = true
    stopBtn.enabled = false
  }

  func previewController(previewController: RPPreviewViewController,
    didFinishWithActivityTypes activityTypes: Set<String>) {
      print("Preview finished activities (activityTypes)")
  }

  func screenRecorderDidChangeAvailability(screenRecorder: RPScreenRecorder) {
    print("Screen recording availability changed")
  }

  func screenRecorder(screenRecorder: RPScreenRecorder,
    didStopRecordingWithError error: NSError,
    previewViewController: RPPreviewViewController?) {
    print("Screen recording finished")
  }

The previewControllerDidFinish(_:) method is important, because it gets called when the user is finished with the preview controller. Here you’ll need to dismiss the preview controller.

Then I’ll define my recorder object:

  let recorder = RPScreenRecorder.sharedRecorder()

When the record button is pressed, I’ll see whether recording is possible:

    startBtn.enabled = true
    stopBtn.enabled = false

    guard recorder.available else{
      print("Cannot record the screen")
      return
    }

If it is, I’ll start recording:

    recorder.delegate = self

    recorder.startRecordingWithMicrophoneEnabled(true){err in
      guard err == nil else {
        if err!.code == RPRecordingErrorCode.UserDeclined.rawValue{
          print("User declined app recording")
        }
        else if err!.code == RPRecordingErrorCode.InsufficientStorage.rawValue{
          print("Not enough storage to start recording")
        }
        else {
          print("Error happened = (err!)")
        }
        return
      }

      print("Successfully started recording")
      self.startBtn.enabled = false
      self.stopBtn.enabled = true

    }
Note

I am checking the error codes for specific ReplayKit errors such as RPRecordingErrorCode.UserDeclined and RPRecordingErrorCode.InsufficientStorage.

The first time you attempt to record the user screen in any app, the user will be prompted to allow or disallow this with a dialog that looks similar to that shown in Figure 3-28.

Figure 3-28. Permission to record the screen is requested from the user

Now when the user is finished recording and presses the stop button, I’ll stop the recording and present the preview controller:

    recorder.stopRecordingWithHandler{controller, err in

      guard let previewController = controller where err == nil else {
        self.startBtn.enabled = true
        self.stopBtn.enabled = false
        print("Failed to stop recording")
        return
      }

      previewController.previewControllerDelegate = self

      self.presentViewController(previewController, animated: true,
        completion: nil)

    }

The preview controller looks like this Figure 3-29.

Figure 3-29. The user is previewing what she recorded on the screen earlier and can save and share the results
Note

Throughout this whole process, your app doesn’t get direct access to the recorded content. This protects the user’s privacy.

See Also

Recipe 7.1

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset