Chapter 6. The User Interface

UIKit is the main framework for working with various UI components on iOS. You can use other frameworks, such as OpenGL, to build your own UI the way you want, without being constrained by UIKit, but almost all developers use UIKit at some stage in their applications to bring intuitive user interfaces to their apps. One of the main reasons for this is that UIKit by default takes advantage of all the latest technologies in iOS and is kept up to date. For instance, many years back when Apple started producing Retina displays for iOS devices, all apps that were using UIKit could take advantage of the much sharper resolution afforded by Retina displays without requiring an update to their UIKit components. Applications that were using other technologies for rendering text had to update their apps to conform with Retina displays.

In this chapter, we will have a look at some of the most interesting features of UIKit and playgrounds.

6.1 Animating Views

Problem

You have an instance of UIView and you would like to apply various animations to it, such as changing its background color inside an animation block.

Solution

Use the UIViewPropertyAnimator class and specify the properties of your views that you would like to animate, including their new values. For instance, you can instantiate UIViewPropertyAnimator and set a delay and an animation length, and then change the background color of your view instances inside the animation block of your UIViewPropertyAnimator instance. You can then simply call the startAnimation() function on this instance to start the animation(s).

Discussion

Let’s have a look at an example. Create a single view application in Xcode (see Figure 6-1). In your Main.storyboard file, place a UIView instance in the middle of the screen and then connect it to your view controller, under the name animatingView. So now the top part of your view controller should look like this:

import UIKit

class ViewController: UIViewController {

  @IBOutlet var animatingView: UIView!

  ...
            
Figure 6-1. Create an application using this template

Our goal in this recipe is to change the background color of this new view to a random color every time the user taps on the view; in addition, we would like this color change to be animated. So go to Interface Builder and in the Object Library, find Tap Gesture Recognizer (see Figure 6-2) and drag and drop it into your newly created view. Then connect the tap gesture recognizer’s Sent Actions outlet to your view controller under a new method called animatingViewTapped(_:) (see Figure 6-2). The tap gesture recognizer placed on our view controller associates the gesture recognizer with that view.

Figure 6-2. New view

In our view controller we will define an array of colors of type UIColor. Later we will pick a random one and assign it to this view whenever the user taps on it:

let colors: [UIColor] = [
 .red,
 .blue,
 .yellow,
 .orange,
 .green,
 .brown
]
            

Imagine picking a random color from this array of colors. What if that random color is the same color as the one currently assigned to the view? We need an algorithm that can pick a color that is not equal to the view’s current color. So let’s write that function.

func randomColor(notEqualTo currentColor: UIColor) -> UIColor{

  var foundColor = currentColor

  repeat{
    let index = Int(arc4random_uniform(UInt32(colors.count)))
    foundColor = colors[index]
  } while foundColor.isEqual(currentColor)

  return foundColor

}
            

In this function we use the repeat...while syntax in order to find a random value. We then compare it with the current color and if they are the same, repeat this process until we find a color that is not the same as the old one.

Last but not least, we need to program our animatingViewTapped(_:) function and use an instance of UIViewPropertyAnimator to animate the change of background color of our view. And for that we can use the init(duration:curve:animations:) initializer of UIViewPropertyAnimator. duration is a value of type TimeInterval, which is the duration of the animation in seconds. curve is of type UIViewAnimationCurve. animations, which is where you will actually do your animations, is a block that has no parameters and no return value. Once done, we call the startAnimation() method of our property animator:

@IBAction func animatingViewTapped(_ sender: AnyObject) {

  let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeIn){
    [weak animatingView, weak self] in

    guard
      let view = animatingView,
      let strongSelf = self,
      let viewBackgroundColor = view.backgroundColor
      else {return}

    view.backgroundColor = strongSelf.randomColor(
      notEqualTo: viewBackgroundColor)

  }

  animator.startAnimation()

}
            

Have a look at the code now in the simulator. When you see the view in the center of the screen, tap on it and watch how the background color changes!

6.2 Attaching Live Views to Playgrounds

Problem

You are working on a UIView instance (or one of its subclasses, such as UITableViewCell), are constantly making changes to it in order to get it right, and would like to see your changes continuously without having to re-compile and re-run your app on the simulator.

