目次

はじめに

本書の構成
対象読者
動作環境
本書のスコープ
サンプルコード

第1章 SwiftUI概要

1.1 コンセプト
1.2 特徴
1.3 宣言的シンタックス
1.4 データバインディング
1.5 プレビュー機能

第2章 SwiftUIのレイアウトシステム

2.1 UIKitのレイアウトシステム
2.2 SwiftUIのViewレイアウト
2.3 View修飾子を利用したViewの変更
2.4 HStack/VStack/ZStack

第3章 SwiftUIの座標空間

3.1 Viewサイズをどうやって知る?
3.2 GeometryReader
3.3 GeometryReaderの使い方
3.4 originで表示座標を知ろう
3.5 globalは本当にRootView?
3.6 Min, Mid, Max座標
3.7 SwiftUIとUIViewの座標情報対応表
3.8 ScrollViewと座標

第4章 基本Viewの使い方

4.1 Text
4.2 Image
4.3 Button
4.4 Path
4.5 Rectangle
4.6 Spacer
4.7 List
4.8 Section
4.9 NavigationView
4.10 TabView
4.11 ScrollView
4.12 TextField
4.13 View Presentation
4.14 iOS 14から登場したコンポーネント

第5章 iOS 14新機能: App、Scene、WindowGroup

5.1 100% SwiftUIアプリ
5.2 @main
5.3 App、Scene、View
5.4 アプリの状態を検知する: ScenePhase
5.5 UIApplicationDelegateAdaptor

第6章 SwiftUIのデータ管理

6.1 Single Source of Truth
6.2 データ管理のProperty Wrapper
6.3 プロパティ
6.4 @State
6.5 @Binding
6.6 @Environment
6.7 ObservableObjectプロトコル
6.8 iOS 14新機能:@StateObject
6.9 @ObservedObject
6.10 @EnvironmentObject
6.11 iOS 14新機能:@AppStorageと@SceneStorage
6.12 SwiftUIのデータ管理フローチャート

第7章 Combine

7.1 概要
7.2 Combineフレームワークの登場人物
7.3 Combineのライフサイクル
7.4 NotificationCenterのPublisher
7.5 Operator
7.6 独自でPublisherを作る

第8章 レシピ:GitHubAPIリポジトリー検索アプリ

8.1 アプリ概要
8.2 レイアウト
8.3 カード型UI
8.4 NavigationViewとTextField
8.5 SafariViewControllerをSwiftUIで使う
8.6 HomeView
8.7 GitHub Search APIについて
8.8 Model
8.9 ロジック
8.10 エラーが起きたら

第9章 レシピ:お絵かきアプリ

9.1 アプリ概要
9.2 レイアウト
9.3 ジェスチャーロジック  
9.4 線の色を変える
9.5 キャンバスを画像で保存する

第10章 レシピ:写真フィルターアプリ

10.1 アプリ概要
10.2 レイアウト
10.3 アプリ立ち上げ時にアクションシートを表示する
10.4 Image Pickerから画像を取得する
10.5 CIFilterでフィルター加工
10.6 FilterBannerView
10.7 画像の保存
10.8 他の画像に変更する

第11章 iOS 14新機能: Widget

11.1 Widget概要
11.2 Todoアプリ概要
11.3 Widget機能を追加する
11.4 テンプレートコードで理解するWidgetKit
11.5 TodoアプリにWidget機能追加
11.6 StaticConfigurationでWidget実装
11.7 IntentConfigurationでWidget実装
11.8 WidgetBundle

あとがき

感想、フィードバック
謝辞
参考文献

はじめに

 皆さんはSwiftUIでアプリ開発していますか?チュートリアルを試したけど自分のアプリはどう作ればいいかわからない!見よう見まねで作ったけど行き詰まった、周りに相談する人がいない!などなどお困りではないですか?本書はそんな悩みを解決します。本書は本格的なアプリのレシピを提供し、アプリの作り方の流れが学べます。さらにレイアウトシステムやViewの使い方など、SwiftUIで開発する上で誰もが理解すべき原理原則を解説します。基礎から応用まで幅広く解説された本書を読んでSwiftUIでアプリを作りましょう!

