Active Filters: UIKit

Topics

#246: UITextField B-sides πŸ“Ό

Topics

UITextField is one of the sort of "low key" hidden gems of UIKit. Today we'll look at some of the lesser-known things it can do and how to configure them. Let's dive in.

We'll begin by creating a text field and customizing its look and feel. First, instead of boring plain placeholder, let's spice things up a bit:

let placeholder = NSMutableAttributedString()

placeholder.append(
  AttributedString(
    string: "Spaceship ",
    attributes: [NSFontAttributeName : boldFont]
  )
)

placeholder.append(
  AttributedString(
    string: "Name",
    attributes: [NSFontAttributeName : font]
  )
)

textField.attributedPlaceholder = placeholder

Nice, now let's customize how our text field works.

Suppose we wanted to clear out its contents when a user tapped it. We simply set clearsOnBeginEditing to true.

Similarly, if we wanted to clear the contents not when the user tapped the field, but rather when the user began typing content, we can set clearsOnInsertion to true.

Let's add some more "padding" to our text field. This turns out to be a tricker than expected.

UITextField allows for customization of the rects it uses to draw text via subclassing:

class PaddedTextField : UITextField {
  override func textRect(forBounds bounds: CGRect) -> CGRect {
    return bounds.insetBy(dx: 16.0, dy: 8.0)
  }
}

Finally, we'd like allow users to fancy up (bold, italic, underline) the names of their spaceships. Turns out UITextField makes this super easy:

textField.allowsEditingTextAttributes = true

With attribute editing enabled, we now get this great editing rich-text UI for free. Neat!

In Bite #241, we learned about all the great improvements to UICollectionView's cell lifecycle in iOS 10. Today we'll check out another iOS 10 improvement to UICollectionView: The prefetchDataSource! Let's get started.

UICollectionView gains a new property this year called prefetchDataSource. Just like the existing delegate and dataSource properties, we an simply set it to some object that implements the new UICollectionViewDataSourcePrefetching protocol.

This protocol is brand new in iOS 10, and requires we implement just one new function:

public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath])

When this function is called, we can examine the indexPaths array we're passed in to know which cells are "coming up soon" and thus which cells we should probably begin loading the data for. The indexPaths will be ordered "ascending by geometric distance" which basically means the indexPaths that need to be displayed on screen soonest will be first.

Let's use this new functionality to let our UICollectionView pre-load the images that go in our cells, This way when they scroll on screen, the images will already be visible and won't have to "load in" in front of the user. Classy! 🎩

class SpaceshipsViewController : UICollectionViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    collectionView?.prefetchDataSource = self
  }
}

extension SpaceshipsViewController : UICollectionViewDataSourcePrefetching {
  func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
    ImagePreloader.preloadImagesForIndexPaths(indexPaths) // happens on a background queue
  }

  func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
    ImagePreloader.cancelPreloadImagesForIndexPaths(indexPaths) // again, happens on a background queue
  }
}

That's literally it. We make sure to implement the optional cancelPrefetchingForItems function so that we don't waste resources preloading images the user won't actually see.

UICollectionViewDataSourcePrefetching doesn't replace our existing data loading architecture/functionality. It's essentially a hint as to which cells will be displayed soon, and which ones we might want to begin loading content for.

Finally, just like in Bite #241, we can opt-out of this new prefetching behavior by either simply leaving prefetchDataSource set to nil, or by setting isPrefetchingEnabled to false.

Oh one more thing: All of this works exactly the same on UITableView too now, thanks to the (almost identical) new UITableViewDataSourcePrefetching protocol.

UICollectionView is one of the most important tools in our toolbox as iOS developers. Achieving smooth scrolling performance is a badge of honor for many of us. This year in iOS 10, Apple has made substantial changes to how cells make their way into collection views.

Today we'll take a look at these changes, and how we can take advantage of them in our apps. Let's dive in! 🏊

Here's the TLDR:

UICollectionView will now call cellForItemAtIndexPath: way before it used to. Sometimes the cells we return from that function won't even end up being displayed. We should proceed accordingly.

This dramatically improves scrolling performance as the system intelligently manages everything, giving us beautiful, buttery-smooth, 60fps scrolling. Neat!

Here's the slightly more wordy version:

In iOS 10, Apple has enhanced UICollectionView's ability to "pre-load" cells before they appear on screen.

Essentially, UICollectionView is now aware of the direction the user is scrolling, and will "look ahead" to cells that will be coming on to screen soon, call the appropriate existing delegate functions (like cellForItemAtIndexPath:) as needed.


Let's look at how the UICollectionViewCell lifecycle has changed in iOS 10. We'll start with how things are today, in iOS 9:

First, cellForItemAtIndexPath: is called, and we dequeue a cell. UICollectionView pulls it from the reuse queue, and calls prepareForReuse on it.

Next, we'll configure the cell to our needs. Setting the content of labels, etc.

