0
1

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で「必ずMain Threadで呼ぶべきもの」まとめ

Posted at

Swiftで「必ずMain Threadで呼ぶべきもの」まとめ


なぜ Main Thread が大事なのか

iOSアプリでは、

  • UIの更新
  • UIKit / AppKit オブジェクトの操作
  • 一部のフレームワークのコールバック

メインスレッド(メインキュー)で実行することが前提 になっています。
理由はシンプルで、

  • UIKit はスレッドセーフではない
  • 複数スレッドから UI を触るとクラッシュや表示崩れの原因になる

からです。

Swift Concurrency(async/await@MainActor)が入ってからも、この考え方は変わりません。
「UIは必ず Main」 これだけは頭に固定でOKです。


Main Thread で実行すべき代表的なもの

1. UIKit(UI)の更新全般

  • UIView / UILabel / UIButton などのプロパティ変更
  • addSubview(_:) / removeFromSuperview()
  • UITableView / UICollectionViewreloadData(), performBatchUpdates
  • レイアウト制約(Auto Layout)の追加・更新
// ❌ これはバックグラウンドキューから呼んだら危険
self.titleLabel.text = "読み込み完了"

// ✅ 正しくは Main Thread にディスパッチ
DispatchQueue.main.async {
    self.titleLabel.text = "読み込み完了"
    self.view.layoutIfNeeded()
}

2. 画面遷移・アラート表示

  • UINavigationController.pushViewController(_:animated:)
  • present(_:animated:completion:)
  • UIAlertController の表示
func showErrorAlert(message: String) {
    let show = {
        let alert = UIAlertController(
            title: "エラー",
            message: message,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        self.present(alert, animated: true)
    }

    // 念のためメインスレッドにまとめる
    if Thread.isMainThread {
        show()
    } else {
        DispatchQueue.main.async(execute: show)
    }
}

3. Swift Concurrency と @MainActor

Swift Concurrency を使うなら、UIを扱うクラスやメソッドに @MainActor を付ける のが安全です。

@MainActor
final class ProfileViewModel: ObservableObject {
    @Published var name: String = ""

    func load() async {
        // ネットワーク処理などは別スレッドで動くが…
        let fetched = await fetchUserName()

        // @MainActor のおかげで、ここは必ず Main Thread
        self.name = fetched
    }

    private func fetchUserName() async -> String {
        // ネットワークやDBアクセスなど(バックグラウンド想定)
        "サンストライプ"
    }
}
  • @MainActor が付いているクラス / メソッド内は自動的に「Main Thread で実行される」保証が付きます。
  • SwiftUI の View 更新も基本 @MainActor です。

4. Notification / Delegate からの UI 更新

NotificationCenter や デリゲートコールバックは、
どのスレッドで飛んでくるかが API によって違う ので、UI更新時は Main に寄せるのが安全です。

NotificationCenter.default.addObserver(
    forName: .someBackgroundTaskDidFinish,
    object: nil,
    queue: nil
) { [weak self] notification in
    // ここがどのスレッドか分からないので…
    DispatchQueue.main.async {
        self?.statusLabel.text = "完了しました!"
    }
}

5. CoreData の「メインコンテキスト」

CoreData には「メインキュー用(mainQueueConcurrencyType)」の NSManagedObjectContext がよく使われます。
このコンテキストを使う場合も、Main Thread で扱う のが原則です。

let context = persistentContainer.viewContext // mainQueueConcurrencyType

func updateUI() {
    // ✅ Main Thread ならOK
    let items = try? context.fetch(MyEntity.fetchRequest())
    // items をもとに UI 更新
}

逆に Main Thread に乗せない方がいいもの

  • 重い計算処理(画像加工、圧縮など)
  • 大量の JSON パース
  • ネットワークリクエスト
  • データベースの重いクエリ

これらは バックグラウンドキューで行い、結果だけ Main Thread に返して UI を更新する のが定石です。

func loadData() {
    // バックグラウンドで重い処理
    DispatchQueue.global(qos: .userInitiated).async {
        let result = self.fetchDataFromNetwork()

        // 結果を UI に反映するときだけ Main
        DispatchQueue.main.async {
            self.items = result
            self.tableView.reloadData()
        }
    }
}

サンプルコード①:API から取得してラベルに反映

final class UserViewController: UIViewController {

    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var activityIndicator: UIActivityIndicatorView!

    override func viewDidLoad() {
        super.viewDidLoad()
        loadUser()
    }

    private func loadUser() {
        // UI開始は Main
        activityIndicator.startAnimating()

        DispatchQueue.global(qos: .userInitiated).async { [weak self] in
            guard let self else { return }

            // 🔹 ここはバックグラウンドでOK
            let name = self.fetchUserName()

            // 🔹 UI更新は必ずMain
            DispatchQueue.main.async {
                self.activityIndicator.stopAnimating()
                self.nameLabel.text = name
            }
        }
    }

    private func fetchUserName() -> String {
        // APIアクセスやDBアクセスを想定
        Thread.sleep(forTimeInterval: 1.0) // ダミーの待ち時間
        return "Sunstripe User"
    }
}

サンプルコード②:Swift Concurrency + @MainActor でシンプルに書く

@MainActor
final class UserViewController: UIViewController {

    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var activityIndicator: UIActivityIndicatorView!

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            await loadUser()
        }
    }

    private func fetchUserName() async throws -> String {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return "Sunstripe User"
    }

    // @MainActor クラス内なので、このメソッドも Main
    private func loadUser() async {
        activityIndicator.startAnimating()

        do {
            // 🔹 ここでバックグラウンドに切り替わるのは
            //   Swift Concurrency がよしなにやってくれる
            let name = try await fetchUserName()

            // 🔹 戻ってきたら必ず Main
            nameLabel.text = name
        } catch {
            nameLabel.text = "エラーが発生しました"
        }

        activityIndicator.stopAnimating()
    }
}

まとめ

  • UI系はとにかく Main Thread(Main Queue)
  • 画面遷移・アラート・レイアウトも Main
  • Swift Concurrency を使うなら @MainActor を積極的に活用
  • 重い処理はバックグラウンドで行い、結果だけ Main で UI 反映

このあたりを記事の基本ルールとして押さえておけば、
「なんかよく分からないクラッシュ」「たまに起きるUIバグ」の多くが避けられます。


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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?