iPad上的PKCanvasView和外部屏幕之间的实时绘图同步

huangapple go评论72阅读模式
英文:

Real-time drawing synchronization between PKCanvasView on iPad and external screen

问题

**情境:**
我正在使用`SwiftUI`开发一个iPad应用程序,并连接了一个外部屏幕到iPad上。在我的应用中,我想使用`PKCanvasView`提供绘图功能。目标是允许用户在iPad屏幕上绘制,并实时在外部屏幕上显示绘图。我应该如何在iPad上的`PKCanvasView`和外部屏幕之间实现这种同步?

我已尝试使用`ObservableObject`,然后访问canvas属性将其传递给ExternalCanvas视图。

当我尝试这样做时,我可以在iPad上绘制,但当我连接外部监视器时,它会将`PKCanvasView`从iPad屏幕上移除并放在外部监视器上。

**编辑:** 我已经与ChatGPT交流了将近3小时,试图获得解决方案,但它似乎无法理解我试图实现的概念,而且越是尝试,离期望的结果越远。
```swift
class SharedData: ObservableObject {
    @Published var canvas: PKCanvasView = PKCanvasView()
}
import SwiftUI
import PencilKit

struct ExternalView: View {
    @EnvironmentObject var shared: SharedData
    var body: some View {
        ExternalCanvas(canvasView: shared.canvas)
    }
}
struct ExternalCanvas: UIViewRepresentable {
    @State var canvasView: PKCanvasView

    func makeUIView(context: Context) -> PKCanvasView {
        canvasView.drawingPolicy = .anyInput
        canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
        return canvasView
    }

    func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
}

<details>
<summary>英文:</summary>

**Situation:**
I&#39;m developing an iPad app using `SwiftUI`, and I have an external screen connected to the iPad. In my app, I want to provide a drawing feature using `PKCanvasView`. The goal is to allow users to draw on the iPad screen and have the drawing display in real-time on the external screen. How can I achieve this synchronization between the `PKCanvasView` on the iPad and the external screen?

I have tried using an `ObservableObject` and then accessing the canvas property to pass it to the ExternalCanvas View

When I try this, I can draw on the iPad but when I connect an external monitor it removes the `PKCanvasView` off the iPad screen and puts it on the external monitor.

**EDIT:** I have also been back and forward with ChatGPT for almost 3 hours trying to get a resolution, but it was unable to suggest anything, mainly because it didn&#39;t seem to grasp the concept of what I was trying to achieve and they more it tried the further it got from the desired outcome

class SharedData : ObservableObject {
@Published var canvas : PKCanvasView = PKCanvasView()
}

import SwiftUI
import PencilKit
struct ExternalView: View {
@EnvironmentObject var shared : SharedData
var body: some View {
ExternalCanvas(canvasView: shared.canvas)
}
}


struct ExternalCanvas: UIViewRepresentable {
@State var canvasView: PKCanvasView

func makeUIView(context: Context) -&gt; PKCanvasView {
    canvasView.drawingPolicy = .anyInput
    canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
    return canvasView
}

func updateUIView(_ canvasView: PKCanvasView, context: Context) { }

}


</details>


# 答案1
**得分**: 1

`PKCanvasView` 是 `UIView` 的子类。`UIView` 只能有一个父视图。因此,你不能同时在两个地方(内置屏幕和外部屏幕)使用同一个 `PKCanvasView` 实例。你需要为想显示绘图的每个地方都使用一个 `PKCanvasView`,并确保所有 `PKCanvasView` 的 `drawing` 属性保持同步。

`PKCanvasView` 仅在笔画结束时更新其 `drawing`,因此你不能完全实现所有视图的实时同步。这就是你得到的效果。

首先,我编写了一个 `PKCanvasView` 的封装:

```swift
struct PKCanvasViewWrapper: View {
    @Binding var drawing: PKDrawing

    var body: some View {
        Rep(spec: self)
    }
}

我喜欢将我的 UIViewControllerRepresentable 类型保持私有,因此该封装只是一个 View,它使用名为 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)
        }
    }
}

Controller 类型是一个管理 PKCanvasViewUIViewController

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
            }
        }
    }
}

Controller 类型符合 PKCanvasViewDelegate,以便画布在绘图更改时通知它:

extension PKCanvasViewWrapper.Controller: PKCanvasViewDelegate {
    func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
        if canvasView.drawing != spec.drawing {
            spec.drawing = canvasView.drawing
        }
    }
}

在一个真实的应用中,你可能想要为 PKCanvasViewWrapper 类型添加更多属性以指定如何配置 PKCanvasView。然后,在 Controllerupdate(to:) 方法中,你需要使用这些属性来配置 PKCanvasView

然后,我编写了一个 ObservableObject 类型来保存共享的 PKDrawing

class AppModel: ObservableObject {
    @Published var drawing: PKDrawing

    init() {
        drawing = PKDrawing()
    }
}

我在我的 AppDelegate 中添加了一个 appModel 属性,以便在所有场景中都可以使用:

@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)
    }
}

然后,我编写了一个 SwiftUI 视图,显示在内部屏幕上:

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)
        }
    }
}

然后我编写了一个场景代理来创建该视图并在窗口中显示它:

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
    }
}

我复制了用于外部屏幕的视图和场景代理,只是在所有地方将 "Internal" 更改为 "External"。

最后,我在应用程序目标的 Info 标签中更新了 "Application Scene Manifest":

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

你可以在 此 GitHub 仓库 中找到完整的项目。

英文:

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:

iPad上的PKCanvasView和外部屏幕之间的实时绘图同步

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) -&gt; 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]?) -&gt; Bool {
        return true
    }

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -&gt; UISceneConfiguration {
        return UISceneConfiguration(name: &quot;Default Configuration&quot;, 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(&quot;Internal Screen&quot;)
            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.

huangapple
  • 本文由 发表于 2023年6月12日 16:25:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/76454788.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定