Chapter 11. Advanced Input Using Sensors

When the iPhone was first announced, the multi-touch screen was the focal point of its high-tech appeal. However, it has always had a fantastic user interface due to the many other sensors that are built in, smoothing out every aspect of the user experience. The accelerometer knew when you tilted your device and rotated web pages automatically. A proximity sensor knew when your phone was up to your face so it would turn off the screen. An ambient light sensor would automatically adjust the backlight based on the room you were in.

Since those early days, each new iteration of the iPhone hardware has introduced more and more sensors that allow new generations of apps to provide even better experiences. Earlier in the book, we looked at very common input methods like multi-touch gestures, and positioning sensors like GPS with Core Location. In this chapter, we're going to take a look at some of the more advanced sensors, covering:

  • Device information through UIDevice
  • The basics of the Core Motion framework
  • Using third-party charting frameworks to plot data
  • Getting accelerometer data from the Apple Watch

In this chapter, we'll be taking a break from our Snippets project, so get ready to take a bit of a breather by learning some fun new ways to interact with an iOS device!

Note

Due to the subject nature of this chapter, most of the code we'll be writing will not be possible to run in a simulator. If you don't have a device plugged in you can still follow along and learn, but you'll make the most of this chapter if you have the actual hardware.

Device status with UIDevice

Many of the simpler sensors available in iOS are exposed to us through the UIDevice class. Responsible for keeping track of the overall device state, this class has a bunch of little features that make it actually quite fun to poke around!

If you haven't already, create a new single view Xcode project; I called mine Sensors. Then, head over to your default view controller, and at the top of the class let's grab a reference to our current device to make it easier to get information later. Our class should start off looking like this:

class ViewController: UIViewController {

    let device = UIDevice.currentDevice()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Now let's see what we can do!

Accessing orientation state

One of the more important pieces of sensory information that you might need when developing your own apps is checking the current state of the device's orientation. In recent years, auto layout has made it less necessary to manually check for device orientation, but it's still a useful trick to know!

Let's start off by talking about how iOS represents orientation data. The device's orientation is defined using an enum called UIDeviceOrientation, of which all the cases are self-explanatory:

enum UIDeviceOrientation : Int {
    case unknown
    case portrait
    case portraitUpsideDown
    case landscapeLeft
    case landscapeRight
    case faceUp
    case faceDown
}

To check the current orientation of the device, you can just access the orientation property like so:

device.orientation

However, usually it makes the most sense to not check what the orientation is, but to be notified when it changes. To detect orientation changes we have two options: be notified when the screen auto-rotates, or register directly to receive notifications when the orientation changes.

The first option works well when you need to know about orientation changes in the UI, since it is only called when the interface actually rotates. This is great because the orientation you get will match the orientation of the user interface, and can only ever be one of the supported orientations:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    switch device.orientation
    {
    case .landscapeLeft:
        print("Landscape Left")
    case .landscapeRight:
        print("Landscape Right")
    case .portrait:
        print("Portrait")
    case .portraitUpsideDown:
        print("Portrait Upside Down")
    default:
        print("Other orientation")
    }
}

To do this, we use the the viewWillTransition(toSize:)function, which lets us know when our screen is about to change sizes during a rotation event. Here, you can see that inside the function we're running a switch statement on device.orientation, which prints out the orientation based on the value.

The other option (being notified directly on device orientation change) is nice because it fires immediately, and doesn't care about what the supported orientations are. It also doesn't care what the user interface is doing, so it's perfect for when you really just need to know the orientation of the device.

To implement this, we need to register our view controller to receive the orientation change notifications. We'll do that right before our view is shown to the user:

override func viewWillAppear(_ animated: Bool) {
    let nc = NotificationCenter.default
    let oriSel = #selector(ViewController.onOrientationChange)
    let oriNot = NSNotification.Name.UIDeviceOrientationDidChange
    nc.addObserver(self, selector: oriSel, name: oriNot, object: nil)
}

Then we need to remove our class as an observer right after the view is taken away from the user:

override func viewWillDisappear(_ animated: Bool) {
    let nc = NotificationCenter.default
    let oriNot = NSNotification.Name.UIDeviceOrientationDidChange
    nc.removeObserver(self, name: oriNot, object: nil)
}

We're using a new class here called NSNotificationCenter. This is not the same notification center that is used to manage alerts that you get from apps on your iPhone. The NSNotificationCenter class processes notifications, which are essentially strings. It also manages lists of objects that are listening to each one of those notifications. When an object posts a notification to the notification center, it alerts all of the objects that are listening for that notification that it has been posted, and then all of those objects can react accordingly. So here, we are telling the notification center that our view controller wants to be alerted when the UIDeviceOrientationDidChangeNotification gets posted, and that when it does it should call the onOrientationChange function. So let's create that function now:

func onOrientationChange() {
    switch device.orientation
    {
    case .landscapeLeft:
        print("Landscape Left")
    case .landscapeRight:
        print("Landscape Right")
    case .portrait:
        print("Portrait")
    case .portraitUpsideDown:
        print("Portrait Upside Down")
    case .faceUp:
        print("Face Up")
    case .faceDown:
        print("Face Down")
    default:
        print("Other orientation")
    }
}

This function is very similar to the viewWillTransition(toSize:) function we created earlier, except that we can now detect the .faceUp and .faceDown orientations which do not trigger UI auto-rotation.

Try running this project on your device with one method enabled, and then the other. Spin your device around and see what results are spit out to the console!

Checking the proximity sensor

