iOS 原生 App 是怎麼 deselectRow 的

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

但 iOS 原生的 App 說不,我還能夠作得更好,這是系統設置裏的效果:git

側滑返回時,deselect 動畫會隨着滑動手勢的進度而改變,搜了一下,國內彷佛沒有太多相關的文章,而且我手頭經常使用的幾款軟件都作到沒有相似的效果。github

搜了一下以後,發現國外的記錄也不多,只有三篇文章記錄了這個交互,其中寫的比較詳細的是這篇 The Hit List Diary #17 – clearsSelectionOnViewWillAppearswift

轉場動畫的抽象 transitionCoordinator

這個交互實際上是經過 UIViewControllertransitionCoordinator 屬性實現的,它的類型是 UIViewControllerTransitionCoordinatorapi

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

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 這個協議的文檔,這裏摘錄一段我以爲比較有趣的描述:iview

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.ide

比起 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 而且規範使用的話,那要加入這個效果就很簡單了。

結語

這是完整的示例

參考連接:

以爲文章還不錯的話能夠關注一下個人博客

相關文章
相關標籤/搜索