ひとしれずひっそり

主にソフトに関することをメモしていきます。過程をそのまま書いていたりするので間違いが含まれます。鵜呑みしない様に。

Background処理ではDispatchQueue.mainに気をつけろ

iOSアプリでバックグラウンドに移った時に区切りのいいところまで実行させたい時にバッググラウントでの処理を要求する必要がある。

こんな感じ

    private var taskIdentifier: UIBackgroundTaskIdentifier!

    func becomeBackground() {
        taskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
            // 与えられた時間内に終了しない場合の強制終了
            UIApplication.shared.endBackgroundTask(self.taskIdentifier)
        })

        // ここで区切りのいいところまで処理させる。
        
        // 終了したらバックグラウンドタスクを終了する
        UIApplication.shared.endBackgroundTask(taskIdentifier)
    }

例えば、カウンター処理でバックグラウンドに入ったらカウンターを停止させて終了したい。

カウンターはpublisherが処理していて停止指令をだしてから停止状態になるまで待たせる。

        // ここで区切りのいいところまで処理させる。
        publisher!.stop()
        while publisher!.stopped == false {
            Thread.sleep(forTimeInterval: 1)
        }

しかしpublisher!.stoppedはViewでも使用するのでDispatchQueue.main.async()を使用してmain threadで更新する様にしている。
BackgroundTask処理中はmain threadに処理が移らないため、stoppedにならずバックグラウンド処理が終了しない。

class Publisher: ObservableObject {
    
    @Published var run = false
    @Published var stopped = true
    @Published var count: UInt32 = 0
    
    private var thread: Thread!
        
    init() {
        thread = Thread(block: {
            DispatchQueue.main.async {
                self.stopped = !self.run
            }
            while(true) {
                while self.run {
                    Thread.sleep(forTimeInterval: 1)
                    DispatchQueue.main.async {
                        self.count += 1
                    }
                }
                DispatchQueue.main.async {
                    self.stopped = true
                }
                while !self.run {
                    Thread.sleep(forTimeInterval: 1)
                }
                DispatchQueue.main.async {
                    self.stopped = false
                }
            }
        })
        thread.start()
    }
    
    func start() {
        run = true
    }
    
    func stop() {
        run = false
    }
    
}

iOSのBackgrounTaskでデッドロック · GitHub

BackgroundTaskで扱う状態変数はPublishedにしない変数を使用しなければならない。
しかしView更新用のPublishedなのも欲しいので二重構造にする必要がある。

Publisher

    // view更新用
    @Published var stopped = true
    
    // 内部で扱う
    var stopped_rawValue = true {
        didSet {
            DispatchQueue.main.async {
                self.stopped = self.stopped_rawValue
            }
        }
    }
    
            while(true) {
                while self.run {
                    Thread.sleep(forTimeInterval: 1)
                    DispatchQueue.main.async {
                        self.count += 1
                    }
                }
                self.stopped_rawValue = true
                while !self.run {
                    Thread.sleep(forTimeInterval: 1)
                }
                self.stopped_rawValue = false
            }

Controller

        while publisher!.stopped_rawValue == false {
            Thread.sleep(forTimeInterval: 1)
        }

これでバックグラウンドに移ると一旦カウントが停止し、フォアグラウンドに戻ると再開できる様になる。

iOSのBackgrounTaskでデッドロック · GitHub