讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 11.10 延遲執行 >

11.10 延遲執行

你的代碼會以響應某個事件執行;不過反過來,代碼可能又會觸發新的事件或事件鏈。有時,這會導致一些惡果:應用可能會崩潰,或是Cocoa可能不會按照你的要求去做。為了解決這個問題,有時需要暫時跳出Cocoa本身的事件鏈,在繼續之前等待一切就緒。

這項技術叫作延遲執行。你告訴Cocoa去做某件事情,但不是現在,而是不久的將來,當一切就緒後再做。也許只需要非常短暫的延遲,甚至接近0秒鐘,只是為了讓Cocoa完成某些事情,如佈局界面。從技術上來說,這是在繼續執行代碼前讓當前的運行循環完成,並展開當前方法的整個調用棧。

在開發iOS應用時,使用延遲執行的頻率可能會比你想像得要多。隨著經驗的不斷累積,你會有一種感覺,知道何時應該使用延遲執行來解決問題。

在iOS編程中,使用延遲執行的主要方式是通過調用dispatch_after實現的。它接收一個塊(一個函數),表示指定的時間過後會發生什麼事情。不過調用dispatch_after有點複雜,特別是在Swift中,因為要進行大量的類型轉換;因此,我編寫了一個輔助函數,用於簡化以及調用dispatch_after:


func delay(delay:Double, closure:->) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue, closure)
}
  

該輔助函數非常重要,因此我將其粘貼到編寫的每個應用的AppDelegate類文件頂部。這樣使用起來就會方便很多!為了使用它,我需要調用delay並傳遞一個延遲時間(通常是個很短的時間,如0.1秒)和一個匿名函數,表示延遲過後要做什麼事情。注意,該匿名函數中所要做的事情在不久的將來才會做;你會將自己的代碼劃分為一行一行的執行序列。這樣,延遲執行就是其所在函數中的最後一個調用,並且不返回任何值。

如下示例來自於我所編寫的一個應用,用戶輕拍表中的一行,我的代碼會通過創建並展示一個新的視圖控制器進行響應:


override func tableView(tableView: UITableView,
    didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let t = TracksViewController(
            mediaItemCollection: self.albums[indexPath.row])
        self.navigationController!.pushViewController(
            t, animated: true)
}
  

但遺憾的是,對TracksViewController初始化器init(mediaItemCollection:)的調用需要一些時間,這樣應用就會停下來,同時表中的這一行會高亮顯示;雖然時間很短,但會讓用戶感到奇怪。為了通過某種動作來掩飾這種延遲,我在用戶輕拍表中某一行時,讓UITableViewCell子類顯示一個旋轉的活動指示器:


override func setSelected(selected: Bool, animated: Bool) {
    if selected {
        self.activityIndicator.startAnimating
    } else {
        self.activityIndicator.stopAnimating
    }
    super.setSelected(selected, animated: animated)
}
  

不過還有問題:這個旋轉的活動指示器不會出現,也不會旋轉。原因在於事件疊加到了一起。直到UITableView的委託方法tableView:didSelectRowAtIndexPath:完成時才會調用UITableViewCell的setSelected:animated:。不過,我們想要掩蓋的延遲是在tableView:didSelectRowAtIndexPath:過程中的,整個問題就在於它完成的沒那麼快。

這時,延遲執行就派上用場了!我重寫了tableView:didSelectRowAtIndexPath:,使之能夠立刻完成,這樣觸發setSelected:animated:就會導致活動指示器立刻出現並開始旋轉,稍後,我會使用延遲執行來調用init(mediaItemCollection:),就在界面恢復原狀之時:


override func tableView(tableView: UITableView,
    didSelectRowAtIndexPath indexPath: NSIndexPath) {
        delay(0.1) { // let spinner start spinning
            let t = TracksViewController(
                mediaItemCollection: self.albums[indexPath.row])
            self.navigationController!.pushViewController(
                t, animated: true)
        }
}