今日のゴール
- UserDefaultsに資源を保存する
- アプリ起動時にUserDefaultsから資源量を読み込む
今日のキーワード
- Generics
- RawRepresentable
- private(set)
- NSCoder
やったこと
一度落ち着いて考える
- 一旦バックグラウンドフォアグラウンドは忘れよう
- アプリキル時に値を保存、再起動時に資源量を読み込んで更新するようにするよ
- 実は資源名とか資源画像とか値が不変なものはシリアライズして保存しなくていいのでは?という疑念が浮かんできたけど、インスタンスの保存にはうまくいっているはずなのでこれも一回忘れるよ
処理フローを考える
- アプリが起動して資源が自動生産を始める
- アプリがキルされるタイミングでその時の時刻とインスタンスをUserDefaultsに保存する
- アプリが再起動される
- VCでResourceからインスタンスを作った後、UserDefaultsを確認してデータが存在していれば読み込む
- 読み込んだ時刻と保存してある時刻を比較してどの程度時間が経過しているか差分を計算する
- 差分に応じて値を資源保有量に足す
という方針でまずは進めるよ
前回willTerminateで受け取った保存処理をResourceクラス側に実装していたけど、実際のResourceインスタンスもってるのがVCなのでやっぱり処理をVC側に移すよ(そして多分設計的な話だとビジネスロジックはVCに書くべきではないのでアプリ設計がよろしくない けどまあ気にしない)
ViewDidLoadに必要な処理を書いていく
override func viewDidLoad() {
super.viewDidLoad()
// キャラクターのインスタンスを生成してViewControllerのプロパティに設定
guard let valkyrie = Character(name: "【戦乙女】ヴァルキリー", charaImage: UIImage(named: "sozai.png")) else {
return
}
self.valkyrie = valkyrie
// imageViewのimageにキャラのimageを設定
imageView.image = valkyrie.standardImage
name.text = valkyrie.name
name.adjustsFontSizeToFitWidth = true
strLabel.text = "攻撃力: \(String(describing: valkyrie.status.str))"
defLabel.text = "防御力: \(String(describing: valkyrie.status.def))"
lucLabel.text = "天運: \(String(describing: valkyrie.status.luc))"
conditionLabel.text = "状態: " + valkyrie.getCondition().description()
// UserDefaultsに保存した資源があるかチェック
if let savedObject = UserDefaults.standard.object(forKey: "resource") as? [String: Any] {
guard let parsedDate = parse(from: savedObject) else { return }
resource = parsedDate.resource
resource.manufacture(from: parsedDate.interval)
}
resource.setUpTimer()
resourceImageView.image = resource.image
resourceQuantityLabel.text = String(self.resource.getQuantity())
resourceQuantityLabel.sizeToFit()
NotificationCenter.default.addObserver(forName: Notification.Name(resource.name), object: nil, queue: .main) {notification in
self.reloadResource(with: notification)
}
NotificationCenter.default.addObserver(forName: .UIApplicationWillTerminate, object: nil, queue: .main) { (_) in
print("willTernimate...")
// 資源をシリアライズ化、アプリ終了時刻とともにUserDefaultsに保存する
let serializeResource = NSKeyedArchiver.archivedData(withRootObject: self.resource),
terminateDate = Date(),
saveData: [String: Any] = ["resource": serializeResource,
"terminateDate": terminateDate]
let userDefault = UserDefaults.standard
userDefault.set(saveData, forKey: "resource")
}
}
// UserDefaultsに保存されたオブジェクトをパースして(Resource, TimeInterval)のタプルで返すメソッド
func parse(from savedObject: [String: Any]) -> (resource: Resource, interval: TimeInterval)? {
guard let parsedResourceData = savedObject["resource"] as? Data,
let resource = NSKeyedUnarchiver.unarchiveObject(with: parsedResourceData) as? Resource,
let parsedTerminateDate = savedObject["terminateDate"] as? Date else {
return nil
}
return (resource, Date().timeIntervalSince(parsedTerminateDate))
}
ResourceクラスにはTimeIntervalから増加分を計算して資源保有量に追加するmanufactureメソッドを追加するよ
~省略~
func manufacture(from interval: TimeInterval) {
quantity += lround(interval / incrementInterval) * increaseAmount
}
~省略~
おお、保存されとるやんけ!
だけどこれだけではダメ
- 今の状態だとよろしくないことができてしまうよ・・・
- iPhone本体の「設定」から時間を進めると資源を増加させることができるよ・・・
- 例えば1日分進めると、3600秒 / 5(incrementInterval) * 100(increaseAmount)で72,000も増やせてしまうよ・・・
- 良さげな解決手段が思い浮かばないので一旦issueとして積んでおくよ
- こういうチート対策ってどうしてるんだろうか🤔
シリアライズ・デシリアライズを安全に書きやすく拡張する
-
保存が案外簡単にうまくできたので、諸々調べてる途中で見つけた下記記事を参考にちょっとリファクタするよ
-
encode/decodeする際のkeyからrawValueを駆逐するよ
-
NSCoder+CodingKeySettable.swiftを実装する
-
NSCoderを拡張してrawValueを駆逐する
-
ResourceでUIImageをシリアライズ・デシリアライズしていたけど、あまり良くなさそうなので画像ファイル名をプロパティとしてもち、それを保存することにする
-
ついでに、Resourceのgetter書くのめんどいのでprivate(set)で隠蔽する
import Foundation
extension NSCoder {
func encode<T: RawRepresentable>(_ object: Any?, forKey key: T) {
encode(object, forKey: key.rawValue as! String)
}
func encodeInteger<T: RawRepresentable>(_ object: Int, forKey key: T) {
encode(object, forKey: key.rawValue as! String)
}
func encodeFloat<T: RawRepresentable>(_ object: Float, forKey key: T) {
encode(object, forKey: key.rawValue as! String)
}
func encodeDouble<T: RawRepresentable>(_ object: Double, forKey key: T) {
encode(object, forKey: key.rawValue as! String)
}
func encodeBool<T: RawRepresentable>(_ object: Bool, forKey key: T) {
encode(object, forKey: key.rawValue as! String)
}
func decodeObject<T: RawRepresentable>(forKey key: T) -> Any? {
return decodeObject(forKey: key.rawValue as! String)
}
func decodeInteger<T: RawRepresentable>(forKey key: T) -> Int {
return decodeInteger(forKey: key.rawValue as! String)
}
func decodeFloat<T: RawRepresentable>(forKey key: T) -> Float {
return decodeFloat(forKey: key.rawValue as! String)
}
func decodeDouble<T: RawRepresentable>(forKey key: T) -> Double {
return decodeDouble(forKey: key.rawValue as! String)
}
func decodeBool<T: RawRepresentable>(forKey key: T) -> Bool {
return decodeBool(forKey: key.rawValue as! String)
}
}
final class Resource: NSObject, NSCoding {
let name: String // 資源名(ex: 光のオーブ)
let imageName: String
let image: UIImage // 資源image
private(set) var quantity: Int // 資源保有量 デフォルト10000
private(set) var incrementInterval: TimeInterval // 増加間隔(秒) デフォルト5秒
private(set) var incrementAmount: Int // 増加量 デフォルトでは100
private(set) var incrementTimer: Timer
init(name: String,
imageName: String,
quantity: Int = 100000,
incrementInterval: TimeInterval = 0.1,
increaseAmount: Int = 100) {
self.name = name
self.imageName = imageName
self.image = UIImage(named: imageName)!
self.quantity = quantity
self.incrementInterval = incrementInterval
self.incrementAmount = increaseAmount
self.incrementTimer = Timer()
super.init()
}
private enum CodingKey: String {
case name
case imageName
case quantity
case incrementInterval
case incrementAmount
}
required init?(coder aDecoder: NSCoder) {
guard
let name = aDecoder.decodeObject(forKey: CodingKey.name) as? String,
let imageName = aDecoder.decodeObject(forKey: CodingKey.imageName) as? String,
let image = UIImage(named: imageName)
else {
print("coder失敗")
return nil
}
let quantity = aDecoder.decodeInteger(forKey: CodingKey.quantity),
incrementInterval = aDecoder.decodeDouble(forKey: CodingKey.incrementInterval),
increaseAmount = aDecoder.decodeInteger(forKey: CodingKey.incrementAmount)
self.name = name
self.imageName = imageName
self.image = image
self.quantity = quantity
self.incrementInterval = incrementInterval
self.incrementAmount = increaseAmount
self.incrementTimer = Timer()
super.init()
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: CodingKey.name)
aCoder.encode(imageName, forKey: CodingKey.imageName)
aCoder.encodeInteger(quantity, forKey: CodingKey.quantity)
aCoder.encodeDouble(incrementInterval, forKey: CodingKey.incrementInterval)
aCoder.encodeInteger(incrementAmount, forKey: CodingKey.incrementAmount)
}
~~省略~~
}
keyを手打ちしなくてよいので安全になる!(はず!)
CodingKeyすらも駆逐したかったけど、いい感じの実装が思い浮かばず・・・orz
結果
- とりあえずゴールは達成した
- チート問題マジでどうしよう
- 今回の作業ブランチはこちら
オススメの開発手法について
- HKDD(Hiroti Karaoke Driven Development)という開発手法をオススメします
- 実践方法は簡単。カラオケ屋に一人でPCを持ち込んで開発、行き詰まったらカラオケで数曲熱唱して再び開発に戻るだけ。
- 歌うことによってストレスが発散され、新たな発想を得やすくなります!(割とマジで)
- みなさんぜひHKDDで快適なプログラミングライフを!
次回
最後に
ご指摘・リファクタ大歓迎です