Closed30

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

うーんこれはちょっと嫌な感じがする

https://stackoverflow.com/questions/59503399/how-to-define-a-protocol-as-a-type-for-a-observedobject-property

蔀

Canvasまわりのコメントアウトしてたpreviewをとりあえず整備した
NoteInformationNoteDocument をテストデータにすると、previewができなくなる
これがよくわからない……あまり意味はないけど、とりあえずnilにして、データがない状態でプレビューしとく

蔀

よく見てみると、NoteDocument 引数にしてるコンポーネントは軒並みpreviewダメだったんだな

蔀

https://github.com/0si43/PiecesOfPaper/issues/116

このissueの原因わかった。
initialContentSize() の中で、WidthかHeightだけ入れるとダメなんだ
例えばこんな絵

これはHeightだけウィンドウサイズを超過するので、contentSizeをHeightだけ入れるロジックになる。
これだとWidthが0のままなので、コンテントの大きさを0として扱うっぽい

蔀

パターンを整理しよう。

  • 比較対象はウィンドウサイズとPKDrawingのサイズ
  • 4パターンになるはず
  • (イコールは一旦考えない)
  1. Window Height > PKDrawing Height && Window Width > PKDrawing Width
  2. Window Height > PKDrawing Height && Window Width < PKDrawing Width
  3. Window Height < PKDrawing Height && Window Width > PKDrawing Width
  4. 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にするとかあるけども

蔀

CanvasDelegateBridgeObject という、PencilKitのprotocolに準拠するためだけの存在を解体したい

蔀

こんなんで受けられそう

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 で準拠する方が適切では?

https://qiita.com/ezura/items/6036c6e100599b601482

蔀
func makeUIViewController(context: UIViewControllerRepresentableContext<UIActivityViewControllerWrapper>)
                                -> UIActivityViewController {}

これ、なんでこんな悲惨なことになってるのかと思ったら、Xcodeのスタブ補完で入れたのかな

func makeUIViewController(context: Context) -> UIActivityViewController {}

これでOK

蔀

UIKitベースのライブラリのDelegateメソッド受けるのを、Coordinatorに移譲する作業DONE

蔀

CanvasRouter が変な処理になってるから、これも解体したいけど、SwiftUIの画面遷移のベストプラクティスがよくわかんないな

蔀

becomeFirstResponder って実はよくわかってないな

蔀

PKCanvasViewPKCanvasViewWrapper に持たせる設計に書き直したんだけど、パレットの制御が上手くできなくなった

蔀
    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を閉じたときのツールピッカーのディレイが気になるが、クリティカルじゃないので見送る
テストコードはもうちょっと充実させたいが、アプデ優先でいこう

このスクラップは2022/05/28にクローズされました