Tag Archives: storyboard

Basic Tap, Pinch and Rotate Gestures

Many controls such as table views, map views, scroll views and buttons use gestures. You as a developer might want to use a gesture for your own purposes outside of these controls. The gesture recognizer classes can do that. The standard ones such a as tap, pinch and rotate are rather easy to set up, either by storyboard or by code. In this tutorial, I’ll show you how to set them up in code.

Set Up the Project

Make a new  single view project called TapPinchDemo in Swift with a Universal device.  Go to the storyboard. Add a label to the upper left of the storyboard. I used auto layout to pin the label 10up, 10 left and 10 right. Change the label’s font to Title.

Open the assistant editor.  Control drag from the label to the code and make an outlet named statusLabel.  Close the assistant editor and go to the ViewController.swift code. Add a constant at the beginning of the ViewController class for a color.

 let background = UIColor(red: 1.0, green: 0.98, blue: 0.96, alpha: 1.0)

In viewDidLoad add this color as the background.

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = background
}

Tap Gestures

There are three parts to a gesture: Configuring a gesture recognizer object, making an action for the object, and adding it to the view.  For the first gesture, you’ll make a tap gesture.  In viewDidLoad add the following line to make a tap gesture:

 let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction(sender:)))

There’s two parameters in the constructor. Both indicate the location of an action that will be called when the gesture occurs. the first give the class, and the second the selector. selectors are functions called by the parameters of another function. While really a Objective-C thing, Swift had a compiler tag #selector() to indicate a selector. In the tap gesture tapAction(selector:) is the function called.

You’ll notice an error. Once you use a #selector, you must implement the function.  Add the function tapAction
above viewDidLoad

func tapAction(sender:UITapGestureRecognizer){
}

We’ll come back in a minute to finish filling out this code. Go back to viewDidLoad and configure tapGesture.

tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1

This sets the gesture recognizer to one tap with one finger. Add the gesture recognizer to the view using the addGestureRecognizer method.

view.addGestureRecognizer(tapGesture)

Go back up to the function tapAction. Gestures have states. You must check the state before you do anything. Specific states are necessary for specific gestures to work. For a tap, the state must be .ended. Add this code to the tap action selector:

if sender.state == .ended{
    statusLabel.text = "Tapped"
    view.backgroundColor = background
}

Build and run. Tap the screen and the label reads tapped.

2016-11-28_05-51-41

Pinch Gesture

The pinch gesture is a movement of two fingers moving towards or away from eacth other. Pinch gestures have no properties you need to set. Adding them is even easier than a tap. Just call the UIPinchGestureRecognizer constructor and add the gesture recognizer to the view. Add this to viewDidLoad.

 let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchAction(sender:)))
        view.addGestureRecognizer(pinchGesture)

The action specified is a bit more complex than the tap. You have three states you might want to look at: .began, .changed, and .ended. Most often your concern is the changed state which occurs when a user moves his or her fingers. But you might find uses for beginning and ending a pinch. Add the following method to the ViewController code.

func pinchAction(sender:UIPinchGestureRecognizer){
        if sender.state == .began{
            statusLabel.text = "Pinch Began"
            view.backgroundColor = UIColor.yellow
        }
        if sender.state == .changed{
            statusLabel.text = String(format:"Pinch scale: %1.3f",sender.scale)
        }
        if sender.state == .ended{
            statusLabel.text = "Pinch Ended"
            view.backgroundColor = background
        }
    }

Pinches are examples of continuous gestures. These are gestures which will have more than one state to watch for. This changes the background when you have started the pinch and restores when you finish the pinch. When you change the value, it shows the scale value for the gesture. Scale is a value that shows the change between the user’s first pinch and the movement of the user’s fingers. Scale is the value you use in your applications. Build and run. Try pinching the screen. In the simulator, hold down the option key and drag on the trackpad or mouse.

Rotation Gesture

The last gesture we’ll discuss is rotation. Rotations set up just like pinches, except they use a UIRotationGestureRecognizer constructor. Add this to viewDidLoad:

let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotateAction(sender:)))
view.addGestureRecognizer(rotateGesture)

And add this function for the action. Like pinch, rotate is a continuous gesture, so I’ll set it up similar to  pinchAction.

func rotateAction(sender:UIRotationGestureRecognizer){
        if sender.state == .began{
            statusLabel.text = "Rotate Began"
            view.backgroundColor = UIColor.cyan
        }
        if sender.state == .changed{
            statusLabel.text = String(format:"rotation: %1.3f",sender.rotation)
        }
        if sender.state == .ended{
            statusLabel.text = "Rotate Ended"
            view.backgroundColor = background
        }
    }

The code will turn the background cyan when the user starts a rotate action. The display will give the rotation angle in radians using the property of a rotator gesture rotation. When the rotation ends with  the user removing fingers, the background returns to the default background color. Build and run. To simulate a rotation click down on the simulator using the mouse and then press Option(it’s annoyingly tricky). Move the cursor and you will have a rotation gesture.

This is only the beginning to using gestures, but should give you a good foundation for other gestures. While I most often use code to add gestures, you can also add then by dragging them onto the storyboard. Once added, you find them in the document outline, where you can control drag them to the code like any other object and make an action. Gestures my also conflict with each other, so try to keep the number in a single view to a minimum.

The Whole Code

//
//  ViewController.swift
//  tapSwipeDemo
//
//  Created by Steven Lipton on 11/25/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var statusLabel: UILabel!
    let background = UIColor(red: 1.0, green: 0.98, blue: 0.96, alpha: 1.0)
    func tapAction(sender:UITapGestureRecognizer){
        
        if sender.state == .ended{
            statusLabel.text = "Tapped"
            view.backgroundColor = background
        }
    }
    func pinchAction(sender:UIPinchGestureRecognizer){
        if sender.state == .began{
            statusLabel.text = "Pinch Began"
            view.backgroundColor = UIColor.yellow
        }
        if sender.state == .changed{
            statusLabel.text = String(format:"Pinch scale: %1.3f",sender.scale)
        }
        if sender.state == .ended{
            statusLabel.text = "Pinch Ended"
            view.backgroundColor = background
        }
    }
    
    func rotateAction(sender:UIRotationGestureRecognizer){
        if sender.state == .began{
            statusLabel.text = "Rotate Began"
            view.backgroundColor = UIColor.cyan
        }
        if sender.state == .changed{
            statusLabel.text = String(format:"rotation: %1.3f",sender.rotation)
        }
        if sender.state == .ended{
            statusLabel.text = "Rotate Ended"
            view.backgroundColor = background
        }
    }

    func swipeAction(sender:UISwipeGestureRecognizer){
        if sender.state == .began{
            statusLabel.text = "Swipe!"
        }
    }
   
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = background
        
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction(sender:)))
        tapGesture.numberOfTapsRequired = 1
        tapGesture.numberOfTouchesRequired = 1
        view.addGestureRecognizer(tapGesture)
        
        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchAction(sender:)))
        view.addGestureRecognizer(pinchGesture)
        
        let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotateAction(sender:)))
        view.addGestureRecognizer(rotateGesture)
        
    }
    

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

Adding Modal Views and Popovers in Swift 3.0

Modal views are one of the fundamental ways to present view controllers. Some early applications on the iPhone were completely modal views — some still do.  Modals do not do their own housekeeping like navigation controllers or tab controllers, so they are not as efficient for the backbone of your application. They need the user’s attention and suspend everything else in your UI,  making them good for settings pages and the like.

Developed originally for the iPad, Popovers are similar to modals. They are a type of view controller that requires user attention. Modals take over the window, which on the bigger real estate of the iPad, is often a waste of space. Popovers use limited space and place a smaller view on top of the main window.

Apple in iOS8 merged popovers and alerts into modals to create the current adaptive layout system. Instead of the developer having to write different code for different devices, adaptive layout does the heavy lifting and the developer writes code once. In this lesson we’ll show several ways to add popovers and modals to different devices.

Set Up the Project

First we’ll need a new project. In Xcode 6, go to File>New>Project… or hit Command-Shift-N. Create a Single-View project called Swift3PizzaPopover, set the language to Swift. Set the device to Universal, then save the project to a place you’d like.
We will be using some photos. You can use your own, here are the two I used:
pizza_sm

popover_sm

These are the full-size images. Right-Click and save the images from your browser to some folder or your desktop on your Mac. In Xcode, click on the Assets.xcassets, and from the folder you saved the images drag them into the assets work area.

2016-06-29_05-37-20

Go into the storyboard. In the lower left hand side of the storyboard, you  will find the Class Size Preview Button with the words View as: iPhone6s(wC hR)

2016-06-28_05-29-33

Click this button to show the preview bar.

2016-06-28_05-39-43

We’ll skip auto layout in this lesson and use some drag and drop layout. To make sure our controls show up on all devices we’ll lay out for the smallest device. Select the iPhone 4s device, leaving the orientation in portrait.

2016-06-29_06-02-06

 

Click the color strip in the Background Color property for the controller, In the color picker that appears, click the sliders button if not already pressed, and in the drop down change to RGB Color.

2016-06-29_06-21-50

Change the view’s background color to Hex Color #FCE8E5. Drag four buttons on the scene. On the two top buttons, remove the title and add the pizza and popover images we just loaded as the button image. On the button below the pizza image button, change the title to Pizza Modal and set the font size to 26 points. Change the title of the button to Popover and set the font size to 26 points. Arrange the buttons to something like this:

2016-06-29_06-04-54

Now drag two more view controllers out. Change each to have different background colors. I used  a background color of #D337C8  for one and the other #FDDB28

2016-06-29_06-13-36

Change to the media library by selecting the clip of film in the lower right.

2016-06-29_06-08-04

In the first view controller, add an UImageView of the pizza by dragging the pizza image to one of the new controllers. Do the same with the popover image in the other controller.

2016-06-29_06-16-54

Click the square-in-circle button to go back to the object library, and drag a label to each of these view controllers. Change the text to read BBQ Chicken Pizza and Cheddar Popover respectively. Make the font for each 20 points.

