Home iOS & Swift Books SwiftUI Apprentice

17
Interfacing With UIKit Written by Caroline Begbie

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Sometimes you’ll need a user interface feature that is not available in SwiftUI. UIKit is a framework to develop user interfaces that’s been around since the first iPhone in 2008. Being such a mature framework, it’s grown in complexity and scope over the years, and it’ll take SwiftUI some time to catch up. With UIKit, among other things, you can handle Pencil interactions, support pointer behaviors, do more complex touch and gesture recognizing and any amount of image and color manipulation.

With UIKit as a backup to SwiftUI, you can achieve any interface design that you can imagine. You’ve already used UIImage to load images into a SwiftUI Image view, and it’s almost as easy to use any UIKit view in place of a SwiftUI view using the UIViewRepresentable protocol.

This chapter will cover loading images and photos from outside of your app. First, you’ll load photos from your Photos library, and then you’ll drag or copy images from other apps, such as Safari.

UIKit

UIKit has a completely different data flow paradigm from SwiftUI. With SwiftUI, you define Views and a source of truth. When that source of truth changes, the views automatically update. UIView does not have a concept of bound data, so you must explicitly update the view when data changes.

UIKit vs SwiftUI
UIKit vs SwiftUI

Whenever you can for new apps, you should stick with SwiftUI. However, UIKit does have useful frameworks, and it’s often impossible to accomplish some things without using one of them. PhotoKit provides access to photos and videos in the Photos app, and the PhotosUI framework provides a user interface for asset selection.

Using the Representable protocols

Representable protocols are how you insert UIKit views into your SwiftUI apps. Instead of creating a structure that conforms to View for a SwiftUI view, you create a structure that conforms to either UIViewRepresentable — for a single view — or UIViewControllerRepresentable, if you want to use a view controller for complex management of views. To receive information from the UIKit view, you create a Coordinator.

UIViewRepresentable and Coordinator
UOMeofVewzajonhogxe oyz Qiolyuxiwag

import SwiftUI

struct PhotoPicker: UIViewRepresentable {
}
func makeUIView(context: Context) -> UILabel {
  let label = UILabel()
  label.text = "Hello UIKit!"
  return label
}

func updateUIView(_ uiView: UILabel, context: Context) {
}
struct PhotoPicker_Previews: PreviewProvider {
  static var previews: some View {
    Text("Hello SwiftUI!")
      .background(Color.yellow)
  }
}
PhotoPicker()
Compare previews
Vecgoso brezeocx

UIKit delegate pattern

Many of the UIKit classes use protocols that ask for a delegate object to deal with events. For example, when creating a UITableView, you specify a class that implements UITableViewDelegate and manages what the app should do when the user selects a row in the table.

Delegate pattern
Fopilana hawgoht

Representable PhotoPicker with delegation
Xakfodotvacdu FfuvuTijwal hipr gigakoxiob

Picking photos

As the system photo picker is a subclass of UIViewController, your Representable structure will be a UIViewControllerRepresentable.

import PhotosUI
struct PhotoPicker: UIViewControllerRepresentable {
  func makeUIViewController(context: Context) 
    -> some UIViewController {
  }
  
  func updateUIViewController(
    _ uiViewController: UIViewControllerType,
    context: Context
  ) {
  }
}
// 1
var configuration = PHPickerConfiguration()
configuration.filter = .images
// 2
configuration.selectionLimit = 0
// 3
let picker = 
  PHPickerViewController(configuration: configuration)
return picker
class PhotosCoordinator: NSObject,
  PHPickerViewControllerDelegate {
}
func picker(
  _ picker: PHPickerViewController,
  didFinishPicking results: [PHPickerResult]
) {
}
@Binding var images: [UIImage]
struct PhotoPicker_Previews: PreviewProvider {
  static var previews: some View {
    PhotoPicker(images: .constant([UIImage]()))
  }
}
var parent: PhotoPicker

init(parent: PhotoPicker) {
  self.parent = parent
}
func makeCoordinator() -> PhotosCoordinator {
  PhotosCoordinator(parent: self)
}

Setting the delegate

➤ Add this to makeUIViewController(context:) at the end of the method, before returning picker:

picker.delegate = context.coordinator

NSItemProvider

You’ve now set up the interface between the SwiftUI PhotoPicker and the UIKit PHPickerViewController. All that’s left is to load the images array from the modal results.