本書の構成

 本書ではSwiftUIでアプリを作成できるようになるため、まずアプリ作成に必要な知識や概念を解説します。SwiftUIのレイアウトシステムや、Viewの使い方、Combineなどです。その後3つのアプリを作ります。どれも実践的なアプリです。アプリ作成は「アプリ概要」「レイアウト」「ロジック」を重点に解説します。自分でアプリを作る際には、この三段階を思い出していただければアプリを作れるはずです。

 ・第1章「SwiftUI概要」

  ─SwiftUIの概要を解説します。SwiftUIとはどんなコンセプトで作られ、どんな特徴や利点があるのかをお伝えします。

 ・第2章「SwiftUIのレイアウトシステム」

  ─Viewのレイアウトがどのように決まるのかを解説します。これを理解すればアプリの画面作成の助けになるでしょう。

 ・第3章「SwiftUIの座標空間」

  ─SwiftUIで各Viewの座標やサイズを把握する方法を解説します。

 ・第4章「基本Viewの使い方」

  ─TextImageなど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

 各章のサンプルプロジェクトがディレクトリでまとめられています。どのプロジェクトをサンプルとして利用するのかは各章でお伝えします。

第1章 SwiftUI概要

 SwiftUIはWWDC 19で発表された新しいフレームワークです。Swiftでユーザーインターフェースを宣言的に記述できます。macOS/iOS/tvOS/watchOSなどのAppleプラットフォーム向けに、同じコードでビルドできます。ステートドリブンなシステムで、データが更新されると自動的にViewも更新されるため、バグを減少させることができます。この章ではSwiftUIの概要を解説します。

1.1 コンセプト

 「アプリの基本機能を高品質のまま素早く作成し、アプリ独自の機能に開発者が時間を割けるようにする」がSwiftUIのコンセプトです。基本機能とは、Viewのレイアウトが正しく表示される、ボタンタップが正常に機能するなどユーザーがアプリを使う上で気にも留めない、正しく動いて当たり前の機能のことです。

 当たり前の機能であっても正しく動作するように実装するには一定の開発時間が必要です。SwiftUIを使えば品質は保ったまま素早く実装ができます。

図1.1: SwiftUI開発の利点

1.2 特徴

 SwiftUIの特徴は次のようなものがあります。

 ・宣言的シンタックスでViewをレイアウト

 ・データバインディングでViewとデータの不整合が減らせる

 ・Xcode 11からのプレビュー機能でリアルタイムにViewレイアウトを確認しながら開発可能

 このような特徴のおかげで今までかけていた時間をアプリ自体の価値を高めることに使えるのです。ここで上げた3つの特徴をひとつずつ見ていきましょう。

1.3 宣言的シンタックス

 UIKit/AppKitでアプリを開発する場合、Viewの作成は「どのようにViewを扱うか」という命令型プログラミングの考えに基づいて行われていました。次の例はUIImageViewに画像に角丸とドロップシャドーをつけたViewを作成するコードです。

リスト1.1: UIImageViewで角丸シャドー

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を作成し、layershadowColorshadowOffsetshadowRadiusshadowOpacityを更新してドロップシャドーのViewを作成します。UIKitでは角丸を表現するにはclipsToBoundsをtrueにする必要がありますが、こうするとシャドーが表示されなくなります。なので角丸のドロップシャドーを作成する場合は角丸のViewとシャドーのViewが2つ必要になります。最後にviewに対してimageViewshadowViewaddSubviewでView階層に組み込めば表示されます。このようにViewを作成し、そのViewのプロパティを操作することでViewの見た目を変えることがUIKitでの基本となります。表示結果は次のようになります。

図1.2: 角丸シャドー表示結果

 これと同じ見た目のViewをSwiftUIで作成するコードは次のとおりになります。

リスト1.2: 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操作を宣言的に行うことができるのです。

1.4 データバインディング

 Viewに紐づくデータが更新された場合、正しくViewを更新させることは開発者の責務です。従来の開発では、データ更新時にViewが正しく更新されるかどうかを時間をかけて検証する必要がありました。SwiftUIではViewにバインドするデータを宣言することで、データが更新されると自動的にViewも更新されます。Viewに同期するデータをバインディングするためにSwift 5.1のProperty Wrapperという機能を利用できます。

 バインディングデータとして扱うには$マークを変数の前につけます。

 例としてTextFieldに入力された文字をTextラベルに表示してみましょう。

