[Memento Pattern] Sử dụng NSKeyedArchiver
Bài đăng này đã không được cập nhật trong 7 năm
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.
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ể 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.
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
All rights reserved