let itemProviders = results.map(\.itemProvider)
for item in itemProviders {
  // load the image from the item here 
}
// 1
if item.canLoadObject(ofClass: UIImage.self) {
  // 2
  item.loadObject(ofClass: UIImage.self) { image, error in
    // 3
    if let error = error {
      print("Error!", error.localizedDescription)
    } else {
      // 4
      DispatchQueue.main.async {
        if let image = image as? UIImage {
          self.parent.images.append(image)
        }
      }
    }
  }
}
@Environment(\.presentationMode) var presentationMode
parent.presentationMode.wrappedValue.dismiss()
System photo picker
Jrxrim llixo hikyel

Adding PhotoPicker to your app

To use PhotoPicker, you’ll need hook it up to your Photos toolbar button and save the loaded photos as ImageElements.

@State private var images: [UIImage] = []
case .photoPicker:
  PhotoPicker(images: $images)
  .onDisappear {
    for image in images {
      card.addElement(uiImage: image)
    }
    images = []
  }
Added photos
Iqqoq qtafit

Adding photos to the simulator

If you want more photos than the ones Apple supplies, you can simply drag and drop your photos from Finder on to the simulator. The simulator will place these into the Photos library and you can then access them from PhotoPicker.

Drag and drop from other apps

As well as adding photos from the ones on your device, you’ll add drag and drop of images from any app. Similar to the photos system modal, you do this using an item provider.

Drop Safari
Fvev Kiyetu

Drag a giraffe
Pgov u zubagna

Uniform Type Identifiers

Your app needs to distinguish between dropping an image and dropping another format, such as text. Most apps have associated data formats. For example, when you right-click a macOS file and choose Open With, the menu presents you with all the apps associated with that file’s data format. When you right-click a .png file, you might see a list like this:

.png app list
.ybh abn tubl

Adding the drop view modifier

In Xcode, open CardDetailView.swift. Add a new modifier to content above the toolbar modifier:

// 1
.onDrop(of: [.image], isTargeted: nil) { 
  // 2
  itemProviders, _ in
  // 3  
  return true
}
Drop is active
Vnod om oczice

for item in itemProviders {
  if item.canLoadObject(ofClass: UIImage.self) {
    item.loadObject(ofClass: UIImage.self) { image, _ in
      if let image = image as? UIImage {
        DispatchQueue.main.async {
          card.addElement(uiImage: image)
        }
      }
    }
  }
}
A tower of giraffes
O dobof uj rajiclay

Refactoring the code

CardDetailView is getting quite large and complex now, and you should start to think about how you can refactor it and split out as much code as you can. A cleaner way of writing the drop code would be to use an alternative modifier that calls a new structure as a delegate.

import SwiftUI

struct CardDrop: DropDelegate {
  @Binding var card: Card
}
func performDrop(info: DropInfo) -> Bool {
  let itemProviders = info.itemProviders(for: [.image])

  for item in itemProviders {
    if item.canLoadObject(ofClass: UIImage.self) {
      item.loadObject(ofClass: UIImage.self) { image, _ in
        if let image = image as? UIImage {
          DispatchQueue.main.async {
            card.addElement(uiImage: image)
          }
        }
      }
    }
  }
  return true
}
.onDrop(of: [.image], delegate: CardDrop(card: $card))
Final drag and drop
Fejay tyec olh xpon

Challenge

Challenge: Leverage PencilKit

Now that you know how to host UIKit views in SwiftUI, you have access to a wide range of Apple frameworks. One fun framework is PencilKit where you can draw into a canvas.

A scribble using PencilKit
I fgzepzca ubadv PevburYib

Key points

  • SwiftUI and UIKit can go hand in hand. Use SwiftUI wherever you can and, when you want a tasty UIKit framework, use it with the Representable protocols. If you have a UIKit app, you can also host SwiftUI views with UIHostingController.
  • The delegate pattern is common throughout UIKit. Classes hold a delegate property of a protocol type to which you assign a new object conforming to that protocol. The UIKit object performs methods on its delegate.
  • PHPickerViewController is an easy way to select photos and videos from the photo library. Access to photos generally requires permission, and you’d have to set up usage in your Info.plist. However, PHPickerViewController ensures privacy by running in a separate process, and your app only has access to media that the user selects.
  • Item providers enable passing data more easily between apps.
  • Using Uniform Type Identifiers and the onDrop modifier, you can support drag and drop in your app.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.