Active Filters: UIKit

Today we'll check out a library called Tactile. It's a "Swiftier" way to respond to gesture recognizers and control events. Let's get started.

Tactile gives us a safer, more idiomatic way to install/react to gesture recognizers (Check Bite #38 for gesture basics).

In it's simplest form, Tactile works like this:

let tapGR = UITapGestureRecognizer()
tapGR.numberOfTapsRequired = 2

view.on(tapGR) { (tapGR: UITapGestureRecognizer) in
  // ...
}

We can also react to specific state changes:

let panGR = UIPanGestureRecognizer()
view.on(panGR, [.Began, .Ended], beganOrEnded)

func beganOrEnded(panGR: UIPanGestureRecognizer) {
  // ...
}

Tactile also adds similar functions to UIControl:

button.on(.TouchUpInside, tapped)
button.on([
  .TouchUpInside: tapped,
  .TouchUpOutside: cancelledTap
])

We can attach a single recognizer to multiple views:

viewA.on(tapGR, { tapGR in /* ... */ })
viewB.on(tapGR, { tapGR in /* ... */ })

Last but not least, we can remove all recognizers with

view.off()

just one with:

view.off(tap)

or all of a type with:

view.off(UITapGestureRecognizer.self)

More info about Tactile can be found at git.io/tactile

Topics

#188: Adding a Top Shelf Extension to a tvOS App 🍸📺

Topics

One of the best parts of tvOS is the Top Shelf. It's the bit just above the top row of apps on the home screen that shows previews of each app's content when it's focused (think "Trending Videos", "Recent Photos", etc.).

Today we'll learn how we can provide this same type of functionality in our own apps, by adding a Top Shelf Extension. Let's get started:

We'll add a new target to our app. We'll create a new tvOS > Application Extension > TV Services Extension.

Xcode will generate a ServiceProvider class for us that conforms to the TVTopShelfProvider protocol and provides TVContentItems back to the system.

We'll use .Sectioned for our style so we can provide items like this:

The .Inline style is neat too, but its purpose is only for displaying wide banners like the App Store app.

We'll create a single section item, then set its topShelfItems to an array of items for the things we actually want to display.

var topShelfItems: [TVContentItem] { // ...
  shipsSection.title = "Recent Launches"
  shipsSection.topShelfItems = recentlyLaunchedShips()
    .map { $0.asTVContentItem(sectionID) }
  // ...

We can use TVContentItem's properties to customize/enhance its behavior:

item.title = name
item.imageURL = NSURL(string: "https://spaceshipsapp.com/ships/\(identifier).jpg")
item.imageShape = .HDTV
item.displayURL = NSURL(string: "spaceships://ship/\(identifier)")
item.playURL = NSURL(string: "spaceships://ship/\(identifier)")

There's actually a lot of features packed into this little class: Simple things like separate deep-link URL for selecting, or pressing the Play button on an item, or more advanced functionality like the currentPosition property, which can help us display the user's "watch progress" on our top shelf items.

Download a fully working example project here

Topics

#186: Focus Engine Basics on tvOS 📺

Topics

One of the biggest things that sets tvOS apart from iOS is the Focus Engine. Today we'll check out what it does, how it works, and how we can interact with it in our apps. Let's get started.

The Focus Engine is the part of tvOS that manages which view the user is currently focused on in tvOS's interface, as well as moving to another view when the user performs a gesture on their remote (or a game controller).

We get all of this functionality "for free", as long as we're using UIKit controls to construct the UI of our apps.

The Focus Engine will listen for a user's input like a left swipe, then it will look for views that are currently to the left on screen of the currently focused view, and move focus to them.

For many apps, that's really all there is to it. 1.) Construct our UI, 2.) Focus Engine handles it all.

For more advanced usage, there's a few places we can "hook into" and interact with the Focus Engine. Here's some examples:

  • We can update the currently focused view in a similar way to how make layout changes on iOS. First we override the preferredFocusView property on our UIViewController, then we call setNeedsFocusUpdate. (Only works if the view controller already contains the currently focused view).

  • We can have the system ask us if it's ok to make a proposed focus change. For this we'd implement shouldUpdateFocusInContent on our view controllers, answering ‘yes' or ‘no'.

  • Lastly, we can use the didUpdateFocusInContext function to (for example) animate our custom views alongside the system's animations.

When a user first installs one of our apps, it'd be nice if we could hold their hand a bit, pointing out where the important bits are.

Today we'll look at Gecco, a library that can make creating these walkthroughs quite easy. Let's dive in.

Gecco is built using UIKit's custom transition protocols.

This means there's simply a view controller that we'll configure and present over our content that will display the "spotlight" effect.

In it's most basic form, we can use Gecco by creating a SpotlightViewController, then configuring it's spotlight property to be one of the supplied Oval, Rect, or RoundRect enum cases:

let spotlightVC = SpotlightViewController()

spotlightVC.spotlight = .Oval(
  center: CGPointMake(100, 100),
  width: 100)

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

Then we simply present it like normal, neat! In simple cases this is all we need, next we'll look at customizing things further.

We can actually subclass Gecco's SpotlightViewController and use it's spotlightView property to animate the spotlight effect calling out different shapes, even morphing from one to another:

class CustomSpotlightViewController : SpotlightViewController {
  var currentStep = 0
  func updateSpotlightView() {
    switch currentStep {
      case 0: spotlight = .Oval(center: CGPointMake(349, 42), width: 50)
      case 1: spotlightView.move(.Oval(center: CGPointMake(300, 42), width: 50))
      case 2: spotlightView.move(.RoundedRect(center: CGPointMake(375 / 2, 42), size: CGSizeMake(120, 40), radius: 6), moveType: .Disappear)
      case 3: dismissViewControllerAnimated(true, completion: nil)
      default: break
    }

    currentStep++
  }
}

Note about hard-coding values: We've used exact values here to make it easy to understand how Gecco works. In a real world scenario, you would probably be grabbing these values from your views after they've been through a layout pass.

More info about Gecco can be found at git.io/gecco

iOS has some great built-in support for different keyboards for different types of data, but the built-in numeric-keypad keyboard style does leave a few things to be desired (a "done" button, decimal point, a "hide" button, etc).

Today we'll look at a library called MMNumberKeyboard by Matías Martínez that does a great job of filling in these gaps. Let's try it out.

Setting up a text field to use the library's keyboard is extremely straightforward. First we'll create and configure the keyboard:

let numberKeyboard = MMNumberKeyboard(frame: CGRectZero)
numberKeyboard.allowsDecimalPoint = true
numberKeyboard.delegate = self

Then we'll set it as the inputView of our text field:

spaceshipPriceTextField = UITextField(frame: CGRectZero)
spaceshipPriceTextField.inputView = numberKeyboard

Now, when our text field becomes the first responder, the keyboard will be much more useful. Lastly, we can (optionally) add some delegate functions:

func numberKeyboard(numberKeyboard: MMNumberKeyboard!, shouldInsertText text: String!) -> Bool
func numberKeyboardShouldReturn(numberKeyboard: MMNumberKeyboard!) -> Bool

More info about MMNumberKeyboard can be found at git.io/numberkeyboard

Topics

#172: UIMenuController Basics 💬

Topics

Menus have been a part of iOS since iOS 3. They're those little black bubbles that often contain copy/paste/etc options.

They're great for offering more intermediate or advanced features right in-context over the content they'll manipulate.

Today we'll take a look at the basics of getting them working in our own view controllers. Let's get started:

First we'll add a function for when our menu item is selected:

class SpaceshipsViewController : UIViewController {
  func copySpaceship() {
    // do the thing
  }
}

Then we'll need to override a couple of functions:

extension SpaceshipsViewController {
  override func canBecomeFirstResponder() -> Bool {
    return true
  }

  override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
    switch action {
    case Selector("copySpaceship"): return true
    default: return false
    }
  }
}

Next, we'll need to actually show our menu. There are plenty of ways to do this, but one common technique is a long press gesture recognizer:

view.addGestureRecognizer(
  UILongPressGestureRecognizer(
    target: self, 
    action: Selector("showMenu")
  )
)

Finally, we'll implement our showMenu function:

func showMenu() {
  becomeFirstResponder()

  let menu = UIMenuController.sharedMenuController()
  menu.menuItems = [UIMenuItem(title: "Copy", action: Selector("copySpaceship"))]

  menu.setTargetRect(view.bounds, inView: view)
  menu.setMenuVisible(true, animated: true)
}

Success!

Breakpoints in Xcode are a great way to pause execution and poke around at what's going on in our app. Today we'll look at some ways we can use them without pausing execution. Let's dive in.

We'll start by pressing (while editing) to set a breakpoint:

Then, we can right click to edit the breakpoint. The popover that appears may appear simple, but it's packing tons of power.

We'll enable that last checkbox straight away, then click Add Action to see our available options.

We can add a Debugger Command action to (for example) print a value whenever the breakpoint hits. We now have a dynamic log statement we can easily enable/disable.

The last option, "Sound", might not seem that useful at first glance. We can use this to (for example) debug high performance code, or whenever we'd like to hear an audible "ding" when a certain code path is hit.

After adding an action, we'll see a couple +/- buttons, this means we can actually pair both these techniques together, neat. Happy debugging!

Today we'll checkout another great library from Alexander Schuch called StatefulViewController.

It can help us add loading, empty, and error states to our view controllers. Let's get started:

Most apps we build must load data before showing it to the user. Those apps will also need to handle the case where there's no data available, or when something goes wrong.

StatefulViewController allows us to easily add all of this functionality to our view controllers.

While this might seem trivial, there's actually quite a bit of logic and state management involved. StatefulViewController helps eliminate boilerplate and help us focus on building the parts of our app that make it unique and useful.

Let's try it out. First, we'll make a view controller, and adopt the protocol:

class ViewController: UIViewController, StatefulViewController { }

Then we'll need to set up our placeholder views:

// inside viewDidLoad
loadingView = LoadingView(frame: view.frame)
emptyView = EmptyView(frame: view.frame)
errorView = ErrorView(frame: view.frame)

Next, we'll need to set up the initial state when the view controller's view is about to appear:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)
  setupInitialViewState()
}

