Skip to content

Commit

Permalink
Merge pull request #117 from uhooi/feature/set_default_times
Browse files Browse the repository at this point in the history
Set default times
  • Loading branch information
uhooi authored Jan 3, 2023
2 parents ecf6385 + 9cee491 commit ca92c8e
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 18 deletions.
31 changes: 31 additions & 0 deletions LokiPackage/Sources/Data/Sakatsu/DefaultSaunaSetRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import UserDefaultsCore

public protocol DefaultSaunaSetRepository {
func defaultSaunaSet() throws -> SaunaSet
func saveDefaultSaunaSet(_ defaultSaunaSet: SaunaSet) throws
}

public struct DefaultSaunaSetUserDefaultsClient {
public static let shared: Self = .init()
private static let defaultSaunaSetKey = "defaultSaunaSet"

private let userDefaultsClient = UserDefaultsClient.shared

private init() {}
}

extension DefaultSaunaSetUserDefaultsClient: DefaultSaunaSetRepository {
public func defaultSaunaSet() throws -> SaunaSet {
do {
return try userDefaultsClient.object(forKey: Self.defaultSaunaSetKey)
} catch UserDefaultsError.missingValue {
return .init()
} catch {
throw error
}
}

public func saveDefaultSaunaSet(_ defaultSaunaSet: SaunaSet) throws {
try userDefaultsClient.set(defaultSaunaSet, forKey: Self.defaultSaunaSetKey)
}
}
4 changes: 4 additions & 0 deletions LokiPackage/Sources/Data/Sakatsu/Sakatsu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public struct Sakatsu: Identifiable {
public var afterword: String? = nil

public init() {}

public init(saunaSets: [SaunaSet]) {
self.saunaSets = saunaSets
}
}

extension Sakatsu: Equatable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
"Please try again after some time." = "Please try again after some time.";

// SakatsuSettingsScreen
"Default times" = "Default times";
"Settings" = "Settings";
"Version" = "Version";
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
"Please try again after some time." = "時間をおいて再度お試しください。";

// SakatsuSettingsScreen
"Default times" = "デフォルト時間";
"Settings" = "設定";
"Version" = "バージョン";
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import UICore
import SakatsuData

