iOS 原生 App 是怎麼 deselectRow 的

四娘發表於2018-12-29

這兩天偶然發現系統設定裡 tableView deselectRow 的時機和效果都很特別,正常情況下我們的 deselect 操作都會在 didSelect 代理方法裡執行,抑或者是更加細緻一點,在 viewDidAppear 裡完成。

但 iOS 原生的 App 說不,我還可以做得更好,這是系統設定裡的效果:

iOS 原生 App 是怎麼 deselectRow 的

側滑返回時,deselect 動畫會隨著滑動手勢的進度而改變,搜了一下,國內似乎沒有太多相關的文章,並且我手頭常用的幾款軟體都做到沒有類似的效果。

搜了一下之後,發現國外的記錄也很少,只有三篇文章記錄了這個互動,其中寫的比較詳細的是這篇 The Hit List Diary #17 – clearsSelectionOnViewWillAppear

轉場動畫的抽象 transitionCoordinator

這個互動其實是通過 UIViewControllertransitionCoordinator 屬性實現的,它的型別是 UIViewControllerTransitionCoordinator

簡單來說,它可以幫助我們在轉場動畫里加入一些自定義的動畫,自定義動畫的進度和生命週期會與轉場動畫保持一致,使用它可以達到更加自然和一致的轉場效果,例如 push 動畫裡 navigationBar 背景顏色的變化,它提供了這幾個方法供我們註冊動畫生命週期的回撥:

protocol UIViewControllerTransitionCoordinator {
    func animate(
        alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, 
        completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil
    ) -> Bool
    
    func animateAlongsideTransition(
        in view: UIView?,
        animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, 
        completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil
    ) -> Bool
    
    func notifyWhenInteractionChanges(
        _ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void
    )
}
複製程式碼

推薦大家去看一下 UIViewControllerTransitionCoordinator 這個協議的文件,這裡摘錄一段我覺得比較有趣的描述:

Using the transition coordinator to handle view hierarchy animations is preferred over making those same changes in the viewWillAppear(_:) or similar methods of your view controllers. The blocks you register with the methods of this protocol are guaranteed to execute at the same time as the transition animations. More importantly, the transition coordinator provides important information about the state of the transition, such as whether it was cancelled, to your animation blocks through the UIViewControllerTransitionCoordinatorContext object.

比起 viewWillAppear 和其它相似的 ViewController 生命週期函式,我們更加推薦使用 transitionCoordinator 處理檢視層級的動畫。你註冊的函式可以保證與轉場動畫同時執行。更重要的是,transitionCoordinator 通過 UIViewControllerTransitionCoordinatorContext 協議提供了轉場動畫的狀態等重要資訊,例如動畫是否已被取消等。

我由於最近業務的原因,第一個想起的就是 navigationBar,像是 barTintColor 這種屬性就可以使用 transitionCoordinator 做到更加自然的動畫轉場。

實現與封裝

我看了別人的文章並且嘗試其它集中方式之後,感覺 transitionCoordinator 獲取的最佳時機應該是 viewWillAppear,實現的邏輯大概是這樣:

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

    // 判斷是否有被選中的 Row
    if let selectedIndexPath = tableView.indexPathForSelectedRow {
        // 判斷是否有 transitionCoordinator
        if let coordinator = transitionCoordinator {
            // 有的情況下,通過 coordinator 註冊 animation block
            coordinator.animate(
                alongsideTransition: { _ in
                    self.tableView.deselectRow(at: selectedIndexPath, animated: true)
                },
                completion: { context in
                    // 如果轉場動畫被取消了,則需要讓 tableView 回到被選中的狀態
                    guard context.isCancelled else { return }
                    self.tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)
                }
            )
        } else {
            // 沒有的情況下直接 deselect 
            tableView.deselectRow(at: selectedIndexPath, animated: animated)
        }
    }
}
複製程式碼

如果把 transitionCoordinator 單純地看成是一個動畫抽象(拋開轉場),我們希望跟隨動畫完成的操作就是 deselect,那麼就可以更進一步地把這個 deselect 的操作封裝到 UITableView 的 extension 裡:

extension UITableView {

    public func deselectRowIfNeeded(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) {
        guard let selectedIndexPath = selectRowAtIndexPath else { return }
    
        guard let coordinator = transitionCoordinator else {
            self.deselectRow(at: selectedIndexPath, animated: animated)
            return
        }

        coordinator.animate(
            alongsideTransition: { _ in
                self.deselectRow(at: selectedIndexPath, animated: true)
            },
            completion: { context in
                guard context.isCancelled else { return }
                self.selectRow(at: selectedIndexPath, animated: false, scrollPosition: .none)
            }
        )
    }
}
複製程式碼

接著只要在 viewWillAppear 裡呼叫即可:

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

    tableView.deselectRowIfNeeded(with: transitionCoordinator, animated: true)
}
複製程式碼

如果大家在專案裡封裝了自己的 TableViewController 並且規範使用的話,那要加入這個效果就很簡單了。

結語

這是完整的示例

參考連結:

覺得文章還不錯的話可以關注一下我的部落格

相關文章