release
Laurent 3 years ago
parent df5bb8c8d4
commit 2eb0232bf7
  1. 72
      LaunchWidget/SingleCountdownView.swift
  2. 8
      LeCountdown.xcodeproj/project.pbxproj
  3. 6
      LeCountdown/AppDelegate.swift
  4. 133
      LeCountdown/Conductor.swift
  5. 188
      LeCountdown/CountdownScheduler.swift
  6. 4
      LeCountdown/Info.plist
  7. 15
      LeCountdown/LeCountdownApp.swift
  8. 12
      LeCountdown/Model/Model+Extensions.swift
  9. 54
      LeCountdown/Sound/Sound.swift
  10. 13
      LeCountdown/Sound/SoundPlayer.swift
  11. BIN
      LeCountdown/Sound_Assets/forest_stream.mp3
  12. 8
      LeCountdown/Views/ContentView.swift
  13. 10
      LeCountdown/Views/CountdownFormView.swift
  14. 2
      LeCountdown/Views/DialView.swift
  15. 15
      LeCountdown/Views/NewCountdownView.swift

@ -17,9 +17,18 @@ struct SingleCountdownView: View {
var body: some View {
VStack {
Text(countdown.name ?? "")
Text(countdown.duration.minuteSecond)
}.foregroundColor(Color.white)
HStack {
VStack(alignment: .leading) {
Text(countdown.displayName.uppercased())
Text(countdown.duration.minuteSecond)
}
Spacer()
}
Spacer()
}
.padding()
.monospaced()
.foregroundColor(Color.white)
.font(self.font)
.widgetURL(countdown.url)
}
@ -27,7 +36,7 @@ struct SingleCountdownView: View {
private var font: Font {
switch family {
case .systemSmall, .systemMedium, .systemLarge, .systemExtraLarge:
return .title2
return .body
default:
return .body
}
@ -39,6 +48,11 @@ struct MultiCountdownView: View {
@Environment(\.widgetFamily) var family: WidgetFamily
private let columns: [GridItem] = [
GridItem(spacing: 10.0),
GridItem(spacing: 10.0),
]
var countdowns: [Countdown]
var body: some View {
@ -46,20 +60,52 @@ struct MultiCountdownView: View {
if countdowns.isEmpty {
VoidView()
} else {
HStack {
LazyVGrid(
columns: columns,
spacing: 10.0
) {
ForEach(countdowns) { countdown in
Link(destination: countdown.url) {
VStack {
Text(countdown.name ?? "")
Text(countdown.duration.minuteSecond)
HStack {
VStack(alignment: .leading) {
Spacer()
Text(countdown.displayName.uppercased())
Text(countdown.duration.minuteSecond)
Spacer()
}
Spacer()
}
.font(self.font)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal)
.font(.callout)
.background(.cyan)
.foregroundColor(.white)
.monospaced()
.cornerRadius(16.0)
}
}
}.frame(maxWidth: .infinity)
}.padding()
// HStack {
// ForEach(countdowns) { countdown in
//
// Link(destination: countdown.url) {
// VStack {
// Text(countdown.name ?? "")
// Text(countdown.duration.minuteSecond)
// }
// .font(self.font)
// .frame(maxWidth: .infinity, maxHeight: .infinity)
// }
//
// }
// }.frame(maxWidth: .infinity)
}
}
@ -78,7 +124,7 @@ struct MultiCountdownView: View {
struct CountdownView_Previews: PreviewProvider {
static var previews: some View {
SingleCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemSmall))
SingleCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemSmall)).background(.black)
SingleCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .accessoryRectangular))
MultiCountdownView(countdowns: self.countdowns(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemMedium))
}

