diff --git a/LaunchWidget/LaunchWidgetLiveActivity.swift b/LaunchWidget/LaunchWidgetLiveActivity.swift index c23fb7c..b4da2f4 100644 --- a/LaunchWidget/LaunchWidgetLiveActivity.swift +++ b/LaunchWidget/LaunchWidgetLiveActivity.swift @@ -47,10 +47,10 @@ struct LaunchWidgetLiveActivity: Widget { Text(context.attributes.name.uppercased()) .font(.callout) - }.padding() + } + .padding() .monospaced() .foregroundColor(.white) - .activityBackgroundTint(Color(red: 1.0, green: 0.3, blue: 0.4)) .activitySystemActionForegroundColor(.white) } dynamicIsland: { context in DynamicIsland { @@ -72,11 +72,23 @@ struct LaunchWidgetLiveActivity: Widget { } } } compactLeading: { - Text("L") + Text(context.attributes.name.uppercased()) } compactTrailing: { - Text("T") + if context.attributes.isTimer { + let range = Date()...context.attributes.date + Text(timerInterval: range, + pauseTime: range.lowerBound) + } else { + Text(context.attributes.date, style: .timer) + } } minimal: { - Text("Min") + if context.attributes.isTimer { + let range = Date()...context.attributes.date + Text(timerInterval: range, + pauseTime: range.lowerBound) + } else { + Text(context.attributes.date, style: .timer) + } } .widgetURL(URL(string: context.attributes.id)) .keylineTint(Color.red) diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 5c9e0da..20e1ca1 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ C4286EA12A1502FD0070D075 /* Stopwatch+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */; }; C4286EA32A1503320070D075 /* Stopwatch+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */; }; C4286EA42A1503330070D075 /* Stopwatch+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */; }; + C4286EA62A150A7E0070D075 /* TimePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4286EA52A150A7E0070D075 /* TimePickerView.swift */; }; C42E96FB29E59E72005B1B8C /* BackgroundBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */; }; C42E96FD29E5B06D005B1B8C /* ActivityCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */; }; C42E96FE29E5B5CD005B1B8C /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2B6429A3C37D00CB4FBA /* Filter.swift */; }; @@ -373,6 +374,7 @@ C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.1.xcdatamodel; sourceTree = ""; }; C4286E952A14EC4E0070D075 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataClass.swift"; sourceTree = ""; }; + C4286EA52A150A7E0070D075 /* TimePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerView.swift; sourceTree = ""; }; C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundBlurView.swift; sourceTree = ""; }; C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCalendarView.swift; sourceTree = ""; }; C42E970129E6B32B005B1B8C /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; @@ -951,6 +953,7 @@ C4E5D68729BB3FE1008E7465 /* SiriTimerView.swift */, C4F8B165298A9ABB005C86A5 /* SoundFormView.swift */, C4BA2AD52993F62700CB4FBA /* SoundSelectionView.swift */, + C4286EA52A150A7E0070D075 /* TimePickerView.swift */, C4BA2ADA299549BC00CB4FBA /* TimerModel.swift */, C473C33829ACDBD70056B38A /* TipView.swift */, C4BA2B2E299E69A000CB4FBA /* View+Extension.swift */, @@ -1247,6 +1250,7 @@ C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, C4E5D68829BB3FE1008E7465 /* SiriTimerView.swift in Sources */, C4E5D66629B73AED008E7465 /* StartTimerIntent.swift in Sources */, + C4286EA62A150A7E0070D075 /* TimePickerView.swift in Sources */, C4BA2AD62993F62700CB4FBA /* SoundSelectionView.swift in Sources */, C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */, C4BA2B3E299FC86800CB4FBA /* StatisticsView.swift in Sources */, diff --git a/LeCountdown/Views/Countdown/CountdownFormView.swift b/LeCountdown/Views/Countdown/CountdownFormView.swift index d140dec..f6664d8 100644 --- a/LeCountdown/Views/Countdown/CountdownFormView.swift +++ b/LeCountdown/Views/Countdown/CountdownFormView.swift @@ -9,9 +9,9 @@ import SwiftUI enum CountdownField: Int, Hashable { case name - case hours - case minutes - case seconds +// case hours +// case minutes +// case seconds } struct CountdownFormView : View { @@ -22,10 +22,12 @@ struct CountdownFormView : View { var nameBinding: Binding - var secondsBinding: Binding - var minutesBinding: Binding - var hoursBinding: Binding +// var secondsBinding: Binding +// var minutesBinding: Binding +// var hoursBinding: Binding + var durationBinding: Binding + var imageBinding: Binding var repeatCountBinding: Binding @@ -34,67 +36,88 @@ struct CountdownFormView : View { var body: some View { - Group { - Section { - TextField("Name", text: nameBinding) + Form { + Section(header: Text("Name for tracking the activity")) { + TextField("name", text: nameBinding) .focused($focusedField, equals: .name) .onSubmit { - self.focusNextField($focusedField) + self.focusedField = nil } - } header: { - Text("Name") - } footer: { - if !self.nameBinding.wrappedValue.isEmpty { - Text("Ask Siri: \(Bundle.main.applicationName) \(self.nameBinding.wrappedValue)!") - } } + + + + +// Section { +//// self.focusedField = nil +// +// TextField("Name", text: nameBinding) +// .focused($focusedField, equals: .name) +// .onSubmit { +// self.focusedField = nil +// +//// self.focusNextField($focusedField) +// } +// } header: { +// Text("Name") +// } footer: { +// if !self.nameBinding.wrappedValue.isEmpty { +// Text("Ask Siri: \(Bundle.main.applicationName) \(self.nameBinding.wrappedValue)!") +// } +// } Section { - TextField("Hours", text: hoursBinding) - .keyboardType(.numberPad) - .focused($focusedField, equals: .hours) - .onSubmit { - self.focusNextField($focusedField) - } - TextField("Minutes", text: minutesBinding) - .keyboardType(.numberPad) - .focused($focusedField, equals: .minutes) - .onSubmit { - self.focusNextField($focusedField) - } - TextField("Seconds", text: secondsBinding) - .keyboardType(.numberPad) - .focused($focusedField, equals: .seconds) - .onSubmit { - self.focusedField = nil - } + + TimePickerView(duration: self.durationBinding) + +// TextField("Hours", text: hoursBinding) +// .keyboardType(.numberPad) +// .focused($focusedField, equals: .hours) +// .onSubmit { +// self.focusNextField($focusedField) +// } +// TextField("Minutes", text: minutesBinding) +// .keyboardType(.numberPad) +// .focused($focusedField, equals: .minutes) +// .onSubmit { +// self.focusNextField($focusedField) +// } +// TextField("Seconds", text: secondsBinding) +// .keyboardType(.numberPad) +// .focused($focusedField, equals: .seconds) +// .onSubmit { +// self.focusedField = nil +// } } header: { LabeledContent("Duration", value: self.duration().hourMinuteSecond).font(.footnote) } SoundFormView( model: self.model, - imageBinding: imageBinding, - repeatCountBinding: repeatCountBinding) + imageBinding: self.imageBinding, + repeatCountBinding: self.repeatCountBinding) } } func duration() -> TimeInterval { - let formatter = NumberFormatter() - var hours: Int = 0 - var minutes: Int = 0 - var seconds: Int = 0 - if let h = formatter.number(from: hoursBinding.wrappedValue) { - hours = h.intValue - } - if let m = formatter.number(from: minutesBinding.wrappedValue) { - minutes = m.intValue - } - if let s = formatter.number(from: secondsBinding.wrappedValue) { - seconds = s.intValue - } - return Double(seconds) + 60 * Double(minutes) + 60 * 60 * Double(hours) + return self.durationBinding.wrappedValue + + +// let formatter = NumberFormatter() +// var hours: Int = 0 +// var minutes: Int = 0 +// var seconds: Int = 0 +// if let h = formatter.number(from: hoursBinding.wrappedValue) { +// hours = h.intValue +// } +// if let m = formatter.number(from: minutesBinding.wrappedValue) { +// minutes = m.intValue +// } +// if let s = formatter.number(from: secondsBinding.wrappedValue) { +// seconds = s.intValue +// } +// return Double(seconds) + 60 * Double(minutes) + 60 * 60 * Double(hours) } } @@ -108,9 +131,10 @@ struct CountdownFormView_Previews: PreviewProvider { Form { CountdownFormView( nameBinding: .constant(""), - secondsBinding: .constant(""), - minutesBinding: .constant(""), - hoursBinding: .constant(""), +// secondsBinding: .constant(""), +// minutesBinding: .constant(""), +// hoursBinding: .constant(""), + durationBinding: .constant(0.0), imageBinding: .constant(.pic3), repeatCountBinding: .constant(2), intervalRepeatBinding: .constant(2)) diff --git a/LeCountdown/Views/Countdown/NewCountdownView.swift b/LeCountdown/Views/Countdown/NewCountdownView.swift index 343fd92..52e1148 100644 --- a/LeCountdown/Views/Countdown/NewCountdownView.swift +++ b/LeCountdown/Views/Countdown/NewCountdownView.swift @@ -55,9 +55,11 @@ struct CountdownEditView : View { @State var nameString: String = "" - @State var secondsString: String = "" - @State var minutesString: String = "" - @State var hoursString: String = "" +// @State var secondsString: String = "" +// @State var minutesString: String = "" +// @State var hoursString: String = "" + + @State var duration: TimeInterval = 0.0 @State var soundRepeatCount: Int16 = 0 @State var image: CoolPic = .pic1 @@ -70,12 +72,7 @@ struct CountdownEditView : View { @State var error: Error? = nil var tabSelection: Binding? = nil - -// @FocusState private var textFieldIsFocused: Bool - -// @FetchRequest(sortDescriptors: []) -// private var timers: FetchedResults - + @State var _isNewCountdown: Bool = false // false if editing an existing countdown @State var _hasLoaded = false @@ -112,42 +109,43 @@ struct CountdownEditView : View { } } - Form { +// Form { - EmptyView().id("anchor") +// EmptyView().id("anchor") CountdownFormView( focusedField: _focusedField, nameBinding: $nameString, - secondsBinding: $secondsString, - minutesBinding: $minutesString, - hoursBinding: $hoursString, +// secondsBinding: $secondsString, +// minutesBinding: $minutesString, +// hoursBinding: $hoursString, + durationBinding: $duration, imageBinding: $image, repeatCountBinding: $soundRepeatCount) .environmentObject(self.model) - BasePresetsView { preset in - self._loadPreset(preset) - } - }.toolbar { +// BasePresetsView { preset in +// self._loadPreset(preset) +// } +// } + .toolbar { ToolbarItemGroup(placement: .keyboard) { Button { - Logger.log("NIL!!!!") self.focusedField = nil } label: { Image(systemName: "keyboard.chevron.compact.down") } - Spacer() - Button { - self.focusPreviousField($focusedField) - } label: { - Image(systemName: "chevron.up") - } - Button { - self.focusNextField($focusedField) - } label: { - Image(systemName: "chevron.down") - } +// Spacer() +// Button { +// self.focusPreviousField($focusedField) +// } label: { +// Image(systemName: "chevron.up") +// } +// Button { +// self.focusNextField($focusedField) +// } label: { +// Image(systemName: "chevron.down") +// } } } .onChange(of: self.shouldScrollToTop) { newValue in @@ -232,20 +230,22 @@ struct CountdownEditView : View { fileprivate func _loadPreset(_ preset: Preset) { self.nameString = preset.localizedName - let nf = NumberFormatter() - let minutes = Int(preset.duration / 60.0) - if minutes > 0 { - self.minutesString = nf.string(from: NSNumber(value: minutes)) ?? "" - } else { - self.minutesString = "" - } + self.duration = preset.duration - let seconds = Int(preset.duration) - minutes * 60 - if seconds > 0 { - self.secondsString = nf.string(from: NSNumber(value: seconds)) ?? "" - } else { - self.secondsString = "" - } +// let nf = NumberFormatter() +// let minutes = Int(preset.duration / 60.0) +// if minutes > 0 { +// self.minutesString = nf.string(from: NSNumber(value: minutes)) ?? "" +// } else { +// self.minutesString = "" +// } +// +// let seconds = Int(preset.duration) - minutes * 60 +// if seconds > 0 { +// self.secondsString = nf.string(from: NSNumber(value: seconds)) ?? "" +// } else { +// self.secondsString = "" +// } self.model.group = preset.intervalGroup self.model.soundModel.loadPreset(preset) @@ -254,19 +254,21 @@ struct CountdownEditView : View { fileprivate func _loadCountdown(_ countdown: Countdown) { - let hours = Int(countdown.duration / 3600.0) - let minutes = Int(countdown.duration - Double(hours * 3600)) / 60 - let seconds = countdown.duration - Double(hours * 3600) - Double(minutes * 60) + self.duration = countdown.duration - if hours > 0 { - self.hoursString = self._numberFormatter.string(from: NSNumber(value: hours)) ?? "" - } - if minutes > 0 { - self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? "" - } - if seconds > 0 { - self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? "" - } +// let hours = Int(countdown.duration / 3600.0) +// let minutes = Int(countdown.duration - Double(hours * 3600)) / 60 +// let seconds = countdown.duration - Double(hours * 3600) - Double(minutes * 60) +// +// if hours > 0 { +// self.hoursString = self._numberFormatter.string(from: NSNumber(value: hours)) ?? "" +// } +// if minutes > 0 { +// self.minutesString = self._numberFormatter.string(from: NSNumber(value: minutes)) ?? "" +// } +// if seconds > 0 { +// self.secondsString = self._numberFormatter.string(from: NSNumber(value: seconds)) ?? "" +// } if let name = countdown.activity?.name, !name.isEmpty { self.nameString = name @@ -283,20 +285,20 @@ struct CountdownEditView : View { } } - fileprivate let _numberFormatter = NumberFormatter() - - fileprivate var _seconds: Double { - return self._numberFormatter.number(from: self.secondsString)?.doubleValue ?? 0.0 - } - - fileprivate var _minutes: Double { - return self._numberFormatter.number(from: self.minutesString)?.doubleValue ?? 0.0 - } - - fileprivate var _hours: Double { - return self._numberFormatter.number(from: self.hoursString)?.doubleValue ?? 0.0 - } - +// fileprivate let _numberFormatter = NumberFormatter() +// +// fileprivate var _seconds: Double { +// return self._numberFormatter.number(from: self.secondsString)?.doubleValue ?? 0.0 +// } +// +// fileprivate var _minutes: Double { +// return self._numberFormatter.number(from: self.minutesString)?.doubleValue ?? 0.0 +// } +// +// fileprivate var _hours: Double { +// return self._numberFormatter.number(from: self.hoursString)?.doubleValue ?? 0.0 +// } + fileprivate func _cancel() { self.viewContext.rollback() self.isPresented = false @@ -311,7 +313,9 @@ struct CountdownEditView : View { cd = Countdown(context: viewContext) } - cd.duration = self._hours * 3600.0 + self._minutes * 60.0 + self._seconds +// cd.duration = self._hours * 3600.0 + self._minutes * 60.0 + self._seconds + cd.duration = self.duration + if self._isNewCountdown { let max: Int16 do { diff --git a/LeCountdown/Views/Reusable/MailView.swift b/LeCountdown/Views/Reusable/MailView.swift index 756fe0e..0f1831a 100644 --- a/LeCountdown/Views/Reusable/MailView.swift +++ b/LeCountdown/Views/Reusable/MailView.swift @@ -12,28 +12,19 @@ import SwiftUI struct MailView: UIViewControllerRepresentable { @Binding var isShowing: Bool -// @Binding var result: Result? class Coordinator: NSObject, MFMailComposeViewControllerDelegate { @Binding var isShowing: Bool -// @Binding var result: Result? init(isShowing: Binding) { _isShowing = isShowing -// _result = result } func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { isShowing = false - -// guard error == nil else { -// self.result = .failure(error!) -// return -// } -// self.result = .success(result) } } diff --git a/LeCountdown/Views/Reusable/TimePickerView.swift b/LeCountdown/Views/Reusable/TimePickerView.swift new file mode 100644 index 0000000..e4a3902 --- /dev/null +++ b/LeCountdown/Views/Reusable/TimePickerView.swift @@ -0,0 +1,127 @@ +// +// TimePickerView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 17/05/2023. +// + +import SwiftUI + +struct MultiComponentPicker: View { + let columns: [Column] + var selections: [Binding] + + init?(columns: [Column], selections: [Binding]) { + guard !columns.isEmpty && columns.count == selections.count else { + return nil + } + + self.columns = columns + self.selections = selections + } + + var body: some View { + GeometryReader { geometry in + HStack(spacing: 0) { + ForEach(0.. CGFloat { context[HorizontalAlignment.center] } + } + + static let customCenter = Self(CustomCenter.self) +} + +struct TimePickerView: View { + + @Binding var duration: TimeInterval + + @State var hours = 0 + @State var minutes = 0 + @State var seconds = 0 + + var hoursBinding: Binding { Binding( + get: { self.hours }, + set: { + self.hours = $0 + self._computeDuration() + } + ) } + + var minutesBinding: Binding { Binding( + get: { self.minutes }, + set: { + self.minutes = $0 + self._computeDuration() + } + ) } + + var secondsBinding: Binding { Binding( + get: { self.seconds }, + set: { + self.seconds = $0 + self._computeDuration() + } + ) } + + var columns = [ + MultiComponentPicker.Column(label: "h", options: Array(0..<24).map { MultiComponentPicker.Column.Option(text: "\($0)", tag: $0) }), + MultiComponentPicker.Column(label: "min", options: Array(0..<60).map { MultiComponentPicker.Column.Option(text: "\($0)", tag: $0) }), + MultiComponentPicker.Column(label: "sec", options: Array(0..<60).map { MultiComponentPicker.Column.Option(text: "\($0)", tag: $0) }), + ] + + fileprivate func _computeDuration() { + self.duration = Double(hours) * 3600.0 + Double(minutes) * 60.0 + Double(seconds) + Logger.log("duration = \(self.duration)") + } + + var body: some View { + MultiComponentPicker(columns: columns, selections: [hoursBinding, minutesBinding, secondsBinding]) + .frame(height: 200) + .previewLayout(.sizeThatFits) + } +} + +struct MultiComponentPicker_Previews: PreviewProvider { + static var previews: some View { + TimePickerView(duration: .constant(0.0)).padding(48.0) + } +}