web-dev-qa-db-ja.com

Swift Timer.scheduledTimer()が機能しない

Swiftアプリに2つのビューがあります。以下のようにセグエを実行しています。

ViewController.Swift -----------------> GameViewController.Swift

GameViewControllerをロードすると、値の配列もViewController.SwiftからGameViewController.Swiftに渡されます。

GameViewController.Swiftでタイマーを初期化する必要があります

タイマーを初期化してメソッドを呼び出そうとしましたが、機能しません。

以下は私のコードスニペットです。

ViewController.Swift

func signIn(difficultyLvl:String){
    let username = usernameTxt.text
    let password = passwordTxt.text

    let url = URL(string: "http://192.168.1.106/speed/scoreBoardController.php?username="+username!+"&password="+password!+"&action=SIGNIN")

    let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in
        let isPassed = String(data: data!, encoding:.utf8)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

        var gameViewControllerParams = [Int: [String: String]]()
        gameViewControllerParams[0] = ["userId" : isPassed!]
        gameViewControllerParams[1] = ["difficultyLvl" : difficultyLvl]

        if(isPassed != "null"){
            self.performSegue(withIdentifier: "gotoGame", sender: gameViewControllerParams)
        }
    }

    task.resume()
}

GameViewController.Swift

class GameViewController: UIViewController {

    var gameViewControllerParams = [Int: [String: String]]()

    override func viewDidLoad() {
        super.viewDidLoad()

        let _ = Timer.scheduledTimer(timeInterval: 1.0, target:self, selector: #selector(self.setCalculationLs), userInfo:nil,repeats: true)
    }

    func setCalculationLs(){
        print("Timing")
    }

}
8

タイマーはバックグラウンドキューでは機能しません(実行ループを作成したり、既存の実行ループで手動でスケジュールしたりする手先の早業がなければ)。ただし、とにかく、メインキュー以外からUIの更新を開始しないでください。

つまり、(バックグラウンドキューで実行される)performSegue完了クロージャからURLSessionを呼び出しているので、実際にはバックグラウンドキューからもviewDidLoadを実行しています。したがって、タイマーをスケジュールする試みは失敗しています。これを回避するには、performSegueコードをメインキューに手動でディスパッチする必要があります。

let task = URLSession.shared.dataTask(with: url!) { data, response, error in
    ...

    if isPassed != "null" {
        DispatchQueue.main.async {
            self.performSegue(withIdentifier: "gotoGame", sender: ...)
        }
    }
}

一部のコードがメインキューで実行されているかどうかわからない場合は、 ドキュメント を参照してください。または、ディスパッチの前提条件を使用できます。

dispatchPrecondition(condition: .onQueue(.main))

そうすれば、バックグラウンドキューから誤ってコードを呼び出した場合に、(デバッグビルドで)アプリを停止します。


現在の問題とは関係ありませんが、余談ですが、タイマーとビューコントローラー間の強い参照サイクルを回避するために、通常はタイマーへの参照を保持して、ビューが消えたときにinvalidateできるようにします。 (たとえば、viewDidAppearでタイマーを作成し、viewDidDisappearでタイマーを削除します)。それ以外の場合は、却下された後もGameViewControllerを保持することになります。例:

class GameViewController: UIViewController {

    weak var timer: Timer?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        timer = Timer.scheduledTimer(timeInterval: 1.0, target:self, selector: #selector(setCalculationLs(_:)), userInfo: nil, repeats: true)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        timer?.invalidate()
    }

    @objc func setCalculationLs(_ timer: Timer) {
        print("Tick")
    }
}

または、iOS 10以降では、weakselfを参照し、invalidatedeinitにあるブロックベースのバリアントを使用できます。

class GameViewController: UIViewController {

    weak var timer: Timer?

    override func viewDidLoad() {
        super.viewDidLoad()

        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
            self?.setCalculationLs()
        }
    }

    deinit {
        timer?.invalidate()
    }

    func setCalculationLs() {
        print("Tick")
    }

}
17
Rob