@ -64,6 +64,8 @@
C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5E2984205000D5D950 /* ViewModifiers.swift */; };
C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; };
C4F8B1552988751B005C86A5 /* DialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B1542988751B005C86A5 /* DialView.swift */; };
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; };
C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -162,6 +164,8 @@
C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = "<group>"; };
C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -237,6 +241,7 @@
C438C7C829803CA000BF3EF9 /* AppDelegate.swift */,
C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */,
C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */,
C4F8B15629891271005C86A5 /* Conductor.swift */,
C438C80B2981DE2E00BF3EF9 /* Views */,
C438C8092981DDF800BF3EF9 /* Model */,
C445FA8D2987B82E0054D761 /* Sound */,
@ -366,6 +371,7 @@
C445FA962987D0CF0054D761 /* Sound_Assets */ = {
isa = PBXGroup;
children = (
C4F8B15829891528005C86A5 /* forest_stream.mp3 */,
C445FA942987D01C0054D761 /* train_horn.mp3 */,
);
path = Sound_Assets;
@ -521,6 +527,7 @@
buildActionMask = 2147483647;
files = (
C4060DC7297AE73D003FAB80 /* Preview Assets.xcassets in Resources */,
C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */,
C445FA952987D01C0054D761 /* train_horn.mp3 in Resources */,
C4060DC4297AE73D003FAB80 /* Assets.xcassets in Resources */,
);
@ -568,6 +575,7 @@
C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */,
C4F8B1552988751B005C86A5 /* DialView.swift in Sources */,
C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */,
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */,
C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */,
C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */,
C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */,

@ -13,7 +13,7 @@ class AppDelegate : NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
AppEnvironment.sun.cleanup()
Conductor.maestro.cleanup()
return true
}
@ -25,14 +25,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
print("didReceive notification")
AppEnvironment.sun.stopSoundIfNecessary()
// Conductor.maestro.stopSoundIfNecessary()
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
print("willPresent notification")
completionHandler([.banner])
AppEnvironment.sun.notifyUser(countdownId: notification.request.identifier, cancel: false)
Conductor.maestro.notifyUser(countdownId: notification.request.identifier, cancel: false)
}

@ -0,0 +1,133 @@
//
// AppEnvironment.swift
// LeCountdown
//
// Created by Laurent Morvillier on 31/01/2023.
//
import Foundation
import ActivityKit
class Conductor : ObservableObject {
static let maestro: Conductor = Conductor()
@UserDefault(Key.dates.rawValue, defaultValue: [:]) static var savedDates: [String : DateInterval]
init() {
self.notificationDates = Conductor.savedDates
}
@Published var notificationDates: [String : DateInterval] = [:] {
didSet {
Conductor.savedDates = notificationDates
}
}
enum Key : String {
case dates
}
func startCountdown(_ date: Date, countdown: Countdown) {
DispatchQueue.main.async {
let dateInterval = DateInterval(start: Date(), end: date)
self.notificationDates[countdown.stringId] = dateInterval
self._launchLiveActivity(countdown: countdown, endDate: date)
}
}
func notifyUser(countdownId: String, cancel: Bool) {
// self._playSound(countdownId: countdownId)
endCountdown(countdownId: countdownId, cancel: cancel)
}
func endCountdown(countdownId: String, cancel: Bool) {
DispatchQueue.main.async {
if !cancel {
self._recordActivity(countdownId: countdownId)
}
self.notificationDates.removeValue(forKey: countdownId)
self._endLiveActivity(countdownId: countdownId)
}
}
func cleanup() {
let now = Date()
for (key, value) in self.notificationDates {
if value.end < now {
self.endCountdown(countdownId: key, cancel: false)
}
}
}
fileprivate func _recordActivity(countdownId: String) {
let context = PersistenceController.shared.container.viewContext
if let countdown = context.object(stringId: countdownId) as? Countdown,
let dateInterval = self.notificationDates[countdownId] {
do {
try CoreDataRequests.recordActivity(countdown: countdown, dateInterval: dateInterval)
} catch {
print("Could not record activity = \(error)")
// TODO: show error to user
}
}
}
// MARK: - Live Activity
fileprivate func _launchLiveActivity(countdown: Countdown, endDate: Date) {
if ActivityAuthorizationInfo().areActivitiesEnabled {
let contentState = LaunchWidgetAttributes.ContentState(ended: false)
let attributes = LaunchWidgetAttributes(id: countdown.stringId, name: countdown.displayName, endDate: endDate)
let activityContent = ActivityContent(state: contentState, staleDate: endDate.addingTimeInterval(30.0))
do {
let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent)
print("Requested a countdown Live Activity \(String(describing: liveActivity.id)).")
} catch (let error) {
print("Error requesting countdown Live Activity \(error.localizedDescription).")
}
}
}
fileprivate func _liveActivity(countdownId: String) -> ActivityKit.Activity<LaunchWidgetAttributes>? {
return ActivityKit.Activity<LaunchWidgetAttributes>.activities.first(where: { $0.attributes.id == countdownId } )
}
func updateLiveActivities() {
for (countdownId, interval) in self.notificationDates {
if let activity = self._liveActivity(countdownId: countdownId) {
Task {
let ended = interval.end < Date()
let state = LaunchWidgetAttributes.ContentState(ended: ended)
let content = ActivityContent(state: state, staleDate: interval.end)
await activity.update(content)
print("Ending the Live Activity: \(activity.id)")
}
}
}
}
fileprivate func _endLiveActivity(countdownId: String) {
print("Trt to end the Live Activity: \(countdownId)")
if let activity = self._liveActivity(countdownId: countdownId) {
Task {
let state = LaunchWidgetAttributes.ContentState(ended: true)
let content = ActivityContent(state: state, staleDate: Date())
await activity.end(content, dismissalPolicy: .immediate)
print("Ending the Live Activity: \(activity.id)")
}
}
}
}

