你也可以自訂搖動還原 (Shake to Undo) 的功能?一起來拆解並實作吧!


在 iOS 上編輯內容的時候,如果要還原或重做步驟的話,通常可以透過搖動來呼叫出一個還原的警告:

搖動還原-1

這個搖動還原 (Shake to Undo) 功能在 UITextView 或者 UITextField 等文字編輯的 view 上是內建的,但大多數其他的 view 都沒有預設實作。還好,UIKit 其實已經幫我們做好了從動作偵測到顯示警告的部分,我們只需要提供第一響應者 (First Responder),並使用它的 undoManager 就可以了。

編者按:由於這篇文章是前兩篇的延伸教程,所以在看這篇文章之前,請先參考 UIAlertController 教程First Responder 教程

使用系統提供的搖動還原

第一步,就是去啟用 UIApplication 單例的 applicationSupportsShakeToEdit 功能:

接著,找到想要被執行還原/重做的響應者,也就是 UIResponder 物件,並使用它的 undoManager 來做還原管理:

第三個步驟是,我們必須要將該響應者設成第一響應者:

如此一來,就可以在 ViewController 的場景使用搖動還原的功能了。

破解運作原理

要實作系統提供的搖動還原功能就是這麼簡單。然而,你有沒有曾經好奇過,為什麼一定要使 ViewController 成為第一響應者,搖動還原才會有作用呢?答案可能跟你想的不一樣。

成為第一響應者的作用,是讓 ViewController 可以最早接收動作事件。可是在搖動還原的過程中,誰第一個接收到搖動的事件卻不重要。這件事可以用這個方法來證明:

事實上,直接把動作事件傳給響應鏈的最尾端 ── UIApplication 也是可以的:

由此可以確認,搖動還原的動作事件處理其實是 UIApplication 在負責的。這延伸出兩個問題,第一:為什麼不是第一響應者,也就是 ViewController 自己處理呢?第二,那又為什麼要讓 ViewController 成為第一響應者呢?

第一個問題是跟這個功能背後的實作設計有關,但很可惜,我並不是 UIKit 團隊的一員,所以沒辦法回答說為什麼要設計成由 UIApplication 來處理搖動還原,而不是直接做進 UIResponder 裡面。合理的猜測是,搖動還原包含了顯示警告的動作,而這個動作超出了 UIResponder 的責任範圍。比如說,UIView 就不應該有呈現警告的能力。相比起來,由 UIApplication 來處理更合乎它們的職責分配。

第二個問題的答案則是已經藏在前面的實作步驟裡了 ── ViewController 必須成為第一響應者才能啟用搖動還原功能,是因為這樣子 UIApplication 才知道要用它的 undoManager 來還原/重做。

UIResponder 的設計其實很有趣,因為它不僅僅是一個響應者,更是一個編輯者。舉例來說,它擁有一個 undoManager 屬性,並且遵守了 UIResponderStandardEditActions 這個定義編輯動作的協定 (protocol)。再者,becomeFirstResponder()resignFirstResponder() 這兩個方法在 UITextViewUITextField 上面,其實就是進入與退出編輯模式的意思。

從這點來說,第一響應者雖然表面上只是第一個接收動作與鍵盤等輸入的物件,但它其實更常代表的是現在正在被編輯的物件。比如說,除了剛剛提到的文字編輯物件之外,當我們想要針對某個響應者物件去顯示編輯選單的時候,我們也是要先使該物件成為第一響應者,好讓 UIMenuController 知道它正在編輯的對象是誰,要把編輯動作傳送給哪個響應者。同樣的概念也可以被套用在搖動還原上 ── 我們使要被還原/重做的響應者成為第一響應者,代表我們向 UIApplication 宣告說這個響應者正在被編輯,要它針對該第一響應者去顯示還原警告。

現在,我們已經大致知道 UIKit 內建的搖動還原,是由 UIApplication 在接收到搖動動作之後,去針對第一響應者的 undoManager 顯示一個警告。畫成圖的話就是這樣:

搖動還原-2

平常的時候,我們並不用知道這圖中的各個步驟要怎麼做到,因為 UIApplicationapplicationSupportsShakeToEdit 已經都幫你實作好了。然而,它使用起來雖簡單,它的 UI 卻幾乎沒有自訂的空間,因為我們完全無法取得它所呈現的 UIAlertController 物件。不只是沒有公開 API 而已,是連該警告物件所在的視窗都沒有在 UIApplication.shared.windows 裡面。

讓我們執行一個簡單的檢查。用 Xcode 啟動模擬器或實機上的 app,並呼叫出搖動還原的警告之後,執行 Xcode 裡的 Debug View Hierarchy 功能:

搖動還原-3

沒錯,該警告根本沒有在整個 app 的 view 階層裡面。也就是說,如果想要自訂這個還原警告的界面,哪怕只是改變它按鈕的顏色,我們都得要自己去重新實作整個機制才行。