Now we can visually start and stop loading when we need:

startLoading()
endLoading(error: nil)

Lastly, we'll need to implement one more function to let the library know when there's content available:

func hasContent() -> Bool {
  return spaceships.count > 0
}

More info about StatefulViewController can be found at git.io/stateful

Parallax headers have become very common in iOS apps. They're a great way to (for example) show off large graphical content, or summarize/focus a screenful of content.

Today we'll look at a fantastic library from Maxime Epain called MXParallaxHeader. Let's go:

A parallax header essentially involves displaying a view above a scroll view, which grows and shrinks as the user scrolls the scroll view.

It's also common to see the content of the parallax header view re-center itself as the user scrolls, so the content is visible for as long as possible while scrolling up.

MXParallaxHeader offers a few different options that make customizing this behavior quite simple.
We can center, fill, or lock our content to the top or bottom. Neat!

Let's use MXParallaxHeader to add a simple parallax header to a scroll view:

scrollView.parallaxHeader.view = headerView
scrollView.parallaxHeader.height = 150
scrollView.parallaxHeader.mode = .Fill
scrollView.parallaxHeader.minimumHeight = 20

Even cooler than that, MXParallaxHeader extends UIViewController to add a couple extra properties that allow us to easily add parallax header to any view controller.

Header views can optionally adopt the MXParallaxHeaderProtocol to implement custom parallax animations based on the user's scroll progress:

func parallaxHeaderDidScroll(parallaxHeader: MXParallaxHeader) {
  let angle = parallaxHeader.progress * CGFloat(M_PI) * 2
  self.falcon.transform = CGAffineTransformRotate(CGAffineTransformIdentity, angle)
}

More info about MXParallaxHeader can be found at git.io/mxparallaxheader

Today we'll look at a fun and interesting library that can help improve how users navigate around in our apps.

It comes to us from Taiki Suzuki and it's called SAHistoryNavigationViewController.

It works by augmenting UINavigationController's traditional interactions to add a gesture to view each of the view controller's that have been pushed on to it. We can use it just like a regular navigation controller:

let vc = SpaceshipsViewController()
let nc = SAHistoryNavigationViewController(rootViewController: vc)

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

The library will handle showing the history scroll view when the user long presses the back button. If the user's device supports 3D Touch, the history scroll view activate with that gesture instead.

We can also trigger the history view to be shown at anytime like this:

navigationController?.showHistory()

We can customize the background color shown behind all the “zoomed out” view controller easily:

navigationController?.setHistoryBackgroundColor(.blackColor())

We can also customize the background view by subclassing, and overriding the contentView function.

More info about SAHistoryNavigationViewController can be found at git.io/historync

Page 5 of 9