@ -7,7 +7,6 @@
import Foundation
import UserNotifications
import ActivityKit
class CountdownScheduler {
@ -20,7 +19,7 @@ class CountdownScheduler {
func cancelCurrentNotifications(countdown: Countdown) {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [countdown.stringId])
AppEnvironment.sun.endCountdown(countdownId: countdown.stringId, cancel: true)
Conductor.maestro.endCountdown(countdownId: countdown.stringId, cancel: true)
}
fileprivate func _scheduleCountdownNotification(countdown: Countdown, handler: @escaping (Result<Date?, Error>) -> Void) {
@ -36,10 +35,25 @@ class CountdownScheduler {
}
content.body = body
content.sound = UNNotificationSound.defaultCritical
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: countdown.soundName))
content.interruptionLevel = .critical
self._createNotification(countdown: countdown, content: content, handler: handler)
// if countdown.repeats {
// for i in 1...50 {
// let offset = Double(i) * (countdown.coolSound.duration + 1.0)
//// self._createNotification(countdown: countdown, offset: offset, content: content, handler: handler)
// }
// }
}
fileprivate func _createNotification(countdown: Countdown, offset: TimeInterval = 0.0, content: UNMutableNotificationContent, handler: @escaping (Result<Date?, Error>) -> Void) {
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration, repeats: false)
let request = UNNotificationRequest(identifier: countdown.objectID.uriRepresentation().absoluteString,
let duration = countdown.duration
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration + offset, repeats: false)
let request = UNNotificationRequest(identifier: countdown.objectID.uriRepresentation().absoluteString + "/\(offset)",
content: content,
trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
@ -48,12 +62,15 @@ class CountdownScheduler {
handler(.failure(error))
print("Scheduling error = \(error)")
} else {
if let triggerDate = trigger.nextTriggerDate() {
AppEnvironment.sun.startCountdown(triggerDate, countdown: countdown)
handler(.success(trigger.nextTriggerDate()))
} else {
let backupDate = Date().addingTimeInterval(duration)
AppEnvironment.sun.startCountdown(backupDate, countdown: countdown)
if offset == 0.0 {
if let triggerDate = trigger.nextTriggerDate() {
Conductor.maestro.startCountdown(triggerDate, countdown: countdown)
handler(.success(trigger.nextTriggerDate()))
} else {
let backupDate = Date().addingTimeInterval(duration)
Conductor.maestro.startCountdown(backupDate, countdown: countdown)
}
}
}
}
@ -62,152 +79,3 @@ class CountdownScheduler {
}
}
class AppEnvironment : ObservableObject {
static let sun: AppEnvironment = AppEnvironment()
var soundPlayer: SoundPlayer? = nil
@UserDefault(Key.dates.rawValue, defaultValue: [:]) static var savedDates: [String : DateInterval]
init() {
self.notificationDates = AppEnvironment.savedDates
}
@Published var notificationDates: [String : DateInterval] = [:] {
didSet {
AppEnvironment.savedDates = notificationDates
}
}
enum Key : String {
case dates
}
func startCountdown(_ date: Date, countdown: Countdown) {
DispatchQueue.main.async {
let dateInterval = DateInterval(start: Date(), end: date)
self.notificationDates[countdown.stringId] = dateInterval
self._launchLiveActivity(countdown: countdown, endDate: date)
}
}
func notifyUser(countdownId: String, cancel: Bool) {
self._playSound(countdownId: countdownId)
endCountdown(countdownId: countdownId, cancel: cancel)
}
func endCountdown(countdownId: String, cancel: Bool) {
DispatchQueue.main.async {
if !cancel {
self._recordActivity(countdownId: countdownId)
}
self.notificationDates.removeValue(forKey: countdownId)
// self._updateLiveActivity(countdownId: countdownId, endDate: <#T##Date#>)
self._endLiveActivity(countdownId: countdownId)
}
}
func cleanup() {
let now = Date()
for (key, value) in self.notificationDates {
if value.end < now {
self.endCountdown(countdownId: key, cancel: false)
}
}
}
fileprivate func _recordActivity(countdownId: String) {
let context = PersistenceController.shared.container.viewContext
if let countdown = context.object(stringId: countdownId) as? Countdown,
let dateInterval = self.notificationDates[countdownId] {
do {
try CoreDataRequests.recordActivity(countdown: countdown, dateInterval: dateInterval)
} catch {
print("Could not record activity = \(error)")
// TODO: show error to user
}
}
}
// MARK: - Sound
fileprivate func _playSound(countdownId: String) {
let countdown = PersistenceController.shared.container.viewContext.object(stringId: countdownId) as? Countdown
let soundFile = countdown?.soundFile ?? Sound.allCases[0].soundFile
let soundPlayer = SoundPlayer()
self.soundPlayer = soundPlayer
do {
try soundPlayer.playSound(soundFile: soundFile, repeats: countdown?.repeats ?? true)
} catch {
print("error = \(error)")
// TODO: manage error
}
}
func stopSoundIfNecessary() {
self.soundPlayer?.stop()
}
// MARK: - Live Activity
fileprivate func _launchLiveActivity(countdown: Countdown, endDate: Date) {
if ActivityAuthorizationInfo().areActivitiesEnabled {
let contentState = LaunchWidgetAttributes.ContentState(ended: false)
let attributes = LaunchWidgetAttributes(id: countdown.stringId, name: countdown.displayName, endDate: endDate)
let activityContent = ActivityContent(state: contentState, staleDate: endDate.addingTimeInterval(30.0))
do {
let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent)
print("Requested a countdown Live Activity \(String(describing: liveActivity.id)).")
} catch (let error) {
print("Error requesting countdown Live Activity \(error.localizedDescription).")
}
}
}
fileprivate func _liveActivity(countdownId: String) -> ActivityKit.Activity<LaunchWidgetAttributes>? {
return ActivityKit.Activity<LaunchWidgetAttributes>.activities.first(where: { $0.attributes.id == countdownId } )
}
func updateLiveActivities() {
for (countdownId, interval) in self.notificationDates {
if let activity = self._liveActivity(countdownId: countdownId) {
Task {
let ended = interval.end < Date()
let state = LaunchWidgetAttributes.ContentState(ended: ended)
let content = ActivityContent(state: state, staleDate: interval.end)
await activity.update(content)
print("Ending the Live Activity: \(activity.id)")
}
}
}
}
fileprivate func _endLiveActivity(countdownId: String) {
print("Trt to end the Live Activity: \(countdownId)")
if let activity = self._liveActivity(countdownId: countdownId) {
Task {
let state = LaunchWidgetAttributes.ContentState(ended: true)
let content = ActivityContent(state: state, staleDate: Date())
await activity.end(content, dismissalPolicy: .immediate)
print("Ending the Live Activity: \(activity.id)")
}
}
}
}

