你的代碼會以響應某個事件執行;不過反過來,代碼可能又會觸發新的事件或事件鏈。有時,這會導致一些惡果:應用可能會崩潰,或是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) } }