Files
PlantGuide/PlantGuide/Presentation/Scenes/Rooms/RoomsListView.swift
Trey t 681476a499 WIP: Various UI and feature improvements
- 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>
2026-01-31 22:50:04 -06:00

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()
}