那麼,該怎麼做呢?

手動實作搖動還原

在開始之前,我們要先把系統內建的實作關掉,以免搖一下就使內建跟自製的警告一起跳出來:

接著要做的,是定義方法的介面。假設我們已經按照這篇文章這篇文章定義了這些方法:

那我們就只需要再多定義一個介面,如下:

由這個 convenience init,我們可以很方便地產生還原警告。至於實作的話,不外乎用 undoManager 的各種屬性來產生 UIAlertAction,並決定警告的標題:

準備好以上這些方法之後,接著就是想辦法攔截搖動的動作事件了。也就是說,我們必須要找到某個響應鏈上的響應者,並複寫它的 motionEnded(_:with:) 方法。

在響應鏈中,有四個物件是通常都會被經過的:UIApplication.shared、主視窗、主視窗的根 View Controller、以及第一響應者本身。在這四個地方攔截搖動事件都可以,但越前面者就越一勞永逸。接下來,就讓我們一一來看看要如何實作事件攔截。

用第一響應者攔截

我們前面例子的第一響應者是 ViewController。如果是在這裡就攔截的話,就必須在所有需要搖動還原的響應者物件上都實作,但優點是實作方便。這個方法比較適合確定只會有一個地方需要搖動還原的狀況。

我們可以看到,除了複寫所需的模板碼 (boilerplate code) 外,我們只用了三行就完成實作了,而且還不需要再去問第一響應者是誰。

用根 View Controller 攔截

這個方式也相當方便,因為只要在一個地方實作就可以支援整個 app 的搖動還原。

然而,根 VC 的職責通常已經很重,再加上這段程式碼很可能會讓它更為龐大、紊亂。所以,在職責相對輕的 UIWindowUIApplication 實作可能是更好的選擇。唯一的問題是,要攔截動作事件就必須要寫 subclass,但 UIWindowUIApplication 可以被 subclass 嗎?我們又要怎麼讓 UIKit 去使用我們寫的 subclass 呢?

用主視窗攔截

即使官方文件裡說它們通常不會被 subclass,但這兩個 class 還是可以被 subclass 的,只是需要再特別告知 UIKit 去用我們寫的 subclass 而已。這裡我們先來建立 UIWindow 的 subclass Window

接著,我們要告訴 UIKit 去用 Window,而方法是把一個 Window 的實體,指派給 AppDelegatewindow 屬性:

當然,這是在使用 storyboard 的情況下。如果沒有用 storyboard 的話,那就只要把原本的 UIWindow 建構式改成 Window 建構式就可以了:

如此一來,UIKit 就會使用我們的 Window,而不是原本的 UIWindow,來建構 app 的主視窗了。

用 UIApplication 攔截

用主視窗攔截已經是很棒的解法了,但如果你夠 hardcore,想照 UIKit 原本的設計,將攔截搖動事件的職責交給 UIApplication 的話,你會需要做更多步驟。

首先,一樣是先 subclass UIApplication

接著,我們需要手動指定 UIApplication 的 subclass,而這是透過在一個叫做 main.swift 的檔案裡實作 UIApplicationMain(_:_:_:_:) 這個函式所達成的:

如果實作了這個函式的話,我們就得把 AppDelegate@UIApplicationMain 特性拿掉:

如此一來,UIKit 就會使用 Application 來啟動應用程式了!

更臻完美

到此為止,我們已經可以在搖動手機的時候去顯示還原警告了,但有些行為還是跟內建的不太一樣。首先,還原警告顯示的時候沒有震動回饋,而這只要在攔截處改成如下即可:

另外,為了要避免還原警告重複出現,我們可以再加一個判斷式:

接下來⋯

透過重新實作整個搖動還原功能,你有沒有更了解 UIKit 裡第一響應者的設計模式了呢?現在我們已經掌控了整個機制的程式碼,接下來,我們可以開始試著玩玩它,比如說,將搖動偵測改成按鈕偵測,或者把還原的 UI 改掉。剛好最近著名繪圖 app Procreate 的開發團隊釋出了該 app 的還原手勢,是用兩指與三指輕點去執行還原與重做,你也可以試著將它套用到我們介紹的設計模式看看啊!


iOS 開發者、寫作者、filmmaker。現正負責開發 Storyboards by narrativesaw 此一故事板文件 app 中。深深認同 Swift 對於程式碼易讀性的重視。個人網站:lihenghsu.com。電郵:[email protected]

blog comments powered by Disqus
訂閲電子報

訂閲電子報

AppCoda致力於發佈優質iOS程式教學,你不必每天上站,輸入你的電子郵件地址訂閱網站的最新教學文章。每當有新文章發佈,我們會使用電子郵件通知你。

已收你的指示。請你檢查你的電郵,我們已寄出一封認證信,點擊信中鏈結才算完成訂閱。

Shares
Share This