- Add AllTasksView and PlantEditView components - Update CoreDataStack CloudKit container ID - Improve CameraView and IdentificationViewModel - Update MainTabView, RoomsListView, UpcomingTasksSection - Minor fixes to PlantGuideApp and SettingsViewModel Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
232 lines
7.0 KiB
Swift
232 lines
7.0 KiB
Swift
//
|
|
// RoomsListView.swift
|
|
// PlantGuide
|
|
//
|
|
// Created on 2026-01-23.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
// MARK: - RoomsListView
|
|
|
|
/// SwiftUI View for managing rooms from Settings.
|
|
///
|
|
/// Displays a list of all rooms with their icons and names.
|
|
/// Supports creating new rooms, editing existing rooms, deleting
|
|
/// non-default rooms, and reordering via drag-and-drop.
|
|
///
|
|
/// ## Features
|
|
/// - List of rooms with icon and name
|
|
/// - Swipe to delete (for non-default rooms)
|
|
/// - Drag to reorder
|
|
/// - Add button in toolbar
|
|
/// - Navigation to RoomEditorView for editing
|
|
/// - Empty state when no rooms exist
|
|
@MainActor
|
|
struct RoomsListView: View {
|
|
|
|
// MARK: - Properties
|
|
|
|
@State private var viewModel = RoomsViewModel()
|
|
|
|
/// Whether to show the create room sheet
|
|
@State private var showCreateSheet = false
|
|
|
|
/// Whether to show the delete confirmation dialog
|
|
@State private var showDeleteConfirmation = false
|
|
|
|
/// The room pending deletion (for confirmation)
|
|
@State private var roomToDelete: Room?
|
|
|
|
// MARK: - Body
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if viewModel.isLoading && viewModel.rooms.isEmpty {
|
|
loadingView
|
|
} else if viewModel.rooms.isEmpty {
|
|
emptyStateView
|
|
} else {
|
|
roomsList
|
|
}
|
|
}
|
|
.navigationTitle("Manage Rooms")
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button {
|
|
showCreateSheet = true
|
|
} label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
.accessibilityLabel("Add Room")
|
|
}
|
|
}
|
|
.task {
|
|
await viewModel.loadRooms()
|
|
}
|
|
.refreshable {
|
|
await viewModel.loadRooms()
|
|
}
|
|
.sheet(isPresented: $showCreateSheet, onDismiss: {
|
|
// Reload rooms after create to ensure UI reflects changes
|
|
Task {
|
|
await viewModel.loadRooms()
|
|
}
|
|
}) {
|
|
RoomEditorView(mode: .create) { name, icon in
|
|
await viewModel.createRoom(name: name, icon: icon)
|
|
}
|
|
}
|
|
.sheet(item: $viewModel.selectedRoom, onDismiss: {
|
|
// Reload rooms after edit to ensure UI reflects changes
|
|
Task {
|
|
await viewModel.loadRooms()
|
|
}
|
|
}) { room in
|
|
RoomEditorView(mode: .edit(room)) { name, icon in
|
|
var updatedRoom = room
|
|
updatedRoom.name = name
|
|
updatedRoom.icon = icon
|
|
await viewModel.updateRoom(updatedRoom)
|
|
}
|
|
}
|
|
.alert("Error", isPresented: .init(
|
|
get: { viewModel.errorMessage != nil },
|
|
set: { if !$0 { viewModel.clearError() } }
|
|
)) {
|
|
Button("OK", role: .cancel) {
|
|
viewModel.clearError()
|
|
}
|
|
} message: {
|
|
if let error = viewModel.errorMessage {
|
|
Text(error)
|
|
}
|
|
}
|
|
.confirmationDialog(
|
|
"Delete Room",
|
|
isPresented: $showDeleteConfirmation,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("Delete", role: .destructive) {
|
|
if let room = roomToDelete {
|
|
Task {
|
|
await viewModel.deleteRoom(room)
|
|
}
|
|
}
|
|
roomToDelete = nil
|
|
}
|
|
Button("Cancel", role: .cancel) {
|
|
roomToDelete = nil
|
|
}
|
|
} message: {
|
|
if let room = roomToDelete {
|
|
Text("Are you sure you want to delete \"\(room.name)\"? Plants in this room will be moved to \"Other\".")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Subviews
|
|
|
|
/// Loading state view
|
|
private var loadingView: some View {
|
|
ProgressView("Loading rooms...")
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
|
|
/// Empty state when no rooms exist
|
|
private var emptyStateView: some View {
|
|
ContentUnavailableView {
|
|
Label("No Rooms", systemImage: "house")
|
|
} description: {
|
|
Text("Add rooms to organize your plants by location.")
|
|
} actions: {
|
|
Button {
|
|
showCreateSheet = true
|
|
} label: {
|
|
Text("Add Room")
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
|
|
/// List of rooms with swipe actions and reordering
|
|
private var roomsList: some View {
|
|
List {
|
|
ForEach(viewModel.rooms) { room in
|
|
RoomRow(room: room)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
viewModel.selectedRoom = room
|
|
}
|
|
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
|
if viewModel.canDeleteRoom(room) {
|
|
Button(role: .destructive) {
|
|
roomToDelete = room
|
|
showDeleteConfirmation = true
|
|
} label: {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onMove { source, destination in
|
|
Task {
|
|
await viewModel.moveRooms(from: source, to: destination)
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
}
|
|
}
|
|
|
|
// MARK: - RoomRow
|
|
|
|
/// A single row in the rooms list displaying the room icon and name.
|
|
private struct RoomRow: View {
|
|
|
|
let room: Room
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Image(systemName: room.icon)
|
|
.font(.title3)
|
|
.foregroundStyle(Color.accentColor)
|
|
.frame(width: 32)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(room.name)
|
|
.font(.body)
|
|
|
|
if room.isDefault {
|
|
Text("Default")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.vertical, 4)
|
|
.accessibilityElement(children: .combine)
|
|
.accessibilityLabel("\(room.name)\(room.isDefault ? ", default room" : "")")
|
|
.accessibilityHint("Tap to edit room")
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
RoomsListView()
|
|
}
|
|
|
|
#Preview("Empty State") {
|
|
// For preview with empty state, would need mock view model
|
|
RoomsListView()
|
|
}
|