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())
}
GetQiitaTagsErrorAction
#Preview {
ContentView()
.environment(\.getQiitaTags, GetQiitaTagsErrorAction())
}