1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【物語で学ぶ Swift Concurrency】「MainActor × nonisolated」の最強コンビでメインスレッドをブロックせずに快適なUI更新!

Last updated at Posted at 2025-02-22

1.MainActor って何?どうして必要なの?

生徒「先生、Swift のコードで @MainActor っていうのを見かけたんですけど、これは何なんでしょうか?」

先生MainActor は、アプリの画面(UI)を操作するコードが“メインスレッド”で安全に実行されるようにする仕組みなんだよ。Swift Concurrency では、データの競合を防ぐためにアクター(Actor)と呼ばれる単位で並行性を管理するでしょ? その中でも MainActor は、内部的には DispatchQueue.main を使いながら、UI 操作が必ずメインスレッドで実行されるように守ってくれる特別なアクターなんだ。」

生徒「なるほど。UI を更新するときに、メインスレッド以外で操作したら不具合が起こるって聞いたことがあります。MainActor を使うと、そういう問題を防げるんですね!」

先生「そう。UI 系のコードを安全に書くためには、@MainActor を使うのが基本になるね。」


2.MainActor の適用方法

生徒「でも具体的に MainActor はどうやって使うんですか? たとえばクラスやメソッド単位に付けるとか、いろいろ方法があるって聞きました。」

先生「うん、MainActor を“どこに”付けるかがポイントだね。たとえば、クラス全体を @MainActor にしてしまうと、そのクラスのプロパティやメソッドはすべてメインスレッド上で動くように“暗黙的”に指定される。もし一部のメソッドだけメインスレッドで動かしたいなら、プロパティやメソッドごとに @MainActor を付けられるよ。」

2-1. 型全体へ @MainActor を適用する

@MainActor
class UserDataSource {
    // ここで宣言されるプロパティ・メソッドは
    // メインスレッド(MainActor)で動く
    var user: String = ""

    func updateUser() {
        // UI 更新などを安全に行える
    }

    // ただし nonisolated を使うと、MainActor から分離可能
    nonisolated func sendLogs() {
        // ここは MainActor じゃなく呼び出せる
    }
}

生徒@MainActor をクラスの頭につけると、その中のプロパティやメソッドは全部メインスレッド実行の対象になるんですね。でも nonisolated で解除って何ですか?」

先生nonisolated は『このメソッドはアクター(今回だと MainActor)に隔離されないで呼び出せる』という意味だよ。メインスレッド以外でも呼び出す必要がある処理の場合、こう書けば “この部分だけ” アクターの縛りを外すことができるんだ。」


3.プロパティやメソッド個別に @MainActor を付ける

先生「型全体を @MainActor にしなくても、プロパティやメソッド単位で付けることもできる。たとえばこんな感じ。」

struct Mypage {
    @MainActor var info: String = ""

    @MainActor
    func updateInfo() {
        // ここは MainActor で動作
    }

    func sendLogs() {
        // ここは普通の関数で、MainActor は適用されない
    }
}

生徒「ふむふむ。info プロパティと updateInfo() にだけ @MainActor 付いてますね。これなら使い分けられそう。」


4.MainActor で UI データを更新してみる

生徒「実際に UI データを更新するときはどうするんですか?」

先生「代表的なのは、ViewModel と呼ばれるクラスを丸ごと @MainActor にして、その中で @Published var text みたいなプロパティを持って、UI に表示する文字列を管理するやり方。非同期でサーバーからデータ取ってきて、それをメインスレッドでUIに反映する流れが分かりやすいんだ。」

@MainActor
final class ViewModel: ObservableObject {
    @Published private(set) var text: String = ""

    // サーバー通信はメインスレッドでやらなくていいので nonisolated にする
    nonisolated func fetchUser() async {
        // サーバー問い合わせの通信を想定
        // ここは await とか時間のかかる処理を非同期でやる
        // もし中で text を更新しようとするとエラーになる

        // 結局、この非同期メソッドの中では text に代入ができない
        // 代わりに「値を返して、メインスレッドに戻ってから代入する」必要がある
    }

