struct code for the section
unknown
swift
3 months ago
23 kB
5
Indexable
struct ImageDetailView: View { private var board: SavedBoard? // State properties @State private var imageName: String @State private var selectedCars: [DraggableCar] @State private var selectedRoadSigns: [DraggableRoadSign] @State private var isSaveBoardModalPresented = false @State private var boardName: String = "" @State private var saveMessage: String? @State private var selectedCarID: UUID? @State private var currentPath = DrawnPath(path: Path(), color: Color.black, size: 5) @State private var paths: [DrawnPath] = [] @State private var undonePaths: [DrawnPath] = [] @State private var isDrawing = false @State private var strokeColor: Color = .red @State private var strokeSize: CGFloat = 5 @State private var isRoadSignMenuPresented = false @State private var isCarMenuPresented = false // Initializer for loading a saved board init(board: SavedBoard) { self.board = board _imageName = State(initialValue: board.imageName) _selectedCars = State(initialValue: board.selectedCars) _selectedRoadSigns = State(initialValue: board.selectedSigns) _paths = State(initialValue: board.drawnPaths) } // Initializer for new board creation init(imageName: String) { self.board = nil _imageName = State(initialValue: imageName) _selectedCars = State(initialValue: []) _selectedRoadSigns = State(initialValue: []) _paths = State(initialValue: []) // Initialize empty paths } @Environment(\.colorScheme) var colorScheme var isIphone: Bool { UIDevice.current.userInterfaceIdiom == .phone } var isIpad: Bool { UIDevice.current.userInterfaceIdiom == .pad } var iconColor: Color { if isIpad { return .white } else { return colorScheme == .dark ? .white : .black } } var body: some View { GeometryReader { geometry in ZStack { Image(imageName) .resizable() .aspectRatio(contentMode: isIphone ? .fit : .fill) .frame(width: geometry.size.width, height: geometry.size.height) .edgesIgnoringSafeArea(.all) // Drawing canvas if isDrawing { Canvas { context, size in context.stroke(currentPath.path, with: .color(currentPath.color), lineWidth: currentPath.size) for drawnPath in paths { context.stroke(drawnPath.path, with: .color(drawnPath.color), lineWidth: drawnPath.size) } } .background(Color.clear) .gesture( DragGesture(minimumDistance: 0) .onChanged { value in guard isDrawing else { return } let newPoint = value.location if currentPath.path.isEmpty { currentPath.path.move(to: newPoint) } else { currentPath.path.addLine(to: newPoint) } } .onEnded { _ in if isDrawing { paths.append(currentPath) currentPath = DrawnPath(path: Path(), color: strokeColor, size: strokeSize) undonePaths.removeAll() } } ) } else { Canvas { context, size in for drawnPath in paths { context.stroke(drawnPath.path, with: .color(drawnPath.color), lineWidth: drawnPath.size) } } .background(Color.clear) } // // Stroke size slider in the pencil section // if isDrawing { // VStack { // HStack { // Text("") // .foregroundColor(.white) // .padding(.leading, 20) // Slider(value: $strokeSize, in: 1...20, step: 1) // .accentColor(strokeColor) // .padding(.trailing, 20) // .frame(width: 500) // } // .padding(.top, 930) // Adjusted to move it to the top // Spacer() // } // } // Draggable and rotatable cars ForEach($selectedCars) { $car in ZStack { Image(car.imageName) .resizable() .scaledToFit() .frame(width: car.size.width, height: car.size.height) // Use car.size .rotationEffect(car.rotation, anchor: .center) // Rotate around its center .position(car.position) // Rotation Handle if selectedCarID == car.id { // Show the handle only for the selected car ZStack { // Blue Circle Background Circle() .fill(Color.gray) .frame(width: 40, height: 40) // Clockwise Arrow Icon (inside the circle) Image("rotation-clock+anticlock") .resizable() .scaledToFit() .frame(width: 30, height: 30) .foregroundColor(.white) } .position( x: car.position.x + cos(car.rotation.radians) * 70, y: car.position.y + sin(car.rotation.radians) * 70 ) .gesture( DragGesture() .onChanged { value in // Calculate angle based on car center and drag position let dx = value.location.x - car.position.x let dy = value.location.y - car.position.y let angle = atan2(dy, dx) car.rotation = .radians(angle) } ) } } .gesture( DragGesture() .onChanged { value in car.position = value.location // Update position while dragging } ) .onTapGesture { selectedCarID = (selectedCarID == car.id) ? nil : car.id // Toggle selection } .zIndex(selectedCarID == car.id ? 2 : 1) // Highlight selected car } // Rotation and Delete Buttons for selected car if let selectedCar = selectedCars.first(where: { $0.id == selectedCarID }) { HStack { // Rotation Button Button(action: { if let index = selectedCars.firstIndex(where: { $0.id == selectedCarID }) { selectedCars[index].rotation += .degrees(15) // Increment rotation by 15 degrees } }) { Image(systemName: "arrow.clockwise.circle.fill") .resizable() .frame(width: 0, height: 0) .foregroundColor(.blue) } // Delete Button Button(action: { selectedCars.removeAll { $0.id == selectedCarID } // Remove selected car selectedCarID = nil // Deselect after deletion }) { Image(systemName: "trash.fill") .resizable() .frame(width: 40, height: 40) .foregroundColor(.red) } } .position(x: selectedCar.position.x + 120, y: selectedCar.position.y) // Position buttons near the car } ForEach($selectedRoadSigns) { $roadSign in ZStack { // Road sign image Image(roadSign.imageName) .resizable() .scaledToFit() .frame(width: roadSign.imageName == "discount for you only - 42" ? 320 : 100, height: roadSign.imageName == "discount for you only - 42" ? 320 : 100) .rotationEffect(roadSign.rotation) // Apply rotation .position(roadSign.position) .onTapGesture { // Toggle selection roadSign.isSelected.toggle() roadSign.zIndex = roadSign.isSelected ? 10 : 0 } // Conditionally show trash icon and rotation handle if the sign is selected if roadSign.isSelected { // Trash icon Image(systemName: "trash.fill") .resizable() .scaledToFit() .frame(width: 40, height: 80) .font(.system(size: 40)) .foregroundColor(.red) .position(x: roadSign.position.x + 80, y: roadSign.position.y - 80) .onTapGesture { if let index = selectedRoadSigns.firstIndex(where: { $0.id == roadSign.id }) { selectedRoadSigns.remove(at: index) } } // Rotation handle (matching the car's style) ZStack { // Blue Circle Background Circle() .fill(Color.gray) .frame(width: 40, height: 40) // Clockwise Arrow Icon (inside the circle) Image("rotation-clock+anticlock") // Use the same image as the car's rotation handle .resizable() .scaledToFit() .frame(width: 30, height: 30) .foregroundColor(.white) } .position( x: roadSign.position.x + cos(roadSign.rotation.radians) * 70, // Fixed distance from center y: roadSign.position.y + sin(roadSign.rotation.radians) * 70 ) .gesture( DragGesture() .onChanged { value in // Calculate angle based on road sign center and drag position let dx = value.location.x - roadSign.position.x let dy = value.location.y - roadSign.position.y let angle = atan2(dy, dx) roadSign.rotation = .radians(angle) } ) } } .gesture( DragGesture() .onChanged { value in roadSign.position = value.location // Update position while dragging } ) .zIndex(roadSign.zIndex) } // Toolbar at the bottom VStack { Spacer() HStack { // Road Sign Menu Icon Button(action: { isRoadSignMenuPresented.toggle() }) { Image(systemName: "signpost.left.fill") // You can change this icon as desired .resizable() .scaledToFit() .frame(width: 40, height: 40) .foregroundColor(iconColor) .padding(10) .background(Circle().stroke(iconColor, lineWidth: 2)) } .sheet(isPresented: $isRoadSignMenuPresented) { RoadSignMenuView(selectedRoadSigns: $selectedRoadSigns) } // Car menu button Button(action: { isCarMenuPresented.toggle() }) { Image(systemName: "car.fill") .resizable() .scaledToFit() .frame(width: 40, height: 40) .foregroundColor(iconColor) .padding(10) .background(Circle().stroke(iconColor, lineWidth: 2)) } .sheet(isPresented: $isCarMenuPresented) { CarMenuView(selectedCars: $selectedCars) } Spacer() // Save Board Button Button(action: { isSaveBoardModalPresented.toggle() }) { Text("Save Board") .foregroundColor(.black) .padding(10) .background(Capsule().fill(Color.white)) .overlay(Capsule().stroke(Color.black, lineWidth: 2)) // Black outline } // Undo, redo, clear, and drawing toggle buttons if isDrawing { Button(action: undo) { Image(systemName: "arrow.uturn.backward.circle.fill") .resizable() .scaledToFit() .frame(width: 40, height: 40) .foregroundColor(iconColor) .padding(10) .background(Circle().stroke(iconColor, lineWidth: 2)) } Button(action: redo) { Image(systemName: "arrow.uturn.forward.circle.fill") .resizable() .scaledToFit() .frame(width: 40, height: 40) .foregroundColor(iconColor) .padding(10) .background(Circle().stroke(iconColor, lineWidth: 2)) } Button(action: clearDrawing) { Image(systemName: "trash.fill") .resizable() .scaledToFit() .frame(width: 40, height: 40) .foregroundColor(iconColor) .padding(10) .background(Circle().stroke(iconColor, lineWidth: 2)) } } Button(action: { if !isDrawing { currentPath = DrawnPath(path: Path(), color: strokeColor, size: strokeSize) } isDrawing.toggle() }) { Image(systemName: "pencil.circle.fill") .resizable() .scaledToFit() .frame(width: 40, height: 40) .foregroundColor(iconColor) .padding(10) .background(Circle().stroke(iconColor, lineWidth: 2)) } } .padding(.horizontal, 20) .padding(.vertical, 15) .padding(.bottom, geometry.safeAreaInsets.bottom) // Respect bottom safe area } .zIndex(1) } } .sheet(isPresented: $isSaveBoardModalPresented) { VStack { Text("Save Board") .font(.title) .padding() TextField("Enter board name", text: $boardName) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() // Display save message if available if let message = saveMessage { Text(message) .foregroundColor(message.contains("Error") || message == "Board name cannot be empty" ? .red : .green) .padding() } HStack { Button("Close") { isSaveBoardModalPresented = false saveMessage = nil boardName = "" } .padding() .background(Color.red) .foregroundColor(.white) .cornerRadius(8) Button("Save") { saveBoard() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) } } .padding() .frame(width: 300, height: 200) } .navigationBarBackButtonHidden(false) } func saveBoard() { guard !boardName.isEmpty else { saveMessage = "Board name cannot be empty" return } do { // Create the SavedBoard object let newBoard = SavedBoard( name: boardName, imageName: imageName, selectedCars: selectedCars, selectedSigns: selectedRoadSigns, drawnPaths: paths // Include drawn paths ) // Encode to JSON let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(newBoard) // Create filename (sanitize for filesystem) let sanitizedFileName = boardName.replacingOccurrences(of: " ", with: "_") .filter { $0.isLetter || $0.isNumber || $0 == "_" } let fileURL = getDocumentsDirectory() .appendingPathComponent("\(sanitizedFileName).json") print("Saving file to: \(fileURL.path)") // Print the path to console // Save to documents directory try data.write(to: fileURL, options: .atomic) saveMessage = "Board saved successfully!" boardName = "" // Clear the name field // isSaveBoardModalPresented = false // Close the modal } catch { saveMessage = "Error saving board: \(error.localizedDescription)" } } func sanitizeFileName(_ name: String) -> String { let invalidCharacters = CharacterSet(charactersIn: ":/\\?%*|\"<>") return name.components(separatedBy: invalidCharacters).joined(separator: "") } private func undo() { if let lastPath = paths.last { paths.removeLast() undonePaths.append(lastPath) } } private func redo() { if let lastUndonePath = undonePaths.last { undonePaths.removeLast() paths.append(lastUndonePath) } } private func clearDrawing() { paths.removeAll() undonePaths.removeAll() currentPath = DrawnPath(path: Path(), color: strokeColor, size: strokeSize) } }
Editor is loading...
Leave a Comment