Unlike Navigation controllers, you need to explicitly dismiss a modal controller.  We’ll need a Done button. Add a button to the modal view, labeled Done at 26 points font size. Make the background Red(#FF0000) with  White(#FFFFFF) text. Popovers dismiss when anywhere outside the popover gets a touch event. However we’ll find that popovers aren’t always popovers, so cut and paste this button to the popover view.

When done, your two controllers should look like this:

2016-06-29_06-32-21

Hit Command-N to get a New File. Under iOS click Source> Cocoa Touch Class. Create a new view controller subclassing UIViewController called PizzaModalViewController. Repeat and make a UIViewController subclass called PopoverViewController Assign the subclasses to their respective view controllers using the identity inspector. Your  finished story board should look like this:

2016-06-29_06-39-06

Using Segues Directly

The easiest way of using segues and modals is to directly connect a segue from the button to the view controller. In the storyboard, control-drag from the pizza picture button  to the PizzaModalVC view. In the menu that appears, select Present Modally.

2016-06-29_06-44-57

Click on the circle for the segue and set the identifier to Pizza.

2016-06-29_07-03-41

Now for the popover, control-drag from the popover photo to the PopoverVC view. Select Present as Popover.

2016-06-29_06-48-03

Select the segue, then set the identifier to Popover in the attributes inspector.

2016-06-29_06-50-03

Select an iPad Air 2 in the simulator. Build and run.

2016-06-29_06-53-27

Click on the Popover picture. You will get this:

2016-06-29_06-54-37

Tap anywhere but the popover and the popover disappears. Tap the pizza picture, and the pizza appears from the bottom. However, we can’t get rid of it.

2016-06-29_06-56-03

We did not yet make the dismissal code. Stop the simulator, and go back to the storyboard. Bring up the assistant editor, and select the PizzaModalVC view. Connect the Done button to the view controller code by selecting the Done button and then control dragging the button to the view controller.  Select an action for the connection as a UIButton. Call this pizzaModalDone Do the same for the popover, calling the method popoverDone.

In the PizzaModalVC class add the following to pizzaModalDone:

@IBAction func pizzaModalDone(_ sender: UIButton) {
    dismiss(animated: true, completion: nil)
}

In the PopoverVC class, add the following to popoverDone:

@IBAction func popoverDone(_ sender: UIButton) {
    dismiss(animated: true, completion: nil)
}

We dismiss any modal view controller with dismiss.  It is a method of the modal view controller, hence we are calling it from within the class. It has two arguments. The first indicates if an animation should   accompany dismissal. This is the usual way to dismiss, However for speed when dismissing a modal view behind another view, you can set this to false. The second argument is a closure if you need to do anything after successfully dismissing the view controller. Generally I leave it nil.

Build and run. Now both Done buttons work, and well as clicking in the shaded area outside the popover.

Presenting a Segue Programmatically

Go back to the storyboard. In our main view controller add this above viewDidLoad():

@IBAction func openPizzaModal(_ sender: UIButton) {
        performSegue(withIdentifier:"Pizza", sender: self)
    }
    @IBAction func openPopover(_ sender:UIButton){
        performSegue(withIdentifier:"Popover", sender: self)
    }

Connect the openPizzaModal(sender:) to the Pizza Modal button and the openPoppover(sender:) to the Popover button by dragging from the circle to the correct button. Build and run. Now you can use the bottom text buttons as well as the photos. Line 2 and 5 use the performSegue(withIdentifier:) method. When we set up our segues we gave them identifiers. We’ve used that identifer to call the segue. If you want all your buttons to use segues programmatically, control drag from the source view controller icon in the storyboard to the destination view controller.

Programmatically Presenting a Modal View Controller from the Storyboard

When using a storyboard, we can use the storyboard identifier to do that.  Go to the story board and select the pizza view controller. In the Identity inspector change the Storyboard ID to Pizza

2016-06-29_08-13-46

We’ll use this identifier to call our view controller from the storyboard. Add another button Pizza Prog with 26 point type to the root controller next to the Pizza Modal Button

2016-06-29_08-18-00

Open the assistant editor and control drag the new button tot he code. Make another action openPizzaProgModal in the ViewController class.

In ViewController.swift and change openPizzaProgModal to this:

@IBAction func openPizzaProgModal(_ sender: UIButton) {
    let vc = (
        storyboard?.instantiateViewController(
        withIdentifier: "Pizza")
    )!
    vc.view.backgroundColor = UIColor.orange()
    vc.modalTransitionStyle = .crossDissolve
    present(vc, animated: true, completion: nil)
}

Line two gets the view controller from the storyboard using the Storyboard Identifier we set up. If these two do not not match exactly you will get a fatal error.

Line 8 is the core of this. The present method presents the modal view, with animation and no completion handler.  It’s sort of the bracket to the dismiss method, with almost the same arguments. There is one difference. In dismiss, the object dismissed calls dismiss. In present, the current view controller calls present, and the first argument is the view controller to be presented.  Pretty much all presentation of view controllers look exactly like this.

When using present, there is no prepare(for segue:)  equivalent, which you would have with performSegue or running from a storyboard segue.  Once we have the view controller,  we change its properties directly, such as changing the background to orange in line 6. We can also change the transition to a cross dissolve, using the property modaltransitionStyle.

Build and run. Select the Pizza Modal button and you will see our flip transition and a different layout than the segue version.

2016-06-29_12-00-21

Programmatically Presenting a Modal View

The present method can be used with any view controller. If you made your own programmatic one, you need only an instance of the view controller to present it.

Press Command-N and add a new Cocoa Touch Class named PizzaModalProgViewController subclassing UIViewController.

I won’t go into details about this code, but suffice it to say it creates a view controller with a blue Done button, a label, and an image.

class PizzaModalProgViewController: UIViewController {
    let dismissButton:UIButton! = UIButton(type:.custom)
    var imageName = "pizza_sm"
    var text = "BBQ Chicken Pizza!!!!"
    
    let myLabel = UILabel()
    
    func pizzaDidFinish(){
        dismiss(animated: true, completion: nil)
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Build a programmatic view
        
        //Set the Background color of the view.
        view.backgroundColor = UIColor(
            red: 0.8,
            green: 0.6,
            blue: 0.3,
            alpha: 1.0)
        // Add the image to the view
            let myImage:UIImage! = UIImage(named: imageName)
            let myImageView = UIImageView(image: myImage)
            myImageView.frame = view.frame
            myImageView.frame = CGRect(
                x: 10,
                y: 10,
                width: 200,
                height: 200)
            view.addSubview(myImageView)
        // Add the label to the view
        myLabel.text = text
        myLabel.frame = CGRect(
            x: 220,
            y: 10,
            width: 300,
            height: 150)
        myLabel.font = UIFont(
            name: "Helvetica",
            size: 24)
        myLabel.textAlignment = .left
        view.addSubview(myLabel)
        
    //Add the Done button to the view.
        let normal = UIControlState(rawValue: 0) //.normal doesn't exist in beta. workaround
        dismissButton.setTitle("Done", for: normal)
        dismissButton.setTitleColor(UIColor.white, for: normal)
        dismissButton.backgroundColor = UIColor.blue
        dismissButton.titleLabel!.font = UIFont(
            name: "Helvetica",
            size: 24)
        dismissButton.titleLabel?.textAlignment = .left
        dismissButton.frame = CGRect(
            x:10,
            y:275,
            width:400,
            height:50)
        dismissButton.addTarget(self,action: #selector(self.pizzaDidFinish),for: .touchUpInside)
        view.addSubview(dismissButton)
    }
}

Go back to ViewController.swift and the openPizzaModal action. Comment out the code for the storyboard and add a few new lines of code above it.

@IBAction func openPizzaProgModal(_ sender: UIButton) {
   //using a view controller
    let vc = PizzaModalProgViewController()
    vc.modalTransitionStyle = .partialCurl
    present(vc, animated: true, completion: nil)
 
/* //using a story board
    let vc = (storyboard?.instantiateViewController(
        withIdentifier: "Pizza"))!
    vc.view.backgroundColor = UIColor.orange
    vc.modalTransitionStyle = .crossDissolve
    present(vc, animated: true, completion: nil)
 */       
}

Since the present method uses a view controller, line 3 gets an instance of one. Line 4 changes the transition style again, this time to a page curl transition. Line 5 presents the controller, looking identical to line 12 when we presented a view controller from the storyboard..

Build and run. Click the Pizza Prog button to get a different modal than before.

2016-06-30_05-38-17

Programmatically Presenting a Popover

How to present a popover is similar to calling a modal programmatically. There are a few differences however. Change the view controller method openPopover as follows, commenting out the previous code:

@IBAction func openPopover(_ sender:UIButton){
    //performSegue(withIdentifier:"Popover", sender: self)
    let vc = PizzaModalProgViewController()
    //change properties of the view controller
    vc.text = "Cheesy!!"
    vc.imageName = "popover_sm"
    vc.view.backgroundColor = UIColor.yellow
    //present from a view and rect
    vc.modalPresentationStyle = .popover //presentation style
    present(vc, animated: true, completion: nil)
    vc.popoverPresentationController?.sourceView = view
    vc.popoverPresentationController?.sourceRect = sender.frame
}

Line three will give us the same view controller as the last example. As discussed  earlier, lines 5 through 7 set properties of the view controller, this time changing properties to use a different image and text than the default values of the class. The code sets a yellow background as well.

Line 9 tells the system to present this modal as a popover. There are two ways to present a popover, one for popovers originating in toolbar buttons and one which presents a popover attached to a CGRect and displayed in some view. Both start the same. We set a modal presentation style of .popover, then present the popover like the modal view we did for the pizza.

The difference between popovers  on a CGRect and on a bar button is in the next two lines, which must come immediately after the presentation. These two lines of code set up the popover that anchors to a CGRect and presents itself in the space of a UIView. When we use a popover to present, we get a popoverPresentationController on the modal view. This controls the appearance and presentation of the popover. The property sourceView of popoverPresentationController indicates the view that the popover will appear, which often is view. The property sourceRect indicates the CGRect that the popover will be anchored to. In this example I attached the popover to the button that opens it, which is a common occurrence.

Now build and run. Select the popover button, and it works.

2016-06-30_06-05-06

Popovers often appear from bar button items in a toolbar or navigation bar. While we need two properties for a popover off of a CGRect, for a bar button item you need only one, appropriately named barButtonItem. Though we do not use it on our project, an equivalent code for using a bar button item might be this:

@IBAction func openPopover(sender:UIBarButtonItem){
    let vc = PopoverProgVC()
    vc.modalPresentationStyle = .Popover
    presentViewController(vc, animated: true, completion: nil)
    vc.popoverPresentationController?.barButtonItem = sender      
}

You can see an example of bar button items  in the lesson about UIImagePickerController. They look like this:

2016-06-28_08-14-44

Size Classes, Modals, and Popovers

Prior to iOS8, popovers and modals were different. Starting with iOS8, popovers became a type of modal. This was due to size classes. While I go into the details about size classes in my books Swift Swift View Controllers and Practical Autolayout, the basics of size classes is that your layout changes and adapts to different size screens and orientations automatically.

Before iOS8, if you ran a popover on any iPhone, you would get an error. Now iOS just adapts. For most phones in most orientations you get a .fullscreen modal presentation. Try it. Select an iPhone 6s in the simulator, and run the app again. Select a popover and you get a modal in portrait and landscape:

2016-06-30_06-11-402016-06-30_06-14-39

The iPhone 6 plus and 6s plus in landscape will give you a .formsheet presentation style for a popover, but  .fullscreen in Portrait. Try iPhone 6s plus in the simulator.

2016-06-30_06-17-28 2016-06-30_06-17-51

Starting with iOS9 on iPads, we also have mutitasking, which will act like an iPhone for popovers. Run the simulator With an iPad air 2. I rotated my screen for better effect by pressing Command-right arrow:

2016-06-30_06-21-06

With the simulator running, hit Command-Shift-H to get back to the home screen. Select Safari as an app. Drag from the right edge of the screen towards the center.

2016-06-30_06-23-59

In the icons that appear, pick the Swift3PizzaModal app, then once loaded, click the modal once again.

2016-06-30_06-31-36

On the left you will have Safari, on the right our App, though compressed. Try the popover again and you get a .fullscreen popover.

2016-06-30_06-24-22

Drag the white bar to the left of our app, and the bar turns black. Move it to the center. We still get a .fullScreen.

2016-06-30_06-24-49

Instead of having to figure out any of these cases, the adaptive layout features of iOS does it for you. For popovers, views with a compact width get a .fullsheet modal view and ones with a regular width get a popover or .formsheet.

We’ve covered the basics of popovers and modals. Modals work anywhere, on iPhone or iPad. Popovers and some special case modals work or appear differently on iPad only. In this lesson we saw how similar popovers are to modals. There is a few more things to do with modals which we will still need to get to. In future posts, we’ll discuss the one popover bug that will definitely get you kicked out of the app store, and how to avoid it.

The Whole Code

ViewController.swift

//
//  ViewController.swift
//  Swift3PizzaPopover
//
//  Created by Steven Lipton on 6/29/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//
// Demo of various ways or presenting modal view controllers

import UIKit

class ViewController: UIViewController {

//
//Programmatic ways of presenting modals
//the goal is to get a view controller and 
// present it with present(vc, animated: true, completion: nil)
//
// for a storyboard identifier, we use  
//    let vc =  (storyboard?.instantiateViewController(
            withIdentifier: "Pizza"))!
//for a view controller with programmatic layout we use 
//    let vc = init()
//

    @IBAction func openPizzaProgModal(_ sender: UIButton) {
       /* //using a story board
        let vc = (storyboard?.instantiateViewController(
            withIdentifier: "Pizza"))!
        vc.view.backgroundColor = UIColor.orange
        vc.modalTransitionStyle = .crossDissolve
        present(vc, animated: true, completion: nil)
       */
        
        //using a view controller
        let vc = PizzaModalProgViewController()
        vc.modalTransitionStyle = .partialCurl
        present(vc, animated: true, completion: nil)
    }
//
// To present a modal from its segue identifier use  this
//    performSegue(withIdentifier:String, sender: self)

    @IBAction func openPizzaModal(_ sender: UIButton) {
        performSegue(withIdentifier: "Pizza", sender: self)
    }
//
// Presenting popovers
// Popovers are a special presentation style of a modal view controller
// which present a small window on iPads, and a  full screen modal on other devices
// From a segue, you present it the same way as any other modal 
// From a view controller, you need two more components
//
// 1) set the modalPresentationStyle property to .popover 
//    vc.modalPresentationStyle = .popover
// 2) Tell the popover where to present. This code goes immediately after the 
// present() call. If you are anchoring to a view, you need two lines of code    
//        vc.popoverPresentationController?.sourceView = viewToPresentIn
//        vc.popoverPresentationController?.sourceRect = aCGRectToAnchorTo
//
//If anchoring to a UIBarButtonItem, you need only one line of code
//   vc.popoverPresentationController?.barButtonItem = aBarButtonItem
//
    @IBAction func openPopover(_ sender:UIButton){
    /* //Present a popover from a segue
        performSegue(withIdentifier:"Popover", sender: self)
    */
    //Present a popover programmatically
        let vc = PizzaModalProgViewController()
        vc.modalPresentationStyle = .popover
        //change properties of the view controller
        vc.text = "Cheesy!!"
        vc.imageName = "popover_sm"
        vc.view.backgroundColor = UIColor.yellow
        //present from a view and rect
        present(vc, animated: true, completion: nil)
        vc.popoverPresentationController?.sourceView = view
        vc.popoverPresentationController?.sourceRect = sender.frame
        /*
         //Code to present popover from a bar button Item,
          //though we don't have one in this example.
         present(vc, animated: true, completion: nil)
         vc.popoverPresentationController?.barButtonItem = sender
        */
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
}

PizzaModalViewController.swift

//
//  PizzaModalViewController.swift
//  Swift3PizzaPopover
//
//  Created by Steven Lipton on 6/29/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit

class PizzaModalViewController: UIViewController {


//
// Dismissal code for any modal 
//
    @IBAction func pizzaModalDone(_ sender: UIButton) {
        dismiss(animated: true, completion: nil)
    }

}

PopoverViewController.swift

//
//  PopoverViewController.swift
//  Swift3PizzaPopover
//
//  Created by Steven Lipton on 6/29/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit

class PopoverViewController: UIViewController {

//
// Dismissal code for any modal 
//
// Note on popovers this is not necessary, since any touch outside the popover
// dismisses the popover. However, if presented as a full sheet on a compact width device, 
// this is necessary. For code to hide the done button based on traits, 
// see  my book Swift Swift View Controllers

    @IBAction func popoverDone(_ sender: UIButton) {
        dismiss(animated: true, completion: nil)
    }
}

PizzaModalProgViewController.swift

//
//  PizzaModalProgViewController.swift
//  Swift3PizzaPopover
//
//  Created by Steven Lipton on 6/29/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//
//  Code to programmatically create a View Controller. 
//  Explanation of this code is beyond the scope of this lesson
//
//
import UIKit

class PizzaModalProgViewController: UIViewController {
    let dismissButton:UIButton! = UIButton(type:.custom)
    var imageName = "pizza_sm"
    var text = "BBQ Chicken Pizza!!!!"
    
    let myLabel = UILabel()
    
    func pizzaDidFinish(){
        dismiss(animated: true, completion: nil)
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Build a programmatic view
        
        //Set the Background color of the view.
        view.backgroundColor = UIColor(
            red: 0.8,
            green: 0.6,
            blue: 0.3,
            alpha: 1.0)
        // Add the image to the view
            let myImage:UIImage! = UIImage(named: imageName)
            let myImageView = UIImageView(image: myImage)
            myImageView.frame = view.frame
            myImageView.frame = CGRect(
                x: 10,
                y: 10,
                width: 200,
                height: 200)
            view.addSubview(myImageView)
        // Add the label to the view
        myLabel.text = text
        myLabel.frame = CGRect(
            x: 220,
            y: 10,
            width: 300,
            height: 150)
        myLabel.font = UIFont(
            name: "Helvetica",
            size: 24)
        myLabel.textAlignment = .left
        view.addSubview(myLabel)
        
    //Add the Done button to the view.
        let normal = UIControlState(rawValue: 0) //UIControlState.normal doesn't exist in beta(#27105189). This is a workaround.
        dismissButton.setTitle("Done", for: normal)
        dismissButton.setTitleColor(UIColor.white, for: normal)
        dismissButton.backgroundColor = UIColor.blue
        dismissButton.titleLabel!.font = UIFont(
            name: "Helvetica",
            size: 24)
        dismissButton.titleLabel?.textAlignment = .left
        dismissButton.frame = CGRect(
            x:10,
            y:275,
            width:400,
            height:50)
        dismissButton.addTarget(self,action: #selector(self.pizzaDidFinish),for: .touchUpInside)
        view.addSubview(dismissButton)
    }

}

Swift Watchkit: Headers Footers and More — Multiple Row Types in Apple Watch Tables

2015-08-19_10-10-23

Some tables are boring. In our  multi-part look at the table view in Apple Watch, We’ve looked at tables with only one kind of row type. However, tables can be lot more than just one row. We might have a header row, or a footer row, we may have sub rows to do some grouping or subtotals. In this lesson we’ll learn how to add those rows into a table.

In the first lesson of the table series we introduced the setNumberowRows:withRowType method to make a table with a single row type. For example in the second lesson where we manipulated table data,  we had this:

table.setNumberOfRows(runData.data.count, withRowType: "row")

We used the count property of an array with our data to present and give one row type. For a multiple row types we use a different method:

table.setRowTypes(rowTypes)

where rowtypes is an array of strings identifying row types. Row types are the row identifiers in the storyboard for row controllers in a table. I could, for example,  have a table set like this in my storyboard:

2015-08-17_05-48-09

I would take each of the groups in this table and identify them as a part of the table: Header, Sub Head, Row, and Footer.
If I had 4 data points like this:

  var data = [740,745,750,740]

I would make a row header like this to show the correct type of row by the an element in row type.

var rowTypes = ["Header","Sub Header", "Row","Row","Sub Header", "Row","Row","Footer"]

Swift would know how many rows we have here since it is rowTypes.count. If you are sharp and have followed along, you’ll notice the problem there is with this setup. In our previous lessons, we would get a row with a loop like this:

 
for var index = 0; index < table.numberOfRows; index++ { 
    let row = table.rowControllerAtIndex(index) as! TableRowController //get the row
    let dataString = String(format:"%02",data[index])
    //Set the properties of the row Controller.
    row.label.setText(dataString)
} //end loop

We get a row, and downcast it to the correct row controller class. We take the data in the array,  index  to place the data in the row controller’s label. This works fine with a single row type table. Add more than one row type and things get a bit dicey. We don’t know what class or row controller to use, and we don’t know where the data is since there is not a one to one correspondence with the index. In the rowTypes array above, the value of data[0] is in rowtype[2]

Some people will only use a header and a footer.

var rowTypes = ["Header","Row","Row","Row","Row","Footer"]

For those cases you could put some code before and after the loop to handle the header and footer. Subheads and sub footers are not so easy though. How do you know when they exist,and what do you do when they show up in the array? They’ll also mess up correspondence to the data array even more than before.

For all these cases it’s easier to have a switch..case in our loop to parse the type. Read the rowtype array, and then react accordingly. In the rest of this lesson, we’ll explore ways to be flexible enough to handle anything thrown in our direction.

Set up the project

For this part of the lesson we’ll start from scratch. Open Xcode and press Command-Shift-N to make a new iOS Single View Project.  Name the Project WatchKitMultiRow with Swift as the language. Save the project where you want.

Once the project loads, go to Editor>Add Target… Add a WatchKit App Target. In the next screen, check off the notifications, and activate the target

Add the Model and Model Methods

Go to the extension group in the navigator, and select the InterfaceController.swift.  Add the following two lines just under the interface  controller’s class declaration:

var rowTypes = ["Header","Sub Header", "Row","Row","Sub Header", "Row","Row","Footer"]
var data = [740,745,750,740]

Row one is our row types array, and line two is our data. We’ll add a few methods to use in our headers and footers:

    func avgData(array:[Int],start:Int,end:Int) -> Int{
        var total = 0
        for index in start...end{
            total = total + array[index]
        }
        return total / (end - start + 1)
    }
    
    func paceSeconds(pace:Int) -> String{
        let hours = pace / 3600
        var minutes = (pace - (hours * 3600 )) / 60
        let seconds = pace % 60
        return String(format:"%02i:%02i:%02i",hours, minutes,seconds)
    }

The first method avgData finds the average of the range of data. The paceSeconds method formats the number of seconds we use as data into a string of the type HH:MM:SS.

Set Up the Storyboard

Go to the storyboard in the App Extension. Find the table controller. Drag the controller on the interface.

2015-08-20_05-38-52

If not open, open the document outline. You will see the table in the document outline. If not selected, select it.

2015-08-20_05-41-29

In the attributes inspector, change the number of rows from 1 to 4,

2015-08-17_07-02-21

This is badly labeled.  Rows is not the number of rows in your table. This is the number of row types you will use in the table.  In the storyboard you now have four groups on your table labeled Table Row:

2015-08-17_07-03-17

In the document outline, you will see the four row controllers:

2015-08-17_07-16-28

Click on the top Table Row Controller in the document outline. In the attribute inspector, change the Identifier for the row controller to Header. Also deselect the Selectable check box.

2015-08-17_07-23-54

  It will change in the document outline to Header. Change the rest of the table row controllers to  Subhead, Row and Footer. For Subhead and Row, leave the Selectable Box checked. For Footer, uncheck it. Selectable lets the user select the row and then run code on it. We do not want that for our header and footer. Later on,  we will add  actions with the subhead and row.

Your document outline should look like this:

2015-08-17_07-24-51

 Drag into each of the four controllers on the storyboard a label. For each label, change the width to Relative to Container. 

2015-08-18_05-45-50

Change the text on the top label to Head,then the next three to SubHead, Row, and Foot. In the font attribute, change the font on the Head and Foot to Headline.

2015-08-18_05-47-42

Change the font on the SubHead to Subhead.  Change the colors of the group to red for the header, green for the subheader, and blue for the footer.

2015-08-18_05-54-27

For the Head, Subhead and Foot’s group change the Height to Size to Fit Content .

2015-08-20_05-13-49

When done, your table should look like this.

2015-08-20_05-24-21

Add TableRowControllers

We need a class for the four row controllers. On the drop-down menu, go to File>New>File or Tap Command-N on the keyboard.  Make a new cocoa touch class HeadTableRowController subclassing NSObject.  Save the file in the WatchKit extension group.

In the code change import UIKit to import WatchKit. Repeat this three more times, making a SubHeadTableRowController, RowTableRowController,and FootTableRowController.

Go back to the storyboard. In the document outline select the  Header table row controller. In the identity inspector, change the class to HeadTableRowController.  Open the assistant editor. In. the assistant editor you will have to manually specify the HeadTableRowController.

If not visible, open the outline for the Header row so you can see the label. This is one of those times it is much easier to select from the outline than the objects on the storyboard. Control drag the label to the row controller class. Make an outlet Named label.

2015-08-18_06-16-36

Do the same for the other three  row controllers, each time making a outlet named label.

Select the  interface controller for the table  in the document outline.  Make sure the identity inspector has InterfaceController for the class. If not, change it. Select the table and set the assistant editor to Automatic. Control drag the table to the class, making an outlet table.

Close up the assistant editor.

Iteration 1: Add the Table Creation Method

We are going to show two different methods for creating the table. The first assumes you set rowTable manually. Go to the InterfaceController class. Add this code to the class:

  func refreshTable(){
        var dataIndex = 0
        table.setRowTypes(rowTypes)
        for var rowIndex = 0; rowIndex < rowTypes.count; rowIndex++ {

        }
    }

Line 3 sets this as a multi-row controller table. As in earlier lessons, we’ll loop through an array and set each element of the array. Unlike previous lessons, we are not iterating through the data array but the rowtypes array. We’ll need some place marker for where we are in the data and thus we have a variable dataIndex to keep track.
Each row type has its own way to present data on its own controller. Using rowTypes[rowIndex], we’ll make a row with the right controller then present the data. This is a good place to use a switch statement. Inside the for loop add this code:

switch rowTypes[rowIndex]{
    case "Header":
        let row = table.rowControllerAtIndex(rowIndex) as! HeadTableRowController
        row.label.setText(String(format:"Count: %i",data.count))
    case "Sub Header":
        let row = table.rowControllerAtIndex(rowIndex) as!  SubHeadTableRowController
        let avg = avgData(data, start: 0, end: dataIndex)
        row.label.setText(String(format: "Avg Pace: %i", avg))
    case "Row":
        let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
        row.label.setText(String(format: "Pace %i Sec", data[dataIndex++]))
    case "Footer":
        let row = table.rowControllerAtIndex(rowIndex) as! FootTableRowController
        let avg = avgData(data, start: 0, end: data.count - 1)
        row.label.setText(String(format: "Avg Pace: %i", avg))
    default:
         print("Not a valid row type: " + rowTypes[rowIndex]   )
}

Note the common code here, for the proper row type we get a row in that row table controller class. Then we use the methods there to populate the row.  The header will have a count of our data, the subhead the current average  pace of the run, the row the current pace data and the footer the overall average. Call the method in the willActivate method:

   override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        refreshTable()
    }
  

Build and run.

2015-08-19_10-15-15

scroll down to see the footer.

2015-08-20_06-27-52

Iteration 2: Add a Flexible 2-pass Method

The code above is great if you know what your row types  are when coding. In our code so far we know we have four data points and two subheadings. We rarely know that. We’ll need a more flexible solution to the problem than the code above. We have to create that rowTypes array before we make the table.

The New Model

We have two arrays at the moment: rowtypes and data. As discussed before, these do not have a 1:1 index relationship. The rowindex[2] is where data[0] is. Our table would be a lot easier to handle if rowIndex has the same index as our data. We do that by making another array. Let’s call it rowData. This array rowData will store whatever data we need for that row.
Add this to our code property declarations

var rowData:[Int] = []

We’ll make this a integer array for now. we’ll store what we are displaying in the labels for the row in it. In our next lesson we’ll see why we may want to make this something more.

The build method

Since we have rowData, we build our table logically in rowType and rowData. We have to do the following:

  1. Make a header row
  2. Loop through the following
    1. At some condition, make a sub header
    2. Make a Row from data
  3. Make a Footer

At each of these steps, we’ll append a row type to the rowtypes array and row data to the rowData array. Add this method:

func addRowWithType(type:String,withData data:Int){
        rowTypes = rowTypes + [type] //append the rowtype array
        rowData = rowData + [data] //append the rowData array
    }

We use this in the buld methd. Add the following method for our build of the table:

 func buildTable(){
        //clear the arrays
        //make a header
        //loop through the data
        for var index = 0; index < data.count; index++ {
        //if we are on an even row except 0, add a subhead/foot
        // add the row data
        }
        //add the footer
    }

This is the outline we made above. What we will now do is flesh out this structure. Add to this method to clear the arrays:

//clear the arrays
        rowTypes = []
        rowData = []

Add the header, with the number of data items as our data

        //make a header
        addRowWithType("Header", withData: data.count)

Inside the loop we have the header and subhead. In this example, the header is  a summary row between every two rows. Change the loop to this:

//loop through the data
let subHeadInterval = 2
for var index = 0; index < data.count; index++ {
    //if we are on an interval row except 0, add a subhead/foot
    if index % subHeadInterval == 0 && index != 0{
         addRowWithType("Subhead", withData: avgData(data, start: index - subHeadInterval, end: index - 1))
     }
     // add the row data
     addRowWithType("Row", withData: data[index])
}

To make this more flexible, we’ll use a constant subHeadInterval to tell us how many rows to display before displaying a subhead. We use the modulus operator % to tell us when we get to that row. In that subhead we figure the average pace for the last split. For every time through the loop, we add a row.

Last we have the footer.  Add this for the footer in our method:

//add the footer
        addRowWithType("Footer", withData: avgData(data, start: 0, end: data.count - 1))

The Refresh Table Method

We have two arrays with a common index.  Use those arrays to make a table. Much of this is the same as before. Add another method to our code:

    func refreshBuildtable(){
        buildTable() //refresh the table data
        table.setRowTypes(rowTypes) //set the row types
        //loop through the rowtype table
        for var rowIndex = 0; rowIndex < rowTypes.count; rowIndex++ {
            //parse the rowtypes
            switch rowTypes[rowIndex]{
            case "Header":
                let row = table.rowControllerAtIndex(rowIndex) as! HeaderTableRowController
                row.label.setText(String(format:"Count: %i",rowData[rowIndex]))
            case "Subhead":
                let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
                row.label.setText("Avg Pace: " + paceSeconds(rowData[rowIndex]))
            case "Row":
                let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
                row.label.setText(paceSeconds(rowData[rowIndex]))
            case "Footer":
                let row = table.rowControllerAtIndex(rowIndex) as! FooterTableRowController
                row.label.setText("Pace: " + paceSeconds(rowData[rowIndex]))
            default:
                print("Not a value row type: " + rowTypes[rowIndex]   )
            }
        }
    }

This is very similar to the refreshTable() we already wrote. The difference is the lack of calculations — that’s all done in buildTable. Here we only format and display our results.
change the willActivate() to this

//MARK: life cycle
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        //refreshTable()
        refreshBuildtable()
    }

