🦋

SwiftUI: onAppear(perform:) みたいなイベントハンドラを自作する

2022/07/02に公開

UIViewRepresentableNSViewRepresentableを用いてUIKitやAppKitのViewのDelegateをいい感じにハンドリングしようと思うと、.onAppear(perform:).onHover(perform:)のような感じでチェーンメソッドでイベントのコールバックを登録できるようにしたい需要が発生すると思います。

こういうやつ
var body: some View {
    Text("")
        .onAppear {
            // do something
        }
        .onHover { flag in
            // do something
        }
}

簡単な例

まずはViewRepresentableは少々複雑なので簡単なViewから。

HogeView.swift
struct HogeView: View {
    private var piyoHandler: (() -> Void)?

    var body: some View {
        Text("piyo")
            .onHover(perform: { flag in
                if flag {
                    piyoHandler?()
                }
            })
    }

    // イベントを登録するやつ
    func onPiyo(perform action: @escaping () -> Void) -> HogeView {
        var hogeView = self
        hogeView.piyoHandler = action
        return hogeView
    }
}

ポイント

  • コールバック用のhandlerをプロパティに定義する。
  • イベントを登録するfuncを定義する。@escapingを忘れずに。
  • self.handler = action; return selfとするのではなく(できない)、selfのコピーを作ってhandlerにactionを代入してからコピーをreturnしているところがキモ。
利用場面
struct ContentView: View {
    var body: some View {
        HogeView()
            .onPiyo {
                print("Hello World")
            }
    }
}

ViewRepresentable版

NSTextFieldNSViewRepresentableでラップして、controlTextDidEndEditing()onEndEditing()で受け取れるようにする例です。

NSTextFieldView
import SwiftUI
import AppKit

struct NSTextFieldView: NSViewRepresentable {
    typealias NSViewType = NSTextField

    @Binding private var text: String
    private let placeholder: String

    // コールバック用のハンドラを用意
    private var endEditingHandler: (() -> Void)?

    init(_ placeholder: String, text: Binding<String>) {
        self._text = text
        self.placeholder = placeholder
    }

    func makeNSView(context: Context) -> NSTextField {
        let textField = NSTextField(frame: NSRect.zero)
        textField.delegate = context.coordinator
        textField.stringValue = text
        textField.placeholderString = placeholder
        return textField
    }

    func updateNSView(_ nsView: NSTextField, context: Context) {
        nsView.stringValue = text
        // コールバックをここで渡す
        context.coordinator.endEditingHandler = endEditingHandler
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: NSTextFieldView

        // Coordinatorの中でも同じハンドラを定義
        var endEditingHandler: (() -> Void)?

        init(_ parent: NSTextFieldView) {
            self.parent = parent
        }

        func controlTextDidChange(_ obj: Notification) {
            if let textField = obj.object as? NSTextField {
                parent.text = textField.stringValue
            }
        }

        func controlTextDidEndEditing(_ obj: Notification) {
            endEditingHandler?()
        }
    }

    // コールバック登録用の窓口を用意
    func onEndEditing(perform action: @escaping () -> Void) -> NSTextFieldView {
        var textFieldView = self
        textFieldView.endEditingHandler = action
        return textFieldView
    }
}
利用場面
struct ContentView: View {
    @State var text: String = "World"

    var body: some View {
        NSTextFieldView("Hello", text: $text)
            .onEndEditing {
                print("end editing")
            }
    }
}

Discussion