Active Filters: UIKit

Topics

#82: Keyboard Notifications πŸ””

Topics

iOS's software keyboard has evolved quite a bit over the years. Just last week we saw a new variant of it on the new iPad Pro. The user can also connect a hardware keyboard at anytime, hiding the software one. It's important, now more than ever, that we not make any assumptions about how and when the keyboard will transition on and off the screen.

It'd be great if the system could tell us all the details of how and where it's about to animate. Then we could use that info to move our own controls and views right alongside it. For this we can listen for some keyboard notifications_, and react appropriately. Let's take a look.

We'll start by registering for two notifications in viewDidLoad:

NSNotificationCenter.defaultCenter().addObserver(self,
  selector: "keyboardWillShowOrHide:", name: UIKeyboardWillShowNotification, object: nil)

NSNotificationCenter.defaultCenter().addObserver(self,
  selector: "keyboardWillShowOrHide:", name: UIKeyboardWillHideNotification, object: nil)

Fun fact: Starting in iOS 9, we don't need to unregister these in deinit, the system now does it for us! πŸŽ‰

Next, we'll grab all the necessary values out of the notification's userInfo, and use them to animate our own views exactly alongside the keyboard as it slides on or off the screen:

func keyboardWillShowOrHide(notification: NSNotification) {
  let i = notification.userInfo!
  let start = view.convertRect(i[UIKeyboardFrameBeginUserInfoKey]!.CGRectValue(), fromView: view.window)
  let end = view.convertRect(i[UIKeyboardFrameEndUserInfoKey]!.CGRectValue(), fromView: view.window)

  bottomConstraint.constant -= (end.origin.y - start.origin.y)
  view.setNeedsUpdateConstraints()

  let duration = i[UIKeyboardAnimationDurationUserInfoKey]!.doubleValue

  UIView.animateWithDuration(duration, delay: 0, options: .BeginFromCurrentState, animations: {
    self.view.layoutIfNeeded()
  }, completion: nil)
}

We grab the start and end frames, convert them to our view controller's view's coordinate space, and use the difference to move a constraint. Then we animate the constraint like we covered Bite #9.

Topics

#80: View Controller Previews πŸ‘“

Topics

View Controller Previews are another iOS SDK feature announced with 3D Touch on the iPhone 6S and iPhone 6S Plus. They allow users to easily "peek" into a piece of content by pressing lightly, then pressing a little more firmly to actually open it. Let's take a look.

First we'll make sure 3D Touch is available, and register for previewing:

if traitCollection.forceTouchCapability == .Available {
  registerForPreviewingWithDelegate(self, sourceView: view)
} else { print("3D Touch is not available on this device.") }

Then we need to conform to UIViewControllerPreviewingDelegate:

func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
  guard let indexPath = tableView.indexPathForRowAtPoint(location),
            cell = tableView.cellForRowAtIndexPath(indexPath) else { return nil }

  let vc = DetailViewController(spaceship: spaceships[indexPath.row])

  vc.preferredContentSize = CGSize(width: 0.0, height: 200.0)
  previewingContext.sourceRect = cell.frame

  return detailViewController
}

In the first function we'll need to return a fully configured UIViewController that will serve as the preview. We're coming from a table view controller, so we'll use the passed in location to grab the cell that was pressed, and configure our preview view controller.

We also set the cell's frame to be our previewContext's sourceRect. This accomplishes "blurring out" all the other elements on the screen during the preview.

We finish things out by implementing the second function, called when the user decides to "pop" into the content. We could use a different view controller but we'll just reuse our existing preview one and show it:

func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
  showViewController(viewControllerToCommit, sender: self)
}

Topics

#79: Static Shortcut Items πŸ“Œ

Topics

Shortcut Items were introduced with the announcement of 3D Touch on iPhone 6S and iPhone 6S Plus. They allow users to quickly jump to a particular section or feature of an app, straight from the home screen.

Shortcut Items come in two forms: Static, and Dynamic. Static shortcut items live in the Info.plist of our project. Dynamic shortcut items are defined in code, and configured at run time. Let's take a look at how to implement static shortcut items.

The first step is to configure the shortcut items in our Info.plist file:

Then, we need to implement one new function on our app delegate. We're using JLRoutes here (covered in Bite #62) to run some code when a given URL is passed in.

Then we'll call the provided completionHandler, with a Bool indicating whether we were able to handle the item.

func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {
  let url = NSURL(string: shortcutItem.userInfo["url"])!

  let didHandle = JLRoutes.routeURL(url)

  completionHandler(didHandle)
}

Note: At publish time of this Bite, Xcode did not offer a way to test shortcut items in the Simulator. To test shortcut items' functionality, this project by Conrad Kramer was used.

UPDATE: We covered Dynamic Shortcut Items in Bite #88. Head there to read about how to modify an app's shortcut items in code, at runtime.

Topics

#73: UIImage Tips πŸŒ„

Topics

UIImage is a deceptively powerful part of UIKit. Let's take a look at some of the different ways we can use it:

Template Images

Let UIKit do the heavy lifting by using template images. UIKit only looks at the alpha channel, and draws the image using the tint color of the view it's contained in.

UIImage(named: "filters-icon")!
  .imageWithRenderingMode(.AlwaysTemplate)

   

Animation

UIImages can be animated. A UIImage can contain many images within it, as well as a duration. Put the containing image in a UIImageView and call startAnimating to see it.

Pattern Images

This one's fun. We can create a UIColor from a UIImage. The resulting "color" will be our image, tiled. We can then use this UIColor as a background color of a view.

UIColor(patternImage: UIImage("bg-pattern")!)

Stretchable Images

Sometimes we don't want to write a bunch of Core Graphics code to implement things like rounded corners, inner shadows, etc. UIImage has a great feature for implementing resizable (also sometimes called 9-patch) images.

We want to implement a fancy button. All we need is a tiny little 9x9 image. In code, we'll tell UIKit to load the image, and create a resizable image from it.

let buttonBGImage = UIImage(named: "button-stretchable")!
  .resizableImageWithCapInsets(UIEdgeInsets(
    top: 4.0,
    left: 4.0,
    bottom: 4.0,
    right: 4.0
  ))

Write JPEG Data to a File

The second parameter controls quality/compression of the JPEG. Smaller numbers result in smaller but uglier image files.

let data = UIImageJPEGRepresentation(someImage, 1.0)

data?.writeToFile(imageFilePath, atomically: true)

Topics

#72: Customizing Navigation Bars πŸ€

Topics

Navigation Bars are a versatile part of iOS. Let's take a look at some different ways we can customize their appearance and behavior.

Custom Fonts and Colors

let nba = UINavigationBar.appearance()
let bba = UIBarButtonItem.appearance()

let titleTextAttributes = [ NSFontAttributeName : navBarFont ]

nba.barTintColor = green
nba.tintColor = darkGreen
nba.titleTextAttributes = titleTextAttributes

bba.setTitleTextAttributes(titleTextAttributes, forState: .Normal)

Hide/Show Navigation Bar when Scrolling

navigationController?.hidesBarsOnSwipe = true

This will cause our navigation bar to slide on and off screen as the user scrolls, just like in Safari for iOS.

Empty Back Button

We want a back button with no text. We could set the title of our first view controller to an empty string, but then our first view controller would be title-less. We can get the best of both worlds by giving the first view controller a custom back button item with an empty title. We'll need to do this before we push on a new view controller.

navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .Plain, target: nil, action: nil)

Use an Image as a Title

let imageView = UIImageView(image: UIImage(named: "app-logo")!)
navigationItem.titleView = imageView

Topics

#71: UIActivityViewController Basics 🌼

Topics

UIActivityViewController is one of the main ways we can allow our apps to talk with each other, as well as allow our users to share content with the world. Let's take a look. UIActivityViewController accepts an array of activityItems which can be of many types:

let shareText = "#71: UIActivityViewController Basics"
let shareURL = NSURL(string: "https://littlebitesofcocoa.com/71")!
let shareImage = UIImage(named: "71-thumbnail")!

let itemsToShare = [shareText, shareURL, shareImage]

let activityVC = UIActivityViewController(
  activityItems: itemsToShare, applicationActivities: nil
)

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

The activity view controller will intelligently use each item. For example: On Twitter, the text will become the text of a tweet. The URL and image will be appended and shared natively as an image attached to the tweet.

UIActivityViewController is pretty clever. For example, it's smart enough to know if the NSURL we hand it is to a video in the user's Photos library. In such a case, it responds by letting the user share the video with services like Vimeo. We can also pass in an NSData containing the actual video.

We can even allow printing our content by wrapping it in a UISimpleTextPrintFormatter and passing that in:

let printData = UISimpleTextPrintFormatter(text: biteContent)
let itemsToShare = [biteTitle, printData]

Finally, we can use the completeWithItemsHandler closure to learn if the user actually performed a share or action in our activity view controller, as well as what specific activity they chose.

activityVC.completionWithItemsHandler = {
  (activityType, completed, items, error) in

  guard completed else { print("User cancelled."); return }

  print("Completed With Activity Type: \(activityType)")

  if activityType == UIActivityTypePostToFacebook {
    print("Shared on Facebook")
  }
}

Content Compression Resistance Priority controls how the frame of a UIView will be calculated when one or more Auto Layout constraints describe its width or height as being smaller than its intrinsic content size. Let's look at a bare bones example:

Here's a button with a really long name:

We've added a simple constraint telling Auto Layout to try to keep the width of our button at 44 points. Auto Layout does as its told and collapses our button making it completely unreadable. Don't worry, we can use Compression Resistance to stop this.

We select our button in Interface Builder, head over to the size inspector (⌘βŒ₯ + 5), and set it's horizontal Compression Resistance Priority to 1000.

Now, we'll change the priority of our original 44 point width constraint to something less than 1000. We'll use 999 to emphasize the point, but this could be any number from 0 - 999.