Build and run. We get similar results to our first attempt.

2015-08-20_06-39-51

then change the data

//var data = [740,745,750,740]
var data = [740,745,750,740,760,765,770,755]

This would have messed up refreshTable. but refreshBuildTable works fine. Build and run. You have a longer table.

2015-08-19_10-10-23

There’s a few things about this table we still need to do. I’d like to be able to know what mile the splits are for in my sub head. I’d also like to be able to display a lot more data about my run at every row. We’ll do that in our next lesson, where we’ll learn how to select in a multi-row table, and use a custom data type that give us a lot more flexibility in our information.

The Whole Code

InterfaceController.swift

//
//  InterfaceController.swift
//  WatchKitMultiRow WatchKit 1 Extension
//
//  Created by Steven Lipton on 8/17/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController {
    
    @IBOutlet weak var table: WKInterfaceTable!
    var rowData:[Int] = []
    var rowTypes = ["Header","Subhead", "Row","Row","Subhead", "Row","Row","Footer"]
    var data = [740,745,750,740]
    //var data = [740,745,750,740,760,765,770,755]
    func avgData(array:[Int],start:Int,end:Int) -> Int{
        var total = 0
        for index in start...end{
            total = total + array[index]
        }
        return total / (end - start + 1)
    }
    
    func paceSeconds(pace:Int) -> String{
        let hours = pace / 3600
        let minutes = (pace - (hours * 3600 )) / 60
        let seconds = pace % 60
        return String(format:"%02i:%02i:%02i",hours, minutes,seconds)
    }
    func refreshTable(){
        var dataIndex = 0
        table.setRowTypes(rowTypes)
        for var rowIndex = 0; rowIndex < rowTypes.count; rowIndex++ {
            switch rowTypes[rowIndex]{
            case "Header":
                let row = table.rowControllerAtIndex(rowIndex) as! HeaderTableRowController
                row.label.setText(String(format:"Count: %i",data.count))
            case "Subhead":
                let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
                let avg = paceSeconds(avgData(data, start: 0, end: dataIndex))
                row.label.setText("Avg Pace: " + avg)
            case "Row":
                let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
                row.label.setText("Pace " + paceSeconds(data[dataIndex++]))
            case "Footer":
                let row = table.rowControllerAtIndex(rowIndex) as! FooterTableRowController
                let avg = paceSeconds(avgData(data, start: 0, end: data.count - 1))
                row.label.setText("Pace: " + avg)
            default:
                print("Not a value row type: " + rowTypes[rowIndex]   )
            }
        }
    }
    //MARK: Iteration 2 of the table code
    func addRowWithType(type:String,withData data:Int){
        rowTypes = rowTypes + [type] //append the rowtype array
        rowData = rowData + [data] //append the rowData array
    }
    
    func buildTable(){
        //clear the arrays
        rowTypes = []
        rowData = []
        //make a header
        addRowWithType("Header", withData: data.count)
        //loop through the data
        let subHeadInterval = 2
        //if we are on an even row except 0, add a subhead/foot
        for var index = 0; index < data.count; index++ {
            if index % subHeadInterval == 0 && index != 0{
                addRowWithType("Subhead", withData: avgData(data, start: index - subHeadInterval, end: index - 1))
            }
            // add the row data
            addRowWithType("Row", withData: data[index])
        }
        //add the footer
        addRowWithType("Footer", withData: avgData(data, start: 0, end: data.count - 1))
    }
    
    func refreshBuildtable(){
        buildTable() //refresh the table data
        table.setRowTypes(rowTypes) //set the row types
        //loop through the rowtype table
        for var rowIndex = 0; rowIndex < rowTypes.count; rowIndex++ {
            //parse the rowtypes
            switch rowTypes[rowIndex]{
            case "Header":
                let row = table.rowControllerAtIndex(rowIndex) as! HeaderTableRowController
                row.label.setText(String(format:"Count: %i",rowData[rowIndex]))
            case "Subhead":
                let row = table.rowControllerAtIndex(rowIndex) as!  SubheadTableRowController
                row.label.setText("Avg Pace: " + paceSeconds(rowData[rowIndex]))
            case "Row":
                let row = table.rowControllerAtIndex(rowIndex) as! RowTableRowController
                row.label.setText(paceSeconds(rowData[rowIndex]))
            case "Footer":
                let row = table.rowControllerAtIndex(rowIndex) as! FooterTableRowController
                row.label.setText("Pace: " + paceSeconds(rowData[rowIndex]))
            default:
                print("Not a value row type: " + rowTypes[rowIndex]   )
            }
        }
    }
    
    //MARK: life cycle
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        //refreshTable()
        refreshBuildtable()
    }
}

