Active Filters: UIKit

Topics

#321: Opening Files from the Files App 📂

Topics

Apple's Files.app is a great way to interact with files on iOS.

Today, we'll begin taking a look at how our app can better integrate with it. First up, we're going to learn how to allow users to open files in our app, from the Files.app. Let's get started.

We'll begin by creating a new project with the Single View App template in Xcode. In the future, we'll look at how Document Picker apps work, but for simplicity, we'll use just a plain single view app this time.

Then, we'll head into our AppDelegate.swift file, and add a new function:

func application(
  _ app: UIApplication, 
  open inputURL: URL, 
  options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
  // TODO
}

This application(_:open:options:) function will be called when the user selects a file inside the Files app and presses the Share (  ) button on it, and selects our app in the UIActivityViewController that appears.

Pro Tip: We can also access this by long-pressing a file in the Files app and selecting Share from the menu:

Next we'll need to tell Xcode that our app supports the type of files we want to our app to be able to open. For this, we'll choose Text Files.

We'll click on our project at the top of the project navigator and click Info at the top. We'll find this screen:

We want that area labeled Document Types (0). We'll click the + button and enter in Text for the Name and public.text for Types.

(This public.text thing is part of a system called Uniform Type Identifiers, Apple uses these to identify specific kinds of file types. Read more here).

Then, we'll add a new key to our Info.plist to make our app work more smoothly. We'll right-click on the keys at the top and select Add Row, then add the key LSSupportsOpeningDocumentsInPlace. We'll make it a Boolean and set its value to YES:

This allows our app to work with the files users open, without needing to first copy them into its own sandbox directory.

Finally, we'll head back to our AppDelegate.swift file and fill out that function we added earlier:

func application(
  _ app: UIApplication, 
  open url: URL, 
  options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
  let needTo = url.startAccessingSecurityScopedResource()

  do {
    let data = try Data(contentsOf: url)
    let contents = String(
      data: data,
      encoding: String.Encoding.utf8
    )

    print(contents)
  } catch(let error) { print(error) }

  if needTo {
    url.stopAccessingSecurityScopedResource()
  }

  return true
}

First we setup a do/catch block so we can catch and print any errors that might happen during all of this. Then we call a special function on our inputURL.

Since, we're opening in place, iOS has gives us what Apple calls a "security scoped" URL. (Read more about them here).

First we have to tell the system when we begin using a URL, saving the Bool value that url.startAccessingSecurityScopedResource() returns. If that Bool is true, then we'll also need to call stopAccessingSecurityScopedResource on our URL once we're finished with it.

After that we simply read the Data at the URL, then put it into a String so we can print it out.

Now we can Build & Run the app on our device, open the Files.app, choose an .txt file, share it, and our app appears in the Share sheet:

Tapping our app's icon opens it, and runs our code to print out the contents of the file, neat!

That's all for today. Shout out to friend of the show Casey for inspiring the idea for this Bite!

Have an idea for a Bite? Send it in!

Topics

#312: Asset Catalog Improvements 🎨

Topics

Asset Catalogs have been around for a few Xcode releases. They're a great way for us to organize and configure graphical assets (among many other things) for our app. Today we'll check out the improvements to Asset Catalogs in Xcode 9. Let's dive in.

First up, colors.

Yep, we can now define and organize named colors inside of an Asset Catalog!

We can select "New Color Set", and then use all of the normal features of Asset Catalogs, including new Wide Gamut support.

Then, in our code we can reference these colors like this:

view.backgroundColor = UIColor(named: "wood")

Neat!

The second big addition we're going to look at is "real" vector-based asset support.

In past Xcode releases, we were able to add image assets to our catalogs with a format of PDF. This worked great, but under the hood, Xcode would simply render our asset at the @1x, @2x, and @3x sizes and save non-vector (i.e. png images) into our app's bundle.

In Xcode 9 however, we're given a beautiful new checkbox called "Preserve Vector Data".

This means that if our image is loaded in our code, and we ask it to display at a a larger size than it is by default, it will be scaled up at runtime by the system.

This means we can now write code like this without any quality loss at render time:

let normal  = UIImageView(image: UIImage(named: "cocoapod"))
normal.tintColor = UIColor(named: "covfefe")

view.addSubview(normal)

let big  = UIImageView(image: UIImage(named: "cocoapod"))
big.tintColor = UIColor(named: "cream")
big.frame = CGRect(x: 50, y: 200, width: normal.bounds.size.width * 2, height: normal.bounds.size.height * 2)

view.addSubview(big)

Very, very cool.

Topics

#311: Round Corner Improvements ✅

Topics

Today we're going to talk about corners. Specifically, rounded ones. In iOS 11, Apple has improved the way we can specify and work with the rounding of our views' corners. Let's take a look.

