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!

Today we'll continue looking at UICollectionView's more advanced capabilities by checking out a little-known feature hiding inside UICollectionViewController.

It allows us to automatically animate the transition between layouts when pushing on to a navigation controller stack. Neat!

Let's give it a try.

We'll start with the same simple baseline collection view controller we used in Bite #306, and Bite #307, SquaresViewController.

It looks like this:

class SquaresViewController: UICollectionViewController {
  var items = [Item]()

  override func collectionView(
    _ collectionView: UICollectionView, 
    numberOfItemsInSection section: Int
  ) -> Int {
    return items.count
  }

  override func collectionView(
    _ collectionView: UICollectionView, 
    cellForItemAt indexPath: IndexPath
  ) -> UICollectionViewCell {
    let cell = collectionView
      .dequeueReusableCell(
        withReuseIdentifier: "ItemCell", 
        for: indexPath
      )

    cell.backgroundColor = items[indexPath.item].color

    return cell
  }
}

Nothing fancy, just standard UICollectionViewController stuff.

Next, we'll subclass it twice. Once for our "small" cells:

class SmallViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()
    layout.itemSize = CGSize(width: 50, height: 50)

    super.init(collectionViewLayout: layout)

    items = (0...50).map { _ in Item(color: .random()) }
  }

We'll fill it up with some random items, like we did previously, here in our "small" subclassed view controller.

Then, when a user selects one of the cells, we'll push on a "big" view controller, setting its items property to our own:

  override func collectionView(
    _ collectionView: UICollectionView, 
    didSelectItemAt indexPath: IndexPath
  ) {
    let bigVC = BigViewController()

    bigVC.items = items

    navigationController?
      .pushViewController(bigVC, animated: true)
  }
}

Again, nothing really too crazy here. Lastly, before we can try it out, lets make our BigViewController:

class BigViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()

    layout.itemSize = CGSize(width: 100, height: 100)

    super.init(collectionViewLayout: layout)
  }

We'll make our layout's itemSize property a little larger than before, then we'll override collectionView(_:didSelectItemAt:) one more time.

This time we'll simply call popViewController:

  override func collectionView(
    _ collectionView: UICollectionView, 
    didSelectItemAt indexPath: IndexPath
  ) {
    navigationController?.popViewController(animated: true)
  }
}

If we build and run now, everything works as expected, but there's no fancy transition happening. Just a regular navigation controller push.

Let's fix this.

We'll add one line of code to SmallViewController's init function, setting useLayoutToLayoutNavigationTransitions to false:

class SmallViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()    
    layout.itemSize = CGSize(width: 50, height: 50)

    super.init(collectionViewLayout: layout)

    useLayoutToLayoutNavigationTransitions = false
    // .. 
  }

  // ..

Then we'll do set the same property to the true in BigViewController:

class BigViewController : SquaresViewController {
  init() {
    let layout = UICollectionViewFlowLayout()
    layout.itemSize = CGSize(width: 100, height: 100)

    super.init(collectionViewLayout: layout)

    useLayoutToLayoutNavigationTransitions = true
  }