    func didTapButton() {
        Task {
            text = "" 
            // いったんクリアして

            // 別メソッドで値を取得
            text = await fetchUser()
        }
    }

    private func waitOneSecond(with string: String) async -> String {
        try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
        return string
    }
}

生徒「あ! nonisolated func fetchUser() の中で text を直接書き換えようとするとエラーが出るんですね。『Property 'text' isolated to global actor 'MainActor' cannot be mutated from a non-isolated context』ってやつ。」

先生「そうそう。@MainActor で保護されてる text を、nonisolated な文脈からそのまま書き換えるとデータ競合(データレース)が起こるかもしれないから、コンパイラがエラーで止めてくれるんだよ。だから fetchUser() はサーバーから名前だけ返して、実際に text に代入するのはメインスレッドに戻ってからにする、というやり方が必要になる。」


5.「値を返す」形に修正してみる

生徒「たしかに fetchUser() の中で text を更新すると怒られるなら、値を返す関数にすればいいんですね。」

先生「そう。具体的にはこうすればコンパイルが通るようになる。」

@MainActor
final class ViewModel: ObservableObject {
    @Published private(set) var text: String = ""

    // nonisolated だが、String を返す
    nonisolated func fetchUser() async -> String {
        return await waitOneSecond(with: "タカシ")
    }

    func didTapButton() {
        Task {
            text = ""
            text = await fetchUser()
        }
    }

    private func waitOneSecond(with string: String) async -> String {
        try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
        return string
    }
}

先生「これなら fetchUser() が文字列を返すだけだから、サーバー通信部分はメインスレッドに縛られないで実行できるし、didTapButton() の中で text をちゃんとメインアクターコンテキストにいる状態で更新できるよ。」

生徒「なるほど! つまり、データ競合を起こさないために、アクターの外側(nonisolated なとこ)からアクターの中のプロパティに変更を加えないようにする、っていう流れですね。」

先生「そういうこと。Swift Concurrency はコンパイラレベルでチェックが入るから、ルールに反したらビルドエラーで教えてくれる。ちゃんとルールに沿えば UI 更新と非同期処理をきれいに分けられるわけだ。」


6.MainActornonisolated を組み合わせるメリット

生徒「結局これってどういうメリットがあるんでしょうか?」

先生「たとえば、UI に表示する部分は @MainActor で守ってあるから、画面の更新を安全に並列処理できる。だけどサーバー通信みたいにメインスレッドじゃなくていい部分は nonisolated で外に逃がすことで、メインスレッドをブロックせずにすむ。こうするとアプリがカクつきにくくなるし、スムーズに動くようになるよ。」

生徒「なるほど。UI が止まるとユーザ体験が悪いですもんね。そういった点でも MainActornonisolated の使い分けは重要なんですね。」


7.まとめ

生徒「今日習ったことを整理すると――」

  1. @MainActor とは?

    • UI 操作コードを安全にメインスレッドで実行する仕組み。
    • クラスや構造体をまるごと @MainActor にするか、必要なプロパティやメソッドにだけ @MainActor を付ける方法がある。
  2. nonisolated の意味

    • そのメソッドやプロパティが「アクターに隔離されずにアクセス可能」になる。
    • ただし @MainActor で保護されているプロパティを書き換えようとすると、コンパイルエラーになる。
  3. UI 更新の流れ

    • ViewModel@MainActor で保護し、UI 表示に使うプロパティ(例:text)を用意する。
    • サーバーなど時間のかかる処理は nonisolated なメソッドで行い、アクター外で値を受け取る。
    • 実際の UI プロパティ更新は MainActor 上に戻ってから行う。
  4. コンパイラエラーによる保護

    • MainActor に属するプロパティを誤って他所から書き換えようとすると、コンパイルエラーで教えてくれる。
    • スレッドセーフティを言語レベルでサポートしている。
  5. メリット

    • メインスレッドをブロックせずに UI を更新できる。
    • データ競合を防ぎながら、わかりやすいコードが書ける。

生徒「ありがとうございます。さっそく自分のアプリでも実践してみますね!」

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?