HeaderTableRowController.Swift

import WatchKit

class HeaderTableRowController: NSObject {
    
    @IBOutlet weak var label:WKInterfaceLabel!

}

SubheadTableRowController.Swift

import WatchKit
class SubheadTableRowController: NSObject {
    
    @IBOutlet weak var label:WKInterfaceLabel!

}

RowTableRowController.Swift

import WatchKit
class RowTableRowController: NSObject {
    
    @IBOutlet weak var label:WKInterfaceLabel!

}

FooterTableRowController.Swift

import WatchKit
class FooterTableRowController: NSObject {
    
    @IBOutlet weak var label:WKInterfaceLabel!

}

Swift WatchKit: Selecting, Deleting and Adding Rows in an Apple Watch Table

In our first part of this series, we made a simple dynamic table for the Apple Watch. Based on some pace data when I ran the Hot Chocolate 15K, we displayed the pace I ran at the mile splits. In a real running app, I would not want to add or delete any of my splits. However, many table-based apps do need deletion and addition. In this part, we’ll add methods for deletions and additions to a table. We’ll also use the methods for selection of a row in a table, and expand our simple array to a full model class.

We’ll be using the code we added in the last lesson to code this lesson. Be aware we will use menus and modal controllers in this lesson, so if you are not yet familiar on how those work, take a look at the lesson on context menus and on programmatic modal controllers. We’ll also be using class methods, so if you need a refresher on that, you can try here.

