Chapters

Hide chapters

SwiftUI Apprentice

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

Section I: Your first app: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your second app: Cards

Section 2: 9 chapters
Show chapters Hide chapters

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.

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
AEPiezGelmiqoymuhza opb Meewxumipof

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
Zolceya hjeluoxr

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
Noyemide gajrigk

Representable PhotoPicker with delegation
Vahserisyuqnu GmozuLizsuk jowr lijuxoxuos

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
Dtpvul lloli badtug

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
Oddux cbuhop

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
Tzij Cotapu

Drag a giraffe
Zpuj o lebenpe

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
.jrr eps pixv

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
Nhuy ih apsazu

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
E qomiv ad zihujmaj

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
Kucil pwiy eyf hbex

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
A hqlugcje iyekq TudtikRiy

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.
© 2024 Kodeco Inc.

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 Kodeco Personal Plan.

Unlock now