Open14

FlutterエンジニアのSwift勉強2 ~SwiftUI編~

heyhey1028heyhey1028

画像の表示

  1. Assets.xcassetsフォルダに表示したい画像を保存
  2. Image()コンポーネントにフォルダ以下のパスを指定(拡張子は不要)
  3. サイズ変更や枠線などはImageコンポーネントのmodifierで指定

引数

decorative

systemName

Reference

https://capibara1969.com/1861/

heyhey1028heyhey1028

スプラッシュ画面を実装する

  1. スプラッシュ画面の画像をAssets.xcassetsに追加
  2. info.plistでLaunchScreenの設定へ移動(proejct > TARGETS > Custom iOS Target Properties >Info)
  3. Image Name, Background colorを設定
    • Image Name: Assetsに追加した画像名を入力(拡張子不要)
    • Background Color: Primaryなら通常は白背景、ダークモードで黒背景。Assetsに色を登録することで好きな背景色を設定することも可能。

※画像サイズは動的にリサイズされる事はないので、320 x 320pxで用意しておく(厳密には各iOSデバイス向けに用意する必要がある)

Reference

https://design.aoziso.com/launchscreen/
https://qiita.com/Hackenbacker/items/85c8f785c2df6f1f7534

heyhey1028heyhey1028

画面の状態保持

プロパティに以下で紹介するProperty Wrapperを用途に応じてアノテーションする事で状態値の管理と通知を行う。

  • 単一Viewをスコープとする場合はStateを用いる
  • 複数のViewをスコープとする場合はObservableObjectを用いる
  • Stateを使う場合は@Binding、ObservableObjectを使う場合は@StateObject,@ObservedObject,@EnvironmentObjectを使って、離れたViewに値をバインディングさせる
  • プロパティに対して付与する@xxxxProperty 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以下のサブビューで@EnvironmentObjectObservableObjectに依存するプロパティを定義できる

使い方

State

ObservedObject

Ref.

https://developer.apple.com/videos/play/wwdc2020/10040/
https://zenn.dev/ueshun/articles/2b26aaad40d6a3
https://blog.code-candy.com/swiftui_binding/

heyhey1028heyhey1028

[SwiftUI] ボタンアクションに応じてViewを切り替える

TODO

  1. Viewの状態値(ローカル変数)を定義
  2. 条件分岐でViewを切り替える
  3. 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

https://qiita.com/nakanami/items/6acf73453e29122e7195
https://muukii.medium.com/swift-メンバ変数アクセス時にselfは書くべき-7a1b1069bf9d#:~:text=Swiftはselfを書か,を付ける必要があります。

heyhey1028heyhey1028

[SwiftUI] 画面遷移を実装する

基本的にはNavigationStackNavigationLinkという2つのコンポーネントを使用して画面遷移を実現します。iOS15まではNavigationViewが用いられていましたが、iOS16以降でNavigationStackが使われるようになりました。

画面遷移を管理するコンポーネント。階層的に積み上げら(スタックさ)れるViewを管理する容器の役割を担う。

ある画面から別の画面への遷移を実現する為のコンポーネント。NavigationStack内で使用され、特定のViewへのリンクを表示する。

デフォルトでバックボタンを備えたViewへ遷移を行い、その他にもnavigationTitlenavigationDestinationといったモディファイヤで遷移元のViewにタイトルを付与したり値を渡した画面遷移などを定義する事ができます。

基礎的な使い方

  1. トップレベルのViewをNavigationStackでラップ
  2. NavgiationLinkコンポーネントをViewに配置
  3. 遷移先Viewとリンクテキストを定義
  4. 遷移先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

https://swappli.com/screentransitions/
https://swappli.com/about-navigationstack/
https://note.com/taatn0te/n/n0b2a1833a73f
https://blog.code-candy.com/swiftui_navigationstack/

heyhey1028heyhey1028

