remindwall / RWPureSwift / Sources / EditSettingsNew / TopLevel / CalendarPicker.swift
Back to remindwall — Current RemindWall
//
// CalendarPicker.swift
// RWPureSwift
//
// Created by Christopher Hotchkiss on 2/14/26.
//
import AppTypes
import CalendarAsync
import ComposableArchitecture
import Dao
import Dependencies
@preconcurrency import EventKit
import SwiftUI
@Reducer
public struct CalendarPickerFeature {
@Dependency(\.calendarAsync) var calendarAsync
@ObservableState
public struct State: Equatable {
@Shared(.appStorage(CALENDAR_SETTING_KEY)) var selectedCalendar: CalendarId?
@Shared(.syncedSetting(CALENDAR_DESCRIPTOR_SETTING_KEY)) var calendarDescriptor: CalendarDescriptor?
var calendarStatus: EKAuthorizationStatus = .notDetermined
var availableCalendars: [EKCalendar]?
/// A synced pick exists but no local calendar matched it.
public var needsRePick: Bool {
selectedCalendar == nil
&& calendarDescriptor != nil
&& calendarDescriptor != .noSelection
}
public init() {}
}
public enum Action {
case onAppear
case selectCalendar(CalendarId?)
case tapOpenSettings
case tapAuthorizeAccess
case authorizationComplete(Bool)
case loadListComplete([EKCalendar]?)
}
public init() {}
public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
state.calendarStatus = calendarAsync.calendarAccess()
return loadList(state: &state)
case let .selectCalendar(calendar):
state.$selectedCalendar.withLock { $0 = calendar }
// Sync title+source so other devices can resolve the same
// calendar; an explicit "None" pick syncs as .noSelection.
// A failed lookup writes nothing - never clobber a synced
// descriptor with a guess.
if let calendar {
if let matched = state.availableCalendars?
.first(where: { $0.calendarIdentifier == calendar.rawValue }) {
state.$calendarDescriptor.withLock {
$0 = CalendarDescriptor(
title: matched.title,
sourceTitle: matched.source?.title ?? ""
)
}
}
} else {
state.$calendarDescriptor.withLock { $0 = .noSelection }
}
return .none
case .tapOpenSettings:
return .run { [calendarAsync] send in
await calendarAsync.openCalendarSettings()
}
case .tapAuthorizeAccess:
return .run { [calendarAsync] send in
let granted = try await calendarAsync.requestAccess()
await send(.authorizationComplete(granted))
}
case let .authorizationComplete(granted):
state.calendarStatus = calendarAsync.calendarAccess()
if granted {
return loadList(state: &state)
}
return .none
case let .loadListComplete(list):
state.availableCalendars = list
return .none
}
}
}
func loadList(state: inout State) -> Effect<Action> {
if state.calendarStatus != .fullAccess {
state.availableCalendars = nil
return .none
}
return .run { [calendarAsync] send in
let calendars = calendarAsync.getCalendars()
await send(.loadListComplete(calendars.isEmpty ? nil : calendars))
}
}
}
public struct CalendarPickerView: View {
let store: StoreOf<CalendarPickerFeature>
public init(store: StoreOf<CalendarPickerFeature>) {
self.store = store
}
public var body: some View {
HStack {
if store.calendarStatus == .denied {
Text("In order to use calendar events you will need to allow calendar access in the Settings App.")
Button("Open Settings Application") {
store.send(.tapOpenSettings)
}
} else if store.calendarStatus == .restricted {
Text("In order to use calendar events you will need to allow calendar access from Screen Time.")
Button("Open Settings Application") {
store.send(.tapOpenSettings)
}
} else if store.calendarStatus != .fullAccess {
Button("Authorize Calendar Access") {
store.send(.tapAuthorizeAccess)
}
} else if store.availableCalendars == nil {
ContentUnavailableView("No Calendars Found", systemImage: "calendar")
} else {
VStack(alignment: .leading) {
if store.needsRePick {
Label(
"Synced calendar \"\(store.calendarDescriptor?.title ?? "")\" isn't on this device — pick again.",
systemImage: "exclamationmark.triangle"
)
.foregroundStyle(.orange)
.accessibilityIdentifier("CalendarNeedsRePick")
}
Picker("Calendars", selection: Binding(
get: { store.selectedCalendar },
set: { store.send(.selectCalendar($0)) }
)) {
Text("None").tag(nil as CalendarId?)
ForEach(store.availableCalendars!, id: \.calendarIdentifier) { calendar in
Text(calendar.title).tag(CalendarId(calendar.calendarIdentifier) as CalendarId?)
}
}
#if !os(macOS)
.pickerStyle(.navigationLink)
#endif
}
}
}.onAppear {
store.send(.onAppear)
}
}
}
#Preview {
let _ = prepareDependencies {
$0.defaultDatabase = try! $0.appDatabase()
}
NavigationStack {
Form {
CalendarPickerView(
store: Store(
initialState: CalendarPickerFeature.State()
) {
CalendarPickerFeature()
}
)
}
}
}