The proximity sensor is one of the little black circles near the earpiece on your iPhone. It detects when your face is close to the phone so that it can turn off the screen during calls. However, the same functionality is also available to developers to do with as they please.

Setting up proximity state changes is very similar to the way we used the notification center to check for orientation changes. Before we register for notifications, though, we need to tell the device to start monitoring the proximity sensor:

override func viewDidLoad() {
    super.viewDidLoad()
    
    device.isProximityMonitoringEnabled = true
}

Always remember to turn off the proximity sensor when you're done with it; there's no sense running the sensor if we don't need it!

Next we have to register for a new notification related to the proximity sensor. Let's update our viewWillAppear() and viewWillDisapper() functions to make our view controller listen for UIDeviceProximityStateDidChangeNotification , and call a function called onProximityChange:

override func viewWillAppear(_ animated: Bool) {
    let nc = NotificationCenter.default
    let oriSel = #selector(ViewController.onOrientationChange)
    let prxSel = #selector(ViewController.onProximityChange)

    let oriNot = NSNotification.Name.UIDeviceOrientationDidChange
    let prxNot = NSNotification.Name.UIDeviceProximityStateDidChange

    nc.addObserver(self, selector: oriSel, name: oriNot, object: nil)
    nc.addObserver(self, selector: prxSel, name: prxNot, object: nil)
}

Getting the hang of NSNotificationCenter? Next we have to create our callback function, onProximityChange():

func onProximityChange() {
    let proximity = device.proximityState ? "Near" : "Far"
    print(proximity)
}

Here, we're just checking to see what the new state is. If it's true (activated), we create a string that reads Near, and if it's false (not activated) we set it to Far. Then we print out the string.

Note

In that last function, we're using what is known as the ternary conditional operator. It is composed of three parts (the condition, the true return value, and the false return value) and is structured like this:

(conditional statement) ? (if true return this) : (if false return this)

The ternary conditional is usually useful in assignment cases like this one, where you want to assign one value if something is true, and something else if it is false.

If you build and run the app now, you should see that when you cover the proximity sensor with your finger, the screen will turn off and you'll get your Near/Far string printed out to the console. Unfortunately, the screen will always turn off with the proximity sensor, and there isn't anything you can do about it. (Also, some devices don't have a proximity sensor, like the iPod Touch, so keep that in mind!)

Getting battery status

One of the most fun pieces of data you can access is the user's battery level. In most cases there isn't anything particularly useful to do with this information, but with a little creativity anything is possible! For example, maybe you're making a game and you can add an achievement called risk taker if the user beats a difficult level when the battery is below 5% charge.

Anyway, accessing this information is very similar to the way we access the proximity state. However, this time there are two different notifications we can subscribe to: when the battery level changes, and when the battery state changes. The first one is just the percentage, but the second one is an enum, UIDeviceBatteryState, that looks like this:

enum UIDeviceBatteryState : Int {
    case unknown
    case unplugged
    case charging
    case full
}

In this example, we're going to subscribe to changes in the state, but you're welcome to check out the documentation and try to subscribe to changes in the battery level.

Since we've seen most of this code before, let's just lay it all out:

override func viewDidLoad() {
    super.viewDidLoad()
    
    device.proximityMonitoringEnabled = true
    device.batteryMonitoringEnabled = true
}

override func viewWillAppear(_ animated: Bool) {
    let nc = NotificationCenter.default
    let oriSel = #selector(ViewController.onOrientationChange)
    let prxSel = #selector(ViewController.onProximityChange)
    let batSel = #selector(ViewController.onBatteryStateChange)

    let oriNot = NSNotification.Name.UIDeviceOrientationDidChange
    let prxNot = NSNotification.Name.UIDeviceProximityStateDidChange
    let batNot = NSNotification.Name.UIDeviceBatteryStateDidChange

    nc.addObserver(self, selector: oriSel, name: oriNot, object: nil)
    nc.addObserver(self, selector: prxSel, name: prxNot, object: nil)
    nc.addObserver(self, selector: batSel, name: batNot, object: nil)
}

override func viewWillDisappear(_ animated: Bool) {
    let nc = NotificationCenter.default
    let oriNot = NSNotification.Name.UIDeviceOrientationDidChange
    let prxNot = NSNotification.Name.UIDeviceProximityStateDidChange
    let batNot = NSNotification.Name.UIDeviceBatteryStateDidChange
    
    nc.removeObserver(self, name: oriNot, object: nil)
    nc.removeObserver(self, name: prxNot, object: nil)
    nc.removeObserver(self, name: batNot, object: nil)
}

override func viewWillDisappear(animated: Bool) {
    let nc = NSNotificationCenter.defaultCenter()
    let oriNot = UIDeviceOrientationDidChangeNotification
    let proNot = UIDeviceProximityStateDidChangeNotification
    let batNot = UIDeviceBatteryStateDidChangeNotification
    nc.removeObserver(self, name: oriNot, object: nil)
    nc.removeObserver(self, name: proNot, object: nil)
    nc.removeObserver(self, name: batNot, object: nil)
}

Again, first we set battery monitoring to Enabled. Then we register our battery state change notification in viewWillAppear, and unregister in viewWillDisappear. Finally, we need to create our callback function, onBatteryStateChange:

func onBatteryStateChange() {
    switch device.batteryState
    {
    case .unplugged:
        print("Battery Unplugged")
    case .charging:
        print("Battery Charging")
    case .full:
        print("Battery Full")
    default:
        print("Battery State Unknown")
    }
}

Here, we check the device's battery state, and print out some results depending on its current state. We could also use device.batteryLevel to get the exact charge of the battery.

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

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