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/UICollectionViewのreloadData(),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バグ」の多くが避けられます。