release
Laurent 3 years ago
parent 5410f41f82
commit 4d49774ec8
  1. 4
      LeCountdown.xcodeproj/project.pbxproj
  2. 83
      LeCountdown/Views/ContentView.swift
  3. 6
      LeCountdown/Views/CountdownFormView.swift
  4. 51
      LeCountdown/Views/ImageSelectionView.swift
  5. 23
      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 */; };
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 = "<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>"; };
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -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 */,

@ -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

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

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

@ -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: {

@ -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