Let's begin with where we were before iOS 11.

Before iOS 11, configuring a UIView to have round corners went like this:

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 8

The clipsToBounds ensures the "clipping" takes place, and the next rounds all four corners with a radius of 8:

Pretty straightforward.

This approach works great for rounding all four corners of a view. But what if we wanted to round only some of the four corners? Previously we'd need to drop down and create our own mask layer, giving it a path we created manually. Bummer.

iOS 11 makes this way easier.

Meet CACornerMask!

Now, we can use the same code as before, with one small addition:

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 8
view.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]

Now we can use the new CACornerMask option set to specify which corners we'd like rounded. Here we've asked for just the bottom two corners to be rounded with an 8 point radius:

Neat!

Before we go, there's one last thing to know about round corners in iOS 11:

They are now completely animatable! 🎉

In previous releases, this next bit of code wouldn't do anything, but in iOS 11 this works exactly as expected:

let view = UIView()

view.clipsToBounds = true
view.layer.cornerRadius = 16

UIViewPropertyAnimator(duration: 1, curve: .linear) {
  view.layer.cornerRadius = 0
}.startAnimation()

Very cool.

Topics

#310: Screen Edges in iOS 11 📲

Topics

Continuing our look at the new tidbits and goodies from WWDC 2017, today we'll learn about the changes to screen edge behavior in iOS 11. Let's dive in.

What we're really talking about here is the behavior when a user drags from offscreen. This could be the user trying to pull down Notification Center from the top, or bring up Control Center from the bottom of the screen.

In past iOS releases, the system has looked at the visibility of the status bar to determine how to behave in these cases.

If we configured one of our view controllers to hide the status bar, the system would show a little "tab" UI with an arrow that the user would need to drag a second time before Notification Center or Control Center would be shown:

Keying off of the visibility of the status bar probably isn't the best way for us to "tell" the system what to do here.

In iOS 11, we've been given a wonderful new way to describe how our app should behave when the user performs these gestures.

Now, all we need to do is override this function in our view controlller:

override func preferredScreenEdgesDeferringSystemGestures() -> UIRectEdge {
  return .bottom
}

This way, we can describe exactly which edges of the screen we'd like to allow the system gestures to behave "normally", and which we'd like to have defer for a second drag. Neat!

Finally, if the state of our view controller ever changes enough to warrant a change in this behavior, we can call the following new function to update it:

setNeedsUpdateOfScreenEdgesDeferringSystemGestures()

Topics

#309: UIFontMetrics 📐

Topics

Happy WWDC 2017! Today we're beginning our look at the incredibly large list of updates and improvements announced this week with UIFontMetrics. Let's jump in.

We'll start with the problem we're trying to solve. It's all about Dynamic Type.

Our users can adjust their preferred Dynamic Type value in Settings.app to display the text in our apps larger and more prominently.

This works great when we want to use the system default font, since we can simply write some code like this:

let font = UIFont.preferredFont(forTextStyle: .headline)

This will return a UIFont that is appropriate for the given text style, adjusted by size and weight to match the user's Dynamic Type setting.

But what about custom fonts? In the past this was a bit cumbersome and we often had to resort to ugly hacks.

Enter UIFontMetrics! We can use this new type in iOS 11 to ask the system to scale our font size for us:

let headlineMetrics = UIFontMetrics(forTextStyle: .headline)
let fontBeforeScaling = UIFont(name: "Chicago", size: 16.0)
let font = headlineMetrics.scaledFont(for: fontBeforeScaling)

Neat!

There's always one more fun trick tucked away in UIFontMetrics, and that is scaling arbitrary values. This is great for helping us size our UI elements (for example buttons or headers) to accomodate dynamically sized fonts that live inside:

let headlineMetrics = UIFontMetrics(forTextStyle: .headline)
let heightBeforeScaling = 44.0
let height = headlineMetrics.scaledValue(forValue: heightBeforeScaling)

No more large text in tiny buttons. Very cool!

Topics

#305: Working with External Displays on iOS 📱📺

Topics

The iPhone and iPad are awesome devices with beautiful displays. Sometimes though, we might want to show parts of our app on a bigger, external screen.

Today we'll look at how we can do just that on iOS, let's dive in.

Before we begin let's break down what exactly we're talking about here:

UIKit exposes any external display we connect to our iPhone or iPad with an adapter as a UIScreen. The same is also true when we turn-on Airplay Mirroring to an Apple TV.

Now, let's write some code.

We'll eventually use Notifications to learn when a screen is connected/disconnected, but many users may launch our app with a screen already connected, so let's start by checking for that.

We'll create a couple instance properties to hold our the UIWindow and UIViewController we'll be displaying externally:

var externalDisplayWindow: UIWindow?
var externalDisplayVC: StatusBoardViewController?