Then, right before the cell is scrolled into view, willDisplayCell:atIndexPath: is called.

The user looks at the beautiful cell we've created until they grow tired of it, and eventually scroll it off screen.

At this point, we'll get a didEndDisplayingCell:atIndexPath: call, and our cell will re-enter the reuse queue.

If the user changes their mind and scrolls back up, the whole process starts over with cellForItemAtIndexPath: 😨


Now, how about in iOS 10? Next we'll look at how this process has improved:

In iOS 10, things are mostly the same, all the way up until the point where the cell goes off screen at the end.

Yes, didEndDisplayingCell:atIndexPath: is called as usual, but the cell is not immediately put into the reuse queue.

Instead, the system keeps it around for a bit. This way, if the user starts scrolling the other direction, the cell is already ready to be displayed on screen again. Very cool!


UICollectionView also has a new trick when working with multi-column layouts.

(This is where the β€œprefetching” bit comes in).

In these layouts, the system will optimize for scrolling performance by sending cells through their lifecycle sooner, at times determined intelligently by the system.

Cells will go through their lifecycle one at a time, each being created via an incoming call to cellForItemAtIndexPath:.

Later, once the entire "row" of cells is about to be scrolled on to screen, willDisplayCell:atIndexPath: will be called on each cell.

Apple describes cell pre-fetching as an "adaptive" technology. In other words, it will respond to how users are interacting with our apps.

For example, UICollectionView will try to look for "down time" when the user isn't scrolling (or at least isn't scrolling quickly) to prefetch some cells. If the user begins wildly scrolling very quickly, cell prefetching will stop until they slow down.


Finally, here's some general advice and info that we should consider when architecting our UICollectionView code going forward:

  • Set up our cells in cellForRowIndexPath:, try to keep the willDisplay/didEndDisplay callbacks light weight.

  • We should now expect some cells to be created (cellForItemAtIndexPath:) but never displayed.

If for some wacky reason we didn't want this wonderful new behavior, we can set our collection view's isPrefetchingEnabled property to false (it defaults to true in iOS 10).

These improvements are obviously really great, but what about actually doing the work of loading the data that gets displayed in our cells. Well, for that you'll have to come back for tomorrow's Bite!

UISegmentedControl is great, but it's not animated, and has limits to how much you can customize it. UISwitch is great too, it is animated, but it's limited to a yes/no choice, and customization again can be quite tricky. Today we'll look at a library from George Marmaridis called BetterSegmentedControl that aims to helps us here.

Let's try it out in a navigation bar:

let c = BetterSegmentedControl(titles: ["Lights On", "Lights Off"])

c.frame = CGRect(x: 35.0, y: 40.0, width: 200.0, height: 30.0)
c.cornerRadius = 3.0
c.titleFont = UIFont(name: "Avenir", size: 13.0)
c.backgroundColor = .darkGrayColor()
c.titleColor = .lightGrayColor()
c.selectedTitleColor = .whiteColor()
c.bouncesOnChange = false

navigationItem.titleView = c

Awesome! More info about BetterSegmentedControl can be found at git.io/bsc

Life needs limits. Whether its for usability reasons or maybe we're building the next Twitter, eventually we'll need to enforce a limit on how much content a user can enter into a UITextView. Let's take a look at how to do this.

let CharacterLimit = 182

class ViewController: UIViewController, UITextViewDelegate {
  let textView = UITextView(frame: CGRect.zero)

  override func viewDidLoad() {
    super.viewDidLoad()
    textView.delegate = self
  }

  func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    let currentText = textView.text as NSString
    let updatedText = currentText.stringByReplacingCharactersInRange(range, withString: text)

    return updatedText.characters.count <= CharacterLimit
  }
}

We'll start by defining our limit as a global variable, making it easy to change later.

Next, we'll add a new UITextView to our view controller, and set ourselves as its delegate. (The text view's layout is handled off camera).

Finally, in the shouldChangeTextInRange delegate function, we'll perform our checks.

We'll apply the proposed changes, and check if the resulting length is within our limits. The Bool we return describes if the change should be allowed.

And, we're done!

Update: March 28th, 6:15 PM

A previous version of this Bite contained an inferior implementation of the shouldChangeTextInRange: function. The one shown here comes from reader Filip Radelic. Thanks Filip!

Our apps can do tons of useful things with users' data. Before we can though, we must always ask for their permission. Whether it's their physical location, photos, or contacts, the user is always in control of when and how we can access their data.

Each of these respective APIs has it's own way of asking the user if they will allow us to access the data within.

Today we'll check out a library called Permission that unifies all of these permission request into one beautiful API. Let's dive in.

We begin by declaring which piece of data we'd like access to:

let permission: Permission = .Photos

