completeevent button has a custo progress bar to work on ios aswell

extracted the com[lete button
This commit is contained in:
neon443
2025-06-15 18:55:38 +01:00
parent 6533fb85ed
commit 0cffe243eb
4 changed files with 216 additions and 182 deletions

View File

@@ -73,6 +73,8 @@
A98C20CE2DE7308E0008D61C /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CD2DE7308E0008D61C /* ArchiveView.swift */; }; A98C20CE2DE7308E0008D61C /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CD2DE7308E0008D61C /* ArchiveView.swift */; };
A98C20D02DE731BD0008D61C /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CF2DE731BD0008D61C /* HomeView.swift */; }; A98C20D02DE731BD0008D61C /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20CF2DE731BD0008D61C /* HomeView.swift */; };
A98C20D42DE7339E0008D61C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20D32DE7339E0008D61C /* AboutView.swift */; }; A98C20D42DE7339E0008D61C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98C20D32DE7339E0008D61C /* AboutView.swift */; };
A9BAC6882DFF242300EC8E44 /* CompleteEventButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */; };
A9BAC6892DFF242300EC8E44 /* CompleteEventButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */; };
A9D1C34D2DFE10FA00703C2D /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8402DCAABE00064DCA0 /* EventListView.swift */; }; A9D1C34D2DFE10FA00703C2D /* EventListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949F8402DCAABE00064DCA0 /* EventListView.swift */; };
A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */; }; A9FC7EEA2D2823920020D75B /* NearFutureWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -157,6 +159,7 @@
A98C20CD2DE7308E0008D61C /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = "<group>"; }; A98C20CD2DE7308E0008D61C /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = "<group>"; };
A98C20CF2DE731BD0008D61C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; }; A98C20CF2DE731BD0008D61C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
A98C20D32DE7339E0008D61C /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; A98C20D32DE7339E0008D61C /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteEventButton.swift; sourceTree = "<group>"; };
A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; }; A9FC7EE92D28238A0020D75B /* NearFutureWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgets.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -227,6 +230,7 @@
A90D49512DDE2D0000781124 /* Extensions.swift */, A90D49512DDE2D0000781124 /* Extensions.swift */,
A90D49202DDE0A3B00781124 /* Model */, A90D49202DDE0A3B00781124 /* Model */,
A91EF80A2DFC910000B8463D /* ViewModifiers.swift */, A91EF80A2DFC910000B8463D /* ViewModifiers.swift */,
A9BAC6872DFF238100EC8E44 /* CompleteEventButton.swift */,
); );
path = Shared; path = Shared;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -529,6 +533,7 @@
A91EF80E2DFC9A0C00B8463D /* WhatsNewView.swift in Sources */, A91EF80E2DFC9A0C00B8463D /* WhatsNewView.swift in Sources */,
A91EF8192DFD77BF00B8463D /* SymbolsLoader.swift in Sources */, A91EF8192DFD77BF00B8463D /* SymbolsLoader.swift in Sources */,
A95E9EE42DFC77D400ED655F /* ImportView.swift in Sources */, A95E9EE42DFC77D400ED655F /* ImportView.swift in Sources */,
A9BAC6892DFF242300EC8E44 /* CompleteEventButton.swift in Sources */,
A91EF80C2DFC910000B8463D /* ViewModifiers.swift in Sources */, A91EF80C2DFC910000B8463D /* ViewModifiers.swift in Sources */,
A95E9ED92DFC742B00ED655F /* AccentIcon.swift in Sources */, A95E9ED92DFC742B00ED655F /* AccentIcon.swift in Sources */,
A91EF8102DFCB66C00B8463D /* SettingsView.swift in Sources */, A91EF8102DFCB66C00B8463D /* SettingsView.swift in Sources */,
@@ -575,6 +580,7 @@
A95E9ED82DFC742B00ED655F /* AccentIcon.swift in Sources */, A95E9ED82DFC742B00ED655F /* AccentIcon.swift in Sources */,
A90D49532DDE2D0000781124 /* Extensions.swift in Sources */, A90D49532DDE2D0000781124 /* Extensions.swift in Sources */,
A949F8522DCAABE00064DCA0 /* iCloudSettingsView.swift in Sources */, A949F8522DCAABE00064DCA0 /* iCloudSettingsView.swift in Sources */,
A9BAC6882DFF242300EC8E44 /* CompleteEventButton.swift in Sources */,
A949F8532DCAABE00064DCA0 /* ImportView.swift in Sources */, A949F8532DCAABE00064DCA0 /* ImportView.swift in Sources */,
A949F8542DCAABE00064DCA0 /* SettingsView.swift in Sources */, A949F8542DCAABE00064DCA0 /* SettingsView.swift in Sources */,
A91EF81E2DFD796600B8463D /* SymbolsPicker.swift in Sources */, A91EF81E2DFD796600B8463D /* SymbolsPicker.swift in Sources */,

