PoPのCanvasまわりのロジックを改善する
とりあえずSwiftUIのプレビューを整備する
デバイスを定義
xcrun simctl list devicetypes
でシミュレーターの文字列一覧が出せる
enum TargetPreviewDevice: String, Identifiable, CaseIterable {
var id: String { rawValue }
case iPhone13Pro = "iPhone 13 Pro"
case iPadPro5th = "iPad Pro (12.9-inch) (5th generation)"
}
// MARK: - PreviewProvider
struct SideBarList_Previews: PreviewProvider {
static var previews: some View {
ForEach(TargetPreviewDevice.allCases) { deviceName in
SideBarList()
.previewDevice(PreviewDevice(rawValue: deviceName.rawValue))
.previewDisplayName(deviceName.rawValue)
}
}
}
こんな感じで
APIとの通信があるところは、テストデータを差しこまないといけないんだけど、ちょっと困ってる
SwiftUIじゃなければ、ViewModelにprotocol設定して終わりなんだけど、
SwiftUIだとジェネリクス使うなり、一手間いるみたい
struct ItemView<Model>: View where Model: ItemViewModel {
@ObservedObject var viewModel: Model
// …
}
うーんこれはちょっと嫌な感じがする
このissueの原因わかった。
initialContentSize()
の中で、WidthかHeightだけ入れるとダメなんだ
例えばこんな絵
これはHeightだけウィンドウサイズを超過するので、contentSizeをHeightだけ入れるロジックになる。
これだとWidthが0のままなので、コンテントの大きさを0として扱うっぽい
パターンを整理しよう。
- 比較対象はウィンドウサイズとPKDrawingのサイズ
- 4パターンになるはず
- (イコールは一旦考えない)
- Window Height > PKDrawing Height && Window Width > PKDrawing Width
- Window Height > PKDrawing Height && Window Width < PKDrawing Width
- Window Height < PKDrawing Height && Window Width > PKDrawing Width
- Window Height < PKDrawing Height && Window Width < PKDrawing Width
1のときは何もしなくていい
2、 3 のときは、ノートデータがはみ出るところはPKDrawingを優先、おさまるところはWindowサイズを入れる
4はPKDrawingを代入
これでたぶんオッケー
private var isDrawingWiderThanWindow: Bool {
canvasView.frame.width < canvasView.drawing.bounds.maxX
}
private var isDrawingHigherThanWindow: Bool {
canvasView.frame.height < canvasView.drawing.bounds.maxY
}
func initialContentSize() {
guard !canvasView.drawing.bounds.isNull else { return }
if isDrawingWiderThanWindow, isDrawingHigherThanWindow {
canvasView.contentSize = .init(width: canvasView.drawing.bounds.maxX,
height: canvasView.drawing.bounds.maxY)
} else if isDrawingWiderThanWindow, !isDrawingHigherThanWindow {
canvasView.contentSize = .init(width: canvasView.drawing.bounds.maxX,
height: canvasView.frame.height)
} else if !isDrawingWiderThanWindow, isDrawingHigherThanWindow {
canvasView.contentSize = .init(width: canvasView.frame.width,
height: canvasView.drawing.bounds.maxY)
}
canvasView.contentOffset = .zero
}
しかし……一度大きいDrawingを編集してから、ウィンドウにおさまるサイズのノート描くと、前のサイズが残っちゃうな。
PKCanvasView
の使いまわしが良くないな。
なんでこんな設計にしたんだっけな……
今回のアップデートはとりあえず開けなくなるのを回避できればいいわ
根本的にはPKCanvasView
の使い回しをやめるとか、DelegateのところをCoordinatorにするとかあるけども
最近PoPで絵描いたときに、ピンチイン/ピンチアウトがどうしても欲しいなと思った
MagnificationGesture
だと、ちょっと違う挙動になる
UIScrollViewのDelegateメソッドで簡単にできることが判明
CanvasDelegateBridgeObject
という、PencilKitのprotocolに準拠するためだけの存在を解体したい
UIViewRepresentable
と Coordinator
を使うんだと思ってるけど、UIKitベースのSDKのprotocolのサンプルがないので、正解がよくわからない
これがサンプルになるかな。
func makeUIViewController(context: Context) -> PHPickerViewController {
// …
let picker = // …
picker.delegate = context.coordinator
return picker
}
こんなんで受けられそう
struct PKCanvasViewWrapper: UIViewRepresentable {
@Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.tool = PKInkingTool(.pen, color: .black, width: 1)
canvasView.delegate = context.coordinator
return canvasView
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, PKCanvasViewDelegate {
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
print("temp")
}
private func updateContentSizeIfNeeded(_ canvasView: PKCanvasView) {
print("temp")
}
}
}
シェアシートはなんでCanvasDelegateBridgeObject
でプロトコル準拠させてるんだっけ?
UIActivityViewControllerWrapper
で準拠する方が適切では?
func makeUIViewController(context: UIViewControllerRepresentableContext<UIActivityViewControllerWrapper>)
-> UIActivityViewController {}
これ、なんでこんな悲惨なことになってるのかと思ったら、Xcodeのスタブ補完で入れたのかな
func makeUIViewController(context: Context) -> UIActivityViewController {}
これでOK
UIKitベースのライブラリのDelegateメソッド受けるのを、Coordinatorに移譲する作業DONE
CanvasRouter
が変な処理になってるから、これも解体したいけど、SwiftUIの画面遷移のベストプラクティスがよくわかんないな
becomeFirstResponder
って実はよくわかってないな
PKCanvasView
をPKCanvasViewWrapper
に持たせる設計に書き直したんだけど、パレットの制御が上手くできなくなった
func updateUIView(_ canvasView: PKCanvasView, context: Context) {
context.coordinator.toolPicker.setVisible(showToolPicker, forFirstResponder: canvasView)
canvasView.becomeFirstResponder()
}
これで挙動は期待したものになった
=== AttributeGraph: cycle detected through attribute 597016 ===
2022-04-16 10:27:21.631559+0900 Pieces of Paper[71685:3161582] [UIFocus] Failed to update focus with context <UIFocusUpdateContext: 0x280512c60: previouslyFocusedItem=(null), nextFocusedItem=(null), focusHeading=None>. No additional info available.
ただコンソールにこのメッセージが出まくるのが気になる
なんか依存関係が複雑になりすぎているために出るワーニングらしい?
private var isDrawingWiderThanWindow: Bool {
canvasView.frame.width < canvasView.drawing.bounds.maxX
}
ここの判定、canvasView.frameが0になっていて、Canvasの広さ判定をミスっていた
あまり使いたくなかったけど、矩形が取れないので、 UIScreen.main.bounds
で判定するか
NoteDocument
(あるいは UIDocument
)でずっとテストデータにできなくて苦しんでいた。
fileURLにテキトーに https://www.google.com/
とかを入れると、落ちてしまっていた。
file:///xxx
を入れると、イニシャルがとりあえず通ることに気づいた
使いやすいようにメソッドを生やした
static func createTestData() -> NoteDocument {
guard let url = URL(string: "file:///test") else {
fatalError()
}
return NoteDocument(fileURL: url, entity: NoteEntity(drawing: PKDrawing()))
}
Canvasを閉じたときのツールピッカーのディレイが気になるが、クリティカルじゃないので見送る
テストコードはもうちょっと充実させたいが、アプデ優先でいこう