Solution

Xcode now allows you to simulate screens the way the user sees them in special environments known as playgrounds. Follow these steps to add a live view to your playground:

  1. Import the PlaygroundSupport framework into your playground with the import statement.
  2. Set an instance of UIView or UIViewController to the PlaygroundPage.current.liveView property, which is of type PlaygroundLiveViewable?.
  3. Press Command-Alt-Enter on your keyboard while on Xcode to show the assistant editor. After attaching a live view to your playground, you can see the view at all times as you make changes to it, in the assistant editor (Figure 6-3).
Figure 6-3. Our live view is displayed in the assistant editor

Discussion

Live views are great for seeing what you’re doing while making rapid changes to a view or a view controller. The traditional way of making rapid changes to a view or a view controller and seeing the changes was to write the code first, then compile and run the application, which takes a lot more time than seeing your changes live in the playground.

The liveView property of the current playground is of type PlaygroundLiveViewable?, which itself is a protocol that is defined as shown here:

public protocol PlaygroundLiveViewable {

 /// A custom `PlaygroundLiveViewRepresentation` for this instance.
 ///
 /// The value of this property can but does not need to be the same every time;
 /// PlaygroundLiveViewables may choose to create a new view or view controller
 /// every time.
 /// - seealso: `PlaygroundLiveViewRepresentation`
 public var playgroundLiveViewRepresentation:
   PlaygroundSupport.PlaygroundLiveViewRepresentation { get }
}
            

It expects conforming objects to it to implement a playgroundLiveViewRepresentation property of type PlaygroundSupport.PlaygroundLiveViewRepresentation. That’s an enumeration defined in this way:

public enum PlaygroundLiveViewRepresentation {

 /// A view which will be displayed as the live view.
 ///
 /// - note: This view must be the root of a view hierarchy
 /// (i.e., it must not have a superview), and it must *not* be
 /// owned by a view controller.
 case view(UIView)

 /// A view controller whose view will be displayed as the live 
 /// view.
 /// - note: This view controller must be the root of a view
 /// controller hierarchy (i.e., it has no parent view controller),
 /// and its view must *not* have a superview.
 case viewController(UIViewController)
}
            

In other words, every UIView or UIViewController instance can be placed inside the liveView property:

import UIKit
import PlaygroundSupport

extension Double{
  var toSize: CGSize{
    return .init(width: self, height: self)
  }
}

extension CGSize{
  var toRectWithZeroOrigin: CGRect{
    return CGRect(origin: .zero, size: self)
  }
}

let view = UIView(frame: 300.toSize.toRectWithZeroOrigin)
view.backgroundColor = .blue
PlaygroundPage.current.liveView = view
            

This means that custom objects that can be represented and drawn in a UIView instance, such as a Person structure, can conform to the PlaygroundLiveViewable protocol and then be assigned to the liveView property of your playground. This procedure allows you to modify the view representation of the object rapidly and see the changes immediately in the playground.

6.3 Running Playgrounds as Interactive and Continuous Apps

Problem

You want your playground code to have a main loop to emulate a real iOS app that doesn’t just run from start to finish, but rather lives for as long as the user presses the stop (or home) button. This will allow you to create interactive applications even in your playgrounds, when mixed with what you learned in Recipe 6.2.

Solution

Set the needsIndefiniteExecution: Bool property of your current playground to true when you need it to run indefinitely. Once you are done with your work, you can set this property back to false (its default value).

Note

You access this property by first importing the PlaygroundSupport framework. Then you can access this property through PlaygroundPage.current.needsIndefiniteExecution.

Discussion

