[Memento Pattern] Sử dụng NSKeyedArchiver
Một trong những cách triển khai của Memento pattern là Archiving . Nó chuyển đổi object thành 1 stream có thể save và restore 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 ...
Một trong những cách triển khai của Memento pattern là Archiving. Nó chuyển đổi object thành 1 stream có thể save và restore 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.
Đầ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ể encode và decode 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.
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.
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 }
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