remindwall / RWPureSwift / Sources / EditSettingsNew / TopLevel / SettingsResolver.swift
Back to remindwall — Current RemindWall
import AppTypes
import CalendarAsync
import ComposableArchitecture
import Dao
import Dependencies
import Foundation
import PhotoKitAsync
/// Keeps the device-local album/calendar ids in step with their synced
/// descriptors. Runs for the app's lifetime from AppNavigation:
///
/// - descriptor present -> resolve it to a local id and cache it; no local
/// match clears the stale id, which is what surfaces "pick again" in the
/// pickers (their needsRePick is descriptor-set-and-id-nil).
/// - descriptor absent but an id is cached -> backfill the descriptor from
/// the local pick, so a pre-descriptor kiosk publishes its configuration
/// without a re-pick (the id-side twin of the S1.2 seed).
/// - a deliberate calendar "None" syncs as `.noSelection`, distinct from
/// absent, so it clears other devices instead of triggering backfill.
///
/// Reconciliation is skipped entirely while the library/calendar isn't
/// authorized - an unauthorized device must never clear a good id.
@Reducer
public struct SettingsResolverFeature: Sendable {
@Dependency(\.photoKitAlbums) var photoKitAlbums
@Dependency(\.calendarAsync) var calendarAsync
@ObservableState
public struct State: Equatable {
@Shared(.appStorage(ALBUM_SETTING_KEY)) var selectedAlbum: AlbumLocalId?
@Shared(.syncedSetting(ALBUM_DESCRIPTOR_SETTING_KEY)) var albumDescriptor: AlbumDescriptor?
@Shared(.appStorage(CALENDAR_SETTING_KEY)) var selectedCalendar: CalendarId?
@Shared(.syncedSetting(CALENDAR_DESCRIPTOR_SETTING_KEY)) var calendarDescriptor: CalendarDescriptor?
public init() {}
}
public enum Action {
case start
case stop
case _albumDescriptorChanged(AlbumDescriptor?)
case _calendarDescriptorChanged(CalendarDescriptor?)
case _setAlbum(AlbumLocalId?)
case _setAlbumDescriptor(AlbumDescriptor)
case _setCalendar(CalendarId?)
case _setCalendarDescriptor(CalendarDescriptor)
}
enum CancelID { case observation }
public init() {}
public var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .start:
// The publishers prepend the current value, so subscribing IS
// the launch reconcile; later emissions cover remote writes.
let albumDescriptor = state.$albumDescriptor
let calendarDescriptor = state.$calendarDescriptor
return .merge(
.publisher {
albumDescriptor.publisher.map(Action._albumDescriptorChanged)
},
.publisher {
calendarDescriptor.publisher.map(Action._calendarDescriptorChanged)
}
)
.cancellable(id: CancelID.observation, cancelInFlight: true)
case .stop:
return .cancel(id: CancelID.observation)
case let ._albumDescriptorChanged(descriptor):
guard photoKitAlbums.libraryAccess() == .authorized else { return .none }
if let descriptor {
return .run { [photoKitAlbums] send in
await send(._setAlbum(photoKitAlbums.findAlbum(descriptor)))
}
}
guard let cached = state.selectedAlbum else { return .none }
return .run { [photoKitAlbums] send in
guard let backfilled = await photoKitAlbums.albumDescriptor(cached) else { return }
await send(._setAlbumDescriptor(backfilled))
}
case let ._calendarDescriptorChanged(descriptor):
guard calendarAsync.calendarAccess() == .fullAccess else { return .none }
if let descriptor {
if descriptor == .noSelection {
return .run { send in await send(._setCalendar(nil)) }
}
return .run { [calendarAsync] send in
await send(._setCalendar(calendarAsync.findCalendar(descriptor)))
}
}
guard let cached = state.selectedCalendar else { return .none }
return .run { [calendarAsync] send in
guard let backfilled = calendarAsync.calendarDescriptor(cached) else { return }
await send(._setCalendarDescriptor(backfilled))
}
case let ._setAlbum(albumId):
if state.selectedAlbum != albumId {
state.$selectedAlbum.withLock { $0 = albumId }
}
return .none
case let ._setAlbumDescriptor(descriptor):
state.$albumDescriptor.withLock { $0 = descriptor }
return .none
case let ._setCalendar(calendarId):
if state.selectedCalendar != calendarId {
state.$selectedCalendar.withLock { $0 = calendarId }
}
return .none
case let ._setCalendarDescriptor(descriptor):
state.$calendarDescriptor.withLock { $0 = descriptor }
return .none
}
}
}
}