struct code for the section
unknown
swift
9 months ago
23 kB
6
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