  // ..

That's it. UIKit will notice that we've set our BigViewController's useLayoutToLayoutNavigationTransitions to true, and will automatically animate the transition between the two layouts.

Note that the automatic transition animation that UIKit renders is smart enough to make sure the selected cell is still visible after we push in. A nice touch.

It's always fun to discover the handy little behaviors hiding out in to UIKit.

Finally, a couple of important notes:

First, the same UICollectionView is actually reused when all of this magic happens. The pushed on view controller will not create its own. This may or may not matter for any given app, but good to know.

Second, the root view controller (SmallViewController in our case) will still be set as the delegate and dataSource when the new view controller is pushed on.

If we needed to change this behavior, we could conform to the UINavigationControllerDelegate protocol and change these values each time a different view controller is about to be be shown. The code might look something like this:

extension SquaresViewController : UINavigationControllerDelegate {
  func navigationController(
    _ navigationController: UINavigationController, 
    willShow viewController: UIViewController, 
    animated: Bool
  ) {
    guard let squaresVC = viewController as? SquaresViewController else { return }

    squaresVC.collectionView?.delegate = squaresVC
    squaresVC.collectionView?.dataSource = squaresVC
  }
}

That's all for today, have a specific UICollectionView question you'd like answered? Send it along!.

Download the Xcode project we built in this Bite right here.

Weekly Sponsor: PSPDFKit πŸ“‘

We're welcoming back one of our favorite sponsors this week, it's PSPDFKit! PSPDFKit delivers an intuitive & seamless SDK for integrating PDF functionality into apps on iOS, Android, and the Web.

PDF files have been around a long time. Over that time they evolved to become an incredibly versatile and functional format. They're also wide-ranging in terms of how they're used. Comic books, Apartment leases, flight manuals, etc. All powered by PDFs.

Being able to take advantage of all this great functionality in our apps would be huge for our users.

With it, we can beautifully display PDFs, and allow our users to intuitively annotate documents, and much, much more.

Let's get started.

We'll head over to PSPDFKit's wonderful documentation area and follow the Getting Started steps to integrate the Framework directly, or through a system like CocoaPods.

Once we've got PSPDFKit in our app, we can start playing around.

PSPDFKit provides tons of great APIs for programmtically interacting with PDF files, as well as a bunch of polished, (and incredibly customizable) user interfaces to allow our users to read and manipulate PDF files themselves.

Let's try opening a PDF, and displaying it:

let document = PSPDFDocument(url: documentURL)

let controller = PSPDFViewController(document: document, configuration: PSPDFConfiguration { builder in
    builder.thumbnailBarMode = .scrollable
    builder.scrollDirection = .horizontal
})

We create a new PSPDFDocument, then a new view controller to show it. We use PSPDFKit's awesome PSPDFConfiguration type to neatly build up our settings.

From here we can simply present it like any other view controller. Neat!

PSPDFKit stands out with an awesome, well thought-out API. It's clear that they care deeply about API design, and developer experience.

Before we go, let's try out another couple features. First up, Annotations:

// Create a new PSPDFDocument
let document = PSPDFDocument(url: documentURL)

// Create a link annotation, and set its URL
let linkAnnotation = PSPDFLinkAnnotation(url: URL(string: "https://pspdfkit.com")!)

// Position the annotation in the document
let boundingBox = CGRect(x: 200, y: 400, width: 50, height: 300)
linkAnnotation.boundingBox = boundingBox

// Customize the link annotation's appearance
PSPDFLinkAnnotationView.appearance().borderColor = .blue

// Add the newly created annotation to the document
document.add([linkAnnotation])

Very cool, Next up, Forms. Our users can of course fill out any form fields in documents they open, but we can actually fill them out programmtically too:

let document = PSPDFDocument(url: documentURL)

guard let annotations = document.annotationsForPage(at: 0, type: .widget) else { return }

for annotation in annotations {
    switch annotation {
    case let textFieldFormElement as PSPDFTextFieldFormElement:
        guard let fieldName = textFieldFormElement.fieldName else { return }
        textFieldFormElement.contents = String(format: "Test %@", arguments: [fieldName])

    case let buttonFormElement as PSPDFButtonFormElement:
        buttonFormElement.toggleButtonSelectionStateAndSendNotification(true)
    default:
        break
    }
}

Very cool. We iterated through the form fields and filled them with some example content, and toggled the selected state for any buttons in the form.

We have truly only scratched the surface of what's possible with PSPDFKit here.

PSPDFKit offers a ton of other great features like indexed search, digital signatures, and even document editing! They also provide wonderfully fast and responsive support.

Be sure to check out PSPDFKit's extensive Guides area to find complete documentation and tons of extensive guides.

Thanks to PSPDFKit for their support of LBOC!

Today we'll continue our look at UICollectionView by diving into animation. Let's begin.

We'll start with the same simple baseline collection view controller we made at the beginning of Bite #306.

Now, we'll add two new collection view layouts to our view controller:

var small: UICollectionViewFlowLayout = {
  let layout = UICollectionViewFlowLayout()

  layout.itemSize = CGSize(width: 75, height: 75)

  return layout
}()

var big: UICollectionViewFlowLayout = {
  let layout = UICollectionViewFlowLayout()

  layout.itemSize = CGSize(width: 150, height: 150)

  return layout
}()

Then, we'll override init on our view controller, and hand it our "small" layout:

init() {
  super.init(collectionViewLayout: small)
}

Nice. At this point we have a basic collection view controller, scrolling our small cells:

Now, we'll add some code to toggle between our two layouts when the user taps any cell:

override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  let newLayout = (collectionView.collectionViewLayout == small ? big : small)

