├── .github └── workflows │ └── test.yml ├── .gitignore ├── .swiftlint.yml ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── AppFeature │ ├── App.swift │ ├── AppView.swift │ ├── Buttons.swift │ ├── DateSelector.swift │ └── RandomDate.swift ├── ConfigurationFeature │ ├── Configuration.swift │ └── ConfigurationView.swift ├── SpeechRecognizerCore │ ├── SpeechRecognizer.swift │ └── SpeechRecognizerButton.swift ├── TTSCore │ ├── SpeakButton.swift │ └── TTS.swift └── WidgetFeature │ └── Widget.swift ├── TellTime └── TellTime.xcodeproj │ └── xcshareddata │ └── xcschemes │ └── TellTime.xcscheme ├── Tests ├── ConfigurationFeatureTests │ ├── ConfigurationTests.swift │ ├── ConfigurationViewTests.swift │ └── __Snapshots__ │ │ └── ConfigurationViewTests │ │ ├── testConfigurationViews.1.png │ │ └── testConfigurationViewsInLandscape.1.png ├── SpeechRecognizerCoreTests │ └── SpeechRecognizerTests.swift └── TTSCoreTests │ ├── SpeakButtonTests.swift │ ├── TTSTests.swift │ └── __Snapshots__ │ └── SpeakButtonTests │ └── testSpeakButtons.1.png ├── docs └── assets │ └── iPhoneScreenshots.png ├── privacy.md └── telltime ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Package.swift ├── TellTime.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── TellTime ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Test Clock 120x120-1.png │ │ ├── Test Clock 120x120.png │ │ ├── Test Clock 152x152.png │ │ ├── Test Clock 167x167.png │ │ ├── Test Clock 180x180.png │ │ ├── Test Clock 20x20.png │ │ ├── Test Clock 29x29.png │ │ ├── Test Clock 40x40-1.png │ │ ├── Test Clock 40x40-2.png │ │ ├── Test Clock 40x40.png │ │ ├── Test Clock 58x58-1.png │ │ ├── Test Clock 58x58.png │ │ ├── Test Clock 60x60.png │ │ ├── Test Clock 76x76.png │ │ ├── Test Clock 80x80-1.png │ │ ├── Test Clock 80x80.png │ │ ├── Test Clock 87x87.png │ │ └── Test Clock-2.png │ ├── Contents.json │ ├── Logo.imageset │ │ ├── Contents.json │ │ ├── TellTimeLogo200x200.png │ │ ├── TellTimeLogo400x400.png │ │ └── TellTimeLogo600x600.png │ └── clock-svg.imageset │ │ ├── Contents.json │ │ └── method-draw-image.svg ├── Info.plist ├── LaunchScreen.storyboard ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── TellTimeUKApp.swift ├── TellTimeTests ├── Clock │ ├── Elements │ │ └── __Snapshots__ │ │ │ ├── ArmTests │ │ │ ├── testArmWithAnAngle.1.png │ │ │ ├── testArms.1.png │ │ │ ├── testArtNouveauArm.1.png │ │ │ ├── testBiggerArm.1.png │ │ │ ├── testDrawnArms.1.png │ │ │ └── testDrawningArm.1.png │ │ │ ├── BorderTests │ │ │ └── testClassicClockBorders.1.png │ │ │ └── IndicatorsTests │ │ │ └── testIndicators.1.png │ ├── Face │ │ └── __Snapshots__ │ │ │ ├── ClockFaceTests │ │ │ ├── testClockFaceNeutral.1.png │ │ │ └── testClockFaceSmiling.1.png │ │ │ ├── EyeTests │ │ │ └── testEyes.1.png │ │ │ └── MouthTests │ │ │ └── testMouths.1.png │ └── __Snapshots__ │ │ └── ClockTests │ │ ├── testClockViewArtNouveauStyle.1.png │ │ ├── testClockViewDrawingStyle.1.png │ │ ├── testClockViewWithFace.1.png │ │ └── testClockViews.1.png ├── Common │ └── __Snapshots__ │ │ └── SwiftUIExtensionsTests │ │ └── testPathExtension.1.png ├── Configuration │ └── __Snapshots__ │ │ └── ConfigurationViewTests │ │ ├── testConfigurationViews.1.png │ │ └── testConfigurationViewsInLandscape.1.png └── TTS │ └── __Snapshots__ │ └── SpeakButtonTests │ └── testSpeakButtons.1.png └── TellTimeWidget ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json ├── Contents.json └── WidgetBackground.colorset │ └── Contents.json ├── Info.plist ├── TellTimeWidget.intentdefinition └── TellTimeWidget.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Swift Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: macOS-12 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Build 17 | run: xcodebuild -scheme telltime-Package -destination "platform=iOS Simulator,name=iPhone 14 Pro" 18 | 19 | - name: Run test 20 | run: xcodebuild test -scheme telltime-Package -destination "platform=iOS Simulator,name=iPhone 14 Pro" 21 | 22 | - uses: actions/upload-artifact@v3 23 | if: failure() 24 | with: 25 | name: failed-screenshots 26 | path: '~/Library/Developer/CoreSimulator/Devices/*/data/tmp/*Tests' 27 | retention-days: 5 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | .DS_Store 3 | .build 4 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | trailing_comma: 2 | mandatory_comma: true 3 | 4 | disabled_rules: 5 | - identifier_name 6 | - multiple_closures_with_trailing_closure 7 | 8 | opt_in_rules: 9 | - force_unwrapping 10 | - implicitly_unwrapped_optional 11 | - implicit_return 12 | 13 | analyzer_rules: 14 | - explicit_self 15 | - unused_import 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Renaud Jenny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "combine-schedulers", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/combine-schedulers", 7 | "state" : { 8 | "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", 9 | "version" : "0.9.1" 10 | } 11 | }, 12 | { 13 | "identity" : "renaudjennyaboutview", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/renaudjenny/RenaudJennyAboutView", 16 | "state" : { 17 | "revision" : "88b458bdb1cfed265a025f7d257d4fad4d5a5b24", 18 | "version" : "1.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-case-paths", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-case-paths", 25 | "state" : { 26 | "revision" : "870133b7b2387df136ad301ec67b2e864b51dda1", 27 | "version" : "0.14.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-clocks", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/swift-clocks", 34 | "state" : { 35 | "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172", 36 | "version" : "0.2.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-collections", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-collections", 43 | "state" : { 44 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 45 | "version" : "1.0.4" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-composable-architecture", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/pointfreeco/swift-composable-architecture", 52 | "state" : { 53 | "revision" : "3e8eee1efe99d06e99426d421733b858b332186b", 54 | "version" : "0.52.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-custom-dump", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 61 | "state" : { 62 | "revision" : "de8ba65649e7ee317b9daf27dd5eebf34bd4be57", 63 | "version" : "0.9.1" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-dependencies", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/pointfreeco/swift-dependencies", 70 | "state" : { 71 | "revision" : "98650d886ec950b587d671261f06d6b59dec4052", 72 | "version" : "0.4.1" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-identified-collections", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/pointfreeco/swift-identified-collections", 79 | "state" : { 80 | "revision" : "ad3932d28c2e0a009a0167089619526709ef6497", 81 | "version" : "0.7.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-past-ten", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/renaudjenny/swift-past-ten", 88 | "state" : { 89 | "branch" : "main", 90 | "revision" : "d5bd80b859811eaf4bf56c98d90dff3339f3f418" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-snapshot-testing", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 97 | "state" : { 98 | "revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334", 99 | "version" : "1.11.0" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-speech-recognizer", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/renaudjenny/swift-speech-recognizer", 106 | "state" : { 107 | "branch" : "main", 108 | "revision" : "d405095aed8069fece60badb17b5c93449b65cf1" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-to-ten", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/renaudjenny/swift-to-ten", 115 | "state" : { 116 | "branch" : "main", 117 | "revision" : "b7379ed384c052df74a08ef5a988c5281eda0aeb" 118 | } 119 | }, 120 | { 121 | "identity" : "swift-tts", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/renaudjenny/swift-tts", 124 | "state" : { 125 | "revision" : "01ed546e43f75e45eee49ed91f5a449d86e48fa3", 126 | "version" : "2.1.2" 127 | } 128 | }, 129 | { 130 | "identity" : "swiftclockui", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/renaudjenny/SwiftClockUI", 133 | "state" : { 134 | "revision" : "b4977887fcfd42201a709357076e3f111ff23c97", 135 | "version" : "2.0.0" 136 | } 137 | }, 138 | { 139 | "identity" : "swiftregex5", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/johnno1962/SwiftRegex5", 142 | "state" : { 143 | "revision" : "8ecb4a8570757b7f8bce72f82242742e13891f99", 144 | "version" : "5.2.3" 145 | } 146 | }, 147 | { 148 | "identity" : "swiftui-navigation", 149 | "kind" : "remoteSourceControl", 150 | "location" : "https://github.com/pointfreeco/swiftui-navigation", 151 | "state" : { 152 | "revision" : "47dd574b900ba5ba679f56ea00d4d282fc7305a6", 153 | "version" : "0.7.1" 154 | } 155 | }, 156 | { 157 | "identity" : "xctest-dynamic-overlay", 158 | "kind" : "remoteSourceControl", 159 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 160 | "state" : { 161 | "revision" : "ab8c9f45843694dd16be4297e6d44c0634fd9913", 162 | "version" : "0.8.4" 163 | } 164 | } 165 | ], 166 | "version" : 2 167 | } 168 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "telltime", 8 | platforms: [.iOS(.v15), .macOS(.v13)], 9 | products: [ 10 | .library(name: "AppFeature", targets: ["AppFeature"]), 11 | .library(name: "ConfigurationFeature", targets: ["ConfigurationFeature"]), 12 | .library(name: "SpeechRecognizerCore", targets: ["SpeechRecognizerCore"]), 13 | .library(name: "TTSCore", targets: ["TTSCore"]), 14 | .library(name: "WidgetFeature", targets: ["WidgetFeature"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/renaudjenny/RenaudJennyAboutView", from: "1.1.0"), 18 | .package(url: "https://github.com/renaudjenny/SwiftClockUI", from: "2.0.0"), 19 | .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.52.0"), 20 | .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.4.1"), 21 | .package(url: "https://github.com/renaudjenny/swift-past-ten", from: "1.1.0"), 22 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.11.0"), 23 | .package(url: "https://github.com/renaudjenny/swift-speech-recognizer", from: "1.0.0"), 24 | .package(url: "https://github.com/renaudjenny/swift-to-ten", from: "1.2.0"), 25 | .package(url: "https://github.com/renaudjenny/swift-tts", from: "2.1.2"), 26 | ], 27 | targets: [ 28 | .target( 29 | name: "AppFeature", 30 | dependencies: [ 31 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 32 | "ConfigurationFeature", 33 | .product(name: "RenaudJennyAboutView", package: "RenaudJennyAboutView"), 34 | "SpeechRecognizerCore", 35 | .product(name: "SwiftClockUI", package: "SwiftClockUI"), 36 | .product(name: "SwiftPastTenDependency", package: "swift-past-ten"), 37 | .product(name: "SwiftToTenDependency", package: "swift-to-ten"), 38 | "TTSCore", 39 | ] 40 | ), 41 | .target( 42 | name: "ConfigurationFeature", 43 | dependencies: [ 44 | .product(name: "SwiftClockUI", package: "SwiftClockUI"), 45 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 46 | ] 47 | ), 48 | .testTarget( 49 | name: "ConfigurationFeatureTests", 50 | dependencies: [ 51 | "ConfigurationFeature", 52 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), 53 | ] 54 | ), 55 | .target( 56 | name: "SpeechRecognizerCore", 57 | dependencies: [ 58 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 59 | .product(name: "SwiftSpeechRecognizerDependency", package: "swift-speech-recognizer"), 60 | ] 61 | ), 62 | .testTarget(name: "SpeechRecognizerCoreTests", dependencies: ["SpeechRecognizerCore"]), 63 | .target( 64 | name: "TTSCore", 65 | dependencies: [ 66 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), 67 | .product(name: "SwiftTTSDependency", package: "swift-tts"), 68 | ] 69 | ), 70 | .testTarget( 71 | name: "TTSCoreTests", 72 | dependencies: [ 73 | "TTSCore", 74 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), 75 | ] 76 | ), 77 | .target( 78 | name: "WidgetFeature", 79 | dependencies: [ 80 | .product(name: "Dependencies", package: "swift-dependencies"), 81 | .product(name: "SwiftClockUI", package: "SwiftClockUI"), 82 | .product(name: "SwiftPastTenDependency", package: "swift-past-ten"), 83 | ] 84 | ) 85 | ] 86 | ) 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tell Time 🇬🇧⏰ 2 | 3 | [![Swift Test](https://github.com/renaudjenny/TellTime/actions/workflows/test.yml/badge.svg)](https://github.com/renaudjenny/TellTime/actions/workflows/test.yml) 4 | 5 | >As a French guy in London, when people told me the time, I was always lost. Now thanks to this app, I can confirm what I hear and what I should say to tell the time 😄. 6 | 7 | A tiny iOS Swift project with SwiftUI. 8 | 9 | 📲 App Store: https://apps.apple.com/gb/app/tell-time-uk/id1496541173 10 | 11 | ## Screenshots 12 | 13 | ![Screenshots of the application from an iPhone](docs/assets/iPhoneScreenshots.png) 14 | 15 | ## Features 16 | 17 | * 🐰 Time is written in British english, like **It's twenty past seven AM** for 07:20 18 | * ⏰ Nice clock gives you the selected time 19 | * 👆 You can move the clock arms to set the time 20 | * 🕰 Customise the design of the clock (Classic, Art Nouveau or Drawing Style) 21 | * ⏱ Display minute/hour indicators or limited hour as your convenience 22 | * 🗣 Time can be heard with a British accent 23 | * 🐢 You can slow down the spoken utterance in configuration (Speech rate) 24 | * 👂 You can activate the Speech recognition to train your pronunciation. 25 | * 👾 Today Widget gives you the current time 26 | 27 | ## Icons and illustrations 28 | 29 | All artistic work has been made by [Mathilde Seyller](https://instagram.com/myobriel). Go follow her! 30 | 31 | ## Minimum required to build the project 32 | 33 | Works with **Xcode 13.3**. 34 | 35 | ## Libraries used 36 | 37 | * ⏰ [SwiftClockUI](https://github.com/renaudjenny/SwiftClockUI): SwiftUI library that provide the Clock, with draggable arms and different design and options 38 | * 🏠 [The Composable Architecture (TCA)](https://github.com/pointfreeco/swift-composable-architecture): library for helping building apps in a consistent and understandable way. Using two main principles: Single point of truth and unidirectional flow. It's also way more easier to test than any architecture I worked with. 39 | * 🇬🇧 [SwiftPastTen](https://github.com/renaudjenny/SwiftPastTen): Swift framework to provide you the British way to tell the time by passing a "HH:mm" formatted string 40 | * 🇬🇧 [SwiftToTen](https://github.com/renaudjenny/SwiftToTen): Provide useful Swift function to recognize British english time and try converting it to Date 41 | * 🗣 [SwiftTTSCombine](https://github.com/renaudjenny/SwiftTTSCombine): Swift Combine framework to use Text To Speech directly wrapped in Combine way 42 | * 👂 [SwiftSpeechCombine](https://github.com/renaudjenny/SwiftSpeechCombine): Swift Combine framework to use Speech recognition directly wrapped in Combine way 43 | * 📸 [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing): Snapshort testing library from **Point-Free** to test views 44 | -------------------------------------------------------------------------------- /Sources/AppFeature/App.swift: -------------------------------------------------------------------------------- 1 | import ConfigurationFeature 2 | import SwiftUI 3 | import Combine 4 | import Speech 5 | import AVFoundation 6 | import ComposableArchitecture 7 | import SpeechRecognizerCore 8 | import SwiftPastTenDependency 9 | import SwiftToTenDependency 10 | import TTSCore 11 | 12 | public struct App: ReducerProtocol { 13 | public struct State: Equatable { 14 | var date: Date = Date() 15 | var configuration = Configuration.State() 16 | var tts = TTS.State() 17 | var speechRecognizer = SpeechRecognizer.State() 18 | var isAboutPresented = false 19 | var tellTime: String? 20 | } 21 | 22 | public enum Action: Equatable { 23 | case setDate(Date) 24 | case setRandomDate 25 | case configuration(Configuration.Action) 26 | case tts(TTS.Action) 27 | case speechRecognizer(SpeechRecognizer.Action) 28 | case appStarted 29 | case presentAbout 30 | case hideAbout 31 | } 32 | 33 | @Dependency(\.date) var date 34 | @Dependency(\.calendar) var calendar 35 | @Dependency(\.randomDate) var randomDate 36 | @Dependency(\.tellTime) var tellTime 37 | @Dependency(\.recognizeTime) var recognizeTime 38 | @Dependency(\.speechRecognizer) var speechRecognizer 39 | @Dependency(\.mainQueue) var mainQueue 40 | 41 | public var body: some ReducerProtocol { 42 | Scope(state: \.configuration, action: /App.Action.configuration) { 43 | Configuration() 44 | } 45 | Scope(state: \.tts, action: /App.Action.tts) { 46 | TTS() 47 | } 48 | Scope(state: \.speechRecognizer, action: /App.Action.speechRecognizer) { 49 | SpeechRecognizer() 50 | } 51 | Reduce { state, action in 52 | struct RecognizedTimeDebounceId: Hashable { } 53 | 54 | switch action { 55 | case let .setDate(date): 56 | return setDate(date, state: &state) 57 | case .setRandomDate: 58 | let randomDate = randomDate() 59 | return setDate(randomDate, state: &state) 60 | case .appStarted: 61 | if state.tellTime == nil { 62 | return setDate(state.date, state: &state) 63 | } 64 | return .none 65 | case .presentAbout: 66 | state.isAboutPresented = true 67 | return .none 68 | case .hideAbout: 69 | state.isAboutPresented = false 70 | return .none 71 | case let .speechRecognizer(.setUtterance(utterance)): 72 | guard 73 | let utterance = utterance, 74 | let date = recognizeTime(time: utterance, calendar: calendar) 75 | else { return .none } 76 | speechRecognizer.stopRecording() 77 | return setDate(date, state: &state) 78 | case .configuration(.binding(\.$speechRateRatio)): 79 | @Dependency(\.tts) var tts 80 | tts.setRateRatio(state.configuration.speechRateRatio) 81 | return .none 82 | case .configuration: return .none 83 | case .tts: return .none 84 | case .speechRecognizer: return .none 85 | } 86 | } 87 | } 88 | 89 | private func setDate(_ date: Date, state: inout App.State) -> EffectTask { 90 | state.date = date 91 | let time = SwiftPastTen.formattedDate(date, calendar: calendar) 92 | state.tellTime = (try? tellTime(time: time)) ?? "" 93 | state.tts.text = state.tellTime ?? "" 94 | return .none 95 | } 96 | } 97 | 98 | public extension Store where State == App.State, Action == App.Action { 99 | static var live: StoreOf { Store(initialState: App.State(), reducer: App()) } 100 | } 101 | 102 | #if DEBUG 103 | extension Store where State == App.State, Action == App.Action { 104 | static func preview(modifyState: (inout App.State) -> Void = { _ in }) -> Self { 105 | let state: App.State = .preview(modifyState: modifyState) 106 | return Self(initialState: state, reducer: App()) 107 | } 108 | 109 | static var preview: Self { preview() } 110 | } 111 | 112 | extension App.State { 113 | static func preview( 114 | modifyState: (inout App.State) -> Void = { _ in } 115 | ) -> Self { 116 | var state = App.State() 117 | modifyState(&state) 118 | return state 119 | } 120 | 121 | static var preview: Self { .preview() } 122 | } 123 | #endif 124 | -------------------------------------------------------------------------------- /Sources/AppFeature/AppView.swift: -------------------------------------------------------------------------------- 1 | import ConfigurationFeature 2 | import SwiftUI 3 | import ComposableArchitecture 4 | import Combine 5 | import SwiftClockUI 6 | import RenaudJennyAboutView 7 | 8 | public struct AppView: View { 9 | public struct ViewState: Equatable { 10 | var date: Date 11 | var time: String? 12 | var recognizedUtterance: String? 13 | var clockStyle: ClockStyle 14 | var clockConfiguration: ClockConfiguration 15 | var isConfigurationPresented: Bool 16 | var isAboutPresented: Bool 17 | 18 | init(_ state: App.State) { 19 | date = state.date 20 | time = state.tellTime 21 | recognizedUtterance = state.speechRecognizer.utterance 22 | clockStyle = state.configuration.clockStyle 23 | clockConfiguration = state.configuration.clock 24 | isConfigurationPresented = state.configuration.isPresented 25 | isAboutPresented = state.isAboutPresented 26 | } 27 | } 28 | 29 | @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? 30 | @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? 31 | 32 | let store: StoreOf 33 | 34 | public init(store: StoreOf) { 35 | self.store = store 36 | } 37 | 38 | public var body: some View { 39 | WithViewStore(store.stateless) { viewStore in 40 | NavigationView { 41 | content 42 | .navigationBarTitle("Tell Time") 43 | .padding() 44 | } 45 | .navigationViewStyle(StackNavigationViewStyle()) 46 | .onAppear { viewStore.send(.appStarted) } 47 | .onOpenURL(perform: { openURL($0, viewStore: viewStore) }) 48 | } 49 | } 50 | 51 | private var content: some View { 52 | VStack { 53 | if verticalSizeClass == .regular && horizontalSizeClass == .compact { 54 | Spacer() 55 | clockView 56 | Spacer() 57 | timeText 58 | Spacer() 59 | DateSelector(store: store) 60 | Spacer() 61 | Buttons(store: store) 62 | } else if verticalSizeClass == .compact { 63 | HStack { 64 | clockView.padding() 65 | VStack { 66 | timeText 67 | Spacer() 68 | DateSelector(store: store) 69 | Spacer() 70 | Buttons(store: store) 71 | } 72 | } 73 | } else { 74 | VStack { 75 | clockView 76 | VStack { 77 | timeText 78 | DateSelector(store: store) 79 | } 80 | HStack { 81 | Spacer() 82 | Buttons(store: store) 83 | Spacer() 84 | } 85 | } 86 | } 87 | navigationLinks 88 | } 89 | } 90 | 91 | private var clockView: some View { 92 | WithViewStore(store, observe: ViewState.init) { viewStore in 93 | ClockView() 94 | .environment(\.clockDate, viewStore.binding(get: \.date, send: App.Action.setDate)) 95 | .environment(\.clockStyle, viewStore.clockStyle) 96 | .environment(\.clockConfiguration, viewStore.clockConfiguration) 97 | } 98 | } 99 | 100 | private var timeText: some View { 101 | WithViewStore(store, observe: { $0.tellTime }) { viewStore in 102 | viewStore.state.map { time in 103 | Text(time) 104 | .font(.title2) 105 | .foregroundColor(.red) 106 | .padding() 107 | } 108 | } 109 | } 110 | 111 | private var navigationLinks: some View { 112 | WithViewStore(store, observe: ViewState.init) { viewStore in 113 | VStack { 114 | NavigationLink( 115 | destination: ConfigurationView(store: store.scope( 116 | state: \.configuration, 117 | action: { App.Action.configuration($0) } 118 | )), 119 | isActive: viewStore.binding(get: \.isConfigurationPresented, send: App.Action.configuration(.hide)), 120 | label: EmptyView.init 121 | ) 122 | NavigationLink( 123 | destination: AboutView(appId: "id1496541173") { 124 | Image(uiImage: #imageLiteral(resourceName: "Logo")).shadow(radius: 5) 125 | }, 126 | isActive: viewStore.binding(get: \.isAboutPresented, send: App.Action.hideAbout), 127 | label: EmptyView.init 128 | ) 129 | } 130 | } 131 | } 132 | 133 | // TODO: logic to move in the reducer 134 | private func openURL(_ url: URL, viewStore: ViewStore) { 135 | @Dependency(\.date) var date 136 | viewStore.send(.setDate(date.now)) 137 | guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) 138 | else { return } 139 | 140 | guard 141 | let clockStyleValue = urlComponents 142 | .queryItems? 143 | .first(where: { $0.name == "clockStyle" })? 144 | .value, 145 | let clockStyle = ClockStyle.allCases.first(where: { String($0.id) == clockStyleValue }) 146 | else { return } 147 | viewStore.send(.configuration(.set(\.$clockStyle, clockStyle))) 148 | 149 | if urlComponents.queryItems?.first(where: { $0.name == "speak" })?.value == "true" { 150 | viewStore.send(.tts(.speak)) 151 | } 152 | } 153 | } 154 | 155 | #if DEBUG 156 | struct AppView_Previews: PreviewProvider { 157 | static var previews: some View { 158 | Group { 159 | AppView(store: .preview) 160 | AppView(store: .preview) 161 | .previewLayout(.fixed(width: 800, height: 400)) 162 | .preferredColorScheme(.dark) 163 | .environment(\.horizontalSizeClass, .compact) 164 | .environment(\.verticalSizeClass, .compact) 165 | AppView(store: .preview) 166 | .previewLayout(.fixed(width: 1600, height: 1000)) 167 | .environment(\.locale, Locale(identifier: "fr_FR")) 168 | } 169 | } 170 | } 171 | #endif 172 | -------------------------------------------------------------------------------- /Sources/AppFeature/Buttons.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ComposableArchitecture 3 | import TTSCore 4 | 5 | struct Buttons: View { 6 | let store: StoreOf 7 | 8 | var body: some View { 9 | WithViewStore(store.stateless) { viewStore in 10 | HStack { 11 | SpeakButton(store: store.scope(state: \.tts, action: App.Action.tts)) 12 | Spacer() 13 | Button { viewStore.send(.setRandomDate) } label: { 14 | Image(systemName: "shuffle") 15 | .padding() 16 | .accentColor(.white) 17 | .background(Color.red) 18 | .cornerRadius(8) 19 | } 20 | Spacer() 21 | Button { viewStore.send(.configuration(.present)) } label: { 22 | Image(systemName: "gear") 23 | .padding() 24 | .accentColor(.red) 25 | } 26 | Spacer() 27 | Button { viewStore.send(.presentAbout) } label: { 28 | Image(systemName: "questionmark.circle") 29 | .padding() 30 | .accentColor(.red) 31 | } 32 | } 33 | .frame(maxWidth: 800) 34 | .padding([.horizontal, .top]) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AppFeature/DateSelector.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SpeechRecognizerCore 3 | import SwiftUI 4 | 5 | struct DateSelector: View { 6 | struct ViewState: Equatable { 7 | var date: Date 8 | var recognizedUtterance: String? 9 | 10 | init(_ state: App.State) { 11 | date = state.date 12 | recognizedUtterance = state.speechRecognizer.utterance 13 | } 14 | } 15 | 16 | let store: StoreOf 17 | 18 | var body: some View { 19 | WithViewStore(store, observe: ViewState.init) { viewStore in 20 | VStack { 21 | ZStack { 22 | HStack { 23 | SpeechRecognizerButton(store: store.scope( 24 | state: \.speechRecognizer, 25 | action: App.Action.speechRecognizer 26 | )) 27 | Spacer() 28 | } 29 | 30 | DatePicker( 31 | selection: viewStore.binding(get: \.date, send: App.Action.setDate), 32 | displayedComponents: [.hourAndMinute] 33 | ) { 34 | Text("Select a time") 35 | } 36 | .labelsHidden() 37 | .padding() 38 | 39 | Spacer() 40 | } 41 | viewStore.recognizedUtterance.map { recognizedUtterance in 42 | Text(recognizedUtterance) 43 | } 44 | } 45 | .frame(maxWidth: 800) 46 | .padding(.horizontal) 47 | } 48 | } 49 | } 50 | 51 | #if DEBUG 52 | struct DateSelector_Previews: PreviewProvider { 53 | static var previews: some View { 54 | Group { 55 | DateSelector(store: .preview) 56 | VStack { 57 | DateSelector(store: .preview) 58 | Buttons(store: .preview) 59 | } 60 | } 61 | } 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/AppFeature/RandomDate.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | import Foundation 3 | import XCTestDynamicOverlay 4 | 5 | public struct RandomDate { 6 | var randomDate: () -> Date 7 | 8 | public init(randomDate: @escaping () -> Date) { 9 | self.randomDate = randomDate 10 | } 11 | 12 | public func callAsFunction() -> Date { 13 | randomDate() 14 | } 15 | } 16 | 17 | private extension RandomDate { 18 | static let live = Self { 19 | @Dependency(\.calendar) var calendar 20 | let hour = [Int](1...12).randomElement() ?? 0 21 | let minute = [Int](0...59).randomElement() ?? 0 22 | return calendar.date(bySettingHour: hour, minute: minute, second: 0, of: .epoch) ?? .epoch 23 | } 24 | static let preview = RandomDate.live 25 | static let test = Self(randomDate: unimplemented("RandomDate.randomDate")) 26 | } 27 | 28 | private extension Date { 29 | static var epoch: Date { .init(timeIntervalSince1970: 0) } 30 | } 31 | 32 | private enum RandomDateKey: DependencyKey { 33 | static let liveValue = RandomDate.live 34 | static let previewValue = RandomDate.preview 35 | static let testValue = RandomDate.test 36 | } 37 | 38 | extension DependencyValues { 39 | var randomDate: RandomDate { 40 | get { self[RandomDateKey.self] } 41 | set { self[RandomDateKey.self] = newValue } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ConfigurationFeature/Configuration.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftClockUI 3 | 4 | public struct Configuration: ReducerProtocol { 5 | public struct State: Equatable { 6 | @BindingState public var clock = ClockConfiguration() 7 | @BindingState public var clockStyle: ClockStyle = .classic 8 | @BindingState public var speechRateRatio: Float = 1 9 | public var isPresented = false 10 | 11 | public init( 12 | clock: ClockConfiguration = ClockConfiguration(), 13 | clockStyle: ClockStyle = .classic, 14 | speechRateRatio: Float = 1, 15 | isPresented: Bool = false 16 | ) { 17 | self.clock = clock 18 | self.clockStyle = clockStyle 19 | self.speechRateRatio = speechRateRatio 20 | self.isPresented = isPresented 21 | } 22 | } 23 | 24 | public enum Action: BindableAction, Equatable { 25 | case binding(BindingAction) 26 | case present 27 | case hide 28 | } 29 | 30 | public init() {} 31 | 32 | public var body: some ReducerProtocol { 33 | BindingReducer() 34 | 35 | Reduce { state, action in 36 | switch action { 37 | case .binding: 38 | return .none 39 | case .present: 40 | state.isPresented = true 41 | return .none 42 | case .hide: 43 | state.isPresented = false 44 | return .none 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ConfigurationFeature/ConfigurationView.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftClockUI 3 | import SwiftUI 4 | 5 | public struct ConfigurationView: View { 6 | let store: StoreOf 7 | 8 | @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? 9 | @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? 10 | 11 | public init(store: StoreOf) { 12 | self.store = store 13 | } 14 | 15 | public var body: some View { 16 | WithViewStore(store, observe: { $0 }) { viewStore in 17 | Group { 18 | if verticalSizeClass == .regular && horizontalSizeClass == .regular { 19 | regularStackedView() 20 | } else if verticalSizeClass == .compact || horizontalSizeClass == .regular { 21 | horizontalStackedView() 22 | } else { 23 | verticalStackedView() 24 | } 25 | } 26 | .padding() 27 | .navigationBarTitle("Configuration") 28 | } 29 | } 30 | 31 | private func verticalStackedView() -> some View { 32 | WithViewStore(store, observe: { $0 }) { viewStore in 33 | VStack { 34 | stylePicker().pickerStyle(SegmentedPickerStyle()) 35 | clockView() 36 | controls() 37 | Spacer() 38 | } 39 | } 40 | } 41 | 42 | private func horizontalStackedView() -> some View { 43 | WithViewStore(store, observe: { $0 }) { viewStore in 44 | HStack { 45 | VStack { 46 | clockView() 47 | stylePicker().pickerStyle(SegmentedPickerStyle()) 48 | } 49 | controls() 50 | } 51 | } 52 | } 53 | 54 | private func regularStackedView() -> some View { 55 | WithViewStore(store, observe: { $0 }) { viewStore in 56 | VStack { 57 | HStack { 58 | clockView() 59 | VStack { 60 | allClockStylePicker() 61 | } 62 | .frame(maxWidth: 400) 63 | } 64 | controls().frame(maxWidth: 800) 65 | Spacer() 66 | } 67 | } 68 | } 69 | 70 | private func clockView() -> some View { 71 | WithViewStore(store, observe: { $0 }) { viewStore in 72 | ClockView() 73 | .padding() 74 | .allowsHitTesting(false) 75 | .environment(\.clockStyle, viewStore.clockStyle) 76 | .environment(\.clockConfiguration, viewStore.clock) 77 | } 78 | } 79 | 80 | private func controls() -> some View { 81 | WithViewStore(store, observe: { $0 }) { viewStore in 82 | VStack { 83 | HStack { 84 | Text("Speech rate: \(viewStore.speechRateRatioPercentage)%") 85 | Slider(value: viewStore.binding(\.$speechRateRatio), in: 0.5...1.0) 86 | .accentColor(.red) 87 | } 88 | Toggle(isOn: viewStore.binding(\.$clock.isMinuteIndicatorsShown)) { 89 | Text("Minute indicators") 90 | } 91 | .disabled(!viewStore.isMinuteIndicatorsToggleEnabled) 92 | Toggle(isOn: viewStore.binding(\.$clock.isHourIndicatorsShown)) { 93 | Text("Hour indicators") 94 | } 95 | .disabled(!viewStore.isHourIndicatorsToggleEnabled) 96 | Toggle(isOn: viewStore.binding(\.$clock.isLimitedHoursShown)) { 97 | Text("Limited hour texts") 98 | } 99 | } 100 | } 101 | } 102 | 103 | private func stylePicker() -> some View { 104 | WithViewStore(store, observe: { $0 }) { viewStore in 105 | Picker("Style", selection: viewStore.binding(\.$clockStyle)) { 106 | ForEach(ClockStyle.allCases) { style in 107 | Text(style.description).tag(style) 108 | } 109 | } 110 | } 111 | } 112 | 113 | private func allClockStylePicker() -> some View { 114 | WithViewStore(store, observe: { $0 }) { viewStore in 115 | ForEach(ClockStyle.allCases) { style in 116 | Button(action: { viewStore.send(.set(\.$clockStyle, style)) }, label: { 117 | ClockView() 118 | .allowsHitTesting(false) 119 | .environment(\.clockStyle, style) 120 | .environment(\.clockConfiguration, viewStore.clock) 121 | .padding() 122 | }) 123 | } 124 | } 125 | } 126 | 127 | private func isMinuteIndicatorsToggleEnabled(state: Configuration.State) -> Bool { 128 | state.clockStyle != .steampunk 129 | } 130 | } 131 | 132 | private extension Configuration.State { 133 | var isMinuteIndicatorsToggleEnabled: Bool { clockStyle != .steampunk } 134 | var isHourIndicatorsToggleEnabled: Bool { clockStyle != .artNouveau && clockStyle != .steampunk } 135 | var speechRateRatioPercentage: Int { Int((speechRateRatio * 100).rounded()) } 136 | } 137 | 138 | #if DEBUG 139 | public struct ConfigurationView_Previews: PreviewProvider { 140 | public static var previews: some View { 141 | Preview() 142 | } 143 | 144 | private struct Preview: View { 145 | @Environment(\.calendar) var calendar 146 | 147 | var body: some View { 148 | NavigationView { 149 | ConfigurationView(store: .preview) 150 | } 151 | .environment(\.clockDate, .constant(.init(hour: 10, minute: 10, calendar: calendar))) 152 | } 153 | } 154 | } 155 | 156 | extension Store where State == Configuration.State, Action == Configuration.Action { 157 | static var preview: StoreOf { 158 | Store(initialState: Configuration.State(), reducer: Configuration()) 159 | } 160 | } 161 | 162 | struct ConfigurationViewLandscape_Previews: PreviewProvider { 163 | static var previews: some View { 164 | Preview() 165 | } 166 | 167 | private struct Preview: View { 168 | @Environment(\.calendar) var calendar 169 | 170 | var body: some View { 171 | NavigationView { 172 | ConfigurationView(store: .preview) 173 | } 174 | .environment(\.verticalSizeClass, .compact) 175 | .environment(\.horizontalSizeClass, .compact) 176 | .environment(\.clockDate, .constant(.init(hour: 10, minute: 10, calendar: calendar))) 177 | .previewLayout(.fixed(width: 568, height: 320)) 178 | } 179 | } 180 | } 181 | 182 | struct ConfigurationViewRegular_Previews: PreviewProvider { 183 | static var previews: some View { 184 | Preview() 185 | } 186 | 187 | private struct Preview: View { 188 | @Environment(\.calendar) var calendar 189 | 190 | var body: some View { 191 | NavigationView { 192 | ConfigurationView(store: .preview) 193 | } 194 | .navigationViewStyle(StackNavigationViewStyle()) 195 | .environment(\.verticalSizeClass, .regular) 196 | .environment(\.horizontalSizeClass, .regular) 197 | .environment(\.clockDate, .constant(.init(hour: 10, minute: 10, calendar: calendar))) 198 | .previewLayout(.fixed(width: 1000, height: 900)) 199 | } 200 | } 201 | } 202 | #endif 203 | -------------------------------------------------------------------------------- /Sources/SpeechRecognizerCore/SpeechRecognizer.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import Speech 3 | import SwiftSpeechRecognizerDependency 4 | 5 | public struct SpeechRecognizer: ReducerProtocol { 6 | public struct State: Equatable { 7 | public var status: SpeechRecognitionStatus = .notStarted 8 | public var authorizationStatus: SFSpeechRecognizerAuthorizationStatus = .notDetermined 9 | public var utterance: String? 10 | 11 | public init( 12 | status: SpeechRecognitionStatus = .notStarted, 13 | authorizationStatus: SFSpeechRecognizerAuthorizationStatus = .notDetermined, 14 | utterance: String? = nil 15 | ) { 16 | self.status = status 17 | self.authorizationStatus = authorizationStatus 18 | self.utterance = utterance 19 | } 20 | } 21 | 22 | public enum Action: Equatable { 23 | case buttonTapped 24 | case startRecording 25 | case stopRecording 26 | case setStatus(SpeechRecognitionStatus) 27 | case setUtterance(String?) 28 | case requestAuthorization 29 | case setAuthorizationStatus(SFSpeechRecognizerAuthorizationStatus) 30 | case cancel 31 | } 32 | 33 | @Dependency(\.speechRecognizer) var speechRecognizer 34 | 35 | public init() {} 36 | 37 | private enum AuthorizationStatusTaskID {} 38 | private enum RecognitionStatusTaskID {} 39 | private enum NewUtteranceTaskID {} 40 | 41 | public func reduce(into state: inout State, action: Action) -> EffectTask { 42 | switch action { 43 | case .buttonTapped: 44 | switch state.status { 45 | case .recording, .stopping: 46 | return stopRecording(state: &state) 47 | case .notStarted, .stopped: 48 | return startRecording(state: &state) 49 | } 50 | case .startRecording: 51 | return startRecording(state: &state) 52 | case .stopRecording: 53 | return stopRecording(state: &state) 54 | case let .setStatus(status): 55 | state.status = status 56 | switch status { 57 | case .recording, .stopping: 58 | state.utterance = nil 59 | case .notStarted, .stopped: 60 | break 61 | } 62 | return .none 63 | case let .setUtterance(utterance): 64 | state.utterance = utterance 65 | return .none 66 | case let .setAuthorizationStatus(authorizationStatus): 67 | state.authorizationStatus = authorizationStatus 68 | switch authorizationStatus { 69 | case .authorized: 70 | return startRecording(state: &state) 71 | case .notDetermined: 72 | return requestAuthorization(state: &state) 73 | default: break 74 | } 75 | return .none 76 | case .requestAuthorization: 77 | return requestAuthorization(state: &state) 78 | case .cancel: 79 | return .cancel(ids: [AuthorizationStatusTaskID.self, RecognitionStatusTaskID.self, NewUtteranceTaskID.self]) 80 | } 81 | } 82 | 83 | private func startRecording(state: inout State) -> EffectTask { 84 | guard state.authorizationStatus == .authorized 85 | else { return requestAuthorization(state: &state) } 86 | do { 87 | try speechRecognizer.startRecording() 88 | return .merge( 89 | .run(operation: { send in 90 | for await recognitionStatus in speechRecognizer.recognitionStatus() { 91 | await send(.setStatus(recognitionStatus)) 92 | } 93 | }) 94 | .cancellable(id: RecognitionStatusTaskID.self), 95 | .run(operation: { send in 96 | for await newUtterance in speechRecognizer.newUtterance() { 97 | await send(.setUtterance(newUtterance)) 98 | } 99 | }) 100 | .cancellable(id: NewUtteranceTaskID.self) 101 | ) 102 | } catch { 103 | return stopRecording(state: &state) 104 | } 105 | } 106 | 107 | private func stopRecording(state: inout State) -> EffectTask { 108 | speechRecognizer.stopRecording() 109 | return .none 110 | } 111 | 112 | private func requestAuthorization(state: inout State) -> EffectTask { 113 | speechRecognizer.requestAuthorization() 114 | return .run { send in 115 | for await authorizationStatus in speechRecognizer.authorizationStatus() { 116 | await send(.setAuthorizationStatus(authorizationStatus)) 117 | } 118 | } 119 | .cancellable(id: AuthorizationStatusTaskID.self) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/SpeechRecognizerCore/SpeechRecognizerButton.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftSpeechRecognizerDependency 3 | import SwiftUI 4 | 5 | public struct SpeechRecognizerButton: View { 6 | struct ViewState: Equatable { 7 | var isRecording: Bool 8 | var label: Text 9 | var image: Image 10 | 11 | init(_ state: SpeechRecognizer.State) { 12 | switch state.status { 13 | case .notStarted, .stopped: 14 | self.label = Text("Speech Recognition ready") 15 | self.isRecording = false 16 | self.image = Image(systemName: "waveform.circle") 17 | case .recording: 18 | self.label = Text("Speech Recognition recording...") 19 | self.isRecording = true 20 | self.image = Image(systemName: "record.circle") 21 | case .stopping: 22 | self.label = Text("Speech Recognition stopping...") 23 | self.isRecording = false 24 | self.image = Image(systemName: "stop.circle") 25 | } 26 | self.isRecording = state.status == .recording 27 | 28 | } 29 | } 30 | 31 | let store: StoreOf 32 | 33 | public init(store: StoreOf) { 34 | self.store = store 35 | } 36 | 37 | public var body: some View { 38 | WithViewStore(store, observe: ViewState.init) { viewStore in 39 | Button { viewStore.send(.buttonTapped) } label: { 40 | viewStore.image 41 | .resizable() 42 | .accentColor(.white) 43 | .padding(4) 44 | .background(Color.red) 45 | .cornerRadius(8) 46 | .opacity(viewStore.isRecording ? 0.8 : 1) 47 | .frame(width: 50, height: 50) 48 | } 49 | .accessibilityLabel(viewStore.label) 50 | } 51 | } 52 | } 53 | 54 | #if DEBUG 55 | import Speech 56 | import SwiftSpeechRecognizer 57 | 58 | public struct SpeechRecognizerButton_Previews: PreviewProvider { 59 | public static var previews: some View { 60 | Preview(store: .preview) 61 | } 62 | 63 | private struct Preview: View { 64 | let store: StoreOf 65 | 66 | var body: some View { 67 | WithViewStore(store, observe: { $0 }) { viewStore in 68 | VStack { 69 | Text("Hi") 70 | SpeechRecognizerButton(store: store) 71 | Text(viewStore.utterance ?? "").multilineTextAlignment(.center) 72 | Text("Speech status: \(viewStore.status.previewDescription)") 73 | Text("Authorization status: \(viewStore.authorizationStatus.previewDescription)") 74 | } 75 | .padding() 76 | } 77 | } 78 | } 79 | } 80 | 81 | public extension Store where State == SpeechRecognizer.State, Action == SpeechRecognizer.Action { 82 | static var preview: Store { 83 | Store(initialState: SpeechRecognizer.State(), reducer: SpeechRecognizer()) 84 | } 85 | } 86 | 87 | extension SpeechRecognitionStatus { 88 | var previewDescription: String { 89 | switch self { 90 | case .notStarted: return "not started" 91 | case .recording: return "recording" 92 | case .stopping: return "stopping" 93 | case .stopped: return "stopped" 94 | } 95 | } 96 | } 97 | 98 | extension SFSpeechRecognizerAuthorizationStatus { 99 | var previewDescription: String { 100 | switch self { 101 | case .notDetermined: return "not determined" 102 | case .denied: return "denied" 103 | case .restricted: return "restricted" 104 | case .authorized: return "authorized" 105 | @unknown default: return "unknown!" 106 | } 107 | } 108 | } 109 | #endif 110 | -------------------------------------------------------------------------------- /Sources/TTSCore/SpeakButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ComposableArchitecture 3 | 4 | public struct SpeakButton: View { 5 | struct ViewState: Equatable { 6 | var isSpeaking: Bool 7 | var widthProgressRatio: CGFloat 8 | 9 | init(_ state: TTS.State) { 10 | self.isSpeaking = state.isSpeaking 11 | self.widthProgressRatio = state.speakingProgress 12 | } 13 | } 14 | 15 | let store: StoreOf 16 | @ObservedObject private var viewStore: ViewStore 17 | 18 | public init(store: StoreOf) { 19 | self.store = store 20 | _viewStore = ObservedObject( 21 | initialValue: ViewStore(store.scope(state: ViewState.init)) 22 | ) 23 | } 24 | 25 | public var body: some View { 26 | Button { viewStore.send(.speak) } label: { 27 | Image(systemName: "speaker.2") 28 | .padding() 29 | .accentColor(.white) 30 | .cornerRadius(8) 31 | } 32 | .disabled(viewStore.isSpeaking) 33 | .background(GeometryReader { geometry in 34 | Rectangle() 35 | .fill(Color.gray) 36 | .cornerRadius(8) 37 | Rectangle() 38 | .size( 39 | width: geometry.size.width * viewStore.widthProgressRatio, 40 | height: geometry.size.height 41 | ) 42 | .fill(Color.red) 43 | .cornerRadius(8) 44 | .animation(.easeInOut, value: viewStore.widthProgressRatio) 45 | }) 46 | } 47 | } 48 | 49 | #if DEBUG 50 | public struct SpeakButton_Previews: PreviewProvider { 51 | public static var previews: some View { 52 | VStack { 53 | SpeakButton(store: .preview) 54 | SpeakButton(store: .preview(modifyState: { 55 | $0.speakingProgress = 1/4 56 | $0.isSpeaking = true 57 | })) 58 | SpeakButton(store: .preview(modifyState: { 59 | $0.speakingProgress = 1/2 60 | $0.isSpeaking = true 61 | })) 62 | SpeakButton(store: .preview(modifyState: { 63 | $0.speakingProgress = 3/4 64 | $0.isSpeaking = true 65 | })) 66 | SpeakButton(store: .preview(modifyState: { 67 | $0.speakingProgress = 9/10 68 | $0.isSpeaking = true 69 | })) 70 | SpeakButton(store: .preview(modifyState: { 71 | $0.speakingProgress = 1 72 | $0.isSpeaking = true 73 | })) 74 | } 75 | } 76 | } 77 | 78 | public extension Store where State == TTS.State, Action == TTS.Action { 79 | static func preview(modifyState: (inout TTS.State) -> Void) -> Store { 80 | var state = TTS.State() 81 | modifyState(&state) 82 | return Store(initialState: state, reducer: TTS()) 83 | } 84 | static var preview: Store { preview(modifyState: { _ in }) } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/TTSCore/TTS.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import Foundation 3 | import SwiftTTSDependency 4 | 5 | public struct TTS: ReducerProtocol { 6 | public struct State: Equatable { 7 | public var text = "" 8 | public var isSpeaking = false 9 | public var speakingProgress = 1.0 10 | public var rateRatio: Float = 1.0 11 | 12 | public init( 13 | text: String = "", 14 | isSpeaking: Bool = false, 15 | speakingProgress: Double = 1.0, 16 | rateRatio: Float = 1.0 17 | ) { 18 | self.text = text 19 | self.isSpeaking = isSpeaking 20 | self.speakingProgress = speakingProgress 21 | self.rateRatio = rateRatio 22 | } 23 | } 24 | 25 | public enum Action: Equatable { 26 | case changeRateRatio(Float) 27 | case speak 28 | case startSpeaking 29 | case stopSpeaking 30 | case changeSpeakingProgress(Double) 31 | } 32 | 33 | @Dependency(\.tts) var tts 34 | 35 | public init() {} 36 | 37 | public var body: some ReducerProtocol { 38 | Reduce { state, action in 39 | switch action { 40 | case let .changeRateRatio(rateRatio): 41 | state.rateRatio = rateRatio 42 | tts.setRateRatio(rateRatio) 43 | return .none 44 | case .speak: 45 | tts.speak(state.text) 46 | return .run { send in 47 | for await isSpeaking in tts.isSpeaking() { 48 | if isSpeaking { 49 | await send(.startSpeaking) 50 | } else { 51 | await send(.stopSpeaking) 52 | } 53 | } 54 | } 55 | case .startSpeaking: 56 | state.isSpeaking = true 57 | return .run { send in 58 | for await progress in tts.speakingProgress() { 59 | await send(.changeSpeakingProgress(progress)) 60 | } 61 | } 62 | case .stopSpeaking: 63 | state.isSpeaking = false 64 | return .none 65 | case let .changeSpeakingProgress(speakingProgress): 66 | state.speakingProgress = speakingProgress 67 | return .none 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/WidgetFeature/Widget.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | import SwiftClockUI 3 | import SwiftUI 4 | import SwiftPastTenDependency 5 | import WidgetKit 6 | 7 | public struct WidgetView: View { 8 | let date: Date 9 | let design: Design 10 | @Environment(\.widgetFamily) var family: WidgetFamily 11 | 12 | public init(date: Date, design: Int) { 13 | self.date = date 14 | self.design = Design(rawValue: design) ?? .classic 15 | } 16 | 17 | public var body: some View { 18 | switch family { 19 | case .systemSmall: smallView 20 | case .systemMedium: mediumView 21 | case .systemLarge: largeView 22 | default: Text("Error") 23 | } 24 | } 25 | 26 | private var smallView: some View { 27 | VStack { 28 | switch design { 29 | case .unknown, .text: Text(time).padding() 30 | default: clock.padding() 31 | } 32 | } 33 | .widgetURL(url()) 34 | } 35 | 36 | private var mediumView: some View { 37 | HStack { 38 | clock 39 | VStack { 40 | Text(time) 41 | Spacer() 42 | Link(destination: url(speak: true)) { 43 | speakButton 44 | } 45 | } 46 | } 47 | .padding() 48 | .widgetURL(url()) 49 | } 50 | 51 | private var largeView: some View { 52 | VStack { 53 | clock 54 | Spacer() 55 | HStack { 56 | Spacer() 57 | Text(time) 58 | Spacer() 59 | Link(destination: url(speak: true)) { 60 | speakButton 61 | } 62 | } 63 | Spacer() 64 | } 65 | .padding() 66 | .widgetURL(url()) 67 | } 68 | 69 | private var clock: some View { 70 | ClockView() 71 | .allowsHitTesting(false) 72 | .environment(\.clockDate, .constant(date)) 73 | .environment(\.clockStyle, design.clockStyle) 74 | } 75 | 76 | private var speakButton: some View { 77 | Image(systemName: "speaker.2") 78 | .foregroundColor(.white) 79 | .padding() 80 | .cornerRadius(8) 81 | .background(Color.red.cornerRadius(8)) 82 | } 83 | 84 | private var time: String { 85 | @Dependency(\.calendar) var calendar 86 | @Dependency(\.tellTime) var tellTime 87 | guard let time = try? tellTime(time: SwiftPastTen.formattedDate(date, calendar: calendar)) 88 | else { return "" } 89 | return time 90 | } 91 | 92 | private func url(speak: Bool = false) -> URL { 93 | var urlComponents = URLComponents() 94 | urlComponents.host = "renaud.jenny.telltime" 95 | urlComponents.queryItems = [ 96 | URLQueryItem(name: "clockStyle", value: "\(design.clockStyle.id)"), 97 | URLQueryItem(name: "speak", value: "\(speak)"), 98 | ] 99 | guard let url = urlComponents.url else { 100 | fatalError("Cannot build the URL from the Widget") 101 | } 102 | return url 103 | } 104 | } 105 | 106 | /// Design needs to be a perfect match with TellTimeWidget Design 107 | enum Design: Int { 108 | case unknown 109 | case classic 110 | case artNouveau 111 | case drawing 112 | case steampunk 113 | case text 114 | } 115 | 116 | extension Design { 117 | var clockStyle: ClockStyle { 118 | switch self { 119 | case .unknown, .classic, .text: return .classic 120 | case .artNouveau: return .artNouveau 121 | case .drawing: return .drawing 122 | case .steampunk: return .steampunk 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /TellTime/TellTime.xcodeproj/xcshareddata/xcschemes/TellTime.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Tests/ConfigurationFeatureTests/ConfigurationTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import ConfigurationFeature 3 | import XCTest 4 | 5 | @MainActor 6 | class ConfigurationTests: XCTestCase { 7 | func testClockStyleChanged() async { 8 | let store = TestStore(initialState: Configuration.State(), reducer: Configuration()) 9 | await store.send(.set(\.$clockStyle, .artNouveau)) { 10 | $0.clockStyle = .artNouveau 11 | } 12 | await store.send(.set(\.$clockStyle, .drawing)) { 13 | $0.clockStyle = .drawing 14 | } 15 | await store.send(.set(\.$clockStyle, .steampunk)) { 16 | $0.clockStyle = .steampunk 17 | } 18 | await store.send(.set(\.$clockStyle, .classic)) { 19 | $0.clockStyle = .classic 20 | } 21 | } 22 | 23 | func testMinuteIndicatorChanged() async { 24 | let store = TestStore(initialState: Configuration.State(), reducer: Configuration()) 25 | await store.send(.set(\.$clock.isMinuteIndicatorsShown, false)) { 26 | $0.clock.isMinuteIndicatorsShown = false 27 | } 28 | await store.send(.set(\.$clock.isMinuteIndicatorsShown, true)) { 29 | $0.clock.isMinuteIndicatorsShown = true 30 | } 31 | } 32 | 33 | func testHourIndicatorChanged() async { 34 | let store = TestStore(initialState: Configuration.State(), reducer: Configuration()) 35 | await store.send(.set(\.$clock.isHourIndicatorsShown, false)) { 36 | $0.clock.isHourIndicatorsShown = false 37 | } 38 | await store.send(.set(\.$clock.isHourIndicatorsShown, true)) { 39 | $0.clock.isHourIndicatorsShown = true 40 | } 41 | } 42 | 43 | func testLimitHourDisplayedChanged() async { 44 | let store = TestStore(initialState: Configuration.State(), reducer: Configuration()) 45 | await store.send(.set(\.$clock.isLimitedHoursShown, true)) { 46 | $0.clock.isLimitedHoursShown = true 47 | } 48 | await store.send(.set(\.$clock.isLimitedHoursShown, false)) { 49 | $0.clock.isLimitedHoursShown = false 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/ConfigurationFeatureTests/ConfigurationViewTests.swift: -------------------------------------------------------------------------------- 1 | import ConfigurationFeature 2 | import SnapshotTesting 3 | import SwiftUI 4 | import XCTest 5 | 6 | class ConfigurationViewTests: XCTestCase { 7 | func testConfigurationViews() { 8 | let configurationViews = ConfigurationView_Previews.previews 9 | assertSnapshot(matching: configurationViews, as: .image(layout: .fixed(width: 320, height: 568))) 10 | } 11 | 12 | func testConfigurationViewsInLandscape() { 13 | let configurationViews = ConfigurationView_Previews.previews 14 | assertSnapshot(matching: configurationViews, as: .image(layout: .fixed(width: 568, height: 320))) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/ConfigurationFeatureTests/__Snapshots__/ConfigurationViewTests/testConfigurationViews.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/Tests/ConfigurationFeatureTests/__Snapshots__/ConfigurationViewTests/testConfigurationViews.1.png -------------------------------------------------------------------------------- /Tests/ConfigurationFeatureTests/__Snapshots__/ConfigurationViewTests/testConfigurationViewsInLandscape.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/Tests/ConfigurationFeatureTests/__Snapshots__/ConfigurationViewTests/testConfigurationViewsInLandscape.1.png -------------------------------------------------------------------------------- /Tests/SpeechRecognizerCoreTests/SpeechRecognizerTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SpeechRecognizerCore 3 | import XCTest 4 | 5 | @MainActor 6 | class SpeechRecognitionTests: XCTestCase { 7 | func testStartRecordingFirstTimeAsksForAuthorization() async { 8 | let requestAuthorizationExpectation = expectation(description: "Request authorization") 9 | let startRecordingExpectation = expectation(description: "Start recording") 10 | 11 | var recordingStarted: () -> Void = {} 12 | let store = TestStore(initialState: SpeechRecognizer.State(), reducer: SpeechRecognizer()) { dependencies in 13 | dependencies.speechRecognizer.requestAuthorization = { requestAuthorizationExpectation.fulfill() } 14 | dependencies.speechRecognizer.authorizationStatus = { AsyncStream { continuation in 15 | continuation.yield(.authorized) 16 | } } 17 | dependencies.speechRecognizer.startRecording = { startRecordingExpectation.fulfill() } 18 | dependencies.speechRecognizer.recognitionStatus = { AsyncStream { continuation in 19 | continuation.yield(.recording) 20 | } } 21 | dependencies.speechRecognizer.newUtterance = { AsyncStream { continuation in 22 | recordingStarted = { 23 | continuation.yield("Test") 24 | } 25 | } } 26 | } 27 | 28 | await store.send(.buttonTapped) 29 | await store.receive(.setAuthorizationStatus(.authorized)) { 30 | $0.authorizationStatus = .authorized 31 | } 32 | await store.receive(.setStatus(.recording)) { 33 | $0.status = .recording 34 | } 35 | recordingStarted() 36 | await store.receive(.setUtterance("Test")) { 37 | $0.utterance = "Test" 38 | } 39 | 40 | await fulfillment(of: [requestAuthorizationExpectation, startRecordingExpectation]) 41 | await store.send(.cancel) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/TTSCoreTests/SpeakButtonTests.swift: -------------------------------------------------------------------------------- 1 | import SnapshotTesting 2 | import SwiftUI 3 | import TTSCore 4 | import XCTest 5 | 6 | @MainActor 7 | class SpeakButtonTests: XCTestCase { 8 | func testSpeakButtons() { 9 | let speakButtons = SpeakButton_Previews.previews 10 | assertSnapshot(matching: speakButtons, as: .image(precision: 1, perceptualPrecision: 99/100)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/TTSCoreTests/TTSTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import TTSCore 3 | import XCTest 4 | 5 | @MainActor 6 | class TTSTests: XCTestCase { 7 | let scheduler = DispatchQueue.test 8 | 9 | func testUpdateRatio() async { 10 | let setRatioExpectation = expectation(description: "TTS Set ratio is called") 11 | let store = TestStore(initialState: TTS.State(), reducer: TTS()) { 12 | $0.tts.setRateRatio = { value in 13 | XCTAssertEqual(value, 0.75) 14 | setRatioExpectation.fulfill() 15 | } 16 | } 17 | 18 | await store.send(.changeRateRatio(0.75)) { 19 | $0.rateRatio = 0.75 20 | } 21 | 22 | wait(for: [setRatioExpectation], timeout: 0.1) 23 | } 24 | 25 | func testTTSSpeak() async { 26 | let utterance = "It's ten o'clock" 27 | let speechExpectation = expectation(description: "TTS Speech is called") 28 | let isSpeakingExpectation = expectation(description: "TTS isSpeking is called") 29 | let speakingProgressExpectation = expectation(description: "TTS speaking progress is called") 30 | var isSpeakingCallback: () -> Void = { } 31 | var speakingProgressCallback: () -> Void = { } 32 | let store = TestStore(initialState: TTS.State(text: utterance), reducer: TTS()) { 33 | $0.tts.speak = { text in 34 | XCTAssertEqual(text, utterance) 35 | speechExpectation.fulfill() 36 | } 37 | $0.tts.isSpeaking = { AsyncStream { continuation in 38 | isSpeakingCallback = { 39 | continuation.yield(true) 40 | continuation.finish() 41 | isSpeakingExpectation.fulfill() 42 | } 43 | } } 44 | $0.tts.speakingProgress = { AsyncStream { continuation in 45 | speakingProgressCallback = { 46 | continuation.yield(0.5) 47 | continuation.finish() 48 | speakingProgressExpectation.fulfill() 49 | } 50 | } } 51 | } 52 | 53 | await store.send(.speak) 54 | 55 | isSpeakingCallback() 56 | await store.receive(.startSpeaking) { 57 | $0.isSpeaking = true 58 | } 59 | 60 | speakingProgressCallback() 61 | await store.receive(.changeSpeakingProgress(0.5)) { 62 | $0.speakingProgress = 0.5 63 | } 64 | 65 | wait( 66 | for: [speechExpectation, isSpeakingExpectation, speakingProgressExpectation], 67 | timeout: 0.1 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/TTSCoreTests/__Snapshots__/SpeakButtonTests/testSpeakButtons.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/Tests/TTSCoreTests/__Snapshots__/SpeakButtonTests/testSpeakButtons.1.png -------------------------------------------------------------------------------- /docs/assets/iPhoneScreenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/docs/assets/iPhoneScreenshots.png -------------------------------------------------------------------------------- /privacy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | This policy applies to all information collected or submitted on Renaud Jenny apps for iPhone, iPad and any other devices and platforms. 4 | 5 | ## Information we collect 6 | 7 | I don't collect any information or data with Tell Time UK. This app can even fully work without any internet connection. 8 | 9 | ## Technical basics 10 | 11 | I don't use Apple Notification service yet. So there is no informations saved anywhere. 12 | 13 | ## iCloud 14 | 15 | I don't store your data in Apple’s iCloud service. 16 | 17 | ## Ads and analytics 18 | 19 | My apps are free from ads. 20 | 21 | My apps doesn't use any analytics to track you. 22 | 23 | No personal data is used. 24 | 25 | ## Information usage 26 | 27 | I don't collect any information usage. 28 | 29 | ## Security 30 | 31 | My app doesn't use any internet connection to work. So there is no security regards about this. 32 | 33 | ## Your Consent 34 | 35 | By using my apps, you consent to my privacy policy. 36 | 37 | ## Contacting Me 38 | 39 | If you have questions regarding this privacy policy, you may email renaud.jenny@gmail.com. 40 | 41 | ## Changes to this policy 42 | 43 | If I decide to change our privacy policy, I will post those changes on this page. Summary of changes so far: 44 | 45 | August 5, 2020: Second update published. 46 | -------------------------------------------------------------------------------- /telltime/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /telltime/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | // Leave blank. This is only here so that Xcode doesn't display it. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "app", 7 | products: [], 8 | targets: [] 9 | ) 10 | -------------------------------------------------------------------------------- /telltime/TellTime.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4C28CDB22513F72000DAE010 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4C28CDB12513F72000DAE010 /* LaunchScreen.storyboard */; }; 11 | 4C3CCC6124B39353008F0699 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C3CCC6024B39353008F0699 /* WidgetKit.framework */; }; 12 | 4C3CCC6324B39353008F0699 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C3CCC6224B39353008F0699 /* SwiftUI.framework */; }; 13 | 4C3CCC6624B39353008F0699 /* TellTimeWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3CCC6524B39353008F0699 /* TellTimeWidget.swift */; }; 14 | 4C3CCC6924B39356008F0699 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C3CCC6824B39356008F0699 /* Assets.xcassets */; }; 15 | 4C3CCC6B24B39356008F0699 /* TellTimeWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 4C3CCC6724B39353008F0699 /* TellTimeWidget.intentdefinition */; }; 16 | 4C3CCC6C24B39356008F0699 /* TellTimeWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 4C3CCC6724B39353008F0699 /* TellTimeWidget.intentdefinition */; }; 17 | 4C3CCC6F24B39356008F0699 /* TellTimeWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4C3CCC5F24B39353008F0699 /* TellTimeWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 18 | 4C4A811929CFC03600463169 /* TTSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4C4A811829CFC03600463169 /* TTSCore */; }; 19 | 4C5FD3DC29F4C7F1008EB5C9 /* AppFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 4C5FD3DB29F4C7F1008EB5C9 /* AppFeature */; }; 20 | 4C5FD3DE29F4CCA2008EB5C9 /* WidgetFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 4C5FD3DD29F4CCA2008EB5C9 /* WidgetFeature */; }; 21 | 4CB1D4BC22A7293300786385 /* TellTimeUKApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB1D4BB22A7293300786385 /* TellTimeUKApp.swift */; }; 22 | 4CB1D4C222A7293500786385 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CB1D4C122A7293500786385 /* Assets.xcassets */; }; 23 | 4CB1D4C522A7293500786385 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4CB1D4C422A7293500786385 /* Preview Assets.xcassets */; }; 24 | 4CC951C329E291440022C537 /* ConfigurationFeature in Frameworks */ = {isa = PBXBuildFile; productRef = 4CC951C229E291440022C537 /* ConfigurationFeature */; }; 25 | 4CEEF27829D92B8C00CD1DC8 /* SpeechRecognizerCore in Frameworks */ = {isa = PBXBuildFile; productRef = 4CEEF27729D92B8C00CD1DC8 /* SpeechRecognizerCore */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 4C3CCC6D24B39356008F0699 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 4CB1D4B022A7293300786385 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 4C3CCC5E24B39353008F0699; 34 | remoteInfo = TellTimeWidgetExtension; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXCopyFilesBuildPhase section */ 39 | 4C3B32F2242BD11800D17D70 /* Embed Frameworks */ = { 40 | isa = PBXCopyFilesBuildPhase; 41 | buildActionMask = 2147483647; 42 | dstPath = ""; 43 | dstSubfolderSpec = 10; 44 | files = ( 45 | ); 46 | name = "Embed Frameworks"; 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | 4CCBFD1823F870CA004EFBD8 /* Embed App Extensions */ = { 50 | isa = PBXCopyFilesBuildPhase; 51 | buildActionMask = 2147483647; 52 | dstPath = ""; 53 | dstSubfolderSpec = 13; 54 | files = ( 55 | 4C3CCC6F24B39356008F0699 /* TellTimeWidgetExtension.appex in Embed App Extensions */, 56 | ); 57 | name = "Embed App Extensions"; 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXCopyFilesBuildPhase section */ 61 | 62 | /* Begin PBXFileReference section */ 63 | 4C28CDB12513F72000DAE010 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 64 | 4C3CCC5F24B39353008F0699 /* TellTimeWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TellTimeWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 4C3CCC6024B39353008F0699 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 66 | 4C3CCC6224B39353008F0699 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 67 | 4C3CCC6524B39353008F0699 /* TellTimeWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TellTimeWidget.swift; sourceTree = ""; }; 68 | 4C3CCC6724B39353008F0699 /* TellTimeWidget.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = TellTimeWidget.intentdefinition; sourceTree = ""; }; 69 | 4C3CCC6824B39356008F0699 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 70 | 4C3CCC6A24B39356008F0699 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | 4CB1756329C729C200972E74 /* telltime */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = telltime; path = ..; sourceTree = ""; }; 72 | 4CB1D4B822A7293300786385 /* Tell Time UK.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Tell Time UK.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 73 | 4CB1D4BB22A7293300786385 /* TellTimeUKApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TellTimeUKApp.swift; sourceTree = ""; }; 74 | 4CB1D4C122A7293500786385 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 75 | 4CB1D4C422A7293500786385 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 76 | 4CB1D4C922A7293500786385 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 77 | 4CCBFD0C23F870CA004EFBD8 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; 78 | /* End PBXFileReference section */ 79 | 80 | /* Begin PBXFrameworksBuildPhase section */ 81 | 4C3CCC5C24B39353008F0699 /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | 4C3CCC6324B39353008F0699 /* SwiftUI.framework in Frameworks */, 86 | 4C5FD3DE29F4CCA2008EB5C9 /* WidgetFeature in Frameworks */, 87 | 4C3CCC6124B39353008F0699 /* WidgetKit.framework in Frameworks */, 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | 4CB1D4B522A7293300786385 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | 4CC951C329E291440022C537 /* ConfigurationFeature in Frameworks */, 96 | 4C4A811929CFC03600463169 /* TTSCore in Frameworks */, 97 | 4CEEF27829D92B8C00CD1DC8 /* SpeechRecognizerCore in Frameworks */, 98 | 4C5FD3DC29F4C7F1008EB5C9 /* AppFeature in Frameworks */, 99 | ); 100 | runOnlyForDeploymentPostprocessing = 0; 101 | }; 102 | /* End PBXFrameworksBuildPhase section */ 103 | 104 | /* Begin PBXGroup section */ 105 | 4C3CCC6424B39353008F0699 /* TellTimeWidget */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 4C3CCC6524B39353008F0699 /* TellTimeWidget.swift */, 109 | 4C3CCC6724B39353008F0699 /* TellTimeWidget.intentdefinition */, 110 | 4C3CCC6824B39356008F0699 /* Assets.xcassets */, 111 | 4C3CCC6A24B39356008F0699 /* Info.plist */, 112 | ); 113 | path = TellTimeWidget; 114 | sourceTree = ""; 115 | }; 116 | 4CB1D4AF22A7293300786385 = { 117 | isa = PBXGroup; 118 | children = ( 119 | 4CCBFD0B23F870CA004EFBD8 /* Frameworks */, 120 | 4CB1D4B922A7293300786385 /* Products */, 121 | 4CB1756329C729C200972E74 /* telltime */, 122 | 4CB1D4BA22A7293300786385 /* TellTime */, 123 | 4C3CCC6424B39353008F0699 /* TellTimeWidget */, 124 | ); 125 | sourceTree = ""; 126 | }; 127 | 4CB1D4B922A7293300786385 /* Products */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 4CB1D4B822A7293300786385 /* Tell Time UK.app */, 131 | 4C3CCC5F24B39353008F0699 /* TellTimeWidgetExtension.appex */, 132 | ); 133 | name = Products; 134 | sourceTree = ""; 135 | }; 136 | 4CB1D4BA22A7293300786385 /* TellTime */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 4CB1D4BB22A7293300786385 /* TellTimeUKApp.swift */, 140 | 4CB1D4C122A7293500786385 /* Assets.xcassets */, 141 | 4CB1D4C922A7293500786385 /* Info.plist */, 142 | 4CB1D4C322A7293500786385 /* Preview Content */, 143 | 4C28CDB12513F72000DAE010 /* LaunchScreen.storyboard */, 144 | ); 145 | path = TellTime; 146 | sourceTree = ""; 147 | }; 148 | 4CB1D4C322A7293500786385 /* Preview Content */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 4CB1D4C422A7293500786385 /* Preview Assets.xcassets */, 152 | ); 153 | path = "Preview Content"; 154 | sourceTree = ""; 155 | }; 156 | 4CCBFD0B23F870CA004EFBD8 /* Frameworks */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 4CCBFD0C23F870CA004EFBD8 /* NotificationCenter.framework */, 160 | 4C3CCC6024B39353008F0699 /* WidgetKit.framework */, 161 | 4C3CCC6224B39353008F0699 /* SwiftUI.framework */, 162 | ); 163 | name = Frameworks; 164 | sourceTree = ""; 165 | }; 166 | /* End PBXGroup section */ 167 | 168 | /* Begin PBXNativeTarget section */ 169 | 4C3CCC5E24B39353008F0699 /* TellTimeWidgetExtension */ = { 170 | isa = PBXNativeTarget; 171 | buildConfigurationList = 4C3CCC7224B39356008F0699 /* Build configuration list for PBXNativeTarget "TellTimeWidgetExtension" */; 172 | buildPhases = ( 173 | 4C3CCC5B24B39353008F0699 /* Sources */, 174 | 4C3CCC5C24B39353008F0699 /* Frameworks */, 175 | 4C3CCC5D24B39353008F0699 /* Resources */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | ); 181 | name = TellTimeWidgetExtension; 182 | packageProductDependencies = ( 183 | 4C5FD3DD29F4CCA2008EB5C9 /* WidgetFeature */, 184 | ); 185 | productName = TellTimeWidgetExtension; 186 | productReference = 4C3CCC5F24B39353008F0699 /* TellTimeWidgetExtension.appex */; 187 | productType = "com.apple.product-type.app-extension"; 188 | }; 189 | 4CB1D4B722A7293300786385 /* TellTime */ = { 190 | isa = PBXNativeTarget; 191 | buildConfigurationList = 4CB1D4E222A7293500786385 /* Build configuration list for PBXNativeTarget "TellTime" */; 192 | buildPhases = ( 193 | 4CB1D4B422A7293300786385 /* Sources */, 194 | 4CB1D4B522A7293300786385 /* Frameworks */, 195 | 4CB1D4B622A7293300786385 /* Resources */, 196 | 4CC35E0B23429E7700D9C8D6 /* ShellScript */, 197 | 4CCBFD1823F870CA004EFBD8 /* Embed App Extensions */, 198 | 4C3B32F2242BD11800D17D70 /* Embed Frameworks */, 199 | ); 200 | buildRules = ( 201 | ); 202 | dependencies = ( 203 | 4C3CCC6E24B39356008F0699 /* PBXTargetDependency */, 204 | ); 205 | name = TellTime; 206 | packageProductDependencies = ( 207 | 4C4A811829CFC03600463169 /* TTSCore */, 208 | 4CEEF27729D92B8C00CD1DC8 /* SpeechRecognizerCore */, 209 | 4CC951C229E291440022C537 /* ConfigurationFeature */, 210 | 4C5FD3DB29F4C7F1008EB5C9 /* AppFeature */, 211 | ); 212 | productName = telltime; 213 | productReference = 4CB1D4B822A7293300786385 /* Tell Time UK.app */; 214 | productType = "com.apple.product-type.application"; 215 | }; 216 | /* End PBXNativeTarget section */ 217 | 218 | /* Begin PBXProject section */ 219 | 4CB1D4B022A7293300786385 /* Project object */ = { 220 | isa = PBXProject; 221 | attributes = { 222 | LastSwiftUpdateCheck = 1200; 223 | LastUpgradeCheck = 1200; 224 | ORGANIZATIONNAME = "Renaud JENNY"; 225 | TargetAttributes = { 226 | 4C3CCC5E24B39353008F0699 = { 227 | CreatedOnToolsVersion = 12.0; 228 | }; 229 | 4CB1D4B722A7293300786385 = { 230 | CreatedOnToolsVersion = 11.0; 231 | }; 232 | }; 233 | }; 234 | buildConfigurationList = 4CB1D4B322A7293300786385 /* Build configuration list for PBXProject "TellTime" */; 235 | compatibilityVersion = "Xcode 9.3"; 236 | developmentRegion = en; 237 | hasScannedForEncodings = 0; 238 | knownRegions = ( 239 | en, 240 | Base, 241 | ); 242 | mainGroup = 4CB1D4AF22A7293300786385; 243 | packageReferences = ( 244 | ); 245 | productRefGroup = 4CB1D4B922A7293300786385 /* Products */; 246 | projectDirPath = ""; 247 | projectRoot = ""; 248 | targets = ( 249 | 4CB1D4B722A7293300786385 /* TellTime */, 250 | 4C3CCC5E24B39353008F0699 /* TellTimeWidgetExtension */, 251 | ); 252 | }; 253 | /* End PBXProject section */ 254 | 255 | /* Begin PBXResourcesBuildPhase section */ 256 | 4C3CCC5D24B39353008F0699 /* Resources */ = { 257 | isa = PBXResourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | 4C3CCC6924B39356008F0699 /* Assets.xcassets in Resources */, 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | 4CB1D4B622A7293300786385 /* Resources */ = { 265 | isa = PBXResourcesBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | 4C28CDB22513F72000DAE010 /* LaunchScreen.storyboard in Resources */, 269 | 4CB1D4C522A7293500786385 /* Preview Assets.xcassets in Resources */, 270 | 4CB1D4C222A7293500786385 /* Assets.xcassets in Resources */, 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | /* End PBXResourcesBuildPhase section */ 275 | 276 | /* Begin PBXShellScriptBuildPhase section */ 277 | 4CC35E0B23429E7700D9C8D6 /* ShellScript */ = { 278 | isa = PBXShellScriptBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | ); 282 | inputFileListPaths = ( 283 | ); 284 | inputPaths = ( 285 | ); 286 | outputFileListPaths = ( 287 | ); 288 | outputPaths = ( 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | shellPath = /bin/sh; 292 | shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 293 | }; 294 | /* End PBXShellScriptBuildPhase section */ 295 | 296 | /* Begin PBXSourcesBuildPhase section */ 297 | 4C3CCC5B24B39353008F0699 /* Sources */ = { 298 | isa = PBXSourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 4C3CCC6B24B39356008F0699 /* TellTimeWidget.intentdefinition in Sources */, 302 | 4C3CCC6624B39353008F0699 /* TellTimeWidget.swift in Sources */, 303 | ); 304 | runOnlyForDeploymentPostprocessing = 0; 305 | }; 306 | 4CB1D4B422A7293300786385 /* Sources */ = { 307 | isa = PBXSourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | 4C3CCC6C24B39356008F0699 /* TellTimeWidget.intentdefinition in Sources */, 311 | 4CB1D4BC22A7293300786385 /* TellTimeUKApp.swift in Sources */, 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | /* End PBXSourcesBuildPhase section */ 316 | 317 | /* Begin PBXTargetDependency section */ 318 | 4C3CCC6E24B39356008F0699 /* PBXTargetDependency */ = { 319 | isa = PBXTargetDependency; 320 | target = 4C3CCC5E24B39353008F0699 /* TellTimeWidgetExtension */; 321 | targetProxy = 4C3CCC6D24B39356008F0699 /* PBXContainerItemProxy */; 322 | }; 323 | /* End PBXTargetDependency section */ 324 | 325 | /* Begin XCBuildConfiguration section */ 326 | 4C3CCC7024B39356008F0699 /* Debug */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 330 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 331 | CODE_SIGN_IDENTITY = "Apple Development"; 332 | CODE_SIGN_STYLE = Automatic; 333 | CURRENT_PROJECT_VERSION = 1; 334 | DEVELOPMENT_TEAM = MBQ98GC8JT; 335 | INFOPLIST_FILE = TellTimeWidget/Info.plist; 336 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 337 | LD_RUNPATH_SEARCH_PATHS = ( 338 | "$(inherited)", 339 | "@executable_path/Frameworks", 340 | "@executable_path/../../Frameworks", 341 | ); 342 | MARKETING_VERSION = 3.1.0; 343 | PRODUCT_BUNDLE_IDENTIFIER = renaud.jenny.telltime.TellTimeWidget; 344 | PRODUCT_NAME = "$(TARGET_NAME)"; 345 | SKIP_INSTALL = YES; 346 | SWIFT_VERSION = 5.0; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | }; 349 | name = Debug; 350 | }; 351 | 4C3CCC7124B39356008F0699 /* Release */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 355 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 356 | CODE_SIGN_IDENTITY = "Apple Development"; 357 | CODE_SIGN_STYLE = Automatic; 358 | CURRENT_PROJECT_VERSION = 1; 359 | DEVELOPMENT_TEAM = MBQ98GC8JT; 360 | INFOPLIST_FILE = TellTimeWidget/Info.plist; 361 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 362 | LD_RUNPATH_SEARCH_PATHS = ( 363 | "$(inherited)", 364 | "@executable_path/Frameworks", 365 | "@executable_path/../../Frameworks", 366 | ); 367 | MARKETING_VERSION = 3.1.0; 368 | PRODUCT_BUNDLE_IDENTIFIER = renaud.jenny.telltime.TellTimeWidget; 369 | PRODUCT_NAME = "$(TARGET_NAME)"; 370 | SKIP_INSTALL = YES; 371 | SWIFT_VERSION = 5.0; 372 | TARGETED_DEVICE_FAMILY = "1,2"; 373 | }; 374 | name = Release; 375 | }; 376 | 4CB1D4E022A7293500786385 /* Debug */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 382 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 383 | CLANG_CXX_LIBRARY = "libc++"; 384 | CLANG_ENABLE_MODULES = YES; 385 | CLANG_ENABLE_OBJC_ARC = YES; 386 | CLANG_ENABLE_OBJC_WEAK = YES; 387 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 388 | CLANG_WARN_BOOL_CONVERSION = YES; 389 | CLANG_WARN_COMMA = YES; 390 | CLANG_WARN_CONSTANT_CONVERSION = YES; 391 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 392 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 393 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 394 | CLANG_WARN_EMPTY_BODY = YES; 395 | CLANG_WARN_ENUM_CONVERSION = YES; 396 | CLANG_WARN_INFINITE_RECURSION = YES; 397 | CLANG_WARN_INT_CONVERSION = YES; 398 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 399 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 400 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 401 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 402 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 403 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 404 | CLANG_WARN_STRICT_PROTOTYPES = YES; 405 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 406 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 407 | CLANG_WARN_UNREACHABLE_CODE = YES; 408 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 409 | COPY_PHASE_STRIP = NO; 410 | DEBUG_INFORMATION_FORMAT = dwarf; 411 | ENABLE_STRICT_OBJC_MSGSEND = YES; 412 | ENABLE_TESTABILITY = YES; 413 | GCC_C_LANGUAGE_STANDARD = gnu11; 414 | GCC_DYNAMIC_NO_PIC = NO; 415 | GCC_NO_COMMON_BLOCKS = YES; 416 | GCC_OPTIMIZATION_LEVEL = 0; 417 | GCC_PREPROCESSOR_DEFINITIONS = ( 418 | "DEBUG=1", 419 | "$(inherited)", 420 | ); 421 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 422 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 423 | GCC_WARN_UNDECLARED_SELECTOR = YES; 424 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 425 | GCC_WARN_UNUSED_FUNCTION = YES; 426 | GCC_WARN_UNUSED_VARIABLE = YES; 427 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 428 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 429 | MTL_FAST_MATH = YES; 430 | ONLY_ACTIVE_ARCH = YES; 431 | SDKROOT = iphoneos; 432 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 433 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 434 | }; 435 | name = Debug; 436 | }; 437 | 4CB1D4E122A7293500786385 /* Release */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ALWAYS_SEARCH_USER_PATHS = NO; 441 | CLANG_ANALYZER_NONNULL = YES; 442 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 443 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 444 | CLANG_CXX_LIBRARY = "libc++"; 445 | CLANG_ENABLE_MODULES = YES; 446 | CLANG_ENABLE_OBJC_ARC = YES; 447 | CLANG_ENABLE_OBJC_WEAK = YES; 448 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 449 | CLANG_WARN_BOOL_CONVERSION = YES; 450 | CLANG_WARN_COMMA = YES; 451 | CLANG_WARN_CONSTANT_CONVERSION = YES; 452 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 453 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 454 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 455 | CLANG_WARN_EMPTY_BODY = YES; 456 | CLANG_WARN_ENUM_CONVERSION = YES; 457 | CLANG_WARN_INFINITE_RECURSION = YES; 458 | CLANG_WARN_INT_CONVERSION = YES; 459 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 460 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 461 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 462 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 463 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 464 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 465 | CLANG_WARN_STRICT_PROTOTYPES = YES; 466 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 467 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 468 | CLANG_WARN_UNREACHABLE_CODE = YES; 469 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 470 | COPY_PHASE_STRIP = NO; 471 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 472 | ENABLE_NS_ASSERTIONS = NO; 473 | ENABLE_STRICT_OBJC_MSGSEND = YES; 474 | GCC_C_LANGUAGE_STANDARD = gnu11; 475 | GCC_NO_COMMON_BLOCKS = YES; 476 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 477 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 478 | GCC_WARN_UNDECLARED_SELECTOR = YES; 479 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 480 | GCC_WARN_UNUSED_FUNCTION = YES; 481 | GCC_WARN_UNUSED_VARIABLE = YES; 482 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 483 | MTL_ENABLE_DEBUG_INFO = NO; 484 | MTL_FAST_MATH = YES; 485 | SDKROOT = iphoneos; 486 | SWIFT_COMPILATION_MODE = wholemodule; 487 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 488 | VALIDATE_PRODUCT = YES; 489 | }; 490 | name = Release; 491 | }; 492 | 4CB1D4E322A7293500786385 /* Debug */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 496 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 497 | CODE_SIGN_IDENTITY = "Apple Development"; 498 | CODE_SIGN_STYLE = Automatic; 499 | CURRENT_PROJECT_VERSION = 1; 500 | DEVELOPMENT_ASSET_PATHS = "telltime/Preview\\ Content"; 501 | DEVELOPMENT_TEAM = MBQ98GC8JT; 502 | ENABLE_PREVIEWS = YES; 503 | INFOPLIST_FILE = telltime/Info.plist; 504 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 505 | LD_RUNPATH_SEARCH_PATHS = ( 506 | "$(inherited)", 507 | "@executable_path/Frameworks", 508 | ); 509 | MARKETING_VERSION = 3.1.0; 510 | PRODUCT_BUNDLE_IDENTIFIER = renaud.jenny.telltime; 511 | PRODUCT_NAME = "Tell Time UK"; 512 | PROVISIONING_PROFILE_SPECIFIER = ""; 513 | SWIFT_VERSION = 5.0; 514 | TARGETED_DEVICE_FAMILY = "1,2"; 515 | }; 516 | name = Debug; 517 | }; 518 | 4CB1D4E422A7293500786385 /* Release */ = { 519 | isa = XCBuildConfiguration; 520 | buildSettings = { 521 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 522 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 523 | CODE_SIGN_IDENTITY = "Apple Development"; 524 | CODE_SIGN_STYLE = Automatic; 525 | CURRENT_PROJECT_VERSION = 1; 526 | DEVELOPMENT_ASSET_PATHS = "telltime/Preview\\ Content"; 527 | DEVELOPMENT_TEAM = MBQ98GC8JT; 528 | ENABLE_PREVIEWS = YES; 529 | INFOPLIST_FILE = telltime/Info.plist; 530 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 531 | LD_RUNPATH_SEARCH_PATHS = ( 532 | "$(inherited)", 533 | "@executable_path/Frameworks", 534 | ); 535 | MARKETING_VERSION = 3.1.0; 536 | PRODUCT_BUNDLE_IDENTIFIER = renaud.jenny.telltime; 537 | PRODUCT_NAME = "Tell Time UK"; 538 | PROVISIONING_PROFILE_SPECIFIER = ""; 539 | SWIFT_VERSION = 5.0; 540 | TARGETED_DEVICE_FAMILY = "1,2"; 541 | }; 542 | name = Release; 543 | }; 544 | /* End XCBuildConfiguration section */ 545 | 546 | /* Begin XCConfigurationList section */ 547 | 4C3CCC7224B39356008F0699 /* Build configuration list for PBXNativeTarget "TellTimeWidgetExtension" */ = { 548 | isa = XCConfigurationList; 549 | buildConfigurations = ( 550 | 4C3CCC7024B39356008F0699 /* Debug */, 551 | 4C3CCC7124B39356008F0699 /* Release */, 552 | ); 553 | defaultConfigurationIsVisible = 0; 554 | defaultConfigurationName = Release; 555 | }; 556 | 4CB1D4B322A7293300786385 /* Build configuration list for PBXProject "TellTime" */ = { 557 | isa = XCConfigurationList; 558 | buildConfigurations = ( 559 | 4CB1D4E022A7293500786385 /* Debug */, 560 | 4CB1D4E122A7293500786385 /* Release */, 561 | ); 562 | defaultConfigurationIsVisible = 0; 563 | defaultConfigurationName = Release; 564 | }; 565 | 4CB1D4E222A7293500786385 /* Build configuration list for PBXNativeTarget "TellTime" */ = { 566 | isa = XCConfigurationList; 567 | buildConfigurations = ( 568 | 4CB1D4E322A7293500786385 /* Debug */, 569 | 4CB1D4E422A7293500786385 /* Release */, 570 | ); 571 | defaultConfigurationIsVisible = 0; 572 | defaultConfigurationName = Release; 573 | }; 574 | /* End XCConfigurationList section */ 575 | 576 | /* Begin XCSwiftPackageProductDependency section */ 577 | 4C4A811829CFC03600463169 /* TTSCore */ = { 578 | isa = XCSwiftPackageProductDependency; 579 | productName = TTSCore; 580 | }; 581 | 4C5FD3DB29F4C7F1008EB5C9 /* AppFeature */ = { 582 | isa = XCSwiftPackageProductDependency; 583 | productName = AppFeature; 584 | }; 585 | 4C5FD3DD29F4CCA2008EB5C9 /* WidgetFeature */ = { 586 | isa = XCSwiftPackageProductDependency; 587 | productName = WidgetFeature; 588 | }; 589 | 4CC951C229E291440022C537 /* ConfigurationFeature */ = { 590 | isa = XCSwiftPackageProductDependency; 591 | productName = ConfigurationFeature; 592 | }; 593 | 4CEEF27729D92B8C00CD1DC8 /* SpeechRecognizerCore */ = { 594 | isa = XCSwiftPackageProductDependency; 595 | productName = SpeechRecognizerCore; 596 | }; 597 | /* End XCSwiftPackageProductDependency section */ 598 | }; 599 | rootObject = 4CB1D4B022A7293300786385 /* Project object */; 600 | } 601 | -------------------------------------------------------------------------------- /telltime/TellTime.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /telltime/TellTime.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /telltime/TellTime.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "combine-schedulers", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/combine-schedulers", 7 | "state" : { 8 | "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab", 9 | "version" : "0.9.1" 10 | } 11 | }, 12 | { 13 | "identity" : "renaudjennyaboutview", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/renaudjenny/RenaudJennyAboutView", 16 | "state" : { 17 | "revision" : "88b458bdb1cfed265a025f7d257d4fad4d5a5b24", 18 | "version" : "1.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-case-paths", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-case-paths", 25 | "state" : { 26 | "revision" : "870133b7b2387df136ad301ec67b2e864b51dda1", 27 | "version" : "0.14.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-clocks", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/swift-clocks", 34 | "state" : { 35 | "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172", 36 | "version" : "0.2.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-collections", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-collections", 43 | "state" : { 44 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 45 | "version" : "1.0.4" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-composable-architecture", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/pointfreeco/swift-composable-architecture", 52 | "state" : { 53 | "revision" : "3e8eee1efe99d06e99426d421733b858b332186b", 54 | "version" : "0.52.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-custom-dump", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 61 | "state" : { 62 | "revision" : "de8ba65649e7ee317b9daf27dd5eebf34bd4be57", 63 | "version" : "0.9.1" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-dependencies", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/pointfreeco/swift-dependencies", 70 | "state" : { 71 | "revision" : "98650d886ec950b587d671261f06d6b59dec4052", 72 | "version" : "0.4.1" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-identified-collections", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/pointfreeco/swift-identified-collections", 79 | "state" : { 80 | "revision" : "ad3932d28c2e0a009a0167089619526709ef6497", 81 | "version" : "0.7.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-past-ten", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/renaudjenny/swift-past-ten", 88 | "state" : { 89 | "revision" : "84066e7940d44929b7d51809d4294073d1006230", 90 | "version" : "1.1.0" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-snapshot-testing", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", 97 | "state" : { 98 | "revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334", 99 | "version" : "1.11.0" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-speech-recognizer", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/renaudjenny/swift-speech-recognizer", 106 | "state" : { 107 | "revision" : "7a7947a6f9fb1edaa48ee0e8d1c523f5fd8e9bd8", 108 | "version" : "1.0.0" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-to-ten", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/renaudjenny/swift-to-ten", 115 | "state" : { 116 | "revision" : "a5045b95dd581e02e38b56565e3305f54d43e4ed", 117 | "version" : "1.2.0" 118 | } 119 | }, 120 | { 121 | "identity" : "swift-tts", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/renaudjenny/swift-tts", 124 | "state" : { 125 | "revision" : "01ed546e43f75e45eee49ed91f5a449d86e48fa3", 126 | "version" : "2.1.2" 127 | } 128 | }, 129 | { 130 | "identity" : "swiftclockui", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/renaudjenny/SwiftClockUI", 133 | "state" : { 134 | "revision" : "b4977887fcfd42201a709357076e3f111ff23c97", 135 | "version" : "2.0.0" 136 | } 137 | }, 138 | { 139 | "identity" : "swiftregex5", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/johnno1962/SwiftRegex5", 142 | "state" : { 143 | "revision" : "8ecb4a8570757b7f8bce72f82242742e13891f99", 144 | "version" : "5.2.3" 145 | } 146 | }, 147 | { 148 | "identity" : "swiftui-navigation", 149 | "kind" : "remoteSourceControl", 150 | "location" : "https://github.com/pointfreeco/swiftui-navigation", 151 | "state" : { 152 | "revision" : "47dd574b900ba5ba679f56ea00d4d282fc7305a6", 153 | "version" : "0.7.1" 154 | } 155 | }, 156 | { 157 | "identity" : "xctest-dynamic-overlay", 158 | "kind" : "remoteSourceControl", 159 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 160 | "state" : { 161 | "revision" : "ab8c9f45843694dd16be4297e6d44c0634fd9913", 162 | "version" : "0.8.4" 163 | } 164 | } 165 | ], 166 | "version" : 2 167 | } 168 | -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Test Clock 40x40-2.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Test Clock 60x60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Test Clock 58x58-1.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Test Clock 87x87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Test Clock 80x80-1.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Test Clock 120x120-1.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Test Clock 120x120.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Test Clock 180x180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Test Clock 20x20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Test Clock 40x40-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Test Clock 29x29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Test Clock 58x58.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Test Clock 40x40.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Test Clock 80x80.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Test Clock 76x76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Test Clock 152x152.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Test Clock 167x167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Test Clock-2.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 120x120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 120x120-1.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 120x120.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 152x152.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 167x167.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 180x180.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 20x20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 20x20.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 29x29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 29x29.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 40x40-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 40x40-1.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 40x40-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 40x40-2.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 40x40.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 58x58-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 58x58-1.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 58x58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 58x58.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 60x60.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 76x76.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 80x80-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 80x80-1.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 80x80.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 87x87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock 87x87.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/AppIcon.appiconset/Test Clock-2.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "TellTimeLogo200x200.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "TellTimeLogo400x400.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "TellTimeLogo600x600.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/Logo.imageset/TellTimeLogo200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/Logo.imageset/TellTimeLogo200x200.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/Logo.imageset/TellTimeLogo400x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/Logo.imageset/TellTimeLogo400x400.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/Logo.imageset/TellTimeLogo600x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTime/Assets.xcassets/Logo.imageset/TellTimeLogo600x600.png -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/clock-svg.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "method-draw-image.svg", 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 | -------------------------------------------------------------------------------- /telltime/TellTime/Assets.xcassets/clock-svg.imageset/method-draw-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | Tell Time 7 | 8 | -------------------------------------------------------------------------------- /telltime/TellTime/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSRequiresIPhoneOS 22 | 23 | NSMicrophoneUsageDescription 24 | TellTime listen for specific words when you press the speech recognition button, like the time you're dictating. For instance, if it understands "five to five", it will display 04:55 and move the arms on the Clock accordingly 25 | NSSpeechRecognitionUsageDescription 26 | TellTime listen for specific words when you press the speech recognition button, like the time you're dictating. For instance, if it understands "five to five", it will display 04:55 and move the arms on the Clock accordingly 27 | NSUserActivityTypes 28 | 29 | ConfigurationIntent 30 | 31 | UIApplicationSceneManifest 32 | 33 | UIApplicationSupportsMultipleScenes 34 | 35 | 36 | UILaunchStoryboardName 37 | LaunchScreen 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /telltime/TellTime/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /telltime/TellTime/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /telltime/TellTime/TellTimeUKApp.swift: -------------------------------------------------------------------------------- 1 | import AppFeature 2 | import SwiftUI 3 | 4 | @main 5 | struct TellTimeUKApp: SwiftUI.App { 6 | 7 | var body: some Scene { 8 | WindowGroup { 9 | AppView(store: .live) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testArmWithAnAngle.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testArmWithAnAngle.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testArms.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testArms.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testArtNouveauArm.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testArtNouveauArm.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testBiggerArm.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testBiggerArm.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testDrawnArms.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testDrawnArms.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testDrawningArm.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/ArmTests/testDrawningArm.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/BorderTests/testClassicClockBorders.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/BorderTests/testClassicClockBorders.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Elements/__Snapshots__/IndicatorsTests/testIndicators.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Elements/__Snapshots__/IndicatorsTests/testIndicators.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Face/__Snapshots__/ClockFaceTests/testClockFaceNeutral.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Face/__Snapshots__/ClockFaceTests/testClockFaceNeutral.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Face/__Snapshots__/ClockFaceTests/testClockFaceSmiling.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Face/__Snapshots__/ClockFaceTests/testClockFaceSmiling.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Face/__Snapshots__/EyeTests/testEyes.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Face/__Snapshots__/EyeTests/testEyes.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/Face/__Snapshots__/MouthTests/testMouths.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/Face/__Snapshots__/MouthTests/testMouths.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViewArtNouveauStyle.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViewArtNouveauStyle.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViewDrawingStyle.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViewDrawingStyle.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViewWithFace.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViewWithFace.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViews.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Clock/__Snapshots__/ClockTests/testClockViews.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Common/__Snapshots__/SwiftUIExtensionsTests/testPathExtension.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Common/__Snapshots__/SwiftUIExtensionsTests/testPathExtension.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Configuration/__Snapshots__/ConfigurationViewTests/testConfigurationViews.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Configuration/__Snapshots__/ConfigurationViewTests/testConfigurationViews.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/Configuration/__Snapshots__/ConfigurationViewTests/testConfigurationViewsInLandscape.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/Configuration/__Snapshots__/ConfigurationViewTests/testConfigurationViewsInLandscape.1.png -------------------------------------------------------------------------------- /telltime/TellTimeTests/TTS/__Snapshots__/SpeakButtonTests/testSpeakButtons.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/renaudjenny/TellTime/43ab01c380dcfc0c5daa15ddbdd880001b6feb06/telltime/TellTimeTests/TTS/__Snapshots__/SpeakButtonTests/testSpeakButtons.1.png -------------------------------------------------------------------------------- /telltime/TellTimeWidget/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 | -------------------------------------------------------------------------------- /telltime/TellTimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /telltime/TellTimeWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /telltime/TellTimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /telltime/TellTimeWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | TellTimeWidget 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSExtension 24 | 25 | NSExtensionPointIdentifier 26 | com.apple.widgetkit-extension 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /telltime/TellTimeWidget/TellTimeWidget.intentdefinition: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | INEnums 6 | 7 | 8 | INEnumDisplayName 9 | Design 10 | INEnumDisplayNameID 11 | MaulU7 12 | INEnumGeneratesHeader 13 | 14 | INEnumName 15 | Design 16 | INEnumType 17 | Regular 18 | INEnumValues 19 | 20 | 21 | INEnumValueDisplayName 22 | Classic 23 | INEnumValueDisplayNameID 24 | iQhdzb 25 | INEnumValueName 26 | unknown 27 | 28 | 29 | INEnumValueDisplayName 30 | Classic 31 | INEnumValueDisplayNameID 32 | bKTLWx 33 | INEnumValueIndex 34 | 1 35 | INEnumValueName 36 | classic 37 | 38 | 39 | INEnumValueDisplayName 40 | Art Nouveau 41 | INEnumValueDisplayNameID 42 | 7h2mDV 43 | INEnumValueIndex 44 | 2 45 | INEnumValueName 46 | artNouveau 47 | 48 | 49 | INEnumValueDisplayName 50 | Drawing 51 | INEnumValueDisplayNameID 52 | 5epwMO 53 | INEnumValueIndex 54 | 3 55 | INEnumValueName 56 | drawing 57 | 58 | 59 | INEnumValueDisplayName 60 | Steampunk 61 | INEnumValueDisplayNameID 62 | hHLIwp 63 | INEnumValueIndex 64 | 4 65 | INEnumValueName 66 | steampunk 67 | 68 | 69 | INEnumValueDisplayName 70 | Text 71 | INEnumValueDisplayNameID 72 | ySJMgK 73 | INEnumValueIndex 74 | 5 75 | INEnumValueName 76 | text 77 | 78 | 79 | 80 | 81 | INIntentDefinitionModelVersion 82 | 1.2 83 | INIntentDefinitionNamespace 84 | 88xZPY 85 | INIntentDefinitionSystemVersion 86 | 20C69 87 | INIntentDefinitionToolsBuildVersion 88 | 12C33 89 | INIntentDefinitionToolsVersion 90 | 12.3 91 | INIntents 92 | 93 | 94 | INIntentCategory 95 | information 96 | INIntentDescription 97 | Configure the design of the displayed Clock 98 | INIntentDescriptionID 99 | tVvJ9c 100 | INIntentEligibleForWidgets 101 | 102 | INIntentIneligibleForSuggestions 103 | 104 | INIntentLastParameterTag 105 | 2 106 | INIntentName 107 | Configuration 108 | INIntentParameters 109 | 110 | 111 | INIntentParameterConfigurable 112 | 113 | INIntentParameterDisplayName 114 | Design 115 | INIntentParameterDisplayNameID 116 | C5UU5M 117 | INIntentParameterDisplayPriority 118 | 1 119 | INIntentParameterEnumType 120 | Design 121 | INIntentParameterEnumTypeNamespace 122 | 88xZPY 123 | INIntentParameterName 124 | design 125 | INIntentParameterPromptDialogs 126 | 127 | 128 | INIntentParameterPromptDialogCustom 129 | 130 | INIntentParameterPromptDialogType 131 | Configuration 132 | 133 | 134 | INIntentParameterPromptDialogCustom 135 | 136 | INIntentParameterPromptDialogType 137 | Primary 138 | 139 | 140 | INIntentParameterPromptDialogCustom 141 | 142 | INIntentParameterPromptDialogFormatString 143 | There are ${count} options matching ‘${design}’. 144 | INIntentParameterPromptDialogFormatStringID 145 | eMwt1B 146 | INIntentParameterPromptDialogType 147 | DisambiguationIntroduction 148 | 149 | 150 | INIntentParameterPromptDialogCustom 151 | 152 | INIntentParameterPromptDialogFormatString 153 | Just to confirm, you wanted ‘${design}’? 154 | INIntentParameterPromptDialogFormatStringID 155 | tR9EbD 156 | INIntentParameterPromptDialogType 157 | Confirmation 158 | 159 | 160 | INIntentParameterTag 161 | 2 162 | INIntentParameterType 163 | Integer 164 | 165 | 166 | INIntentResponse 167 | 168 | INIntentResponseCodes 169 | 170 | 171 | INIntentResponseCodeName 172 | success 173 | INIntentResponseCodeSuccess 174 | 175 | 176 | 177 | INIntentResponseCodeName 178 | failure 179 | 180 | 181 | 182 | INIntentTitle 183 | Configuration 184 | INIntentTitleID 185 | gpCwrM 186 | INIntentType 187 | Custom 188 | INIntentVerb 189 | View 190 | 191 | 192 | INTypes 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /telltime/TellTimeWidget/TellTimeWidget.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | import SwiftUI 3 | import WidgetFeature 4 | import WidgetKit 5 | 6 | struct Provider: IntentTimelineProvider { 7 | 8 | func placeholder(in context: Context) -> SimpleEntry { 9 | SimpleEntry( 10 | date: Date(), 11 | configuration: ConfigurationIntent() 12 | ) 13 | } 14 | 15 | func getSnapshot( 16 | for configuration: ConfigurationIntent, 17 | in context: Context, 18 | completion: @escaping (SimpleEntry) -> Void 19 | ) { 20 | let entry = SimpleEntry( 21 | date: Date(), 22 | configuration: configuration 23 | ) 24 | completion(entry) 25 | } 26 | 27 | func getTimeline( 28 | for configuration: ConfigurationIntent, 29 | in context: Context, 30 | completion: @escaping (Timeline) -> Void 31 | ) { 32 | var entries: [SimpleEntry] = [] 33 | 34 | // Generate a timeline consisting of all minute entries an hour apart, starting from the current date. 35 | let date = Date() 36 | let hour = Calendar.current.component(.hour, from: date) 37 | let minute = Calendar.current.component(.minute, from: date) 38 | let flooredDate = Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) ?? Date() 39 | for minuteOffset in 0 ..< 60 { 40 | guard let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: flooredDate) else { 41 | continue 42 | } 43 | let entry = SimpleEntry( 44 | date: entryDate, 45 | configuration: configuration 46 | ) 47 | entries.append(entry) 48 | } 49 | 50 | let timeline = Timeline(entries: entries, policy: .atEnd) 51 | completion(timeline) 52 | } 53 | } 54 | 55 | struct SimpleEntry: TimelineEntry { 56 | public let date: Date 57 | public let configuration: ConfigurationIntent 58 | } 59 | 60 | struct TellTimeWidgetEntryView: View { 61 | var entry: Provider.Entry 62 | 63 | var body: some View { 64 | WidgetView(date: entry.date, design: entry.configuration.design.rawValue) 65 | } 66 | } 67 | 68 | @main 69 | struct TellTimeWidget: SwiftUI.Widget { 70 | private let kind: String = "TellTimeWidget" 71 | 72 | public var body: some WidgetConfiguration { 73 | IntentConfiguration( 74 | kind: kind, 75 | intent: ConfigurationIntent.self, 76 | provider: Provider() 77 | ) { entry in 78 | TellTimeWidgetEntryView(entry: entry) 79 | } 80 | .configurationDisplayName("Tell Time UK") 81 | .description("Widget for help you telling you the time the British English way.") 82 | } 83 | } 84 | 85 | #if DEBUG 86 | struct WidgetView_Previews: PreviewProvider { 87 | static var previews: some View { 88 | Group { 89 | Preview().previewContext(WidgetPreviewContext(family: .systemSmall)) 90 | Preview().previewContext(WidgetPreviewContext(family: .systemMedium)) 91 | Preview().previewContext(WidgetPreviewContext(family: .systemLarge)) 92 | } 93 | } 94 | 95 | private struct Preview: View { 96 | @Environment(\.calendar) private var calendar 97 | 98 | var body: some View { 99 | TellTimeWidgetEntryView( 100 | entry: SimpleEntry( 101 | date: Date(), 102 | configuration: ConfigurationIntent() 103 | ) 104 | ) 105 | } 106 | } 107 | } 108 | #endif 109 | --------------------------------------------------------------------------------