├── .gitattributes ├── .github └── workflows │ └── ios.yml ├── .gitignore ├── LICENSE ├── MVC ├── Alarm-ios-swift.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── Alarm-ios8-swift.xccheckout │ │ ├── IDEWorkspaceChecks.plist │ │ └── WeatherAlarm.xccheckout ├── Alarm │ ├── Alarm.swift │ ├── AlarmAddEditViewController.swift │ ├── AlarmApplicationDelegate.swift │ ├── Alarms.swift │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Identifier.swift │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── LabelEditViewController.swift │ ├── MainAlarmViewController.swift │ ├── MediaViewController.swift │ ├── NotificationScheduler.swift │ ├── NotificationSchedulerDelegate.swift │ ├── Store.swift │ ├── UITableView+Extension.swift │ ├── UIWindow+Extension.swift │ ├── WeekdaysViewController.swift │ ├── bell.mp3 │ └── tickle.mp3 └── AlarmTests │ ├── Info.plist │ ├── MainAlarmViewControllerTests.swift │ └── SchedulerTests.swift └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.github/workflows/ios.yml: -------------------------------------------------------------------------------- 1 | name: iOS starter workflow 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test default scheme using any available iPhone simulator 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Set Default Scheme 18 | run: | 19 | cd MVC 20 | scheme_list=$(xcodebuild -list -json | tr -d "\n") 21 | default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") 22 | echo $default | cat >default 23 | echo Using default scheme: $default 24 | - name: Build 25 | env: 26 | scheme: ${{ 'default' }} 27 | platform: ${{ 'iOS Simulator' }} 28 | run: | 29 | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) 30 | cd MVC 31 | device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` 32 | if [ $scheme = default ]; then scheme=$(cat default); fi 33 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 34 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 35 | xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" 36 | - name: Test 37 | env: 38 | scheme: ${{ 'default' }} 39 | platform: ${{ 'iOS Simulator' }} 40 | run: | 41 | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) 42 | cd MVC 43 | device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` 44 | if [ $scheme = default ]; then scheme=$(cat default); fi 45 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 46 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 47 | xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | tags 3 | UserInterfaceState.xcuserstate 4 | *.xcbkptlist 5 | xcuserdata 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 @natsu1211 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 | -------------------------------------------------------------------------------- /MVC/Alarm-ios-swift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 53; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7701B7B31E99414800908B6C /* UIWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7701B7B21E99414800908B6C /* UIWindow+Extension.swift */; }; 11 | 7773A0F81E43276700811A1D /* Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7773A0F71E43276700811A1D /* Identifier.swift */; }; 12 | 77A8570D1E40D76000971367 /* AlarmApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A8570C1E40D76000971367 /* AlarmApplicationDelegate.swift */; }; 13 | 77A8570F1E41A04600971367 /* NotificationSchedulerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A8570E1E41A04600971367 /* NotificationSchedulerDelegate.swift */; }; 14 | 9E16062C1C62358F009B2407 /* MediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E16062B1C62358F009B2407 /* MediaViewController.swift */; }; 15 | 9E1F30731C49128000C66F79 /* NotificationScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1F30721C49128000C66F79 /* NotificationScheduler.swift */; }; 16 | 9E3B062F1BB8347000453456 /* bell.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9E3B062E1BB8347000453456 /* bell.mp3 */; }; 17 | 9E603F591BD7CA6600303E93 /* LabelEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E603F581BD7CA6600303E93 /* LabelEditViewController.swift */; }; 18 | 9E92445B1C690A8D00724DFA /* tickle.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9E92445A1C690A8D00724DFA /* tickle.mp3 */; }; 19 | 9E9BEE071BCF8FF8000CF364 /* WeekdaysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9BEE061BCF8FF8000CF364 /* WeekdaysViewController.swift */; }; 20 | 9EED78721AA0F03F00961BC2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EED78711AA0F03F00961BC2 /* AppDelegate.swift */; }; 21 | 9EED78771AA0F03F00961BC2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9EED78751AA0F03F00961BC2 /* Main.storyboard */; }; 22 | 9EED78791AA0F03F00961BC2 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9EED78781AA0F03F00961BC2 /* Images.xcassets */; }; 23 | 9EED787C1AA0F03F00961BC2 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9EED787A1AA0F03F00961BC2 /* LaunchScreen.xib */; }; 24 | 9EED78941AA1E51700961BC2 /* Alarm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EED78931AA1E51700961BC2 /* Alarm.swift */; }; 25 | 9EED789A1AA36C7900961BC2 /* AlarmAddEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EED78991AA36C7900961BC2 /* AlarmAddEditViewController.swift */; }; 26 | 9EEEC58C1BC641BF00459CF7 /* MainAlarmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EEEC58B1BC641BF00459CF7 /* MainAlarmViewController.swift */; }; 27 | FB2EEBE52AAFB6DB00EB6EB6 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB2EEBE42AAFB6DB00EB6EB6 /* UITableView+Extension.swift */; }; 28 | FB5234332AAE186900FDC1FF /* Alarms.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB5234322AAE186900FDC1FF /* Alarms.swift */; }; 29 | FB5234352AAE18AF00FDC1FF /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB5234342AAE18AF00FDC1FF /* Store.swift */; }; 30 | FB81C11F2AB5E3CA0044C5BD /* MainAlarmViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB81C11E2AB5E3CA0044C5BD /* MainAlarmViewControllerTests.swift */; }; 31 | FBAF88772AB5C84600B7A9BC /* SchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBAF88762AB5C84600B7A9BC /* SchedulerTests.swift */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 9EED78821AA0F03F00961BC2 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 9EED78641AA0F03F00961BC2 /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 9EED786B1AA0F03F00961BC2; 40 | remoteInfo = WeatherAlarm; 41 | }; 42 | /* End PBXContainerItemProxy section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 7701B7B21E99414800908B6C /* UIWindow+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIWindow+Extension.swift"; sourceTree = ""; }; 46 | 7773A0F71E43276700811A1D /* Identifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Identifier.swift; sourceTree = ""; }; 47 | 77A8570C1E40D76000971367 /* AlarmApplicationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlarmApplicationDelegate.swift; sourceTree = ""; }; 48 | 77A8570E1E41A04600971367 /* NotificationSchedulerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSchedulerDelegate.swift; sourceTree = ""; }; 49 | 9E16062B1C62358F009B2407 /* MediaViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaViewController.swift; sourceTree = ""; }; 50 | 9E1F30721C49128000C66F79 /* NotificationScheduler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationScheduler.swift; sourceTree = ""; }; 51 | 9E3B062E1BB8347000453456 /* bell.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = bell.mp3; sourceTree = ""; }; 52 | 9E603F581BD7CA6600303E93 /* LabelEditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelEditViewController.swift; sourceTree = ""; }; 53 | 9E92445A1C690A8D00724DFA /* tickle.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = tickle.mp3; sourceTree = ""; }; 54 | 9E9BEE061BCF8FF8000CF364 /* WeekdaysViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeekdaysViewController.swift; sourceTree = ""; }; 55 | 9EED786C1AA0F03F00961BC2 /* Alarm-ios-swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Alarm-ios-swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 9EED78701AA0F03F00961BC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57 | 9EED78711AA0F03F00961BC2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 58 | 9EED78761AA0F03F00961BC2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 59 | 9EED78781AA0F03F00961BC2 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 60 | 9EED787B1AA0F03F00961BC2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 61 | 9EED78811AA0F03F00961BC2 /* Alarm-ios-swiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Alarm-ios-swiftTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 9EED78861AA0F03F00961BC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 63 | 9EED78931AA1E51700961BC2 /* Alarm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = ""; }; 64 | 9EED78991AA36C7900961BC2 /* AlarmAddEditViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlarmAddEditViewController.swift; sourceTree = ""; }; 65 | 9EEEC58B1BC641BF00459CF7 /* MainAlarmViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainAlarmViewController.swift; sourceTree = ""; }; 66 | FB2EEBE42AAFB6DB00EB6EB6 /* UITableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 67 | FB5234322AAE186900FDC1FF /* Alarms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarms.swift; sourceTree = ""; }; 68 | FB5234342AAE18AF00FDC1FF /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 69 | FB81C11E2AB5E3CA0044C5BD /* MainAlarmViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAlarmViewControllerTests.swift; sourceTree = ""; }; 70 | FBAF88762AB5C84600B7A9BC /* SchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchedulerTests.swift; sourceTree = ""; }; 71 | /* End PBXFileReference section */ 72 | 73 | /* Begin PBXFrameworksBuildPhase section */ 74 | 9EED78691AA0F03F00961BC2 /* Frameworks */ = { 75 | isa = PBXFrameworksBuildPhase; 76 | buildActionMask = 2147483647; 77 | files = ( 78 | ); 79 | runOnlyForDeploymentPostprocessing = 0; 80 | }; 81 | 9EED787E1AA0F03F00961BC2 /* Frameworks */ = { 82 | isa = PBXFrameworksBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | 770405701EA09FB2005F9379 /* Extension */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 7701B7B21E99414800908B6C /* UIWindow+Extension.swift */, 95 | FB2EEBE42AAFB6DB00EB6EB6 /* UITableView+Extension.swift */, 96 | ); 97 | name = Extension; 98 | sourceTree = ""; 99 | }; 100 | 7732E60F1E37594300C9D9A2 /* Protocol */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 77A8570C1E40D76000971367 /* AlarmApplicationDelegate.swift */, 104 | 77A8570E1E41A04600971367 /* NotificationSchedulerDelegate.swift */, 105 | ); 106 | name = Protocol; 107 | sourceTree = ""; 108 | }; 109 | 7751AD191E36F41E00CDED58 /* Model */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 9EED78931AA1E51700961BC2 /* Alarm.swift */, 113 | FB5234322AAE186900FDC1FF /* Alarms.swift */, 114 | FB5234342AAE18AF00FDC1FF /* Store.swift */, 115 | ); 116 | name = Model; 117 | sourceTree = ""; 118 | }; 119 | 77A857121E42FDAD00971367 /* Controller */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 9EEEC58B1BC641BF00459CF7 /* MainAlarmViewController.swift */, 123 | 9EED78991AA36C7900961BC2 /* AlarmAddEditViewController.swift */, 124 | 9E16062B1C62358F009B2407 /* MediaViewController.swift */, 125 | 9E9BEE061BCF8FF8000CF364 /* WeekdaysViewController.swift */, 126 | 9E603F581BD7CA6600303E93 /* LabelEditViewController.swift */, 127 | ); 128 | name = Controller; 129 | sourceTree = ""; 130 | }; 131 | 77A857131E42FDF200971367 /* App */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 9E1F30721C49128000C66F79 /* NotificationScheduler.swift */, 135 | 9EED78711AA0F03F00961BC2 /* AppDelegate.swift */, 136 | 7773A0F71E43276700811A1D /* Identifier.swift */, 137 | ); 138 | name = App; 139 | sourceTree = ""; 140 | }; 141 | 77A857141E42FE4400971367 /* View */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 9EED78751AA0F03F00961BC2 /* Main.storyboard */, 145 | 9EED787A1AA0F03F00961BC2 /* LaunchScreen.xib */, 146 | ); 147 | name = View; 148 | sourceTree = ""; 149 | }; 150 | 9EED78631AA0F03F00961BC2 = { 151 | isa = PBXGroup; 152 | children = ( 153 | 9EED786E1AA0F03F00961BC2 /* Alarm */, 154 | 9EED78841AA0F03F00961BC2 /* AlarmTests */, 155 | 9EED786D1AA0F03F00961BC2 /* Products */, 156 | ); 157 | sourceTree = ""; 158 | }; 159 | 9EED786D1AA0F03F00961BC2 /* Products */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 9EED786C1AA0F03F00961BC2 /* Alarm-ios-swift.app */, 163 | 9EED78811AA0F03F00961BC2 /* Alarm-ios-swiftTests.xctest */, 164 | ); 165 | name = Products; 166 | sourceTree = ""; 167 | }; 168 | 9EED786E1AA0F03F00961BC2 /* Alarm */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 770405701EA09FB2005F9379 /* Extension */, 172 | 77A857131E42FDF200971367 /* App */, 173 | 77A857121E42FDAD00971367 /* Controller */, 174 | 9EED78781AA0F03F00961BC2 /* Images.xcassets */, 175 | 7751AD191E36F41E00CDED58 /* Model */, 176 | 7732E60F1E37594300C9D9A2 /* Protocol */, 177 | 9EED786F1AA0F03F00961BC2 /* Supporting Files */, 178 | 77A857141E42FE4400971367 /* View */, 179 | ); 180 | path = Alarm; 181 | sourceTree = ""; 182 | }; 183 | 9EED786F1AA0F03F00961BC2 /* Supporting Files */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 9E92445A1C690A8D00724DFA /* tickle.mp3 */, 187 | 9E3B062E1BB8347000453456 /* bell.mp3 */, 188 | 9EED78701AA0F03F00961BC2 /* Info.plist */, 189 | ); 190 | name = "Supporting Files"; 191 | sourceTree = ""; 192 | }; 193 | 9EED78841AA0F03F00961BC2 /* AlarmTests */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 9EED78851AA0F03F00961BC2 /* Supporting Files */, 197 | FBAF88762AB5C84600B7A9BC /* SchedulerTests.swift */, 198 | FB81C11E2AB5E3CA0044C5BD /* MainAlarmViewControllerTests.swift */, 199 | ); 200 | path = AlarmTests; 201 | sourceTree = ""; 202 | }; 203 | 9EED78851AA0F03F00961BC2 /* Supporting Files */ = { 204 | isa = PBXGroup; 205 | children = ( 206 | 9EED78861AA0F03F00961BC2 /* Info.plist */, 207 | ); 208 | name = "Supporting Files"; 209 | sourceTree = ""; 210 | }; 211 | /* End PBXGroup section */ 212 | 213 | /* Begin PBXNativeTarget section */ 214 | 9EED786B1AA0F03F00961BC2 /* Alarm-ios-swift */ = { 215 | isa = PBXNativeTarget; 216 | buildConfigurationList = 9EED788B1AA0F03F00961BC2 /* Build configuration list for PBXNativeTarget "Alarm-ios-swift" */; 217 | buildPhases = ( 218 | 9EED78681AA0F03F00961BC2 /* Sources */, 219 | 9EED78691AA0F03F00961BC2 /* Frameworks */, 220 | 9EED786A1AA0F03F00961BC2 /* Resources */, 221 | ); 222 | buildRules = ( 223 | ); 224 | dependencies = ( 225 | ); 226 | name = "Alarm-ios-swift"; 227 | productName = WeatherAlarm; 228 | productReference = 9EED786C1AA0F03F00961BC2 /* Alarm-ios-swift.app */; 229 | productType = "com.apple.product-type.application"; 230 | }; 231 | 9EED78801AA0F03F00961BC2 /* Alarm-ios-swiftTests */ = { 232 | isa = PBXNativeTarget; 233 | buildConfigurationList = 9EED788E1AA0F03F00961BC2 /* Build configuration list for PBXNativeTarget "Alarm-ios-swiftTests" */; 234 | buildPhases = ( 235 | 9EED787D1AA0F03F00961BC2 /* Sources */, 236 | 9EED787E1AA0F03F00961BC2 /* Frameworks */, 237 | 9EED787F1AA0F03F00961BC2 /* Resources */, 238 | ); 239 | buildRules = ( 240 | ); 241 | dependencies = ( 242 | 9EED78831AA0F03F00961BC2 /* PBXTargetDependency */, 243 | ); 244 | name = "Alarm-ios-swiftTests"; 245 | productName = WeatherAlarmTests; 246 | productReference = 9EED78811AA0F03F00961BC2 /* Alarm-ios-swiftTests.xctest */; 247 | productType = "com.apple.product-type.bundle.unit-test"; 248 | }; 249 | /* End PBXNativeTarget section */ 250 | 251 | /* Begin PBXProject section */ 252 | 9EED78641AA0F03F00961BC2 /* Project object */ = { 253 | isa = PBXProject; 254 | attributes = { 255 | BuildIndependentTargetsInParallel = YES; 256 | LastSwiftMigration = 0720; 257 | LastSwiftUpdateCheck = 0720; 258 | LastUpgradeCheck = 1430; 259 | ORGANIZATIONNAME = LongGames; 260 | TargetAttributes = { 261 | 9EED786B1AA0F03F00961BC2 = { 262 | CreatedOnToolsVersion = 6.1.1; 263 | LastSwiftMigration = ""; 264 | SystemCapabilities = { 265 | com.apple.BackgroundModes = { 266 | enabled = 1; 267 | }; 268 | }; 269 | }; 270 | 9EED78801AA0F03F00961BC2 = { 271 | CreatedOnToolsVersion = 6.1.1; 272 | LastSwiftMigration = 1430; 273 | TestTargetID = 9EED786B1AA0F03F00961BC2; 274 | }; 275 | }; 276 | }; 277 | buildConfigurationList = 9EED78671AA0F03F00961BC2 /* Build configuration list for PBXProject "Alarm-ios-swift" */; 278 | compatibilityVersion = "Xcode 3.2"; 279 | developmentRegion = en; 280 | hasScannedForEncodings = 0; 281 | knownRegions = ( 282 | en, 283 | Base, 284 | ); 285 | mainGroup = 9EED78631AA0F03F00961BC2; 286 | productRefGroup = 9EED786D1AA0F03F00961BC2 /* Products */; 287 | projectDirPath = ""; 288 | projectRoot = ""; 289 | targets = ( 290 | 9EED786B1AA0F03F00961BC2 /* Alarm-ios-swift */, 291 | 9EED78801AA0F03F00961BC2 /* Alarm-ios-swiftTests */, 292 | ); 293 | }; 294 | /* End PBXProject section */ 295 | 296 | /* Begin PBXResourcesBuildPhase section */ 297 | 9EED786A1AA0F03F00961BC2 /* Resources */ = { 298 | isa = PBXResourcesBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 9E3B062F1BB8347000453456 /* bell.mp3 in Resources */, 302 | 9E92445B1C690A8D00724DFA /* tickle.mp3 in Resources */, 303 | 9EED78771AA0F03F00961BC2 /* Main.storyboard in Resources */, 304 | 9EED787C1AA0F03F00961BC2 /* LaunchScreen.xib in Resources */, 305 | 9EED78791AA0F03F00961BC2 /* Images.xcassets in Resources */, 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | 9EED787F1AA0F03F00961BC2 /* Resources */ = { 310 | isa = PBXResourcesBuildPhase; 311 | buildActionMask = 2147483647; 312 | files = ( 313 | ); 314 | runOnlyForDeploymentPostprocessing = 0; 315 | }; 316 | /* End PBXResourcesBuildPhase section */ 317 | 318 | /* Begin PBXSourcesBuildPhase section */ 319 | 9EED78681AA0F03F00961BC2 /* Sources */ = { 320 | isa = PBXSourcesBuildPhase; 321 | buildActionMask = 2147483647; 322 | files = ( 323 | 9EED789A1AA36C7900961BC2 /* AlarmAddEditViewController.swift in Sources */, 324 | 7773A0F81E43276700811A1D /* Identifier.swift in Sources */, 325 | 9E1F30731C49128000C66F79 /* NotificationScheduler.swift in Sources */, 326 | FB5234352AAE18AF00FDC1FF /* Store.swift in Sources */, 327 | 9E16062C1C62358F009B2407 /* MediaViewController.swift in Sources */, 328 | 9EED78721AA0F03F00961BC2 /* AppDelegate.swift in Sources */, 329 | 77A8570D1E40D76000971367 /* AlarmApplicationDelegate.swift in Sources */, 330 | 9EED78941AA1E51700961BC2 /* Alarm.swift in Sources */, 331 | 9E603F591BD7CA6600303E93 /* LabelEditViewController.swift in Sources */, 332 | 9EEEC58C1BC641BF00459CF7 /* MainAlarmViewController.swift in Sources */, 333 | FB2EEBE52AAFB6DB00EB6EB6 /* UITableView+Extension.swift in Sources */, 334 | 7701B7B31E99414800908B6C /* UIWindow+Extension.swift in Sources */, 335 | 77A8570F1E41A04600971367 /* NotificationSchedulerDelegate.swift in Sources */, 336 | FB5234332AAE186900FDC1FF /* Alarms.swift in Sources */, 337 | 9E9BEE071BCF8FF8000CF364 /* WeekdaysViewController.swift in Sources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | 9EED787D1AA0F03F00961BC2 /* Sources */ = { 342 | isa = PBXSourcesBuildPhase; 343 | buildActionMask = 2147483647; 344 | files = ( 345 | FBAF88772AB5C84600B7A9BC /* SchedulerTests.swift in Sources */, 346 | FB81C11F2AB5E3CA0044C5BD /* MainAlarmViewControllerTests.swift in Sources */, 347 | ); 348 | runOnlyForDeploymentPostprocessing = 0; 349 | }; 350 | /* End PBXSourcesBuildPhase section */ 351 | 352 | /* Begin PBXTargetDependency section */ 353 | 9EED78831AA0F03F00961BC2 /* PBXTargetDependency */ = { 354 | isa = PBXTargetDependency; 355 | target = 9EED786B1AA0F03F00961BC2 /* Alarm-ios-swift */; 356 | targetProxy = 9EED78821AA0F03F00961BC2 /* PBXContainerItemProxy */; 357 | }; 358 | /* End PBXTargetDependency section */ 359 | 360 | /* Begin PBXVariantGroup section */ 361 | 9EED78751AA0F03F00961BC2 /* Main.storyboard */ = { 362 | isa = PBXVariantGroup; 363 | children = ( 364 | 9EED78761AA0F03F00961BC2 /* Base */, 365 | ); 366 | name = Main.storyboard; 367 | sourceTree = ""; 368 | }; 369 | 9EED787A1AA0F03F00961BC2 /* LaunchScreen.xib */ = { 370 | isa = PBXVariantGroup; 371 | children = ( 372 | 9EED787B1AA0F03F00961BC2 /* Base */, 373 | ); 374 | name = LaunchScreen.xib; 375 | sourceTree = ""; 376 | }; 377 | /* End PBXVariantGroup section */ 378 | 379 | /* Begin XCBuildConfiguration section */ 380 | 9EED78891AA0F03F00961BC2 /* Debug */ = { 381 | isa = XCBuildConfiguration; 382 | buildSettings = { 383 | ALWAYS_SEARCH_USER_PATHS = NO; 384 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 385 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 386 | CLANG_CXX_LIBRARY = "libc++"; 387 | CLANG_ENABLE_MODULES = YES; 388 | CLANG_ENABLE_OBJC_ARC = YES; 389 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 390 | CLANG_WARN_BOOL_CONVERSION = YES; 391 | CLANG_WARN_COMMA = YES; 392 | CLANG_WARN_CONSTANT_CONVERSION = YES; 393 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 394 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 395 | CLANG_WARN_EMPTY_BODY = YES; 396 | CLANG_WARN_ENUM_CONVERSION = YES; 397 | CLANG_WARN_INFINITE_RECURSION = YES; 398 | CLANG_WARN_INT_CONVERSION = YES; 399 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 400 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 401 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 402 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 403 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 404 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 405 | CLANG_WARN_STRICT_PROTOTYPES = YES; 406 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 407 | CLANG_WARN_UNREACHABLE_CODE = YES; 408 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 409 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 410 | COPY_PHASE_STRIP = NO; 411 | ENABLE_STRICT_OBJC_MSGSEND = YES; 412 | ENABLE_TESTABILITY = YES; 413 | GCC_C_LANGUAGE_STANDARD = gnu99; 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_SYMBOLS_PRIVATE_EXTERN = NO; 422 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 423 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 424 | GCC_WARN_UNDECLARED_SELECTOR = YES; 425 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 426 | GCC_WARN_UNUSED_FUNCTION = YES; 427 | GCC_WARN_UNUSED_VARIABLE = YES; 428 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 429 | MTL_ENABLE_DEBUG_INFO = YES; 430 | ONLY_ACTIVE_ARCH = YES; 431 | SDKROOT = iphoneos; 432 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 433 | TARGETED_DEVICE_FAMILY = "1,2"; 434 | }; 435 | name = Debug; 436 | }; 437 | 9EED788A1AA0F03F00961BC2 /* Release */ = { 438 | isa = XCBuildConfiguration; 439 | buildSettings = { 440 | ALWAYS_SEARCH_USER_PATHS = NO; 441 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 442 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 443 | CLANG_CXX_LIBRARY = "libc++"; 444 | CLANG_ENABLE_MODULES = YES; 445 | CLANG_ENABLE_OBJC_ARC = YES; 446 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 447 | CLANG_WARN_BOOL_CONVERSION = YES; 448 | CLANG_WARN_COMMA = YES; 449 | CLANG_WARN_CONSTANT_CONVERSION = YES; 450 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 451 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 452 | CLANG_WARN_EMPTY_BODY = YES; 453 | CLANG_WARN_ENUM_CONVERSION = YES; 454 | CLANG_WARN_INFINITE_RECURSION = YES; 455 | CLANG_WARN_INT_CONVERSION = YES; 456 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 457 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 458 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 459 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 460 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 461 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 462 | CLANG_WARN_STRICT_PROTOTYPES = YES; 463 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 464 | CLANG_WARN_UNREACHABLE_CODE = YES; 465 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 466 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 467 | COPY_PHASE_STRIP = YES; 468 | ENABLE_NS_ASSERTIONS = NO; 469 | ENABLE_STRICT_OBJC_MSGSEND = YES; 470 | GCC_C_LANGUAGE_STANDARD = gnu99; 471 | GCC_NO_COMMON_BLOCKS = YES; 472 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 473 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 474 | GCC_WARN_UNDECLARED_SELECTOR = YES; 475 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 476 | GCC_WARN_UNUSED_FUNCTION = YES; 477 | GCC_WARN_UNUSED_VARIABLE = YES; 478 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 479 | MTL_ENABLE_DEBUG_INFO = NO; 480 | SDKROOT = iphoneos; 481 | SWIFT_COMPILATION_MODE = wholemodule; 482 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 483 | TARGETED_DEVICE_FAMILY = "1,2"; 484 | VALIDATE_PRODUCT = YES; 485 | }; 486 | name = Release; 487 | }; 488 | 9EED788C1AA0F03F00961BC2 /* Debug */ = { 489 | isa = XCBuildConfiguration; 490 | buildSettings = { 491 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 492 | DEVELOPMENT_TEAM = DN47FZ6Y9F; 493 | INFOPLIST_FILE = Alarm/Info.plist; 494 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 495 | LD_RUNPATH_SEARCH_PATHS = ( 496 | "$(inherited)", 497 | "@executable_path/Frameworks", 498 | ); 499 | PRODUCT_BUNDLE_IDENTIFIER = Long.Alarm.ios.swift; 500 | PRODUCT_NAME = "Alarm-ios-swift"; 501 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 502 | SWIFT_VERSION = 4.0; 503 | }; 504 | name = Debug; 505 | }; 506 | 9EED788D1AA0F03F00961BC2 /* Release */ = { 507 | isa = XCBuildConfiguration; 508 | buildSettings = { 509 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 510 | DEVELOPMENT_TEAM = DN47FZ6Y9F; 511 | INFOPLIST_FILE = Alarm/Info.plist; 512 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 513 | LD_RUNPATH_SEARCH_PATHS = ( 514 | "$(inherited)", 515 | "@executable_path/Frameworks", 516 | ); 517 | PRODUCT_BUNDLE_IDENTIFIER = Long.Alarm.ios.swift; 518 | PRODUCT_NAME = "Alarm-ios-swift"; 519 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 520 | SWIFT_VERSION = 4.0; 521 | }; 522 | name = Release; 523 | }; 524 | 9EED788F1AA0F03F00961BC2 /* Debug */ = { 525 | isa = XCBuildConfiguration; 526 | buildSettings = { 527 | BUNDLE_LOADER = "$(TEST_HOST)"; 528 | CLANG_ENABLE_MODULES = YES; 529 | FRAMEWORK_SEARCH_PATHS = ( 530 | "$(SDKROOT)/Developer/Library/Frameworks", 531 | "$(inherited)", 532 | ); 533 | GCC_PREPROCESSOR_DEFINITIONS = ( 534 | "DEBUG=1", 535 | "$(inherited)", 536 | ); 537 | INFOPLIST_FILE = AlarmTests/Info.plist; 538 | LD_RUNPATH_SEARCH_PATHS = ( 539 | "$(inherited)", 540 | "@executable_path/Frameworks", 541 | "@loader_path/Frameworks", 542 | ); 543 | PRODUCT_BUNDLE_IDENTIFIER = "LongGames.$(PRODUCT_NAME:rfc1034identifier)"; 544 | PRODUCT_NAME = "Alarm-ios-swiftTests"; 545 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 546 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 547 | SWIFT_VERSION = 4.0; 548 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Alarm-ios-swift.app/Alarm-ios-swift"; 549 | }; 550 | name = Debug; 551 | }; 552 | 9EED78901AA0F03F00961BC2 /* Release */ = { 553 | isa = XCBuildConfiguration; 554 | buildSettings = { 555 | BUNDLE_LOADER = "$(TEST_HOST)"; 556 | CLANG_ENABLE_MODULES = YES; 557 | FRAMEWORK_SEARCH_PATHS = ( 558 | "$(SDKROOT)/Developer/Library/Frameworks", 559 | "$(inherited)", 560 | ); 561 | INFOPLIST_FILE = AlarmTests/Info.plist; 562 | LD_RUNPATH_SEARCH_PATHS = ( 563 | "$(inherited)", 564 | "@executable_path/Frameworks", 565 | "@loader_path/Frameworks", 566 | ); 567 | PRODUCT_BUNDLE_IDENTIFIER = "LongGames.$(PRODUCT_NAME:rfc1034identifier)"; 568 | PRODUCT_NAME = "Alarm-ios-swiftTests"; 569 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 570 | SWIFT_VERSION = 4.0; 571 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Alarm-ios-swift.app/Alarm-ios-swift"; 572 | }; 573 | name = Release; 574 | }; 575 | /* End XCBuildConfiguration section */ 576 | 577 | /* Begin XCConfigurationList section */ 578 | 9EED78671AA0F03F00961BC2 /* Build configuration list for PBXProject "Alarm-ios-swift" */ = { 579 | isa = XCConfigurationList; 580 | buildConfigurations = ( 581 | 9EED78891AA0F03F00961BC2 /* Debug */, 582 | 9EED788A1AA0F03F00961BC2 /* Release */, 583 | ); 584 | defaultConfigurationIsVisible = 0; 585 | defaultConfigurationName = Release; 586 | }; 587 | 9EED788B1AA0F03F00961BC2 /* Build configuration list for PBXNativeTarget "Alarm-ios-swift" */ = { 588 | isa = XCConfigurationList; 589 | buildConfigurations = ( 590 | 9EED788C1AA0F03F00961BC2 /* Debug */, 591 | 9EED788D1AA0F03F00961BC2 /* Release */, 592 | ); 593 | defaultConfigurationIsVisible = 0; 594 | defaultConfigurationName = Release; 595 | }; 596 | 9EED788E1AA0F03F00961BC2 /* Build configuration list for PBXNativeTarget "Alarm-ios-swiftTests" */ = { 597 | isa = XCConfigurationList; 598 | buildConfigurations = ( 599 | 9EED788F1AA0F03F00961BC2 /* Debug */, 600 | 9EED78901AA0F03F00961BC2 /* Release */, 601 | ); 602 | defaultConfigurationIsVisible = 0; 603 | defaultConfigurationName = Release; 604 | }; 605 | /* End XCConfigurationList section */ 606 | }; 607 | rootObject = 9EED78641AA0F03F00961BC2 /* Project object */; 608 | } 609 | -------------------------------------------------------------------------------- /MVC/Alarm-ios-swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MVC/Alarm-ios-swift.xcodeproj/project.xcworkspace/xcshareddata/Alarm-ios8-swift.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 6FF4950D-DDC3-4E7E-B65E-6088150D049C 9 | IDESourceControlProjectName 10 | Alarm-ios8-swift 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | DE6820B9BBB7F476978F92739110C5C202A9136C 14 | https://github.com/natsu1211/WeatherAlarm.git 15 | 16 | IDESourceControlProjectPath 17 | IOS/Alarm-ios8-swift.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | DE6820B9BBB7F476978F92739110C5C202A9136C 21 | ../../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/natsu1211/WeatherAlarm.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | DE6820B9BBB7F476978F92739110C5C202A9136C 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | DE6820B9BBB7F476978F92739110C5C202A9136C 36 | IDESourceControlWCCName 37 | Alarm 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MVC/Alarm-ios-swift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MVC/Alarm-ios-swift.xcodeproj/project.xcworkspace/xcshareddata/WeatherAlarm.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | D787772E-9756-4841-A2E6-675506042B20 9 | IDESourceControlProjectName 10 | WeatherAlarm 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | DE6820B9BBB7F476978F92739110C5C202A9136C 14 | https://github.com/natsu1211/WeatherAlarm.git 15 | 16 | IDESourceControlProjectPath 17 | IOS/WeatherAlarm.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | DE6820B9BBB7F476978F92739110C5C202A9136C 21 | ../../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/natsu1211/WeatherAlarm.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | DE6820B9BBB7F476978F92739110C5C202A9136C 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | DE6820B9BBB7F476978F92739110C5C202A9136C 36 | IDESourceControlWCCName 37 | WeatherAlarm 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MVC/Alarm/Alarm.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Alarm: Codable { 4 | let uuid: UUID 5 | var date: Date 6 | var enabled: Bool 7 | var snoozeEnabled: Bool 8 | var repeatWeekdays: [Int] 9 | var mediaID: String 10 | var mediaLabel: String 11 | var label: String 12 | 13 | convenience init() { 14 | self.init(uuid: UUID(), date: Date(), enabled: true, snoozeEnabled: false, repeatWeekdays: [], mediaID: "", mediaLabel: "bell", label: "Alarm") 15 | } 16 | 17 | init(uuid: UUID, date: Date, enabled: Bool, snoozeEnabled: Bool, repeatWeekdays: [Int], mediaID: String, mediaLabel: String, label: String) { 18 | self.uuid = uuid 19 | self.date = date 20 | self.enabled = enabled 21 | self.snoozeEnabled = snoozeEnabled 22 | self.repeatWeekdays = repeatWeekdays 23 | self.mediaID = mediaID 24 | self.mediaLabel = mediaLabel 25 | self.label = label 26 | } 27 | 28 | enum CodingKeys: CodingKey { 29 | case uuid 30 | case date 31 | case enabled 32 | case snoozeEnabled 33 | case repeatWeekdays 34 | case mediaID 35 | case mediaLabel 36 | case label 37 | } 38 | 39 | required init(from decoder: Decoder) throws { 40 | let container: KeyedDecodingContainer = try decoder.container(keyedBy: Alarm.CodingKeys.self) 41 | 42 | self.uuid = try container.decode(UUID.self, forKey: Alarm.CodingKeys.uuid) 43 | self.date = try container.decode(Date.self, forKey: Alarm.CodingKeys.date) 44 | self.enabled = try container.decode(Bool.self, forKey: Alarm.CodingKeys.enabled) 45 | self.snoozeEnabled = try container.decode(Bool.self, forKey: Alarm.CodingKeys.snoozeEnabled) 46 | self.repeatWeekdays = try container.decode([Int].self, forKey: Alarm.CodingKeys.repeatWeekdays) 47 | self.mediaID = try container.decode(String.self, forKey: Alarm.CodingKeys.mediaID) 48 | self.mediaLabel = try container.decode(String.self, forKey: Alarm.CodingKeys.mediaLabel) 49 | self.label = try container.decode(String.self, forKey: Alarm.CodingKeys.label) 50 | } 51 | 52 | func encode(to encoder: Encoder) throws { 53 | var container: KeyedEncodingContainer = encoder.container(keyedBy: Alarm.CodingKeys.self) 54 | 55 | try container.encode(self.uuid, forKey: Alarm.CodingKeys.uuid) 56 | try container.encode(self.date, forKey: Alarm.CodingKeys.date) 57 | try container.encode(self.enabled, forKey: Alarm.CodingKeys.enabled) 58 | try container.encode(self.snoozeEnabled, forKey: Alarm.CodingKeys.snoozeEnabled) 59 | try container.encode(self.repeatWeekdays, forKey: Alarm.CodingKeys.repeatWeekdays) 60 | try container.encode(self.mediaID, forKey: Alarm.CodingKeys.mediaID) 61 | try container.encode(self.mediaLabel, forKey: Alarm.CodingKeys.mediaLabel) 62 | try container.encode(self.label, forKey: Alarm.CodingKeys.label) 63 | } 64 | } 65 | 66 | extension Alarm { 67 | var formattedTime: String { 68 | let dateFormatter = DateFormatter() 69 | dateFormatter.dateFormat = "h:mm a" 70 | return dateFormatter.string(from: self.date) 71 | } 72 | } 73 | 74 | extension Alarm { 75 | static let changeReasonKey = "reason" 76 | static let newValueKey = "newValue" 77 | static let oldValueKey = "oldValue" 78 | static let updated = "updated" 79 | static let added = "added" 80 | static let removed = "removed" 81 | } 82 | -------------------------------------------------------------------------------- /MVC/Alarm/AlarmAddEditViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Foundation 3 | 4 | class AlarmAddEditViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{ 5 | 6 | @IBOutlet weak var datePicker: UIDatePicker! 7 | @IBOutlet weak var tableView: UITableView! 8 | 9 | var alarms: Alarms? 10 | var currentAlarm: Alarm? 11 | var isEditMode = false 12 | 13 | 14 | private var snoozeEnabled = false 15 | private var label = "" 16 | private var repeatWeekdays: [Int] = [] 17 | private var mediaLabel = "" 18 | private var mediaID = "" 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | if let alarm = currentAlarm { 23 | snoozeEnabled = alarm.snoozeEnabled 24 | label = alarm.label 25 | repeatWeekdays = alarm.repeatWeekdays 26 | mediaLabel = alarm.mediaLabel 27 | mediaID = alarm.mediaID 28 | } 29 | } 30 | 31 | override func didReceiveMemoryWarning() { 32 | super.didReceiveMemoryWarning() 33 | } 34 | 35 | @IBAction func saveEditAlarm(_ sender: AnyObject) { 36 | let date = NotificationScheduler.correctSecondComponent(date: datePicker.date) 37 | 38 | if let alarm = currentAlarm { 39 | alarm.date = date 40 | alarm.enabled = true 41 | alarm.snoozeEnabled = snoozeEnabled 42 | alarm.label = label 43 | alarm.mediaID = mediaID 44 | alarm.mediaLabel = mediaLabel 45 | alarm.repeatWeekdays = repeatWeekdays 46 | if isEditMode { 47 | alarms?.update(alarm) 48 | } 49 | else { 50 | alarms?.add(alarm) 51 | } 52 | } 53 | self.performSegue(withIdentifier: Identifier.saveSegueIdentifier, sender: self) 54 | } 55 | 56 | 57 | func numberOfSections(in tableView: UITableView) -> Int { 58 | // Return the number of sections. 59 | if isEditMode { 60 | return 2 61 | } 62 | else { 63 | return 1 64 | } 65 | } 66 | 67 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 68 | if section == 0 { 69 | return 4 70 | } 71 | else { 72 | return 1 73 | } 74 | } 75 | 76 | 77 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 78 | //var cell = tableView.dequeueReusableCell(withIdentifier: Identifier.settingIdentifier) ?? UITableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: Identifier.settingIdentifier) 79 | var cell = UITableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: Identifier.settingIdentifier) 80 | 81 | if indexPath.section == 0 { 82 | if indexPath.row == 0 { 83 | cell.textLabel?.text = "Repeat" 84 | if let alarm = currentAlarm { 85 | cell.detailTextLabel?.text = WeekdaysViewController.repeatText(weekdays: repeatWeekdays) 86 | cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator 87 | } 88 | } 89 | else if indexPath.row == 1 { 90 | cell.textLabel?.text = "Label" 91 | cell.detailTextLabel?.text = label 92 | cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator 93 | } 94 | else if indexPath.row == 2 { 95 | cell.textLabel?.text = "Sound" 96 | cell.detailTextLabel?.text = mediaLabel 97 | cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator 98 | } 99 | else if indexPath.row == 3 { 100 | cell.textLabel?.text = "Snooze" 101 | let sw = UISwitch(frame: CGRect()) 102 | sw.addTarget(self, action: #selector(AlarmAddEditViewController.snoozeSwitchTapped(_:)), for: UIControlEvents.touchUpInside) 103 | 104 | sw.setOn(snoozeEnabled == true, animated: false) 105 | cell.accessoryView = sw 106 | } 107 | 108 | return cell 109 | } 110 | else if indexPath.section == 1 { 111 | cell = UITableViewCell( 112 | style: UITableViewCellStyle.default, reuseIdentifier: Identifier.settingIdentifier) 113 | cell.textLabel?.text = "Delete Alarm" 114 | cell.textLabel?.textAlignment = .center 115 | cell.textLabel?.textColor = UIColor.red 116 | } 117 | return cell 118 | } 119 | 120 | 121 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 122 | let cell = tableView.cellForRow(at: indexPath) 123 | if indexPath.section == 0 { 124 | switch indexPath.row{ 125 | case 0: 126 | performSegue(withIdentifier: Identifier.weekdaysSegueIdentifier, sender: self) 127 | cell?.setSelected(true, animated: false) 128 | cell?.setSelected(false, animated: false) 129 | case 1: 130 | performSegue(withIdentifier: Identifier.labelSegueIdentifier, sender: self) 131 | cell?.setSelected(true, animated: false) 132 | cell?.setSelected(false, animated: false) 133 | case 2: 134 | performSegue(withIdentifier: Identifier.soundSegueIdentifier, sender: self) 135 | cell?.setSelected(true, animated: false) 136 | cell?.setSelected(false, animated: false) 137 | default: 138 | break 139 | } 140 | } 141 | else if indexPath.section == 1 { 142 | //delete alarm 143 | guard let alarm = currentAlarm else {fatalError()} 144 | alarms?.remove(alarm) 145 | performSegue(withIdentifier: Identifier.saveSegueIdentifier, sender: self) 146 | } 147 | 148 | } 149 | 150 | @IBAction func snoozeSwitchTapped (_ sender: UISwitch) { 151 | snoozeEnabled = sender.isOn 152 | } 153 | 154 | 155 | // MARK: - Navigation 156 | 157 | // In a storyboard-based application, you will often want to do a little preparation before navigation 158 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 159 | // Get the new view controller using segue.destinationViewController. 160 | // Pass the selected object to the new view controller. 161 | if segue.identifier == Identifier.soundSegueIdentifier { 162 | // TODO 163 | guard let dist = segue.destination as? MediaViewController else {fatalError()} 164 | dist.mediaID = mediaID 165 | dist.mediaLabel = mediaLabel 166 | 167 | } 168 | else if segue.identifier == Identifier.labelSegueIdentifier { 169 | guard let dist = segue.destination as? LabelEditViewController else {fatalError()} 170 | dist.label = label 171 | } 172 | else if segue.identifier == Identifier.weekdaysSegueIdentifier { 173 | guard let dist = segue.destination as? WeekdaysViewController else {fatalError()} 174 | dist.weekdays = repeatWeekdays 175 | } 176 | } 177 | 178 | @IBAction func unwindFromWeekdaysView(_ segue: UIStoryboardSegue) { 179 | guard let src = segue.source as? WeekdaysViewController else {fatalError()} 180 | repeatWeekdays = src.weekdays 181 | tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .none) 182 | } 183 | 184 | @IBAction func unwindFromLabelEditView(_ segue: UIStoryboardSegue) { 185 | guard let src = segue.source as? LabelEditViewController else {fatalError()} 186 | label = src.label 187 | tableView.reloadRows(at: [IndexPath(row: 1, section: 0)], with: .none) 188 | } 189 | 190 | @IBAction func unwindFromMediaView(_ segue: UIStoryboardSegue) { 191 | guard let src = segue.source as? MediaViewController else {fatalError()} 192 | mediaLabel = src.mediaLabel ?? "" 193 | mediaID = src.mediaID ?? "" 194 | tableView.reloadRows(at: [IndexPath(row: 2, section: 0)], with: .none) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /MVC/Alarm/AlarmApplicationDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol AlarmApplicationDelegate { 4 | func playSound(_ soundName: String) 5 | } 6 | -------------------------------------------------------------------------------- /MVC/Alarm/Alarms.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Alarms: Codable { 4 | private var alarms: [Alarm] 5 | 6 | enum CodingKeys: CodingKey { 7 | case alarms 8 | } 9 | 10 | init() { 11 | self.alarms = [Alarm]() 12 | } 13 | 14 | required init(from decoder: Decoder) throws { 15 | let container: KeyedDecodingContainer = try decoder.container(keyedBy: Alarms.CodingKeys.self) 16 | 17 | self.alarms = try container.decode([Alarm].self, forKey: Alarms.CodingKeys.alarms) 18 | 19 | } 20 | 21 | func getAlarm(ByUUIDStr uuidString: String) -> Alarm?{ 22 | return alarms.first(where: {$0.uuid.uuidString == uuidString}) 23 | } 24 | 25 | func encode(to encoder: Encoder) throws { 26 | var container: KeyedEncodingContainer = encoder.container(keyedBy: Alarms.CodingKeys.self) 27 | 28 | try container.encode(self.alarms, forKey: Alarms.CodingKeys.alarms) 29 | } 30 | 31 | 32 | func add(_ alarm: Alarm) { 33 | alarms.append(alarm) 34 | let newIndex = alarms.index { $0.uuid == alarm.uuid }! 35 | Store.shared.save(self, notifying: alarm, userInfo: [ 36 | Alarm.changeReasonKey: Alarm.added, 37 | Alarm.newValueKey: newIndex 38 | ]) 39 | } 40 | 41 | func remove(_ alarm: Alarm) { 42 | guard let index = alarms.index(where: { $0.uuid == alarm.uuid }) else { return } 43 | remove(at: index) 44 | } 45 | 46 | func remove(at index: Int) { 47 | let alarm = alarms[index] 48 | let uuidStr = alarm.uuid.uuidString 49 | alarms.remove(at: index) 50 | Store.shared.save(self, notifying: nil, userInfo: [ 51 | Alarm.changeReasonKey: Alarm.removed, 52 | Alarm.oldValueKey: index, 53 | Alarm.newValueKey: uuidStr 54 | ]) 55 | } 56 | 57 | func update(_ alarm: Alarm) { 58 | guard let index = alarms.index(where: { $0.uuid == alarm.uuid }) else { return } 59 | Store.shared.save(self, notifying: alarm, userInfo: [ 60 | Alarm.changeReasonKey: Alarm.updated, 61 | Alarm.oldValueKey: index, 62 | Alarm.newValueKey: index 63 | ]) 64 | } 65 | 66 | var count: Int { 67 | return alarms.count 68 | } 69 | 70 | var uuids: Set { 71 | return Set(alarms.map { $0.uuid.uuidString }) 72 | } 73 | 74 | subscript(index: Int) -> Alarm { 75 | return alarms[index] 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /MVC/Alarm/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Foundation 3 | import AudioToolbox 4 | import AVFoundation 5 | import UserNotifications 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate, AVAudioPlayerDelegate, UNUserNotificationCenterDelegate, AlarmApplicationDelegate{ 9 | 10 | var window: UIWindow? 11 | private var audioPlayer: AVAudioPlayer? 12 | private let notificationScheduler: NotificationSchedulerDelegate = NotificationScheduler() 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 15 | do { 16 | try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback) 17 | } catch let error as NSError{ 18 | print("could not set session. err:\(error.localizedDescription)") 19 | } 20 | do { 21 | try AVAudioSession.sharedInstance().setActive(true) 22 | } catch let error as NSError{ 23 | print("could not active session. err:\(error.localizedDescription)") 24 | } 25 | 26 | UNUserNotificationCenter.current().delegate = self 27 | notificationScheduler.requestAuthorization() 28 | notificationScheduler.registerNotificationCategories() 29 | window?.tintColor = UIColor.red 30 | 31 | return true 32 | } 33 | 34 | // The method will be called on the delegate only if the application is in the foreground. If the method is not implemented or the handler is not called in a timely manner then the notification will not be presented. The application can choose to have the notification presented as a sound, badge, alert and/or in the notification list. This decision should be based on whether the information in the notification is otherwise visible to the user. 35 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 36 | 37 | //show an alert window 38 | let alertController = UIAlertController(title: "Alarm", message: nil, preferredStyle: .alert) 39 | let userInfo = notification.request.content.userInfo 40 | guard 41 | let snoozeEnabled = userInfo["snooze"] as? Bool, 42 | let soundName = userInfo["soundName"] as? String, 43 | let uuidStr = userInfo["uuid"] as? String 44 | else {return} 45 | 46 | playSound(soundName) 47 | //schedule notification for snooze 48 | if snoozeEnabled { 49 | let snoozeOption = UIAlertAction(title: "Snooze", style: .default) { 50 | (action:UIAlertAction) in 51 | self.audioPlayer?.stop() 52 | self.notificationScheduler.setNotificationForSnooze(ringtoneName: soundName, snoozeMinute: 9, uuid: uuidStr) 53 | } 54 | alertController.addAction(snoozeOption) 55 | } 56 | 57 | let stopOption = UIAlertAction(title: "OK", style: .default) { 58 | (action:UIAlertAction) in 59 | self.audioPlayer?.stop() 60 | AudioServicesRemoveSystemSoundCompletion(kSystemSoundID_Vibrate) 61 | let alarms = Store.shared.alarms 62 | if let alarm = alarms.getAlarm(ByUUIDStr: uuidStr) { 63 | if alarm.repeatWeekdays.isEmpty { 64 | alarm.enabled = false 65 | alarms.update(alarm) 66 | } 67 | } 68 | } 69 | 70 | alertController.addAction(stopOption) 71 | window?.visibleViewController?.navigationController?.present(alertController, animated: true, completion: nil) 72 | completionHandler(.list) 73 | } 74 | 75 | // The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:. 76 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 77 | let userInfo = response.notification.request.content.userInfo 78 | guard 79 | let soundName = userInfo["soundName"] as? String, 80 | let uuid = userInfo["uuid"] as? String 81 | else {return} 82 | 83 | switch response.actionIdentifier { 84 | case Identifier.snoozeActionIdentifier: 85 | // notification fired when app in background, snooze button clicked 86 | notificationScheduler.setNotificationForSnooze(ringtoneName: soundName, snoozeMinute: 9, uuid: uuid) 87 | break 88 | case Identifier.stopActionIdentifier: 89 | // notification fired when app in background, ok button clicked 90 | let alarms = Store.shared.alarms 91 | if let alarm = alarms.getAlarm(ByUUIDStr: uuid) { 92 | if alarm.repeatWeekdays.isEmpty { 93 | alarm.enabled = false 94 | alarms.update(alarm) 95 | } 96 | } 97 | break 98 | default: 99 | break 100 | } 101 | 102 | completionHandler() 103 | } 104 | 105 | //AlarmApplicationDelegate protocol 106 | func playSound(_ soundName: String) { 107 | //vibrate phone first 108 | AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) 109 | //set vibrate callback 110 | AudioServicesAddSystemSoundCompletion(SystemSoundID(kSystemSoundID_Vibrate),nil, 111 | nil, 112 | { (_:SystemSoundID, _:UnsafeMutableRawPointer?) -> Void in 113 | AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) 114 | }, 115 | nil) 116 | 117 | guard let filePath = Bundle.main.path(forResource: soundName, ofType: "mp3") else {fatalError()} 118 | let url = URL(fileURLWithPath: filePath) 119 | 120 | do { 121 | audioPlayer = try AVAudioPlayer(contentsOf: url) 122 | } catch let error as NSError { 123 | audioPlayer = nil 124 | print("audioPlayer error \(error.localizedDescription)") 125 | return 126 | } 127 | 128 | if let player = audioPlayer { 129 | player.delegate = self 130 | player.prepareToPlay() 131 | //negative number means loop infinity 132 | player.numberOfLoops = -1 133 | player.play() 134 | } 135 | } 136 | 137 | //AVAudioPlayerDelegate protocol 138 | func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { 139 | 140 | } 141 | 142 | func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { 143 | 144 | } 145 | 146 | //UIApplicationDelegate protocol 147 | func applicationWillResignActive(_ application: UIApplication) { 148 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 149 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 150 | } 151 | 152 | func applicationDidEnterBackground(_ application: UIApplication) { 153 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 154 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 155 | } 156 | 157 | func applicationWillEnterForeground(_ application: UIApplication) { 158 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 159 | } 160 | 161 | func applicationDidBecomeActive(_ application: UIApplication) { 162 | notificationScheduler.syncAlarmStateWithNotification() 163 | } 164 | 165 | func applicationWillTerminate(_ application: UIApplication) { 166 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /MVC/Alarm/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /MVC/Alarm/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | 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 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /MVC/Alarm/Identifier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Identifier { 4 | static let stopActionIdentifier = "ALARM_IOS_SWIFT_STOP" 5 | static let snoozeActionIdentifier = "ALARM_IOS_SWIFT_SNOOZE" 6 | static let alarmCategoryIndentifier = "ALARM_CATEGORY" 7 | static let snoozeAlarmCategoryIndentifier = "SNOOZE_ALARM_CATEGORY" 8 | 9 | static let addSegueIdentifier = "addSegue" 10 | static let editSegueIdentifier = "editSegue" 11 | static let saveSegueIdentifier = "saveEditSegue" 12 | static let soundSegueIdentifier = "soundSegue" 13 | static let labelSegueIdentifier = "labelEditSegue" 14 | static let weekdaysSegueIdentifier = "weekdaysSegue" 15 | static let settingIdentifier = "setting" 16 | static let musicIdentifier = "musicIdentifier" 17 | static let alarmCellIdentifier = "alarmCell" 18 | 19 | static let labelUnwindIdentifier = "labelUnwindSegue" 20 | static let soundUnwindIdentifier = "soundUnwindSegue" 21 | static let weekdaysUnwindIdentifier = "weekdaysUnwindSegue" 22 | } 23 | -------------------------------------------------------------------------------- /MVC/Alarm/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /MVC/Alarm/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIBackgroundModes 26 | 27 | audio 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /MVC/Alarm/LabelEditViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class LabelEditViewController: UIViewController, UITextFieldDelegate { 4 | 5 | @IBOutlet weak var labelTextField: UITextField! 6 | var label: String = "" 7 | 8 | override func viewDidLoad() { 9 | super.viewDidLoad() 10 | labelTextField.becomeFirstResponder() 11 | // Do any additional setup after loading the view. 12 | self.labelTextField.delegate = self 13 | 14 | labelTextField.text = label 15 | 16 | //defined in UITextInputTraits protocol 17 | labelTextField.returnKeyType = UIReturnKeyType.done 18 | labelTextField.enablesReturnKeyAutomatically = true 19 | } 20 | 21 | override func didReceiveMemoryWarning() { 22 | super.didReceiveMemoryWarning() 23 | // Dispose of any resources that can be recreated. 24 | } 25 | 26 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 27 | label = textField.text ?? "" 28 | performSegue(withIdentifier: Identifier.labelUnwindIdentifier, sender: self) 29 | return false 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /MVC/Alarm/MainAlarmViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class MainAlarmViewController: UITableViewController{ 4 | 5 | private let alarmDelegate: AlarmApplicationDelegate = AppDelegate() 6 | private let scheduler: NotificationSchedulerDelegate = NotificationScheduler() 7 | private let alarms: Alarms = Store.shared.alarms 8 | 9 | private var selectedIndexPath: IndexPath? 10 | 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | tableView.allowsSelectionDuringEditing = true 14 | //add notification handler 15 | NotificationCenter.default.addObserver(self, selector: #selector(handleChangeNotification(_:)), name: Store.changedNotification, object: nil) 16 | //dynamically append the edit button 17 | if alarms.count != 0 { 18 | self.navigationItem.leftBarButtonItem = editButtonItem 19 | } else { 20 | self.navigationItem.leftBarButtonItem = nil 21 | } 22 | } 23 | 24 | override func didReceiveMemoryWarning() { 25 | super.didReceiveMemoryWarning() 26 | } 27 | 28 | // MARK: - Table view data source 29 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 30 | return 90 31 | } 32 | 33 | override func numberOfSections(in tableView: UITableView) -> Int { 34 | // Return the number of sections. 35 | return 1 36 | } 37 | 38 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 39 | // Return the number of rows in the section. 40 | if alarms.count == 0 { 41 | tableView.separatorStyle = UITableViewCellSeparatorStyle.none 42 | } else { 43 | tableView.separatorStyle = UITableViewCellSeparatorStyle.singleLine 44 | } 45 | return alarms.count 46 | } 47 | 48 | 49 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 50 | selectedIndexPath = indexPath 51 | if isEditing { 52 | performSegue(withIdentifier: Identifier.editSegueIdentifier, sender: self) 53 | } 54 | } 55 | 56 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 57 | //let cell = tableView.dequeueReusableCell(withIdentifier: Identifier.alarmCellIdentifier) ?? UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: Identifier.alarmCellIdentifier) 58 | let cell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: Identifier.alarmCellIdentifier) 59 | 60 | //cell text 61 | cell.selectionStyle = .none 62 | let alarm = alarms[indexPath.row] 63 | let amAttr: [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue) : UIFont.systemFont(ofSize: 20.0)] 64 | let str = NSMutableAttributedString(string: alarm.formattedTime, attributes: amAttr) 65 | let timeAttr: [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue) : UIFont.systemFont(ofSize: 45.0)] 66 | str.addAttributes(timeAttr, range: NSMakeRange(0, str.length-2)) 67 | cell.textLabel?.attributedText = str 68 | cell.detailTextLabel?.text = alarm.label 69 | 70 | let sw = UISwitch(frame: CGRect()) 71 | sw.transform = CGAffineTransform(scaleX: 0.9, y: 0.9); 72 | sw.addTarget(self, action: #selector(MainAlarmViewController.switchTapped(_:)), for: UIControlEvents.valueChanged) 73 | cell.accessoryView = sw 74 | 75 | if alarm.enabled { 76 | cell.backgroundColor = UIColor.white 77 | cell.textLabel?.alpha = 1.0 78 | cell.detailTextLabel?.alpha = 1.0 79 | sw.setOn(true, animated: false) 80 | } else { 81 | cell.backgroundColor = UIColor.groupTableViewBackground 82 | cell.textLabel?.alpha = 0.5 83 | cell.detailTextLabel?.alpha = 0.5 84 | sw.setOn(false, animated: false) 85 | } 86 | 87 | //delete empty seperator line 88 | tableView.tableFooterView = UIView(frame: CGRect.zero) 89 | return cell 90 | } 91 | 92 | @IBAction func switchTapped(_ sender: UISwitch) { 93 | guard let indexPath = tableView.indexPath(forSubView: sender) else {return} 94 | selectedIndexPath = indexPath 95 | let alarm = alarms[indexPath.row] 96 | alarm.enabled = sender.isOn 97 | alarms.update(alarm) 98 | } 99 | 100 | @objc func handleChangeNotification(_ notification: Notification) { 101 | 102 | guard let userInfo = notification.userInfo else { 103 | return 104 | } 105 | 106 | // Handle changes to contents 107 | if let changeReason = userInfo[Alarm.changeReasonKey] as? String { 108 | let newValue = userInfo[Alarm.newValueKey] 109 | let oldValue = userInfo[Alarm.oldValueKey] 110 | switch (changeReason, newValue, oldValue) { 111 | case let (Alarm.removed, (uuid as String)?, (oldValue as Int)?): 112 | tableView.deleteRows(at: [IndexPath(row: oldValue, section: 0)], with: .fade) 113 | if alarms.count == 0 { 114 | self.navigationItem.leftBarButtonItem = nil 115 | } 116 | scheduler.cancelNotification(ByUUIDStr: uuid) 117 | case let (Alarm.added, (index as Int)?, _): 118 | tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .fade) 119 | self.navigationItem.leftBarButtonItem = editButtonItem 120 | let alarm = alarms[index] 121 | scheduler.setNotification(date: alarm.date, ringtoneName: alarm.mediaLabel, repeatWeekdays: alarm.repeatWeekdays, snoozeEnabled: alarm.snoozeEnabled, onSnooze: false, uuid: alarm.uuid.uuidString) 122 | case let (Alarm.updated, (index as Int)?, _): 123 | let alarm = alarms[index] 124 | let uuid = alarm.uuid.uuidString 125 | if alarm.enabled { 126 | scheduler.updateNotification(ByUUIDStr: uuid, date: alarm.date, ringtoneName: alarm.mediaLabel, repeatWeekdays: alarm.repeatWeekdays, snoonzeEnabled: alarm.snoozeEnabled) 127 | } else { 128 | scheduler.cancelNotification(ByUUIDStr: uuid) 129 | } 130 | tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .fade) 131 | default: tableView.reloadData() 132 | } 133 | } else { 134 | tableView.reloadData() 135 | } 136 | } 137 | 138 | // Override to support editing the table view. 139 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 140 | if editingStyle == .delete { 141 | let index = indexPath.row 142 | alarms.remove(at: index) 143 | } 144 | } 145 | 146 | // In a storyboard-based application, you will often want to do a little preparation before navigation 147 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 148 | // Get the new view controller using segue.destinationViewController. 149 | // Pass the selected object to the new view controller. 150 | guard 151 | let dist = segue.destination as? UINavigationController, 152 | let addEditController = dist.topViewController as? AlarmAddEditViewController 153 | else {return} 154 | if segue.identifier == Identifier.addSegueIdentifier { 155 | addEditController.navigationItem.title = "Add Alarm" 156 | addEditController.isEditMode = false 157 | addEditController.alarms = alarms 158 | addEditController.currentAlarm = Alarm() 159 | } 160 | else if segue.identifier == Identifier.editSegueIdentifier { 161 | guard let indexPath = selectedIndexPath else {return} 162 | addEditController.navigationItem.title = "Edit Alarm" 163 | addEditController.isEditMode = true 164 | addEditController.alarms = alarms 165 | addEditController.currentAlarm = alarms[indexPath.row] 166 | } 167 | } 168 | 169 | @IBAction func unwindFromAddEditAlarmView(_ segue: UIStoryboardSegue) { 170 | isEditing = false 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /MVC/Alarm/MediaViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MediaPlayer 3 | 4 | class MediaViewController: UITableViewController, MPMediaPickerControllerDelegate { 5 | 6 | private let numberOfRingtones = 2 7 | var mediaItem: MPMediaItem? 8 | var mediaLabel: String? 9 | var mediaID: String? 10 | 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | } 14 | 15 | override func viewWillDisappear(_ animated: Bool) { 16 | performSegue(withIdentifier: Identifier.soundUnwindIdentifier, sender: self) 17 | } 18 | 19 | override func didReceiveMemoryWarning() { 20 | super.didReceiveMemoryWarning() 21 | // Dispose of any resources that can be recreated. 22 | } 23 | 24 | override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { 25 | guard let header = view as? UITableViewHeaderFooterView else { return } 26 | header.textLabel?.textColor = UIColor.gray 27 | header.textLabel?.font = UIFont.boldSystemFont(ofSize: 10) 28 | header.textLabel?.frame = header.frame 29 | header.textLabel?.textAlignment = .left 30 | } 31 | 32 | 33 | // MARK: - Table view data source 34 | 35 | override func numberOfSections(in tableView: UITableView) -> Int { 36 | // Return the number of sections. 37 | return 4 38 | } 39 | 40 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 41 | // Return the number of rows in the section. 42 | if section == 0 { 43 | return 1 44 | } 45 | else if section == 1 { 46 | return 1 47 | } 48 | else if section == 2 { 49 | return 1 50 | } 51 | else { 52 | return numberOfRingtones 53 | } 54 | } 55 | 56 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { if section == 0 { 57 | return nil 58 | } 59 | else if section == 1 { 60 | return nil 61 | } 62 | else if section == 2 { 63 | return "SONGS" 64 | } 65 | else { 66 | return "RINGTONS" 67 | } 68 | } 69 | 70 | override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 71 | return 40.0 72 | } 73 | 74 | 75 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 76 | var cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: Identifier.musicIdentifier) ?? UITableViewCell( 77 | style: UITableViewCellStyle.default, reuseIdentifier: Identifier.musicIdentifier) 78 | 79 | if indexPath.section == 0 { 80 | if indexPath.row == 0 { 81 | cell.textLabel?.text = "Buy More Tones" 82 | } 83 | } 84 | else if indexPath.section == 1 { 85 | cell.textLabel?.text = "Vibration" 86 | cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator 87 | } 88 | else if indexPath.section == 2 { 89 | if indexPath.row == 0 { 90 | cell.textLabel?.text = "Pick a song" 91 | cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator 92 | } 93 | } 94 | else if indexPath.section == 3 { 95 | if indexPath.row == 0 { 96 | cell.textLabel?.text = "bell" 97 | } 98 | else if indexPath.row == 1 { 99 | cell.textLabel?.text = "tickle" 100 | } 101 | 102 | if cell.textLabel?.text == mediaLabel { 103 | cell.accessoryType = UITableViewCellAccessoryType.checkmark 104 | } 105 | } 106 | 107 | return cell 108 | } 109 | 110 | 111 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 112 | let mediaPicker = MPMediaPickerController(mediaTypes: MPMediaType.anyAudio) 113 | mediaPicker.delegate = self 114 | mediaPicker.prompt = "Select any song!" 115 | mediaPicker.allowsPickingMultipleItems = false 116 | if indexPath.section == 2 { 117 | if indexPath.row == 0 { 118 | self.present(mediaPicker, animated: true, completion: nil) 119 | } 120 | } 121 | else if indexPath.section == 3 { 122 | let cell = tableView.cellForRow(at: indexPath) 123 | cell?.accessoryType = UITableViewCellAccessoryType.checkmark 124 | mediaLabel = cell?.textLabel?.text 125 | cell?.setSelected(true, animated: true) 126 | cell?.setSelected(false, animated: true) 127 | let cells = tableView.visibleCells 128 | for c in cells { 129 | let section = tableView.indexPath(for: c)?.section 130 | if (section == indexPath.section && c != cell) { 131 | c.accessoryType = UITableViewCellAccessoryType.none 132 | } 133 | } 134 | } 135 | } 136 | 137 | 138 | //MPMediaPickerControllerDelegate 139 | func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection:MPMediaItemCollection){ 140 | if !mediaItemCollection.items.isEmpty { 141 | let aMediaItem = mediaItemCollection.items[0] 142 | 143 | self.mediaItem = aMediaItem 144 | mediaID = (self.mediaItem?.value(forProperty: MPMediaItemPropertyPersistentID)) as? String 145 | //self.dismiss(animated: true, completion: nil) 146 | } 147 | } 148 | 149 | func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) { 150 | self.dismiss(animated: true, completion: nil) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /MVC/Alarm/NotificationScheduler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | import UserNotifications 4 | 5 | class NotificationScheduler : NotificationSchedulerDelegate 6 | { 7 | // we need to request user for notifiction permission first 8 | func requestAuthorization() { 9 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { 10 | (authorized, _) in 11 | if authorized { 12 | print("notification authorized") 13 | } else { 14 | // may need to try other way to make user authorize your app 15 | print("not authorized") 16 | } 17 | } 18 | } 19 | 20 | 21 | func registerNotificationCategories() { 22 | // Define the custom actions 23 | let snoozeAction = UNNotificationAction(identifier: Identifier.snoozeActionIdentifier, title: "Snooze", options: [.foreground]) 24 | let stopAction = UNNotificationAction(identifier: Identifier.stopActionIdentifier, title: "OK", options: [.foreground]) 25 | 26 | let snoonzeActions = [snoozeAction, stopAction] 27 | let nonSnoozeActions = [stopAction] 28 | 29 | let snoozeAlarmCategory = UNNotificationCategory(identifier: Identifier.snoozeAlarmCategoryIndentifier, 30 | actions: snoonzeActions, 31 | intentIdentifiers: [], 32 | hiddenPreviewsBodyPlaceholder: "", 33 | options: .customDismissAction) 34 | 35 | let nonSnoozeAlarmCategroy = UNNotificationCategory(identifier: Identifier.alarmCategoryIndentifier, 36 | actions: nonSnoozeActions, 37 | intentIdentifiers: [], 38 | hiddenPreviewsBodyPlaceholder: "", 39 | options: .customDismissAction) 40 | // Register the notification category 41 | UNUserNotificationCenter.current().setNotificationCategories([snoozeAlarmCategory, nonSnoozeAlarmCategroy]) 42 | } 43 | 44 | // sync alarm state to scheduled notifications for some situation (app in background and user didn't click notification to bring the app to foreground) that 45 | // alarm state is not updated correctly 46 | func syncAlarmStateWithNotification() { 47 | UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { 48 | requests in 49 | print(requests) 50 | let alarms = Store.shared.alarms 51 | let uuidNotificationsSet = Set(requests.map({$0.content.userInfo["uuid"] as! String})) 52 | let uuidAlarmsSet = alarms.uuids 53 | let uuidDeltaSet = uuidAlarmsSet.subtracting(uuidNotificationsSet) 54 | print(uuidDeltaSet) 55 | for uuid in uuidDeltaSet { 56 | if let alarm = alarms.getAlarm(ByUUIDStr: uuid) { 57 | if alarm.enabled { 58 | alarm.enabled = false 59 | // since this method will cause UI change, make sure run on main thread 60 | DispatchQueue.main.async { 61 | alarms.update(alarm) 62 | } 63 | } 64 | } 65 | } 66 | }) 67 | } 68 | 69 | private func getNotificationDates(baseDate date: Date, onWeekdaysForNotify weekdays:[Int]) -> [Date] 70 | { 71 | var notificationDates: [Date] = [Date]() 72 | let calendar = Calendar(identifier: Calendar.Identifier.gregorian) 73 | let now = Date() 74 | let flags: NSCalendar.Unit = [NSCalendar.Unit.weekday, NSCalendar.Unit.weekdayOrdinal, NSCalendar.Unit.day] 75 | let dateComponents = (calendar as NSCalendar).components(flags, from: date) 76 | let weekday = dateComponents.weekday ?? 0 77 | 78 | //no repeat 79 | if weekdays.isEmpty { 80 | //scheduling date is eariler than current date 81 | if date < now { 82 | //plus one day, otherwise the notification will be fired righton 83 | notificationDates.append((calendar as NSCalendar).date(byAdding: NSCalendar.Unit.day, value: 1, to: date, options:.matchStrictly)!) 84 | } else { 85 | notificationDates.append(date) 86 | } 87 | } 88 | else { 89 | let daysInWeek = 7 90 | for wdIndex in weekdays { 91 | // weekdays index start from 1 (Sunday) 92 | let wd = wdIndex + 1 93 | var wdDate: Date? 94 | //schedule on next week 95 | if compare(weekday: wd, with: weekday) == .before { 96 | wdDate = (calendar as NSCalendar).date(byAdding: NSCalendar.Unit.day, value: wd + daysInWeek - weekday, to: date, options:.matchStrictly) 97 | } 98 | //schedule on today or next week 99 | else if compare(weekday: wd, with: weekday) == .same { 100 | //scheduling date is eariler than current date, then schedule on next week 101 | if date.compare(now) == .orderedAscending { 102 | wdDate = (calendar as NSCalendar).date(byAdding: NSCalendar.Unit.day, value: daysInWeek, to: date, options:.matchStrictly) 103 | } 104 | else { 105 | wdDate = date 106 | } 107 | } 108 | //schedule on next days of this week 109 | else { 110 | wdDate = (calendar as NSCalendar).date(byAdding: NSCalendar.Unit.day, value: wd - weekday, to: date, options:.matchStrictly) 111 | } 112 | 113 | //fix second component to 0 114 | if let date = wdDate { 115 | let correctedDate = NotificationScheduler.correctSecondComponent(date: date, calendar: calendar) 116 | notificationDates.append(correctedDate) 117 | } 118 | } 119 | } 120 | return notificationDates 121 | } 122 | 123 | static func correctSecondComponent(date: Date, calendar: Calendar = Calendar(identifier: Calendar.Identifier.gregorian)) -> Date { 124 | let second = calendar.component(.second, from: date) 125 | let d = (calendar as NSCalendar).date(byAdding: NSCalendar.Unit.second, value: -second, to: date, options:.matchStrictly)! 126 | return d 127 | } 128 | 129 | func setNotification(date: Date, ringtoneName: String, repeatWeekdays: [Int], snoozeEnabled: Bool, onSnooze: Bool, uuid: String) { 130 | let datesForNotification = getNotificationDates(baseDate: date, onWeekdaysForNotify: repeatWeekdays) 131 | 132 | for d in datesForNotification { 133 | let notificationContent = UNMutableNotificationContent() 134 | notificationContent.title = "Alarm" 135 | notificationContent.body = "Wake Up" 136 | notificationContent.categoryIdentifier = snoozeEnabled ? Identifier.snoozeAlarmCategoryIndentifier 137 | : Identifier.alarmCategoryIndentifier 138 | notificationContent.sound = UNNotificationSound(named: ringtoneName + ".mp3") 139 | notificationContent.userInfo = ["snooze" : snoozeEnabled, "uuid": uuid, "soundName": ringtoneName] 140 | //repeat weekly if repeat weekdays are selected 141 | //no repeat with snooze notification 142 | let repeats = !repeatWeekdays.isEmpty && !onSnooze 143 | // make dataComponents only contain [weekday, hour, minute] component to make it repeat weakly 144 | let dateComponents = Calendar.current.dateComponents([.weekday,.hour,.minute], from: d) 145 | let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: repeats) 146 | let request = UNNotificationRequest(identifier: uuid, 147 | content: notificationContent, 148 | trigger: trigger) 149 | 150 | // schedule notification by adding request to notification center 151 | UNUserNotificationCenter.current().add(request) { error in 152 | if let e = error { 153 | print(e.localizedDescription) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func setNotificationForSnooze(ringtoneName: String, snoozeMinute: Int, uuid: String) { 160 | let calendar = Calendar(identifier: Calendar.Identifier.gregorian) 161 | let now = Date() 162 | let snoozeDate = (calendar as NSCalendar).date(byAdding: NSCalendar.Unit.minute, value: snoozeMinute, to: now, options:.matchStrictly)! 163 | setNotification(date: snoozeDate, ringtoneName: ringtoneName, repeatWeekdays: [], snoozeEnabled: true, onSnooze: true, uuid: uuid) 164 | } 165 | 166 | func cancelNotification(ByUUIDStr uuid: String) { 167 | UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [uuid]) 168 | } 169 | 170 | func updateNotification(ByUUIDStr uuid: String, date: Date, ringtoneName: String, repeatWeekdays: [Int], snoonzeEnabled: Bool) { 171 | cancelNotification(ByUUIDStr: uuid) 172 | setNotification(date: date, ringtoneName: ringtoneName, repeatWeekdays: repeatWeekdays, snoozeEnabled: snoonzeEnabled, onSnooze: false, uuid: uuid) 173 | } 174 | 175 | enum weekdaysComparisonResult { 176 | case before 177 | case same 178 | case after 179 | } 180 | 181 | // 1 == Sunday, 2 == Monday and so on 182 | func compare(weekday w1: Int, with w2: Int) -> weekdaysComparisonResult 183 | { 184 | if w1 != 1 && (w1 < w2 || w2 == 1) {return .before} 185 | else if w1 == w2 {return .same} 186 | else {return .after} 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /MVC/Alarm/NotificationSchedulerDelegate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | protocol NotificationSchedulerDelegate { 5 | func requestAuthorization() 6 | func registerNotificationCategories() 7 | func setNotification(date: Date, ringtoneName: String, repeatWeekdays: [Int], snoozeEnabled: Bool, onSnooze: Bool, uuid: String) 8 | func setNotificationForSnooze(ringtoneName: String, snoozeMinute: Int, uuid: String) 9 | func cancelNotification(ByUUIDStr uuid: String) 10 | func updateNotification(ByUUIDStr uuid: String, date: Date, ringtoneName: String, repeatWeekdays: [Int], snoonzeEnabled: Bool) 11 | func syncAlarmStateWithNotification() 12 | } 13 | 14 | -------------------------------------------------------------------------------- /MVC/Alarm/Store.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Store { 4 | let alarms: Alarms = load() 5 | // singleton 6 | static let shared = Store() 7 | static let changedNotification = Notification.Name("StoreChanged") 8 | 9 | func save(_ data: Alarms, notifying: Alarm?, userInfo: [AnyHashable: Any]) { 10 | if let jsonData = try? JSONEncoder().encode(data) { 11 | UserDefaults.standard.set(jsonData, forKey: .UserDefaultsKey) 12 | } 13 | NotificationCenter.default.post(name: Store.changedNotification, object: notifying, userInfo: userInfo) 14 | } 15 | 16 | static func load() -> Alarms{ 17 | if let data = UserDefaults.standard.data(forKey: .UserDefaultsKey) { 18 | if let alarms = try? JSONDecoder().decode(Alarms.self, from: data) { 19 | return alarms 20 | } 21 | } 22 | return Alarms() 23 | } 24 | 25 | func clear() { 26 | for key in UserDefaults.standard.dictionaryRepresentation().keys { 27 | UserDefaults.standard.removeObject(forKey: key.description) 28 | } 29 | } 30 | } 31 | 32 | fileprivate extension String { 33 | static let UserDefaultsKey = "UserDefaultsData" 34 | } 35 | -------------------------------------------------------------------------------- /MVC/Alarm/UITableView+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public extension UITableView { 5 | func indexPath(forSubView subView: UIView?) -> IndexPath? { 6 | var view = subView 7 | while view != nil && !(view is UITableViewCell) { 8 | view = subView?.superview 9 | } 10 | if let cellView = view as? UITableViewCell { 11 | return self.indexPath(for: cellView) 12 | } else { 13 | return nil 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MVC/Alarm/UIWindow+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public extension UIWindow { 5 | var visibleViewController: UIViewController? { 6 | return UIWindow.getVisibleViewControllerFrom(vc: self.rootViewController) 7 | } 8 | 9 | static func getVisibleViewControllerFrom(vc: UIViewController?) -> UIViewController? { 10 | if let nc = vc as? UINavigationController { 11 | return UIWindow.getVisibleViewControllerFrom(vc: nc.visibleViewController) 12 | } else if let tc = vc as? UITabBarController { 13 | return UIWindow.getVisibleViewControllerFrom(vc: tc.selectedViewController) 14 | } else { 15 | if let pvc = vc?.presentedViewController { 16 | return UIWindow.getVisibleViewControllerFrom(vc: pvc) 17 | } else { 18 | return vc 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MVC/Alarm/WeekdaysViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class WeekdaysViewController: UITableViewController { 4 | 5 | var weekdays: [Int] = [Int]() 6 | 7 | override func viewDidLoad() { 8 | super.viewDidLoad() 9 | } 10 | 11 | override func viewWillDisappear(_ animated: Bool) { 12 | performSegue(withIdentifier: Identifier.weekdaysUnwindIdentifier, sender: self) 13 | } 14 | 15 | override func didReceiveMemoryWarning() { 16 | super.didReceiveMemoryWarning() 17 | // Dispose of any resources that can be recreated. 18 | } 19 | 20 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 21 | let cell = super.tableView(tableView, cellForRowAt: indexPath) 22 | 23 | for weekday in weekdays { 24 | if weekday == indexPath.row { 25 | cell.accessoryType = UITableViewCellAccessoryType.checkmark 26 | } 27 | } 28 | return cell 29 | } 30 | 31 | 32 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 33 | let cell = tableView.cellForRow(at: indexPath)! 34 | 35 | if let index = weekdays.index(of: (indexPath.row)) { 36 | weekdays.remove(at: index) 37 | cell.setSelected(true, animated: true) 38 | cell.setSelected(false, animated: true) 39 | cell.accessoryType = UITableViewCellAccessoryType.none 40 | } 41 | else{ 42 | weekdays.append(indexPath.row) 43 | cell.setSelected(true, animated: true) 44 | cell.setSelected(false, animated: true) 45 | cell.accessoryType = UITableViewCellAccessoryType.checkmark 46 | } 47 | } 48 | } 49 | 50 | 51 | extension WeekdaysViewController { 52 | static func repeatText(weekdays: [Int]) -> String { 53 | if weekdays.count == 7 { 54 | return "Every day" 55 | } 56 | 57 | if weekdays.isEmpty { 58 | return "Never" 59 | } 60 | 61 | var weekdaysSorted = weekdays.sorted(by: <) 62 | // Does swift has static cached emtpy string? 63 | var ret = "" 64 | for day in weekdaysSorted { 65 | ret += weekdaysText[day] 66 | } 67 | return ret 68 | } 69 | } 70 | 71 | fileprivate extension WeekdaysViewController { 72 | static let weekdaysText = ["Sun ", "Mon ", "Tue ", "Wed ", "Thu ", "Fri ", "Sat "] 73 | } 74 | -------------------------------------------------------------------------------- /MVC/Alarm/bell.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natsu1211/Alarm-ios-swift/9575d135a60c4a9e5f78041cf00c77bbbb93a83d/MVC/Alarm/bell.mp3 -------------------------------------------------------------------------------- /MVC/Alarm/tickle.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natsu1211/Alarm-ios-swift/9575d135a60c4a9e5f78041cf00c77bbbb93a83d/MVC/Alarm/tickle.mp3 -------------------------------------------------------------------------------- /MVC/AlarmTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /MVC/AlarmTests/MainAlarmViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Alarm_ios_swift 3 | 4 | 5 | func constructTestingViews(navDelegate: UINavigationControllerDelegate) -> (UIStoryboard, AppDelegate, UINavigationController, MainAlarmViewController) { 6 | let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) 7 | 8 | let navigationController = storyboard.instantiateViewController(withIdentifier: "navController") as! UINavigationController 9 | navigationController.delegate = navDelegate 10 | 11 | let mainAlarmViewController = navigationController.viewControllers.first as! MainAlarmViewController 12 | mainAlarmViewController.loadViewIfNeeded() 13 | 14 | let window = UIWindow() 15 | window.rootViewController = mainAlarmViewController 16 | let appDelegate = AppDelegate() 17 | appDelegate.window = window 18 | 19 | window.makeKeyAndVisible() 20 | return (storyboard, appDelegate, navigationController, mainAlarmViewController) 21 | } 22 | 23 | final class MainAlarmViewControllerTests: XCTestCase, UINavigationControllerDelegate { 24 | 25 | var storyboard: UIStoryboard! = nil 26 | var appDelegate: AppDelegate! = nil 27 | var navigationController: UINavigationController! = nil 28 | var mainAlarmViewController: MainAlarmViewController! = nil 29 | 30 | override func setUpWithError() throws { 31 | // Put setup code here. This method is called before the invocation of each test method in the class. 32 | let tuple = constructTestingViews(navDelegate: self) 33 | storyboard = tuple.0 34 | appDelegate = tuple.1 35 | navigationController = tuple.2 36 | mainAlarmViewController = tuple.3 37 | } 38 | 39 | func testStartupConfiguration() { 40 | let viewControllers = navigationController.viewControllers 41 | XCTAssert(viewControllers.first as? MainAlarmViewController == mainAlarmViewController) 42 | 43 | let navigationItemTitle = mainAlarmViewController.navigationItem.title 44 | XCTAssert(navigationItemTitle == "Alarm") 45 | 46 | XCTAssertNil(mainAlarmViewController.navigationItem.leftBarButtonItem) 47 | let rightButton = mainAlarmViewController.navigationItem.rightBarButtonItem 48 | XCTAssertNotNil(rightButton) 49 | XCTAssert(rightButton?.target?.identifier == "addSegue") 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /MVC/AlarmTests/SchedulerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Alarm_ios_swift 3 | 4 | final class SchedulerTests: XCTestCase { 5 | 6 | let scheduler = NotificationScheduler() 7 | 8 | func testCompareWeekdays() { 9 | XCTAssertEqual(scheduler.compare(weekday: 1, with: 1), NotificationScheduler.weekdaysComparisonResult.same) 10 | XCTAssertEqual(scheduler.compare(weekday: 2, with: 2), NotificationScheduler.weekdaysComparisonResult.same) 11 | XCTAssertEqual(scheduler.compare(weekday: 6, with: 1), NotificationScheduler.weekdaysComparisonResult.before) 12 | XCTAssertEqual(scheduler.compare(weekday: 2, with: 3), NotificationScheduler.weekdaysComparisonResult.before) 13 | XCTAssertEqual(scheduler.compare(weekday: 3, with: 2), NotificationScheduler.weekdaysComparisonResult.after) 14 | XCTAssertEqual(scheduler.compare(weekday: 1, with: 7), NotificationScheduler.weekdaysComparisonResult.after) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alarm-ios-swift 2 | 3 | Partially clone of Apple's built-in alarm app in swift. 4 | 5 | - Almost the same UI as the Apple's official alarm app (old version) 6 | - In newer IOS, Apple changed the style of DatePicker, which I think too small to click. 7 | - No new added HealthKit functionality. 8 | - Add, Delete, Edit Alarm (One shot and Repeating). Snooze. 9 | - Local ringtone selection only. No access to music app library. 10 | - Text not localized. 11 | 12 | As far as I know, it's impossible to make a full clone of official alarm app using existing APIs. 13 | The major issues are, 14 | - Third-party alarm app rely on notification to notify user, whether local or remote. However, notifacation cannot override the ringer switch behaviour nor can they override “Do Not Disturb” mode, which means your alarm app may could not even make any sound. Moreover, User may disallow your app to send notificaiton, which make the alarm app just useless. 15 | - Notification cannot wake up the app, it depends on whether user response to the notifacation to bring your alarm app to foreground. moreover, Notification sounds have a maximum duration of 30 seconds. This means that once the 30 seconds is up, no more sound. If user just ignore or failed to reponse before the notification dismissed, the alarm app could not do anything more. No snooze, no sound, which is critical to an alarm app. 16 | - Applications cannot set the volume above or below the devices set volume, nor can it suppress the sounds of other applications. 17 | 18 | however, I believe you can still get something useful from this project, if you are new to ios app development. 19 | - How to organize app in MVC or MVVM architecture, which means carefully design the data flow of your app and make your codebase easy to maintain. 20 | - Though a lot of new architectures there to choose nowadays, MVC or MVVM is still the one easy to apply to your existing code and useful. 21 | - How to schedule and handle local notification correctly. 22 | - How to use UITableView. 23 | - Storyboard-based view transition. 24 | 25 | ## Demo 26 | 27 | 28 | ## Branch 29 | `main` 30 | Main branch. Contains implementations in MVC architecture. 31 | 32 | `below-ios10.0` 33 | Using old local notification APIs deprecated since IOS 10.0. 34 | Behaves almost the same as new APIs. 35 | *Caution: Not well tested and not maintained anymore. Reference use only.* 36 | 37 | ## Technical Details 38 | ### MVC 39 | When we develop iOS applications using the UIKit, our program is essentially already in the MVC architecture. However, simply organizing our application into the three parts of View, Model, and Controller does not necessarily make our application easier to maintain. We also need to consider how and when the Model changes when a user interacts with the UI, as well as other parts of the UI. 40 | 41 | Take this alarm clock application as an example. Users can delete an alarm on the main scene, and they can also delete it on the editing scene. These two interfaces have different Controllers, so naturally, they have different code to handle the user's delete action. The most straightforward implementation would be to add the corresponding processing code (such as delete, updating the TableView, updating Notifications, etc.) wherever a user might delete the alarm. 42 | However, this often results in duplicated code. In more complex Apps, users might have multiple ways to achieve a function. If we just add similar handling code wherever user input is processed, then redundant code would be scattered everywhere. 43 | 44 | But take a while, we can find no matter how the user deletes the alarm, what we ultimately need to do is remove the corresponding Alarm from our Model. We can set some rules for ourselves, or rather, change the order of processing. That is, we don't changed the model and view in the same place. We handle the user actions, only change the state of the Model, and other UI need to change are automatically adjusted by observing changes of the Model. Moreover, the operations that can be performed on the Model (such as add, delete, update) are often far fewer than the actions a user can make on the interface. This means we need even less code to update the UI in one place, which makes our codebase easier to maintain. That's the basic thinking of MVC and lots of other architectures. 45 | 46 | 47 | #### Model 48 | The Model of this App is straightforward, comprising just three classes. 49 | 50 | The `Alarm` class represents a single Alarm and defines various attributes needed for an individual Alarm. 51 | 52 | The `Alarms` class represents all alarms, and internally, it simply contains an array of Alarm instances. The `Alarms` class also defines several helper methods, such as `add`, `remove`, and `update`, which are called in the handler of user's operations on the Alarm. 53 | 54 | The `Store` class is responsible for serializing and deserializing the `Alarms` class, allowing us to save and load alarms added by the user. This ensures that added and edited Alarms don't disappear and are displayed correctly when the app is opened next time. 55 | In terms of implementation details, since we implemented the `Codable` Protocol for the `Alarm` and `Alarms` classes, we can use `JSONEncoder` and `JSONDecoder` to easily serialize the `Alarm` and Alarms classes into JSON strings. These JSON strings are then saved to the phone's storage space via UserDefaults. When the Store class is instantiated, it is also responsible for retrieving the serialized Alarms from UserDefaults, deserializing them, and constructing instance of `Alarms`. Throughout the entire lifecycle of the app, there is only one instance of both Store and Alarms. To be clear, when we write something like `let alarms = Store.shared.alarms`, we are just copy the reference to the instance, not creating a new instance. 56 | 57 | 58 | #### View 59 | Regarding the View, there isn't much to say. The View in this app is entirely based on Storyboard. We can intuitively layout the UI in the Storyboard and also set up the transitions between different scenes (Apple calls it "Segue"). 60 | 61 | Setting aside SwiftUI, even though we can also use XIB and pure code to construct the UI, relying mainly on Storyboard is usually a better choice. We can prepare everything in advance, and when a scene transition is needed, we simply call the `performSegue` function. One point worth mentioning is, if you're puzzled about how to pass required data during transitions, consider defining the data to be passed as properties of the target ViewController. Then, in the `prepare` function (which gets invoked before the `performSegue` function), you can retrieve the instance of the target ViewController and directly assign values to these properties. 62 | 63 | #### Controller 64 | In an MVC iOS app, the ViewController acts as the Controller. It holds reference to the View and is responsible for processing user actions. At the same time, it also holds reference to the Model and can make changes to it. As mentioned earlier, the key to making MVC successful is that, within the handler processing user actions, we only make changes to the Model, rather than directly altering other UI elements. Other UI elements are updated by monitoring changes to the Model. 65 | 66 | While there are many ways to achieve this goal, in this app, we use the `NotificationCenter` from `Foundation` to do so. Within this app, when the Model has a change, it will post a notification (not a Local Notification) through the `Store` class to inform observers. This notification contains details on which Alarm changed and the reason of the change (whether it was added, deleted, or updated). Observers of the notification can then process accordingly. You can find the relevant code in the `MainAlarmViewController`. 67 | 68 | The crucial part involves the subscription code, 69 | ```swift 70 | NotificationCenter.default.addObserver(self, selector: #selector(handleChangeNotification(_:)), name: Store.changedNotification, object: nil) 71 | ``` 72 | and the subsequent handling after receiving a notification. 73 | ```swift 74 | @objc func handleChangeNotification(_ notification: Notification) { 75 | 76 | guard let userInfo = notification.userInfo else { 77 | return 78 | } 79 | 80 | // Handle changes to contents 81 | if let changeReason = userInfo[Alarm.changeReasonKey] as? String { 82 | let newValue = userInfo[Alarm.newValueKey] 83 | let oldValue = userInfo[Alarm.oldValueKey] 84 | switch (changeReason, newValue, oldValue) { 85 | case let (Alarm.removed, (uuid as String)?, (oldValue as Int)?): 86 | tableView.deleteRows(at: [IndexPath(row: oldValue, section: 0)], with: .fade) 87 | if alarms.count == 0 { 88 | self.navigationItem.leftBarButtonItem = nil 89 | } 90 | scheduler.cancelNotification(ByUUIDStr: uuid) 91 | case let (Alarm.added, (index as Int)?, _): 92 | tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .fade) 93 | self.navigationItem.leftBarButtonItem = editButtonItem 94 | let alarm = alarms[index] 95 | scheduler.setNotification(date: alarm.date, ringtoneName: alarm.mediaLabel, repeatWeekdays: alarm.repeatWeekdays, snoozeEnabled: alarm.snoozeEnabled, onSnooze: false, uuid: alarm.uuid.uuidString) 96 | case let (Alarm.updated, (index as Int)?, _): 97 | let alarm = alarms[index] 98 | let uuid = alarm.uuid.uuidString 99 | if alarm.enabled { 100 | scheduler.updateNotification(ByUUIDStr: uuid, date: alarm.date, ringtoneName: alarm.mediaLabel, repeatWeekdays: alarm.repeatWeekdays, snoonzeEnabled: alarm.snoozeEnabled) 101 | } else { 102 | scheduler.cancelNotification(ByUUIDStr: uuid) 103 | } 104 | tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .fade) 105 | default: tableView.reloadData() 106 | } 107 | } else { 108 | tableView.reloadData() 109 | } 110 | } 111 | ``` 112 | We only need to update the UITableView within this function, eliminating the need to write UITableView update code in every view action handler. 113 | 114 | #### Local Notification 115 | As previously mentioned, this app uses Local Notification to alert users when the alarm time is reached. 116 | 117 | Local Notifications are dispatched at predefined dates and times to present content that the user might be interested in. Upon receiving a Local Notification, the system will display it differently depending on the current state of the phone. Subsequently, based on how users respond to the notification, the app's required actions may differ. For instance, when the phone is locked, the notification will appear in the Notification Center. However, when the phone is unlocked, it will show as a banner at the top of the screen (assuming the user has granted the app permission to send notifications and to display them in this manner). 118 | 119 | Additionally, starting from iOS 8.0, Local Notification can include Actions, allowing users to respond directly without opening the app. Initially, Actions could only accompany buttons, but in recent versions of iOS, notifications can even include images and videos. In this app, we utilize the Action feature to allow users to turn off the alarm or snooze it. A Action must belongs to a `UNNotificationCategory`, and every Local Notification will have a category, let the system knows which Actions the Notification should have. 120 | 121 | The app handle Local Notification at `AppDelegate`. When our app is in the foreground, we can directly handle Notifications within `func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)`. At this case, we use `AVAudioPlayer` to play the ringtone in an infinite loop until the user responds. In this scenario, our app can function as an alarm clock. However, in practical use, users are unlikely to always keep an alarm clock app in the foreground. 122 | When in the background, several situations might arise. Firstly, as mentioned earlier, when the app is in the background and system receives a notification sent by it, the notification will appear as a banner at the top of screen, which only lasts for a few seconds. If the user taps on the banner before it disappears, our app will be brought to the foreground, reverting to the situation described above. 123 | If the user long-presses the banner, it will display the Actions associated with that notification. Here, we have set two Actions: Snooze and OK. If the user taps on the Snooze button, it essentially schedules a Local Notification to be sent 9 minutes later. Choosing OK simply turns off that Alarm. Actions can be handled within `func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)`. 124 | If the user doesn't manage to handle the notification in time or ignores it, we can't do any further processing. After the longest 30-second ringtone ends, users won't hear any more sound. In any case, we can't replicate the actions of the built-in alarm clock app. we can only try to simulate its functions through some workarounds. 125 | 126 | It's important to note that any app intending to send Local Notifications must first obtain the user's permission. In this app, we request permission to send notifications the first time the app is launched, by calling `requestAuthorization()`. The system will prompt the user with an alert window, asking them to choose whether or not to grant this permission. If the user accidentally clicks "No," then the app will be unable to send Local Notifications, make it useless. This is also one of the reasons why third-party alarm clock apps can't function the same way as the built-in system ones. It's a feature of iOS, and there's not much we can do about it (although you might consider prompting the user with a pop-up explaining why this permission is so crucial, in hopes that they might go to settings and change the permission setting). 127 | 128 | The methods for requesting user permissions and scheduling notifications may vary based on the iOS version. After iOS 10, Apple redesigned the Notification-related APIs. The `main` branch contains the latest approach, while the `below-ios10.0` branch includes the methods used before iOS 10.0. Of course, given that iOS 10.0 is quite dated now, you likely won't need to know the older methods. Implementations related to Local Notification can be found at `NotificationScheduler.Swift`. 129 | 130 | ## License 131 | MIT 132 | 133 | --------------------------------------------------------------------------------