release
Laurent 3 years ago
parent 676c813bab
commit 1a8bcf6a4d
  1. 20
      LeCountdown.xcodeproj/project.pbxproj
  2. 5
      LeCountdown/LeCountdownApp.swift
  3. 15
      LeCountdown/Model/Model+Extensions.swift
  4. 56
      LeCountdown/TimerRouter.swift
  5. 31
      LeCountdown/Views/Components/PermissionAlertView.swift
  6. 217
      LeCountdown/Views/ContentView.swift
  7. 98
      LeCountdown/Views/DialView.swift

@ -120,6 +120,9 @@
C4F8B1C0298ACA61005C86A5 /* Model+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C806298195E600BF3EF9 /* Model+Extensions.swift */; };
C4F8B1C3298ACBDB005C86A5 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA912987CC8A0054D761 /* Sound.swift */; };
C4F8B1C6298ACC1F005C86A5 /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA8E2987B83B0054D761 /* SoundPlayer.swift */; };
C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B1CF298BF2E2005C86A5 /* DialView.swift */; };
C4F8B1D2298BF646005C86A5 /* PermissionAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B1D1298BF646005C86A5 /* PermissionAlertView.swift */; };
C4F8B1D8298C0727005C86A5 /* TimerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -243,6 +246,9 @@
C4F8B1B7298AC81D005C86A5 /* CountdownDialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownDialView.swift; sourceTree = "<group>"; };
C4F8B1BC298AC8DE005C86A5 /* AlarmDialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmDialView.swift; sourceTree = "<group>"; };
C4F8B1BE298ACA0B005C86A5 /* StopwatchDialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopwatchDialView.swift; sourceTree = "<group>"; };
C4F8B1CF298BF2E2005C86A5 /* DialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialView.swift; sourceTree = "<group>"; };
C4F8B1D1298BF646005C86A5 /* PermissionAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionAlertView.swift; sourceTree = "<group>"; };
C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerRouter.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -319,6 +325,7 @@
C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */,
C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */,
C4F8B15629891271005C86A5 /* Conductor.swift */,
C4F8B1D7298C0727005C86A5 /* TimerRouter.swift */,
C438C80B2981DE2E00BF3EF9 /* Views */,
C438C8092981DDF800BF3EF9 /* Model */,
C445FA8D2987B82E0054D761 /* Sound */,
@ -430,7 +437,9 @@
C4F8B1BA298AC83F005C86A5 /* Alarm */,
C4F8B1B9298AC830005C86A5 /* Countdown */,
C4F8B1BB298AC848005C86A5 /* Stopwatch */,
C4F8B1D3298BF686005C86A5 /* Components */,
C4060DC1297AE73B003FAB80 /* ContentView.swift */,
C4F8B1CF298BF2E2005C86A5 /* DialView.swift */,
C438C80E29828B8600BF3EF9 /* RecordsView.swift */,
C4742B5A298414B000D5D950 /* ImageSelectionView.swift */,
C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */,
@ -508,6 +517,14 @@
path = Stopwatch;
sourceTree = "<group>";
};
C4F8B1D3298BF686005C86A5 /* Components */ = {
isa = PBXGroup;
children = (
C4F8B1D1298BF646005C86A5 /* PermissionAlertView.swift */,
);
path = Components;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -701,12 +718,14 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C4F8B1D2298BF646005C86A5 /* PermissionAlertView.swift in Sources */,
C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */,
C4F8B1AB298AC3A0005C86A5 /* Countdown+CoreDataProperties.swift in Sources */,
C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */,
C4F8B187298AC234005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */,
C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */,
C4F8B1D8298C0727005C86A5 /* TimerRouter.swift in Sources */,
C4F8B186298AC234005C86A5 /* Activity+CoreDataClass.swift in Sources */,
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */,
C4F8B17E298AC234005C86A5 /* Alarm+CoreDataClass.swift in Sources */,
@ -718,6 +737,7 @@
C4F8B1A8298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataProperties.swift in Sources */,
C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */,
C445FA922987CC8A0054D761 /* Sound.swift in Sources */,
C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */,
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */,
C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */,
C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */,

@ -6,6 +6,7 @@
//
import SwiftUI
import CoreData
import BackgroundTasks
enum BGTaskIdentifier : String {
@ -22,7 +23,7 @@ struct LeCountdownApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Conductor.maestro)
.environmentObject(BoringContext())
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
self._willEnterForegroundNotification()
@ -50,7 +51,7 @@ struct LeCountdownApp: App {
// }
// }
}
fileprivate func _registerBackgroundRefreshes() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTaskIdentifier.refresh.rawValue, using: nil) { task in
self._handleAppRefresh(task: task as! BGAppRefreshTask)

@ -9,6 +9,10 @@ import Foundation
import SwiftUI
import CoreData
enum TimerError: Error {
case notificationAuthorizationMissing
}
extension AbstractTimer {
var displayName: String {
@ -23,7 +27,7 @@ extension AbstractTimer {
if let url = URL(string: self.stringId) {
return url
} else {
return URL(fileURLWithPath: self.stringId) // stupid fallthrough
fatalError("Can't produce url with \(self.stringId)")
}
}
@ -54,15 +58,6 @@ extension AbstractSoundTimer {
extension Countdown {
// override func view() -> any View {
// return CountdownLiveView(countdown: self)
// .environmentObject(Conductor.maestro)
// }
//
// override func editView() -> any View {
//
// }
static func fake(context: NSManagedObjectContext) -> Countdown {
let cd = Countdown(context: context)
cd.duration = 4 * 60.0

@ -0,0 +1,56 @@
//
// TimerRouter.swift
// LeCountdown
//
// Created by Laurent Morvillier on 02/02/2023.
//
import Foundation
import NotificationCenter
class TimerRouter {
static func performAction(timer: AbstractTimer, handler: @escaping (Result<Bool, Error>) -> Void) {
switch timer {
case let countdown as Countdown:
self._launchCountdown(countdown, handler: handler)
case let alarm as Alarm:
self._scheduleAlarm(alarm, handler: handler)
case let stopwatch as Stopwatch:
self._startStopwatch(stopwatch, handler: handler)
default:
print("missing launcher for \(self)")
}
}
fileprivate static func _launchCountdown(_ countdown: Countdown, handler: @escaping (Result<Bool, Error>) -> Void) {
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch settings.authorizationStatus {
case .notDetermined, .denied:
handler(.failure(TimerError.notificationAuthorizationMissing))
default:
CountdownScheduler.master.scheduleIfPossible(countdown: countdown) { result in
switch result {
case .success(_):
handler(.success(true))
case .failure(let failure):
handler(.failure(failure))
}
}
}
}
}
fileprivate static func _scheduleAlarm(_ alarm: Alarm, handler: @escaping (Result<Bool, Error>) -> Void) {
}
fileprivate static func _startStopwatch(_ stopwatch: Stopwatch, handler: @escaping (Result<Bool, Error>) -> Void) {
}
}

@ -0,0 +1,31 @@
//
// PermissionAlertView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 02/02/2023.
//
import SwiftUI
struct PermissionAlertView: View {
var body: some View {
Button("Show permissions") {
self._showPermissionSettings()
}
Button("OK", role: .cancel) { }
}
fileprivate func _showPermissionSettings() {
if let url = URL(string: UIApplication.openNotificationSettingsURLString) {
UIApplication.shared.open(url)
}
}
}
struct PermissionAlertView_Previews: PreviewProvider {
static var previews: some View {
PermissionAlertView()
}
}

@ -8,10 +8,19 @@
import SwiftUI
import CoreData
struct ContentView: View {
class BoringContext : ObservableObject {
@Published var isShowingNewData = false
@Published var error: Error?
@Published var showDefaultAlert: Bool = false
@Published var showPermissionAlert: Bool = false
}
@EnvironmentObject var environment: Conductor
struct ContentView: View {
@EnvironmentObject var boringContext: BoringContext
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
@ -21,14 +30,6 @@ struct ContentView: View {
fileprivate let itemSpacing: CGFloat = 10.0
@State private var isShowingNewData = false
// @State private var isShowingNewStopwatch = false
// @State private var isShowingNewAlarm = false
@State var error: Error?
@State var showDefaultAlert: Bool = false
@State var showPermissionAlert: Bool = false
private let columns: [GridItem] = [
GridItem(spacing: 10.0),
GridItem(spacing: 10.0),
@ -51,29 +52,8 @@ struct ContentView: View {
ReorderableForEach(items: timersArray) { timer in
ZStack(alignment: .topTrailing) {
Image(timer.imageName).resizable()
Button {
self._launchTimer(timer)
} label: {
self._dialView(timer: timer)
}
NavigationLink {
self._editView(timer: timer, isPresented: $isShowingNewData)
} label: {
Image(systemName: "gearshape.fill")
.font(.system(size: 24, weight: .light))
.padding()
.foregroundColor(Color.white)
}
}
.frame(width: width, height: width)
.cornerRadius(40.0)
DialView(timer: timer, frameSize: width) .environment(\.managedObjectContext, viewContext)
.environmentObject(boringContext)
} moveAction: { from, to in
self._reorder(from: from, to: to)
@ -82,52 +62,24 @@ struct ContentView: View {
}.padding(itemSpacing)
.navigationTitle("Youpi")
.alert(error?.localizedDescription ?? "missing error", isPresented: $showDefaultAlert) {
.alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) {
Button("OK", role: .cancel) { }
}
.alert("You need to accept notifications, please check your settings", isPresented: $showPermissionAlert, actions: {
Button("Show permissions") {
self._showPermissionSettings()
}
Button("OK", role: .cancel) { }
})
.sheet(isPresented: self.$isShowingNewData, content: {
NewCountdownView(isPresented: $isShowingNewData) .environment(\.managedObjectContext, viewContext)
.alert("You need to accept notifications, please check your settings", isPresented: $boringContext.showPermissionAlert) {
PermissionAlertView()
}
.sheet(isPresented: $boringContext.isShowingNewData, content: {
NewCountdownView(isPresented: $boringContext.isShowingNewData) .environment(\.managedObjectContext, viewContext)
})
.sheet(isPresented: self.$isShowingNewData, content: {
NewStopwatchView(isPresented: $isShowingNewData) .environment(\.managedObjectContext, viewContext)
.sheet(isPresented: $boringContext.isShowingNewData, content: {
NewStopwatchView(isPresented: $boringContext.isShowingNewData) .environment(\.managedObjectContext, viewContext)
})
.sheet(isPresented: self.$isShowingNewData, content: {
NewAlarmView(isPresented: $isShowingNewData) .environment(\.managedObjectContext, viewContext)
.sheet(isPresented: $boringContext.isShowingNewData, content: {
NewAlarmView(isPresented: $boringContext.isShowingNewData) .environment(\.managedObjectContext, viewContext)
})
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button {
self.isShowingNewData = true
} label: {
HStack {
Image(systemName: "timer")
Text("countdown")
}
}
Button {
self.isShowingNewStopwatch = true
} label: {
HStack {
Image(systemName: "stopwatch")
Text("stopwatch")
}
}
Button {
self.isShowingNewAlarm = true
} label: {
HStack {
Image(systemName: "alarm")
Text("alarm")
}
}
MainToolbarView(isShowingNewData: $boringContext.isShowingNewData)
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink {
@ -139,47 +91,18 @@ struct ContentView: View {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.onAppear {
self._askPermissions()
}
.onOpenURL { url in
print("open URL = \(url)")
self._startCountdownIfPossible(url: url)
self._performActionIfPossible(url: url)
}
}
}
@ViewBuilder
fileprivate func _dialView(timer: AbstractTimer) -> some View {
switch timer {
case let countdown as Countdown:
CountdownDialView(countdown: countdown)
case let alarm as Alarm:
AlarmDialView(alarm: alarm)
case let stopwatch as Stopwatch:
StopwatchDialView(stopwatch: stopwatch)
default:
Text("missing dial view")
}
}
@ViewBuilder
fileprivate func _editView(timer: AbstractTimer, isPresented: Binding<Bool>) -> some View {
switch timer {
case let countdown as Countdown:
CountdownEditView(countdown: countdown, isPresented: isPresented)
case let alarm as Alarm:
AlarmEditView(alarm: alarm, isPresented: isPresented)
case let stopwatch as Stopwatch:
StopwatchEditView(stopwatch: stopwatch, isPresented: isPresented)
default:
Text("missing edit view")
}
}
fileprivate func _reorder(from: IndexSet, to: Int) {
var timers: [AbstractTimer] = self.timersArray
timers.move(fromOffsets: from, toOffset: to)
@ -189,61 +112,77 @@ struct ContentView: View {
do {
try viewContext.save()
} catch {
self.error = error
boringContext.error = error
}
}
fileprivate func _startCountdownIfPossible(url: URL) {
fileprivate func _askPermissions() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { success, error in
print("requestAuthorization > success = \(success), error = \(String(describing: error))")
}
}
fileprivate func _performActionIfPossible(url: URL) {
let urlString = url.absoluteString
if let countdown = viewContext.object(stringId: urlString) as? Countdown {
if let timer = viewContext.object(stringId: urlString) as? AbstractTimer {
print("Start countdown: \(countdown.name ?? ""), \(countdown.duration)")
TimerRouter.performAction(timer: timer) { result in
switch result {
case .success:
break
case .failure(let failure):
switch failure {
case TimerError.notificationAuthorizationMissing:
self.boringContext.showPermissionAlert = true
default:
self.boringContext.error = failure
self.boringContext.showDefaultAlert = true
}
}
}
self._launchCountdown(countdown)
// print("Start countdown: \(countdown.name ?? ""), \(countdown.duration)")
// self._launchCountdown(countdown)
} else {
print("countdown not found with id = \(urlString)")
print("timer not found with id = \(urlString)")
}
}
fileprivate func _launchTimer(_ timer: AbstractTimer) {
}
}
struct MainToolbarView: View {
fileprivate func _launchCountdown(_ countdown: Countdown) {
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch settings.authorizationStatus {
case .notDetermined, .denied:
self.showPermissionAlert = true
default:
CountdownScheduler.master.scheduleIfPossible(countdown: countdown) { result in
switch result {
case .success(_):
break
case .failure(let failure):
self.error = failure
self.showDefaultAlert = true
}
}
var isShowingNewData: Binding<Bool>
var body: some View {
Button {
self.isShowingNewData.wrappedValue = true
} label: {
HStack {
Image(systemName: "timer")
Text("countdown")
}
}
}
fileprivate func _askPermissions() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { success, error in
print("requestAuthorization > success = \(success), error = \(String(describing: error))")
Button {
self.isShowingNewData.wrappedValue = true
} label: {
HStack {
Image(systemName: "stopwatch")
Text("stopwatch")
}
}
}
fileprivate func _showPermissionSettings() {
if let url = URL(string: UIApplication.openNotificationSettingsURLString) {
UIApplication.shared.open(url)
Button {
self.isShowingNewData.wrappedValue = true
} label: {
HStack {
Image(systemName: "alarm")
Text("alarm")
}
}
}
}
fileprivate extension Countdown {

@ -0,0 +1,98 @@
//
// DialView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 02/02/2023.
//
import SwiftUI
struct DialView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var boringContext: BoringContext
@State var timer: AbstractTimer
var frameSize: CGFloat
var body: some View {
ZStack(alignment: .topTrailing) {
Image(timer.imageName).resizable()
Button {
self._launchTimer(timer)
} label: {
self._dialView(timer: timer)
}
NavigationLink {
self._editView(timer: timer, isPresented: $boringContext.isShowingNewData)
} label: {
Image(systemName: "gearshape.fill")
.font(.system(size: 24, weight: .light))
.padding()
.foregroundColor(Color.white)
}
}
.frame(width: frameSize, height: frameSize)
.cornerRadius(40.0)
}
@ViewBuilder
fileprivate func _dialView(timer: AbstractTimer) -> some View {
switch timer {
case let countdown as Countdown:
CountdownDialView(countdown: countdown)
case let alarm as Alarm:
AlarmDialView(alarm: alarm)
case let stopwatch as Stopwatch:
StopwatchDialView(stopwatch: stopwatch)
default:
Text("missing dial view")
}
}
@ViewBuilder
fileprivate func _editView(timer: AbstractTimer, isPresented: Binding<Bool>) -> some View {
switch timer {
case let countdown as Countdown:
CountdownEditView(countdown: countdown, isPresented: isPresented)
case let alarm as Alarm:
AlarmEditView(alarm: alarm, isPresented: isPresented)
case let stopwatch as Stopwatch:
StopwatchEditView(stopwatch: stopwatch, isPresented: isPresented)
default:
Text("missing edit view")
}
}
fileprivate func _launchTimer(_ timer: AbstractTimer) {
TimerRouter.performAction(timer: timer) { result in
switch result {
case .success:
break
case .failure(let failure):
switch failure {
case TimerError.notificationAuthorizationMissing:
self.boringContext.showPermissionAlert = true
default:
self.boringContext.error = failure
self.boringContext.showDefaultAlert = true
}
}
}
}
}
struct DialView_Previews: PreviewProvider {
static var previews: some View {
DialView(timer: Countdown.fake(context: PersistenceController.preview.container.viewContext), frameSize: 150.0)
}
}
Loading…
Cancel
Save