kota1021
@kota1021 (松本 幸太郎)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

同期的な重い処理をdispatchし、その結果をUIに反映する方法

解決したいこと

Swift Concurrencyの下記の処理はどこが良くないか知りたい

下記コードではModelが保持している[Word]を条件に合わせてfilterし、その結果をPublished propertyに代入しています。
Modelが保持している[Word]は要素数が1万を超えるため、main threadでfilterした場合UIがガクガクになってしまいます。そこで他のthreadで同時並行的に処理をしたいのですが、Arrayのfilterという同期的な処理をasync awaitでうまく取得する方法がわからず苦戦しています。

助言をいただけると非常に助かります!
よろしくお願いします。

WordsProvider.swift
@Published var words:[Word] = []

@MainActor //UIに反映されるので@MainActor
public func setWords() async {
    self.words = await filteredWords()
}
@MainActor //dispatchするために@MainActorをコメントアウトするとUIに反映されない
private func filteredWords() async -> [Word]{
    async let words = Model.shared.words.filter {...} //ここが重いからdispatchしたい
    return await words
}

0

1Answer

ご提示の filteredWords() の中で行っている filter(_:) を非同期で処理したいという質問であると理解しました。filteredWords() 内の実装を、たとえば TaskGroup を用いて以下のようにすることができます。

private func filteredWords() async -> [Word] {
    await withTaskGroup(of: ([Word].Index, Word)?.self) { group in
        zip(Model.shared.words.indices, Model.shared.words).forEach { (index, word) in
            group.addTask {
                let isIncluded = ... // ここに filter の isIncluded クロージャで行っていた処理を書く(引数は `word` を使う)
                return isIncluded ? (index, word) : nil
            }
        }
        
        return await group
            .reduce(into: [Int : Word]()) {
                guard let element = $1 else { return }
                $0[element.0] = element.1
            }
            .sorted(by: <)
            .map(\.value)
    }
}

上記の isIncluded に当てはめる処理は TaskGroup.addTask(priority:operation:)operation 内にあるため、async/await を用いることもできます。

また、上記では zip(_:_:) を用いましたが、Swift Algorithms の indexed() を用いることもできます


以上が回答となりますが、並列に filter の処理を行ってからそれを同期な順番と一致するように Array に戻しているため、ループ(処理)の回数自体はものすごく増えていることにご注意ください。

1Like

Comments

  1. @kota1021

    Questioner

    ご回答いただきありがとうございます。
    とても嬉しいです。
    ただ、私の質問の仕方が不明瞭だったと思うのですが、今回の質問の意図としては、filterのそれぞれの要素のisIncludedを並行に実行するということではなく、UIの更新をブロックしない形でArrayをfilterしたい、というものでした。
    そこで、より明瞭な質問をさせていただくために、コードを見直し、再度以下のようにfilteredWords()から@MainActorを外すと、今回は期待通りの挙動をしてくれました。おそらく質問を投稿した時点においてなんらかの見落としがあったのだと思います。
    しかし、いただいたご回答のおかげでwithTaskGroup{}の使い方がより一層理解できました。ありがとうございます!

    @Published var words:[Word] = []
    
    @MainActor
    public func setWords() async {
            self.words = await filteredWords()
    }
    
    private func filteredWords() async -> [Word]{
        print("Thread.isMainThread: \(Thread.isMainThread)")//false、だがきちんとUIには反映された。
        async let words = Model.shared.words.filter {...}
        
        return await words
    }
    

Your answer might help someone💌