[Memento Pattern] Sử dụng NSKeyedArchiver

Một trong những cách triển khai của Memento patternArchiving. Nó chuyển đổi object thành 1 stream có thể saverestore lại nhưng không phơi bày các private properties ra các external class.

Ta có nhiều lựa chọn để lưu mảng các objects.

  • NSUserDefaults : lưu app settings, preferences, user defaults
  • NSKeyedArchiver : để general data storage
  • Core data : cho complex data storage (như database)

Ở topic này, ta không bàn tới CoreData mà chỉ xem vì sao nên dùng NSKeyedArchiver hơn là NSUserDefaults.

Tạo 1 custom object

Đầu tiên, ta tạo 1 class là Player và cung cấp các phương thức cho cả 2 options. Tuy nhiên muốn thực thi được, phương thức load & save của NSKeyedArchiver yêu cầu vài code để handle array của custom object Player này. 1 custom class muốn archive phải tích hợp NSCoding sau đó nó có thể encodedecode chính nó và những properties của nó.

class Player: NSObject, NSCoding {
    
    var name: String = ""
    
    init(name: String) {
        print("designated initializer")
        self.name = name
        super.init()
    }

    func encodeWithCoder(aCoder: NSCoder) {
        print("encodeWithCoder")
        aCoder.encodeObject(name, forKey: "name")
    }
    
    // since we inherit from NSObject, we're not a final class -> therefore this initializer must be declared as 'required'
    // it also must be declared as a 'convenience' initializer, because we still have a designated initializer as well
    required convenience init?(coder aDecoder: NSCoder) {
        print("decodeWithCoder")
        guard let unarchivedName = aDecoder.decodeObjectForKey("name") as? String
            else {
                return nil
        }
        
        // now (we must) call the designated initializer
        self.init(name: unarchivedName)
    }
}
  • encodeWithCoder: gọi khi muốn archive 1 thực thể của class này
  • initWithCoder: khi bạn unarchive 1 thực thể để tạo 1 đối tượng Player

Player giờ đã có thể archived. Ta thực hiện các phương thức để save và load 1 Player.

Sử dụng NSKeyedArchiver

Với NSKeyedArchiver ta có thể dễ dàng lưu vào những file cụ thể, hơn là phải lo lắng về name của unique 'key' cho từng property.

    private class func getFileURL() -> NSURL {
        // construct a URL for a file named 'Players' in the DocumentDirectory
        let documentsDirectory = NSFileManager().URLsForDirectory((.DocumentDirectory), inDomains: .UserDomainMask).first!
        let archiveURL = documentsDirectory.URLByAppendingPathComponent("Players")
        
        return archiveURL
    }
    
    class func savePlayersToDisk(players: [Player]) {
        let success = NSKeyedArchiver.archiveRootObject(players, toFile: Player.getFileURL().path!)
        if !success {
            print("failed to save") // you could return the error here to the caller
        }
    }
    
    class func loadPlayersFromDisk() -> [Player]? {
        return NSKeyedUnarchiver.unarchiveObjectWithFile(Player.getFileURL().path!) as? [Player]
    }

Khi bạn archive 1 object mà chứa các object khác, archiver sẽ tự động archive object con và các object con của object con này... Trong vd trên, ta archive với list players. NSArray và Player đều hỗ trợ NSCopying interface, mọi thứ trong array sẽ tự động được archived.

Sử dụng NSUserDefaults

Ta cần convert array Player thành NSData, NSUserDefaults không thể handle arrays của custom objects. Nó bị giới hạn bởi NSString, NSNumber, NSDate, NSArray, NSData. Có vài convenience methods như setBool, setInteger, ... nhưng không có method cho 1 custom object. Lưu ý NSKeyedArchiver sẽ lặp qua array player. Vì vậy encodeWithCoder sẽ được gọi cho từng object trong array

    class func savePlayersToUserDefaults(players: [Player]) {
        let dataBlob = NSKeyedArchiver.archivedDataWithRootObject(players)
        NSUserDefaults.standardUserDefaults().setObject(dataBlob, forKey: "PlayersInUserDefaults")
        NSUserDefaults.standardUserDefaults().synchronize()
    }
    
    class func loadPlayersFromUserDefaults() -> [Player]? {
        guard let decodedNSDataBlob = NSUserDefaults.standardUserDefaults().objectForKey("PlayersInUserDefaults") as? NSData,
            let loadedPlayersFromUserDefault = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSDataBlob) as? [Player]
            else {
                return nil
        }
        return loadedPlayersFromUserDefault
    }

Thực thi

     override func viewDidLoad() {
        super.viewDidLoad()
        
        // create some data
        let player1 = Player(name: "John")
        let player2 = Player(name: "Patrick")
        let playersArray = [player1, player2]
        
        print("--- NSUserDefaults demo ---")
        Player.savePlayersToUserDefaults(playersArray)
        if let retreivedPlayers = Player.loadPlayersFromUserDefaults() {
            print("loaded \(retreivedPlayers.count) players from NSUserDefaults")
            print("\(retreivedPlayers[0].name)")
            print("\(retreivedPlayers[1].name)")
        } else {
            print("failed")
        }
        
        print("--- file demo ---")
        Player.savePlayersToDisk(playersArray)
        if let retreivedPlayers = Player.loadPlayersFromDisk() {
            print("loaded \(retreivedPlayers.count) players from disk")
            print("\(retreivedPlayers[0].name)")
            print("\(retreivedPlayers[1].name)")
        } else {
            print("failed")
        }
    }

Như đã nói ở trên cả 2 phương thức đều ra cùng 1 kết quả. Tuy nhiên trong thực tế ta cần handle error tốt hơn nếu archive và unarchive bị fail.

designated initializer
designated initializer
--- NSUserDefaults demo ---
encodeWithCoder
encodeWithCoder
decodeWithCoder
designated initializer
decodeWithCoder
designated initializer
loaded 2 players from NSUserDefaults
John
Patrick
--- file demo ---
encodeWithCoder
encodeWithCoder
decodeWithCoder
designated initializer
decodeWithCoder
designated initializer
loaded 2 players from disk
John
Patrick