#SwiftUI 水の巻 🐥
けっきょくのところ、 UIView も SwiftUIView も View であり、 UI である。
宮本武蔵が「いかにもして、敵をひとへに、 うをつなぎにおひなす心にしかけて、 敵のかさなるとミヘバ、 其まゝ間をすかさず、強くはらひこむべし(五輪書)」というように、View としてひとえに扱ってしまえば、おそるるにたらない。
さて、両方 View であり、 今のところUIKit で扱う方が楽なフレームワーク(たとえば ARKit の ARView とか、 ARSCNView とか)もあるからには、SwiftUIView と UIView をならべてつかいたいこともあるだろう。
そんなときつかうのが UIViewRepresentable という UIView のコンテナなのだが、そうするととうぜんながら
SwiftUIView と UIView(Representable) の間で値を反映しあって View の変更を共有したい。
値わたしには方法がある。
その方法を記しておこう。
#本日の獲物
こちらが我々の SwiftUIView である。
ボタンで true false をきりかえて 犬か猫の絵文字を表示するだけ。
struct ContentView: View {
@State var isDog: Bool = true
var body: some View {
Button(action: {isDog.toggle()}, label: {
Text(makeButtonEmojiFromIsDog())
})
}
func makeButtonEmojiFromIsDog() -> String { // 絵文字を表示
if isDog {
return "🐶"
} else {
return "🐱"
}
}
}
こちらが UIKitView である。
ボタンで true false をきりかえて 犬か猫の文字を表示するだけ。
import UIKit
class IsDogButtonView: UIView {
var isDog:Bool = true {
didSet {
button.setTitle(makeButtonStringFromIsDog(), for: .normal)
}
}
var button:UIButton!
required override init(frame frameRect: CGRect) {
super.init(frame:frameRect)
button = UIButton(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
addSubview(button)
button.setTitle(makeButtonStringFromIsDog(), for: .normal)
button.titleLabel?.font = .systemFont(ofSize: 100, weight: .black)
button.setTitleColor(.blue, for: .normal)
button.setBackgroundImage(UIImage(systemName: "hand.point.up"), for: .highlighted)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func buttonTapped(){
isDog.toggle()
}
func makeButtonStringFromIsDog() -> String { // こっちは文字を表示
if isDog {
return "Dog"
} else {
return "Cat"
}
}
}
ならべて表示するとこんな感じ。
SwiftUIView のなかに UIViewRepresentable コンテナを入れた。
とうぜんながら、まだおたがいの挙動はシンクロしない。
【おのおののプロパティを更新しているだけの View たち】
struct ContentView: View {
@State var isDog: Bool = true
var body: some View {
VStack {
Button(action: {isDog.toggle()}, label: {
Text(makeStringFromIsDog())
.font(.system(size: 100))
})
IsDogButtonViewContainer()
.frame(width: 200, height: 200, alignment: .center)
}
}
func makeStringFromIsDog() -> String {
if isDog {
return "🐶"
} else {
return "🐱"
}
}
}
struct IsDogButtonViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> IsDogButtonView {
let isDogButtonView = IsDogButtonView(frame: .zero)
return isDogButtonView
}
func updateUIView(_ uiView: IsDogButtonView, context: Context) {
}
}
#プロパティをシンクロさせよう
SwiftUIView も UIView も isDog という変数をもっているので、これを値わたしでシンクロさせて、 おたがいのボタンタップによる View の変更を反映できるようにしていこう。
#SwiftUIView → UIViewRepresentable
UIViewRepresentable に Binding var で isDog の変更を反映させて、 updateUIView で UIView に値をわたす。
State と Binding は反映しあう。だから、 ContentView で State isDog に変更があったときは、 UIViewRepresentable の Biding isDog にも変更が降りてくる。 で、
UIViewRepresentable でアップデートがあった時には updateUIView が呼ばれるので、組み合わせれば UIView に値を渡せる。
@Binding var isDog: Bool // +
func updateUIView(_ uiView: IsDogButtonView, context: Context) {
uiView.isDog = isDog // +
}
SwiftUIView で UIViewRepresentable を初期化する時に、 isDog をドルマークつけて渡して、バインディング(結びつける)させる。
IsDogButtonViewContainer(isDog: $isDog) // +
これで SwiftUI の値を UIView に渡して変更を反映できる。
【SwiftUI (エモジ)の変更は、 UIView(文字)に反映されるようになった】
逆方向の変更はまだ反映されない。
#UIViewRepresentable → SwiftUIView
デリゲートメソッドを作って、 Coordinator にデリゲートを継承させて、UIViewRepresentable に値を反映させて、 SwiftUIView にバインディングで渡す。
1、デリゲートメソッドに UIView の変更を渡す
protocol IsDogButtonViewDelegate:NSObjectProtocol { // +
func isDogChanged(isDog:Bool)
}
class IsDogButtonView: UIView {
weak var delegate: IsDogButtonViewDelegate? // +
@objc func buttonTapped(){
isDog.toggle()
delegate?.isDogChanged(isDog:isDog) // +
}
2、コーディネーターにデリゲートを継承させる
func makeUIView(context: Context) -> IsDogButtonView {
let isDogButtonView = IsDogButtonView(frame: .zero)
isDogButtonView.delegate = context.coordinator
return isDogButtonView
}
func makeCoordinator() -> Coordinator { // +
return Coordinator(isDog: $isDog)
}
class Coordinator:NSObject, IsDogButtonViewDelegate { // + コーディネーターにデリゲートを継承させる。
@Binding var isDog:Bool
init(isDog: Binding<Bool>){
_isDog = isDog
}
func isDogChanged(isDog: Bool) {
self.isDog = isDog
}
}
これで、 UIView での変更も SwiftUI に反映される。
【UIView から SwiftUIView にも値が反映されている様】
全体コード
struct ContentView: View {
@State var isDog: Bool = true
var body: some View {
VStack {
Button(action: {isDog.toggle()}, label: {
Text(makeStringFromIsDog())
.font(.system(size: 100))
})
IsDogButtonViewContainer(isDog: $isDog)
.frame(width: 200, height: 200, alignment: .center)
}
}
func makeStringFromIsDog() -> String {
if isDog {
return "🐶"
} else {
return "🐱"
}
}
}
struct IsDogButtonViewContainer: UIViewRepresentable {
@Binding var isDog: Bool
func makeUIView(context: Context) -> IsDogButtonView {
let isDogButtonView = IsDogButtonView(frame: .zero)
isDogButtonView.delegate = context.coordinator
return isDogButtonView
}
func updateUIView(_ uiView: IsDogButtonView, context: Context) {
uiView.isDog = isDog
}
func makeCoordinator() -> Coordinator {
return Coordinator(isDog: $isDog)
}
class Coordinator:NSObject, IsDogButtonViewDelegate {
@Binding var isDog:Bool
init(isDog: Binding<Bool>){
_isDog = isDog
}
func isDogChanged(isDog: Bool) {
self.isDog = isDog
}
}
}
🐣
フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.com
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。