문제상황
<한들>을 제작하며 반드시 구현되어야 하는 기능인
'플레이하던 게임 상태 저장 및 재실행시 불러오기' 를 구현해야 했다.
어떠한 방식으로 이 부분을 구현해야 하나 고민을 많이 하다
Realm 패키지를 설치하여 현재 게임의 모델로 사용하고 있는 Game구조체의 데이터 자체를 Realm에 저장한 후,
게임 실행시 Realm에 저장된 Game을 불러와 ViewModel을 초기화하는 방식으로 구현하기로 했다.
문제는 아예 프로젝트 초기부터 Realm을 도입했다면
Model 자체를 Realm에 Persisted되는 Class로 작성한 후, 해당 Class를 저장하면 간편하게 이를 구현할 수 있었지만,
현재 모델이 Struct로 작성되어있어 이를 모두 Class로 바꾸기에는 Game 모델이 사용된 코드를 전부 갈아엎어야 한다는 문제가 있었다.
해결방법
https://medium.com/@ludovicjamet/how-to-use-struct-with-realm-615fcbc8f0ee
How to use struct with Realm ?
By default, Realm Objects are classes and not structs because they are not values, but auto-updating objects pointing to data in Realm…
medium.com
위의 링크를 참고하여 문제를 해결할 수 있었다.
먼저 현재 게임의 모델에서 사용되는 변수들은 다음과 같다.
struct Game {
private var id: String = UUID().uuidString
private var timestamp = Date()
private var jamoCount = 5
private(set) var gameNumber: Int
private(set) var answer: String
private(set) var wordDict: [WordDict]
private(set) var keyBoard: KeyBoard
private(set) var answerBoard: [[Key]]
private(set) var isGameFinished: Bool = false
private(set) var didPlayerWin: Bool = false
private(set) var currentRow: Int = 0
private(set) var currentColumn: Int = 0
}
일단 이를 Realm에 저장할 수 있도록 동일한 변수를 가지는 Class를 하나 작성했다.
import Foundation
import RealmSwift
class PersistedGame: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var timestamp = Date()
@Persisted var jamoCount = 5
@Persisted var gameNumber = 1
@Persisted var answer: String = ""
@Persisted var keyBoard: Data = Data()
@Persisted var answerBoard: Data = Data()
@Persisted var isGameFinished: Bool = false
@Persisted var didPlayerWin: Bool = false
@Persisted var didPlayerLose: Bool = false
@Persisted var currentRow: Int = 0
@Persisted var currentColumn: Int = 0
}
WordDict는 어차피 WordDictManager라는 싱글톤 클래스를 통해 생성되어 할당되므로 Realm에 저장할 필요가 없어 제외했다.
또한 keyBoard는 커스텀 Struct, answerBoard는 커스텀 Struct <Key>의 이중배열인데, Realm에 그대로 저장하기 위해서는 해당 Struct에 대응하는 Class를 별도로 생성해주어야 했다.
매우 귀찮은 작업이 될 것 같아 생각을 하다가, 어차피 두 변수는 쿼리를 통해 조회될 일이 없으므로 JSON형식의 데이터로 변환한 후 Realm에 저장하기로 했다. Key와 KeyBoard에 Codable 프로토콜이 적용되어 있다면 JSON decoder/encoder로 손쉽게 변환할 수 있다.
그 후 Persistable이라는 프로토콜을 만들어주었다.
public protocol Persistable {
associatedtype PersistedObject: RealmSwift.Object
init(persistedObject: PersistedObject)
func persistedObject() -> PersistedObject
}
Game 구조체에 Persistable 프로토콜을 적용한 후 프로토콜에 포함된 함수를 만들어주면 된다.
extension Game: Persistable {
typealias PersistedObject = PersistedGame
init(persistedObject: PersistedGame) {
let decoder = JSONDecoder()
self.id = persistedObject.id
self.timestamp = persistedObject.timestamp
self.gameNumber = persistedObject.gameNumber
self.jamoCount = persistedObject.jamoCount
self.answer = persistedObject.answer
self.answerBoard = try! decoder.decode([[Key]].self, from: persistedObject.answerBoard)
self.keyBoard = try! decoder.decode(KeyBoard.self, from: persistedObject.keyBoard)
self.didPlayerWin = persistedObject.didPlayerWin
self.isGameFinished = persistedObject.isGameFinished
self.currentRow = persistedObject.currentRow
self.currentColumn = persistedObject.currentColumn
self.wordDict = WordDictManager.shared.wordDictFiveJamo
}
func persistedObject() -> PersistedGame {
let encoder = JSONEncoder()
let persistedGame = PersistedGame()
persistedGame.id = self.id
persistedGame.timestamp = self.timestamp
persistedGame.jamoCount = self.jamoCount
persistedGame.gameNumber = self.gameNumber
persistedGame.answer = self.answer
persistedGame.keyBoard = try! encoder.encode(self.keyBoard)
persistedGame.answerBoard = try! encoder.encode(self.answerBoard)
persistedGame.isGameFinished = self.isGameFinished
persistedGame.didPlayerWin = self.didPlayerWin
persistedGame.currentRow = self.currentRow
persistedGame.currentColumn = self.currentColumn
return persistedGame
}
}
다소 코드가 많이 필요하고 변수가 추가될 때 마다 여러군데를 수정해줘야 한다는 문제가 있지만, 다음과 같이 편하게 Realm에 저장하고 꺼내서 쓸 수 있다.
import Foundation
import SwiftUI
class MainViewModel: ObservableObject {
@Published var game: Game
@Published var isInvalidWordWarningPresented: Bool = false
init () {
if let previousGame = RealmManager.shared.getPreviousGame() {
game = Game(persistedObject: previousGame)
} else {
game = Game(answer: todayAnswer())
}
}
}
이런식으로 init시에는 persistedObject를 넣어주기만 하면 struct가 생성되고,
게임을 저장할 때에는 다음과 같은 함수만 불러주면 된다.
extension Game {
//...
public func saveCurrentGame() {
RealmManager.shared.saveGame(self.persistedObject())
}
//...
}
'iOS' 카테고리의 다른 글
[refactoring] <한들> 바로잡기 - 0. 불필요한 의존성 해제 (0) | 2023.03.06 |
---|---|
[AVFoundation] observer로 AVPlayer의 상태 확인하기 (0) | 2023.02.22 |
[SwiftUI] SwiftUI에서 AVPlayerLayer 사용하기 (0) | 2023.02.20 |
[firebase] firestore에서 받은 데이터 쉽게 decode하기 (0) | 2022.03.27 |
[SwiftUI] List swipeAction을 활성화 한 상태에서 다른 뷰로 전환 시 리스트 멈춤 현상 (0) | 2022.02.22 |
댓글