3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIAdvent Calendar 2024

Day 7

[SwiftUI] 複数回同じmodifierを記述するパターンをまとめるmodifier

Last updated at Posted at 2024-12-10

背景

SwiftUIでアプリを作っていると同じmodifierを複数回記述するパターンに遭遇する。問題設定としては

  • modifierの引数は同じ型で違う値
  • クロージャーは同じ

これを簡潔に書きたい。

enum MyError: Error, Equatable {
    case network
    case badRequest
    case notFound
}

// ある画面で3つのアクションがあり、それぞれからエラーが帰る可能性がある
struct ContentView: View {
    @State var error1: MyError?
    @State var error2: MyError?
    @State var error3: MyError?

    var body: some View {
        Text("app")
            .onChange(of: error1, perform: onError)
            .onChange(of: error2, perform: onError)
            .onChange(of: error3, perform: onError)
    }

    func onError(error: MyError?) {
        guard let error else { return }
        /// ...
    }
}

また、(要件にもよるが) onChange(of: error1 ?? error2 ?? error3) のような手法だと、例えばerror1error2がほぼ同時に発生した時に片方しか呼ばれないので不適とする。

struct ContentView: View {
    @State var error1: MyError?
    @State var error2: MyError?
    @State var error3: MyError?

    var body: some View {
        Text("app")
            .onChange(of: error1 ?? error2 ?? error3, perform: onError)
    }
}

解決策: forEach

あるmodifierを配列の要素分繰り返すmodifier。AnyViewを使っているので多少オーバヘッドはありそうだが、意味的には記事冒頭のコードと等価のはず。

extension View {
    func eraseToAnyView() -> AnyView {
        AnyView(self)
    }

    func forEach<T, ModifiedView: View>(_ elements: [T], modify: (AnyView, T) -> ModifiedView) -> some View {
        elements.reduce(self.eraseToAnyView()) { view, element in
            modify(view, element).eraseToAnyView()
        }
    }
}

struct ContentView: View {
    @State var error1: MyError?
    @State var error2: MyError?
    @State var error3: MyError?

    var body: some View {
        Text("app")
            .forEach([error1, error2, error3]) { view, element in
                view.onChange(of: element, perform: onError)
            }
    }
}

おまけ

実際onChangeが頻出パターンなのでonChange専用のmodifierを作るのが丸いかもしれない。(onChange以外で使えそうなケースがあったら教えてください)

extension View {
    func onChange<T: Equatable>(each elements: [T], perform: @escaping (T) -> Void) -> some View {
        self.forEach(elements) { view, element in
            view.onChange(of: element) { element in
                perform(element)
            }
        }
    }
}

struct ContentView: View {
    @State var error1: MyError?
    @State var error2: MyError?
    @State var error3: MyError?

    var body: some View {
        Text("app")
            .onChange(each: [error1, error2, error3], perform: onError)
    }
}

そしてonChange(each:)を作るだけなら配列にonChangeを行うアプローチでもいけそう。ただし使っているonChangeがiOS17+。
(その他の違いはパフォーマンスだけだと思うが、どちらのパフォーマンスが良いかはわからない)

extension View {
    func onChange<T: Equatable>(each elements: [T], perform: @escaping (T) -> Void) -> some View {
        self.onChange(of: elements) { before, after in
            after.changedElements(from: before).forEach(perform)
        }
    }
}

private extension Array where Element: Equatable {
    func changedElements(from before: [Element]) -> [Element] {
        zip(before, self).filter(!=).map(\.1)
    }
}
3
2
2

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
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?