FlutterエンジニアのSwift勉強2 ~SwiftUI編~
画像の表示
-
Assets.xcassets
フォルダに表示したい画像を保存
-
Image()
コンポーネントにフォルダ以下のパスを指定(拡張子は不要) - サイズ変更や枠線などはImageコンポーネントの
modifier
で指定
引数
decorative
systemName
Reference
スプラッシュ画面を実装する
- スプラッシュ画面の画像を
Assets.xcassets
に追加 - info.plistでLaunchScreenの設定へ移動(proejct > TARGETS > Custom iOS Target Properties >Info)
-
Image Name
,Background color
を設定- Image Name: Assetsに追加した画像名を入力(拡張子不要)
- Background Color:
Primary
なら通常は白背景、ダークモードで黒背景。Assetsに色を登録することで好きな背景色を設定することも可能。
※画像サイズは動的にリサイズされる事はないので、320 x 320pxで用意しておく(厳密には各iOSデバイス向けに用意する必要がある)
Reference
画面の状態保持
プロパティに以下で紹介するProperty Wrapperを用途に応じてアノテーションする事で状態値の管理と通知を行う。
- 単一Viewをスコープとする場合は
State
を用いる - 複数のViewをスコープとする場合は
ObservableObject
を用いる - Stateを使う場合は
@Binding
、ObservableObjectを使う場合は@StateObject
,@ObservedObject
,@EnvironmentObject
を使って、離れたViewに値をバインディングさせる - プロパティに対して付与する
@xxxx
はProperty Wrapperと呼ばれる
State
@State
- Viewのライフサイクルに紐づく
- 実は@StateObjectもViewのライフサイクルに紐づく
- 画面の状態管理に適している
- [FlutterのStatefulWidgetに定義したローカル変数に相当]
@Binding
- 子Viewに
@State
をバインディングする為のProperty Wrapper
ObservableObject
- 複数画面をスコープとする場合に用いる
protocol
- 監視する対象を
ObservableObject
を継承するプロトコルとして定義 - それを
@StateObject
,@ObservedObject
,@EnvironmentObject
のどれかを使ってViewにバインディングする -
ObservableObject
に準拠するプロトコル内で観測したいプロパティに対して@Published
を付ける
@StateObject, @ObservedObject, @EnvironmentObject
@StateObject
- @State同様にViewのライフサイクルに紐づく
- 副作用の処理を伴う状態管理
@ObservedObject
-
View
のライフサイクルに紐づかない
@EnvironmentObject
- ObservableObjectを注入したいViewで
environmentObject
modifierを使用 - すると、そのView以下のサブビューで
@EnvironmentObject
でObservableObject
に依存するプロパティを定義できる
使い方
State
ObservedObject
Ref.
[SwiftUI] ボタンアクションに応じてViewを切り替える
TODO
- Viewの状態値(ローカル変数)を定義
- 条件分岐でViewを切り替える
- Buttonコンポーネントのactionで状態値を変更
Code
import SwiftUI
struct ContentView: View {
@State private var isOngoing: Bool = false // ① 状態値の定義
var body: some View {
VStack {
Image(decorative: "ninja_dash")
.resizable()
.frame(width:100,height: 90)
.foregroundStyle(.tint)
// ② 状態値に応じて条件分岐で切り替え
isOngoing ? Text("Swift Ongoing!!") : Text("Starting Swift!!")
Button(action:{
print("button clicked!!")
self.isOngoing.toggle() // ③ ボタンアクションで状態値を変更
}
){Text("Start")}
.padding()
}
}
}
#Preview {
ContentView()
}
① Viewの状態値(ローカル変数)を定義
@State private var isOngoing: Bool = false // ① 状態値の定義
-
@State
アノテーションをつけたプライベート変数(private var
)を定義 -
@State
とすることでそのViewに閉じた状態値として定義される
② 条件分岐でViewを切り替える
isOngoing ? Text("Swift Ongoing!!") : Text("Starting Swift!!")
- 先に定義した状態値
isOngoing
の値に応じて、View Componentを切り替える - 上記では三項演算子でViewを切り替えている
- [Flutter同様、UIの描画処理内に条件分岐を直接記述してViewの切り替えが出来る]
③ Buttonコンポーネントのactionで状態値を変更
Button(action:{
print("button clicked!!")
self.isOngoing.toggle()
}
-
Button
コンポーネントのaction
フィールドにクリック時の処理を記述 -
Bool
クラスにはデフォルトでtoggle()
メソッドが実装されてるので、そちらで値を切り替え - 変数名の接頭辞
self
はメンバ変数である事を表現しているが、他の変数とコンフリクトがなければ省略可能
成果物
Reference
[SwiftUI] 画面遷移を実装する
基本的にはNavigationStack
とNavigationLink
という2つのコンポーネントを使用して画面遷移を実現します。iOS15まではNavigationView
が用いられていましたが、iOS16以降でNavigationStack
が使われるようになりました。
NavigationStack
画面遷移を管理するコンポーネント。階層的に積み上げら(スタックさ)れるViewを管理する容器の役割を担う。
NavigationLink
ある画面から別の画面への遷移を実現する為のコンポーネント。NavigationStack
内で使用され、特定のViewへのリンクを表示する。
デフォルトでバックボタンを備えたViewへ遷移を行い、その他にもnavigationTitle
やnavigationDestination
といったモディファイヤで遷移元のViewにタイトルを付与したり値を渡した画面遷移などを定義する事ができます。
基礎的な使い方
- トップレベルのViewを
NavigationStack
でラップ -
NavgiationLink
コンポーネントをViewに配置 - 遷移先Viewとリンクテキストを定義
- 遷移先Viewから更に遷移したい際は再度
NavigationLink
を配置
Code
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack{ // ① トップレベルのViewをNavigationStackでラップ
VStack {
Image(decorative: "ninja_dash")
.resizable()
.frame(width: 100,height: 90)
.foregroundStyle(.tint)
Text("Starting Swift!!")
NavigationLink("Go to Second View"){ // ② NavigationLinkを配置
SecondPageView() // ③ リンクテキストと遷移先を定義
}
.padding()
}
}
}
}
struct SecondPageView: View {
var body: some View {
Text("Hello, world!")
// ④ 遷移先Viewで更に遷移したい場合はNavigationLinkを再度定義
NavigationLink("to Third View"){
ThirdPageView()
}
}
}
struct ThirdPageView: View {
var body: some View {
Text("This is Third View")
}
}
成果物
Reference
[SwiftUI]アプリのエントリーポイント
エントリーポイントとはアプリが移動する際に最初に実行される処理の事を指します。SwiftUIでは@main
アノテーションを付与されたApp
プロトコルに準拠した構造体がエントリーポイントとなります。
import SwiftUI
@main
struct sample_swiftApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Sceneプロトコル
var body: some Scene {
WindowGroup {
ContentView()
}
}
Sceneプロトコルはユーザーに表示したい「ビュー階層のコンテナ」として機能します。WindowGroup
とDocumentGroup
が用意されており、一般的なアプリケーションではWindowGroup
、ドキュメント中心のアプリケーションではDocumentGroup
を使います。
Scene
はMacやiPadでの1ウィンドウに相当するもので、iOSアプリケーションでは単体のSceneのみ用いられますが、MacOSやiPadOSでは複数のSceneを定義する事が可能です。
App, Scene, Viewの関係性
Appは複数のSceneで構成され、Sceneは複数の階層的なViewで構成されます。上述の通り、Sceneはウィンドウに相当し、ウィンドウを1つのみ持つアプリケーションであれば、AppとSceneで一対一の関係となります。
Reference
[SwiftUI] 画面をレイアウトしていく
View ↔︎ Widget対応表
SwiftUI | Flutter | 用途 |
---|---|---|
HStack | Row | 水平配置 |
VStack | Column | 垂直配置 |
ZStack | Stack | 重ねて配置 |
List, ForEach | ListView.separated | リスト |
ScrollView | ListView | スクロール可能なリスト |
TabView | BottomNavigationBar | タブバー |
ListとScrollViewの違い
List
はディバイダーが標準で実装されたリストであり、縦方向にのみスクロール可能です。一方、ScrollView
はDividerは実装されておらず、またスクロール方向も自由です。
更にメモリ上に生成されるタイミングが異なり、List
では画面に表示されるタイミングでデータをメモリに生成されますが、ScrollView
では一度に全てのデータをメモリ上に生成されます。その為、無限スクロールのような実装ではList
を使用しましょう。
Reference
SwiftUI以前(iOS13未満)のエントリーポイント
SwiftUI以前ではUI生成に対してUI生成用のGUI「Storyboard
」及びUIkit
を使って開発が行われていました。その際、エントリーポイントに関してもSwiftUIとは異なる記述がなされていました。Flutterプロジェクト内に生成されるiOSのエントリーポイントはこちらのStoryboard
+UIKit
時代が使用されています。
Code
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Reference
[SwiftUI] TabBarを実装する
Flutterで言うところのBottomNavigationBar
であるTabBar
の作成にはTabView
コンポーネントを使います。
Reference
[SwiftUI] Imageで使えるアイコン一覧
- アイコンはSF Symbolsに含まれるとい6000種類以上のアイコンが使用出来ます
- SF SymbolsというMac用アプリをインストールする事で一覧を確認できます
[SwiftUI] コンポーネントのレイアウトを操作
レイアウトに関する原則
Viewのサイズと位置が決まる3ステップ
- 親コンポーネントが子にサイズを提案する
- 提案されたサイズに基づいて子は自身のサイズを決定する
- 親が子を自身の中に配置する
modifierを挿入すると
- modifierはViewと考えると良い
- Viewにmodifierを付与した場合、コンポーネントの階層の上位に挿入されるのはModifier Viewとなる
- modifierを付与されたView本体ではないのがポイント
- ViewにはレイアウトニュートラルなViewが存在する(ex. ContentView, modifier View)
- 親から子の順にサイズ提案がされる為、modifierの順序もレイアウトに影響を及ぼす
- またText > background > frame > backgroundとmodifierを続けることもできる
- これは1つ目のmodifierはTextの上に挿入されたbackgroundだが、2つ目はframeの上に挿入されたbackgroundという意味になる
padding modifier
Viewコンポーネントにpadding
修飾子(modifier)を付与する事で余白を追加することができます。
- デフォルトは四方向に16ポイントの余白
- 第一引数にした方向に余白を追加
- 第二引数で余白の値を指定
-
.top
(上),.bottom
(下),.leading
(左),.trailing
(右)から方向を選択する - その他
.horizontal
,.vertical
も存在 - カスタムの四方向を設定したければ
EdgeInsets(top:xxx,leading:xxx, bottom: xxx, trailing:xxx)
Reference
[SwiftUI] カメラを使う
カメラを実装する際には、以下のアプローチがあります
- デフォルトのカメラアプリの利用
- カスタマイズしたカメラ画面を利用
デフォルトのカメラアプリを利用する場合はUIImagePickerController
を、カスタマイズする場合はAVFoundation
APIを用いてカメラ機能を実装します。
UIViewControllerRepresentable
SwiftUIでは一部SwiftUIで対応しきれずUIKitを利用する機能があります。その際に利用されるのがUIViewRepresentable
,UIViewControllerRepresentable
というプロトコルです。
それぞれ、
UIViewRepresentable
ではmakeUIView()
、updateUIView()
UIViewControllerRepresentable
ではmakeUIViewController()
,updateUIViewController()
の中にUIkitやUIViewControllerを実装する事で利用可能になります。
Reference
[SwiftUI] sheet, fullScreenCover, alert, confirmationDialog
FullScreenCover
Reference
UIViewControllerRepresentable
UIkitとSwiftUIを橋渡しするUIViewControllerRepresentable
の実装方法を整理。
-
makeUIViewController(context:)
とupdateUIViewContrllor(_:cotext:)
の実装 -
Coordinator
の使用