想定読者
SwiftUIでアプリケーション開発をしようと思っている、もしくは現状が事実的なベータ状態でUIKitやAppKitを使った方がまだ早いと現実的な判断をしているiOS開発者の方
はじめに
WWDC19でUIKitを利用しないViewフレームワークのSwiftUIが紹介されkeynoteはとても盛り上がりましたが、ベータ版や続くXcodeのリリースを触るにつれて「これはまだ実用に耐えないだろう」と感じ、SwiftUIが成熟するまで技術選定を先延ばしにした技術者の方は多いと感じています。自分も基本的には同じ意見で、大規模なプロダクションコードにおいては導入するつもりはないです。ですが、個人開発においては自分でリスクと時間がコントロールできるため、**問題を承知の上でリスクを取ってSwiftUIを利用したアプリケーション開発を試しています。**動機は単純な興味です。
この記事では、そのような状況の中でSwiftUIにおいて現状の基本コンポーネント不足を解決しようとしている**SwiftUIX(Extension)**というプロジェクトを発見して「これはもしかしたら良いかもしれない」と思ったので共有していきたいと思います。
問題
現状のSwiftUIについて、自分が出会った実装上の不満を実例をいくつか列挙します。
-
TextView
がない。 -
UIControl
相当のものがない。(公式チュートリアルでUIViewRepresentable
を使うサンプルとして取り上げられていた) -
TextField
のフォーカスがコントロールできない。etc...
などですが、これは大別して
1. Viewの基本コンポーネントが足りない。
2. 該当するViewがあるが、UIKitで使えていた機能が使えない。
という2点の不満に分類できると考えています。
実例のところで少し上げましたが、SwiftUIではこの問題に対して開発者に
- UIViewRepresentable(NSViewRepresentable)
- UIViewControllerRepresentable(NSViewControllerRepresentable)
というUIKitのビューをSwiftUIに取り込むためのブリッジ機能という解決法を提供しています。
ですが、この解決法にもいくつか問題はあります。例えば
- 1つ機能が足りないだけなのに、SwiftUIでは実現不可能なためブリッジの仕組みを作らないと対応できないので、手間がすごくかかる
- クロスプラットフォームで便利な仕組みのはずなのに、結局はUIKit/AppKitの両方をラップしたViewを作ることになりそう。
などが考えられます。
このような問題を考えて、ブリッジング機能を利用して自作する機会は最低限かつ、再利用可能な形に落とし込みたいと思ってGitHubを探っていたところ、SwiftUIXというレポジトリを発見しました。
SwiftUIXとは?
SwiftUIX attempts to fill the gaps of the still nascent SwiftUI framework, providing an extensive suite of components, extensions and utilities to complement the standard library.
「SwiftUIXは、まだ初期段階のSwiftUIフレームワークのギャップを埋めることを試み、標準ライブラリを補完するコンポーネント、extensions、ユーティリティの広範囲のスイート(色々入ったパッケージ的なニュアンス)を提供します。」と書いてある通り、SwiftUIの補完を目的としたプロジェクトです。
This project is also by far the most complete port of missing UIKit/AppKit functionality, striving it to deliver in most Apple-like fashion possible.
「このプロジェクトは、**UIKit/AppKitにはあってSwiftUIにない機能への完全なポート(接続端子の意だと取っている)**であり、可能な限りほとんどAppleのような方法で提供することに努めます」とも書いてあり、プロジェクトの指向性としてとても良いように感じます。
提供されるコンポーネント
以下の表にあるように、SwiftUIで提供されないコンポーネントはPagenationView
のようなプレフィックスがつかない名前で提供され、SwiftUIに存在するが機能不足のものはCocoaList
のようにCocoa
のプレフィックスが付く名前で提供されており、利用可能です。
UIKit | SwiftUI | SwiftUIX |
---|---|---|
UIActivityIndicatorView |
- | ActivityInidcator |
UIPageViewController |
- | PaginationView |
UIScrollView |
ScrollView |
CocoaScrollView |
UITableView |
List |
CocoaList |
UITextField |
TextField |
CocoaTextField |
UIModalPresentationStyle |
- | ModalViewPresentationStyle |
何かとよく話題に上がるCollectionViewもちゃんと実装されています。
Swift Package Managerを利用して導入可能です。
CocoaTextField
サンプルとして、こちらのコンポーネントを紹介していきます。ざっくり言うと、UITextFieldで出来る事はだいたいできます。
自分は画面に入った時にbecomeFirstResponder()
相当のことを実現したかったので利用しました。
コード
import SwiftUI
import SwiftUIX // この並びがキレイで良い
struct InputView: View {
@State var text: String = ""
var body: some View {
NavigationView {
VStack {
CocoaTextField("", text: $text)
.isInitialFirstResponder(true) // これ
.frame(height: 50)
.background(Color(.systemGray6))
.cornerRadius(10)
.padding(.horizontal, 15)
.offset(y: 100)
Spacer()
}
.navigationBarTitle("New item", displayMode: .inline)
.navigationBarItems(leading: Button(action: {
// Do something
}, label: {
Text("Cancel")
}), trailing: Button(action: {
// Do something
}, label: {
Text("Done")
.bold()
}))
}
}
}
struct InputView_Previews: PreviewProvider {
static var previews: some View {
InputView()
}
}
画面
このコンポーネントで利用できるAPI
特に解説のないところはAPI名のみ書きます。
func isIntialFirstResponder(Bool) -> Self
func isFirstResponder(Bool)-> Self
上記の2つのAPIの違いはタイミングで、前者の方はmakeUIView
のタイミングでチェックされますが、後者はupdateUIView
タイミングでチェックされます。
func autocapitalization(UITextAutocapitalizationType) -> Self
func font(UIFont) -> Self
func inputAccessoryView<InputAccessoryView: View>(InputAccessoryView) -> Self
func inputAccessoryView<InputAccessoryView: View>(@ViewBuilder () -> InputAccessoryView) -> Self
キーボード周辺にツールバーを設定したりもPure SwiftUIでは不可能だったので、とても助かるメソッドです。
良さそうに感じるのは<InputAccesoryView: View>
としてSwiftUI.Viewをそのまま可能なところで、@ViewBuilder
にも対応してあり、以下のように非常に直感的なViewの設定が可能になりそうです。
CocoaTextField("", text: $text)
.inputAccesoryView({
HStack {
Button(中略)
Spacer()
Button(中略)
}
})
func inputView<InputView: View>(InputView) -> Self
func keyboardType(UIKeyboardType) -> Self
func placeholder(String) -> Self
サンプルまとめ
このように、SwiftUIで利用したかったUIKitの機能がかなりブリッジングされており利用可能になっています。
特徴
このプロジェクトはSwiftUIXの概要で紹介したように広範囲のスイートを提供しているため、どちらかと言うとフレームワーク
のような働きをしており「単純に足りないコンポーネントだけを提供するライブラリが複数あれば良い」という要求をお持ちの場合は適切ではない可能性があります。
まだ深く利用していないので、後で書き直す可能性がありますが、Pros/Consを感じた範囲で整理してみます。
Pros
- 一つのプロジェクトにまとまっているので、シンタックスの一貫性が保たれている。
- 現時点でかなり多くのコンポーネントがサポートされており、あれこれ探す手間が省ける。
- SwiftUIのデフォルトAPIっぽく開発が進められる方針が取られているので、学習コストが低め。
- 標準コントロールを再利用可能な形で公開したければ、このプロジェクトにコントリビュートすれば良いや。と、労力を集約しやすそう。
Cons
- Viewコンポーネントの補強で閉じない、かなりいろいろ便利な仕組みが提供されているので、単純にViewが欲しいだけの場合は複雑に感じるかも
まとめ
この記事では
- SwiftUIに関する開発者の温度感
- 現状での問題点
- SwiftUIX(Extension)とは
- サンプルを上げつつ、実現可能なことについての解説
- 筆者が感じたPros/Cons
を共有しました。SwiftUIは次第に機能が拡充されていき、SwiftUIXを利用していても段階的にリプレースしていくような流れになると思われますが、あくまで補完を目的としたプロジェクトのため、そのコストも比較的抑えめになりそうな気がします。
もし現状で「個人アプリにSwiftUIを使ってみよう」という方がおられれば、かなり手助けになるプロジェクトになると感じます。そのような際にこの記事が手助けになれば幸いです。長い記事にお付き合いいただきありがとうございました!
おまけ
自分が調べてる中で面白かったことを載せていきます。踏み込んだ内容があり、理解不足で正しくない箇所がある可能性を十分に含みますので、その場合ご指摘等いただけると非常に助かります。
結局UIKitバリバリ使うんでしょ?
はい、そうなります。出来る限りPure Swiftを目指したいですが、現状どうにも不可能なことが多いのでこれはどうしようもないです。
微妙に嫌だなぁという気持ちは自分にもあったのですが、こちらのSwiftUI-Introspectというプロジェクトを見るとそんな気持ちはなくなりました。
このプロジェクトは「一部のSwiftUIコンポーネントはView hierarchyからUIKitのViewが取得できるから、クロージャでそいつを取得できるようにすれば各種操作や属性の設定ができるよね」というとても興味深い方針で進められており、同時に「Text・Image・ButtonはPure Swiftで作成されており、UILabel等のUIKitのパーツと対応していないから取得できないよ」ともされています。
つまり、現状は**「Apple提供の公式SwiftUIコンポーネントでさえも内部的にUIKit/AppKitを利用している可能性が高い」**という事です。(内部の仕組みを完璧に理解している訳ではないので、あくまで可能性というスタンスで理解しています。)
この仮説を考慮すると「まぁ内部的にUIKit/AppKitバリバリ使ったとしてもしゃーないね」と納得することができました。
SwiftUI側のViewをUIKit側で取り込むのってどうやってるの?
UIKit側のViewをSwiftUIで取り込むための仕組みとして用意されているのがUIViewRepresentable
という仕組みですが、逆の機能はまだ公式サポートされていません。しかし、記事の途中のサンプルで紹介したfunc inputAccessoryView<InputAccessoryView: View>(@ViewBuilder () -> InputAccessoryView) -> Self
のように、このプロジェクトはそれを実現させています。これはどのように実現しているのでしょうか?
実装は意外とシンプルでUIHostingViewというUIViewのサブクラスを用意し、内部でUIHostingController
を持っており、init
時にaddSubView(rootViewHostingController.view)
を実行しています。
これに関しては、自分はパフォーマンスに関する知識に乏しいので「UIViewController(UIHostingController)を持つのってメモリ的に大丈夫なのかな?」という不安があるので、詳しい方おられたら教えてもらえたら助かります。
then
メソッドって何をやっているの?
SwiftUIXにおいて、プロパティを設定する時に以下のようなシンタックスが使われています。
public func font(_ font: UIFont) -> Self {
then({ $0.font = font })
}
このthenは何をしているのでしょうか?メソッドを追うと以下のようなことをしています。
extension View {
public func then(_ body: (inout Self) -> Void) -> Self {
var result = self
body(&result)
return result
}
...
}
自分の調べた限り、これは以下の事情を回避する目的で利用されています。
- SwiftUI.Viewはstructである
-
var body: some View
はnon mutating get
であり、ブロック内でselfに対するmutatingな操作ができない。(値の変更も、mutating funcを呼ぶこともできない) - structの関数の返却値は
immutable
であり、普通に実装するとmutating func
を呼ぶことができない
以上の性質を持つViewにSwiftUIのメソッドチェーンと同じ形式でプロパティを設定するために、builder pattern with value typesのパターンが採用されています。これはSwift Forumsでのある投稿のあるコメントで呼ばれていたパターン名ですが、ちょうど良かったので拝借しています。
このパターンがやりたいことは、主に3つで
-
self
の値変更は都合が悪い(上記で述べたように)ので、自身のコピーを取って変更させる。 - 値設定はコピーの挙動になってしまって、関数の実行側で新しい値が設定されても
result
変数に対しての変更とならないので、クロージャに対してはinout
をつけて参照を渡している。 - 最後に変更の適応されたコピーを返している。
という風に実装されているはずです。そして、この実装が各所に散らばらないようにthen
というブロックとして実装されています。
Environment Modifier
引き続きCocoaTextFieldのソースを見ていて疑問に思った点が、所々
@Environment(\.font) var font
@Environment(\.isEnabled) var isEnabled
@Environment(\.multilineTextAlignment) var multilineTextAlignment: TextAlignment
のような書き方がされており「これだと.environment
を外側のviewから実行しないと値注入ができないのでは?SwiftUIXのシンタックスを崩してまでこちらの書き方にするメリットはあるのだろうか...」という事でした。
このメリットを探るべくググっていると、Hacking with SwiftでEnvironment Modifierという概念が紹介されており、ざっくり内容を解説すると「いくつかのModifierではenvironmentに値を設定する事でコンテナの内側のViewに変更を与えている。ただし、そうでないものもあるので、これは実験するしかない。」という感じです。
この挙動の正しさを検証するために、実際に以下のように検証してみました。multilineTextAlignmentはSwiftUIモジュールのViewに存在するModifierです。
CocoaTextField("", $text)
.multilineTextAlignment(.center)
すると、alignmentを.centerに設定することに成功しました。
これにより以下の2つの事実が明確になりました。
-
.font
,.isEnabled
,. multilineTextAlignment
などのいくつかのModifierは、実質的に.environment(keyPath, value)
のシンタックスシュガー -
Environment Modifier
で設定されたものは、Viewの@Environment
で取得できる。
これはとても便利で、自作のEnvironmentValue
とEnvironment Modifier
を合わせて利用することで、任意の階層に対して簡潔に任意の値を注入することが出来て実装の幅が広がるように感じます。
冒頭の「SwiftUIXのシンタックスを崩してまでこちらの書き方にするメリットはあるのだろうか...」という疑問に立ち返って考えると、@Environment
を使った書き方をする方が、通常のSwiftUIの記法を維持しつつ、必要のない公開APIを増やさずに済む、とてもシンプルな変更だということが分かりました。