View File

@@ -12,43 +12,11 @@ struct EventListView: View {
@ObservedObject var viewModel: EventViewModel @ObservedObject var viewModel: EventViewModel
@State var event: Event @State var event: Event
@State var completeInProgress: Bool = false
@State var completeStartTime: Date = .now
@State var progress: Double = 0
@State var timer: Timer?
private let completeDuration: TimeInterval = 3.0
@Environment(\.openWindow) var openWindow @Environment(\.openWindow) var openWindow
@State var largeTick: Bool = false
@State var hovering: Bool = false @State var hovering: Bool = false
func startCompleting() { #if canImport(AppKit)
#if canImport(UIKit)
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
#endif
completeInProgress = true
progress = 0
completeStartTime = .now
timer = Timer(timeInterval: 0.01, repeats: true) { timer in
guard timer.isValid else { return }
guard completeInProgress else { return }
let elapsed = Date().timeIntervalSince(completeStartTime)
progress = min(elapsed, 1.0)
if progress >= 1.0 {
timer.invalidate()
viewModel.completeEvent(&event)
#if canImport(UIKit)
UINotificationFeedbackGenerator().notificationOccurred(.success)
#endif
completeInProgress = false
}
}
RunLoop.main.add(timer!, forMode: .common)
}
#if canImport(AppKit)
var body: some View { var body: some View {
ZStack { ZStack {
Color.black.opacity(hovering ? 0.5 : 0.0) Color.black.opacity(hovering ? 0.5 : 0.0)
@@ -107,42 +75,9 @@ struct EventListView: View {
.multilineTextAlignment(.trailing) .multilineTextAlignment(.trailing)
.foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one) .foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
} }
CompleteEventButton(
Group { viewModel: viewModel,
if completeInProgress { event: $event
ZStack {
ProgressView(value: progress)
.progressViewStyle(.circular)
Image(systemName: "xmark")
.bold()
}
.onTapGesture {
timer?.invalidate()
completeInProgress = false
progress = 0
}
} else {
Image(systemName: event.complete ? "checkmark.circle.fill" : "circle")
.resizable().scaledToFit()
.foregroundStyle(event.complete ? .green : event.color.color)
.bold()
.onTapGesture {
startCompleting()
}
.onHover() { hovering in
withAnimation {
largeTick.toggle()
}
}
.scaleEffect(largeTick ? 1.5 : 1)
}
}
.frame(maxWidth: 20)
.shadow(color: .one.opacity(0.2), radius: 2.5)
.padding(.trailing, 15)
.animation(
.spring(response: 0.2, dampingFraction: 0.75, blendDuration: 2),
value: largeTick
) )
} }
.transition(.opacity) .transition(.opacity)
@@ -170,131 +105,94 @@ struct EventListView: View {
} }
} }
} }
#else #else
var body: some View { var body: some View {
NavigationLink() { HStack {
EditEventView( RoundedRectangle(cornerRadius: 5)
viewModel: viewModel, .frame(width: 7)
event: $event .foregroundStyle(
) event.color.color.opacity(
} label: { event.complete ? 0.5 : 1
ZStack { )
)
VStack(alignment: .leading) {
HStack { HStack {
RoundedRectangle(cornerRadius: 5) Image(systemName: event.symbol)
.frame(width: 7) .resizable()
.foregroundStyle( .scaledToFit()
event.color.color.opacity( .frame(width: 20, height: 20)
event.complete ? 0.5 : 1 .shadow(radius: 5)
)
)
VStack(alignment: .leading) {
HStack {
Image(systemName: event.symbol)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.shadow(radius: 5)
.foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
)
Text("\(event.name)")
.font(.headline)
.foregroundStyle(.one)
.strikethrough(event.complete)
.multilineTextAlignment(.leading)
}
if !event.notes.isEmpty {
Text(event.notes)
.font(.subheadline)
.foregroundStyle(.one.opacity(0.8))
.multilineTextAlignment(.leading)
}
Text(
event.date.formatted(
date: .long,
time: .shortened
)
)
.font(.subheadline)
.foregroundStyle( .foregroundStyle(
.one.opacity( .one.opacity(
event.complete ? 0.5 : 1 event.complete ? 0.5 : 1
) )
) )
if event.recurrence != .none { Text("\(event.name)")
Text("Occurs \(event.recurrence.rawValue)") .font(.headline)
.font(.subheadline) .foregroundStyle(.one)
.foregroundStyle( .strikethrough(event.complete)
.one.opacity(event.complete ? 0.5 : 1)) .multilineTextAlignment(.leading)
}
}
Spacer()
VStack {
Text("\(daysUntilEvent(event.date).long)")
.font(.subheadline)
.foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
.multilineTextAlignment(.trailing)
}
Button() {
startCompleting()
} label: {
if completeInProgress {
ZStack {
ProgressView(value: progress)
.progressViewStyle(.circular)
Image(systemName: "xmark")
.bold()
}
.onTapGesture {
timer?.invalidate()
completeInProgress = false
progress = 0
}
} else {
Image(systemName: event.complete ? "checkmark.circle.fill" : "circle")
.resizable().scaledToFit()
.foregroundStyle(event.complete ? .green : event.color.color)
.bold()
}
}
.buttonStyle(.borderless)
.frame(maxWidth: 25, maxHeight: 25)
.shadow(radius: 5)
.padding(.trailing, 5)
.modifier(hapticSuccess(trigger: event.complete))
} }
.transition(.opacity) if !event.notes.isEmpty {
.padding(.vertical, 5) Text(event.notes)
.overlay( .font(.subheadline)
RoundedRectangle(cornerRadius: 10) .foregroundStyle(.one.opacity(0.8))
.stroke( .multilineTextAlignment(.leading)
.one.opacity(0.5), }
lineWidth: 1 Text(
) event.date.formatted(
date: .long,
time: .shortened
)
) )
.clipShape( .font(.subheadline)
RoundedRectangle(cornerRadius: 10) .foregroundStyle(
.one.opacity(
event.complete ? 0.5 : 1
)
) )
.fixedSize(horizontal: false, vertical: true) if event.recurrence != .none {
Text("Occurs \(event.recurrence.rawValue)")
.font(.subheadline)
.foregroundStyle(
.one.opacity(event.complete ? 0.5 : 1))
}
} }
.contextMenu() { Spacer()
Button(role: .destructive) { VStack {
let eventToModify = viewModel.events.firstIndex() { currEvent in Text("\(daysUntilEvent(event.date).long)")
currEvent.id == event.id .font(.subheadline)
} .foregroundStyle(event.date.timeIntervalSinceNow < 0 ? .red : .one)
if let eventToModify = eventToModify { .multilineTextAlignment(.trailing)
viewModel.events.remove(at: eventToModify) }
viewModel.saveEvents() CompleteEventButton(
} viewModel: viewModel,
} label: { event: $event
Label("Delete", systemImage: "trash") )
}
.transition(.opacity)
.padding(.vertical, 5)
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(.one.opacity(0.5), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 15))
.fixedSize(horizontal: false, vertical: true)
.contextMenu() {
Button(role: .destructive) {
let eventToModify = viewModel.events.firstIndex() { currEvent in
currEvent.id == event.id
} }
if let eventToModify = eventToModify {
viewModel.events.remove(at: eventToModify)
viewModel.saveEvents()
}
} label: {
Label("Delete", systemImage: "trash")
} }
} }
} }
#endif #endif
} }
#Preview("EventListView") { #Preview("EventListView") {

