はじめに
戻ってまいりました!SwiftUI。
いままでのUIはStoryBoardやxibで組み上げ、ロジック周りをSwiftで書いて連携させる書き方でした。StoryBoardやxibの中身はバイナリではなくXMLなので、Gitでの管理上は問題は無いです。
ですが、XMLを直接いじるわけではなく、XCodeが自動的に裏でXMLを組んでいるので、何かの弾みに中身が書き換わることがあります。こうなってしまうと、Git上では変更ありの状態になってしまい、面倒なことになります。
これに対応するために、UI周りをコードでゴリッゴリ書くという方法もあります。ところが!今度はAutoLayoutの問題が出てきます。数多くの解像度や縦横対応は面倒なものがあります。ボタンやラベルを単純に並べるだけなら良いのですが、そのようなアプリはサンプル的に作るものならいざしらず、実用的なものならある程度複雑になると思います。そうなると力技でのコーディングでは少々苦しくなってくると思います。
そこで、とっても便利なSwiftUIの出番です。ですが!それでもSwiftUIで対応していないものもあります。ヨイショして落とす。
その場合はUIKitのUIをお借りしないといけません。
少々理解しづらかったので、今回はその実験です。
少し長くなりますが、大半はコードなので多分大丈夫です。
段取り
SwiftUIからUIKitのUIを使うには、このような段取りで進めます。
UIViewRepresentable
に準拠した構造体を作る- UIKitのViewを使うために
makeUIView
メソッドを実装する - SwiftUIからUIKitへの連携のために
updateUIView
メソッドを実装する - Coordinatorクラスを作る
- Coordinatorクラスの生成のための
makeCoordinator
メソッドを実装する
最後のCoordinator関連の2項目は、お借りするUIKitからSwiftUIへイベントを返す必要が無い場合は不要です。
実演
参考書のご紹介
早速ですが、今回の参考書はこちらです。
この本の12章を参考に進めます。
ただし、本のとおりにUIPageView
やUIActivityView
の事例を写経するのも芸がないです。なので、練習として車輪の再発明をします。
- 入力したテキストに小並感を追加して表示する
UILabel
- テキストに小並感風味を添える処理をする
UIButton
を作ります。2つ並行して進めます。
UIViewRepresentableに準拠した構造体を作る
はじめにUIViewRepresentable
に準拠した構造体を作ります。
struct PowderOrangeLabel:UIViewRepresentable {
}
struct PowderOrangeButton:UIViewRepresentable {
}
小並感に関係するUIということで、PowderOrange
を付けました。
違和感があるのは気のせいでしょう。
UIKitのViewを使うためにmakeUIViewメソッドを実装する
struct PowderOrangeLabel:UIViewRepresentable {
func makeUIView(context: Context) -> UILabel {
let control = UILabel()
control.numberOfLines = 2
return control
}
}
同じようにPowderOrangeButton
においてもUIButton
を生成します。
struct PowderOrangeButton:UIViewRepresentable {
func makeUIView(context: Context) -> UIButton {
let control = UIButton(frame: CGRect(x: 0, y: 0, width: 150, height: 44))
control.setTitle("お礼する", for: .normal)
control.setTitleColor(.orange, for: .normal)
return control
}
}
SwiftUIからUIKitへの連携のためにupdateUIViewメソッドを実装する
SwiftUIのプロパティを参照するために@Binding
を付けたプロパティを宣言します。そのプロパティの値が変更されるたびに呼び出されるのがupdateUIView
メソッドです。
ここではプロパティ(String)の値が空でないときに小並感を付加するようにしました。空ならばラベルを空にします。
struct PowderOrangeLabel:UIViewRepresentable {
@Binding var textString:String
func makeUIView(context: Context) -> UILabel {
let control = UILabel()
control.numberOfLines = 2
return control
}
func updateUIView(_ uiView: UILabel, context: Context) {
if textString != "" {
uiView.text = textString + "。\r\nすごいと思いました。(小並感)"
}
else {
uiView.text = textString
}
}
}
PowderOrangeButton
についても同様です。
ただし、ここではプロパティの値に応じてUIButtonのラベル文字列を変更したりなどしないため、updateUIView
メソッドの中身は空っぽです。
struct PowderOrangeButton:UIViewRepresentable {
@Binding var textString:String
@Binding var resultString:String
func makeUIView(context: Context) -> UIButton {
let control = UIButton(frame: CGRect(x: 0, y: 0, width: 150, height: 44))
control.setTitle("お礼する", for: .normal)
control.setTitleColor(.orange, for: .normal)
return control
}
func updateUIView(_ uiView: UIButton, context: Context) {
}
}
Coordinatorを作る
PowderOrangeLabel
はSwiftUIからUIKitへの一方通行なので、ここまでで完了です。ここからはPowderOrangeButton
のみで進んでいきます。
新しくCoordinator
クラスを作ります。
このクラスでUIKitのイベントを処理し、UIKitからSwiftUIへの連携を行います。これで双方向の連携が実現できます。
Coordinator
クラス内でやるべきことは、
- コーディネートする対象(ここでいうところの
PowderOrangeButton
)を保持する変数の宣言と取得 - UIKitのイベントで実行する処理の記述
です。
今回は、PowderOrangeButton
を受け取るための変数の宣言と取得、UIButton
がタップされたときのイベントで実行する処理を記述しました。
struct PowderOrangeButton:UIViewRepresentable {
@Binding var textString:String
@Binding var resultString:String
func makeUIView(context: Context) -> UIButton {
let control = UIButton(frame: CGRect(x: 0, y: 0, width: 150, height: 44))
control.setTitle("お礼する", for: .normal)
control.setTitleColor(.orange, for: .normal)
return control
}
func updateUIView(_ uiView: UIButton, context: Context) {
}
class Coordinator {
var control: PowderOrangeButton
init(_ control: PowderOrangeButton) {
self.control = control
}
@objc func updateInputString(sender:UITextField) {
if control.textString != "" {
control.resultString = "「" + control.textString + "」" + " \r\nすごいですかそうですか良かったです。\r\n(粉みかん)";
}
else {
control.resultString = "……。"
}
}
}
}
ちなみに、わかりやすいようにクラス名としてCoordinator
と付けていますが、別にこれでなくてもいいです。
SwiftUIで使うためにUIKitをラップした構造体の中で宣言していて、名前の衝突が起こらないので、わかりやすくストレートなCoordinator
にしています。
ところでこのCoordinatorの仕組み、デザインパターンの一種かと思ったのですがそうでもないのですね。
Coordinatorの生成のためのmakeCoordinatorメソッドを実装する
仕上げに、Coordinator
クラスのインスタンスを生成します。
UIViewRepresentable
に準拠した構造体なので、Coordinator
クラスのインスタンス生成にはmakeCoordinator
メソッドを使用します。
これは、makeUIView
メソッドの先頭で一度だけ実行され、context.coordinator
に保持されます。
makeCoordinator
メソッドでCoordinator
クラスのインスタンスを生成した後は、context.coordinator
でクラス内のメソッドを参照できます。
今回はボタンがクリックされたときに処理を実行するため、UIButton
のaddTarget
でtouchUpInside
のときに呼び出されるようにしています。
どんな処理が行われるかはご覧の通り、粉みかんを付すだけです。小並感あふれるコメントに対し粉みかんを付したお礼をします。
何言ってるかわからないと思いますが、要はSwiftUI側からUIKitのイベントを拾いリアクションを返してもらう実験をしているだけです。
余談ですが、未だにContext
で戸惑うときがあります。「文脈」というぐらいなので、都度都度で役割が微妙に違います。その違いについていけないとトンチンカンなことをしていることがあります。
今もまさにその状況になってやしないかとドキドキしています。
struct PowderOrangeButton:UIViewRepresentable {
@Binding var textString:String
@Binding var resultString:String
func makeUIView(context: Context) -> UIButton {
let control = UIButton(frame: CGRect(x: 0, y: 0, width: 150, height: 44))
control.setTitle("お礼する", for: .normal)
control.setTitleColor(.orange, for: .normal)
control.addTarget(context.coordinator, action: #selector(Coordinator.updateInputString(sender:)), for: .touchUpInside)
return control
}
func updateUIView(_ uiView: UIButton, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator {
var control: PowderOrangeButton
init(_ control: PowderOrangeButton) {
self.control = control
}
@objc func updateInputString(sender:UITextField) {
if control.textString != "" {
control.resultString = "「" + control.textString + "」" + " \r\nすごいですかそうですか良かったです。\r\n(粉みかん)"
}
else {
control.resultString = "……。"
}
}
}
}
使ってみる
さてと、では実際に使ってみます。
平常通り新しいプロジェクトを作り、ContentView.swift
にコードを書きます。サンプル的なコードなので、お粗末な点が少々ありますが、ご容赦ください。
struct ContentView: View {
@State var inputString:String = ""
@State var targetString:String = ""
@State var returnString:String = ""
var body: some View {
VStack(alignment: .center, spacing: 8) {
HStack {
TextField("何か一言いただけますか?", text: $inputString)
Button(action: {
self.targetString = self.inputString
}) {Text("答える")}
}
PowderOrangeLabel(textString: $targetString)
.frame(height: 80)
PowderOrangeButton(textString: $targetString, resultString: $returnString)
.frame(height: 48)
Text(self.returnString)
.frame(height: 80)
Spacer()
}
.padding(12.0)
}
}
テキストボックスになにか入れて「答える」ボタンをタップします。
小並感あふれるコメントを頂いたので、お礼に小並感をお返しします。「お礼」ボタンをタップします。
「お礼」ボタンはUIButton
で提供されていて、タップイベントとその処理はラップした構造体内で実行しています。
今日はここまで
UILabelもUIButtonも、SwiftUIでそれぞれText、Buttonとして用意されているものです。小並感を添えるためだけの機能限定版ですが、それらをあえて自分で作ってみました。極端にニッチな需要は満たせると思います。つまんないと言わないで……
参考書に載っている事例とは比べ物にならないほどショボショボですが、実際に作ってみると流れをつかめるのではないかと思います。こういったことをベースに段階を踏んで参考書を読むと、わかりやすいかもしれません。