Let’s have a look at an example. Say that you are designing a view similar to the one we saw in Recipe 6.2 and you are testing the addition of a new tap gesture recognizer. You want to make sure you get a callback when the user taps on the view. Follow these steps:

  1. Make sure to ask for infinite execution time for your playground so that your app can run until you tap on the view, at which point your code can take action, such as to terminate execution:

    import UIKit
    import PlaygroundSupport
    
    PlaygroundPage.current.needsIndefiniteExecution = true
                
  2. Subclass UIView and add your own tap gesture recognizer to it upon initialization. When the tap has come in, finish the execution of the playground with PlaygroundPage.current.finishExecution():

    class TappableView : UIView{
    
      @objc func handleTaps(_ sender: UITapGestureRecognizer){
        PlaygroundPage.current.finishExecution()
      }
    
      override init(frame: CGRect) {
        super.init(frame: frame)
        let recognizer = UITapGestureRecognizer(target: self, action:
          #selector(TappableView.handleTaps(_:)))
        addGestureRecognizer(recognizer)
      }
    
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
    }
                
  3. The rest is reasy! Simply instantiate this view and set it as the liveView of your playground:
extension Double{
  var toSize: CGSize{
    return .init(width: self, height: self)
  }
}

extension CGSize{
  var toRectWithZeroOrigin: CGRect{
    return CGRect(origin: .zero, size: self)
  }
}

let view = TappableView(frame: 300.toSize.toRectWithZeroOrigin)
view.backgroundColor = .blue
PlaygroundPage.current.liveView = view
            

6.4 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 6-4.

Figure 6-4. Vertical and horizontal views

Prior to the latest Xcode version with support for stacked views, we had to set up massive amounts of constraints just to achieve a simple layout like Figure 6-4. 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 6-5.

Figure 6-5. Stacked images

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

Figure 6-6. 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 6-4.

6.5 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 so that we can see more clearly how this works. I want us to achieve the effect shown in Figure 6-7 when running our app on iPhone in portrait mode.

Figure 6-7. 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 6-8).

Figure 6-8. 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 6-6) 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 6-9).

Figure 6-9. 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 6-6). 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 pop up, 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. In this box, set the value to 10 (see Figure 6-10).

Figure 6-10. Set the value to 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. From the pop up that appears, choose Width (see Figure 6-11).

Figure 6-11. Choose the Width option in the pop up 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 6-12).

Figure 6-12. 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 the 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).

6.6 Creating Anchored Constraints in Code

Problem

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

Solution

Use the new anchor properties on UIView (for example, 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 6-6), under the Selected Views, choose the Add Missing Constraints option (see Figure 6-13).

Figure 6-13. 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 6-14). 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 6-14. 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.constraint(equalTo: btn1.trailingAnchor,
   constant: 10).isActive = 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 6-15. 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 6-15. 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 6-16).

Figure 6-16. 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 6-17).
Figure 6-17. 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.constraint(equalTo: btn2.widthAnchor,
  constant:0).isActive = true

6.7 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

To solve this problem, 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], for: .default)

 let settings = UIUserNotificationSettings(
   types: .alert, categories: [category])

 UIApplication.shared.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: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {

   registerForNotifications()

   return true
}
            

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

func application(_ application: UIApplication,
 didFinishLaunchingWithOptions
 launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {

   registerForNotifications()

   return true
}
            

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

func application(_ application: UIApplication,
 didFinishLaunchingWithOptions
 launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {

   registerForNotifications()

   return true
}
            

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?,
  for notification: UILocalNotification,
  withResponseInfo responseInfo: [AnyHashable : Any],
  completionHandler: @escaping () -> 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 6-18).

Figure 6-18. 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 6-19).

Figure 6-19. 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 6-20).

Figure 6-20. Entering text in a local notification

See Also

Recipes 6.2 and 6.3

6.8 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 6-21. 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 6-21. 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...", for: UIControlState())
  btn.addTarget(self, action: #selector(ViewController.addNewItem),
                for: .touchUpInside)
  return btn
}