[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プロトコルはユーザーに表示したい「ビュー階層のコンテナ」として機能します。WindowGroupDocumentGroupが用意されており、一般的なアプリケーションではWindowGroup、ドキュメント中心のアプリケーションではDocumentGroupを使います。

SceneはMacやiPadでの1ウィンドウに相当するもので、iOSアプリケーションでは単体のSceneのみ用いられますが、MacOSやiPadOSでは複数のSceneを定義する事が可能です。

https://developer.apple.com/documentation/swiftui/scene

App, Scene, Viewの関係性

Appは複数のSceneで構成され、Sceneは複数の階層的なViewで構成されます。上述の通り、Sceneはウィンドウに相当し、ウィンドウを1つのみ持つアプリケーションであれば、AppとSceneで一対一の関係となります。

Reference

https://swappli.com/entrypoint/
https://swappli.com/about-scene/#:~:text=Scene は、SwiftUIアプリの,することができます。
https://qiita.com/imchino/items/988a3f3bdc73953fb92e
https://software.small-desk.com/development/2022/08/19/swiftui-summary-scene-wwdc22/

heyhey1028heyhey1028

[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を使用しましょう。
https://blog.code-candy.com/list_scrollview_vary/

Reference

https://qiita.com/mashunzhe/items/1375be076e1734e1cc42
https://astrasprout.main.jp/appworld/swiftui-sectionlistforeach/

heyhey1028heyhey1028

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

https://swift-ios.keicode.com/ios/initialization-sequence.php

heyhey1028heyhey1028

[SwiftUI] コンポーネントのレイアウトを操作

レイアウトに関する原則

Viewのサイズと位置が決まる3ステップ

  1. 親コンポーネントが子にサイズを提案する
  2. 提案されたサイズに基づいて子は自身のサイズを決定する
  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という意味になる

https://www.youtube.com/watch?v=04fzFk367Dg

padding modifier

Viewコンポーネントにpadding修飾子(modifier)を付与する事で余白を追加することができます。

  • デフォルトは四方向に16ポイントの余白
  • 第一引数にした方向に余白を追加
  • 第二引数で余白の値を指定
  • .top(上),.bottom(下),.leading(左),.trailing(右)から方向を選択する
  • その他.horizontal,.verticalも存在
  • カスタムの四方向を設定したければEdgeInsets(top:xxx,leading:xxx, bottom: xxx, trailing:xxx)

Reference

https://qiita.com/yuruhuwakumiai/items/03980193103bade237d4

heyhey1028heyhey1028

[SwiftUI] カメラを使う

カメラを実装する際には、以下のアプローチがあります

  1. デフォルトのカメラアプリの利用
  2. カスタマイズしたカメラ画面を利用

デフォルトのカメラアプリを利用する場合はUIImagePickerControllerを、カスタマイズする場合はAVFoundation APIを用いてカメラ機能を実装します。

UIViewControllerRepresentable

SwiftUIでは一部SwiftUIで対応しきれずUIKitを利用する機能があります。その際に利用されるのがUIViewRepresentable,UIViewControllerRepresentableというプロトコルです。

それぞれ、
UIViewRepresentableではmakeUIView()updateUIView()
UIViewControllerRepresentableではmakeUIViewController(),updateUIViewController()
の中にUIkitやUIViewControllerを実装する事で利用可能になります。

https://blog.studysapuri.jp/entry/2022/03/28/using-uikit-in-swiftui

Reference

https://betterprogramming.pub/effortless-swiftui-camera-d7a74abde37e
https://blog.canopas.com/ios-how-to-integrate-camera-apis-using-swiftui-ea604a2d2d0f
https://developer.apple.com/tutorials/sample-apps/capturingphotos-camerapreview
https://qiita.com/SNQ-2001/items/2cc6e7e35ab98ba02397
https://zenn.dev/joo_hashi/articles/cbb87247dd9418
https://www.youtube.com/playlist?list=PLRqxhkF1SUfswqXcobq9V_bTS0Fv9tADU
https://www.youtube.com/watch?v=ZmPJBiwgZoQ&t=126s

https://zenn.dev/naoya_maeda/articles/6f5c6bec557393

heyhey1028heyhey1028

UIViewControllerRepresentable

UIkitとSwiftUIを橋渡しするUIViewControllerRepresentableの実装方法を整理。

  1. makeUIViewController(context:)updateUIViewContrllor(_:cotext:)の実装
  2. Coordinatorの使用