Next, we'll make a function we can call when our app first launches, and when a new display is connected:

func checkForExternalDisplay() {
  guard let screen = UIScreen.screens.last
  else { return }

We'll create a new UIWindow and assign it to the external UIScreen:

  if externalDisplayWindow == nil {
    externalDisplayWindow = UIWindow(
      frame: screen.bounds
    )

    externalDisplayWindow?.screen = screen
  }

Next, we'll need a view controller. We'll create one and set it as the rootViewController just like usual:

  if externalDisplayVC == nil {
    externalDisplayVC = StatusBoardViewController()
    externalDisplayWindow?.rootViewController = externalDisplayVC

Last but not least, we'll show the window by setting its isHidden property to false.

    externalDisplayWindow?.isHidden = false
  }
}

If we connect a display now, and launch our app, we'll see our content displayed externally, neat!

Let's finish up by wiring up those notifications we mentioned earlier. First we'll start observing them:

NotificationCenter.default
  .addObserver(
    forName: NSNotification.Name.UIScreenDidConnect,
    object: nil,
    queue: nil,
    using: displayConnected
  )

NotificationCenter.default
  .addObserver(
    forName: NSNotification.Name.UIScreenDidDisconnect,
    object: nil,
    queue: nil,
    using: displayDisconnected
)

Then we'll write those two functions to handle each notification:

func displayConnected(notification: Notification) {
  checkForExternalDisplay()
}

func displayDisconnected(notification: Notification) {
  externalDisplayWindow?.isHidden = true

  externalDisplayVC = nil
  externalDisplayWindow = nil
}

Now our app will start/stop showing on the external display automatically when the user connects it. Very cool.

There's plenty more to dive into when working with external displays and UIScreen. We'll go further in future Bites.

That's all for today. Have an idea or request for a Bite? Send it along to hello@littlebitesofcocoa.com!

Topics

#303: Editing Videos in UIKit 📼

Topics

Today we're continuing our hunt for hidden gems in UIKit with UIVideoEditorController.

It's a cousin to UIImagePickerViewController that exposes just the basic video editing functionality from that class in a standalone, dedicated video editing view controller.

Let's give it a try.

First, we'll need a video file to edit. We'll use this one which is part of a freely available collection.

We'll download the video, rename it to something simple, and drag it into Xcode. We'll check the box next to our app in the dialog that appears, so it gets copied to our app target.

Now, let's write some code.

First, we'll make sure our video can be found:

guard let path = Bundle.main.path(forResource: "video", ofType: "mp4") else { return }

...and that UIKit knows how to edit it:

guard UIVideoEditorController.canEditVideo(atPath: path) else { return }

Nice. Next, we can create a new video editor view controller and configure a few things on it:

let editor = UIVideoEditorController()

editor.videoPath = path
editor.videoMaximumDuration = 10.0
editor.videoQuality = .typeIFrame1280x720

Here we've told it our path, and given it a 10 second max duration. (Pro Tip: Default is 10 minutes, set to 0 for no max).

From here we can simply present it like any other view controller:

present(editor, animated: true, completion: nil)

Neat!

The best part is all the functionality is self-contained inside the view controller.

The user can scrub through:

Trim the edges:

Then save the video back to the videoPath we set earlier:

Note: The documentation mentions UIVideoEditorController "only supporting Portrait" orientations, but it seems to work fine in all orientations.

Last but not least, we can (optionally) set a delegate on our UIVideoEditorController to get notified when the user saves or cancels (or a save fails):

editor.delegate = self

extension SomeViewController : 
    UIVideoEditorControllerDelegate, 
    UINavigationControllerDelegate {

  func videoEditorController(_ editor: UIVideoEditorController, 
    didSaveEditedVideoToPath editedVideoPath: String) {

    print("saved!")
  }
}

That's all for today. Know of an interesting UIKit B-side?

Send it on over!

Today we'll continue our series on finding hidden gems in UIKit with UIReferenceLibraryViewController.

Believe it or not there's a entire dictionary (yes like for viewing the definition of words/terms) just hanging out inside UIKit.

Let's give it a try! 📖

We can present a new reference library view controller for any word/term we want like this:

present(
  UIReferenceLibraryViewController(term: "Spaceship"),
  animated: true, 
  completion: nil
)

Very cool.

This works great, but things start to break down when we look for something and it's nowhere to be found:

We can fix this by checking if a term can be found before presenting, using the static dictionaryHasDefinition(forTerm:) function:

if UIReferenceLibraryViewController.dictionaryHasDefinition(forTerm: term) {
  present(
    UIReferenceLibraryViewController(term: term),
    animated: true, 
    completion: nil
  )
}

Much better.

UIReferenceLibraryViewController supports many different languages. New languages can be installed using the "Manage" option in the bottom-left:

