🦋
SwiftUI: onAppear(perform:) みたいなイベントハンドラを自作する
UIViewRepresentable
やNSViewRepresentable
を用いて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版
NSTextField
をNSViewRepresentable
でラップして、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