View File

@@ -47,9 +47,13 @@ struct HomeView: View {
ScrollView { ScrollView {
// LazyVStack { // LazyVStack {
ForEach(filteredEvents) { event in ForEach(filteredEvents) { event in
EventListView(viewModel: viewModel, event: event) NavigationLink() {
.transition(.moveAndFade)
.id(event.complete) } label: {
EventListView(viewModel: viewModel, event: event)
.transition(.moveAndFade)
.id(event.complete)
}
} }
.padding(.horizontal) .padding(.horizontal)
// } // }

View File

@@ -0,0 +1,126 @@
//
// CompleteEventButton.swift
// NearFuture
//
// Created by neon443 on 15/06/2025.
//
import SwiftUI
struct CompleteEventButton: View {
@ObservedObject var viewModel: EventViewModel
@Binding var event: Event
@State var timer: Timer?
@State var largeTick: Bool = false
@State var completeInProgress: Bool = false
@State var completeStartTime: Date = .now
@State var progress: Double = 0
private let completeDuration: TimeInterval = 3.0
var isMac: Bool {
#if canImport(AppKit)
return true
#else
return false
#endif
}
func startCompleting() {
#if canImport(UIKit)
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
#endif
withAnimation { completeInProgress = true }
completeStartTime = .now
progress = 0
timer = Timer(timeInterval: 0.01, repeats: true) { timer in
guard completeInProgress else { return }
guard timer.isValid else { return }
let elapsed = Date().timeIntervalSince(completeStartTime)
progress = min(1, elapsed)
if progress > 1 {
timer.invalidate()
progress = 0
withAnimation { completeInProgress = false }
viewModel.completeEvent(&event)
}
}
RunLoop.main.add(timer!, forMode: .common)
}
var body: some View {
Group {
if completeInProgress {
ZStack {
CircularProgressView(progress: $progress)
Image(systemName: "xmark")
.bold()
}
.onTapGesture {
withAnimation { completeInProgress = false }
}
} else {
Image(systemName: event.complete ? "checkmark.circle.fill" : "circle")
.resizable().scaledToFit()
.foregroundStyle(event.complete ? .green : event.color.color)
.bold()
.onTapGesture {
startCompleting()
}
.onHover() { hovering in
withAnimation {
largeTick.toggle()
}
}
.scaleEffect(largeTick ? 1.5 : 1)
}
}
.frame(maxWidth: isMac ? 20 : 30)
.shadow(color: .one.opacity(0.2), radius: 2.5)
.padding(.trailing, isMac ? 15 : 5)
.transition(.scale)
.animation(.spring, value: completeInProgress)
.animation(
.spring(response: 0.2, dampingFraction: 0.75, blendDuration: 2),
value: largeTick
)
}
}
struct CircularProgressView: View {
@Binding var progress: Double
var body: some View {
ZStack {
Circle()
.stroke(
.two,
lineWidth: 5
)
Circle()
.trim(from: 0, to: progress)
.stroke(
.one,
lineWidth: 5
// style: StrokeStyle(
// lineWidth: 5,
// lineCap: .round
// )
)
.rotationEffect(.degrees(-90))
}
}
}
#Preview {
CompleteEventButton(
viewModel: dummyEventViewModel(),
event: .constant(dummyEventViewModel().example)
)
.scaleEffect(5)
}
#Preview {
CircularProgressView(progress: .constant(0.5))
}