Finally, it's worth noting that we get definition functionality for "free" in UITextFields via the "Look Up" item in the UIMenu that's shown when a user selects some text:

... and it's also worth noting that the view controller that appears when a user taps one of these "Look Up" menu items in a UITextField appears to be far more advanced than the what we get when presenting a plain ol' UIReferenceLibraryViewController:

The Dictionary results are still present, but we also get results across Music, Wikipedia, Movies, Websites, and even the App Store. Very cool.

(And nope, UIKit does not seem to provide a way to trigger this fancier view controller directly. Anyone interested in this should probably file a Radar.)

That's all for today. Those who want more UIKit B-Sides can check out these bites here.

Building great forms for users to enter information on iOS is tough. There's a lot of small details which are easy to get wrong. Today we'll begin looking at ways to improve this process.

First up, is navigation. 📍

Allowing for quick and simple navigation from one form field to the next can dramatically reduce friction for our users.

We'll use a great new library from Thanh Pham called UITextField-Navigation to pull this off. Let's get started.

UITextField-Navigation is built around the concept of telling each field which field should be "next" in the form.

Once we've done that for all of our fields, the library takes over and displays a navigation bar above the keyboard automatically:

We can set up these relationships super easily in in Interface Builder:

...or in code:

let nameTextField = UITextField()
let bioTextView = UITextView()

nameTextField.nextNavigationField = bioTextView

Now, whenever a user begins editing a UITextField or UITextView in our app, a navigation toolbar will automatically be shown above the keyboard:

We can use UIAppearance to completely customize the look and feel of the toolbar:

NavigationFieldToolbar.appearance().barStyle = .black
NavigationFieldToolbar.appearance().backgroundColor = UIColor.purple
NavigationFieldToolbarButtonItem.appearance().tintColor = UIColor.white

Last but not least, we aren't limited to a "standard" set of buttons/items in the toolbar.

We'll use the navigationFieldToolBar optional property that the library adds to UITextField and UITextView to first customize some individual properties directly:

guard let toolbar = nameTextField.navigationFieldToolbar else { return }

toolbar.barStyle = .default
toolbar.backgroundColor = UIColor.red
toolbar.previousButton.title = "Backward!"
toolbar.nextButton.title = "Onward!"
toolbar.doneButton.title = "Dismiss"

...then create our own array of toolbar items:

let expandButton = UIBarButtonItem(
  title: "Expand",
  style: .plain,
  target: self,
  action: #selector(expandForm)
)

let flexible = UIBarButtonItem(
  barButtonSystemItem: .flexibleSpace, 
  target: nil,
  action: nil
)

toolbar.items = [
  toolbar.previousButton,
  toolbar.nextButton,
  flexible,
  expandButton,
  flexible,
  toolbar.doneButton
]

Neat!

Learn more about UITextField-Navigation at git.io/textfieldnav.

Topics

#296: Today Extension B-Sides 📼

Topics

We first covered Today Extensions way back in Bite #36. They're a great way to offer quick, glanceable information or entry-points to our app. Today we'll take a look at some lesser known features of Today Extensions, and how we can use them in our code. Let's begin.

3D Touch Shortcut Widgets

First up, is one of the newest additions to the Today Extension world: 3D Touch Homescreen Widgets.

The coolest bit is, we don't really need to do anything to "get" this. If our app has a Today Extension, the widget will automatically appear when a user 3D Touches our app's icon.

The B-Side comes into play when we have multiple Today Extensions in our app. We'll need a way to tell the system which one to display above our icon. We can do this by adding a new key to our app's Info.plist:

UIApplicationShortcutWidget

(We can also choose "Home Screen Widget" from the keys dropdown).

Neat!

Conditional Display

Today Extensions Widgets don't have to always be visible in the Today view. We can actually tell the system whether or not our widget should be displayed with this one function:

NCWidgetController
  .widgetController()
  .setHasContent(true, 
    forWidgetWithBundleIdentifier: "com.littlebitesofcocoa.latest")

Calling this with false will hide the widget in the Today View until our app calls the function again with a true value.

Opening Our App

This one is a bit of a stretch to truly call a B-side, but it can easily be done incorrectly, so here we are.

It's quite common for a Today Extension Widget to need to open its containing app.

Apple has tightened the reigns in recent OS releases to "validate" when and how apps (and specifically Today Extensions) can open apps. Not to worry, we can use this special function on an NSExtensionContext to open a deep link into our app.

self.extensionContext?.openURL(NSURL("lboc://bites/296"), completionHandler: nil)

Pro Tip: Opening our own app (i.e. our app that contains the Today Extension) is just fine, but beware if we start trying to open other apps using this function, Apple may scrutinize our Today Extension further during App Review.

Page 1 of 9