Improving Touch Rates for Smoother UI Interactions - The User Interface - iOS 9 Swift Programming Cookbook (2015)

iOS 9 Swift Programming Cookbook (2015)

Chapter 3. The User Interface

3.8 Improving Touch Rates for Smoother UI Interactions

Problem

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

Solution

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

Discussion

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

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

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

class MyView : UIView{

}

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

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

NOTE

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

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

var points = [CGPoint]()

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

guard let touch = s.first, event = event,

allTouches = event.coalescedTouchesForTouch(touch)

where allTouches.count > 0 else{

return

}

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

setNeedsDisplay()

}

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

override func touchesBegan(touches: Set<UITouch>,

withEvent event: UIEvent?) {

points.removeAll()

drawForFirstTouchInSet(touches, event: event)

}

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

override func touchesCancelled(touches: Set<UITouch>?,

withEvent event: UIEvent?) {

points.removeAll()

setNeedsDisplayInRect(bounds)

}

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

override func touchesCancelled(touches: Set<UITouch>?,

withEvent event: UIEvent?) {

points.removeAll()

setNeedsDisplayInRect(bounds)

}

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

override func touchesEnded(touches: Set<UITouch>,

withEvent event: UIEvent?) {

guard let touch = touches.first, event = event,

predictedTouches = event.predictedTouchesForTouch(touch)

where predictedTouches.count > 0 else{

return

}

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

setNeedsDisplay()

}

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

override func drawRect(rect: CGRect) {

let con = UIGraphicsGetCurrentContext()

//set background color

CGContextSetFillColorWithColor(con, UIColor.blackColor().CGColor)

CGContextFillRect(con, rect)

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

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

for point in points{

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

if let last = points.last where point != last{

let next = points[points.indexOf(point)! + 1]

CGContextAddLineToPoint(con, next.x, next.y)

}

}

CGContextStrokePath(con)

}

}

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

See Also