├── CircleTimer WatchKit App └── Assets.xcassets │ ├── AccentColor.colorset │ └── Contents.json │ ├── AppIcon.appiconset │ └── Contents.json │ └── Contents.json ├── CircleTimer WatchKit Extension ├── App │ ├── CircleTimerApp.swift │ ├── Complication │ │ └── ComplicationController.swift │ └── Notification │ │ ├── NotificationController.swift │ │ └── NotificationView.swift ├── CircleTimer WatchKit Extension.entitlements ├── Common │ └── Extensions │ │ ├── IntExtension.swift │ │ └── String+Localizable.swift ├── Core │ ├── Context │ │ └── AppContext.swift │ ├── Extensions │ │ └── UserDefaults+Coding.swift │ ├── Logging │ │ └── Logging.swift │ ├── Models │ │ └── CircleTimer.swift │ ├── Repositories │ │ └── TimerRepository.swift │ └── Services │ │ ├── HealthKitSessionProvider.swift │ │ ├── TimerService.swift │ │ └── TimerWorker.swift ├── Info.plist ├── Resources │ ├── Assets.xcassets │ │ ├── Complication.complicationset │ │ │ ├── Circular.imageset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Extra Large.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Bezel.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Circular.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Corner.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Extra Large.imageset │ │ │ │ └── Contents.json │ │ │ ├── Graphic Large Rectangular.imageset │ │ │ │ └── Contents.json │ │ │ ├── Modular.imageset │ │ │ │ └── Contents.json │ │ │ └── Utilitarian.imageset │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── close_icon.imageset │ │ │ ├── Contents.json │ │ │ └── close_icon.png │ │ ├── pause_icon.imageset │ │ │ ├── Contents.json │ │ │ └── pause_icon.png │ │ ├── play_icon.imageset │ │ │ ├── Contents.json │ │ │ └── play_icon.png │ │ ├── restart_icon.imageset │ │ │ ├── Contents.json │ │ │ └── restart_icon.png │ │ └── timer_icon.imageset │ │ │ ├── Contents.json │ │ │ └── timer_icon.png │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── PushNotificationPayload.apns │ ├── en.lproj │ │ └── Localizable.strings │ └── ru.lproj │ │ └── Localizable.strings ├── Screens │ ├── Home │ │ ├── HomeView.swift │ │ └── HomeViewModel.swift │ ├── Settings │ │ ├── SettingsView.swift │ │ └── SettingsViewModel.swift │ └── Timer │ │ ├── TimerButtonModifier.swift │ │ ├── TimerView.swift │ │ └── TimerViewModel.swift └── Views │ ├── CountdownCircleView │ └── CountdownCircleView.swift │ └── TimePickerView │ ├── TimePickerTitleModifier.swift │ ├── TimePickerView.swift │ └── TimePickerViewModel.swift ├── CircleTimer.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── CircleTimer WatchKit App (Complication).xcscheme │ │ ├── CircleTimer WatchKit App (Notification).xcscheme │ │ └── CircleTimer WatchKit App.xcscheme └── xcuserdata │ └── kirillkunst.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── README.md └── screenshots ├── 1.png ├── 2.png └── 3.png /CircleTimer WatchKit App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CircleTimer WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "role" : "notificationCenter", 6 | "scale" : "2x", 7 | "size" : "24x24", 8 | "subtype" : "38mm" 9 | }, 10 | { 11 | "idiom" : "watch", 12 | "role" : "notificationCenter", 13 | "scale" : "2x", 14 | "size" : "27.5x27.5", 15 | "subtype" : "42mm" 16 | }, 17 | { 18 | "idiom" : "watch", 19 | "role" : "companionSettings", 20 | "scale" : "2x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "watch", 25 | "role" : "companionSettings", 26 | "scale" : "3x", 27 | "size" : "29x29" 28 | }, 29 | { 30 | "idiom" : "watch", 31 | "role" : "notificationCenter", 32 | "scale" : "2x", 33 | "size" : "33x33", 34 | "subtype" : "45mm" 35 | }, 36 | { 37 | "idiom" : "watch", 38 | "role" : "appLauncher", 39 | "scale" : "2x", 40 | "size" : "40x40", 41 | "subtype" : "38mm" 42 | }, 43 | { 44 | "idiom" : "watch", 45 | "role" : "appLauncher", 46 | "scale" : "2x", 47 | "size" : "44x44", 48 | "subtype" : "40mm" 49 | }, 50 | { 51 | "idiom" : "watch", 52 | "role" : "appLauncher", 53 | "scale" : "2x", 54 | "size" : "46x46", 55 | "subtype" : "41mm" 56 | }, 57 | { 58 | "idiom" : "watch", 59 | "role" : "appLauncher", 60 | "scale" : "2x", 61 | "size" : "50x50", 62 | "subtype" : "44mm" 63 | }, 64 | { 65 | "idiom" : "watch", 66 | "role" : "appLauncher", 67 | "scale" : "2x", 68 | "size" : "51x51", 69 | "subtype" : "45mm" 70 | }, 71 | { 72 | "idiom" : "watch", 73 | "role" : "quickLook", 74 | "scale" : "2x", 75 | "size" : "86x86", 76 | "subtype" : "38mm" 77 | }, 78 | { 79 | "idiom" : "watch", 80 | "role" : "quickLook", 81 | "scale" : "2x", 82 | "size" : "98x98", 83 | "subtype" : "42mm" 84 | }, 85 | { 86 | "idiom" : "watch", 87 | "role" : "quickLook", 88 | "scale" : "2x", 89 | "size" : "108x108", 90 | "subtype" : "44mm" 91 | }, 92 | { 93 | "idiom" : "watch", 94 | "role" : "quickLook", 95 | "scale" : "2x", 96 | "size" : "117x117", 97 | "subtype" : "45mm" 98 | }, 99 | { 100 | "idiom" : "watch-marketing", 101 | "scale" : "1x", 102 | "size" : "1024x1024" 103 | } 104 | ], 105 | "info" : { 106 | "author" : "xcode", 107 | "version" : 1 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /CircleTimer WatchKit App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/App/CircleTimerApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleTimerApp.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct CircleTimerApp: App { 12 | @SceneBuilder var body: some Scene { 13 | WindowGroup { 14 | NavigationView { 15 | HomeView() 16 | .environment(\.appContext, AppContext.shared) 17 | } 18 | } 19 | 20 | WKNotificationScene(controller: NotificationController.self, category: "circleTimerCategory") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/App/Complication/ComplicationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplicationController.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import ClockKit 9 | 10 | 11 | class ComplicationController: NSObject, CLKComplicationDataSource { 12 | 13 | // MARK: - Complication Configuration 14 | 15 | func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) { 16 | let descriptors = [ 17 | CLKComplicationDescriptor(identifier: "complication", displayName: "CircleTimer", supportedFamilies: CLKComplicationFamily.allCases) 18 | // Multiple complication support can be added here with more descriptors 19 | ] 20 | 21 | // Call the handler with the currently supported complication descriptors 22 | handler(descriptors) 23 | } 24 | 25 | func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) { 26 | // Do any necessary work to support these newly shared complication descriptors 27 | } 28 | 29 | // MARK: - Timeline Configuration 30 | 31 | func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { 32 | // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines 33 | handler(nil) 34 | } 35 | 36 | func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) { 37 | // Call the handler with your desired behavior when the device is locked 38 | handler(.showOnLockScreen) 39 | } 40 | 41 | // MARK: - Timeline Population 42 | 43 | func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { 44 | // Call the handler with the current timeline entry 45 | handler(nil) 46 | } 47 | 48 | func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { 49 | // Call the handler with the timeline entries after the given date 50 | handler(nil) 51 | } 52 | 53 | // MARK: - Sample Templates 54 | 55 | func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { 56 | // This method will be called once per supported complication, and the results will be cached 57 | handler(nil) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/App/Notification/NotificationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationController.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import WatchKit 9 | import SwiftUI 10 | import UserNotifications 11 | 12 | class NotificationController: WKUserNotificationHostingController { 13 | 14 | override var body: NotificationView { 15 | return NotificationView() 16 | } 17 | 18 | override func willActivate() { 19 | // This method is called when watch view controller is about to be visible to user 20 | super.willActivate() 21 | } 22 | 23 | override func didDeactivate() { 24 | // This method is called when watch view controller is no longer visible 25 | super.didDeactivate() 26 | } 27 | 28 | override func didReceive(_ notification: UNNotification) { 29 | // This method is called when a notification needs to be presented. 30 | // Implement it if you use a dynamic notification interface. 31 | // Populate your dynamic notification interface as quickly as possible. 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/App/Notification/NotificationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationView.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NotificationView: View { 11 | var body: some View { 12 | Text("Hello, World!") 13 | } 14 | } 15 | 16 | struct NotificationView_Previews: PreviewProvider { 17 | static var previews: some View { 18 | NotificationView() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/CircleTimer WatchKit Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.healthkit 6 | 7 | com.apple.developer.healthkit.access 8 | 9 | com.apple.developer.healthkit.background-delivery 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Common/Extensions/IntExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntExtension.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Int { 12 | 13 | /// Format integer with leading zeros 14 | var formattedString: String { 15 | String(format: "%02d", self) 16 | } 17 | 18 | /// Calculate components of seconds value 19 | var secondsToHoursMinutesSeconds: (Int, Int, Int) { 20 | return (self / 3600, (self % 3600) / 60, (self % 3600) % 60) 21 | } 22 | 23 | /// Format seconds value 24 | var formattedSecondsView: String { 25 | var components = [String]() 26 | 27 | let timeData = secondsToHoursMinutesSeconds 28 | 29 | if timeData.0 > 0 { 30 | components.append(timeData.0.formattedString) 31 | } 32 | 33 | components.append(timeData.1.formattedString) 34 | components.append(timeData.2.formattedString) 35 | 36 | return components.joined(separator: ":") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Common/Extensions/String+Localizable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Localizable.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 24.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var localized: String { 13 | return NSLocalizedString(self, comment: self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Context/AppContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppContext.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 23.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | // MARK: - Env key 13 | 14 | private struct AppContextKey: EnvironmentKey { 15 | static let defaultValue: AppContextProtocol = AppContext.shared 16 | } 17 | 18 | extension EnvironmentValues { 19 | var appContext: AppContextProtocol { 20 | get { self[AppContextKey.self] } 21 | set { self[AppContextKey.self] = newValue } 22 | } 23 | } 24 | 25 | // MARK: - Providers 26 | 27 | protocol TimerServiceProvider { 28 | var timerService: TimerService { get } 29 | } 30 | 31 | // MARK: - AppContextProtocol 32 | 33 | typealias AppContextProtocol = TimerServiceProvider 34 | 35 | // MARK: - AppContext 36 | 37 | struct AppContext: AppContextProtocol { 38 | 39 | var timerService: TimerService 40 | 41 | static let shared: AppContext = { 42 | let timerRepo = TimerRepository() 43 | let timerService = TimerService(repo: timerRepo) 44 | return AppContext(timerService: timerService) 45 | }() 46 | 47 | } 48 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Extensions/UserDefaults+Coding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Coding.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 24.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - UserDefaults extensions 12 | 13 | public extension UserDefaults { 14 | 15 | /// Set Codable object into UserDefaults 16 | /// 17 | /// - Parameters: 18 | /// - object: Codable Object 19 | /// - forKey: Key string 20 | /// - Throws: UserDefaults Error 21 | func set(object: T, forKey: String) throws { 22 | let jsonData = try JSONEncoder().encode(object) 23 | set(jsonData, forKey: forKey) 24 | } 25 | 26 | /// Get Codable object into UserDefaults 27 | /// 28 | /// - Parameters: 29 | /// - object: Codable Object 30 | /// - forKey: Key string 31 | /// - Throws: UserDefaults Error 32 | func get(objectType: T.Type, forKey: String) throws -> T? { 33 | guard let result = value(forKey: forKey) as? Data else { 34 | return nil 35 | } 36 | return try JSONDecoder().decode(objectType, from: result) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Logging/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 16.02.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Log data in debug mode 13 | /// - Parameters: 14 | /// - tag: Data tag 15 | /// - message: message data 16 | func log(_ tag: String, _ message: Any...) { 17 | #if DEBUG 18 | print("[\(tag)] \(message)") 19 | #endif 20 | } 21 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Models/CircleTimer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleTimer.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 23.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CircleTimer: Codable, Identifiable, ObservableObject, Hashable { 12 | 13 | /// We need some UID for every timer to store it 14 | var id: String 15 | 16 | /// Total seconds amount 17 | var seconds: Int 18 | 19 | /// Is this timer repeative 20 | var continious: Bool 21 | 22 | /// Date of creation 23 | var createdDate: Date 24 | 25 | init(seconds: Int, continious: Bool) { 26 | self.id = UUID().uuidString 27 | self.createdDate = Date() 28 | self.seconds = seconds 29 | self.continious = continious 30 | } 31 | 32 | func hash(into hasher: inout Hasher) { 33 | hasher.combine(id) 34 | hasher.combine(seconds) 35 | } 36 | 37 | static func == (lhs: CircleTimer, rhs: CircleTimer) -> Bool { 38 | lhs.id == rhs.id 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Repositories/TimerRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerRepository.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 23.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol TimerRepositoryProtocol { 13 | func timersPublisher() -> AnyPublisher<[CircleTimer], Never> 14 | func add(timer: CircleTimer) 15 | } 16 | 17 | class TimerRepository: TimerRepositoryProtocol { 18 | 19 | struct Config { 20 | static let timersKey = "com.kunst.circletimer.storage.timers" 21 | } 22 | 23 | /// By default, we store all timers directly in the memory 24 | @Published private var inMemoryTimers: [CircleTimer] = [CircleTimer]() 25 | 26 | /// Standard user defaults storage is our basic storage 27 | private var defaults: UserDefaults = UserDefaults.standard 28 | 29 | init() { 30 | if let timers = try? defaults.get(objectType: [CircleTimer].self, forKey: Config.timersKey) { 31 | inMemoryTimers = timers 32 | } 33 | } 34 | 35 | func timersPublisher() -> AnyPublisher<[CircleTimer], Never> { 36 | return $inMemoryTimers.eraseToAnyPublisher() 37 | } 38 | 39 | func add(timer: CircleTimer) { 40 | if inMemoryTimers.first(where: { $0.seconds == timer.seconds }) != nil { 41 | return 42 | } 43 | 44 | inMemoryTimers.append(timer) 45 | 46 | try? defaults.set(object: inMemoryTimers, forKey: Config.timersKey) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Services/HealthKitSessionProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HealthKitSessionProvider.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 16.02.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import HealthKit 11 | 12 | /// Used for enabling HealthKit session to make timer work when display is off 13 | protocol HealthKitSessionProviderProtocol { 14 | func start() 15 | func end() 16 | } 17 | 18 | class HealthKitSessionProvider: HealthKitSessionProviderProtocol { 19 | 20 | // MARK: - Private 21 | 22 | private lazy var healthStore = HKHealthStore() 23 | 24 | private lazy var configuration: HKWorkoutConfiguration = { 25 | let configuration = HKWorkoutConfiguration() 26 | configuration.activityType = .other 27 | return configuration 28 | }() 29 | 30 | private lazy var session: HKWorkoutSession? = { 31 | let session = try? HKWorkoutSession(healthStore: healthStore, configuration: configuration) 32 | return session 33 | }() 34 | 35 | // MARK: - Actions 36 | 37 | func start() { 38 | session?.startActivity(with: Date()) 39 | } 40 | 41 | func end() { 42 | session?.end() 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Services/TimerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerService.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 23.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol TimerServiceProtocol { 13 | var currentTimer: CircleTimer? { get set } 14 | func timersPublisher() -> AnyPublisher<[CircleTimer], Never> 15 | func add(timer: CircleTimer) 16 | func raiseWorker(for timer: CircleTimer) -> TimerWorker 17 | func destroyWorker(for timer: CircleTimer) 18 | } 19 | 20 | class TimerService: TimerServiceProtocol { 21 | 22 | private var repo: TimerRepositoryProtocol 23 | 24 | /// Timer workers storage by their UID 25 | private var workers = [String: TimerWorker]() 26 | 27 | /// Current timer reference 28 | var currentTimer: CircleTimer? 29 | 30 | init(repo: TimerRepositoryProtocol) { 31 | self.repo = repo 32 | } 33 | 34 | func timersPublisher() -> AnyPublisher<[CircleTimer], Never> { 35 | return repo.timersPublisher() 36 | } 37 | 38 | func add(timer: CircleTimer) { 39 | repo.add(timer: timer) 40 | } 41 | 42 | func raiseWorker(for timer: CircleTimer) -> TimerWorker { 43 | if let worker = workers[timer.id] { 44 | return worker 45 | } 46 | 47 | let worker = TimerWorker(seconds: timer.seconds, continious: timer.continious) 48 | workers[timer.id] = worker 49 | 50 | log("TIMER_SERVICE", "new worker raised") 51 | 52 | return worker 53 | } 54 | 55 | func destroyWorker(for timer: CircleTimer) { 56 | workers[timer.id]?.stop() 57 | workers[timer.id] = nil 58 | 59 | log("TIMER_SERVICE", "worker is destroyed for timer: \(timer.id)") 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Core/Services/TimerWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerWorker.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 24.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import WatchKit 11 | import HealthKit 12 | 13 | class TimerWorker { 14 | 15 | enum State { 16 | case active 17 | case stopped 18 | case paused 19 | } 20 | 21 | /// Represents current state of worker 22 | var state: State = .stopped 23 | 24 | /// Every second timer to countdown 25 | private var timer: Timer? 26 | 27 | /// Total seconds before timer firing 28 | private var seconds: Int 29 | 30 | /// Actual seconds before firing 31 | @Published private(set) var secondsLeft: Int 32 | 33 | /// Count of cycles during timer session 34 | @Published var cyclesCount: Int = 0 35 | 36 | /// Should worker work repeatively 37 | private var continious: Bool 38 | 39 | /// Support background working with healthkit session enabled 40 | private var sessionProvider: HealthKitSessionProviderProtocol 41 | 42 | init(seconds: Int, 43 | continious: Bool, 44 | sessionProvider: HealthKitSessionProviderProtocol = HealthKitSessionProvider()) { 45 | self.seconds = seconds 46 | self.continious = continious 47 | self.secondsLeft = seconds 48 | self.sessionProvider = sessionProvider 49 | 50 | sessionProvider.start() 51 | } 52 | 53 | deinit { 54 | sessionProvider.end() 55 | } 56 | 57 | // MARK: - Public methods 58 | 59 | /// Start timer work with counting cycles 60 | func start() { 61 | state = .active 62 | secondsLeft = seconds 63 | makeTimer() 64 | } 65 | 66 | /// Resume time after stopping 67 | func resume() { 68 | state = .active 69 | makeTimer() 70 | } 71 | 72 | /// Completely stop the worker 73 | func stop() { 74 | state = .stopped 75 | 76 | log("TIMER_WORKER", "stop...") 77 | 78 | timer?.invalidate() 79 | secondsLeft = 0 80 | } 81 | 82 | /// Pause the worker 83 | func pause() { 84 | state = .paused 85 | 86 | log("TIMER_WORKER", "pause...") 87 | 88 | timer?.invalidate() 89 | } 90 | 91 | // MARK: - Private methods 92 | 93 | /// Restart after cycle is finished 94 | private func restart() { 95 | stop() 96 | playNotification() 97 | secondsLeft = seconds 98 | start() 99 | } 100 | 101 | // Create scheduled system timer 102 | private func makeTimer() { 103 | log("TIMER_WORKER", "start...") 104 | 105 | timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] _ in 106 | guard let self = self else { return } 107 | self.checkCycle() 108 | }) 109 | } 110 | 111 | /// Check conditions for current cycle 112 | private func checkCycle() { 113 | if secondsLeft == 0 { 114 | restart() 115 | cyclesCount += 1 116 | } else { 117 | secondsLeft -= 1 118 | log("TIMER_WORKER", "progress: \(secondsLeft.formattedSecondsView)") 119 | } 120 | } 121 | 122 | /// Notify user about cycle ending 123 | private func playNotification() { 124 | WKInterfaceDevice.current().play(.notification) 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : "<=145" 11 | }, 12 | { 13 | "idiom" : "watch", 14 | "scale" : "2x", 15 | "screen-width" : ">183" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "auto-scaling" : "auto" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "Circular.imageset", 5 | "idiom" : "watch", 6 | "role" : "circular" 7 | }, 8 | { 9 | "filename" : "Extra Large.imageset", 10 | "idiom" : "watch", 11 | "role" : "extra-large" 12 | }, 13 | { 14 | "filename" : "Graphic Bezel.imageset", 15 | "idiom" : "watch", 16 | "role" : "graphic-bezel" 17 | }, 18 | { 19 | "filename" : "Graphic Circular.imageset", 20 | "idiom" : "watch", 21 | "role" : "graphic-circular" 22 | }, 23 | { 24 | "filename" : "Graphic Corner.imageset", 25 | "idiom" : "watch", 26 | "role" : "graphic-corner" 27 | }, 28 | { 29 | "filename" : "Graphic Extra Large.imageset", 30 | "idiom" : "watch", 31 | "role" : "graphic-extra-large" 32 | }, 33 | { 34 | "filename" : "Graphic Large Rectangular.imageset", 35 | "idiom" : "watch", 36 | "role" : "graphic-large-rectangular" 37 | }, 38 | { 39 | "filename" : "Modular.imageset", 40 | "idiom" : "watch", 41 | "role" : "modular" 42 | }, 43 | { 44 | "filename" : "Utilitarian.imageset", 45 | "idiom" : "watch", 46 | "role" : "utilitarian" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : "<=145" 11 | }, 12 | { 13 | "idiom" : "watch", 14 | "scale" : "2x", 15 | "screen-width" : ">183" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "auto-scaling" : "auto" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : ">183" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "auto-scaling" : "auto" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : ">183" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "auto-scaling" : "auto" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : ">183" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "auto-scaling" : "auto" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : "<=145" 11 | }, 12 | { 13 | "idiom" : "watch", 14 | "scale" : "2x", 15 | "screen-width" : ">183" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "auto-scaling" : "auto" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : ">183" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | }, 17 | "properties" : { 18 | "auto-scaling" : "auto" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : "<=145" 11 | }, 12 | { 13 | "idiom" : "watch", 14 | "scale" : "2x", 15 | "screen-width" : ">183" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "auto-scaling" : "auto" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "watch", 5 | "scale" : "2x" 6 | }, 7 | { 8 | "idiom" : "watch", 9 | "scale" : "2x", 10 | "screen-width" : "<=145" 11 | }, 12 | { 13 | "idiom" : "watch", 14 | "scale" : "2x", 15 | "screen-width" : ">183" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "auto-scaling" : "auto" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/close_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "close_icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/close_icon.imageset/close_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/CircleTimer WatchKit Extension/Resources/Assets.xcassets/close_icon.imageset/close_icon.png -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/pause_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pause_icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/pause_icon.imageset/pause_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/CircleTimer WatchKit Extension/Resources/Assets.xcassets/pause_icon.imageset/pause_icon.png -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/play_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "play_icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/play_icon.imageset/play_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/CircleTimer WatchKit Extension/Resources/Assets.xcassets/play_icon.imageset/play_icon.png -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/restart_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "restart_icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/restart_icon.imageset/restart_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/CircleTimer WatchKit Extension/Resources/Assets.xcassets/restart_icon.imageset/restart_icon.png -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/timer_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "timer_icon.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Assets.xcassets/timer_icon.imageset/timer_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/CircleTimer WatchKit Extension/Resources/Assets.xcassets/timer_icon.imageset/timer_icon.png -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | WKAppBundleIdentifier 10 | com.kunst.circletimer.watchkitapp 11 | 12 | NSExtensionPointIdentifier 13 | com.apple.watchkit 14 | 15 | UIBackgroundModes 16 | 17 | WKBackgroundModes 18 | 19 | self-care 20 | workout-processing 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/PushNotificationPayload.apns: -------------------------------------------------------------------------------- 1 | { 2 | "aps": { 3 | "alert": { 4 | "body": "Test message", 5 | "title": "Optional title", 6 | "subtitle": "Optional subtitle" 7 | }, 8 | "category": "myCategory", 9 | "thread-id": "5280" 10 | }, 11 | 12 | "WatchKit Simulator Actions": [ 13 | { 14 | "title": "First Button", 15 | "identifier": "firstButtonAction" 16 | } 17 | ], 18 | 19 | "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App." 20 | } 21 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | CircleTimer 4 | 5 | Created by Kirill Kunst on 24.01.2022. 6 | Copyright © 2022 Kirill Kunst. All rights reserved. 7 | */ 8 | 9 | "stop" = "Stop"; 10 | "cancel" = "Cancel"; 11 | "start" = "Start"; 12 | "timers" = "Timers"; 13 | "recent" = "Recent"; 14 | "settings" = "Settings"; 15 | "minutes" = "Minutes"; 16 | "hours" = "Hours"; 17 | "seconds" = "Seconds"; 18 | 19 | 20 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Resources/ru.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | CircleTimer 4 | 5 | Created by Kirill Kunst on 24.01.2022. 6 | Copyright © 2022 Kirill Kunst. All rights reserved. 7 | */ 8 | "stop" = "Стоп"; 9 | "cancel" = "Отмена"; 10 | "start" = "Старт"; 11 | "timers" = "Таймеры"; 12 | "recent" = "Недавние"; 13 | "settings" = "Настройка"; 14 | "minutes" = "Минуты"; 15 | "hours" = "Часы"; 16 | "seconds" = "Секунды"; 17 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Screens/Home/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeView: View { 11 | 12 | @ObservedObject var viewModel = HomeViewModel(appContext: AppContext.shared) 13 | 14 | var body: some View { 15 | VStack() { 16 | ScrollView(.vertical, showsIndicators: false) { 17 | settingsButton 18 | timersList 19 | } 20 | 21 | Spacer() 22 | } 23 | .navigationTitle("timers".localized) 24 | } 25 | 26 | var settingsButton: some View { 27 | NavigationLink(destination: SettingsView()) { 28 | Text("settings".localized) 29 | } 30 | } 31 | 32 | var timersList: some View { 33 | VStack { 34 | if viewModel.timers.count > 0 { 35 | Text("recent".localized) 36 | } 37 | ForEach(viewModel.timers) { timer in 38 | listItem(for: timer) 39 | } 40 | } 41 | .padding(.top, 10.0) 42 | } 43 | 44 | func listItem(for timer: CircleTimer) -> some View { 45 | ZStack { 46 | RoundedRectangle(cornerRadius: 10.0) 47 | .foregroundColor(.orange) 48 | .cornerRadius(10.0) 49 | .frame(height: 50.0) 50 | Text(timer.seconds.formattedSecondsView) 51 | .foregroundColor(Color.black) 52 | 53 | } 54 | .onTapGesture { 55 | viewModel.select(timer: timer) 56 | } 57 | .background(NavigationLink(destination: viewModel.isTimerCreated ? TimerView() : nil, tag: timer, 58 | selection: $viewModel.selectedTimer) { EmptyView() } 59 | .buttonStyle(PlainButtonStyle())) 60 | } 61 | } 62 | 63 | struct HomeView_Previews: PreviewProvider { 64 | static var previews: some View { 65 | HomeView() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Screens/Home/HomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class HomeViewModel: ObservableObject { 12 | 13 | @Published var timers: [CircleTimer] = [CircleTimer]() 14 | @Published var selectedTimer: CircleTimer? = nil 15 | 16 | var isTimerCreated: Bool { 17 | timerService.currentTimer != nil 18 | } 19 | 20 | private var timerService: TimerServiceProtocol 21 | private var cancellables = Set() 22 | 23 | init(appContext: AppContextProtocol) { 24 | timerService = appContext.timerService 25 | timerService.timersPublisher() 26 | .map({ $0.sorted(by: { $0.createdDate > $1.createdDate }) }) 27 | .assign(to: \.timers, on: self) 28 | .store(in: &cancellables) 29 | } 30 | 31 | func select(timer: CircleTimer) { 32 | if let currentTimer = timerService.currentTimer { 33 | timerService.destroyWorker(for: currentTimer) 34 | } 35 | 36 | timerService.currentTimer = timer 37 | selectedTimer = timer 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Screens/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | 12 | @ObservedObject private var viewModel = SettingsViewModel(appContext: AppContext.shared) 13 | @Environment(\.presentationMode) var presentationMode 14 | @State var isActive: Bool = false 15 | 16 | var body: some View { 17 | VStack { 18 | TimePickerView(hour: $viewModel.hour, minute: $viewModel.minute, second: $viewModel.second) 19 | Spacer() 20 | HStack { 21 | stopButton 22 | startButton 23 | } 24 | } 25 | .navigationBarHidden(true) 26 | .navigationBarBackButtonHidden(true) 27 | } 28 | 29 | var startButton: some View { 30 | ZStack { 31 | RoundedRectangle(cornerRadius: 20.0) 32 | .foregroundColor(.green) 33 | .cornerRadius(20.0) 34 | .frame(height: 44.0) 35 | Text("start".localized) 36 | .foregroundColor(Color.black) 37 | .onTapGesture { 38 | if viewModel.isTimerShouldBeCreated { 39 | viewModel.start() 40 | isActive.toggle() 41 | } 42 | } 43 | } 44 | .background(NavigationLink(destination: viewModel.isTimerCreated ? TimerView() : nil, isActive: $isActive) { EmptyView() } 45 | .buttonStyle(PlainButtonStyle())) 46 | } 47 | 48 | var stopButton: some View { 49 | Button(action: { 50 | presentationMode.wrappedValue.dismiss() 51 | }, label: { 52 | ZStack { 53 | RoundedRectangle(cornerRadius: 20.0) 54 | .foregroundColor(.white) 55 | .opacity(0.2) 56 | .cornerRadius(20.0) 57 | .frame(height: 44.0) 58 | Text("cancel".localized) 59 | .foregroundColor(Color.white) 60 | } 61 | }) 62 | .buttonStyle(PlainButtonStyle()) 63 | } 64 | } 65 | 66 | struct SettingsView_Previews: PreviewProvider { 67 | static var previews: some View { 68 | SettingsView() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Screens/Settings/SettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewModel.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | class SettingsViewModel: ObservableObject { 11 | 12 | @Published var hour: Int = 0 13 | @Published var minute: Int = 0 14 | @Published var second: Int = 0 15 | 16 | var isTimerCreated: Bool { 17 | timerService.currentTimer != nil 18 | } 19 | 20 | var isTimerShouldBeCreated: Bool { 21 | totalSeconds > 0 22 | } 23 | 24 | private var totalSeconds: Int { 25 | return (hour * 60 * 60) + (minute * 60) + second 26 | } 27 | 28 | private var timerService: TimerServiceProtocol 29 | 30 | init(appContext: AppContextProtocol) { 31 | timerService = appContext.timerService 32 | } 33 | 34 | func start() { 35 | let timer = CircleTimer(seconds: totalSeconds, continious: true) 36 | timerService.add(timer: timer) 37 | timerService.currentTimer = timer 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Screens/Timer/TimerButtonModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerButtonModifier.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 25.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct TimerButtonImageModifier: ViewModifier { 13 | func body(content: Content) -> some View { 14 | content 15 | .frame(width: 20.0, height: 20.0) 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Screens/Timer/TimerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerView.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TimerView: View { 11 | 12 | @Environment(\.presentationMode) var presentationMode 13 | @ObservedObject private var viewModel = TimerViewModel(appContext: AppContext.shared) 14 | 15 | var body: some View { 16 | VStack { 17 | CountdownCircleView(progress: $viewModel.progress, timeLabel: $viewModel.timeString) 18 | .padding(EdgeInsets(top: 5.0, leading: 0.0, bottom: 0.0, trailing: 0.0)) 19 | Spacer() 20 | HStack { 21 | cancelButton 22 | Spacer() 23 | restartButton 24 | Spacer() 25 | toggleButton 26 | }.padding(.top, 5.0) 27 | } 28 | .onAppear { 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 30 | viewModel.start() 31 | } 32 | } 33 | .navigationBarHidden(true) 34 | .navigationBarBackButtonHidden(true) 35 | } 36 | 37 | var cancelButton: some View { 38 | Button(action: { 39 | viewModel.stop() 40 | presentationMode.wrappedValue.dismiss() 41 | }) { 42 | buttonImage(name: "close_icon") 43 | } 44 | .buttonStyle(.borderless) 45 | } 46 | 47 | var toggleButton: some View { 48 | Button(action: { 49 | viewModel.toggle() 50 | }) { 51 | if viewModel.isPaused { 52 | buttonImage(name: "play_icon") 53 | } else { 54 | buttonImage(name: "pause_icon") 55 | } 56 | } 57 | .buttonStyle(.borderless) 58 | } 59 | 60 | var restartButton: some View { 61 | Button(action: { 62 | viewModel.restart() 63 | }) { 64 | buttonImage(name: "restart_icon") 65 | } 66 | .buttonStyle(.borderless) 67 | } 68 | 69 | func buttonImage(name: String) -> some View { 70 | ZStack { 71 | RoundedRectangle(cornerRadius: 22.0) 72 | .foregroundColor(.white) 73 | .opacity(0.2) 74 | .cornerRadius(22.0) 75 | .frame(width: 44.0, height: 44.0) 76 | Image(name) 77 | .resizable() 78 | .modifier(TimerButtonImageModifier()) 79 | .padding() 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Screens/Timer/TimerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerViewModel.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import Combine 11 | 12 | class TimerViewModel: ObservableObject { 13 | 14 | @Published var timer: CircleTimer? 15 | @Published var progress: Float = 1.0 16 | @Published var timeString: String = "" 17 | @Published var isPaused: Bool = false 18 | 19 | private var timerService: TimerServiceProtocol 20 | private var worker: TimerWorker? 21 | private var cancellables = Set() 22 | 23 | init(appContext: AppContextProtocol) { 24 | timerService = appContext.timerService 25 | } 26 | 27 | func start() { 28 | self.timer = timerService.currentTimer 29 | 30 | guard let timer = self.timer else { return } 31 | worker = timerService.raiseWorker(for: timer) 32 | worker?.stop() 33 | worker?.$secondsLeft.sink(receiveValue: { [weak self] receivedValue in 34 | guard let self = self else { return } 35 | self.timeString = receivedValue.formattedSecondsView 36 | self.progress = Float(Float(receivedValue) / Float(timer.seconds)) 37 | }).store(in: &cancellables) 38 | 39 | worker?.start() 40 | 41 | } 42 | 43 | func stop() { 44 | worker?.stop() 45 | guard let timer = self.timer else { return } 46 | timerService.destroyWorker(for: timer) 47 | timerService.currentTimer = nil 48 | } 49 | 50 | func toggle() { 51 | guard let worker = worker else { return } 52 | 53 | if worker.state == .active { 54 | worker.pause() 55 | isPaused = true 56 | } else if worker.state == .paused { 57 | worker.resume() 58 | isPaused = false 59 | } 60 | } 61 | 62 | func restart() { 63 | worker?.stop() 64 | worker?.start() 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Views/CountdownCircleView/CountdownCircleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountdownCircleView.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct CountdownCircleView: View { 12 | 13 | struct Metrics { 14 | static let backViewOpacity = 0.4 15 | static let lineWidth = 5.0 16 | static let backgroundColor = Color.white 17 | static let foregroundColor = Color.orange 18 | } 19 | 20 | @Binding var progress: Float 21 | @Binding var timeLabel: String 22 | 23 | var body: some View { 24 | ZStack { 25 | backgroundCircle 26 | foregroundCircle 27 | countdownTimeLabel 28 | } 29 | } 30 | 31 | var backgroundCircle: some View { 32 | Circle() 33 | .stroke(lineWidth: Metrics.lineWidth) 34 | .opacity(Metrics.backViewOpacity) 35 | .foregroundColor(Metrics.backgroundColor) 36 | } 37 | 38 | var foregroundCircle: some View { 39 | Circle() 40 | .trim(from: 0.0, to: CGFloat(min(self.progress, 1.0))) 41 | .stroke(style: StrokeStyle(lineWidth: Metrics.lineWidth, lineCap: .round, lineJoin: .round)) 42 | .foregroundColor(Metrics.foregroundColor) 43 | .rotationEffect(Angle(degrees: 270.0)) 44 | } 45 | 46 | var countdownTimeLabel: some View { 47 | Text(timeLabel).foregroundColor(.white) 48 | .font(Font.system(size: 30.0)) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Views/TimePickerView/TimePickerTitleModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerTitleModifier.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 24.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct TimePickerTitleModifier: ViewModifier { 13 | func body(content: Content) -> some View { 14 | content 15 | .foregroundColor(Color.black) 16 | .padding(3.0) 17 | .font(Font.system(size:10.0).bold()) 18 | } 19 | } 20 | 21 | struct TimePickerTitleContainerModifier: ViewModifier { 22 | func body(content: Content) -> some View { 23 | content 24 | .background(Color.green).cornerRadius(12.0) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Views/TimePickerView/TimePickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimePickerView.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TimePickerView: View { 12 | 13 | var viewModel = TimePickerViewModel() 14 | 15 | @Binding public var hour: Int 16 | @Binding public var minute: Int 17 | @Binding public var second: Int 18 | 19 | var body: some View { 20 | componentPickers 21 | } 22 | 23 | private var componentPickers: some View { 24 | HStack { 25 | hoursPicker 26 | minutesPicker 27 | secondsPicker 28 | } 29 | .pickerStyle(.wheel) 30 | } 31 | 32 | private var hoursPicker: some View { 33 | Picker(selection: $hour) { 34 | ForEach(viewModel.hours) { hour in 35 | Text(hour.formattedString) 36 | .minimumScaleFactor(0.5) 37 | .tag(hour) 38 | } 39 | } label: { 40 | pickerTitle(for: "hours".localized) 41 | } 42 | } 43 | 44 | private var minutesPicker: some View { 45 | Picker(selection: $minute) { 46 | ForEach(viewModel.minutes) { minute in 47 | Text(minute.formattedString) 48 | .minimumScaleFactor(0.5) 49 | .tag(minute) 50 | } 51 | } label: { 52 | pickerTitle(for: "minutes".localized) 53 | } 54 | } 55 | 56 | private var secondsPicker: some View { 57 | Picker(selection: $second) { 58 | ForEach(viewModel.seconds) { second in 59 | Text(second.formattedString) 60 | .minimumScaleFactor(0.5) 61 | .tag(second) 62 | } 63 | } label: { 64 | pickerTitle(for: "seconds".localized) 65 | } 66 | } 67 | 68 | private func pickerTitle(for text: String) -> some View { 69 | VStack { 70 | Text(text).modifier(TimePickerTitleModifier()) 71 | }.modifier(TimePickerTitleContainerModifier()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CircleTimer WatchKit Extension/Views/TimePickerView/TimePickerViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimePickerViewModel.swift 3 | // CircleTimer WatchKit Extension 4 | // 5 | // Created by Kirill Kunst on 22.01.2022. 6 | // Copyright © 2022 Kirill Kunst. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct TimePickerViewModel { 12 | var hours: Range { 13 | return 0..<24 14 | } 15 | 16 | var minutes: Range { 17 | return 0..<60 18 | } 19 | 20 | var seconds: Range { 21 | return 0..<60 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CC0C7606279BD076006C220A /* CircleTimer WatchKit App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = CC0C7605279BD076006C220A /* CircleTimer WatchKit App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 11 | CC0C760B279BD077006C220A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC0C760A279BD077006C220A /* Assets.xcassets */; }; 12 | CC0C7616279BD077006C220A /* CircleTimerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C7615279BD077006C220A /* CircleTimerApp.swift */; }; 13 | CC0C761A279BD077006C220A /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C7619279BD077006C220A /* NotificationController.swift */; }; 14 | CC0C761C279BD077006C220A /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C761B279BD077006C220A /* NotificationView.swift */; }; 15 | CC0C761E279BD077006C220A /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C761D279BD077006C220A /* ComplicationController.swift */; }; 16 | CC0C7620279BD077006C220A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC0C761F279BD077006C220A /* Assets.xcassets */; }; 17 | CC0C7623279BD077006C220A /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC0C7622279BD077006C220A /* Preview Assets.xcassets */; }; 18 | CC0C7639279BD23B006C220A /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C7638279BD23B006C220A /* SettingsView.swift */; }; 19 | CC0C763B279BD241006C220A /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C763A279BD241006C220A /* TimerView.swift */; }; 20 | CC0C7640279BD325006C220A /* TimerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C763F279BD325006C220A /* TimerViewModel.swift */; }; 21 | CC0C7642279BD32E006C220A /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C7641279BD32E006C220A /* SettingsViewModel.swift */; }; 22 | CC0C7644279BD335006C220A /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C7643279BD335006C220A /* HomeViewModel.swift */; }; 23 | CC970131279BD7EC00208A51 /* CircleTimer WatchKit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CC0C7610279BD077006C220A /* CircleTimer WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 24 | CC970136279BD9D900208A51 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0C7617279BD077006C220A /* HomeView.swift */; }; 25 | CC970139279BDC0B00208A51 /* TimePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC970138279BDC0B00208A51 /* TimePickerView.swift */; }; 26 | CC97013B279BDCB300208A51 /* TimePickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC97013A279BDCB300208A51 /* TimePickerViewModel.swift */; }; 27 | CC97013E279BDF2F00208A51 /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC97013D279BDF2F00208A51 /* IntExtension.swift */; }; 28 | CC970141279C134A00208A51 /* CountdownCircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC970140279C134A00208A51 /* CountdownCircleView.swift */; }; 29 | CC970148279D575900208A51 /* CircleTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC970147279D575900208A51 /* CircleTimer.swift */; }; 30 | CC97014D279D57FF00208A51 /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC97014C279D57FF00208A51 /* AppContext.swift */; }; 31 | CC970153279D592600208A51 /* TimerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC970152279D592600208A51 /* TimerService.swift */; }; 32 | CC970156279D595900208A51 /* TimerRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC970155279D595900208A51 /* TimerRepository.swift */; }; 33 | CCB9B6C927BD08EF00AED7B8 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CCB9B6C827BD08EF00AED7B8 /* HealthKit.framework */; }; 34 | CCB9B6CB27BD244000AED7B8 /* HealthKitSessionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB9B6CA27BD244000AED7B8 /* HealthKitSessionProvider.swift */; }; 35 | CCB9B6CE27BD259D00AED7B8 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB9B6CD27BD259D00AED7B8 /* Logging.swift */; }; 36 | CCCBDA18279EC44400E69B51 /* UserDefaults+Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCCBDA17279EC44400E69B51 /* UserDefaults+Coding.swift */; }; 37 | CCCBDA1A279EC7B900E69B51 /* TimerWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCCBDA19279EC7B900E69B51 /* TimerWorker.swift */; }; 38 | CCCBDA20279F29E500E69B51 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CCCBDA22279F29E500E69B51 /* Localizable.strings */; }; 39 | CCCBDA25279F2AAA00E69B51 /* String+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCCBDA24279F2AAA00E69B51 /* String+Localizable.swift */; }; 40 | CCCBDA27279F2CF100E69B51 /* TimePickerTitleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCCBDA26279F2CF100E69B51 /* TimePickerTitleModifier.swift */; }; 41 | CCCBDA2B279F44B600E69B51 /* TimerButtonModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCCBDA2A279F44B600E69B51 /* TimerButtonModifier.swift */; }; 42 | /* End PBXBuildFile section */ 43 | 44 | /* Begin PBXContainerItemProxy section */ 45 | CC0C7607279BD076006C220A /* PBXContainerItemProxy */ = { 46 | isa = PBXContainerItemProxy; 47 | containerPortal = CC0C75FB279BD076006C220A /* Project object */; 48 | proxyType = 1; 49 | remoteGlobalIDString = CC0C7604279BD076006C220A; 50 | remoteInfo = "CircleTimer WatchKit App"; 51 | }; 52 | CC970132279BD7EC00208A51 /* PBXContainerItemProxy */ = { 53 | isa = PBXContainerItemProxy; 54 | containerPortal = CC0C75FB279BD076006C220A /* Project object */; 55 | proxyType = 1; 56 | remoteGlobalIDString = CC0C760F279BD077006C220A; 57 | remoteInfo = "CircleTimer WatchKit Extension"; 58 | }; 59 | /* End PBXContainerItemProxy section */ 60 | 61 | /* Begin PBXCopyFilesBuildPhase section */ 62 | CC0C762F279BD077006C220A /* Embed Watch Content */ = { 63 | isa = PBXCopyFilesBuildPhase; 64 | buildActionMask = 2147483647; 65 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; 66 | dstSubfolderSpec = 16; 67 | files = ( 68 | CC0C7606279BD076006C220A /* CircleTimer WatchKit App.app in Embed Watch Content */, 69 | ); 70 | name = "Embed Watch Content"; 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | CC970134279BD7EC00208A51 /* Embed App Extensions */ = { 74 | isa = PBXCopyFilesBuildPhase; 75 | buildActionMask = 2147483647; 76 | dstPath = ""; 77 | dstSubfolderSpec = 13; 78 | files = ( 79 | CC970131279BD7EC00208A51 /* CircleTimer WatchKit Extension.appex in Embed App Extensions */, 80 | ); 81 | name = "Embed App Extensions"; 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXCopyFilesBuildPhase section */ 85 | 86 | /* Begin PBXFileReference section */ 87 | CC0C7601279BD076006C220A /* CircleTimer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CircleTimer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 88 | CC0C7605279BD076006C220A /* CircleTimer WatchKit App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CircleTimer WatchKit App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | CC0C760A279BD077006C220A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 90 | CC0C7610279BD077006C220A /* CircleTimer WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "CircleTimer WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 91 | CC0C7615279BD077006C220A /* CircleTimerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleTimerApp.swift; sourceTree = ""; }; 92 | CC0C7617279BD077006C220A /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 93 | CC0C7619279BD077006C220A /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; }; 94 | CC0C761B279BD077006C220A /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; }; 95 | CC0C761D279BD077006C220A /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = ""; }; 96 | CC0C761F279BD077006C220A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97 | CC0C7622279BD077006C220A /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 98 | CC0C7624279BD077006C220A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 99 | CC0C7625279BD077006C220A /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = ""; }; 100 | CC0C7638279BD23B006C220A /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 101 | CC0C763A279BD241006C220A /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = ""; }; 102 | CC0C763F279BD325006C220A /* TimerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerViewModel.swift; sourceTree = ""; }; 103 | CC0C7641279BD32E006C220A /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 104 | CC0C7643279BD335006C220A /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 105 | CC970138279BDC0B00208A51 /* TimePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerView.swift; sourceTree = ""; }; 106 | CC97013A279BDCB300208A51 /* TimePickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerViewModel.swift; sourceTree = ""; }; 107 | CC97013D279BDF2F00208A51 /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; 108 | CC970140279C134A00208A51 /* CountdownCircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownCircleView.swift; sourceTree = ""; }; 109 | CC970147279D575900208A51 /* CircleTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleTimer.swift; sourceTree = ""; }; 110 | CC97014C279D57FF00208A51 /* AppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; 111 | CC970152279D592600208A51 /* TimerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerService.swift; sourceTree = ""; }; 112 | CC970155279D595900208A51 /* TimerRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerRepository.swift; sourceTree = ""; }; 113 | CCB9B6C727BD08EF00AED7B8 /* CircleTimer WatchKit Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "CircleTimer WatchKit Extension.entitlements"; sourceTree = ""; }; 114 | CCB9B6C827BD08EF00AED7B8 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS8.3.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; }; 115 | CCB9B6CA27BD244000AED7B8 /* HealthKitSessionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitSessionProvider.swift; sourceTree = ""; }; 116 | CCB9B6CD27BD259D00AED7B8 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; 117 | CCCBDA17279EC44400E69B51 /* UserDefaults+Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Coding.swift"; sourceTree = ""; }; 118 | CCCBDA19279EC7B900E69B51 /* TimerWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerWorker.swift; sourceTree = ""; }; 119 | CCCBDA21279F29E500E69B51 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 120 | CCCBDA23279F29F600E69B51 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 121 | CCCBDA24279F2AAA00E69B51 /* String+Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localizable.swift"; sourceTree = ""; }; 122 | CCCBDA26279F2CF100E69B51 /* TimePickerTitleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimePickerTitleModifier.swift; sourceTree = ""; }; 123 | CCCBDA2A279F44B600E69B51 /* TimerButtonModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerButtonModifier.swift; sourceTree = ""; }; 124 | /* End PBXFileReference section */ 125 | 126 | /* Begin PBXFrameworksBuildPhase section */ 127 | CC0C760D279BD077006C220A /* Frameworks */ = { 128 | isa = PBXFrameworksBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | CCB9B6C927BD08EF00AED7B8 /* HealthKit.framework in Frameworks */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXFrameworksBuildPhase section */ 136 | 137 | /* Begin PBXGroup section */ 138 | CC0C75FA279BD076006C220A = { 139 | isa = PBXGroup; 140 | children = ( 141 | CC0C7609279BD076006C220A /* CircleTimer WatchKit App */, 142 | CC0C7614279BD077006C220A /* CircleTimer WatchKit Extension */, 143 | CC0C7602279BD076006C220A /* Products */, 144 | CC970130279BD7EC00208A51 /* Frameworks */, 145 | ); 146 | sourceTree = ""; 147 | }; 148 | CC0C7602279BD076006C220A /* Products */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | CC0C7601279BD076006C220A /* CircleTimer.app */, 152 | CC0C7605279BD076006C220A /* CircleTimer WatchKit App.app */, 153 | CC0C7610279BD077006C220A /* CircleTimer WatchKit Extension.appex */, 154 | ); 155 | name = Products; 156 | sourceTree = ""; 157 | }; 158 | CC0C7609279BD076006C220A /* CircleTimer WatchKit App */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | CC0C760A279BD077006C220A /* Assets.xcassets */, 162 | ); 163 | path = "CircleTimer WatchKit App"; 164 | sourceTree = ""; 165 | }; 166 | CC0C7614279BD077006C220A /* CircleTimer WatchKit Extension */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | CCB9B6C727BD08EF00AED7B8 /* CircleTimer WatchKit Extension.entitlements */, 170 | CC0C7633279BD10B006C220A /* App */, 171 | CC970145279D558300208A51 /* Common */, 172 | CC970150279D590200208A51 /* Core */, 173 | CC0C7634279BD11E006C220A /* Screens */, 174 | CC0C7636279BD13F006C220A /* Resources */, 175 | CC970144279D553200208A51 /* Views */, 176 | ); 177 | path = "CircleTimer WatchKit Extension"; 178 | sourceTree = ""; 179 | }; 180 | CC0C7621279BD077006C220A /* Preview Content */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | CC0C7622279BD077006C220A /* Preview Assets.xcassets */, 184 | ); 185 | path = "Preview Content"; 186 | sourceTree = ""; 187 | }; 188 | CC0C7633279BD10B006C220A /* App */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | CC0C7635279BD126006C220A /* Notification */, 192 | CC0C7637279BD156006C220A /* Complication */, 193 | CC0C7615279BD077006C220A /* CircleTimerApp.swift */, 194 | ); 195 | path = App; 196 | sourceTree = ""; 197 | }; 198 | CC0C7634279BD11E006C220A /* Screens */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | CC0C763E279BD313006C220A /* Timer */, 202 | CC0C763D279BD30D006C220A /* Settings */, 203 | CC0C763C279BD309006C220A /* Home */, 204 | ); 205 | path = Screens; 206 | sourceTree = ""; 207 | }; 208 | CC0C7635279BD126006C220A /* Notification */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | CC0C7619279BD077006C220A /* NotificationController.swift */, 212 | CC0C761B279BD077006C220A /* NotificationView.swift */, 213 | ); 214 | path = Notification; 215 | sourceTree = ""; 216 | }; 217 | CC0C7636279BD13F006C220A /* Resources */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | CCCBDA22279F29E500E69B51 /* Localizable.strings */, 221 | CC0C7621279BD077006C220A /* Preview Content */, 222 | CC0C761F279BD077006C220A /* Assets.xcassets */, 223 | CC0C7624279BD077006C220A /* Info.plist */, 224 | CC0C7625279BD077006C220A /* PushNotificationPayload.apns */, 225 | ); 226 | path = Resources; 227 | sourceTree = ""; 228 | }; 229 | CC0C7637279BD156006C220A /* Complication */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | CC0C761D279BD077006C220A /* ComplicationController.swift */, 233 | ); 234 | path = Complication; 235 | sourceTree = ""; 236 | }; 237 | CC0C763C279BD309006C220A /* Home */ = { 238 | isa = PBXGroup; 239 | children = ( 240 | CC0C7617279BD077006C220A /* HomeView.swift */, 241 | CC0C7643279BD335006C220A /* HomeViewModel.swift */, 242 | ); 243 | path = Home; 244 | sourceTree = ""; 245 | }; 246 | CC0C763D279BD30D006C220A /* Settings */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | CC0C7638279BD23B006C220A /* SettingsView.swift */, 250 | CC0C7641279BD32E006C220A /* SettingsViewModel.swift */, 251 | ); 252 | path = Settings; 253 | sourceTree = ""; 254 | }; 255 | CC0C763E279BD313006C220A /* Timer */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | CC0C763A279BD241006C220A /* TimerView.swift */, 259 | CC0C763F279BD325006C220A /* TimerViewModel.swift */, 260 | CCCBDA2A279F44B600E69B51 /* TimerButtonModifier.swift */, 261 | ); 262 | path = Timer; 263 | sourceTree = ""; 264 | }; 265 | CC970130279BD7EC00208A51 /* Frameworks */ = { 266 | isa = PBXGroup; 267 | children = ( 268 | CCB9B6C827BD08EF00AED7B8 /* HealthKit.framework */, 269 | ); 270 | name = Frameworks; 271 | sourceTree = ""; 272 | }; 273 | CC970137279BDBFD00208A51 /* TimePickerView */ = { 274 | isa = PBXGroup; 275 | children = ( 276 | CC970138279BDC0B00208A51 /* TimePickerView.swift */, 277 | CC97013A279BDCB300208A51 /* TimePickerViewModel.swift */, 278 | CCCBDA26279F2CF100E69B51 /* TimePickerTitleModifier.swift */, 279 | ); 280 | path = TimePickerView; 281 | sourceTree = ""; 282 | }; 283 | CC97013C279BDF2400208A51 /* Extensions */ = { 284 | isa = PBXGroup; 285 | children = ( 286 | CC97013D279BDF2F00208A51 /* IntExtension.swift */, 287 | CCCBDA24279F2AAA00E69B51 /* String+Localizable.swift */, 288 | ); 289 | path = Extensions; 290 | sourceTree = ""; 291 | }; 292 | CC97013F279C133100208A51 /* CountdownCircleView */ = { 293 | isa = PBXGroup; 294 | children = ( 295 | CC970140279C134A00208A51 /* CountdownCircleView.swift */, 296 | ); 297 | path = CountdownCircleView; 298 | sourceTree = ""; 299 | }; 300 | CC970144279D553200208A51 /* Views */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | CC97013F279C133100208A51 /* CountdownCircleView */, 304 | CC970137279BDBFD00208A51 /* TimePickerView */, 305 | ); 306 | path = Views; 307 | sourceTree = ""; 308 | }; 309 | CC970145279D558300208A51 /* Common */ = { 310 | isa = PBXGroup; 311 | children = ( 312 | CC97013C279BDF2400208A51 /* Extensions */, 313 | ); 314 | path = Common; 315 | sourceTree = ""; 316 | }; 317 | CC970146279D574C00208A51 /* Models */ = { 318 | isa = PBXGroup; 319 | children = ( 320 | CC970147279D575900208A51 /* CircleTimer.swift */, 321 | ); 322 | path = Models; 323 | sourceTree = ""; 324 | }; 325 | CC97014E279D58D800208A51 /* Context */ = { 326 | isa = PBXGroup; 327 | children = ( 328 | CC97014C279D57FF00208A51 /* AppContext.swift */, 329 | ); 330 | path = Context; 331 | sourceTree = ""; 332 | }; 333 | CC97014F279D58F900208A51 /* Services */ = { 334 | isa = PBXGroup; 335 | children = ( 336 | CCCBDA19279EC7B900E69B51 /* TimerWorker.swift */, 337 | CC970152279D592600208A51 /* TimerService.swift */, 338 | CCB9B6CA27BD244000AED7B8 /* HealthKitSessionProvider.swift */, 339 | ); 340 | path = Services; 341 | sourceTree = ""; 342 | }; 343 | CC970150279D590200208A51 /* Core */ = { 344 | isa = PBXGroup; 345 | children = ( 346 | CC97014E279D58D800208A51 /* Context */, 347 | CCCBDA16279EC42F00E69B51 /* Extensions */, 348 | CCB9B6CC27BD259500AED7B8 /* Logging */, 349 | CC970146279D574C00208A51 /* Models */, 350 | CC970154279D594B00208A51 /* Repositories */, 351 | CC97014F279D58F900208A51 /* Services */, 352 | ); 353 | path = Core; 354 | sourceTree = ""; 355 | }; 356 | CC970154279D594B00208A51 /* Repositories */ = { 357 | isa = PBXGroup; 358 | children = ( 359 | CC970155279D595900208A51 /* TimerRepository.swift */, 360 | ); 361 | path = Repositories; 362 | sourceTree = ""; 363 | }; 364 | CCB9B6CC27BD259500AED7B8 /* Logging */ = { 365 | isa = PBXGroup; 366 | children = ( 367 | CCB9B6CD27BD259D00AED7B8 /* Logging.swift */, 368 | ); 369 | path = Logging; 370 | sourceTree = ""; 371 | }; 372 | CCCBDA16279EC42F00E69B51 /* Extensions */ = { 373 | isa = PBXGroup; 374 | children = ( 375 | CCCBDA17279EC44400E69B51 /* UserDefaults+Coding.swift */, 376 | ); 377 | path = Extensions; 378 | sourceTree = ""; 379 | }; 380 | /* End PBXGroup section */ 381 | 382 | /* Begin PBXNativeTarget section */ 383 | CC0C7600279BD076006C220A /* CircleTimer */ = { 384 | isa = PBXNativeTarget; 385 | buildConfigurationList = CC0C7630279BD077006C220A /* Build configuration list for PBXNativeTarget "CircleTimer" */; 386 | buildPhases = ( 387 | CC0C75FF279BD076006C220A /* Resources */, 388 | CC0C762F279BD077006C220A /* Embed Watch Content */, 389 | ); 390 | buildRules = ( 391 | ); 392 | dependencies = ( 393 | CC0C7608279BD076006C220A /* PBXTargetDependency */, 394 | ); 395 | name = CircleTimer; 396 | packageProductDependencies = ( 397 | ); 398 | productName = CircleTimer; 399 | productReference = CC0C7601279BD076006C220A /* CircleTimer.app */; 400 | productType = "com.apple.product-type.application.watchapp2-container"; 401 | }; 402 | CC0C7604279BD076006C220A /* CircleTimer WatchKit App */ = { 403 | isa = PBXNativeTarget; 404 | buildConfigurationList = CC0C762C279BD077006C220A /* Build configuration list for PBXNativeTarget "CircleTimer WatchKit App" */; 405 | buildPhases = ( 406 | CC0C7603279BD076006C220A /* Resources */, 407 | CC970134279BD7EC00208A51 /* Embed App Extensions */, 408 | ); 409 | buildRules = ( 410 | ); 411 | dependencies = ( 412 | CC970133279BD7EC00208A51 /* PBXTargetDependency */, 413 | ); 414 | name = "CircleTimer WatchKit App"; 415 | productName = "CircleTimer WatchKit App"; 416 | productReference = CC0C7605279BD076006C220A /* CircleTimer WatchKit App.app */; 417 | productType = "com.apple.product-type.application.watchapp2"; 418 | }; 419 | CC0C760F279BD077006C220A /* CircleTimer WatchKit Extension */ = { 420 | isa = PBXNativeTarget; 421 | buildConfigurationList = CC0C7628279BD077006C220A /* Build configuration list for PBXNativeTarget "CircleTimer WatchKit Extension" */; 422 | buildPhases = ( 423 | CC0C760C279BD077006C220A /* Sources */, 424 | CC0C760D279BD077006C220A /* Frameworks */, 425 | CC0C760E279BD077006C220A /* Resources */, 426 | ); 427 | buildRules = ( 428 | ); 429 | dependencies = ( 430 | ); 431 | name = "CircleTimer WatchKit Extension"; 432 | packageProductDependencies = ( 433 | ); 434 | productName = "CircleTimer WatchKit Extension"; 435 | productReference = CC0C7610279BD077006C220A /* CircleTimer WatchKit Extension.appex */; 436 | productType = "com.apple.product-type.watchkit2-extension"; 437 | }; 438 | /* End PBXNativeTarget section */ 439 | 440 | /* Begin PBXProject section */ 441 | CC0C75FB279BD076006C220A /* Project object */ = { 442 | isa = PBXProject; 443 | attributes = { 444 | BuildIndependentTargetsInParallel = 1; 445 | LastSwiftUpdateCheck = 1320; 446 | LastUpgradeCheck = 1320; 447 | ORGANIZATIONNAME = "Kirill Kunst"; 448 | TargetAttributes = { 449 | CC0C7600279BD076006C220A = { 450 | CreatedOnToolsVersion = 13.2.1; 451 | }; 452 | CC0C7604279BD076006C220A = { 453 | CreatedOnToolsVersion = 13.2.1; 454 | }; 455 | CC0C760F279BD077006C220A = { 456 | CreatedOnToolsVersion = 13.2.1; 457 | }; 458 | }; 459 | }; 460 | buildConfigurationList = CC0C75FE279BD076006C220A /* Build configuration list for PBXProject "CircleTimer" */; 461 | compatibilityVersion = "Xcode 13.0"; 462 | developmentRegion = en; 463 | hasScannedForEncodings = 0; 464 | knownRegions = ( 465 | en, 466 | Base, 467 | ru, 468 | ); 469 | mainGroup = CC0C75FA279BD076006C220A; 470 | packageReferences = ( 471 | ); 472 | productRefGroup = CC0C7602279BD076006C220A /* Products */; 473 | projectDirPath = ""; 474 | projectRoot = ""; 475 | targets = ( 476 | CC0C7600279BD076006C220A /* CircleTimer */, 477 | CC0C7604279BD076006C220A /* CircleTimer WatchKit App */, 478 | CC0C760F279BD077006C220A /* CircleTimer WatchKit Extension */, 479 | ); 480 | }; 481 | /* End PBXProject section */ 482 | 483 | /* Begin PBXResourcesBuildPhase section */ 484 | CC0C75FF279BD076006C220A /* Resources */ = { 485 | isa = PBXResourcesBuildPhase; 486 | buildActionMask = 2147483647; 487 | files = ( 488 | ); 489 | runOnlyForDeploymentPostprocessing = 0; 490 | }; 491 | CC0C7603279BD076006C220A /* Resources */ = { 492 | isa = PBXResourcesBuildPhase; 493 | buildActionMask = 2147483647; 494 | files = ( 495 | CC0C760B279BD077006C220A /* Assets.xcassets in Resources */, 496 | ); 497 | runOnlyForDeploymentPostprocessing = 0; 498 | }; 499 | CC0C760E279BD077006C220A /* Resources */ = { 500 | isa = PBXResourcesBuildPhase; 501 | buildActionMask = 2147483647; 502 | files = ( 503 | CC0C7623279BD077006C220A /* Preview Assets.xcassets in Resources */, 504 | CCCBDA20279F29E500E69B51 /* Localizable.strings in Resources */, 505 | CC0C7620279BD077006C220A /* Assets.xcassets in Resources */, 506 | ); 507 | runOnlyForDeploymentPostprocessing = 0; 508 | }; 509 | /* End PBXResourcesBuildPhase section */ 510 | 511 | /* Begin PBXSourcesBuildPhase section */ 512 | CC0C760C279BD077006C220A /* Sources */ = { 513 | isa = PBXSourcesBuildPhase; 514 | buildActionMask = 2147483647; 515 | files = ( 516 | CC970156279D595900208A51 /* TimerRepository.swift in Sources */, 517 | CC0C763B279BD241006C220A /* TimerView.swift in Sources */, 518 | CC97013B279BDCB300208A51 /* TimePickerViewModel.swift in Sources */, 519 | CC0C761A279BD077006C220A /* NotificationController.swift in Sources */, 520 | CCCBDA1A279EC7B900E69B51 /* TimerWorker.swift in Sources */, 521 | CC970153279D592600208A51 /* TimerService.swift in Sources */, 522 | CC97014D279D57FF00208A51 /* AppContext.swift in Sources */, 523 | CC0C7644279BD335006C220A /* HomeViewModel.swift in Sources */, 524 | CCCBDA27279F2CF100E69B51 /* TimePickerTitleModifier.swift in Sources */, 525 | CCCBDA2B279F44B600E69B51 /* TimerButtonModifier.swift in Sources */, 526 | CC970141279C134A00208A51 /* CountdownCircleView.swift in Sources */, 527 | CCB9B6CE27BD259D00AED7B8 /* Logging.swift in Sources */, 528 | CC970136279BD9D900208A51 /* HomeView.swift in Sources */, 529 | CC0C761E279BD077006C220A /* ComplicationController.swift in Sources */, 530 | CC0C7640279BD325006C220A /* TimerViewModel.swift in Sources */, 531 | CC97013E279BDF2F00208A51 /* IntExtension.swift in Sources */, 532 | CC0C7639279BD23B006C220A /* SettingsView.swift in Sources */, 533 | CCCBDA25279F2AAA00E69B51 /* String+Localizable.swift in Sources */, 534 | CCCBDA18279EC44400E69B51 /* UserDefaults+Coding.swift in Sources */, 535 | CC0C7616279BD077006C220A /* CircleTimerApp.swift in Sources */, 536 | CC0C761C279BD077006C220A /* NotificationView.swift in Sources */, 537 | CC970139279BDC0B00208A51 /* TimePickerView.swift in Sources */, 538 | CC0C7642279BD32E006C220A /* SettingsViewModel.swift in Sources */, 539 | CCB9B6CB27BD244000AED7B8 /* HealthKitSessionProvider.swift in Sources */, 540 | CC970148279D575900208A51 /* CircleTimer.swift in Sources */, 541 | ); 542 | runOnlyForDeploymentPostprocessing = 0; 543 | }; 544 | /* End PBXSourcesBuildPhase section */ 545 | 546 | /* Begin PBXTargetDependency section */ 547 | CC0C7608279BD076006C220A /* PBXTargetDependency */ = { 548 | isa = PBXTargetDependency; 549 | target = CC0C7604279BD076006C220A /* CircleTimer WatchKit App */; 550 | targetProxy = CC0C7607279BD076006C220A /* PBXContainerItemProxy */; 551 | }; 552 | CC970133279BD7EC00208A51 /* PBXTargetDependency */ = { 553 | isa = PBXTargetDependency; 554 | target = CC0C760F279BD077006C220A /* CircleTimer WatchKit Extension */; 555 | targetProxy = CC970132279BD7EC00208A51 /* PBXContainerItemProxy */; 556 | }; 557 | /* End PBXTargetDependency section */ 558 | 559 | /* Begin PBXVariantGroup section */ 560 | CCCBDA22279F29E500E69B51 /* Localizable.strings */ = { 561 | isa = PBXVariantGroup; 562 | children = ( 563 | CCCBDA21279F29E500E69B51 /* en */, 564 | CCCBDA23279F29F600E69B51 /* ru */, 565 | ); 566 | name = Localizable.strings; 567 | sourceTree = ""; 568 | }; 569 | /* End PBXVariantGroup section */ 570 | 571 | /* Begin XCBuildConfiguration section */ 572 | CC0C7626279BD077006C220A /* Debug */ = { 573 | isa = XCBuildConfiguration; 574 | buildSettings = { 575 | ALWAYS_SEARCH_USER_PATHS = NO; 576 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 577 | CLANG_ANALYZER_NONNULL = YES; 578 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 579 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 580 | CLANG_CXX_LIBRARY = "libc++"; 581 | CLANG_ENABLE_MODULES = YES; 582 | CLANG_ENABLE_OBJC_ARC = YES; 583 | CLANG_ENABLE_OBJC_WEAK = YES; 584 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 585 | CLANG_WARN_BOOL_CONVERSION = YES; 586 | CLANG_WARN_COMMA = YES; 587 | CLANG_WARN_CONSTANT_CONVERSION = YES; 588 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 589 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 590 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 591 | CLANG_WARN_EMPTY_BODY = YES; 592 | CLANG_WARN_ENUM_CONVERSION = YES; 593 | CLANG_WARN_INFINITE_RECURSION = YES; 594 | CLANG_WARN_INT_CONVERSION = YES; 595 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 596 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 597 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 598 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 599 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 600 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 601 | CLANG_WARN_STRICT_PROTOTYPES = YES; 602 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 603 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 604 | CLANG_WARN_UNREACHABLE_CODE = YES; 605 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 606 | COPY_PHASE_STRIP = NO; 607 | DEBUG_INFORMATION_FORMAT = dwarf; 608 | ENABLE_STRICT_OBJC_MSGSEND = YES; 609 | ENABLE_TESTABILITY = YES; 610 | GCC_C_LANGUAGE_STANDARD = gnu11; 611 | GCC_DYNAMIC_NO_PIC = NO; 612 | GCC_NO_COMMON_BLOCKS = YES; 613 | GCC_OPTIMIZATION_LEVEL = 0; 614 | GCC_PREPROCESSOR_DEFINITIONS = ( 615 | "DEBUG=1", 616 | "$(inherited)", 617 | ); 618 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 619 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 620 | GCC_WARN_UNDECLARED_SELECTOR = YES; 621 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 622 | GCC_WARN_UNUSED_FUNCTION = YES; 623 | GCC_WARN_UNUSED_VARIABLE = YES; 624 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 625 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 626 | MTL_FAST_MATH = YES; 627 | ONLY_ACTIVE_ARCH = YES; 628 | SDKROOT = iphoneos; 629 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 630 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 631 | }; 632 | name = Debug; 633 | }; 634 | CC0C7627279BD077006C220A /* Release */ = { 635 | isa = XCBuildConfiguration; 636 | buildSettings = { 637 | ALWAYS_SEARCH_USER_PATHS = NO; 638 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 639 | CLANG_ANALYZER_NONNULL = YES; 640 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 641 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 642 | CLANG_CXX_LIBRARY = "libc++"; 643 | CLANG_ENABLE_MODULES = YES; 644 | CLANG_ENABLE_OBJC_ARC = YES; 645 | CLANG_ENABLE_OBJC_WEAK = YES; 646 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 647 | CLANG_WARN_BOOL_CONVERSION = YES; 648 | CLANG_WARN_COMMA = YES; 649 | CLANG_WARN_CONSTANT_CONVERSION = YES; 650 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 651 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 652 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 653 | CLANG_WARN_EMPTY_BODY = YES; 654 | CLANG_WARN_ENUM_CONVERSION = YES; 655 | CLANG_WARN_INFINITE_RECURSION = YES; 656 | CLANG_WARN_INT_CONVERSION = YES; 657 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 658 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 659 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 660 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 661 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 662 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 663 | CLANG_WARN_STRICT_PROTOTYPES = YES; 664 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 665 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 666 | CLANG_WARN_UNREACHABLE_CODE = YES; 667 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 668 | COPY_PHASE_STRIP = NO; 669 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 670 | ENABLE_NS_ASSERTIONS = NO; 671 | ENABLE_STRICT_OBJC_MSGSEND = YES; 672 | GCC_C_LANGUAGE_STANDARD = gnu11; 673 | GCC_NO_COMMON_BLOCKS = YES; 674 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 675 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 676 | GCC_WARN_UNDECLARED_SELECTOR = YES; 677 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 678 | GCC_WARN_UNUSED_FUNCTION = YES; 679 | GCC_WARN_UNUSED_VARIABLE = YES; 680 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 681 | MTL_ENABLE_DEBUG_INFO = NO; 682 | MTL_FAST_MATH = YES; 683 | SDKROOT = iphoneos; 684 | SWIFT_COMPILATION_MODE = wholemodule; 685 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 686 | VALIDATE_PRODUCT = YES; 687 | }; 688 | name = Release; 689 | }; 690 | CC0C7629279BD077006C220A /* Debug */ = { 691 | isa = XCBuildConfiguration; 692 | buildSettings = { 693 | ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; 694 | CODE_SIGN_ENTITLEMENTS = "CircleTimer WatchKit Extension/CircleTimer WatchKit Extension.entitlements"; 695 | CODE_SIGN_IDENTITY = "Apple Development"; 696 | CODE_SIGN_STYLE = Automatic; 697 | CURRENT_PROJECT_VERSION = 1; 698 | DEVELOPMENT_ASSET_PATHS = "\"CircleTimer WatchKit Extension/Resources/Preview Content\""; 699 | DEVELOPMENT_TEAM = 983G5U62PZ; 700 | ENABLE_PREVIEWS = YES; 701 | GENERATE_INFOPLIST_FILE = YES; 702 | INFOPLIST_FILE = "CircleTimer WatchKit Extension/Resources/Info.plist"; 703 | INFOPLIST_KEY_CFBundleDisplayName = "CircleTimer WatchKit Extension"; 704 | INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController"; 705 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 706 | INFOPLIST_KEY_WKWatchOnly = YES; 707 | LD_RUNPATH_SEARCH_PATHS = ( 708 | "$(inherited)", 709 | "@executable_path/Frameworks", 710 | "@executable_path/../../Frameworks", 711 | ); 712 | MARKETING_VERSION = 1.0; 713 | PRODUCT_BUNDLE_IDENTIFIER = com.kunst.circletimer.watchkitapp.watchkitextension; 714 | PRODUCT_NAME = "${TARGET_NAME}"; 715 | PROVISIONING_PROFILE_SPECIFIER = ""; 716 | SDKROOT = watchos; 717 | SKIP_INSTALL = YES; 718 | SWIFT_EMIT_LOC_STRINGS = YES; 719 | SWIFT_VERSION = 5.0; 720 | TARGETED_DEVICE_FAMILY = 4; 721 | WATCHOS_DEPLOYMENT_TARGET = 8.0; 722 | }; 723 | name = Debug; 724 | }; 725 | CC0C762A279BD077006C220A /* Release */ = { 726 | isa = XCBuildConfiguration; 727 | buildSettings = { 728 | ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; 729 | CODE_SIGN_ENTITLEMENTS = "CircleTimer WatchKit Extension/CircleTimer WatchKit Extension.entitlements"; 730 | CODE_SIGN_IDENTITY = "Apple Development"; 731 | CODE_SIGN_STYLE = Automatic; 732 | CURRENT_PROJECT_VERSION = 1; 733 | DEVELOPMENT_ASSET_PATHS = "\"CircleTimer WatchKit Extension/Resources/Preview Content\""; 734 | DEVELOPMENT_TEAM = 983G5U62PZ; 735 | ENABLE_PREVIEWS = YES; 736 | GENERATE_INFOPLIST_FILE = YES; 737 | INFOPLIST_FILE = "CircleTimer WatchKit Extension/Resources/Info.plist"; 738 | INFOPLIST_KEY_CFBundleDisplayName = "CircleTimer WatchKit Extension"; 739 | INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController"; 740 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 741 | INFOPLIST_KEY_WKWatchOnly = YES; 742 | LD_RUNPATH_SEARCH_PATHS = ( 743 | "$(inherited)", 744 | "@executable_path/Frameworks", 745 | "@executable_path/../../Frameworks", 746 | ); 747 | MARKETING_VERSION = 1.0; 748 | PRODUCT_BUNDLE_IDENTIFIER = com.kunst.circletimer.watchkitapp.watchkitextension; 749 | PRODUCT_NAME = "${TARGET_NAME}"; 750 | PROVISIONING_PROFILE_SPECIFIER = ""; 751 | SDKROOT = watchos; 752 | SKIP_INSTALL = YES; 753 | SWIFT_EMIT_LOC_STRINGS = YES; 754 | SWIFT_VERSION = 5.0; 755 | TARGETED_DEVICE_FAMILY = 4; 756 | WATCHOS_DEPLOYMENT_TARGET = 8.0; 757 | }; 758 | name = Release; 759 | }; 760 | CC0C762D279BD077006C220A /* Debug */ = { 761 | isa = XCBuildConfiguration; 762 | buildSettings = { 763 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 764 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 765 | CODE_SIGN_IDENTITY = "Apple Development"; 766 | CODE_SIGN_STYLE = Automatic; 767 | CURRENT_PROJECT_VERSION = 1; 768 | DEVELOPMENT_TEAM = 983G5U62PZ; 769 | GENERATE_INFOPLIST_FILE = YES; 770 | IBSC_MODULE = CircleTimer_WatchKit_Extension; 771 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 772 | MARKETING_VERSION = 1.0; 773 | PRODUCT_BUNDLE_IDENTIFIER = com.kunst.circletimer.watchkitapp; 774 | PRODUCT_NAME = "CircleTimer WatchKit App"; 775 | PROVISIONING_PROFILE_SPECIFIER = ""; 776 | SDKROOT = watchos; 777 | SKIP_INSTALL = YES; 778 | SWIFT_EMIT_LOC_STRINGS = YES; 779 | SWIFT_VERSION = 5.0; 780 | TARGETED_DEVICE_FAMILY = 4; 781 | WATCHOS_DEPLOYMENT_TARGET = 8.0; 782 | }; 783 | name = Debug; 784 | }; 785 | CC0C762E279BD077006C220A /* Release */ = { 786 | isa = XCBuildConfiguration; 787 | buildSettings = { 788 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 789 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 790 | CODE_SIGN_IDENTITY = "Apple Development"; 791 | CODE_SIGN_STYLE = Automatic; 792 | CURRENT_PROJECT_VERSION = 1; 793 | DEVELOPMENT_TEAM = 983G5U62PZ; 794 | GENERATE_INFOPLIST_FILE = YES; 795 | IBSC_MODULE = CircleTimer_WatchKit_Extension; 796 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 797 | MARKETING_VERSION = 1.0; 798 | PRODUCT_BUNDLE_IDENTIFIER = com.kunst.circletimer.watchkitapp; 799 | PRODUCT_NAME = "CircleTimer WatchKit App"; 800 | PROVISIONING_PROFILE_SPECIFIER = ""; 801 | SDKROOT = watchos; 802 | SKIP_INSTALL = YES; 803 | SWIFT_EMIT_LOC_STRINGS = YES; 804 | SWIFT_VERSION = 5.0; 805 | TARGETED_DEVICE_FAMILY = 4; 806 | WATCHOS_DEPLOYMENT_TARGET = 8.0; 807 | }; 808 | name = Release; 809 | }; 810 | CC0C7631279BD077006C220A /* Debug */ = { 811 | isa = XCBuildConfiguration; 812 | buildSettings = { 813 | CODE_SIGN_IDENTITY = "Apple Development"; 814 | CODE_SIGN_STYLE = Automatic; 815 | CURRENT_PROJECT_VERSION = 1; 816 | DEVELOPMENT_TEAM = 983G5U62PZ; 817 | MARKETING_VERSION = 1.0; 818 | PRODUCT_BUNDLE_IDENTIFIER = com.kunst.circletimer; 819 | PRODUCT_NAME = "$(TARGET_NAME)"; 820 | PROVISIONING_PROFILE_SPECIFIER = ""; 821 | SWIFT_VERSION = 5.0; 822 | }; 823 | name = Debug; 824 | }; 825 | CC0C7632279BD077006C220A /* Release */ = { 826 | isa = XCBuildConfiguration; 827 | buildSettings = { 828 | CODE_SIGN_IDENTITY = "Apple Development"; 829 | CODE_SIGN_STYLE = Automatic; 830 | CURRENT_PROJECT_VERSION = 1; 831 | DEVELOPMENT_TEAM = 983G5U62PZ; 832 | MARKETING_VERSION = 1.0; 833 | PRODUCT_BUNDLE_IDENTIFIER = com.kunst.circletimer; 834 | PRODUCT_NAME = "$(TARGET_NAME)"; 835 | PROVISIONING_PROFILE_SPECIFIER = ""; 836 | SWIFT_VERSION = 5.0; 837 | }; 838 | name = Release; 839 | }; 840 | /* End XCBuildConfiguration section */ 841 | 842 | /* Begin XCConfigurationList section */ 843 | CC0C75FE279BD076006C220A /* Build configuration list for PBXProject "CircleTimer" */ = { 844 | isa = XCConfigurationList; 845 | buildConfigurations = ( 846 | CC0C7626279BD077006C220A /* Debug */, 847 | CC0C7627279BD077006C220A /* Release */, 848 | ); 849 | defaultConfigurationIsVisible = 0; 850 | defaultConfigurationName = Release; 851 | }; 852 | CC0C7628279BD077006C220A /* Build configuration list for PBXNativeTarget "CircleTimer WatchKit Extension" */ = { 853 | isa = XCConfigurationList; 854 | buildConfigurations = ( 855 | CC0C7629279BD077006C220A /* Debug */, 856 | CC0C762A279BD077006C220A /* Release */, 857 | ); 858 | defaultConfigurationIsVisible = 0; 859 | defaultConfigurationName = Release; 860 | }; 861 | CC0C762C279BD077006C220A /* Build configuration list for PBXNativeTarget "CircleTimer WatchKit App" */ = { 862 | isa = XCConfigurationList; 863 | buildConfigurations = ( 864 | CC0C762D279BD077006C220A /* Debug */, 865 | CC0C762E279BD077006C220A /* Release */, 866 | ); 867 | defaultConfigurationIsVisible = 0; 868 | defaultConfigurationName = Release; 869 | }; 870 | CC0C7630279BD077006C220A /* Build configuration list for PBXNativeTarget "CircleTimer" */ = { 871 | isa = XCConfigurationList; 872 | buildConfigurations = ( 873 | CC0C7631279BD077006C220A /* Debug */, 874 | CC0C7632279BD077006C220A /* Release */, 875 | ); 876 | defaultConfigurationIsVisible = 0; 877 | defaultConfigurationName = Release; 878 | }; 879 | /* End XCConfigurationList section */ 880 | }; 881 | rootObject = CC0C75FB279BD076006C220A /* Project object */; 882 | } 883 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/xcshareddata/xcschemes/CircleTimer WatchKit App (Complication).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 58 | 60 | 66 | 67 | 68 | 69 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/xcshareddata/xcschemes/CircleTimer WatchKit App (Notification).xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 59 | 61 | 67 | 68 | 69 | 70 | 78 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/xcshareddata/xcschemes/CircleTimer WatchKit App.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 59 | 61 | 67 | 68 | 69 | 70 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/xcuserdata/kirillkunst.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /CircleTimer.xcodeproj/xcuserdata/kirillkunst.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CircleTimer WatchKit App (Complication).xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | CircleTimer WatchKit App (Notification).xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | CircleTimer WatchKit App.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 0 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | CC0C7600279BD076006C220A 26 | 27 | primary 28 | 29 | 30 | CC0C7604279BD076006C220A 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircleTimer 2 | Timer app with ability to reschedule next timer cycle automatically right after previous one 3 | 4 | # Screenshots 5 | 6 |

7 | 8 | 9 | 10 |

11 | 12 | # Made with 13 | 14 | - Swift 15 | - SwiftUI 16 | - WatchKit 17 | - HealthKit 18 | - MVVM 19 | - Combine 20 | 21 | # Maintainers 22 | - [Kirill Kunst](https://github.com/leoru) ([@kirill_kunst](https://twitter.com/kirill_kunst)) 23 | 24 | # License 25 | 26 | Circle Timer app is available under the MIT license. 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leoru/CircleTimer/d35a6af571797c0f64a13188dc86c321a9137403/screenshots/3.png --------------------------------------------------------------------------------