@ -13,5 +13,9 @@
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>

@ -17,7 +17,7 @@ struct LeCountdownApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(AppEnvironment.sun)
.environmentObject(Conductor.maestro)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
self._willEnterForegroundNotification()
@ -28,11 +28,20 @@ struct LeCountdownApp: App {
}
fileprivate func _willEnterForegroundNotification() {
AppEnvironment.sun.cleanup()
Conductor.maestro.cleanup()
}
fileprivate func _onAppear() {
Task {
for s in Sound.allCases {
do {
let d = try await s.duration()
print("\(s) duration = \(d)")
} catch {
print("error = \(error)")
}
}
}
}
}

@ -38,10 +38,18 @@ extension Countdown {
return self.coolpic.rawValue
}
var soundFile: SoundFile {
return Sound.allCases[Int(self.sound)].soundFile
var coolSound: Sound {
return Sound.allCases[Int(self.sound)]
}
var soundName: String {
coolSound.soundName
}
// var soundFile: SoundFile {
// return Sound.allCases[Int(self.sound)].soundFile
// }
static func fake(context: NSManagedObjectContext) -> Countdown {
let cd = Countdown(context: context)
cd.duration = 4 * 60.0

@ -6,15 +6,7 @@
//
import Foundation
struct SoundFile {
var filename: String
var fileExtension: String
var url: URL? {
return Bundle.main.url(forResource: self.filename, withExtension: self.fileExtension)
}
}
import AVFoundation
// Sound id are stored thus case order should not be changed
enum Sound : Int, CaseIterable, Identifiable {
@ -22,17 +14,57 @@ enum Sound : Int, CaseIterable, Identifiable {
var id: Int { return self.rawValue }
case trainhorn // default
case forestStream
var localizedString: String {
switch self {
case .trainhorn: return NSLocalizedString("Train horn", comment: "")
case .forestStream: return NSLocalizedString("Forest stream", comment: "")
}
}
var soundName: String {
switch self {
case .trainhorn: return "train_horn.mp3"
case .forestStream: return "forest_stream.mp3"
}
}
var soundFile: SoundFile {
var duration: TimeInterval {
switch self {
case .trainhorn: return SoundFile(filename: "train_horn", fileExtension: "mp3")
case .trainhorn: return 7.8
case .forestStream: return 300.1
}
}
var url: URL? {
let components = self.soundName.components(separatedBy: ".")
if components.count == 2 {
return Bundle.main.url(forResource: components[0], withExtension: components[1])
} else {
print("bad sound file name for \(self)")
return nil
}
}
// var soundFile: SoundFile {
// switch self {
// case .trainhorn: return SoundFile(filename: "train_horn", fileExtension: "mp3")
// }
// }
func duration() async throws -> TimeInterval {
guard let url = self.url else {
print("sound \(self) has no url")
return -1.0
}
let audioAsset = AVURLAsset.init(url: url, options: nil)
let duration = try await audioAsset.load(.duration)
return CMTimeGetSeconds(duration)
}
}

@ -8,6 +8,15 @@
import Foundation
import AVFoundation
struct SoundFile {
var filename: String
var fileExtension: String
var url: URL? {
return Bundle.main.url(forResource: self.filename, withExtension: self.fileExtension)
}
}
enum SoundPlayerError : Error {
case missingResourceError(file: SoundFile)
}
@ -20,6 +29,10 @@ class SoundPlayer {
guard let url = soundFile.url else {
throw SoundPlayerError.missingResourceError(file: soundFile)
}
let audioSession: AVAudioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback)
try audioSession.setActive(true)
_player = try AVAudioPlayer(contentsOf: url)
_player?.prepareToPlay()

@ -10,7 +10,7 @@ import CoreData
struct CountdownLiveView: View {
@EnvironmentObject var environment: AppEnvironment
@EnvironmentObject var environment: Conductor
@ObservedObject var countdown: Countdown
@ -50,7 +50,7 @@ struct CountdownLiveView: View {
struct ContentView: View {
@EnvironmentObject var environment: AppEnvironment
@EnvironmentObject var environment: Conductor
@Environment(\.managedObjectContext) private var viewContext
@ -90,7 +90,7 @@ struct ContentView: View {
} label: {
CountdownLiveView(countdown: countdown)
.environmentObject(AppEnvironment.sun)
.environmentObject(Conductor.maestro)
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.aspectRatio(1, contentMode: .fit)
@ -206,7 +206,7 @@ struct ContentView: View {
fileprivate extension Countdown {
var endDate: Date? {
return AppEnvironment.sun.notificationDates[self.stringId]?.end
return Conductor.maestro.notificationDates[self.stringId]?.end
}
var isLive: Bool {

@ -58,11 +58,11 @@ struct CountdownFormView : View {
} else {
Image(imageBinding.wrappedValue.rawValue)
}
} .font(Font.system(size: 90.0))
.aspectRatio(1, contentMode: .fit)
.frame(width: 100.0, height: 100.0)
.cornerRadius(40.0)
}
.font(Font.system(size: 90.0))
.aspectRatio(1, contentMode: .fit)
.frame(width: 100.0, height: 100.0)
.cornerRadius(40.0)
}
}

@ -9,7 +9,7 @@ import SwiftUI
struct DialView: View {
@EnvironmentObject var environment: AppEnvironment
@EnvironmentObject var environment: Conductor
var name: String
var duration: String

@ -57,13 +57,14 @@ struct CountdownEditView : View {
var body: some View {
NavigationStack {
CountdownFormView(secondsBinding: $secondsString,
minutesBinding: $minutesString,
nameBinding: $nameString,
imageBinding: $image,
soundBinding: $sound,
repeatsBinding: $soundRepeats,
textFieldIsFocused: $textFieldIsFocused)
CountdownFormView(
secondsBinding: $secondsString,
minutesBinding: $minutesString,
nameBinding: $nameString,
imageBinding: $image,
soundBinding: $sound,
repeatsBinding: $soundRepeats,
textFieldIsFocused: $textFieldIsFocused)
.onAppear {
self._onAppear()
}

Loading…
Cancel
Save