メインスレッドチェックの厳格化
元々、「UIの更新はメインスレッドで行うべし」というお約束はありましたが、Xcode11より、
- iOS13ではサブスレッドからUIコンポーネントを変更するとクラッシュするようになりました。
- サブスレッドからUIコンポーネントに「アクセスしただけ」で警告されるようになりました。
- ビルド時の静的チェックではなく、ランタイムで該当コードを通ると紫にハイライトされます
NotificationCenterのスレッド
- Notificationのpost元がメインスレッドであれば、addObserverで設定した関数(以下observer関数)はメインスレッドで処理されます。
- Notificationのpost元がサブスレッドであれば、observer関数はサブスレッドで処理されます。
私のつまづき経緯
- 既存のNotificationをpostするコードをサブスレッド処理の中に新たに追加
- その既存Notificationは今までメインスレッドからしかpostしていなかった
- 複数のobserver関数のうち一つが、サブスレッドでの動作を想定していないコードになっておりクラッシュが発生
サンプルコード
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(notify(_:)),
name: Notification.Name("Notification"),
object: nil)
}
@IBAction func tappedButton(_ sender: Any) {
// (A)
NotificationCenter.default.post(name: Notification.Name("Notification"),
object: nil,
userInfo: ["result": "This is Main Thread"])
request()
}
func request() {
guard let url = URL(string: "https://www.ibm.com/jp-ja") else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
// (B)
NotificationCenter.default.post(name: Notification.Name("Notification"),
object: nil,
userInfo: ["result": "This is Sub Thread"])
}
task.resume()
}
@objc func notify(_ notification: Notification) {
// (A)のpostを受け取った場合は問題ないが、(B)のpostを受け取ると警告が出る
print("text: \(label.text ?? "")")
if let userInfo = notification.userInfo, let str = userInfo["result"] as? String {
// (A)のpostを受け取った場合は問題ないが、(B)のpostを受け取るとクラッシュする
self.label.text = str
}
}
}
横展開調査が面倒…
- NotificationCenterによって通知する設計は処理の流れが追いづらく、影響範囲や発生条件を特定するのが大変
- (UITestを書かない限り)手作業で動かしてみないと検証できない
再発防止策
observer関数内でUIコンポーネントにアクセスする時は、とにかく DispatchQueue.main.async
で囲むことを徹底!
(精神論…)