  collectionView.setCollectionViewLayout(newLayout, animated: true)
}

Now we can toggle back and forth between our two layouts with a nice looking animation. Very cool.

It's super handy how the setCollectionViewLayout function is smart enough to alter the animtion so the cell we selected is still in view once the animation completes.

We can see this effect exaggerated when we bump up the itemSize of our "big" layout:

var big: UICollectionViewFlowLayout = {
  let layout = UICollectionViewFlowLayout()

  layout.itemSize = CGSize(width: 300, height: 300)

  return layout
}()

Nice!

Download the Xcode project we built in this Bite right here.

One final note here: This just one approach. There are tons of different ways to achieve this (and similar) behavior. We'll take a look at more as we dive deeper into collection views in upcoming Bites.

Have a specific UICollectionView question you'd like answered? Send it along!.

UICollectionView has always been a powerhouse. Its variety and versatility are tough to understate.

We've covered it a bit here on LBOC, but we've still only just scratched the surface. Today we'll begin to dive deeper in what all it can do by customizing how new collection view cells animate when they're being inserted. Let's begin!

We'll start by creating a new single view app in Xcode.

We'll drag out a UICollectionViewController in Interface Builder, set its class to be the ViewController one that comes with the Xcode template, then update ViewController.swift to have some basics:

class ViewController: UICollectionViewController {
  var items = [Item]()

  func addItem() { items.append(Item(color: .random())) }

  override func viewDidLoad() {
    super.viewDidLoad()
    for _ in 0...10 { addItem() }
  }

  override func collectionView(
    _ collectionView: UICollectionView, 
    numberOfItemsInSection section: Int
  ) -> Int {
    return items.count
  }

  override func collectionView(
    _ collectionView: UICollectionView, 
    cellForItemAt indexPath: IndexPath
  ) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(
      withReuseIdentifier: "ItemCell", 
      for: indexPath
    )

    cell.contentView
      .backgroundColor = items[indexPath.item].color

    return cell
  }  
}

Next, we'll embed it in a UINavigationController, and add a UIBarButtonItem to add new items.

We'll Control+Drag the bar button item into our ViewController.swift as a new add: action. We'll wire up it like so:

@IBAction func add(_ sender: UIBarButtonItem) {
  addItem()

  let indexPath = IndexPath(
    item: self.items.count - 1, 
    section: 0
  )

  collectionView?.performBatchUpdates({
    self.collectionView?.insertItems(at: [indexPath])
  }, completion: nil)
}

With this, we have a nice mostly-"boilerplate" setup. Just some square, randomly-colored, cells that fade in when they're inserted:

Not bad. Now let's customize the behavior of those cells as they're being inserted. For this, we'll need our own custom UICollectionViewLayout subclass.

Specifically we'll subclass UICollectionViewFlowLayout and override a few functions to customize the behavior of inserted cells.

class CustomFlowLayout : UICollectionViewFlowLayout {
  var insertingIndexPaths = [IndexPath]()
}

We've added a property to keep track of which index paths are being inserted during each batch update of the collection view.

