Supporting SwiftUI with Core Graphics

Nov 22 2022 · Swift 5.5, iOS 15, Xcode 13

Part 1: Supporting SwiftUI with Core Graphics

03. Host a UIView in a SwiftUI View

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 02. Create a CGImage Next episode: 04. Build a UIKit Drawing Pad

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 03. Host a UIView in a SwiftUI View

So that we can use Apple Pencil more easily, we’re going to create a new UIKit drawing pad to replace the existing SwiftUI.

[Slide 01]

To use a UIKit UIView in our app, we’ll create a SwiftUI view that shows a structure that conforms to UIViewRepresentable. UIViewRepresentable has three required methods.

makeUIView is where we’ll initialize and return the UIView that we’re drawing on. updateUIView is where we’ll update any properties in the UIView from SwiftUI, such as the picked drawing color

makeCoordinator is where we’ll create a Coordinator class. This will hold a Binding property to update SwiftUI with an image taken from the Drawing UIView.

[Slide 02]

App Architecture

In our app, the existing DrawingPadView will host DrawingPadRepresentation and our ColorPicker controls. CanvasView will be a UIView where the user draws. DrawingPadRepresentation will be the intermediary between DrawingPadView and CanvasView

DrawingPadView.swift

The drawing pad view is currently set up to show the SwiftUI drawing pad. It has nearly everything we need to swap in the new UIKit drawing pad. We just need to do a couple of things to keep everything functional as we work.

For example, we want this pickedColor property so that users can still change the color they’re drawing with. We also want this drawing property so we can store the drawing when the user is done, and load the drawing back later. But the drawing will be a UIImage, so, update that.

@State var drawing: UIImage?

And then we just need to comment out a couple of things for now, that we’ll come back and clean up later.

Like everything in this “save” button, except the dismiss. We’ll need to do some work with the cell model before we can save. And the DrawingPad here in body. We’ll just leave it here as a placeholder.

ContentView.swift

You’ll have an error now over in ContentView.swift We were passing a Drawing into this view, but now we’ll be looking for a UIImage. So, just remove this argument for now.

DrawingPadView(❌drawing: cellStore.selectedCell?.drawing❌)

We’ll add it back in later to hook everything up again.

CanvasView.swift

Now we’re ready to get started on our UIKit drawing pad. There will be several new files involved, so, start by setting up a group.

Then create a new Swift file called CanvasView. This will be the UIKit UIView, so import UIKit at the top

import UIKit

and create a CanvasView class inheriting from UIControl.

class CanvasView: UIControl {
}

UIControl inherits from UIView, but adds a target action so that we’ll be able to let DrawingPad know when the drawing pad changes so that it can update the image in the SwiftUI drawing pad view. CanvasView will need properties for the drawing image and the current drawing color:

  var drawingImage: UIImage?
  var color: UIColor = .black

  init(color: UIColor, drawingImage: UIImage?) {
    self.drawingImage = drawingImage
    self.color = color
    super.init(frame: .zero)
  }

Fix the compile error by adding the required initializer for UIView. The “fix” button will do it for you!

required init?(coder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

DrawingPadRepresentation.swift

Now to get this interfacing with SwiftUI, we need our intermediary. Create a new Swift file called DrawingPadRepresentation.swift.

import SwiftUI

and create a structure that conforms to UIViewRepresentable.

struct DrawingPadRepresentation: UIViewRepresentable {
}

Add the drawingImage and color properties that DrawingPadView will send.

  @Binding var drawingImage: UIImage?
  let color: UIColor

We won’t be changing the value of the color here, so we don’t need to set it as a Binding. UIViewRepresentable has some required methods - there’s one to set up the associated UIView and one to update it. Add the method that will set up CanvasView which is our associated view.

func makeUIView(context: Context) -> CanvasView {
  let view = CanvasView(color: color,
                        drawingImage: drawingImage)
  return view
}

Add the method that will update CanvasView.

func updateUIView(_ uiView: CanvasView, context: Context) {
  uiView.color = color
}

CanvasView and our color controls can’t talk to each other directly, so we update CanvasView when the picked color changes.

To send data from the CanvasView back to DrawingPadView, we have to make a coordinator. It’ll be a nested class, so I’ll put it in an extension at the bottom of the file.

class Coordinator: NSObject {
  @Binding var drawingImage: UIImage?
  
  init(drawingImage: Binding<UIImage?>) {
    _drawingImage = drawingImage
  }
}

This coordinator contains the drawing image property. Create a method in the coordinator that we should call when the user finishes his stroke to update this drawing image.

@objc func drawingImageChanged(_ sender: CanvasView) {
  self.drawingImage = sender.drawingImage
}

Shortly we’ll set up a target action on CanvasView to call this method when the user finishes their stroke. In DrawingPadRepresentation, make the coordinator in the protocol method makeCoordinator().

func makeCoordinator() -> Coordinator {
  Coordinator(drawingImage: $drawingImage)
}

This is a protocol method, so will automatically get called when DrawingPadRepresentation initializes. When the user finishes their stroke, we’ll update the drawing image in DrawingPadView. CanvasView is a UIControl, which means that in makeUIView, before returning the view, we can assign a target action to it.

view.addTarget(
  context.coordinator,
  action: #selector(Coordinator.drawingImageChanged(_:)),
  for: .valueChanged)

This sets a target action for valueChanged and calls the coordinator method to update the drawing image.

CanvasView.swift

Open CanvasView and add a method that the view will call when the user finishes his stroke.

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  sendActions(for: .valueChanged)
}

Now when the user finishes his stroke, CanvasView, being a UIControl, will invoke the value changed action.

This will call the target action in DrawingPadRepresentation for valueChanged, and that action will update the coordinator drawing image which has a binding to the drawing image in DrawingPadRepresentation.

And that’s the same as the State property in DrawingPadView.swift It’s a bit complicated, so you might want to go through this part several times.

DrawingPadView.swift

In DrawingPadView, we’ll add a new DrawingPadRepresentation right where the old swiftUI drawing pad used to be.

DrawingPadRepresentation(drawingImage: $drawing, color: pickedColor.uiColor)

Now we’re ready to draw into the UIView.