At this point, permission.status will report .NotDetermined (as we'd expect). Next we'll request the permission, this is what causes the prompt to be presented to the user:

permission.request { status in
  switch status {
  case .Authorized:    print("yay!")
  case .Denied:        print("boo")
  case .Disabled:      print("boo boo")
  case .NotDetermined: print("Β―\_(ツ)_/Β―")
  }
}

But wait, there's more!

Permission is smart. If we request access to something the user has already said β€˜no' to, it will present an alert (with customizable title/message/buttons) that allows the user to jump to the part of Settings.app needed to flip the switch back to β€˜yes'.

Additionally, Permission also provides a PermissionSet type to do this on groups of permissions (i.e. Contacts + Camera + Photos).

There's also a PermissionButton (a UIButton subclass) that can display the current permission state, and prompt when tapped.

More info about Permission can be found at git.io/permission

In-App Purchases have become a very important part of our business. Adding them to an app can involve quite a bit of boilerplate code though. Today we'll look at SwiftyStoreKit, a framework from Andrea Bizzotto that can help.

SwiftyStoreKit provides a few static functions we can use to easily implement all the common In-App Purchase tasks like getting the info about our "products" from Apple, purchasing, restoring, and even receipt verification. Let's take a look:

SwiftyStoreKit.retrieveProductInfo("com.littlebitesofcocoa.products.volume1") { result in
  switch result {
    case .Success(let product):
      let priceString = NSNumberFormatter.localizedStringFromNumber(product.price, numberStyle: .CurrencyStyle)
      print("Product: \(product.localizedDescription), Price: \(priceString)")

    case .Error(let error): print("Error: \(error)")
  }
}

We pass in the product identifier, and supply a closure that can process the result. If successful, we use NSNumberFormatter (Bite #182) to print a nice looking summary of the product.

From here, we just need to wire up the other functions SwiftyStoreKit provides like purchaseProduct, restorePurchases, and verifyReceipt.

More info about SwiftyStoreKit can be found at git.io/swiftystorekit

GIFs are amazing. They make us laugh, cry, and are the medium of choice for cat owners worldwide. 🐱

Unfortunately, GIFs can also be a bit of a headache when it comes to performance. Imagine an iOS app that displays a vertically scrolling list of animated GIFs. Making sure each of those GIFs is properly scaled, animating smoothly and handling memory well can be trickier than you might think.

Today we'll check out a library from Reda Lemeden called Gifu that aims to provide a high performance solution for this.

let imageView = AnimatableImageView(frame: CGRect.zero)
imageView.animateWithImage(named: "cat.gif")

It's that simple! Under the hood, AnimatableImageView uses CADisplayLink. It will keep the current frame, as well as the next few frames in memory, ensuring we're good memory citizens. It ends up looking like this:

Gifu also provides a few functions (startAnimatingGIF, stopAnimatingGIF, and isAnimatingGIF) for controlling the actual animation. Nice.

More info about Gifu can be found at git.io/gifu

Some libraries and frameworks provide large, far-reaching sets of tools for us to build our apps. Sometimes though, we just need a single component. Today we'll look at one such component that does one thing and one thing well: text views that adjust their height as a user types into them.

It's called NextGrowingTextView and it's by Hiroshi Kimura.

let textView = NextGrowingTextView(frame: CGRect.zero)

Then, we can use it as the inputAccessoryView of our view controller. This will cause it to be displayed when it becomes first responder. (Be sure to also return true from canBecomeFirstResponder too!)

self.inputAccessoryView = textView

NextGrowingTextView has a couple of properties for min/max number of allowed lines, but the real star is its robust delegates property allowing us to set closures for all sorts of events:

textView.textViewDidChange = { textView in print(textView) }

More info about NextGrowingTextView can be found at git.io/growingtextview

We've covered 3D Touch quite a bit here, we checked out View Controller Previews with Peek and Pop back in Bite #80.

Peek/Pop are great features, it's a shame only our users with the latest hardware can take advantage of them. Today, we'll check out a brand new library that aims to remedy this. It's from Roy Marmelstein and called PeekPop. Let's dive in.

The basic idea of PeekPop is to allow us to support 3D Touch style peeking and popping on devices that don't actually have 3D Touch hardware

The API is quite similar to UIKit's built-in one, let's try it out.

First, we'll register as a delegate, then conform to it:

peekPop = PeekPop(viewController: self)
peekPop?.registerForPreviewingWithDelegate(self, sourceView: collectionView!)

func previewingContext(previewingContext: PreviewingContext, viewControllerForLocation location: CGPoint) -> UIViewController? {
  return self.previewControllerForLocation(location)
}

func previewingContext(previewingContext: PreviewingContext, commitViewController viewControllerToCommit: UIViewController) {
  self.navigationController?.pushViewController(viewControllerToCommit, animated: false)
}

That's it! Neat.

PeekPop will use 3D Touch if available, then fall back on older devices to monitoring signficant changes in a UITouch's majorRadius property. (Pressing harder usually causes this to increase, even on older devices, since often more of the surface area of the finger is in contact with the screen).

More info about PeekPop can be found at git.io/peekpop

Page 3 of 9