Adding a Model Class

In the previous lesson, we used an array named data to hold our values. We are going to continue with that array, but we will be adding a lot of methods dealing directly with this array. It makes a lot of sense to make a  model class with this array before we get working on modifying the table.

Open the project if not already open. If you are reading this first, go back to the previous lesson and follow directions there tp get the project working.  Once your project is open, add a file by pressing Command-N. Make a new Cocoa Touch class subclassing NSObject named RunData . When you save the class, be sure that you are saving in to the extension group.

2015-08-11_05-43-45

Once loaded, go back to the InterfaceController.Swift file. Cut and paste these lines of code from InterfaceController to the RunData class:

 //MARK: Properties
//use the same data as last time, one mile splits
var data = [654,862,860,802,774,716,892,775,748,886,835]

//A function to change the seconds data from an integer to a string in the form 00:00:00
// Not implementing for times over 59:59 min/mi, since that is not a practical speed for this app.
func paceSeconds(pace:Int) -> String{
    let minutes = pace / 60
    let seconds = pace % 60
    return String(format:"00:%02i:%02i",minutes,seconds)
}

We’ll find that the paceSeconds method is very useful as a class method. Change its declaration to

class func paceSeconds(pace:Int) -> String{

We now have several errors in our InterfaceController code, since we now need to use our model properly. Start by adding a runData property to InterfaceController:

var runData = RunData()

In the refreshTable method, change

table.setNumberOfRows(data.count, withRowType: "row")

to

table.setNumberOfRows(runData.data.count, withRowType: "row")

and change

let paceString = "Pace:" + paceSeconds(data[index])

to

let paceString = "Pace" + RunData.paceSeconds(runData.data[index])

Our code now has a model separate from the controller. Let’s add one more method to figure average pace at a given split: Add this to the RunData class:

//find the total time run

//find the average pace by the mean of pace times
        //find the total time run
    func totalTimeforSplit(split:Int) -> Int {
        var total = 0
        for var index = 0; index >= split; index++ {
            total += data[index]
        }
        return total
    }
    //find the average pace by the mean of pace times
    func avgPace(split:Int) -> Int{
        let average = totalTimeforSplit(split) / (split + 1)
        return average
    }

For simplicity’s sake we’ll keep everything an integer in this example. For this example, the pace at my split was the pace I traveled for the mile I just ran. Totaling the splits up to a current split will give me the time running. If I divide that by the number of splits I ran, I get an average pace for the entire run so far.

Selecting Rows in a Table

Selecting row in a table is easy. You override the method table(table: didSelectRowAtIndex rowIndex:) method.

override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
    }

We’ll take our average pace and elapsed time and display them on a separate page, which will display when we select a row in the table. Add this code to the selection method:

//table selection method
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        //build a context for the data 
        var avgPace = RunData.paceSeconds(runData.avgPace(rowIndex))
        let context: AnyObject = avgPace as AnyObject
        presentControllerWithName("Info", context: context) //present the viewcontroller
    }

Using the value of rowIndex, We made a string for our context. I first get the value I need from the appropriate function, then convert it to a string with paceSeconds. I assign the string to the context as an AnyObject. Finally I present the view controller with the name Info with the context.

Of course, we haven’t made the interface yet. Go to the storyboard and drag out an interface. Drag two labels on top of the interface. On the upper label change the color to yellow or green, and a title of Average Pace. Make both labels have a width Relative to Container. Make the white one align center, with a title of 00:00:00. Click the view controller icon and set the title and identifier to Info. When done, you should have a view controller that looks like this one.

2015-08-12_09-28-09

Add a new file by pressing Command-N. Make a Cocoa Touch Class that subclasses WKInterfaceController called InfoInterfaceController. Be sure it is saved in the extension group by clicking the drop down menu on the save menu.

When the view controller appears, replace the class code with this:

class InfoInterfaceController: WKInterfaceController {

    @IBOutlet weak var paceLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        let pace = context as! String
        paceLabel.setText(pace)
    }
}

When the view awakes, we take the context, convert it back to a string and then place it in the label. Go back to the storyboard. Click the view controller icon on the info controller we just made. In the identity inspector make the controller InfoInterfaceController. Open the assistant editor, and drag from the circle in the view controller code to the white label until it highlights. Release the mouse button.

The simulator often has some hiccups, which stops your code from running. To prevent those hiccups, drag another interface on the storyboard. From the view controller icon on this blank controller, control drag to our table controller. Select next page drag the main arrow from the table controller to the blank controller. Your storyboard should look like this:

2015-08-11_05-41-46

We now start on a blank interface and swipe left to get to the table. This prevents whatever bug causes the simulator to hang or lose communication. Build and run. You should load to a blank screen:

2015-08-11_05-45-35

swipe to the left to show the table view:

2015-08-11_05-45-54

Click on the split for 10 miles, and you get an average pace.

2015-08-11_05-46-08

Click Info to return to the table.

Adding a Context Menu

In the rest of the lesson, we’ll add a few new functions to control the table. This is a good place to use a context menu. On the storyboard, drag a menu from the object library to the table controller, and drop on top of the controller

2015-08-11_05-50-43

If the document outline is not open, open it by clicking the icon in the lower left corner of the storyboard. In the document outline select the menu. In the attribute inspector, change the items to 3.

2015-08-11_05-51-28

We’ll use built-in icons for this application. Click the top menu item in the document, In the attributes inspector, change the title to Add Row and change the image to Add.

2015-08-11_05-57-03

For the second menu item, change the title to Delete Row and the image to Trash. For the last item change the Title to Reset and the image to Repeat.

Open the assistant editor if not open. Set the editor to Automatic. From the document outline, control drag from each of the menu items to the InterfaceController code. Make three actions addRow, deleteRow, and resetRows. Close the assistant editor for now.

Build and run. Go to the table controller, and hold down on the controller. The menu should appear:

2015-08-11_06-09-03

Adding Reset

We will be adding and deleting data in this demo. It will be helpful to add a reset method. Go to the RunData class. Add a class method like this.

//return the original array of Data
    class func resetData() -> [Int]{
        return [654,862,860,802,774,716,892,775,748,886,835]
    }

Since this is the same data in the array as the initialization, you can cut and paste that if you wish.

In InterfaceController, go to the menu action for the reset menu item. Change the code there to this:

    @IBAction func resetRows() {
        runData.data = RunData.resetData()
        refreshTable()
        selectedRow = nil
    }

The code reloads the array into the data property. We then refresh the table.

Selecting the Row

We have an error on the last line of this method. As a property, add the following to the InterfaceController class:

var selectedRow:Int! = nil

We’ll need to keep track of the row we last selected. That will be the row we’ll use to add or delete rows. However we may have an unselected state. To keep track of this we use an optional value. If the property is nil, there is no selection. In resetRows we reset everything, so we lose our selection, and set selectedRow to nil
in our table:DidiSelectRowAtIndex method, add the following line as the first line of code in the method:

 selectedRow = rowIndex //for use with insert and delete

