在Swift中實現撤銷功能

Zheaoli發表於2019-01-23

在過去的一段時間裡,有很多的Blog推出了關於他們想在Swift中所新增的動態特性的文章。事實上Swift 已經成為了一門具有相當多動態特性的語言:它擁有泛型,協議, 頭等函式(譯者注1:first-class function指函式可以向類一樣作為引數傳遞),和包含很多可以的動態操作的函式的標準庫,比如mapfilter等(這意味著我們可以利用更安全更靈活的函式來代替 KVC 來使用 字串)(譯者注2:KVC指Key-Value-Coding一個非正式的 Protocol,提供一種機制來間接訪問物件的屬性)。對於大多數人而言,特別希望介紹反射這一特性,這意味著他們可以在程式執行時進行觀察和修改。

Swift中,反射機制受到很多的限制,但是你仍然你可以在程式碼執行的時候動態的生成和插入一些東西。 比如這裡是怎樣為NSCoding或者是JSON動態生成字典的例項。

今天在這裡,我們將一起看一下在Swift中怎樣去實現撤銷功能。 其中一種方法是通過利用Objective-C中基於的反射機制所提供的NSUndoManager。通過利用struct,我們可以利用不同的方式在我們的APP中實現撤銷這一功能。 在教程開始之前,請務必確保你自己已經理解了Swiftstruct的工作機制(最重要的是理解他們都是獨立的拷貝)。
首先要宣告的一點是,這篇文章並不是想告訴大家我們不需要對runtime進行操作,或者我們提供的是一種NSUndoManager的替代品。這篇文章只是告訴了大家一種不同的思考方式而已。

我們首先建立一個叫做UndoHistorystruct。 通常而言,建立UndoHistory時會伴隨一個警告,提示只有當A是一個struct的時才會生效。為了儲存所有狀態資訊,我們需要將其存放入一個陣列之中。當我們修改了什麼時,我們只需要將其push進陣列中,當我們希望進行撤回時,我們將其從陣列中pop出去。我們通常希望有一個初試狀態,所以我們需要建立一個初始化方法:

    struct UndoHistory<A> {
        private let initialValue: A
        private var history: [A] = []
        init(initialValue: A) {
            self.initialValue = initialValue
        }
    }
複製程式碼

舉個例子,如果我們想在一個tableViewController中通過陣列的方式提供撤銷操作,我們可以建立這樣一個struct

    var history = UndoHistory(initialValue: [1, 2, 3])
複製程式碼

對於不同情境下的撤銷操作,我們可以建立不同的struct來實現:

    struct Person {
        var name: String
        var age: Int
    }
複製程式碼
    var personHistory = UndoHistory(initialValue: Person(name: "Chris", age: 31))
複製程式碼

當然,我們希望獲得當前的狀態,同時設定當前狀態。(換句話說:我們希望實時地操作我們的歷史記錄)。我們可以從history陣列中的最後一項值來獲取我們的狀態,同時如果陣列為空的話,我們便返回我們的初始值。 我們可以通過將當前狀態新增至history陣列來改變我們的操作狀態。

    extension UndoHistory {
        var currentItem: A {
            get {
                return history.last ?? initialValue
            }
            set {
                history.append(newValue)
            }
        }
    }
複製程式碼

比如,如果我們想修改個人年齡(譯者注3:指前面作者編寫的Person結構體中的age屬性), 我們可以通過重新計算屬性來很輕鬆的做到這一點:

    personHistory.currentItem.age += 1
    personHistory.currentItem.age // Prints 32
複製程式碼

當然,undo 方法的編寫並未完成。對於從陣列中移出最後一個元素來講是非常簡單的。 根據你自己的宣告,你可以在陣列為空的時候丟擲一個異常,不過,我沒有選擇這樣一種做法。

    extension UndoHistory {
        mutating func undo() {
            guard !history.isEmpty else { return }
            history.removeLast()
        }
    }
複製程式碼

很簡單的使用它(譯者注4:這裡指作者前面所編寫的undo相關程式碼)

    personHistory.undo()
    personHistory.currentItem.age // Prints 31 again
複製程式碼

當然,我們到現在的UndoHistory操作只是基於一個很簡單的Person類。比如,如果我們想利用Array來實現一個tableviewcontrollerundo操作,我們可以利用屬性來獲取從陣列中得到的元素:

    final class MyTableViewController<item>: UITableViewController {
        var data: UndoHistory<[item]>

        init(value: [Item]) {
            data = UndoHistory(initialValue: value)
            super.init(style: .Plain)
        }

        override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return data.currentItem.count
        }

        override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCellWithIdentifier("Identifier", forIndexPath: indexPath)
            let item = data.currentItem[indexPath.row]
            // configure `cell` with `item`
            return cell
        }

        override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
            guard editingStyle == .Delete else { return }
            data.currentItem.removeAtIndex(indexPath.row)
        }
    }
複製程式碼

struct中另一個非常爽的特性是:我們可以自由的使用監聽者模式。 比如,我們可以修改data的值:

    var data: UndoHistory<[item]> {
        didSet {
            tableView.reloadData()
        }
    }
複製程式碼

我們即使是修改陣列內很深的值(比如:data.currentItem[17].name = “John”),我們通過didSet也能很方便地定位到修改的地方。當然,我們可能希望做一些例如reloadData這樣方便的事情。比如, 我們可以利用Changeset 庫來計算變化,然後來根據插入/刪除/移動/等不同的操作來新增動畫。

很明顯的是, 這種方法有著它自身的缺點。例如,它儲存了整個狀態的歷史操作,不是每次狀態變化之間的不同點。 這種方法只使用了struct來實現undo操作 (更為準確的講:是隻使用了struct中值的一些特性)。這意味著,你並不需要去閱讀 runtime程式設計指導這本書, 你只需要對structgenerics(譯者注5:generics指泛型)有足夠的瞭解。

  1. 為data.currentItem提供了一個可計算的屬性 items 來進行獲取和設定操作,是一個不錯的想法。這使得data-sourcedelegate等方法的實現變得更為容易。
  2. 如果你想更進一步優化,這裡有一些非常有意思的想法:新增恢復功能,或者是編輯功能。你可以在tableView中去實現, 如果你真的很天真的按照這個去做了,那麼你會發現在你的undo歷史中會存在重複記錄。

關於翻牆(硬廣)

這只是一個友情推薦,我現在使用的是一起艾斯,ipv4,ipv6線路全覆蓋,唔,站長人也很好,基本你的合理需求,站長都能滿足。建議可以試試。

相關文章