3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftUIAdvent Calendar 2023

Day 14

SwiftUIでEnvironmentValuesを使ってカスタムActionをDIする

Posted at

SwiftUIで、自分で作ったカスタムActionを使ってDIを行います。

EnvironmentValuesのActionsについて

@Environment で使えるValueにはいくつかカテゴリがあって、その中の一つにActionsというものがあります。

これは、システム側で提供されるcallAsFunctionで定義されたアクションで、一つのタスクを行うことに特化したものになっています。
例えば openURL を使えば URL遷移を行うことができます。

struct OpenURLExample: View {
    @Environment(\.openURL) private var openURL

    var body: some View {
        Button {
            if let url = URL(string: "https://www.example.com") {
                openURL(url)
            }
        } label: {
            Label("Get Help", systemImage: "person.fill.questionmark")
        }
    }
}

iOS 17で追加されたpurchase は、製品の購入を行い、購入結果を取得する、というアクションになっています。

struct PurchaseExample: View {
    @Environment(\.purchase) private var purchase
    let product: Product
    let purchaseOptions: [Product.PurchaseOption]

    var body: some View {
        Button {
            Task {
                let purchaseResult = try? await purchase(product, options: purchaseOptions)
                // Process purchase result.
            }
        } label: {
            Text(product.displayName)
        }
    }
}

システムで提供されているActionsは、purchaseであれば PurchaseAction といったようなstructと結びついています。

カスタムActionを作成する

自作したカスタムActionを、protocolで定義して差し替え可能にしてみようと思います。
Qiita APIからタグ一覧を取得するようなActionを考えます。

struct Tag: Decodable {
    let followersCount: Int
    let iconUrl: String?
    let id: String
    let itemsCount: Int
}

EnvironmentValue用のprotocolの定義

まずはprotocolを定義します。

protocol GetQiitaTagsAction {
    func callAsFunction(page: Int, perPage: Int, sort: String) async throws -> [Tag]
}

そして、そのprotocolに適合したstructを作成します。
固定データを返す GetQiitaTagsFromLocalAction と、必ずエラーにする GetQiitaTagsErrorAction の二つを作成しました。

struct GetQiitaTagsFromLocalAction: GetQiitaTagsAction {
    func callAsFunction(page: Int, perPage: Int, sort: String) async throws -> [Tag] {
        [
            .init(followersCount: 10,
                  iconUrl: nil,
                  id: "テストID",
                  itemsCount: 1)
        ]
    }
}

struct GetQiitaTagsErrorAction: GetQiitaTagsAction {
    struct GetQiitaTagsErrorActionError: Error {}
    
    func callAsFunction(page: Int, perPage: Int, sort: String) async throws -> [Tag] {
        throw GetQiitaTagsErrorActionError()
    }
}

EnvironmentValue用のキーとアクションを追加

EnvironmentValuesにQiitaのタグ取得用のアクションを追加します。
EnvironmentKeyが必要になるので同時に追加します。
defaultValueが必要になるので、デフォルトのGetQiitaTagsActionを追加しておきます。

private struct GetQiitaTagsActionKey: EnvironmentKey {
    static let defaultValue: GetQiitaTagsAction = GetQiitaTagsFromLocalAction()
}

extension EnvironmentValues {
    var getQiitaTags: GetQiitaTagsAction {
        get { self[GetQiitaTagsActionKey.self] }
        set { self[GetQiitaTagsActionKey.self] = newValue }
    }
}

カスタムActionを使う

実際のViewでアクションを使います。
以下のコード例は、ビュー表示時にQiitaのタグ取得を行い、結果を表示します。
task部分でgetQiitaTagsアクションを実行し、結果を取得しています。
もしエラーが起きた場合はアラートを表示します。

struct ContentView: View {
    struct ContentViewError: LocalizedError {
        let error: Error
        
        var errorDescription: String? {
            error.localizedDescription
        }
    }
    
    @Environment(\.getQiitaTags) private var getQiitaTags
    @State private var tags: [Tag] = []
    @State private var contentViewError: ContentViewError?
    @State private var isError = false
    
    var body: some View {
        List(tags, id: \.self.id) { tag in
            VStack {
                Text(tag.id)
                Text(tag.followersCount, format: .number)
                Text(tag.itemsCount, format: .number)
            }
        }
        .alert(isPresented: $isError, error: contentViewError) {
            Button {
                
            } label: {
                Text("OK")
            }
        }
        .task {
            do {
                tags = try await getQiitaTags(page: 2, perPage: 20, sort: "count")
            } catch {
                contentViewError = .init(error: error)
                isError = true
            }
        }
    }
}

プレビュー

実際のgetQiitaTags処理はenvironmentで注入することができるので、アプリ実行時のenvironmentを変更せずに、プレビューのenvironmentを設定することで様々なシチュエーションを確認することができます。

GetQiitaTagsFromLocalAction

#Preview {
    ContentView()
        .environment(\.getQiitaTags, GetQiitaTagsFromLocalAction())
}

GetQiitaTagsFromLocalActionを使って固定データを表示する

GetQiitaTagsErrorAction

#Preview {
    ContentView()
        .environment(\.getQiitaTags, GetQiitaTagsErrorAction())
}

GetQiitaTagsErrorActionを使ってエラー時の画面を確認する

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?