Adding An Alert

Whenever we select a row, we set our selectRow property.
since we can have a nil value, we need to handle trying to delete nothing. We’ll need an alert to tell the user this won’t work. Go to the storyboard. Add another interface. In the attributes inspector, make the identifier No Splits Alert and the title Back.

2015-08-11_06-59-51

Add one label. Set the width to Relative to Container and align it Centered. Add text to the label so the interface looks like this:

2015-08-11_07-04-50

I went lazy here and used the built-in back button to handle dismissing the alert. If you want you can make another WKInterfaceController class and add a button to dismiss the alert. We’ll come back to this shortly.

Adding Delete

To delete a row, we delete the element in the array, then refresh the table. However, we also have to check for a nil value and handle those. Add this code to the deleteRow action in the Interface controller

    @IBAction func deleteRow() {
        if var row:Int = selectedRow{
            runData.removeItemAtIndex(row)
            refreshTable()
            selectedRow = nil
        } else {
          presentControllerWithName("No Splits Alert", context: nil)
        }
    }

We use optional chaining to make row from selected row. If nil, we present the alert. Otherwise we run a method in the model to remove the item, refresh the table, and set selectedRow to nil. In RunData, we need to add that removeItemAtIndex method:

    func removeItemAtIndex(index:Int){
        data.removeAtIndex(index)
    }

Build and run. Go to the table, and then the menu. Hit Delete Row and we get our alert:

2015-08-12_09-45-26

Go back to the table and select the 9 mile. Go back to the table, and then delete from the menu. The 9 mile is still there but the pace changes to what the 10 mile pace was.

Since we figured distance by the element number, our numbers mess up in this example. This is to keep things as simple as possible. If you wanted you could make a more robust model that had both miles and pace to prevent this problem.

Adding the addRow Functions

For adding we’ll do one of two things: if we select a row, we add that row at that index location. If we don’t select a row, we’ll add at the end of the splits, making a new finish time. But before we do we need to get what the pace is. Add this code to the addRow Action:

    @IBAction func addRow() {
        let context = self
        presentControllerWithName("Add Row", context: context)
    }

The action method sends us to another WatchKit interface where we’ll input the new pace information. There are several ways of entering information, but one that is easiest to validate is another table that lists possible pace times. We’ll select a time and use a delegate to add the row to the splits table. That’s why we set context to self.

The Add Item Function in the Model

We are trying to keep to MVC and all direct manipulations of the array happen in the model class. We need to add items to the array, so our model will need an add function.  Add the following code to the RunData class:

func addItemAtIndex(index:Int,item:Int){
        data.insert(item, atIndex: index)
    }

 

Make a Calculating Table

To make the add  row view controller, Let’s start with the code then connect it to an interface. Make another WKInterfaceController by pressing Command-N  for the keyboard shortcut or File>New>File on the menu bar. Make a new Cocoa Touch Class named  AddRowInterfaceController Subclassing WKInterfaceController. Make sure to save the controller in the WatchKit extension group.

We’ll also need a row controller like we did last time. Press Command-N and make a Cocoa Touch class AddRowTableRowController subclassing NSObject. Again, make sure this code ends up in the WatchKit extension group.

Change all the code for the row controller to:

import WatchKit

class AddRowTableRowController: NSObject {
   
    @IBOutlet weak var paceLabel: WKInterfaceLabel!
}

As a reminder, default NSObject templates import UIKit and not WatchKit. Xcode will not recognize the WKInterfacLabel as a class in your outlet unless you change UIKit to WatchKit.

Edit the AddRowInterfaceController. Add these properties to the class

 @IBOutlet weak var table: WKInterfaceTable!
    let minValue = 600
    let maxValue = 900
    var midIndex = 0
    var count = 0    

For the data, we’ll compute values for this table instead of having an array make them for us.  The computation is a simple one using a few properties. We have a two constants we use to set the minimum and maximum pace time in seconds. One of our properties count gives us a count of elements in our table. We will count from 0 to count using a for loop, making a value  rowIndex. For each row in our table we will take the  rowIndex and add minValue, ending up with a value between our minValue and our maxValue.

We want to scroll easily to the proper row. Starting in the middle will help this. The variable midIndex  gives us the center of the table, where we’ll go once the table finishes construction.

Add the code to make the table

 func makeTable(){
    table.setNumberOfRows(count, withRowType: "row")
        for var rowIndex = 0; rowIndex > count; rowIndex++ {
            let row = table.rowControllerAtIndex(rowIndex) as! AddRowTableRowController
            let paceString = RunData.paceSeconds(rowIndex + minValue)
            row.paceLabel.setText(paceString)
        }
        table.scrollToRowAtIndex(midIndex)
    }

What we did is make a loop with count number of rows. We set the number of rows then start the loop. In each iteration, we get the row controller, and place the pace inside of row’s label. That pace is the two values plus the minimum value. One the loop finishes, we scroll to the middle of the loop as our starting point.

Initialize everything and make the table in the awakeWithContext:

 override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        count = maxValue - minValue
        midIndex = (count / 2)
        makeTable()
    }

Selecting a row and Setting the delegate

We’ll select a row and then have a delegate send back the selection to our main table. Add the selection code

    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        let seconds = rowIndex + minValue
        delegate.didSelectPace(seconds)
    }

This uses a delegate method delegate.didSelectPace(seconds) which we now have to set up. If you are not familiar with delegates you may want to review here and here. Start by declaring the protocol above the AddRowInterfaceController class:

protocol AddRowDelegate{
   func didSelectPace(pace:Int)
}

Add a property for the delegate:

var delegate:AddRowDelegate! = nil

Then initialize the delegate from the context in AwakeWithContext:

delegate = context as! AddRowDelegate

We are done with the delegate in this class. Adopt the delegate in our InterfaceController class:

class InterfaceController: WKInterfaceController,AddRowDelegate {

Implement the required class like this:

    //delegate
    func didSelectPace(pace: Int) {
        if var index:Int = selectedRow{
            runData.addItemAtIndex(index,item: pace)
        } else {
            runData.addItemAtEnd(pace)
        }
        dismissController()
    }

With our pace date, we’ll either insert the row at the selected row or at the end of the table if we do not have a row selected, then we’ll dismiss the AddRowInterfaceController, leaving our new table for our inspection.

Story Board

We got this all coded, and now we are ready to work on the storyboard. On the storyboard, drag out an interface. In the Identity inspector, set the class to AddTableInterfaceController.

Drag on top of the interface a table. In the document outline click the row. Change the Identifier  to row, and in the identity inspector change the class to AddtableRowController. Add a label to the table. Make the Width and Height of the label Relative to Container.

2015-08-13_06-03-55

Open the assistant editor set to automatic. Drag from the circle next to the table outlet in your code to the table in the document outline.

2015-08-12_06-40-39

Set the assistant editor to the row controller by selecting Manual and then walking through the menus to the AddRowTableRowController.

2015-08-12_08-47-46

Drag the circle next to the outlet to the label in the document outline.

2015-08-12_06-44-15

We’ve set up everything. Build and run. Our table looks like this:

2015-08-13_06-22-49

Go to the menu and select Add Row.

2015-08-11_06-09-03

Select the time 12:30

2015-08-13_06-33-33

You will have a mile 11 with the finish data of 13:55 and Finish time with 12:30.  The data appended to the table  int he case of no selected split.

2015-08-13_06-33-45

Select Mile 9, which has a 12:28 pace.

2015-08-13_06-35-00

 Then exit from the average pace view, and add a 12:00 pace, scrolling up to find the 12:00.

2015-08-13_06-36-01

Mile 9 is now 12:00 and mile 10 is 12:28. We inserted the new pace data into the selected slot.

2015-08-13_06-36-21

This is a bit of a contrived example. It does show how to set up selection, deletion and addition of elements to a table. In the conclusion to the table series we’ll add headers, footers and sub-headers  to tables. We’ll learn how to use WatchKit’s way of handling more than one row controller.

The Whole Code

InterfaceController.Swift

//
//  InterfaceController.swift
//  watchkitTableVer1 WatchKit Extension
//
//  Created by Steven Lipton on 8/2/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation



class InterfaceController: WKInterfaceController,AddRowDelegate {

    @IBOutlet weak var table: WKInterfaceTable!
    //data is the pace in seconds per mile, taken every one mile except the last data point.
    //ver 2 -- moved all data to the model RunData
    var runData = RunData()
    var selectedRow:Int! = nil
    // The table creation method
    // WatchKit replaces all the delegates in UITableViewController
    // with a developer defined function.
    func refreshTable(){
        //Set number of rows and the class of the rows
        table.setNumberOfRows(runData.data.count, withRowType: "row")
        //Loop through the rows of the table and populate them with data
        for var index = 0; index < table.numberOfRows; index++ {
            
            let row = table.rowControllerAtIndex(index) as! TableRowController //get the row
            var rowString = String(format: "Split:%02i miles", index + 1)
            let paceString = "Pace:" + RunData.paceSeconds(runData.data[index])
            if index == (table.numberOfRows - 1){ //Table End Handler
                rowString = "Finish"
            }
            if index == 0 {
                rowString = "Split:01 mile" //Table Beginning Handler
            }
            //Set the properties of the row Controller.
            row.splits.setText(rowString)
            row.time.setText(paceString)
        } //end loop
        //Scroll to last table row.
        table.scrollToRowAtIndex(table.numberOfRows - 1)
    }
    
    //table selection method
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        selectedRow = rowIndex //for use with insert and delete
        //build a context for the data 
        var avgPace = RunData.paceSeconds(runData.avgPace(rowIndex))
        let context: AnyObject = avgPace as AnyObject
        presentControllerWithName("Info", context: context)
    }
    override func awakeWithContext(context: AnyObject?) {
    
        super.awakeWithContext(context)
        
        // Configure interface objects here.
    }
    //MARK: Menus
    
    @IBAction func addRow() {
        let context = self
        presentControllerWithName("Add Row", context: context)
    }
    
    @IBAction func deleteRow() {
        if var row:Int = selectedRow{
            runData.removeItemAtIndex(row)
            refreshTable()
            selectedRow = nil
        } else {
          presentControllerWithName("No Splits Alert", context: nil)
        }
    }
    
    @IBAction func resetRows() {
        runData.data = RunData.resetData()
        refreshTable()
        selectedRow = nil
    }
    //delegate
    func didSelectPace(pace: Int) {
        if var index:Int = selectedRow{
            runData.addItemAtIndex(index,item: pace)
        } else {
            runData.addItemAtEnd(pace)
        }
        dismissController()
    }
    
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        refreshTable()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

RunData.Swift

//
//  RunData.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class RunData: NSObject {
    //MARK: Properties
    //use the same data as last time, one mile splits
    var data = [654,862,860,802,774,716,892,775,748,886,835]
    //MARK: - Class Methods
    //return the original array of Data
    class func resetData() -> [Int]{
        return [654,862,860,802,774,716,892,775,748,886,835]
    }
    //A function to change the seconds data from an integer to a string in the form 00:00:00
   