func addNewItem(){
  let n = rightStack.arrangedSubviews.count
  let v = lblWithIndex(n)
  rightStack.insertArrangedSubview(v, at: 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.constraint(equalTo: view.trailingAnchor,
                                       constant: -20).isActive = true
  rightStack.topAnchor.constraint(
    equalTo: topLayoutGuide.bottomAnchor).isActive = true

}
            

6.9 Showing Web Content in Safari View Controller

Problem

You want to take advantage of 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 6-22.

Figure 6-22. 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, t.characters.count > 0,
    let u = URL(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
  present(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) {
  dismiss(animated: 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.

6.10 Laying Out Text-Based Content on Your Views

Problem

You would like 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 jump right into 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.green
label.text = "Hello, World"
label.sizeToFit()
view.addSubview(label)

label.leadingAnchor.constraint(
  equalTo: view.readableContentGuide.leadingAnchor).isActive = true

label.topAnchor.constraint(
  equalTo: view.readableContentGuide.topAnchor).isActive = true

label.trailingAnchor.constraint(
  equalTo: view.readableContentGuide.trailingAnchor).isActive = true

label.bottomAnchor.constraint(
  equalTo: view.readableContentGuide.bottomAnchor).isActive = true
            

6.11 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 6-23).

Figure 6-23. 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, let event = event,
    let allTouches = event.coalescedTouches(for: touch),
    allTouches.count > 0 else{
      return
  }

  points += allTouches.map{$0.location(in: self)}

  setNeedsDisplay()

}
            

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

override func touchesBegan(_ touches: Set<UITouch>,
                            with 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>,
                               with event: UIEvent?) {

  points.removeAll()
  setNeedsDisplay(bounds)

}
            

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

override func touchesMoved(_ touches: Set<UITouch>,
                           with event: UIEvent?) {

  drawForFirstTouchInSet(touches, event: event)

}
            

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>,
                           with event: UIEvent?) {

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

  points += predictedTouches.map{$0.location(in: self)}
  setNeedsDisplay()

}
            

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

override func draw(_ rect: CGRect) {

  let con = UIGraphicsGetCurrentContext()

  // set background color
  con?.setFillColor(UIColor.black.cgColor)
  con?.fill(rect)

  con?.setFillColor(UIColor.red.cgColor)
  con?.setStrokeColor(UIColor.red.cgColor)

  for point in points{

    con?.move(to: point)

    if let last = points.last, point != last{
      let next = points[points.index(of: point)! + 1]
      con?.addLine(to: next)
    }

  }

  con?.strokePath()

}
            

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.

6.12 Supporting Right-to-Left Languages

Problem

You are internationalizing your app and, as part of this process, need to support languages that are written from right to left, 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, preferably 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 semantics 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 6-24. There is one text field and four buttons.

Figure 6-24. 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 6-25). 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 6-26 on IB.

Figure 6-25. Horizontal spacing between buttons
Figure 6-26. 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 the 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 6-27).

Figure 6-27. 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 6-28).

Figure 6-28. 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:

import UIKit

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
      .userInterfaceLayoutDirection(
        for: 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 righthand side.

6.13 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. In a word processing program, 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: #selector(ViewController.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: OptionSet>
  (lhs: T, rhs: T) -> T where T.RawValue : SignedInteger{
  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))

  present(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 6-29). While the focus is on the simulator, press the aforementioned key combinations and see the results for yourself.

Figure 6-29. You can enable a hardware keyboard even in the simulator; this is necessary to test the output of this recipe

6.14 Recording the Screen and Sharing the Video

Problem

You want users 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. After you have imported ReplayKit, 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(_:) method 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

The ability to record what’s happening on the screen often comes in handy for users, particularly gamers who might want to share a particularly cool sequence of game play with their friends. To enable this, we first need to define our view controller:

import UIKit
import ReplayKit

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

Set up your UI as shown in Figure 6-30. 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 6-30. 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")
  dismiss(animated: true, completion: nil)
  startBtn.isEnabled = true
  stopBtn.isEnabled = 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: Error,
                    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.shared()
            

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

startBtn.isEnabled = true
stopBtn.isEnabled = false

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

If it is, I’ll start recording:

recorder.delegate = self

recorder.startRecording {[weak self]err in

  guard let strongSelf = self else {return}

  if let error = err as? NSError{
    if error.code == RPRecordingErrorCode.userDeclined.rawValue{
      print("User declined app recording")
    }
    else if error.code == RPRecordingErrorCode.insufficientStorage.rawValue{
      print("Not enough storage to start recording")
    }
    else {
      print("Error happened = (err!)")
    }
    return
  } else {
    print("Successfully started recording")
    strongSelf.startBtn.isEnabled = false
    strongSelf.stopBtn.isEnabled = 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 6-31.

Figure 6-31. 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.stopRecording{controller, err in

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

  previewController.previewControllerDelegate = self

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

}
            

The preview controller looks like that shown in Figure 6-32.

Figure 6-32. 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.

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

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