To populate our property we'll need to override a couple of functions:

override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
  super.prepare(forCollectionViewUpdates: updateItems)

  insertingIndexPaths.removeAll()

  for update in updateItems {
    if let indexPath = update.indexPathAfterUpdate,
                       update.updateAction == .insert {
      insertingIndexPaths.append(indexPath)
    }
  }
}

override func finalizeCollectionViewUpdates() {
  super.finalizeCollectionViewUpdates()

  insertingIndexPaths.removeAll()
}

Nice. Nothing fancy here, we're just collecting the inserted index paths at the beginning of each update, then clearing them out at the end.

The real magic happens when we override one last function:

override func initialLayoutAttributesForAppearingItem(
  at itemIndexPath: IndexPath
) -> UICollectionViewLayoutAttributes? {
  let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)

  if insertingIndexPaths.contains(itemIndexPath) {
    attributes?.alpha = 0.0
    attributes?.transform = CGAffineTransform(
      scaleX: 0.1, 
      y: 0.1
    )
  }

  return attributes
}

What we've done here is to grab the layout attributes our collection view was going to use for the newly inserted item, and modify them slightly, before passing them along back to the system.

Here we've added a transform to scale the item down when it is first added, this will give us a nice "zoom up" effect as each item is added:

That looks neat, but let's try for something even funkier. We can change our transform line to:

attributes?.transform = CGAffineTransform(
  translationX: 0, 
  y: 500.0
)

With this one change, we can achieve an entirely different effect:

Neat!

Download the Xcode project we built in this Bite right here.

That's all for today. Have a specific UICollectionView question you'd like answered? Send it along!.

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!

Weekly Sponsor: Zendesk πŸ’‘

We're welcoming back one of our favorite sponsors this week, it's Zendesk!

None of us have ever built a flawless app.

Chances are, no matter how good we think our app's user experience is, there's probably something users will need help with while they're in our app.

Yet in many apps, getting help is reduced to a "contact us" button that launches an compose email screen, or a just drops the user on a web page.

By forcing folks out of the app to get help, small problems can turn into big annoyances, and those will almost certainly turn into negative App Store reviews and poor ratings.

With Zendesk's Mobile SDKs, we can join the makers of Angry Birds, Venmo and Swiftkey in bringing native, in-app support functionality to our apps quickly and easily.

Our users can view help and support content, and even submit tickets to support without ever leaving our app. Neat!

Tickets go into Zendesk and can include technical details about the user and their device, history with our apps, and more.

Best of all, it's included with Zendesk at no extra charge.

We can use Zendesk's "out-of-the-box" iOS UI components to get up and running quickly, or we can build your own UI with SDK API Providers.

A huge thanks to Zendesk for sponsoring!

Apple ships a ton of great APIs for playing audio in our apps.

They offer a wealth of great functionality, but also a fair amount of complexity.

Often we don't need all that power. Perhaps we're adding sound effects or audio feedback to one of our user interfaces, or maybe we just need to loop some background music in the menu of our video game.

Today we'll check out a new library from Adam Cichy called SwiftySound that can help us simplify our audio playback code. Let's take a look.

First up, simple playback:

Sound.play(file: "unscheduled-offworld-activation.wav")

or

Sound.play(url: someURL)

That's it. Yes, really.

This kind of UIImage-esque simplicity has always been sort of missing from UIKit. Neat to see.

SwiftySound doesn't stop there though, We can easily loop sounds:

Sound.play(file: "main-menu", fileExtension: "wav", numberOfLoops: -1)

We could pass in any Int here to loop the music, but we've passed -1 to indicate we want to loop forever.

One of the most useful features of SwiftySound is the inclusion of a UserDefaults-persisted flag for persisting the user's preference of sound playback being enabled or disabled.

We can use this on our app's Settings screen to allow users to easily enable/disable sound effects globally.

Sound.enabled = soundsSwitch.on