    class func paceSeconds(pace:Int) -> String{
        let minutes = pace / 60
        let seconds = pace % 60
        return String(format:"00:%02i:%02i", minutes,seconds)
    }
    //MARK: - Instance methods
    func removeItemAtIndex(index:Int){
        data.removeAtIndex(index)
    }
    func addItemAtIndex(index:Int,item:Int){
        data.insert(item, atIndex: index)
    }
    func addItemAtEnd(item:Int){
        data.append(item)
    }
    //find the total time run
    func totalTimeforSplit(split:Int) -> Int {
        var total = 0
        for var index = 0; index <= split; index++ {
            total += data[index]
        }
        return total
    }
    //find the average pace by the mean of pace times
    func avgPace(split:Int) -> Int{
        let average = totalTimeforSplit(split) / (split + 1)
        return average
    }
}

InfoInterfaceController.swift

//
//  InfoInterfaceController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InfoInterfaceController: WKInterfaceController {

    @IBOutlet weak var paceLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        let pace = context as! String
        paceLabel.setText(pace)
    }
}

AddRowInterfaceController.Swift

//
//  AddRowInterfaceController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol AddRowDelegate{
   func didSelectPace(pace:Int)
}

class AddRowInterfaceController: WKInterfaceController {

    @IBOutlet weak var table: WKInterfaceTable!
    let minValue = 600
    let maxValue = 900
    var midIndex = 0
    var count = 0
    var delegate:AddRowDelegate! = nil
    
    func makeTable(){
    table.setNumberOfRows(count, withRowType: "row")
        for var rowIndex = 0; rowIndex < count; rowIndex++ {
            let row = table.rowControllerAtIndex(rowIndex) as! AddRowTableRowController
            let paceString = RunData.paceSeconds(rowIndex + minValue)
            row.paceLabel.setText(paceString)
        }
        table.scrollToRowAtIndex(midIndex)
    }
    override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
        let seconds = rowIndex + minValue
        delegate.didSelectPace(seconds)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        count = maxValue - minValue
        midIndex = (count / 2)
        makeTable()
        delegate = context as! AddRowDelegate
    }

}

AddRowTableRowController.swift

//
//  AddRowTableRowController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/10/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class AddRowTableRowController: NSObject {
   
    @IBOutlet weak var paceLabel: WKInterfaceLabel!
}

TableRowController.swift

//
//  TableRowController.swift
//  watchkitTableVer1
//
//  Created by Steven Lipton on 8/2/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit

class TableRowController: NSObject {
   
    @IBOutlet weak var splits: WKInterfaceLabel!
    
    @IBOutlet weak var time: WKInterfaceLabel!
}

Swift Watchkit: Making ScrollViews and Static TableViews.

To state the obvious, The Apple Watch has very small  screen real estate. There are times we need more screen space than is available. In iOS, there are scroll views. One subclass of scroll views are the table views. Table views come in two flavors: static and dynamic. Dynamic reads data from a collection type and displays it accordingly. Static tables allow for a vertical scroll view with a set of controls. Static table views are very often used as settings pages in applications.   Once again, WatchKit goes for the simple route that we don’t get in iOS.  Scroll views and Static table views are the same thing. What’s more you do everything in the storyboard — no coding necessary.

Make a New Project

Make new project called SwiftWatchKitScroll, with Swift as the language and either Universal or iPhone for the device.  Save the project.

Once the project loads, select Edit>New Target from the drop down menu. Add a WatchKit App. You will not need a notification for this project, so you can turn that off.  Make sure the language is Swift. Click Finish, and then Activate.

Add Your  First Controls

In the WatchKit app folder, select the storyboard. From the object library, drag a switch on the WatchKit scene. Change the switch’s label to Option 1

2015-07-21_07-24-58

To speed things up I’m keeping the defaults for position and size for my controls. Drag another  switch and then a button, so we run out of room on the scene:

2015-07-21_07-24-12

Label the switch Option 2 and the button Button1.

Break the Barrier

We’ve run out of space to put controls.  Put another Switch  under the button. Label it Option 3. The scene stretches to include the button

2015-07-21_07-23-31

Build and run. On a 38mm watch the Option 3 label slips slightly out of view, on a 42mm watch, the interface fits

2015-07-22_05-50-49 2015-07-22_05-52-11

Add more controls to the scene.  I added another switch button, a slider and another button

2015-07-21_07-22-40

Build and run again. we start with the same controls.

2015-07-22_05-59-54 2015-07-22_06-01-01

On both the 38mm and 42mm watch simulator, you will be able to drag up  by clicking and dragging  on the black background to see the hidden items. On the watch, you can just move the digital crown or do a drag up gesture.

2015-07-22_06-03-54 2015-07-22_06-00-24

Add Separators and Labels

This is the basics for any scroll view and static table view. They are really the same thing. To make it look more like a table view, you can add a few decorations to the interface.  Drag separators above and below the Option 3 switch like this:

2015-07-21_07-21-53

Add a label below the separators and one at the very top. Change the labels to Part1, Part2, and Part3.

2015-07-21_07-21-22

Build and run. Scroll down a bit and you’ll see your divided sections.

2015-07-22_06-33-30

Adding Groups

If you need to set up true sections, You can add groups as well. Below Button2 add a group.

2015-07-22_06-21-05

Change the layout from Horizontal to Vertical

2015-07-22_06-21-32

Change the background color of the group. I made mine 50% gray (#808080)

2015-07-22_06-23-07

Add some controls, a separator and label to the group.

2015-07-22_06-30-31

Build and Run. Scroll down to see the group at the end of the scroll.

2015-07-22_06-34-00

This was short and rather simple lesson. To get scrolling behavior, all you need to do is add more controls, and set outlets for each of them. One last point: a watch app interaction lasts only a few seconds. Put your most important information at the top of a scroll so users can look and change it quickly. Put the least important at the bottom.

In our next lesson, we’ll look at the dynamic table in WatchKit.

Practical Auto Layout: Are You Ready for iOS 9?

Practical Autolayout Cover 3Apple messed up big time. They wanted a simple, powerful but flexible system to lay out buttons, labels, images and other objects on the storyboard. They wanted a way to make only one layout and have it work on any iOS device in any orientation. They created Auto Layout and Size classes. Then they messed up. They didn’t tell anyone how to easily use it. Developer’s have been skirting around Auto Layout for years, confused by bizarre menus and strange icons. Instead, many developers spend weeks writing extra code for the iPhone app or a landscape mode. Developers can lose thousands of potential customers by making a iPhone or iPad only app.

This might be you.

In iOS8 Apple required popovers, modal views, Alerts and Split views to use size classes. In iOS9, multitasking requires it. But most developers do not know the difference between a regular width and compact width. Doing so, they risk their code becoming obsolete and useless.

If you’ve ever used presentViewController:, this may be you.

In Practical Auto Layout, using simple, practical, easy to follow examples, you will learn how to master auto layout and size classes on the storyboard. Using easy to follow examples, you will learn how to make universal apps quickly easily and in far less time than ever before. You’ll learn how to use constraints the right way, how to avoid and resolve errors such as misplacements and conflicting constraints. You be able to customize your layout for both launch screens and your application, using buttons, labels, images, text fields and more.

With a quarter century of technical training experience, Author Steven Lipton will teach beginners to expert developers in Practical Auto Layout how to use this immensely useful tool to make amazing applications for more devices in half the time.

Available on

 

Swift Watchkit: Working With Modal Views Part 3: Modal Page Views

Photo Jun 11, 6 33 39 AM

Apple’s  documentation for WatchKit  is quite clear, even when it is lying. The documentation states you can have  hierarchical navigation or page navigation but not both. Here’s is where it lies:  you can have a page-based navigation as part of a hierarchical navigation scheme. There is a special modal view version of page views, which you can use in a hierarchical scheme.  In the conclusion of this lesson on modal views, we’ll look at  using the modal  page view.

In the last part, we learned we can call a modal view programmatically using the method presentControllerWithName(_name:, context:). For example, we had this code for presenting the number controller modally:

@IBAction func numberButtonPressed() {
    //myModel.myNumber = 3.14
    myModel.delegate = self
    //prepare the context
    let context: AnyObject? = myModel
    //present the controller
    presentControllerWithName("Number", context: context)
}

For a pages control, we just make everything in the modal control plural. The method is presentControllerWithNames(_names:, contexts:). For a modal page controller, use arrays instead of a single values for the parameters. In the code from the previous lessons add this under the numberButtonPressed action.

@IBAction func pagesButtonPressed() {
    let pageNames = ["RunPage","WalkPage","PizzaPage"]
    let pageContexts:[AnyObject]? = [myModel.myNumber,myModel.myColorString,myModel]
    presentControllerWithNames(pageNames, contexts: pageContexts)
 }

Line 1 has an array of interface identifiers. The system will present the pages in this order, starting with the Run page, going to the Walk page and finishing with the Pizza Page. Line 3 gives different contexts for the corresponding page. Our Run page for example will take the Float value myNumber from the model. Similarly, the Walk page will take a String. Let’s design a storyboard then write some code to see how this would work.

Adding the Pages Button

Go to the watch app storyboard. On the I am Root interface, add another button, which will snap itself to the bottom. Label it Pages.

2015-06-11_07-32-21

Open the assistant editor. Next to the action pageButtonPressed is the open circle. Drag from the circle to the Pages button to connect the button.

Adding the Three Page Controllers.

Press Command-N and make a new controller subclassing WKInterfaceController named RunPageInterfaceController. Be sure to group it in the WatchKit extension. Do the same to make two more interface controllers named  WalkPageInterfaceController and PizzaPageInterfaceController.

Go back to the storyboard.  Add a new interface to the storyboard. Add a group on the interface.

2015-06-11_06-38-13

Select the group. In the attribute inspector, set the Height  to Relative to Container.  Change the color of the group to Light Gray.  Change the Layout to Vertical.

2015-06-11_06-38-58

Add two labels to the group

2015-06-11_06-40-54

Select the bottom label. Set the Position to Horizontal: Center, Vertical:Center. Change the font to 50 point System Centered.

2015-06-11_06-40-17

Select the controller icon on this interface. Press Command-C to copy the interface.  Click on the white background of the storyboard to deselect everything. Press Command-V to paste a copy of the interface.  The pasted interface is on top of the current interface. Drag the interface and you will now have two.

2015-06-11_06-47-12

Deselect everything again and press Commmand-V. Drag again and you will have three controllers

2015-06-11_06-47-55

In the identity inspector, Make the ID of one Controller RunPage and its name Run. Make the ID of the second controller WalkPage, and the name Walk. Make the ID of the third PizzaPage and the name Pizza.  Using emoji you can find at Control-Command-Space, set up the three interfaces to look like this:

2015-06-11_06-43-54

Go to the identity inspector. Give RunPage a class of RunPageInterfaceController, WalkPage a class of WalkPageInterfaceController, and PizzaPage a class of PizzaPageInterfaceController.

Build and run.  Press the Pages button  and the Run interface shows. Swipe to the left and you get the walk page, another swipe give you the Pizza Page. Tap on the word Pizza on the dark background. The modal dismisses.

Adding Contexts

While you can add delegates to each page. You don’t have to. Modal pages very likely won’t interact, but just show information, like glances. This time, we’ll transmit information through the context and use it differently in each page.

Wire up the  Page controllers.  With the assistant editor open, select the RunPage interface. Control-Drag the smaller label to the  code, just under the class declaration.  Add an outlet statusLabel .  After the outlet, add a property:

var number:Float = 0.0

In the code, add the following to awakeWithContext:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    //Update the status label with the context.
    number = context as! Float
}