リスト1.3: @Stateでバインディング

struct BindingView: View {
      @State var inputText: String = ""
      var body: some View {
          VStack {
              TextField("", text: $inputText)
                  .textFieldStyle(RoundedBorderTextFieldStyle())
                  .padding()
              Text(inputText)
          }
      }
  }

 @StateというProperty WrapperをつけたinputTextプロパティをTextFieldtext引数に渡します。バインディングデータにするために$マークがつけられていることに注目しましょう。アプリを実行してTextFieldに文字を入力すると、自動的にTextも更新されます。

図1.3: TextFieldとTextのバインディング

 詳しくは第6章「SwiftUIのデータ管理」で解説します。

1.5 プレビュー機能

 従来のUI開発では、レイアウトはStoryboardファイルやXibファイルを使っていても、最終的なレイアウト確認のためにシミュレーターなどでアプリを実行し、対象画面までアプリを操作する必要がありました。一方SwiftUIでは、Xcode 11からのプレビュー機能によってシミュレーターを起動しなくても、対象画面のレイアウトを直接確認できるようになりました。PreviewProviderプロトコルに準拠する型を作成すると、Xcodeのキャンバス画面にViewのプレビューが表示されます。

リスト1.4: プレビュー作成コード

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))
      }
  }
図1.4: Xcodeのキャンバス画面とプレビュー

 ソースコードを変更するとリアルタイムにキャンバスが更新されます。

 もちろん、SwiftUIでもコードだけでなくGUIでレイアウトが可能です。例として、ボタンを追加する操作をみてみましょう。Libraryボタンをクリックして、Libraryインスペクタを開きます。Buttonを選択して、「Hello, World!」というTextの横にドラッグ&ドロップします。するとキャンバス上ではTextの横にButtonが配置され、コードも更新されます。

図1.5: GUIでレイアウト

 このようにソースコードとキャンバス上のプレビューがリアルタイムに連動することで、効率的なレイアウト実装ができるようになりました。

1.5.1 ライブプレビュー機能

 キャンバスには、ライブプレビュー機能があります。この機能を使うと、Xcodeのキャンバス内でアプリをビルドでき、ユーザーインタラクションを含めて動作確認ができるようになります。プレビュー画面の一番左のボタンをクリックすることで、ライブプレビューを起動できます。

図1.6: ライブプレビュー機能

 シミュレーターを起動しなくても特定の画面に対して確認ができるので開発が楽になります。積極的に使っていきましょう。

第2章 SwiftUIのレイアウトシステム

 ・サンプルコードURL

 ・https://github.com/SatoTakeshiX/SwiftUICatalog/tree/master/SwiftUIOverview

2.1 UIKitのレイアウトシステム

 SwiftUIのレイアウトシステムを解説する前に、これまでUIKitではどのようなレイアウトシステムがあったのかを振り返ってみましょう。iPhone5が発売されるまでは、端末のサイズはどの端末でも変わらなかったので、開発者は各Viewのframeを手動で更新するか、Autoresizing Masksを使ってViewをレイアウトしていました。

図2.1: Autoresizing Masks

 しかし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ではUIStackViewNSLayoutAnchorが導入され、開発者がより簡単にAuto Layoutを導入しやすいように更新が行われてきました。

 そしてもうひとつのAuto Layoutの問題は、容易に制約の矛盾が発生し得るという点です。たとえば、Labelの幅を200にし、かつ左右のマージンを16ずつにした場合は制約の矛盾が発生し、Storybard上ではエラーが表示されます。

図2.2: Auto Layout制約矛盾

 制約の矛盾が発生した場合は、開発者は注意深く各Viewの制約を確認し、矛盾が起こらないように制約やその優先度を変更する必要があります。原因を特定するのは面倒な作業で時間がかかることもしばしばです。

2.2 SwiftUIのViewレイアウト

 一方SwiftUIではAuto Layoutとはまったく別のレイアウトシステムを導入してこの問題を解決しています。SwiftUIでのViewレイアウトがどのようになるか、サンプルとしてSwiftUIファイルを作成した際のテンプレートコードをみていきましょう。

リスト2.1: 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として扱ってかまいません。

