Changes duration selection with a picker

main
Laurent 3 years ago
parent e4758a3fea
commit c3e4d40116
  1. 22
      LaunchWidget/LaunchWidgetLiveActivity.swift
  2. 4
      LeCountdown.xcodeproj/project.pbxproj
  3. 130
      LeCountdown/Views/Countdown/CountdownFormView.swift
  4. 140
      LeCountdown/Views/Countdown/NewCountdownView.swift
  5. 9
      LeCountdown/Views/Reusable/MailView.swift
  6. 127
      LeCountdown/Views/Reusable/TimePickerView.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)

@ -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 = "<group>"; };
C4286E952A14EC4E0070D075 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
C4286E9F2A1502FD0070D075 /* Stopwatch+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stopwatch+CoreDataClass.swift"; sourceTree = "<group>"; };
C4286EA52A150A7E0070D075 /* TimePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerView.swift; sourceTree = "<group>"; };
C42E96FA29E59E72005B1B8C /* BackgroundBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundBlurView.swift; sourceTree = "<group>"; };
C42E96FC29E5B06D005B1B8C /* ActivityCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityCalendarView.swift; sourceTree = "<group>"; };
C42E970129E6B32B005B1B8C /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
@ -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 */,

@ -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,9 +22,11 @@ struct CountdownFormView : View {
var nameBinding: Binding<String>
var secondsBinding: Binding<String>
var minutesBinding: Binding<String>
var hoursBinding: Binding<String>
// var secondsBinding: Binding<String>
// var minutesBinding: Binding<String>
// var hoursBinding: Binding<String>
var durationBinding: Binding<TimeInterval>
var imageBinding: Binding<CoolPic>
@ -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)
}
} header: {
Text("Name")
} footer: {
if !self.nameBinding.wrappedValue.isEmpty {
Text("Ask Siri: \(Bundle.main.applicationName) \(self.nameBinding.wrappedValue)!")
self.focusedField = nil
}
}
// 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))

@ -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
@ -71,11 +73,6 @@ struct CountdownEditView : View {
var tabSelection: Binding<Int>? = nil
// @FocusState private var textFieldIsFocused: Bool
// @FetchRequest(sortDescriptors: [])
// private var timers: FetchedResults<AbstractTimer>
@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,19 +285,19 @@ 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()
@ -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 {

@ -12,28 +12,19 @@ import SwiftUI
struct MailView: UIViewControllerRepresentable {
@Binding var isShowing: Bool
// @Binding var result: Result<MFMailComposeResult, Error>?
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
@Binding var isShowing: Bool
// @Binding var result: Result<MFMailComposeResult, Error>?
init(isShowing: Binding<Bool>) {
_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)
}
}

@ -0,0 +1,127 @@
//
// TimePickerView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 17/05/2023.
//
import SwiftUI
struct MultiComponentPicker<Tag: Hashable>: View {
let columns: [Column]
var selections: [Binding<Tag>]
init?(columns: [Column], selections: [Binding<Tag>]) {
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..<columns.count, id: \.self) { index in
let column = columns[index]
ZStack(alignment: Alignment.init(horizontal: .customCenter, vertical: .center)) {
if (!column.label.isEmpty && !column.options.isEmpty) {
HStack {
Text(verbatim: String("ld"))
.foregroundColor(.clear)
.alignmentGuide(.customCenter) { $0[HorizontalAlignment.center] }
Text(column.label).font(Font.footnote)
}
}
Picker(column.label, selection: selections[index]) {
ForEach(column.options, id: \.tag) { option in
Text(verbatim: option.text).tag(option.tag)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width / CGFloat(columns.count), height: geometry.size.height)
.clipped()
}
}
}
}
}
}
extension MultiComponentPicker {
struct Column {
struct Option {
var text: String
var tag: Tag
}
var label: String
var options: [Option]
}
}
private extension HorizontalAlignment {
enum CustomCenter: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> 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<Int> { Binding(
get: { self.hours },
set: {
self.hours = $0
self._computeDuration()
}
) }
var minutesBinding: Binding<Int> { Binding(
get: { self.minutes },
set: {
self.minutes = $0
self._computeDuration()
}
) }
var secondsBinding: Binding<Int> { 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)
}
}
Loading…
Cancel
Save