PKCanvasView is a subclass of UIView. A UIView can only have a single superview. So you cannot use the same instance of PKCanvasView in two places (built-in screen and external screen) at the same time. You need one PKCanvasView for each place where you want to show the drawing, and you need to keep the drawing property of all the PKCanvasViews in sync.
PKCanvasView only updates its drawing when a stroke ends, so you cannot quite get all the views to synchronize in "real-time". This is what you get:

First, I wrote a wrapper for PKCanvasView:
struct PKCanvasViewWrapper: View {
@Binding var drawing: PKDrawing
var body: some View {
Rep(spec: self)
}
}
I like to keep my UIViewControllerRepresentable types private, so that wrapper is just a View, and it uses a private type named Rep:
extension PKCanvasViewWrapper {
typealias Spec = Self
fileprivate struct Rep: UIViewControllerRepresentable {
var spec: Spec
func makeUIViewController(context: Context) -> Controller {
return Controller(spec: spec)
}
func updateUIViewController(_ controller: Controller, context: Context) {
controller.update(to: spec)
}
}
}
The Controller type is a UIViewController that manages the PKCanvasView:
extension PKCanvasViewWrapper {
fileprivate class Controller: UIViewController {
private var spec: Spec
private let canvas: PKCanvasView
init(spec: Spec) {
self.spec = spec
canvas = PKCanvasView()
canvas.drawingPolicy = .anyInput
canvas.tool = PKInkingTool(.pen)
super.init(nibName: nil, bundle: nil)
self.view = canvas
canvas.delegate = self
}
required init?(coder: NSCoder) { fatalError() }
func update(to spec: Spec) {
self.spec = spec
if canvas.drawing != spec.drawing {
canvas.drawing = spec.drawing
}
}
}
}
The Controller type conforms to PKCanvasViewDelegate so that the canvas can notify it when the drawing changes:
extension PKCanvasViewWrapper.Controller: PKCanvasViewDelegate {
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if canvasView.drawing != spec.drawing {
spec.drawing = canvasView.drawing
}
}
}
In a real app, you might want to add more properties to the PKCanvasViewWrapper type to specify how to configure the PKCanvasView. Then, in the Controller's update(to:) method, you'd need to use those properties to configure the PKCanvasView.
Anyway, I then wrote an ObservableObject type to hold the shared PKDrawing:
class AppModel: ObservableObject {
@Published var drawing: PKDrawing
init() {
drawing = PKDrawing()
}
}
I added an appModel property to my AppDelegate to make it available to all scenes:
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
let model = AppModel()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
}
Then I wrote a SwiftUI view to appear on the internal screen:
struct InternalRootView: View {
@ObservedObject var model: AppModel
var body: some View {
VStack {
Text("Internal Screen")
PKCanvasViewWrapper(drawing: $model.drawing)
.frame(width: 300, height: 300)
.border(Color.gray)
}
}
}
and I wrote a scene delegate to create that view and show it in a window:
class InternalSceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard
let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let scene = scene as? UIWindowScene
else { return }
let window = UIWindow(windowScene: scene)
window.rootViewController = UIHostingController(rootView: InternalRootView(model: appDelegate.model))
window.isHidden = false
self.window = window
}
}
I copied the view and scene delegate for the external screen, just changing "Internal" to "External" everywhere.
Finally, I updated the "Application Scene Manifest" in the Info tab of my app target:
Application Scene Manifest
Enable Multiple Windows NO
Scene Configuration (2 items)
Application Session Role (1 item)
Item 0 (Default Configuration)
Delegate Class Name: $(PRODUCT_MODULE_NAME).InternalSceneDelegate
Configuration Name: Default Configuration
External Display Session Role Non-Interactive (1 item)
Item 0 (Default Configuration)
Delegate Class Name: $(PRODUCT_MODULE_NAME).ExternalSceneDelegate
Configuration Name: Default Configuration
You can find the full project in this github repo.