Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import Foundation
final class PushNotificationSettingsViewModel: Store {
struct State {
var pushNotificationEnable: Bool = false
var pushNotificationTime: Date = .init()
var viewPushNotificationTime: Date = .init()
var sheetPushNotificationTime: Date = .init()
var showTimePicker: Bool = false
var isLoading: Bool = false
var sheetHeight: CGFloat = .pi
Expand All @@ -19,31 +20,33 @@ final class PushNotificationSettingsViewModel: Store {
var alertTitle: String = ""
var alertMessage: String = ""
var pushNotificationHour: Int {
Calendar.current.component(.hour, from: pushNotificationTime)
Calendar.current.component(.hour, from: viewPushNotificationTime)
}
var pushNotificationMinute: Int {
Calendar.current.component(.minute, from: pushNotificationTime)
Calendar.current.component(.minute, from: viewPushNotificationTime)
}
}

enum Action {
case onAppear
case fetchSettings
case setAlert(Bool)
case setLoading(Bool)
case setPushNotificationEnable(Bool)
case setPushNotificationHour(Int)
case setPushNotificationTime(Date)
case setPushNotificationTime(view: Date? = nil, sheet: Date? = nil)
case setShowTimePicker(Bool)
case setSheetHeight(CGFloat)
case selectPresetTime(Date)
case confirmUpdate
case rollbackUpdate
}

enum SideEffect {
case fetchPushNotificationSettings
case updatePushNotificationSettings
}

private let calendar = Calendar.current
@Published private(set) var state: State = .init()
private let calendar = Calendar.current
private let fetchPushSettingsUseCase: FetchPushSettingsUseCase
private let updatePushSettingsUseCase: UpdatePushSettingsUseCase

Expand All @@ -57,36 +60,45 @@ final class PushNotificationSettingsViewModel: Store {

func reduce(with action: Action) -> [SideEffect] {
var state = self.state
var effects: [SideEffect] = []
switch action {
case .onAppear:
return [.fetchPushNotificationSettings]
case .fetchSettings:
effects = [.fetchPushNotificationSettings]
case .setAlert(let isPresented):
setAlert(&state, isPresented: isPresented)
case .setLoading(let value):
state.isLoading = value
case .setPushNotificationEnable(let value):
self.state.pushNotificationEnable = value
return [.updatePushNotificationSettings]
case .setPushNotificationHour(let value):
// 시간만 변경
if let newDate = calendar.date(
bySettingHour: value,
minute: 0, second: 0,
of: state.pushNotificationTime
) {
self.state.pushNotificationTime = newDate
return [.updatePushNotificationSettings]
state.pushNotificationEnable = value
effects = [.updatePushNotificationSettings]
case .setPushNotificationTime(let view, let sheet):
if let value = view {
state.viewPushNotificationTime = value
}
if let value = sheet {
state.sheetPushNotificationTime = value
}
case .setPushNotificationTime(let value):
self.state.pushNotificationTime = value
return [.updatePushNotificationSettings]
case .setShowTimePicker(let value):
state.showTimePicker = value
if !value {
state.sheetPushNotificationTime = state.viewPushNotificationTime
}
case .setSheetHeight(let value):
state.sheetHeight = value
case .selectPresetTime(let date):
state.viewPushNotificationTime = date
state.sheetPushNotificationTime = date
effects = [.updatePushNotificationSettings]
case .confirmUpdate:
state.showTimePicker = false
state.viewPushNotificationTime = state.sheetPushNotificationTime
effects = [.updatePushNotificationSettings]
case .rollbackUpdate:
state.showTimePicker = false
state.sheetPushNotificationTime = state.viewPushNotificationTime
}
self.state = state
return []
return effects
}

func run(_ effect: SideEffect) {
Expand All @@ -101,7 +113,7 @@ final class PushNotificationSettingsViewModel: Store {
if let hour = settings.scheduledTime.hour,
let minute = settings.scheduledTime.minute,
let date = calendar.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) {
self.send(.setPushNotificationTime(date))
self.send(.setPushNotificationTime(view: date, sheet: date))
}
} catch {
send(.setAlert(true))
Expand All @@ -112,14 +124,18 @@ final class PushNotificationSettingsViewModel: Store {
do {
defer { send(.setLoading(false)) }
send(.setLoading(true))
let dateComponents = calendar.dateComponents([.hour, .minute], from: state.pushNotificationTime)
let dateComponents = calendar.dateComponents(
[.hour, .minute],
from: state.sheetPushNotificationTime
)
Comment on lines +127 to +130

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

updatePushNotificationSettings 사이드 이펙트에서 state.sheetPushNotificationTime을 사용하여 시간을 업데이트하고 있습니다. sheetPushNotificationTime은 사용자가 시간 선택 창에서 임시로 선택한 시간이며, 아직 확정되지 않은 값일 수 있습니다. 예를 들어, 사용자가 시간 선택 창을 열어 시간을 변경한 후 '확인'을 누르지 않고 닫은 다음, 푸시 알림 활성화 토글을 누르면 아직 확정되지 않은 시간이 서버에 저장될 수 있습니다. 항상 확정된 시간을 사용하도록 state.viewPushNotificationTime을 사용해야 합니다.

Suggested change
let dateComponents = calendar.dateComponents(
[.hour, .minute],
from: state.sheetPushNotificationTime
)
let dateComponents = calendar.dateComponents(
[.hour, .minute],
from: state.viewPushNotificationTime
)

let settings = PushNotificationSettings(
isEnabled: state.pushNotificationEnable,
scheduledTime: dateComponents
)
try await updatePushSettingsUseCase.execute(settings)
} catch {
send(.setAlert(true))
send(.fetchSettings)
}
}
}
Expand Down
85 changes: 59 additions & 26 deletions DevLog/UI/Setting/PushNotificationSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ struct PushNotificationSettingsView: View {
}
.contentShape(Rectangle())
.onTapGesture {
viewModel.send(.setPushNotificationHour(hour))
viewModel.send(.selectPresetTime(date))
}
}
}
HStack {
Text("사용자 설정")
Spacer()
Text(formattedTimeString(viewModel.state.pushNotificationTime))
Text(formattedTimeString(viewModel.state.viewPushNotificationTime))
.foregroundStyle(.secondary)
if viewModel.state.pushNotificationMinute != 0 {
Image(systemName: "checkmark")
Expand All @@ -67,37 +67,70 @@ struct PushNotificationSettingsView: View {
}
}
.onAppear {
viewModel.send(.onAppear)
viewModel.send(.fetchSettings)
}
.sheet(isPresented: Binding(
get: { viewModel.state.showTimePicker },
set: { _ in viewModel.send(.setShowTimePicker(false)) }
set: { viewModel.send(.setShowTimePicker($0)) }
)) {
DatePicker(
"",
selection: Binding(
get: { viewModel.state.pushNotificationTime },
set: { viewModel.send(.setPushNotificationTime($0)) }
),
displayedComponents: .hourAndMinute
)
.datePickerStyle(.wheel)
.labelsHidden()
.presentationDragIndicator(.hidden)
.presentationDetents([.height(viewModel.state.sheetHeight)])
.onAppear {
UIDatePicker.appearance().minuteInterval = 5
NavigationStack {
DatePicker(
"",
selection: Binding(
get: { viewModel.state.sheetPushNotificationTime },
set: { viewModel.send(.setPushNotificationTime(sheet: $0)) }
),
displayedComponents: .hourAndMinute
)
.datePickerStyle(.wheel)
.labelsHidden()
.presentationDragIndicator(.hidden)
.presentationDetents([.height(viewModel.state.sheetHeight)])
.onAppear { UIDatePicker.appearance().minuteInterval = 5 }
.onDisappear { UIDatePicker.appearance().minuteInterval = 1 /* 기본값으로 복원 */ }
.toolbar { toolbar }
.background(
GeometryReader { geometry in
Color.clear.onAppear {
viewModel.send(.setSheetHeight(geometry.size.height))
}
}
)
}
.onDisappear {
UIDatePicker.appearance().minuteInterval = 1 // 기본값으로 복원
}
}

@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
if #available(iOS 26.0, *) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

iOS 버전 분기 처리에 if #available(iOS 26.0, *)를 사용하셨습니다. iOS 26.0은 아직 출시되지 않은 버전이므로 이 조건문은 항상 false를 반환하게 됩니다. 아마 iOS 16.0의 오타인 것 같습니다. Buttonrole 파라미터는 iOS 15.0부터 사용 가능하며, .cancel, .confirm 등의 역할(role)에 따른 기본 스타일과 동작을 제공합니다. 버전을 iOS 15.0 또는 iOS 16.0으로 수정해야 의도한 대로 동작합니다.

Suggested change
if #available(iOS 26.0, *) {
if #available(iOS 16.0, *) {

ToolbarItem(placement: .topBarLeading) {
Button(role: .cancel) {
viewModel.send(.rollbackUpdate)
}
}
.background(
GeometryReader { geometry in
Color.clear.onAppear {
viewModel.send(.setSheetHeight(geometry.size.height))
}

ToolbarItem(placement: .topBarTrailing) {
Button(role: .confirm) {
viewModel.send(.confirmUpdate)
}
}
} else {
ToolbarItem(placement: .topBarLeading) {
Button {
viewModel.send(.rollbackUpdate)
} label: {
Text("취소")
}
)
}

ToolbarItem(placement: .topBarTrailing) {
Button {
viewModel.send(.confirmUpdate)
} label: {
Text("확인")
.bold()
}
}
}
}

Expand Down