From 4d49774ec897e757f5697ce3cd1656590b2d6fa5 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 31 Jan 2023 17:23:01 +0100 Subject: [PATCH] Fixes --- LeCountdown.xcodeproj/project.pbxproj | 4 + LeCountdown/Views/ContentView.swift | 83 +++++++++------ LeCountdown/Views/CountdownFormView.swift | 6 +- LeCountdown/Views/ImageSelectionView.swift | 51 +++++----- LeCountdown/Views/NewCountdownView.swift | 23 +++-- LeCountdown/Views/ReorderableForEach.swift | 113 +++++++++++++++++++++ 6 files changed, 216 insertions(+), 64 deletions(-) create mode 100644 LeCountdown/Views/ReorderableForEach.swift diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 6276340..e49492d 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -67,6 +67,7 @@ C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; }; C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; }; C4F8B15B29892D40005C86A5 /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA8E2987B83B0054D761 /* SoundPlayer.swift */; }; + C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -167,6 +168,7 @@ C4F8B1542988751B005C86A5 /* DialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialView.swift; sourceTree = ""; }; C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = ""; }; C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = ""; }; + C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -356,6 +358,7 @@ C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */, C438C80E29828B8600BF3EF9 /* RecordsView.swift */, C4F8B1542988751B005C86A5 /* DialView.swift */, + C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */, ); path = Views; sourceTree = ""; @@ -592,6 +595,7 @@ C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */, C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */, C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, + C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */, C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */, C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */, C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */, diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index e50c0d8..b20df15 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -72,44 +72,52 @@ struct ContentView: View { GridItem(spacing: 10.0), ] + var countdownsArray: [Countdown] { + return Array(countdowns) + } + var body: some View { NavigationStack { - - LazyVGrid( - columns: columns, - spacing: itemSpacing - ) { + GeometryReader { reader in + let width: CGFloat = reader.size.width / 2 - 10.0 - ForEach(countdowns) { countdown in + LazyVGrid( + columns: columns, + spacing: itemSpacing + ) { - ZStack(alignment: .topTrailing) { + ReorderableForEach(items: countdownsArray) { countdown in - Button { - self._launchCountdown(countdown) - } label: { + ZStack(alignment: .topTrailing) { + + Image(countdown.imageName).resizable() + + Button { + self._launchCountdown(countdown) + } label: { + + CountdownLiveView(countdown: countdown) + .environmentObject(Conductor.maestro) + + } + + NavigationLink { + CountdownEditView(countdown: countdown, isPresented: $isShowingNewCountdown) + .environment(\.managedObjectContext, viewContext) + } label: { + Image(systemName: "gearshape.fill") + .font(.system(size: 24, weight: .light)) + .padding() + .foregroundColor(Color.white) + } - CountdownLiveView(countdown: countdown) - .environmentObject(Conductor.maestro) - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .aspectRatio(1, contentMode: .fit) - .background(Image(countdown.imageName)) - .cornerRadius(40.0) - - } -// Text("ORder = \(countdown.order)") - - NavigationLink { - CountdownEditView(countdown: countdown, isPresented: $isShowingNewCountdown) - .environment(\.managedObjectContext, viewContext) - } label: { - Image(systemName: "gearshape.fill") - .font(.system(size: 24, weight: .light)) - .padding() - .foregroundColor(Color.white) } + .frame(width: width, height: width) + .cornerRadius(40.0) + } moveAction: { from, to in + self._reorder(from: from, to: to) } } @@ -142,6 +150,10 @@ struct ContentView: View { Image(systemName: "chart.bar.fill") } } + ToolbarItem(placement: .navigationBarLeading) { + EditButton() + } + } .onAppear { self._askPermissions() @@ -154,6 +166,19 @@ struct ContentView: View { } + fileprivate func _reorder(from: IndexSet, to: Int) { + var countdowns: [Countdown] = self.countdownsArray + countdowns.move(fromOffsets: from, toOffset: to) + for (i, countdown) in countdowns.enumerated() { + countdown.order = Int16(i) + } + do { + try viewContext.save() + } catch { + self.error = error + } + } + fileprivate func _startCountdownIfPossible(url: URL) { let urlString = url.absoluteString diff --git a/LeCountdown/Views/CountdownFormView.swift b/LeCountdown/Views/CountdownFormView.swift index 7a22414..c60492f 100644 --- a/LeCountdown/Views/CountdownFormView.swift +++ b/LeCountdown/Views/CountdownFormView.swift @@ -56,15 +56,15 @@ struct CountdownFormView : View { } label: { Group { if let image = self.imageBinding.wrappedValue { - Image(image.rawValue) + Image(image.rawValue).resizable() } else { - Image(imageBinding.wrappedValue.rawValue) + Image(imageBinding.wrappedValue.rawValue).resizable() } } .font(Font.system(size: 90.0)) .aspectRatio(1, contentMode: .fit) .frame(width: 100.0, height: 100.0) - .cornerRadius(40.0) + .cornerRadius(20.0) } diff --git a/LeCountdown/Views/ImageSelectionView.swift b/LeCountdown/Views/ImageSelectionView.swift index 38989e5..9828378 100644 --- a/LeCountdown/Views/ImageSelectionView.swift +++ b/LeCountdown/Views/ImageSelectionView.swift @@ -22,31 +22,33 @@ struct ImageSelectionView: View { NavigationStack { - ScrollView { - - LazyVGrid( - columns: columns, - spacing: 10.0 - ) { - - ForEach(CoolPic.allCases) { coolPic in + GeometryReader { reader in + let width: CGFloat = reader.size.width / 2.0 - 10.0 + + ScrollView { + LazyVGrid( + columns: columns, + spacing: 10.0 + ) { - Group { - Image(coolPic.rawValue) - .aspectRatio(contentMode: .fill) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .aspectRatio(1, contentMode: .fit) - .cornerRadius(40.0) - } - .onTapGesture { - self.imageBinding.wrappedValue = coolPic - self.showBinding.wrappedValue = false - print("coolPic = \(coolPic), image = \(String(describing: self.imageBinding.wrappedValue))") + ForEach(CoolPic.allCases) { coolPic in + ZStack { + Image(coolPic.rawValue) + .resizable() + .frame(width: width, height: width) + .cornerRadius(40.0) + } + .onTapGesture { + self.imageBinding.wrappedValue = coolPic + self.showBinding.wrappedValue = false + print("coolPic = \(coolPic), image = \(String(describing: self.imageBinding.wrappedValue))") + } + } - - } - }.padding(10.0) - }.navigationTitle("Background") + }.padding(10.0) + } + } + .navigationTitle("Background") } } @@ -54,6 +56,7 @@ struct ImageSelectionView: View { struct ImageSelectionView_Previews: PreviewProvider { static var previews: some View { - ImageSelectionView(showBinding: .constant(true), imageBinding: .constant(.pic3)) + ImageSelectionView(showBinding: .constant(true), + imageBinding: .constant(.pic3)) } } diff --git a/LeCountdown/Views/NewCountdownView.swift b/LeCountdown/Views/NewCountdownView.swift index 002924e..dceb899 100644 --- a/LeCountdown/Views/NewCountdownView.swift +++ b/LeCountdown/Views/NewCountdownView.swift @@ -54,9 +54,17 @@ struct CountdownEditView : View { @State var _isAdding: Bool = false + @Environment(\.isPresented) var envIsPresented + var body: some View { NavigationStack { - + Rectangle() + .frame(width: 0.0, height: 0.0) + .onChange(of: envIsPresented) { newValue in + if !newValue && !self._isAdding { + self._save() + } + } CountdownFormView( secondsBinding: $secondsString, minutesBinding: $minutesString, @@ -101,14 +109,13 @@ struct CountdownEditView : View { self._cancel() } } - } - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - self._save() + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + self._save() + } } - } - if !self._isAdding { - ToolbarItem(placement: .bottomBar) { + } else { + ToolbarItem(placement: .navigationBarTrailing) { Button { self.deleteConfirmationShown = true } label: { diff --git a/LeCountdown/Views/ReorderableForEach.swift b/LeCountdown/Views/ReorderableForEach.swift new file mode 100644 index 0000000..158f7ab --- /dev/null +++ b/LeCountdown/Views/ReorderableForEach.swift @@ -0,0 +1,113 @@ +// +// ForEachGridView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 31/01/2023. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct ReorderableForEach: View { + let items: [Item] + let content: (Item) -> Content + let moveAction: (IndexSet, Int) -> Void + + // A little hack that is needed in order to make view back opaque + // if the drag and drop hasn't ever changed the position + // Without this hack the item remains semi-transparent + @State private var hasChangedLocation: Bool = false + + init( + items: [Item], + @ViewBuilder content: @escaping (Item) -> Content, + moveAction: @escaping (IndexSet, Int) -> Void + ) { + self.items = items + self.content = content + self.moveAction = moveAction + } + + @State private var draggingItem: Item? + + var body: some View { + ForEach(items) { item in + content(item) + .overlay(draggingItem == item && hasChangedLocation ? Color.white.opacity(0.8) : Color.clear) + .onDrag { + draggingItem = item + return NSItemProvider(object: "\(item.id)" as NSString) + } + .onDrop( + of: [UTType.text], + delegate: DragRelocateDelegate( + item: item, + listData: items, + current: $draggingItem, + hasChangedLocation: $hasChangedLocation + ) { from, to in + withAnimation { + moveAction(from, to) + } + } + ) + } + } +} + +struct DragRelocateDelegate: DropDelegate { + let item: Item + var listData: [Item] + @Binding var current: Item? + @Binding var hasChangedLocation: Bool + + var moveAction: (IndexSet, Int) -> Void + + func dropEntered(info: DropInfo) { + guard item != current, let current = current else { return } + guard let from = listData.firstIndex(of: current), let to = listData.firstIndex(of: item) else { return } + + hasChangedLocation = true + + if listData[to] != current { + moveAction(IndexSet(integer: from), to > from ? to + 1 : to) + } + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + hasChangedLocation = false + current = nil + return true + } +} + +struct GridData : Identifiable, Equatable { + var id: Int + + var stringId: String { return "\(id)"} +} + +struct ForEachGridView_Previews: PreviewProvider { + + static var gridData = (0...10).map { GridData(id: $0) } + + static let side = 125.0 + + static var previews: some View { + + LazyVGrid(columns: [GridItem(.fixed(50.0)), GridItem(.fixed(side))], spacing: 10.0) { + ReorderableForEach(items: gridData) { data in + Text(data.stringId) + .frame(width: side, height: side) + .background(.cyan) + } moveAction: { from, to in + gridData.move(fromOffsets: from, toOffset: to) + } + } + + } +}