図2.3: ContentViewの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と比較して決定的に異なる特徴といえます。

2.3 View修飾子を利用したViewの変更

 SwiftUIの「Viewは自身のViewサイズを設定」という仕組みのおかげでViewサイズ変更の方法やタイミングをView修飾子と呼ばれるメソッドで変更できます。View修飾子はもとのViewを変更して新しいViewを返すメソッドで、メソッドを連結させることでViewを自由に変更できます。View修飾子のひとつにframeがあります。これは指定したフレーム矩形の大きさにViewを更新するメソッドです。

 ここではframeメソッドでViewを変更した場合にレイアウトはどうなるかをみてみましょう。

リスト2.2: 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の順番になっています。

図2.4: frameでサイズ変更

 レイアウトフローは次のとおりです。

 1.Root ViewがFrameへセーフエリアを除いたサイズを提案する

 2.Frameは提案されたサイズから幅200, 高さ30のサイズを作りTextへ提案する

 3.Textは文字数をもとに自身のサイズを決定してFrameへ伝える

 4.FrameはTextから渡されたサイズをもとにTextを中心に配置してRootViewへ自身のサイズ幅200, 高さ30を伝える

 5.Root ViewはFrameを中心に配置する

図2.5: レイアウトフロー1,2
図2.6: レイアウトフロー3, 4,5

 このようにView修飾子によってViewを変更する際も親Viewから子Viewへ順番にレイアウトされていきます。

 ちなみにステップ4でFrameはTextを中心に配置していますが、alignment引数を指定すれば表示位置を変えることができます。

リスト2.3: alignmentを指定

struct ContentView: View {
      var body: some View {
          Text("Hello World")
              .frame(width: 200, height: 30, alignment: .topLeading)
      }
  }
図2.7: Text配置を左上に変更

 また親Viewから提案されたサイズよりも子Viewのサイズが大きい場合はどうなるでしょうか?それは子Viewが自身のサイズを柔軟に変更できるかによります。Textは親Viewのサイズの提案サイズが自身のサイズよりも小さい場合は親Viewの提案サイズまでサイズを縮めます。

リスト2.4: Textサイズよりも小さいFrameを作る

struct ContentView: View {
      var body: some View {
          Text("Hello World")
              .frame(width: 60, height: 30)
      }
  }

 プレビュー上はこのように表示されます。

図2.8: Textで親Viewの提案サイズが子Viewの幅よりも足りない場合

 「Hello...」と文字が省略されています。frameから提供された幅60、高さ30のサイズに合わせてTextのサイズも縮められたからです。

 親の提案サイズが自身のViewよりも小さい場合でも、自身のViewサイズをそのまま送る場合もあります。Imageは画像を表示するためのViewですが、デフォルトでは画像サイズをそのまま返します。次のコードはwingという幅64、高さ64のサイズの画像をFrameで幅30、高さ30に変更するコードです。

リスト2.5: Imageのサイズよりも小さいFrameを作る

struct BigImageThanParent: View {
      var body: some View {
          Image("wing")
          .frame(width: 30, height: 30)
      }
  }
図2.9: Imageで親Viewの提案サイズが子Viewの幅よりも足りない場合

 Imageの中に表示されている枠線がframeのサイズで、Imageのサイズは幅64、高さ64のままになっています。Imageのサイズを親Viewのサイズに合わせるにはresizableというView修飾子を実行する必要があります。

リスト2.6: resizableでImageサイズを変更

struct BigImageThanParent: View {
      var body: some View {
          Image("wing")
          .resizable()
          .frame(width: 30, height: 30)
      }
  }

 そうすると親ViewであるFrameの幅30、高さ30にImageのサイズが変更されます。

図2.10: Imageがframeのサイズになった

1. SwiftUIのレイアウトを説明するWWDC 19の「Building Custom Views with SwiftUI」には何をもって柔軟性がないのかには言及がありませんでした。

2. layoutPriorityを操作して選ばれる順番を操作したところ、この例ではImage、Text("First")、Text("HStackSecondText")の順番で選ばれていることを確認しました。柔軟性がないという意味は子Viewのサイズが親Viewのサイズによって変更されないことを意味するようです。

試し読みはここまでです。
この続きは、製品版でお楽しみください。