はじめに
第1章 SwiftUI概要
第2章 SwiftUIのレイアウトシステム
第3章 SwiftUIの座標空間
第4章 基本Viewの使い方
第5章 iOS 14新機能: App、Scene、WindowGroup
第6章 SwiftUIのデータ管理
第7章 Combine
第8章 レシピ:GitHubAPIリポジトリー検索アプリ
第9章 レシピ:お絵かきアプリ
第10章 レシピ:写真フィルターアプリ
第11章 iOS 14新機能: Widget
あとがき
皆さんはSwiftUIでアプリ開発していますか?チュートリアルを試したけど自分のアプリはどう作ればいいかわからない!見よう見まねで作ったけど行き詰まった、周りに相談する人がいない!などなどお困りではないですか?本書はそんな悩みを解決します。本書は本格的なアプリのレシピを提供し、アプリの作り方の流れが学べます。さらにレイアウトシステムやViewの使い方など、SwiftUIで開発する上で誰もが理解すべき原理原則を解説します。基礎から応用まで幅広く解説された本書を読んでSwiftUIでアプリを作りましょう!
本書ではSwiftUIでアプリを作成できるようになるため、まずアプリ作成に必要な知識や概念を解説します。SwiftUIのレイアウトシステムや、Viewの使い方、Combineなどです。その後3つのアプリを作ります。どれも実践的なアプリです。アプリ作成は「アプリ概要」「レイアウト」「ロジック」を重点に解説します。自分でアプリを作る際には、この三段階を思い出していただければアプリを作れるはずです。
・第1章「SwiftUI概要」
─SwiftUIの概要を解説します。SwiftUIとはどんなコンセプトで作られ、どんな特徴や利点があるのかをお伝えします。
・第2章「SwiftUIのレイアウトシステム」
─Viewのレイアウトがどのように決まるのかを解説します。これを理解すればアプリの画面作成の助けになるでしょう。
・第3章「SwiftUIの座標空間」
─SwiftUIで各Viewの座標やサイズを把握する方法を解説します。
・第4章「基本Viewの使い方」
─TextやImageなどSwiftUIで提供されている基本的なViewの使い方を解説します。iOS 14から導入されたViewも紹介します。
・第5章「iOS 14新機能: App、Scene、WindowGroup」
─iOS 14からSwiftUIのみでアプリを作成できるようになりました。どのようにアプリを構築するのかをみていきます。
・第6章「SwiftUIのデータ管理」
─SwiftUIはステートドリブンでViewが更新されます。Viewとデータの紐付け方法を学び、適切にView更新ができるようになりましょう。
・第7章「Combine」
─SwiftUIとともにiOS 13から導入された非同期イベントフレームワークCombine。SwiftUIとともに使われる機会が増えていくので、じっくり理解しましょう。
・第8章「レシピ:GitHubAPIリポジトリー検索アプリ」
─GitHubAPIを使ってリポジトリー情報をアプリに表示しましょう。ネットワークとSwiftUIの連携方法が学べます。
・第9章「レシピ:お絵かきアプリ」
─ユーザーが指でなぞると絵が描けるお絵かきアプリをつくましょう。消しゴム機能やキャンバス機能もあります。
・第10章「レシピ:写真フィルターアプリ」
─写真にフィルター加工を施すアプリを作りましょう。Image Pickerとの連携や画像をフィルター加工する方法、親Viewと子Viewのデータの受け渡し方法が学べます。
・第11章「iOS 14新機能: Widget」
─iOS 14から登場した目玉機能、Widgetを学びます。Viewの実装にはSwiftUIが必要なので、WidgetからSwiftUIを導入するのもよいでしょう。
本書は次の読者を対象としています。
・個人/会社問わず、一人でSwiftUIに挑戦している方
・SwiftUIでアプリ開発をしたい方
・SwiftUIのチュートリアルを終わらせてもっと深く学びたい方
本書は次の環境で検証しています。
・iOS 14.0
・Xcode 12.0.1
・Swift 5.3
・macOS 10.15.5
SwiftUIはiOS/macOS/tvOS/watchOSの開発ができるクロスプラットフォームのフレームワークですが、本書はiOSアプリ開発を中心に解説します。ご了承ください。またSwift言語自体の解説は行いません。
次のリポジトリーに本書のサンプルコードが掲載されています。
・https://github.com/SatoTakeshiX/SwiftUICatalog
各章のサンプルプロジェクトがディレクトリでまとめられています。どのプロジェクトをサンプルとして利用するのかは各章でお伝えします。
SwiftUIはWWDC 19で発表された新しいフレームワークです。Swiftでユーザーインターフェースを宣言的に記述できます。macOS/iOS/tvOS/watchOSなどのAppleプラットフォーム向けに、同じコードでビルドできます。ステートドリブンなシステムで、データが更新されると自動的にViewも更新されるため、バグを減少させることができます。この章ではSwiftUIの概要を解説します。
「アプリの基本機能を高品質のまま素早く作成し、アプリ独自の機能に開発者が時間を割けるようにする」がSwiftUIのコンセプトです。基本機能とは、Viewのレイアウトが正しく表示される、ボタンタップが正常に機能するなどユーザーがアプリを使う上で気にも留めない、正しく動いて当たり前の機能のことです。
当たり前の機能であっても正しく動作するように実装するには一定の開発時間が必要です。SwiftUIを使えば品質は保ったまま素早く実装ができます。
SwiftUIの特徴は次のようなものがあります。
・宣言的シンタックスでViewをレイアウト
・データバインディングでViewとデータの不整合が減らせる
・Xcode 11からのプレビュー機能でリアルタイムにViewレイアウトを確認しながら開発可能
このような特徴のおかげで今までかけていた時間をアプリ自体の価値を高めることに使えるのです。ここで上げた3つの特徴をひとつずつ見ていきましょう。
UIKit/AppKitでアプリを開発する場合、Viewの作成は「どのようにViewを扱うか」という命令型プログラミングの考えに基づいて行われていました。次の例はUIImageViewに画像に角丸とドロップシャドーをつけたViewを作成するコードです。
let imageView = UIImageView(image: UIImage(named: "wing"))
imageView.contentMode = .scaleAspectFit
imageView.layer.cornerRadius = 10
imageView.clipsToBounds = true
let shadowView = UIView()
shadowView.layer.cornerRadius = 10
shadowView.layer.shadowColor = UIColor.gray.cgColor
shadowView.layer.shadowOffset = CGSize(width: 10, height: 10)
shadowView.layer.shadowRadius = 10
shadowView.layer.shadowOpacity = 1
shadowView.backgroundColor = .white
view.addSubview(shadowView)
view.addSubview(imageView)
imageViewを作成し、layer.cornerRadiusを更新することで角丸を作ります。clipsToBoundsをtrueにして角丸が表示されるようにします。また別途shadowViewを作成し、layerのshadowColor、shadowOffset、shadowRadius、shadowOpacityを更新してドロップシャドーのViewを作成します。UIKitでは角丸を表現するにはclipsToBoundsをtrueにする必要がありますが、こうするとシャドーが表示されなくなります。なので角丸のドロップシャドーを作成する場合は角丸のViewとシャドーのViewが2つ必要になります。最後にviewに対してimageViewとshadowViewをaddSubviewでView階層に組み込めば表示されます。このようにViewを作成し、そのViewのプロパティを操作することでViewの見た目を変えることがUIKitでの基本となります。表示結果は次のようになります。
これと同じ見た目のViewをSwiftUIで作成するコードは次のとおりになります。
Image("wing")
.cornerRadius(10)
.shadow(color: .gray, radius: 10, x: 10, y: 10)
だいぶコードがスッキリしているのが分かると思います。SwiftUIのView操作はUIKitと異なり宣言的に記述します。Image("wing")で画像を読み込んでImageのViewを作成します。.cornerRadius(10)を実行すると10ポイント角丸になった新しいViewが返されます。さらに.shadowを実行すると灰色で半径10、x軸とy軸が10ずれたドロップシャドーが追加された新しいViewが返されます。
.cornerRadiusや.shadowはView修飾子と呼ばれる、Viewを操作し、新しいViewを返すメソッドです。新しいViewが返されるので、メソッドを連結できます。このようにしてSwiftUIではView操作を宣言的に行うことができるのです。
Viewに紐づくデータが更新された場合、正しくViewを更新させることは開発者の責務です。従来の開発では、データ更新時にViewが正しく更新されるかどうかを時間をかけて検証する必要がありました。SwiftUIではViewにバインドするデータを宣言することで、データが更新されると自動的にViewも更新されます。Viewに同期するデータをバインディングするためにSwift 5.1のProperty Wrapperという機能を利用できます。
バインディングデータとして扱うには$マークを変数の前につけます。
例としてTextFieldに入力された文字をTextラベルに表示してみましょう。
struct BindingView: View {
@State var inputText: String = ""
var body: some View {
VStack {
TextField("", text: $inputText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Text(inputText)
}
}
}
@StateというProperty WrapperをつけたinputTextプロパティをTextFieldのtext引数に渡します。バインディングデータにするために$マークがつけられていることに注目しましょう。アプリを実行してTextFieldに文字を入力すると、自動的にTextも更新されます。
詳しくは第6章「SwiftUIのデータ管理」で解説します。
従来のUI開発では、レイアウトはStoryboardファイルやXibファイルを使っていても、最終的なレイアウト確認のためにシミュレーターなどでアプリを実行し、対象画面までアプリを操作する必要がありました。一方SwiftUIでは、Xcode 11からのプレビュー機能によってシミュレーターを起動しなくても、対象画面のレイアウトを直接確認できるようになりました。PreviewProviderプロトコルに準拠する型を作成すると、Xcodeのキャンバス画面にViewのプレビューが表示されます。
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
// プレビューのキャンバスサイズを指定
.previewLayout(.fixed(width: 200, height: 80))
}
}
ソースコードを変更するとリアルタイムにキャンバスが更新されます。
もちろん、SwiftUIでもコードだけでなくGUIでレイアウトが可能です。例として、ボタンを追加する操作をみてみましょう。Libraryボタンをクリックして、Libraryインスペクタを開きます。Buttonを選択して、「Hello, World!」というTextの横にドラッグ&ドロップします。するとキャンバス上ではTextの横にButtonが配置され、コードも更新されます。
このようにソースコードとキャンバス上のプレビューがリアルタイムに連動することで、効率的なレイアウト実装ができるようになりました。
キャンバスには、ライブプレビュー機能があります。この機能を使うと、Xcodeのキャンバス内でアプリをビルドでき、ユーザーインタラクションを含めて動作確認ができるようになります。プレビュー画面の一番左のボタンをクリックすることで、ライブプレビューを起動できます。
シミュレーターを起動しなくても特定の画面に対して確認ができるので開発が楽になります。積極的に使っていきましょう。
・サンプルコードURL
・https://github.com/SatoTakeshiX/SwiftUICatalog/tree/master/SwiftUIOverview
SwiftUIのレイアウトシステムを解説する前に、これまでUIKitではどのようなレイアウトシステムがあったのかを振り返ってみましょう。iPhone5が発売されるまでは、端末のサイズはどの端末でも変わらなかったので、開発者は各Viewのframeを手動で更新するか、Autoresizing Masksを使ってViewをレイアウトしていました。
しかしiPhone5が発売されると、画面サイズの比率が異なる端末にも対応しなければならなくなりました。またタブレット端末iPadも同じiOSとして発表され、開発者はiPhone、iPadをそれぞれレイアウトする必要に迫られました。異なる画面サイズに対してそれぞれViewのframeを手動で更新したり、Autoresizing Masksでレイアウトするには限界があり、Appleはその答えとしてAuto Layoutという仕組みを導入しました。Auto Layoutは各Viewの距離やサイズをすべて制約として表現します。
ところで、Viewのレイアウトが変更される場合、外部からの変更と内部からの変更の2種類があります。
外部からの変更
・iPadでユーザーがSplit Textを表示、非表示にした場合
・ユーザーが端末を横向きにした場合
・電話中や録音中のステータスバーが表示された場合
・異なるSize Classをサポートする場合
・異なるスクリーンサイズをサポートする場合
内部からの変更
・Viewのコンテンツが変更された場合
─ニュースコンテンツなどで文字数によってセルの高さを変えるなどが当てはまります。
・国際化に対応する場合
─ドイツ語は文字数が長くなる傾向があり、日本語は短くなる傾向があります。またアラビア語では文字の開始場所が英語とは逆に右から左になります。
・Dynamic Typeに対応する場合
─iOS9からユーザーは設定アプリからアプリの文字の大きさを変えられるようになりました。
外部から変更と内部の変更に対応した場合、手動でframeを更新すると作業量が膨大になり現実的ではありません。またAutoresizing Masksでのレイアウトは外部からの変更には対応できますが、内部からの変更には対応できません。Auto Layoutはどちらの変更にも対応できるシステムとなっており、AppleもAuto Layoutの利用を推奨してきました。ただAuto Layoutも問題がないわけではありません。ひとつは学習コストの問題です。これまでとは異なるシステムなので開発者が慣れるまでにはある程度の時間がかかりました。Appleもそれは自覚しているようで、Xcode 8ではStorybard、XibでのSize Classの指定方法が各端末サイズへの指定に変わり、iOS 9ではUIStackViewやNSLayoutAnchorが導入され、開発者がより簡単にAuto Layoutを導入しやすいように更新が行われてきました。
そしてもうひとつのAuto Layoutの問題は、容易に制約の矛盾が発生し得るという点です。たとえば、Labelの幅を200にし、かつ左右のマージンを16ずつにした場合は制約の矛盾が発生し、Storybard上ではエラーが表示されます。
制約の矛盾が発生した場合は、開発者は注意深く各Viewの制約を確認し、矛盾が起こらないように制約やその優先度を変更する必要があります。原因を特定するのは面倒な作業で時間がかかることもしばしばです。
一方SwiftUIではAuto Layoutとはまったく別のレイアウトシステムを導入してこの問題を解決しています。SwiftUIでのViewレイアウトがどのようになるか、サンプルとしてSwiftUIファイルを作成した際のテンプレートコードをみていきましょう。
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
ContentViewという名前のViewのbodyプロパティにTextを指定しているシンプルなViewです。ContentViewを表示するときに現れるViewは次の3つです。
・Root View
・ContentView
・Text
Root ViewはSwiftUIのView階層のトップに存在するViewです。デフォルトではセーフエリアを除いた端末領域になります。ステータスバーとホーム画面移動用のインジケーターの内側の領域です。edgesIgnoringSafeAreaメソッドを実行することで表示領域をセーフエリアを含めたものにすることもできます。次にContentViewですが、表示領域はbodyプロパティのViewの範囲(ここではText)と同じ範囲となります。ですのでViewのレイアウトフローを考える際には今回はContentViewとTextは同じViewとして扱ってかまいません。
レイアウトフローは次のとおりに進みます。
・親Viewが子Viewに対して表示可能領域を提案する
─Root Viewがセーフエリアを除いた領域を子ViewのTextに渡す
・子Viewは自身のサイズを決定し親Viewに伝える
─Textが自身のView領域をRootViewに返す
・親Viewが子Viewの表示位置を決める
─Root ViewはTextを中心に配置する
これで終わりです。どんなに階層が深くてもRoot Viewから配下の子Viewへこのやり取りが続き、最終的にすべてのViewの配置が決定されます。親は表示可能領域と子の配置位置を決めて、子は自身のView領域を決めるのです。ここで重要なのは、親Viewは子Viewのサイズ決定には関与しないということです。子が自身のサイズを決定します。この点がAuto Layoutと比較して決定的に異なる特徴といえます。
SwiftUIの「Viewは自身のViewサイズを設定」という仕組みのおかげでViewサイズ変更の方法やタイミングをView修飾子と呼ばれるメソッドで変更できます。View修飾子はもとのViewを変更して新しいViewを返すメソッドで、メソッドを連結させることでViewを自由に変更できます。View修飾子のひとつにframeがあります。これは指定したフレーム矩形の大きさにViewを更新するメソッドです。
ここではframeメソッドでViewを変更した場合にレイアウトはどうなるかをみてみましょう。
struct ContentView: View {
var body: some View {
Text("Hello World")
.frame(width: 200, height: 30)
}
}
さきほどと同じくContentViewはFrameと同じサイズになるので同じViewとして扱います。Viewの階層はRoot View -> Frame(ContentView) -> Textの順番になっています。
レイアウトフローは次のとおりです。
1.Root ViewがFrameへセーフエリアを除いたサイズを提案する
2.Frameは提案されたサイズから幅200, 高さ30のサイズを作りTextへ提案する
3.Textは文字数をもとに自身のサイズを決定してFrameへ伝える
4.FrameはTextから渡されたサイズをもとにTextを中心に配置してRootViewへ自身のサイズ幅200, 高さ30を伝える
5.Root ViewはFrameを中心に配置する
このようにView修飾子によってViewを変更する際も親Viewから子Viewへ順番にレイアウトされていきます。
ちなみにステップ4でFrameはTextを中心に配置していますが、alignment引数を指定すれば表示位置を変えることができます。
struct ContentView: View {
var body: some View {
Text("Hello World")
.frame(width: 200, height: 30, alignment: .topLeading)
}
}
また親Viewから提案されたサイズよりも子Viewのサイズが大きい場合はどうなるでしょうか?それは子Viewが自身のサイズを柔軟に変更できるかによります。Textは親Viewのサイズの提案サイズが自身のサイズよりも小さい場合は親Viewの提案サイズまでサイズを縮めます。
struct ContentView: View {
var body: some View {
Text("Hello World")
.frame(width: 60, height: 30)
}
}
プレビュー上はこのように表示されます。
「Hello...」と文字が省略されています。frameから提供された幅60、高さ30のサイズに合わせてTextのサイズも縮められたからです。
親の提案サイズが自身のViewよりも小さい場合でも、自身のViewサイズをそのまま送る場合もあります。Imageは画像を表示するためのViewですが、デフォルトでは画像サイズをそのまま返します。次のコードはwingという幅64、高さ64のサイズの画像をFrameで幅30、高さ30に変更するコードです。
struct BigImageThanParent: View {
var body: some View {
Image("wing")
.frame(width: 30, height: 30)
}
}
Imageの中に表示されている枠線がframeのサイズで、Imageのサイズは幅64、高さ64のままになっています。Imageのサイズを親Viewのサイズに合わせるにはresizableというView修飾子を実行する必要があります。
struct BigImageThanParent: View {
var body: some View {
Image("wing")
.resizable()
.frame(width: 30, height: 30)
}
}
そうすると親ViewであるFrameの幅30、高さ30にImageのサイズが変更されます。