release
Laurent 3 years ago
parent 5410f41f82
commit 4d49774ec8
  1. 4
      LeCountdown.xcodeproj/project.pbxproj
  2. 39
      LeCountdown/Views/ContentView.swift
  3. 6
      LeCountdown/Views/CountdownFormView.swift
  4. 19
      LeCountdown/Views/ImageSelectionView.swift
  5. 15
      LeCountdown/Views/NewCountdownView.swift
  6. 113
      LeCountdown/Views/ReorderableForEach.swift

@ -67,6 +67,7 @@
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; }; C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; };
C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; }; C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; };
C4F8B15B29892D40005C86A5 /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA8E2987B83B0054D761 /* SoundPlayer.swift */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -167,6 +168,7 @@
C4F8B1542988751B005C86A5 /* DialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialView.swift; sourceTree = "<group>"; }; C4F8B1542988751B005C86A5 /* DialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialView.swift; sourceTree = "<group>"; };
C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; }; C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; };
C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; }; C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; };
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -356,6 +358,7 @@
C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */, C4060DF6297AFEF2003FAB80 /* NewCountdownView.swift */,
C438C80E29828B8600BF3EF9 /* RecordsView.swift */, C438C80E29828B8600BF3EF9 /* RecordsView.swift */,
C4F8B1542988751B005C86A5 /* DialView.swift */, C4F8B1542988751B005C86A5 /* DialView.swift */,
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -592,6 +595,7 @@
C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */, C4060DCC297AE73D003FAB80 /* LeCountdown.xcdatamodeld in Sources */,
C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */, C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */,
C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */,
C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */, C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */,
C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */, C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */,
C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */, C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */,

@ -72,33 +72,35 @@ struct ContentView: View {
GridItem(spacing: 10.0), GridItem(spacing: 10.0),
] ]
var countdownsArray: [Countdown] {
return Array(countdowns)
}
var body: some View { var body: some View {
NavigationStack { NavigationStack {
GeometryReader { reader in
let width: CGFloat = reader.size.width / 2 - 10.0
LazyVGrid( LazyVGrid(
columns: columns, columns: columns,
spacing: itemSpacing spacing: itemSpacing
) { ) {
ForEach(countdowns) { countdown in ReorderableForEach(items: countdownsArray) { countdown in
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
Image(countdown.imageName).resizable()
Button { Button {
self._launchCountdown(countdown) self._launchCountdown(countdown)
} label: { } label: {
CountdownLiveView(countdown: countdown) CountdownLiveView(countdown: countdown)
.environmentObject(Conductor.maestro) .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 { NavigationLink {
CountdownEditView(countdown: countdown, isPresented: $isShowingNewCountdown) CountdownEditView(countdown: countdown, isPresented: $isShowingNewCountdown)
@ -111,6 +113,12 @@ struct ContentView: View {
} }
} }
.frame(width: width, height: width)
.cornerRadius(40.0)
} moveAction: { from, to in
self._reorder(from: from, to: to)
}
} }
}.padding(itemSpacing) }.padding(itemSpacing)
@ -142,6 +150,10 @@ struct ContentView: View {
Image(systemName: "chart.bar.fill") Image(systemName: "chart.bar.fill")
} }
} }
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
} }
.onAppear { .onAppear {
self._askPermissions() 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) { fileprivate func _startCountdownIfPossible(url: URL) {
let urlString = url.absoluteString let urlString = url.absoluteString

@ -56,15 +56,15 @@ struct CountdownFormView : View {
} label: { } label: {
Group { Group {
if let image = self.imageBinding.wrappedValue { if let image = self.imageBinding.wrappedValue {
Image(image.rawValue) Image(image.rawValue).resizable()
} else { } else {
Image(imageBinding.wrappedValue.rawValue) Image(imageBinding.wrappedValue.rawValue).resizable()
} }
} }
.font(Font.system(size: 90.0)) .font(Font.system(size: 90.0))
.aspectRatio(1, contentMode: .fit) .aspectRatio(1, contentMode: .fit)
.frame(width: 100.0, height: 100.0) .frame(width: 100.0, height: 100.0)
.cornerRadius(40.0) .cornerRadius(20.0)
} }

@ -22,20 +22,20 @@ struct ImageSelectionView: View {
NavigationStack { NavigationStack {
ScrollView { GeometryReader { reader in
let width: CGFloat = reader.size.width / 2.0 - 10.0
ScrollView {
LazyVGrid( LazyVGrid(
columns: columns, columns: columns,
spacing: 10.0 spacing: 10.0
) { ) {
ForEach(CoolPic.allCases) { coolPic in ForEach(CoolPic.allCases) { coolPic in
ZStack {
Group {
Image(coolPic.rawValue) Image(coolPic.rawValue)
.aspectRatio(contentMode: .fill) .resizable()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .frame(width: width, height: width)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(40.0) .cornerRadius(40.0)
} }
.onTapGesture { .onTapGesture {
@ -46,7 +46,9 @@ struct ImageSelectionView: View {
} }
}.padding(10.0) }.padding(10.0)
}.navigationTitle("Background") }
}
.navigationTitle("Background")
} }
} }
@ -54,6 +56,7 @@ struct ImageSelectionView: View {
struct ImageSelectionView_Previews: PreviewProvider { struct ImageSelectionView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ImageSelectionView(showBinding: .constant(true), imageBinding: .constant(.pic3)) ImageSelectionView(showBinding: .constant(true),
imageBinding: .constant(.pic3))
} }
} }

@ -54,9 +54,17 @@ struct CountdownEditView : View {
@State var _isAdding: Bool = false @State var _isAdding: Bool = false
@Environment(\.isPresented) var envIsPresented
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Rectangle()
.frame(width: 0.0, height: 0.0)
.onChange(of: envIsPresented) { newValue in
if !newValue && !self._isAdding {
self._save()
}
}
CountdownFormView( CountdownFormView(
secondsBinding: $secondsString, secondsBinding: $secondsString,
minutesBinding: $minutesString, minutesBinding: $minutesString,
@ -101,14 +109,13 @@ struct CountdownEditView : View {
self._cancel() self._cancel()
} }
} }
}
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") { Button("Save") {
self._save() self._save()
} }
} }
if !self._isAdding { } else {
ToolbarItem(placement: .bottomBar) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
self.deleteConfirmationShown = true self.deleteConfirmationShown = true
} label: { } label: {

@ -0,0 +1,113 @@
//
// ForEachGridView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 31/01/2023.
//
import SwiftUI
import UniformTypeIdentifiers
struct ReorderableForEach<Content: View, Item: Identifiable & Equatable>: 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<Item: Equatable>: 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)
}
}
}
}
Loading…
Cancel
Save