Success! Auto Layout now allows our button's intrinsic content size to take precedent over our width contraint:

Now that we understand Compression Resistance, understanding Content Hugging Priority is easy. It works in a very similar manner, but instead of managing whether a view is made smaller than its intrinsic content size, it deals with whether or not a view can be made larger than its intrinsic content size. Let's look at one more example to illustrate this, here's another button:

If we were to add a set of leading and trailing constraints, telling the button to be as wide as the view controller it's sitting on, it might look something like this:

But, if we set the horizontal Content Hugging Priority of our button to 1000, and the priority of those leading and trailing constraints to 999, our **button **becomes nice and small again:

Using a UISegmentedControl to switch view controllers is very common, even Apple does it:

Let's build this using UIPageViewController and UISegmentedControl.

We start with a blank storyboard, drag out a UIPageViewController and set it as the initial view controller. Then drag out two UIViewControllers to switch between. We'll select the page view controller and choose Editor > Embed In > Navigation Controller.

Then we drag out a UISegmentedControl and drop it into the title area of the navigation bar of our page view controller. Finally, we create an @IBAction function that will get called when the selected segment changes.

The code is pretty straightforward, note the use of R.swift to grab those two view controllers from our storyboard. (covered in Bite #52).

func setViewControllerForIndex(index: Int) {
  setViewControllers(
    [index == 0 ? spaceshipsVC! : crewVC!], 
    direction: .Forward, 
    animated: false, 
    completion: nil
  )
}

override func viewDidLoad() {
  super.viewDidLoad()

  spaceshipsVC = R.storyboard.main.spaceshipsVC
  crewVC = R.storyboard.main.crewVC

  setViewControllerForIndex(0)
}

@IBAction func segmentedControlChanged(sender: UISegmentedControl) {
  setVCForIndex(sender.selectedSegmentIndex)
}

extension ViewController : UIPageViewControllerDataSource {
  func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
    if viewController == crewVC { return spaceshipsVC }

    return nil
  }

  func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
    if viewController == spaceshipsVC { return crewVC }

    return nil
  }
}

Download the complete project here: j.mp/bite055

Topics

#54: Dynamic Type Basics πŸ“„

Topics

Dynamic Type is a system introduced back in iOS 7 with the intention of unifying and simplifying how users change their preference for how big text should be on the screen. While the user choice is simple, under the hood Dynamic Type earns its name by dynamically adjusting things such as spacing between characters and character weights to make text as readable as possible.

Let’s take a look at how to add simple Dynamic Type support.

Text Styles

The main way you interact with Dynamic Type is through the built-in set of text styles. As of iOS 9, 9 styles are available to choose from:

  • UIFontTextStyleTitle1
  • UIFontTextStyleTitle2
  • UIFontTextStyleTitle3
  • UIFontTextStyleHeadline
  • UIFontTextStyleSubheadline
  • UIFontTextStyleBody
  • UIFontTextStyleFootnote
  • UIFontTextStyleCaption1
  • UIFontTextStyleCaption2

Retrieving Fonts

If you’re using the system font, all you need is:

UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)

For custom fonts, you’ll need to grab a font descriptor first, then create the UIFont manually:

let fontSize = UIFontDescriptor
  .preferredFontDescriptorWithTextStyle(UIFontTextStyleHeadline)
  .pointSize

let font = UIFont(name: "Avenir Next", size: fontSize)

Lastly, you'll want to update your UI when the user changes their preferences in Settings.app. Observe this notification and trigger a re-layout when it occurs:

NSNotificationCenter.defaultCenter().addObserver(self,
  selector: "preferredContentSizeChanged:",
  name: UIContentSizeCategoryDidChangeNotification, object: nil)

Let's say you have a table view with some cells showing a list of crew members. It'd be great if when the user taps a cell, it would β€œexpand” to reveal some actions that can be performed on that crew member. Let's dive in.

Setup View Hierarchy

As you can see, we lean on UIStackView pretty heavily. The constraint shown here is the one we'll be animating.

Animate the Constraint

In our cell's setSelected(animated:) function, we animate the constraint just like we covered back in Bite #9. After the animation's done, we hide/show the toolbar. This triggers the top-level stack view to recalculate it's height.

let constant: CGFloat = selected ? 30.0 : 0.0
let options = [.AllowUserInteraction, .BeginFromCurrentState]

UIView.animateWithDuration(0.3, delay: 0.0, options: options, animations: {
  self.toolbarStackViewHeightConstraint.constant = constant
  self.layoutIfNeeded()
}, completion: { completed in
  self.toolbarStackView.hidden = !selected
})

Final Product

Our view controller takes care of resizing our cells by updating the 'expanded' state of each crew member when the selection changes and a pair of calls to tableView.beginUpdates/endUpdates inside the tableView:didSelectRowAtIndexPath: and tableView:didDeselectRowAtIndexPath:.

Download the complete working project here: j.mp/bite050

Page 8 of 9