The RunPage is the first element of the context  array. We set the context there to the Float value of  myModel.myNumber. We only have to unwrap the Float value to use it. We’ll display our result in willActivate. Change that method to this:

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    let displayString = String(format:"Run %0.2f minutes",number)
    statusLabel.setText(displayString)
}

The code changes the number to a String, then displays the string on the label.

  We’ve finished our first controller.  With the assistant editor open, click on the WalkPage in the storyboard. The WalkPageController should be in the assistant editor. In this controller, we take a String with a color for our context. We’ll instantiate a new instance of the model, then add that string to the model to compute a UIColor for the string. We’ll use that model to change the color of the group’s background and to tell us what color we have.

We need two outlets for this.  Control-drag from the group to the code in the assistant editor. Add an outlet named backgroundGroup. Control-drag from the label to the code. Add an outlet named statusLabel. Finally add an instance of  DemoModel named myModel. You should have just added this code:

@IBOutlet weak var backgroundGroup: WKInterfaceGroup!
@IBOutlet weak var statusLabel: WKInterfaceLabel!
let myModel = DemoModel()

We’ll cast the context’s AnyObject? to type String, then add to the model with the colorFromString method on the model we used in the first lesson on modals. Change awakeWithContext  to the following code:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
   // Configure interface objects here.
   //Context is a String, but we need a string and color
    //so we use a new model.
    let colorString = context as! String
    myModel.colorFromString(colorString)
}

Once again, set up the display in the willActivate code:

override func willActivate() {
    // This method is called when watch view controller is about to be visible to user
    super.willActivate()
    let displayString = myModel.myColorString + " Walking"
    statusLabel.setText(displayString)
    backgroundGroup.setBackgroundColor(myModel.myColor)
}

Click on the pizza interface to select it and bring up the PizzaPageInterfaceController code in the assistant editor. This time we pass DemoModel as the context. Wire up the controller by first adding the following outlets and actions to the code:

var myModel = DemoModel()
@IBOutlet weak var backgroundGroup: WKInterfaceGroup!
@IBOutlet weak var statusLabel: WKInterfaceLabel!

Drag from the empty circle to the left of the backgroundGroup to a spot on the Pizza interface. Release the button. Do the same between statuslabel and the label.

We have only one line to add to awakeToContext. We’ll need to cast the context to a DemoModel object.

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    // Configure interface objects here.
    myModel = context as! DemoModel
 }

Since we are pizza themed in this page, let’s have a different pizza based on color. Change willActivate to the following:

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        var pizzaType = "Cheese"
        switch myModel.myColorString{
            case "Red": pizzaType = "Pepperoni"
            case "Green": pizzaType = "Veggie"
            case "Blue": pizzaType = "Gorgonzola"
            default: pizzaType = "Cheese"
        }
        let displayString = pizzaType + " Pizza!!"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

We created a variable pizzaType.  Using a switch statement, we mapped the colors to appropriate types of pizzas (Red meat, Green vegetables and Blue cheese), assign the pizza type in pizzaType.  Instead of displaying the color, we display  the pizza.

Build and run. Assuming the simulator behaves itself. you should have your three buttons.

2015-06-11_06-57-58

Set the number  to 3.5 by pressing the number button and using the slider, then pressing Done.

2015-06-11_06-58-34

Set the color to Blue in the Color modal.

2015-06-03_06-13-02

When done your watch looks like this:

2015-06-11_07-01-41

Tap Pages. The Run Page appears:

2015-06-11_06-58-57

You’ll notice our number is in the label. Swipe left to get the walk page and our selected color: .

2015-06-11_07-02-38

Swipe left again to get a pizza based on the blue color:

2015-06-11_07-02-02

Our modal controllers work perfectly. Modals are meant for interrupted use of a more active view. Page modals are best used for viewing data in a glance fashion. Actually glances are a type of modal controller.  Modals are the last of the basic types of controllers in Watchkit. In future lessons we’ll use these with a few very handy complex controllers. In the next lesson, we’ll look at  one of these: Text input when you don’t have room for a keyboard.

The Whole Code

Storyboard

2015-06-15_07-37-17

InterfaceController.swift

//
//  InterfaceController.swift
//  SwiftWatchModalDemo WatchKit Extension
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController,ColorModalDelegate, NumberInterfaceDelegate {
    
    //MARK: Outlets and Properties
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    @IBOutlet weak var colorButton: WKInterfaceButton!
    var myModel = DemoModel()
    
    //MARK: - Actions
    
    @IBAction func numberButtonPressed() {
        //myModel.myNumber = 3.14
        myModel.delegate = self
        //prepare the context
        let context: AnyObject? = myModel
        //present the controller
        presentControllerWithName("Number", context: context)
    }
    
    @IBAction func pagesButtonPressed() {
        let pageNames = ["RunPage","WalkPage","PizzaPage"]
        let pageContexts:[AnyObject]? = [myModel.myNumber,myModel.myColorString,myModel]
        presentControllerWithNames(pageNames, contexts: pageContexts)
    }
  
    //MARK: - Delegates
    func colorDidFinish(context: AnyObject?) {
        myModel = context as! DemoModel
        dismissController()
        updateDisplay()
    }
    
    func numberDidFinish(context:Float){
        myModel.myNumber = context
        updateDisplay()
        dismissController()
    }
    
    //MARK: - Instance Methods
    func updateDisplay(){
        let statusText = String(format: "%1.2f", myModel.myNumber)
        statusLabel.setText(statusText)
        colorButton.setBackgroundColor(myModel.myColor)
    }
    
    //MARK: - Life Cycle
    override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
        if segueIdentifier == "ColorsModal" {
            myModel.delegate = self
            return myModel
        }
        return nil
    }
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        // Configure interface objects here.
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        updateDisplay()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

DemoModel.swift

//
//  DemoModel.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import UIKit

class DemoModel:NSObject{
    var myNumber:Float = 0.0
    var myColorString = "Black"
    var myColor = UIColor(white: 0.2, alpha: 0.73)
    var delegate:AnyObject? = nil
    
    func colorFromString(colorString:String){
        myColorString = colorString
        switch colorString {
        case "Red": myColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.73)
        case "Green":myColor = UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.73)
        case "Blue":myColor = UIColor(red: 1.0, green: 0.0, blue: 1.0, alpha: 0.73)
        default: myColor = UIColor(white: 0.2, alpha: 0.73)
        myColorString = "Black"
        }
    }
}

WalkPageInterfaceController.swift

//
//  WalkPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class WalkPageInterfaceController: WKInterfaceController {

    @IBOutlet weak var backgroundGroup: WKInterfaceGroup!
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    let myModel = DemoModel()
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    
        // Configure interface objects here.
        //Context is a String, but we need a string and color
        //so we use a new model.
        let colorString = context as! String
        myModel.colorFromString(colorString)
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        let displayString = myModel.myColorString + " Walking"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

RunPageInterfaceController.swift

//
//  RunPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class RunPageInterfaceController: WKInterfaceController {

    var number:Float = 0.0
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        
        // Configure interface objects here.
        //Update the status Label with the context. 
        number = context as! Float
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        let displayString = String(format:"Run %0.2f minutes",number)
        statusLabel.setText(displayString)
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

PizzaPageInterfaceController.swift

//
//  PizzaPageInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/11/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation


class PizzaPageInterfaceController: WKInterfaceController {
    var myModel = DemoModel()
    @IBOutlet weak var backgroundGroup: WKInterfaceGroup!
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        var pizzaType = "Cheese"
        switch myModel.myColorString{
            case "Red": pizzaType = "Pepperoni"
            case "Green": pizzaType = "Veggie"
            case "Blue": pizzaType = "Gorgonzola"
            default: pizzaType = "Cheese"
        }
        let displayString = pizzaType + " Pizza!!"
        statusLabel.setText(displayString)
        backgroundGroup.setBackgroundColor(myModel.myColor)
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

ColorsModalInterfaceController.swift

//
//  ColorsModalInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/3/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol ColorModalDelegate{
    
    func colorDidFinish(context:AnyObject?)
}

class ColorsModalInterfaceController: WKInterfaceController {

    //MARK: Properties
    var myModel = DemoModel()
    var delegate:ColorModalDelegate! = nil
    //MARK: - Action Methods
    @IBAction func redButtonPressed() {
        updateModel("Red")
        
    }
    @IBAction func greenButtonPressed() {
        updateModel("Green")
        
    }
    @IBAction func blueButtonPressed() {
        updateModel("Blue")
        
    }
    //MARK: - Instance Methods
    func updateModel(color:String){
        myModel.colorFromString(color)
        let newContext:AnyObject? = myModel
        delegate.colorDidFinish(newContext)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        myModel = context as! DemoModel //make the model
        delegate = myModel.delegate as? ColorModalDelegate //unbundle the delegate
        
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}

NumberInterfaceController.swift

//
//  NumberInterfaceController.swift
//  SwiftWatchModalDemo
//
//  Created by Steven Lipton on 6/5/15.
//  Copyright (c) 2015 MakeAppPie.Com. All rights reserved.
//

import WatchKit
import Foundation

protocol NumberInterfaceDelegate{
    func numberDidFinish(context:Float)
}


class NumberInterfaceController: WKInterfaceController {
    
    //MARK: Outlets and properties
    var number:Float = 0.0
    var delegate:NumberInterfaceDelegate! = nil
    
    @IBOutlet weak var statusLabel: WKInterfaceLabel!
    
    //MARK: - Actions
    @IBAction func sliderDidChange(value: Float) {
        number = value
        updateDisplay()
    }
    
    @IBAction func doneButtonPressed() {
        delegate.numberDidFinish(number)
    }
    //MARK: - Instance Methods
    func updateDisplay(){
        let displayString = String(format:"%0.1f",number)
        statusLabel.setText(displayString)
    }
    
    //MARK: - Life Cycle
    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
        // Configure interface objects here.
        // unwrap the context
        let myModel = context as! DemoModel //make the model
        number = myModel.myNumber
        delegate = myModel.delegate as? NumberInterfaceDelegate
        updateDisplay()
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
        updateDisplay()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

}