struct SakatsuInputScreen: View {
@StateObject private var viewModel: SakatsuInputViewModel<SakatsuUserDefaultsClient, SakatsuValidator>
@StateObject private var viewModel: SakatsuInputViewModel<DefaultSaunaSetUserDefaultsClient, SakatsuUserDefaultsClient, SakatsuValidator>

private let onSakatsuSave: () -> Void

Expand Down Expand Up @@ -65,10 +65,10 @@ struct SakatsuInputScreen: View {
}

init(
sakatsu: Sakatsu?,
editMode: EditMode,
onSakatsuSave: @escaping () -> Void
) {
self._viewModel = StateObject(wrappedValue: SakatsuInputViewModel(sakatsu: sakatsu ?? .init()))
self._viewModel = StateObject(wrappedValue: SakatsuInputViewModel(editMode: editMode))
self.onSakatsuSave = onSakatsuSave
}
}
Expand Down Expand Up @@ -101,7 +101,7 @@ private extension View {
struct SakatsuInputScreen_Previews: PreviewProvider {
static var previews: some View {
SakatsuInputScreen(
sakatsu: .preview,
editMode: .edit(sakatsu: .preview),
onSakatsuSave: {}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ struct SakatsuInputUiState {
var sakatsuInputError: SakatsuInputError? = nil
}

enum EditMode {
case new
case edit(sakatsu: Sakatsu)
}

// MARK: - Error

enum SakatsuInputError: LocalizedError {
Expand Down Expand Up @@ -43,37 +48,51 @@ enum SakatsuInputError: LocalizedError {

@MainActor
final class SakatsuInputViewModel<
Repository: SakatsuRepository,
SettingsRepository: DefaultSaunaSetRepository,
DataRepository: SakatsuRepository,
Validator: SakatsuValidatorProtocol
>: ObservableObject {
@Published private(set) var uiState: SakatsuInputUiState

private let repository: Repository
private let defaultSaunaSetRepository: SettingsRepository
private let sakatsuRepository: DataRepository
private let validator: Validator

init(
sakatsu: Sakatsu,
repository: Repository = SakatsuUserDefaultsClient.shared,
editMode: EditMode,
defaultSaunaSetRepository: SettingsRepository = DefaultSaunaSetUserDefaultsClient.shared,
sakatsuRepository: DataRepository = SakatsuUserDefaultsClient.shared,
validator: Validator = SakatsuValidator()
) {
self.uiState = SakatsuInputUiState(sakatsu: sakatsu)
self.repository = repository
switch editMode {
case .new:
let defaultSaunaSet = (try? defaultSaunaSetRepository.defaultSaunaSet()) ?? .init()
self.uiState = SakatsuInputUiState(sakatsu: .init(saunaSets: [defaultSaunaSet]))
case let .edit(sakatsu: sakatsu):
self.uiState = SakatsuInputUiState(sakatsu: sakatsu)
}
self.defaultSaunaSetRepository = defaultSaunaSetRepository
self.sakatsuRepository = sakatsuRepository
self.validator = validator
}

private func defaultSaunaSet() -> SaunaSet {
(try? defaultSaunaSetRepository.defaultSaunaSet()) ?? .init()
}
}

// MARK: - Event handler

extension SakatsuInputViewModel {
func onSaveButtonClick() {
do {
var sakatsus = (try? repository.sakatsus()) ?? []
var sakatsus = (try? sakatsuRepository.sakatsus()) ?? []
if let index = sakatsus.firstIndex(of: uiState.sakatsu) {
sakatsus[index] = uiState.sakatsu
} else {
sakatsus.append(uiState.sakatsu)
}
try repository.saveSakatsus(sakatsus.sorted(by: { $0.visitingDate > $1.visitingDate }))
try sakatsuRepository.saveSakatsus(sakatsus.sorted(by: { $0.visitingDate > $1.visitingDate }))
} catch {
uiState.sakatsuInputError = .sakatsuSaveFailed
}
Expand All @@ -84,7 +103,7 @@ extension SakatsuInputViewModel {
}

func onAddNewSaunaSetButtonClick() {
uiState.sakatsu.saunaSets.append(.init())
uiState.sakatsu.saunaSets.append(defaultSaunaSet())
}

func onFacilityNameChange(facilityName: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private extension View {
})
) {
SakatsuInputScreen(
sakatsu: selectedSakatsu,
editMode: selectedSakatsu != nil ? .edit(sakatsu: selectedSakatsu!) : .new,
onSakatsuSave: onSakatsuSave
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import SwiftUI
import UICore
import SakatsuData

struct SakatsuSettingsScreen: View {
@StateObject private var viewModel: SakatsuSettingsViewModel<DefaultSaunaSetUserDefaultsClient, SakatsuValidator>

@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationStack {
SakatsuSettingsView()
.navigationTitle(String(localized: "Settings", bundle: .module))
.sakatsuSettingsScreenToolbar(onCloseButtonClick: { dismiss() })
SakatsuSettingsView(
defaultSaunaSet: viewModel.uiState.defaultSaunaSet,
onDefaultSaunaTimeChange: { defaultSaunaTime in
viewModel.onDefaultSaunaTimeChange(defaultSaunaTime: defaultSaunaTime)
}, onDefaultCoolBathTimeChange: { defaultCoolBathTime in
viewModel.onDefaultCoolBathTimeChange(defaultCoolBathTime: defaultCoolBathTime)
}, onDefaultRelaxationTimeChange: { defaultRelaxationTime in
viewModel.onDefaultRelaxationTimeChange(defaultRelaxationTime: defaultRelaxationTime)
}
)
.navigationTitle(String(localized: "Settings", bundle: .module))
.sakatsuSettingsScreenToolbar(onCloseButtonClick: { dismiss() })
.errorAlert(
error: viewModel.uiState.sakatsuSettingsError,
onDismiss: { viewModel.onErrorAlertDismiss() }
)
}
}

init() {
self._viewModel = StateObject(wrappedValue: SakatsuSettingsViewModel())
}
}

private extension View {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
import SwiftUI
import SakatsuData

struct SakatsuSettingsView: View {
let defaultSaunaSet: SaunaSet

let onDefaultSaunaTimeChange: ((TimeInterval?) -> Void)
let onDefaultCoolBathTimeChange: ((TimeInterval?) -> Void)
let onDefaultRelaxationTimeChange: ((TimeInterval?) -> Void)

var body: some View {
Form {
defaultSaunaSetsSection
versionSection
}
}

private var defaultSaunaSetsSection: some View {
Section {
saunaSetItemTimeInputView(
saunaSetItem: defaultSaunaSet.sauna,
onTimeChange: onDefaultSaunaTimeChange
)
saunaSetItemTimeInputView(
saunaSetItem: defaultSaunaSet.coolBath,
onTimeChange: onDefaultCoolBathTimeChange
)
saunaSetItemTimeInputView(
saunaSetItem: defaultSaunaSet.relaxation,
onTimeChange: onDefaultRelaxationTimeChange
)
} header: {
Text("Default times", bundle: .module)
}
}

private var versionSection: some View {
Section {
HStack {
Expand All @@ -18,12 +45,34 @@ struct SakatsuSettingsView: View {
Text("© 2023 THE Uhooi")
}
}

private func saunaSetItemTimeInputView(
saunaSetItem: any SaunaSetItemProtocol,
onTimeChange: @escaping (TimeInterval?) -> Void
) -> some View {
HStack {
Text(saunaSetItem.emoji + saunaSetItem.title)
TextField(String(localized: "Optional", bundle: .module), value: .init(get: {
saunaSetItem.time
}, set: { newValue in
onTimeChange(newValue)
}), format: .number)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
Text(saunaSetItem.unit)
}
}
}

#if DEBUG
struct SakatsuSettingsView_Previews: PreviewProvider {
static var previews: some View {
SakatsuSettingsView()
SakatsuSettingsView(
defaultSaunaSet: .preview,
onDefaultSaunaTimeChange: { _ in },
onDefaultCoolBathTimeChange: { _ in },
onDefaultRelaxationTimeChange: { _ in }
)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Foundation
import Combine
import SakatsuData

// MARK: UI state

struct SakatsuSettingsUiState {
var defaultSaunaSet: SaunaSet = .init()
var sakatsuSettingsError: SakatsuSettingsError? = nil
}

// MARK: - Error

enum SakatsuSettingsError: LocalizedError {
case defaultSaunaSetFetchFailed(localizedDescription: String)
case defaultSaunaSetSaveFailed(localizedDescription: String)

var errorDescription: String? {
switch self {
case let .defaultSaunaSetFetchFailed(localizedDescription):
return localizedDescription
case let .defaultSaunaSetSaveFailed(localizedDescription):
return localizedDescription
}
}
}

// MARK: - View model

@MainActor
final class SakatsuSettingsViewModel<
Repository: DefaultSaunaSetRepository,
Validator: SakatsuValidatorProtocol
>: ObservableObject {
@Published private(set) var uiState: SakatsuSettingsUiState

private let repository: Repository
private let validator: Validator

init(
repository: Repository = DefaultSaunaSetUserDefaultsClient.shared,
validator: Validator = SakatsuValidator()
) {
self.uiState = SakatsuSettingsUiState()
self.repository = repository
self.validator = validator
refreshDefaultSaunaSet()
}

private func refreshDefaultSaunaSet() {
do {
uiState.defaultSaunaSet = try repository.defaultSaunaSet()
} catch {
uiState.sakatsuSettingsError = .defaultSaunaSetFetchFailed(localizedDescription: error.localizedDescription)
}
}

private func saveDefaultSaunaSet() {
do {
try repository.saveDefaultSaunaSet(uiState.defaultSaunaSet)
} catch {
uiState.sakatsuSettingsError = .defaultSaunaSetSaveFailed(localizedDescription: error.localizedDescription)
}
}
}

// MARK: - Event handler

extension SakatsuSettingsViewModel {
func onDefaultSaunaTimeChange(defaultSaunaTime: TimeInterval?) {
guard validator.validate(saunaTime: defaultSaunaTime) else {
return
}
uiState.defaultSaunaSet.sauna.time = defaultSaunaTime
saveDefaultSaunaSet()
}

func onDefaultCoolBathTimeChange(defaultCoolBathTime: TimeInterval?) {
guard validator.validate(coolBathTime: defaultCoolBathTime) else {
return
}
uiState.defaultSaunaSet.coolBath.time = defaultCoolBathTime
saveDefaultSaunaSet()
}

func onDefaultRelaxationTimeChange(defaultRelaxationTime: TimeInterval?) {
guard validator.validate(relaxationTime: defaultRelaxationTime) else {
return
}
uiState.defaultSaunaSet.relaxation.time = defaultRelaxationTime
saveDefaultSaunaSet()
}

func onErrorAlertDismiss() {
uiState.sakatsuSettingsError = nil
}
}

0 comments on commit ca92c8e

Please sign in to comment.