The value is then automatically persisted and respected across app launches. A nice touch by SwiftySound's author, Adam Cichy.

We're not required to use this singleton Sound instance by the way, we can always create a Sound instance and pass it around:

let sound = Sound(url: soundEffectURL)

Then later:

sound.play()

Last but not least, SwiftySound stands out for being a single Swift file.

It's great to see this kind of power and flexibility in such a tiny little dependency.

😍 More like this please!

Full documentation for SwiftySound is at git.io/swiftysound

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!

Weekly Sponsor: PSPDFKit πŸ“‘

This week, we're welcoming back an awesome sponsor to LBOC, it's PSPDFKit! PSPDFKit delivers an intuitive & seamless SDK for integrating PDF functionality into apps on iOS, Android, and the Web.

PDF files have been around a long time. Over that time they evolved to become an incredibly versatile and functional format. They're also wide-ranging in terms of how they're used. Comic books, Apartment leases, flight manuals, etc. All powered by PDFs.

Being able to take advantage of all this great functionality in our apps would be huge for our users.

With it, we can beautifully display PDFs, and allow our users to intuitively annotate documents, and much, much more.

Let's get started.

We'll head over to PSPDFKit's wonderful documentation area and follow the Getting Started steps to integrate the Framework directly, or through a system like CocoaPods.

Once we've got PSPDFKit in our app, we can start playing around.

PSPDFKit provides tons of great APIs for programmtically interacting with PDF files, as well as a bunch of polished, (and incredibly customizable) user interfaces to allow our users to read and manipulate PDF files themselves.

Let's try opening a PDF, and displaying it:

let document = PSPDFDocument(url: documentURL)

let controller = PSPDFViewController(document: document, configuration: PSPDFConfiguration { builder in
    builder.thumbnailBarMode = .scrollable
    builder.scrollDirection = .horizontal
})

We create a new PSPDFDocument, then a new view controller to show it. We use PSPDFKit's awesome PSPDFConfiguration type to neatly build up our settings.

From here we can simply present it like any other view controller. Neat!

PSPDFKit stands out with an awesome, well thought-out API. It's clear that they care deeply about API design, and developer experience.

Before we go, let's try out another couple features. First up, Annotations:

// Create a new PSPDFDocument
let document = PSPDFDocument(url: documentURL)

// Create a link annotation, and set its URL
let linkAnnotation = PSPDFLinkAnnotation(url: URL(string: "https://pspdfkit.com")!)

// Position the annotation in the document
let boundingBox = CGRect(x: 200, y: 400, width: 50, height: 300)
linkAnnotation.boundingBox = boundingBox

// Customize the link annotation's appearance
PSPDFLinkAnnotationView.appearance().borderColor = .blue

// Add the newly created annotation to the document
document.add([linkAnnotation])

Very cool, Next up, Forms. Our users can of course fill out any form fields in documents they open, but we can actually fill them out programmtically too:

let document = PSPDFDocument(url: documentURL)

guard let annotations = document.annotationsForPage(at: 0, type: .widget) else { return }

for annotation in annotations {
    switch annotation {
    case let textFieldFormElement as PSPDFTextFieldFormElement:
        guard let fieldName = textFieldFormElement.fieldName else { return }
        textFieldFormElement.contents = String(format: "Test %@", arguments: [fieldName])

    case let buttonFormElement as PSPDFButtonFormElement:
        buttonFormElement.toggleButtonSelectionStateAndSendNotification(true)
    default:
        break
    }
}

Very cool. We iterated through the form fields and filled them with some example content, and toggled the selected state for any buttons in the form.

We have truly only scratched the surface of what's possible with PSPDFKit here.

PSPDFKit offers a ton of other great features like indexed search, digital signatures, and even document editing! They also provide wonderfully fast and responsive support.

Be sure to check out PSPDFKit's extensive Guides area to find complete documentation and tons of extensive guides.

Thanks to PSPDFKit for their support of LBOC!

Page 3 of 38