Во-первых, LevelView выглядит так, как будто в нем слишком много логики. Точка зрения заключается в отображении данных модели. Это не включает бизнес-логику, такую как GameStatus.xp > 95
. Это должно быть сделано в другом месте и установлено в поле зрения.
Далее, почему GameStatus статичен? Это только усложняет это. Передайте GameStatus представлению, когда оно изменится. Это работа контроллера представления. Взгляды просто рисуют вещи. Если на ваш взгляд что-то действительно может быть проверено юнитами, то, вероятно, этого не должно быть.
Наконец, часть, с которой вы боретесь - это пользовательские настройки по умолчанию. Так что извлеките этот кусок в общий GameStorage.
protocol GameStorage {
var xp: Int { get set }
var level: Int { get set }
}
Теперь сделайте UserDefaults в GameStorage:
extension UserDefaults: GameStorage {
var xp: Int {
get { /* Read from UserDefaults */ return ... }
set { /* Write to UserDefaults */ }
}
var level: Int {
get { /* Read from UserDefaults */ return ... }
set { /* Write to UserDefaults */ }
}
}
А для тестирования создайте статический:
struct StaticGameStorage: GameStorage {
var xp: Int
var level: Int
}
Теперь, когда вы создаете GameStatus, передайте ему хранилище. Но вы можете присвоить этому значению значение по умолчанию, чтобы вам не приходилось передавать его все время
class GameStatus {
private var storage: GameStorage
// A default parameter means you don't have to pass it normally, but you can
init(storage: GameStorage = UserDefaults.standard) {
self.storage = storage
}
При этом xp и level могут просто передаваться в хранилище. Нет необходимости в специальном шаге «загрузить хранилище сейчас».
private(set) var xp: Int {
get { return storage.xp }
set { storage.xp = newValue }
}
private(set) var level: Int {
get { return storage.level }
set { storage.level = newValue }
}
РЕДАКТИРОВАТЬ: я сделал здесь изменение от GameStatus в качестве структуры к классу. Это потому, что GameStatus не хватает семантики значения. Если существует две копии GameStatus, и вы изменяете одну из них, другая также может измениться (поскольку они обе записывают в UserDefaults). Структура без семантики значения опасна.
Возможно восстановить семантику значения, и это стоит рассмотреть. Например, вместо того, чтобы проходить через xp и level в хранилище, вы можете вернуться к исходному дизайну, который имеет явный шаг «восстановления», который загружается из хранилища (и я предполагаю, что шаг «сохранения» записывает в хранилище). Тогда GameStatus будет подходящей структурой.
Я также извлек бы LevelState, чтобы вам было проще его протестировать, и он захватывает бизнес-логику за пределами представления.
enum LevelState {
case showStart
case showCountdown
case showFinalCountDown
init(xp: Int) {
if xp > 95 {
self = .showFinalCountDown
} else if xp > 90 {
self = .showCountdown
}
self = .showStart
}
}
Если это только когда-либо используется этим одним представлением, это хорошо, чтобы вкладывать его. Только не делай это частным. Вы можете протестировать LevelView.LevelState без каких-либо действий с самим LevelView.
И затем вы можете обновить GameStatus представления так, как вам нужно:
class LevelView: UIView {
var gameStatus: GameStatus? {
didSet {
// Refresh the view with the new status
}
}
var state: LevelState {
guard let xp = gameStatus?.xp else { return .showStart }
return LevelState(xp: xp)
}
//...configurations depending on the level
}
Теперь само представление не нуждается в логическом тестировании. Вы можете провести тестирование на основе изображений, чтобы убедиться, что оно правильно рисует при различных входных данных, но это полностью сквозное. Вся логика проста и тестируема. Вы можете протестировать GameStatus и LevelState вообще без UIKit, передав StaticGameStorage в GameStatus.