英文:
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'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'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) -> 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
类型是一个管理 PKCanvasView
的 UIViewController
:
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
。然后,在 Controller
的 update(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 PKCanvasView
s 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论