diff --git a/Multimer/Multimer.xcodeproj/project.pbxproj b/Multimer/Multimer.xcodeproj/project.pbxproj index 854fb9b..a8d47c0 100644 --- a/Multimer/Multimer.xcodeproj/project.pbxproj +++ b/Multimer/Multimer.xcodeproj/project.pbxproj @@ -7,6 +7,43 @@ objects = { /* Begin PBXBuildFile section */ + 1E1C42E22A765844003CC427 /* RingtoneSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C42E12A765844003CC427 /* RingtoneSelectViewController.swift */; }; + 1E1C43662A7681FA003CC427 /* radar.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43522A7681F8003CC427 /* radar.aiff */; }; + 1E1C43672A7681FA003CC427 /* alarm.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43532A7681F9003CC427 /* alarm.aiff */; }; + 1E1C43682A7681FA003CC427 /* presto.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43542A7681F9003CC427 /* presto.aiff */; }; + 1E1C43692A7681FA003CC427 /* marimba.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43552A7681F9003CC427 /* marimba.aiff */; }; + 1E1C436A2A7681FA003CC427 /* sencha.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43562A7681F9003CC427 /* sencha.aiff */; }; + 1E1C436B2A7681FA003CC427 /* signal.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43572A7681F9003CC427 /* signal.aiff */; }; + 1E1C436C2A7681FA003CC427 /* trill.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43582A7681F9003CC427 /* trill.aiff */; }; + 1E1C436D2A7681FA003CC427 /* xylophone.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43592A7681F9003CC427 /* xylophone.aiff */; }; + 1E1C436E2A7681FA003CC427 /* stargaze.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C435A2A7681F9003CC427 /* stargaze.aiff */; }; + 1E1C43702A7681FA003CC427 /* illuminate.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C435C2A7681F9003CC427 /* illuminate.aiff */; }; + 1E1C43712A7681FA003CC427 /* pianoRiff.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C435D2A7681F9003CC427 /* pianoRiff.aiff */; }; + 1E1C43722A7681FA003CC427 /* bulletin.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C435E2A7681F9003CC427 /* bulletin.aiff */; }; + 1E1C43732A7681FA003CC427 /* pinball.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C435F2A7681F9003CC427 /* pinball.aiff */; }; + 1E1C43742A7681FA003CC427 /* beacon.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43602A7681FA003CC427 /* beacon.aiff */; }; + 1E1C43752A7681FA003CC427 /* bark.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43612A7681FA003CC427 /* bark.aiff */; }; + 1E1C43762A7681FA003CC427 /* oldPhone.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43622A7681FA003CC427 /* oldPhone.aiff */; }; + 1E1C43772A7681FA003CC427 /* duck.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43632A7681FA003CC427 /* duck.aiff */; }; + 1E1C43782A7681FA003CC427 /* reflection.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43642A7681FA003CC427 /* reflection.aiff */; }; + 1E1C43792A7681FA003CC427 /* strum.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43652A7681FA003CC427 /* strum.aiff */; }; + 1E1C437B2A768626003CC427 /* Ringtone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C437A2A768626003CC427 /* Ringtone.swift */; }; + 1E1C43A22A76A36B003CC427 /* default7.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C439A2A76A36A003CC427 /* default7.aiff */; }; + 1E1C43A32A76A36B003CC427 /* default1.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C439B2A76A36A003CC427 /* default1.aiff */; }; + 1E1C43A42A76A36B003CC427 /* default6.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C439C2A76A36A003CC427 /* default6.aiff */; }; + 1E1C43A52A76A36B003CC427 /* default8.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C439D2A76A36A003CC427 /* default8.aiff */; }; + 1E1C43A72A76A36B003CC427 /* default2.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C439F2A76A36A003CC427 /* default2.aiff */; }; + 1E1C43A82A76A36B003CC427 /* default4.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43A02A76A36B003CC427 /* default4.aiff */; }; + 1E1C43A92A76A36B003CC427 /* default5.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43A12A76A36B003CC427 /* default5.aiff */; }; + 1E1C43AB2A76A39F003CC427 /* default3.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 1E1C43AA2A76A39F003CC427 /* default3.aiff */; }; + 1E1C43AD2A77B801003CC427 /* RingtoneSelectReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43AC2A77B801003CC427 /* RingtoneSelectReactor.swift */; }; + 1E1C43B02A7A7775003CC427 /* RingtoneCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43AF2A7A7775003CC427 /* RingtoneCellModel.swift */; }; + 1E1C43B22A7A7781003CC427 /* RingtoneSelectTableViewDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43B12A7A7781003CC427 /* RingtoneSelectTableViewDiffableDataSource.swift */; }; + 1E1C43B42A7A77A0003CC427 /* RingtoneType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43B32A7A77A0003CC427 /* RingtoneType.swift */; }; + 1E1C43B62A7A8EB8003CC427 /* RingtoneViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43B52A7A8EB8003CC427 /* RingtoneViewCell.swift */; }; + 1E1C43B92A7A8F16003CC427 /* UserNotificationCenterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43B82A7A8F16003CC427 /* UserNotificationCenterService.swift */; }; + 1E1C43BB2A7A904E003CC427 /* RingtoneButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43BA2A7A904E003CC427 /* RingtoneButton.swift */; }; + 1E1C43BD2A7A90E7003CC427 /* UIButton+Rx+configurationTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1C43BC2A7A90E7003CC427 /* UIButton+Rx+configurationTitle.swift */; }; 1E23D5352994B9F9008FD287 /* ToolbarType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23D5342994B9F9008FD287 /* ToolbarType.swift */; }; 1E23D5372994BCF8008FD287 /* NotificationName+CustomName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23D5362994BCF8008FD287 /* NotificationName+CustomName.swift */; }; 1E23D53D2994F8FB008FD287 /* CoreDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23D53C2994F8FB008FD287 /* CoreDataError.swift */; }; @@ -48,7 +85,7 @@ 1E651DAC2953165800B4F321 /* CountUpTimerUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E651DAB2953165800B4F321 /* CountUpTimerUseCase.swift */; }; 1E651DAE2953168900B4F321 /* TimerUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E651DAD2953168900B4F321 /* TimerUseCase.swift */; }; 1E651DB0295342BA00B4F321 /* TimerCreateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E651DAF295342BA00B4F321 /* TimerCreateViewController.swift */; }; - 1E651DB229534CEE00B4F321 /* TimerCreateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E651DB129534CEE00B4F321 /* TimerCreateViewModel.swift */; }; + 1E651DB229534CEE00B4F321 /* TimerCreateReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E651DB129534CEE00B4F321 /* TimerCreateReactor.swift */; }; 1E6A19C329937FE8001FA3ED /* NameTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6A19C229937FE8001FA3ED /* NameTextField.swift */; }; 1E6A19C82993AF21001FA3ED /* SwipeRightToStopNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6A19C72993AF21001FA3ED /* SwipeRightToStopNoticeView.swift */; }; 1E6FAD9729403F900021B845 /* CoreDataTimerRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6FAD9629403F900021B845 /* CoreDataTimerRepository.swift */; }; @@ -92,7 +129,7 @@ 1EBEEFE82925B73000299CE7 /* TagColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBEEFE72925B73000299CE7 /* TagColor.swift */; }; 1EBEEFEA2925B85700299CE7 /* TagButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBEEFE92925B85700299CE7 /* TagButton.swift */; }; 1EBEF029292909DD00299CE7 /* TimerModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1EBEF027292909DD00299CE7 /* TimerModel.xcdatamodeld */; }; - 1EBFEE152919270D00747606 /* TimerEditingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBFEE142919270D00747606 /* TimerEditingViewModel.swift */; }; + 1EBFEE152919270D00747606 /* TimerEditingReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EBFEE142919270D00747606 /* TimerEditingReactor.swift */; }; 7DE0DCB2BD437A3652CA4266 /* Pods_Multimer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB67573CFCDE896B6E52525A /* Pods_Multimer.framework */; }; 8C88084EF84FB8081FC09CDE /* Pods_MultimerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02A9A991A8E30450D4458665 /* Pods_MultimerTests.framework */; }; C60ADD68060EFDAE377DA4B2 /* Pods_Multimer_MultimerUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09D9588E5DAECA7EB7A30024 /* Pods_Multimer_MultimerUITests.framework */; }; @@ -120,6 +157,43 @@ 09D9588E5DAECA7EB7A30024 /* Pods_Multimer_MultimerUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Multimer_MultimerUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1E1C42DF2A760939003CC427 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; 1E1C42E02A760939003CC427 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 1E1C42E12A765844003CC427 /* RingtoneSelectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingtoneSelectViewController.swift; sourceTree = ""; }; + 1E1C43522A7681F8003CC427 /* radar.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = radar.aiff; sourceTree = ""; }; + 1E1C43532A7681F9003CC427 /* alarm.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = alarm.aiff; sourceTree = ""; }; + 1E1C43542A7681F9003CC427 /* presto.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = presto.aiff; sourceTree = ""; }; + 1E1C43552A7681F9003CC427 /* marimba.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = marimba.aiff; sourceTree = ""; }; + 1E1C43562A7681F9003CC427 /* sencha.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = sencha.aiff; sourceTree = ""; }; + 1E1C43572A7681F9003CC427 /* signal.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = signal.aiff; sourceTree = ""; }; + 1E1C43582A7681F9003CC427 /* trill.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = trill.aiff; sourceTree = ""; }; + 1E1C43592A7681F9003CC427 /* xylophone.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = xylophone.aiff; sourceTree = ""; }; + 1E1C435A2A7681F9003CC427 /* stargaze.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = stargaze.aiff; sourceTree = ""; }; + 1E1C435C2A7681F9003CC427 /* illuminate.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = illuminate.aiff; sourceTree = ""; }; + 1E1C435D2A7681F9003CC427 /* pianoRiff.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = pianoRiff.aiff; sourceTree = ""; }; + 1E1C435E2A7681F9003CC427 /* bulletin.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = bulletin.aiff; sourceTree = ""; }; + 1E1C435F2A7681F9003CC427 /* pinball.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = pinball.aiff; sourceTree = ""; }; + 1E1C43602A7681FA003CC427 /* beacon.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = beacon.aiff; sourceTree = ""; }; + 1E1C43612A7681FA003CC427 /* bark.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = bark.aiff; sourceTree = ""; }; + 1E1C43622A7681FA003CC427 /* oldPhone.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = oldPhone.aiff; sourceTree = ""; }; + 1E1C43632A7681FA003CC427 /* duck.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = duck.aiff; sourceTree = ""; }; + 1E1C43642A7681FA003CC427 /* reflection.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = reflection.aiff; sourceTree = ""; }; + 1E1C43652A7681FA003CC427 /* strum.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = strum.aiff; sourceTree = ""; }; + 1E1C437A2A768626003CC427 /* Ringtone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ringtone.swift; sourceTree = ""; }; + 1E1C439A2A76A36A003CC427 /* default7.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default7.aiff; sourceTree = ""; }; + 1E1C439B2A76A36A003CC427 /* default1.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default1.aiff; sourceTree = ""; }; + 1E1C439C2A76A36A003CC427 /* default6.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default6.aiff; sourceTree = ""; }; + 1E1C439D2A76A36A003CC427 /* default8.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default8.aiff; sourceTree = ""; }; + 1E1C439F2A76A36A003CC427 /* default2.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default2.aiff; sourceTree = ""; }; + 1E1C43A02A76A36B003CC427 /* default4.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default4.aiff; sourceTree = ""; }; + 1E1C43A12A76A36B003CC427 /* default5.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default5.aiff; sourceTree = ""; }; + 1E1C43AA2A76A39F003CC427 /* default3.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = default3.aiff; sourceTree = ""; }; + 1E1C43AC2A77B801003CC427 /* RingtoneSelectReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingtoneSelectReactor.swift; sourceTree = ""; }; + 1E1C43AF2A7A7775003CC427 /* RingtoneCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingtoneCellModel.swift; sourceTree = ""; }; + 1E1C43B12A7A7781003CC427 /* RingtoneSelectTableViewDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingtoneSelectTableViewDiffableDataSource.swift; sourceTree = ""; }; + 1E1C43B32A7A77A0003CC427 /* RingtoneType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingtoneType.swift; sourceTree = ""; }; + 1E1C43B52A7A8EB8003CC427 /* RingtoneViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingtoneViewCell.swift; sourceTree = ""; }; + 1E1C43B82A7A8F16003CC427 /* UserNotificationCenterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterService.swift; sourceTree = ""; }; + 1E1C43BA2A7A904E003CC427 /* RingtoneButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingtoneButton.swift; sourceTree = ""; }; + 1E1C43BC2A7A90E7003CC427 /* UIButton+Rx+configurationTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Rx+configurationTitle.swift"; sourceTree = ""; }; 1E23D5342994B9F9008FD287 /* ToolbarType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarType.swift; sourceTree = ""; }; 1E23D5362994BCF8008FD287 /* NotificationName+CustomName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+CustomName.swift"; sourceTree = ""; }; 1E23D53A2994EEF3008FD287 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -173,7 +247,7 @@ 1E651DAB2953165800B4F321 /* CountUpTimerUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountUpTimerUseCase.swift; sourceTree = ""; }; 1E651DAD2953168900B4F321 /* TimerUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerUseCase.swift; sourceTree = ""; }; 1E651DAF295342BA00B4F321 /* TimerCreateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerCreateViewController.swift; sourceTree = ""; }; - 1E651DB129534CEE00B4F321 /* TimerCreateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerCreateViewModel.swift; sourceTree = ""; }; + 1E651DB129534CEE00B4F321 /* TimerCreateReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerCreateReactor.swift; sourceTree = ""; }; 1E6A19C229937FE8001FA3ED /* NameTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameTextField.swift; sourceTree = ""; }; 1E6A19C72993AF21001FA3ED /* SwipeRightToStopNoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeRightToStopNoticeView.swift; sourceTree = ""; }; 1E6FAD9629403F900021B845 /* CoreDataTimerRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataTimerRepository.swift; sourceTree = ""; }; @@ -217,7 +291,7 @@ 1EBEEFE72925B73000299CE7 /* TagColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagColor.swift; sourceTree = ""; }; 1EBEEFE92925B85700299CE7 /* TagButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagButton.swift; sourceTree = ""; }; 1EBEF028292909DD00299CE7 /* TimerModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TimerModel.xcdatamodel; sourceTree = ""; }; - 1EBFEE142919270D00747606 /* TimerEditingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerEditingViewModel.swift; sourceTree = ""; }; + 1EBFEE142919270D00747606 /* TimerEditingReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerEditingReactor.swift; sourceTree = ""; }; 2487B77C56F24FE0EFA05BD4 /* Pods-Multimer.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Multimer.release.xcconfig"; path = "Target Support Files/Pods-Multimer/Pods-Multimer.release.xcconfig"; sourceTree = ""; }; 50F7E195491911071C2CCC09 /* Pods-MultimerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultimerTests.debug.xcconfig"; path = "Target Support Files/Pods-MultimerTests/Pods-MultimerTests.debug.xcconfig"; sourceTree = ""; }; 6AE47EFBE8192C59957931B9 /* Pods-Multimer.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Multimer.debug.xcconfig"; path = "Target Support Files/Pods-Multimer/Pods-Multimer.debug.xcconfig"; sourceTree = ""; }; @@ -268,6 +342,60 @@ path = Pods; sourceTree = ""; }; + 1E1C42E52A766C28003CC427 /* Ringtones */ = { + isa = PBXGroup; + children = ( + 1E1C43532A7681F9003CC427 /* alarm.aiff */, + 1E1C43612A7681FA003CC427 /* bark.aiff */, + 1E1C43602A7681FA003CC427 /* beacon.aiff */, + 1E1C435E2A7681F9003CC427 /* bulletin.aiff */, + 1E1C439B2A76A36A003CC427 /* default1.aiff */, + 1E1C439F2A76A36A003CC427 /* default2.aiff */, + 1E1C43AA2A76A39F003CC427 /* default3.aiff */, + 1E1C43A02A76A36B003CC427 /* default4.aiff */, + 1E1C43A12A76A36B003CC427 /* default5.aiff */, + 1E1C439C2A76A36A003CC427 /* default6.aiff */, + 1E1C439A2A76A36A003CC427 /* default7.aiff */, + 1E1C439D2A76A36A003CC427 /* default8.aiff */, + 1E1C43632A7681FA003CC427 /* duck.aiff */, + 1E1C435C2A7681F9003CC427 /* illuminate.aiff */, + 1E1C43552A7681F9003CC427 /* marimba.aiff */, + 1E1C43622A7681FA003CC427 /* oldPhone.aiff */, + 1E1C435D2A7681F9003CC427 /* pianoRiff.aiff */, + 1E1C435F2A7681F9003CC427 /* pinball.aiff */, + 1E1C43542A7681F9003CC427 /* presto.aiff */, + 1E1C43522A7681F8003CC427 /* radar.aiff */, + 1E1C43642A7681FA003CC427 /* reflection.aiff */, + 1E1C43562A7681F9003CC427 /* sencha.aiff */, + 1E1C43572A7681F9003CC427 /* signal.aiff */, + 1E1C435A2A7681F9003CC427 /* stargaze.aiff */, + 1E1C43652A7681FA003CC427 /* strum.aiff */, + 1E1C43582A7681F9003CC427 /* trill.aiff */, + 1E1C43592A7681F9003CC427 /* xylophone.aiff */, + ); + path = Ringtones; + sourceTree = ""; + }; + 1E1C43AE2A79030B003CC427 /* RingtoneSelect */ = { + isa = PBXGroup; + children = ( + 1E1C42E12A765844003CC427 /* RingtoneSelectViewController.swift */, + 1E1C43AC2A77B801003CC427 /* RingtoneSelectReactor.swift */, + 1E1C43AF2A7A7775003CC427 /* RingtoneCellModel.swift */, + 1E1C43B12A7A7781003CC427 /* RingtoneSelectTableViewDiffableDataSource.swift */, + 1E1C43B52A7A8EB8003CC427 /* RingtoneViewCell.swift */, + ); + path = RingtoneSelect; + sourceTree = ""; + }; + 1E1C43B72A7A8F0E003CC427 /* Service */ = { + isa = PBXGroup; + children = ( + 1E1C43B82A7A8F16003CC427 /* UserNotificationCenterService.swift */, + ); + path = Service; + sourceTree = ""; + }; 1E23D53F2994FA17008FD287 /* SettingFooterView */ = { isa = PBXGroup; children = ( @@ -322,6 +450,7 @@ isa = PBXGroup; children = ( 1E48FEA62911F9250090B246 /* Info.plist */, + 1E1C42E52A766C28003CC427 /* Ringtones */, 1E4CB4E32967353E00FC918E /* Localizing */, 1E482BE42913556100175B78 /* Application */, 1E4CB4CC2966BAEA00FC918E /* Presentation */, @@ -357,6 +486,7 @@ 1E4CB4CF2966BC9900FC918E /* Home */, 1E4CB4D12966BCD800FC918E /* TimerCreate */, 1E4CB4D02966BCA400FC918E /* TimerEditing */, + 1E1C43AE2A79030B003CC427 /* RingtoneSelect */, ); path = Presentation; sourceTree = ""; @@ -373,6 +503,7 @@ 1E4CB4CE2966BC8400FC918E /* Data */ = { isa = PBXGroup; children = ( + 1E1C43B72A7A8F0E003CC427 /* Service */, 1E4CB4D62966BD9B00FC918E /* Repository */, 1E4CB4D72966BE0700FC918E /* Persistence */, ); @@ -396,7 +527,7 @@ isa = PBXGroup; children = ( 1E482BCF2912887B00175B78 /* TimerEditingViewController.swift */, - 1EBFEE142919270D00747606 /* TimerEditingViewModel.swift */, + 1EBFEE142919270D00747606 /* TimerEditingReactor.swift */, 1E4CB4E82967395200FC918E /* TimePickerView */, 1E4CB4E7296738C100FC918E /* CustomView */, ); @@ -407,7 +538,7 @@ isa = PBXGroup; children = ( 1E651DAF295342BA00B4F321 /* TimerCreateViewController.swift */, - 1E651DB129534CEE00B4F321 /* TimerCreateViewModel.swift */, + 1E651DB129534CEE00B4F321 /* TimerCreateReactor.swift */, ); path = TimerCreate; sourceTree = ""; @@ -535,6 +666,7 @@ 1E744B91296C488A00CD824D /* UIPickerView+setFixedLabels.swift */, 1E744B92296C488A00CD824D /* UITextField+addLeftPadding.swift */, 1E744B94296C488A00CD824D /* UITextField+Rx+textChanged.swift */, + 1E1C43BC2A7A90E7003CC427 /* UIButton+Rx+configurationTitle.swift */, 1E744B93296C488A00CD824D /* TimerTableViewDiffableDataSource+Rx+update.swift */, 1E744B95296C488A00CD824D /* UIImage+makeSFSymbolImage.swift */, 1E744B96296C488A00CD824D /* UIView+snapshotCellStyle.swift */, @@ -555,6 +687,8 @@ 1E744B9D296C488A00CD824D /* EditViewButtonType.swift */, 1E744B9E296C488A00CD824D /* TimeType.swift */, 1E23D5342994B9F9008FD287 /* ToolbarType.swift */, + 1E1C43B32A7A77A0003CC427 /* RingtoneType.swift */, + 1E1C437A2A768626003CC427 /* Ringtone.swift */, ); path = Enum; sourceTree = ""; @@ -574,6 +708,7 @@ 1E744BA3296C488A00CD824D /* PaddingButton.swift */, 1E744BA4296C488A00CD824D /* SymbolImageButton.swift */, 1E6A19C229937FE8001FA3ED /* NameTextField.swift */, + 1E1C43BA2A7A904E003CC427 /* RingtoneButton.swift */, ); path = Common; sourceTree = ""; @@ -795,11 +930,38 @@ buildActionMask = 2147483647; files = ( 1E48FEA52911F9250090B246 /* LaunchScreen.storyboard in Resources */, + 1E1C43732A7681FA003CC427 /* pinball.aiff in Resources */, + 1E1C43792A7681FA003CC427 /* strum.aiff in Resources */, + 1E1C43752A7681FA003CC427 /* bark.aiff in Resources */, + 1E1C43702A7681FA003CC427 /* illuminate.aiff in Resources */, + 1E1C43A92A76A36B003CC427 /* default5.aiff in Resources */, 1E4CB4B4295F258500FC918E /* InfoPlist.strings in Resources */, + 1E1C43A72A76A36B003CC427 /* default2.aiff in Resources */, + 1E1C43762A7681FA003CC427 /* oldPhone.aiff in Resources */, + 1E1C43A42A76A36B003CC427 /* default6.aiff in Resources */, 1E23D569299518F2008FD287 /* swipe-right-to-stop-timer.json in Resources */, + 1E1C43672A7681FA003CC427 /* alarm.aiff in Resources */, + 1E1C436D2A7681FA003CC427 /* xylophone.aiff in Resources */, + 1E1C43712A7681FA003CC427 /* pianoRiff.aiff in Resources */, + 1E1C43662A7681FA003CC427 /* radar.aiff in Resources */, + 1E1C436A2A7681FA003CC427 /* sencha.aiff in Resources */, 1E4CB4A9295EAB6000FC918E /* Localizable.strings in Resources */, + 1E1C436E2A7681FA003CC427 /* stargaze.aiff in Resources */, 1E23D568299518F2008FD287 /* tap.json in Resources */, + 1E1C43742A7681FA003CC427 /* beacon.aiff in Resources */, 1E48FEA22911F9250090B246 /* Assets.xcassets in Resources */, + 1E1C43A32A76A36B003CC427 /* default1.aiff in Resources */, + 1E1C436B2A7681FA003CC427 /* signal.aiff in Resources */, + 1E1C436C2A7681FA003CC427 /* trill.aiff in Resources */, + 1E1C43A82A76A36B003CC427 /* default4.aiff in Resources */, + 1E1C43782A7681FA003CC427 /* reflection.aiff in Resources */, + 1E1C43772A7681FA003CC427 /* duck.aiff in Resources */, + 1E1C43722A7681FA003CC427 /* bulletin.aiff in Resources */, + 1E1C43682A7681FA003CC427 /* presto.aiff in Resources */, + 1E1C43A22A76A36B003CC427 /* default7.aiff in Resources */, + 1E1C43A52A76A36B003CC427 /* default8.aiff in Resources */, + 1E1C43692A7681FA003CC427 /* marimba.aiff in Resources */, + 1E1C43AB2A76A39F003CC427 /* default3.aiff in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -927,30 +1089,36 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E1C43B02A7A7775003CC427 /* RingtoneCellModel.swift in Sources */, 1E4159F02948E7DF00EDA065 /* SettingFooterView.swift in Sources */, + 1E1C43B62A7A8EB8003CC427 /* RingtoneViewCell.swift in Sources */, 1E744BB7296C488A00CD824D /* PaddingButton.swift in Sources */, 1EBEEFDC292105F500299CE7 /* TimerTableViewDiffableDataSource.swift in Sources */, + 1E1C43BB2A7A904E003CC427 /* RingtoneButton.swift in Sources */, 1E651DB0295342BA00B4F321 /* TimerCreateViewController.swift in Sources */, 1E482BD6291353FF00175B78 /* TimePickerViewDelegate.swift in Sources */, - 1E651DB229534CEE00B4F321 /* TimerCreateViewModel.swift in Sources */, + 1E651DB229534CEE00B4F321 /* TimerCreateReactor.swift in Sources */, 1E482BD22913538800175B78 /* TimerViewCell.swift in Sources */, 1E744BAC296C488A00CD824D /* UIImage+makeSFSymbolImage.swift in Sources */, 1E4CB4DE2966BE9000FC918E /* TimerMO+CoreDataClass.swift in Sources */, 1E4CB4AF295EB5EA00FC918E /* LocalizableString.swift in Sources */, 1E482BDC291354F500175B78 /* Time.swift in Sources */, 1E744BB6296C488A00CD824D /* CellIdentifiable.swift in Sources */, + 1E1C43AD2A77B801003CC427 /* RingtoneSelectReactor.swift in Sources */, 1E4CB4CB2961E24500FC918E /* TimePickerView.swift in Sources */, 1EA3FCE4292E49440073560D /* CoreDataStorage+TimerMO.swift in Sources */, 1E4CB4DC2966BE9000FC918E /* TagColorMO+CoreDataClass.swift in Sources */, 1EA3FCE6292E4B360073560D /* CoreDataStorage+TimeMO.swift in Sources */, 1E651DAE2953168900B4F321 /* TimerUseCase.swift in Sources */, 1E6A19C329937FE8001FA3ED /* NameTextField.swift in Sources */, + 1E1C43B92A7A8F16003CC427 /* UserNotificationCenterService.swift in Sources */, 1E744BB1296C488A00CD824D /* TimerTableViewSection.swift in Sources */, 1EBEEFEA2925B85700299CE7 /* TagButton.swift in Sources */, 1E6A19C82993AF21001FA3ED /* SwipeRightToStopNoticeView.swift in Sources */, 1E6FAD9729403F900021B845 /* CoreDataTimerRepository.swift in Sources */, 1E744BA9296C488A00CD824D /* UITextField+addLeftPadding.swift in Sources */, 1E651DAC2953165800B4F321 /* CountUpTimerUseCase.swift in Sources */, + 1E1C42E22A765844003CC427 /* RingtoneSelectViewController.swift in Sources */, 1E8C28F62A73C0D800935C5E /* HomeCoordinatorAction.swift in Sources */, 1E482BE82913559600175B78 /* Timer.swift in Sources */, 1EBEEFE82925B73000299CE7 /* TagColor.swift in Sources */, @@ -960,6 +1128,7 @@ 1E4CB4F529673E3E00FC918E /* HomeUseCase.swift in Sources */, 1EBEEFE02922446A00299CE7 /* CountDownTimerUseCase.swift in Sources */, 1E482BD4291353F400175B78 /* TimePickerViewDataSource.swift in Sources */, + 1E1C43BD2A7A90E7003CC427 /* UIButton+Rx+configurationTitle.swift in Sources */, 1E4159F5294CCA8400EDA065 /* FilteringNavigationTitleView.swift in Sources */, 1E9BA3C92914CF5400B39E9C /* TimerCellViewModel.swift in Sources */, 1E744BAB296C488A00CD824D /* UITextField+Rx+textChanged.swift in Sources */, @@ -968,11 +1137,14 @@ 1EBEF029292909DD00299CE7 /* TimerModel.xcdatamodeld in Sources */, 1EA3FCE8292E4B730073560D /* CoreDataStorage+TagMO.swift in Sources */, 1EA3FCE0292E43520073560D /* ManagedObjectConvertible.swift in Sources */, + 1E1C43B42A7A77A0003CC427 /* RingtoneType.swift in Sources */, 1E4159EE29485C6900EDA065 /* TimerEditingView.swift in Sources */, - 1EBFEE152919270D00747606 /* TimerEditingViewModel.swift in Sources */, + 1EBFEE152919270D00747606 /* TimerEditingReactor.swift in Sources */, 1E4159F2294C0BDB00EDA065 /* TimeFactory.swift in Sources */, 1E48FE992911F9240090B246 /* AppDelegate.swift in Sources */, 1E744BB3296C488A00CD824D /* EditViewButtonType.swift in Sources */, + 1E1C43B22A7A7781003CC427 /* RingtoneSelectTableViewDiffableDataSource.swift in Sources */, + 1E1C437B2A768626003CC427 /* Ringtone.swift in Sources */, 1E744BB2296C488A00CD824D /* TimerFilteringCondition.swift in Sources */, 1E4CB4DF2966BE9000FC918E /* TimeMO+CoreDataClass.swift in Sources */, 1E744BB4296C488A00CD824D /* TimeType.swift in Sources */, @@ -1218,7 +1390,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.0; + MARKETING_VERSION = 1.5.0; PRODUCT_BUNDLE_IDENTIFIER = com.sanghyeok.Multimer; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1252,7 +1424,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.0; + MARKETING_VERSION = 1.5.0; PRODUCT_BUNDLE_IDENTIFIER = com.sanghyeok.Multimer; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Multimer/Multimer/Data/Persistence/CoreData/CoreDataStorage/CoreDataStorage+TimerMO.swift b/Multimer/Multimer/Data/Persistence/CoreData/CoreDataStorage/CoreDataStorage+TimerMO.swift index 9967686..92d976d 100644 --- a/Multimer/Multimer/Data/Persistence/CoreData/CoreDataStorage/CoreDataStorage+TimerMO.swift +++ b/Multimer/Multimer/Data/Persistence/CoreData/CoreDataStorage/CoreDataStorage+TimerMO.swift @@ -20,6 +20,7 @@ extension CoreDataStorage { startDate: Date? = nil, notificationIdentifier: String? = nil, type: TimerType? = nil, + ringtone: Ringtone? = nil, index: Int? = nil ) { backgroundContext.perform { [weak self] in @@ -34,6 +35,7 @@ extension CoreDataStorage { startDate: startDate, notificationIdentifier: notificationIdentifier, type: type, + ringtone: ringtone, context: self.backgroundContext ) self.saveContext() diff --git a/Multimer/Multimer/Data/Persistence/CoreData/ManagedObjectSubclass/TimerMO+CoreDataClass.swift b/Multimer/Multimer/Data/Persistence/CoreData/ManagedObjectSubclass/TimerMO+CoreDataClass.swift index 7e471ae..69d591e 100644 --- a/Multimer/Multimer/Data/Persistence/CoreData/ManagedObjectSubclass/TimerMO+CoreDataClass.swift +++ b/Multimer/Multimer/Data/Persistence/CoreData/ManagedObjectSubclass/TimerMO+CoreDataClass.swift @@ -28,6 +28,7 @@ extension TimerMO: ModelConvertible { expireDate: expireDate, startDate: startDate, type: type, + ringtone: ringtone, index: Int(index) ) } @@ -44,6 +45,7 @@ extension TimerMO { startDate: Date? = nil, notificationIdentifier: String? = nil, type: TimerType? = nil, + ringtone: Ringtone? = nil, index: Int? = nil, context: NSManagedObjectContext ) { @@ -56,6 +58,7 @@ extension TimerMO { self.startDate = startDate ?? self.startDate self.notificationIdentifier = notificationIdentifier ?? self.notificationIdentifier self.type = type ?? self.type + self.ringtone = ringtone ?? self.ringtone self.index = Int16(index ?? Int(self.index)) } } @@ -82,6 +85,16 @@ extension TimerMO { self.typeValue = Int16(newValue.rawValue) } } + + var ringtone: Ringtone? { + get { + guard let ringtoneValue = self.ringtoneValue else { return nil } + return Ringtone(rawValue: ringtoneValue) + } + set { + self.ringtoneValue = newValue?.rawValue + } + } } @frozen diff --git a/Multimer/Multimer/Data/Persistence/CoreData/TimerModel.xcdatamodeld/TimerModel.xcdatamodel/contents b/Multimer/Multimer/Data/Persistence/CoreData/TimerModel.xcdatamodeld/TimerModel.xcdatamodel/contents index 2e609cc..b8f68b8 100644 --- a/Multimer/Multimer/Data/Persistence/CoreData/TimerModel.xcdatamodeld/TimerModel.xcdatamodel/contents +++ b/Multimer/Multimer/Data/Persistence/CoreData/TimerModel.xcdatamodeld/TimerModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -19,17 +19,11 @@ - + - - - - - - \ No newline at end of file diff --git a/Multimer/Multimer/Data/Repository/CoreDataTimerRepository.swift b/Multimer/Multimer/Data/Repository/CoreDataTimerRepository.swift index 6d58c93..8e96745 100644 --- a/Multimer/Multimer/Data/Repository/CoreDataTimerRepository.swift +++ b/Multimer/Multimer/Data/Repository/CoreDataTimerRepository.swift @@ -52,7 +52,8 @@ final class CoreDataTimerRepository: TimerPersistentRepository { state: TimerState? = nil, expireDate: Date? = nil, startDate: Date? = nil, - type: TimerType? = nil + type: TimerType? = nil, + ringtone: Ringtone? = nil ) -> Completable { return Completable.create { [weak self] completable in guard let self = self else { return Disposables.create { } } @@ -66,7 +67,8 @@ final class CoreDataTimerRepository: TimerPersistentRepository { state: state, expireDate: expireDate, startDate: startDate, - type: type + type: type, + ringtone: ringtone ) completable(.completed) } diff --git a/Multimer/Multimer/Data/Repository/Protocol/TimerPersistentRepository.swift b/Multimer/Multimer/Data/Repository/Protocol/TimerPersistentRepository.swift index 7b61175..8f906a2 100644 --- a/Multimer/Multimer/Data/Repository/Protocol/TimerPersistentRepository.swift +++ b/Multimer/Multimer/Data/Repository/Protocol/TimerPersistentRepository.swift @@ -18,7 +18,8 @@ protocol TimerPersistentRepository { state: TimerState?, expireDate: Date?, startDate: Date?, - type: TimerType? + type: TimerType?, + ringtone: Ringtone? ) -> Completable func saveTimeOfTimer( target identifier: UUID, @@ -42,16 +43,19 @@ extension TimerPersistentRepository { state: TimerState? = nil, expireDate: Date? = nil, startDate: Date? = nil, - type: TimerType? = nil + type: TimerType? = nil, + ringtone: Ringtone? = nil ) -> Completable { - updateTimer(target: identifier, - name: name, - tag: tag, - time: time, - state: state, - expireDate: expireDate, - startDate: startDate, - type: type + updateTimer( + target: identifier, + name: name, + tag: tag, + time: time, + state: state, + expireDate: expireDate, + startDate: startDate, + type: type, + ringtone: ringtone ) } diff --git a/Multimer/Multimer/Data/Service/UserNotificationCenterService.swift b/Multimer/Multimer/Data/Service/UserNotificationCenterService.swift new file mode 100644 index 0000000..5e85f26 --- /dev/null +++ b/Multimer/Multimer/Data/Service/UserNotificationCenterService.swift @@ -0,0 +1,39 @@ +// +// UserNotificationCenterService.swift +// Multimer +// +// Created by 김상혁 on 2023/08/02. +// + +import Foundation +import UserNotifications + +final class UserNotificationCenterService { + static func registerNotification( + ringtone: Ringtone?, + remainingSeconds: TimeInterval, + timerName: String, + notificationIdentifier: String? + ) { + let content = UNMutableNotificationContent() + content.title = LocalizableString.appTitle.localized + content.body = LocalizableString.timerExpired(timerName: timerName).localized + content.sound = .default + + if let ringtoneFileName = ringtone?.name, ringtone != .default1 { + content.sound = UNNotificationSound( + named: UNNotificationSoundName(rawValue: "\(ringtoneFileName).\(Constant.Ringtone.extension)") + ) + } + + if remainingSeconds <= .zero { return } + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: remainingSeconds, repeats: false) + guard let notificationIdentifier = notificationIdentifier else { return } + let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger) + UNUserNotificationCenter.current().add(request) + } + + static func removeNotification(withIdentifiers identifiers: [String]) { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers) + } +} diff --git a/Multimer/Multimer/Domain/Model/Timer.swift b/Multimer/Multimer/Domain/Model/Timer.swift index 03a0ae5..65482f6 100644 --- a/Multimer/Multimer/Domain/Model/Timer.swift +++ b/Multimer/Multimer/Domain/Model/Timer.swift @@ -18,6 +18,7 @@ struct Timer { var startDate: Date? var notificationIdentifier: String? var type: TimerType + var ringtone: Ringtone? var index: Int // TODO: Sound, RepeatCount @@ -38,6 +39,7 @@ struct Timer { expireDate: Date? = nil, startDate: Date? = nil, type: TimerType = .countDown, + ringtone: Ringtone? = .default1, index: Int = .zero) { self.identifier = identifier self.name = name @@ -48,6 +50,7 @@ struct Timer { self.startDate = startDate self.notificationIdentifier = identifier.uuidString self.type = type + self.ringtone = ringtone self.index = index } @@ -61,6 +64,7 @@ struct Timer { self.startDate = timer.startDate self.notificationIdentifier = timer.identifier.uuidString self.type = timer.type + self.ringtone = timer.ringtone self.index = timer.index } @@ -74,7 +78,10 @@ struct Timer { extension Timer: Equatable { static func == (lhs: Timer, rhs: Timer) -> Bool { - return (lhs.name == rhs.name) && (lhs.tag == rhs.tag) && (lhs.time == rhs.time) + return (lhs.name == rhs.name) + && (lhs.tag == rhs.tag) + && (lhs.time == rhs.time) + && (lhs.ringtone == rhs.ringtone) } } @@ -91,6 +98,7 @@ extension Timer: ManagedObjectConvertible { startDate: startDate, notificationIdentifier: notificationIdentifier, type: type, + ringtone: ringtone, index: index, context: context ) diff --git a/Multimer/Multimer/Domain/UseCase/CountDownTimerUseCase.swift b/Multimer/Multimer/Domain/UseCase/CountDownTimerUseCase.swift index 85ae7c1..015f706 100644 --- a/Multimer/Multimer/Domain/UseCase/CountDownTimerUseCase.swift +++ b/Multimer/Multimer/Domain/UseCase/CountDownTimerUseCase.swift @@ -58,7 +58,12 @@ final class CountDownTimerUseCase: TimerUseCase { resetTimer() } - registerNotification() + UserNotificationCenterService.registerNotification( + ringtone: timer.value.ringtone, + remainingSeconds: currentTimer.remainingSeconds, + timerName: currentTimer.name, + notificationIdentifier: currentTimer.notificationIdentifier + ) let expireDate = Date(timeInterval: currentTimer.remainingSeconds, since: .now) timerPersistentRepository @@ -112,7 +117,13 @@ final class CountDownTimerUseCase: TimerUseCase { } timerPersistentRepository - .updateTimer(target: newTimer.identifier, name: newTimer.name, tag: newTimer.tag, time: newTimer.time) + .updateTimer( + target: newTimer.identifier, + name: newTimer.name, + tag: newTimer.tag, + time: newTimer.time, + ringtone: newTimer.ringtone + ) .subscribe(onCompleted: { [weak self] in guard let self = self else { return } self.timer.accept(newTimer) @@ -129,24 +140,18 @@ final class CountDownTimerUseCase: TimerUseCase { dispatchSourceTimer = nil guard let notificationIdentifier = currentTimer.notificationIdentifier else { return } - UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notificationIdentifier]) + UserNotificationCenterService.removeNotification(withIdentifiers: [notificationIdentifier]) } - // TODO: Notification Manager 구현 - private func registerNotification() { - let content = UNMutableNotificationContent() - content.title = LocalizableString.appTitle.localized - content.body = LocalizableString.timerExpired(timerName: currentTimer.name).localized - content.sound = .default - - if currentTimer.remainingSeconds <= .zero { return } - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(currentTimer.remainingSeconds), repeats: false) - guard let notificationIdentifier = currentTimer.notificationIdentifier else { return } - let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request) + deinit { + stopTimer() } - - private func runTimer(by expirationDate: Date) { +} + +// MARK: - Supporting Methods + +private extension CountDownTimerUseCase { + func runTimer(by expirationDate: Date) { if dispatchSourceTimer == nil { dispatchSourceTimer = DispatchSource.makeTimerSource(queue: .global()) } @@ -163,19 +168,18 @@ final class CountDownTimerUseCase: TimerUseCase { self.expireTimer() self.stopTimer() } else { - let remainingTime = Time(totalSeconds: self.currentTimer.totalSeconds, remainingSeconds: remainingTimeInterval) + let remainingTime = Time( + totalSeconds: self.currentTimer.totalSeconds, + remainingSeconds: remainingTimeInterval + ) self.timer.accept(Timer(timer: self.currentTimer, time: remainingTime)) } } } - private func expireTimer() { + func expireTimer() { let expiredTime = TimeFactory.createExpiredTime(of: currentTimer.time) timer.accept(Timer(timer: currentTimer, time: expiredTime)) timerPersistentRepository.saveTimeOfTimer(target: timerIdentifier, time: expiredTime) } - - deinit { - stopTimer() - } } diff --git a/Multimer/Multimer/Localizing/LocalizableString.swift b/Multimer/Multimer/Localizing/LocalizableString.swift index 2c79479..955812e 100644 --- a/Multimer/Multimer/Localizing/LocalizableString.swift +++ b/Multimer/Multimer/Localizing/LocalizableString.swift @@ -39,6 +39,10 @@ enum LocalizableString { case checkNotificationPermissions case allowNotificationAuthorizationAlert case goToSettings + case alertTones + case ringtones + case ringtoneName(ringtone: Ringtone) + var localized: String { switch self { @@ -104,6 +108,12 @@ enum LocalizableString { return String(format: NSLocalizedString("allowNotificationAuthorizationAlert", comment: "")) case .goToSettings: return String(format: NSLocalizedString("goToSettings", comment: "")) + case .alertTones: + return String(format: NSLocalizedString("alertTones", comment: "")) + case .ringtones: + return String(format: NSLocalizedString("ringtones", comment: "")) + case .ringtoneName(let ringtone): + return String(format: NSLocalizedString("\(ringtone.name)", comment: "")) } } } diff --git a/Multimer/Multimer/Localizing/en.lproj/Localizable.strings b/Multimer/Multimer/Localizing/en.lproj/Localizable.strings index d45f552..7ad744a 100644 --- a/Multimer/Multimer/Localizing/en.lproj/Localizable.strings +++ b/Multimer/Multimer/Localizing/en.lproj/Localizable.strings @@ -29,7 +29,7 @@ "noTimerActivatedMessage" = "No Timer Activated"; "onlyActiveTimersAppearMessage" = "Timers in the run, pause, and complete states appear."; "settingTimer" = "Editing %@"; -"timerExpired" = "Timer Expired - %@"; +"timerExpired" = "[Timer Expired] %@"; "appTitle" = "Multi+Timer"; "defaultName" = "Default Name"; "swipeRightToStop" = "Swipe right to stop the timer."; @@ -37,3 +37,36 @@ "checkNotificationPermissions" = "Check Notification Permissions"; "allowNotificationAuthorizationAlert" = "The alarm is not sounding because you have not allowed notification permissions. Please allow notification permissions in Settings."; "goToSettings" = "Go to Settings"; + +// MARK: - Ringtones + +"alertTones" = "ALERT TONES"; +"ringtones" = "RINGTONES"; + +"default1" = "Alert Sound 1"; +"default2" = "Alert Sound 2"; +"default3" = "Alert Sound 3"; +"default4" = "Alert Sound 4"; +"default5" = "Alert Sound 5"; +"default6" = "Alert Sound 6"; +"default7" = "Alert Sound 7"; +"default8" = "Alert Sound 8"; +"alarm" = "Alarm"; +"bark" = "Bark"; +"beacon" = "Beacon"; +"bulletin" = "Bulletin"; +"duck" = "Duck"; +"illuminate" = "Illuminate"; +"marimba" = "Marimba"; +"oldPhone" = "Old Phone"; +"pianoRiff" = "Piano Riff"; +"pinball" = "Pinball"; +"presto" = "Presto"; +"radar" = "Radar"; +"reflection" = "Reflection"; +"sencha" = "Sencha"; +"signal" = "Signal"; +"stargaze" = "Stargaze"; +"strum" = "Strum"; +"trill" = "Trill"; +"xylophone" = "Xylophone"; diff --git a/Multimer/Multimer/Localizing/ja.lproj/Localizable.strings b/Multimer/Multimer/Localizing/ja.lproj/Localizable.strings index b707628..ef28bbf 100644 --- a/Multimer/Multimer/Localizing/ja.lproj/Localizable.strings +++ b/Multimer/Multimer/Localizing/ja.lproj/Localizable.strings @@ -29,7 +29,7 @@ "noTimerActivatedMessage" = "活性化されたタイマーなし"; "onlyActiveTimersAppearMessage" = "実行、一時停止、および完了状態のタイマーが表示されます。"; "settingTimer" = "%@ 設定"; -"timerExpired" = "タイマー 終了 '%@'"; +"timerExpired" = "[タイマー 終了] %@"; "appTitle" = "マルチ+タイマー"; "defaultName" = "デフォルト名"; "swipeRightToStop" = "右にスワイプしてタイマーを停止します。"; @@ -37,3 +37,36 @@ "checkNotificationPermissions" = "通知許可の確認"; "allowNotificationAuthorizationAlert" = "通知許可を許可していないため、アラームが鳴りません。設定で通知許可を許可してください。"; "goToSettings" = "設定"; + +// MARK: - Ringtones + +"alertTones" = "ALERT TONES"; +"ringtones" = "RINGTONES"; + +"default1" = "Alert Sound 1"; +"default2" = "Alert Sound 2"; +"default3" = "Alert Sound 3"; +"default4" = "Alert Sound 4"; +"default5" = "Alert Sound 5"; +"default6" = "Alert Sound 6"; +"default7" = "Alert Sound 7"; +"default8" = "Alert Sound 8"; +"alarm" = "Alarm"; +"bark" = "Bark"; +"beacon" = "Beacon"; +"bulletin" = "Bulletin"; +"duck" = "Duck"; +"illuminate" = "Illuminate"; +"marimba" = "Marimba"; +"oldPhone" = "Old Phone"; +"pianoRiff" = "Piano Riff"; +"pinball" = "Pinball"; +"presto" = "Presto"; +"radar" = "Radar"; +"reflection" = "Reflection"; +"sencha" = "Sencha"; +"signal" = "Signal"; +"stargaze" = "Stargaze"; +"strum" = "Strum"; +"trill" = "Trill"; +"xylophone" = "Xylophone"; diff --git a/Multimer/Multimer/Localizing/ko.lproj/Localizable.strings b/Multimer/Multimer/Localizing/ko.lproj/Localizable.strings index 8f22053..585803f 100644 --- a/Multimer/Multimer/Localizing/ko.lproj/Localizable.strings +++ b/Multimer/Multimer/Localizing/ko.lproj/Localizable.strings @@ -29,7 +29,7 @@ "noTimerActivatedMessage" = "활성화된 타이머 없음"; "onlyActiveTimersAppearMessage" = "실행 / 일시정지 / 완료 상태의 타이머가 나타납니다."; "settingTimer" = "%@ 설정"; -"timerExpired" = "타이머 종료 - %@"; +"timerExpired" = "[타이머 종료] %@"; "appTitle" = "멀티 타이머"; "defaultName" = "기본 이름"; "swipeRightToStop" = "오른쪽으로 밀어서 중지할 수 있습니다."; @@ -37,3 +37,36 @@ "checkNotificationPermissions" = "알림 권한 확인"; "allowNotificationAuthorizationAlert" = "알림 권한을 허용하지 않아 알람이 울리지 않습니다. 설정에서 알림 권한을 허용해 주세요."; "goToSettings" = "설정으로 이동"; + +// MARK: - Ringtones + +"alertTones" = "알림 소리"; +"ringtones" = "벨소리"; + +"default1" = "기본음1"; +"default2" = "기본음2"; +"default3" = "기본음3"; +"default4" = "기본음4"; +"default5" = "기본음5"; +"default6" = "기본음6"; +"default7" = "기본음7"; +"default8" = "기본음8"; +"alarm" = "경보기"; +"bark" = "개 짖는 소리"; +"beacon" = "옅어지는 신호음"; +"bulletin" = "공지음"; +"duck" = "오리 소리"; +"illuminate" = "조명"; +"marimba" = "마림바"; +"oldPhone" = "전화 벨소리"; +"pianoRiff" = "피아노 연주"; +"pinball" = "핀볼"; +"presto" = "프레스토"; +"radar" = "전파 탐지기"; +"reflection" = "반향"; +"sencha" = "녹차"; +"signal" = "신호음"; +"stargaze" = "공상음"; +"strum" = "기타 연주"; +"trill" = "떨리는 소리"; +"xylophone" = "실로폰"; diff --git a/Multimer/Multimer/Localizing/ru.lproj/Localizable.strings b/Multimer/Multimer/Localizing/ru.lproj/Localizable.strings index eb15955..6fd0218 100644 --- a/Multimer/Multimer/Localizing/ru.lproj/Localizable.strings +++ b/Multimer/Multimer/Localizing/ru.lproj/Localizable.strings @@ -8,7 +8,7 @@ "timer" = "Таймер"; "stopwatch" = "Секундомер"; -"countDownTimer" = "⏳ Обратный отсчет"; +"countDownTimer" = "⏳ Таймер"; "countUpStopwatch" = "⏱ Секундомер"; "hour" = "h"; "minute" = "m"; @@ -29,7 +29,7 @@ "noTimerActivatedMessage" = "Таймер не активирован"; "onlyActiveTimersAppearMessage" = "Таймеры в состояниях запуска,\nпаузы и завершения появляются."; "settingTimer" = "Настройка %@"; -"timerExpired" = "Время таймера истекло - %@"; +"timerExpired" = "[Таймер истек] %@"; "appTitle" = "Мульти таймер"; "defaultName" = "Имя по умолчанию"; "swipeRightToStop" = "Свайпните вправо, чтобы остановить таймер."; @@ -37,3 +37,36 @@ "checkNotificationPermissions" = "Проверить разрешения на уведомления"; "allowNotificationAuthorizationAlert" = "Сигнал тревоги не звучит, потому что вы не разрешили уведомления. Пожалуйста, разрешите уведомления в настройках."; "goToSettings" = "Настройки"; + +// MARK: - Ringtones + +"alertTones" = "ALERT TONES"; +"ringtones" = "RINGTONES"; + +"default1" = "звук уведомления 1"; +"default2" = "звук уведомления 2"; +"default3" = "звук уведомления 3"; +"default4" = "звук уведомления 4"; +"default5" = "звук уведомления 5"; +"default6" = "звук уведомления 6"; +"default7" = "звук уведомления 7"; +"default8" = "звук уведомления 8"; +"alarm" = "Сигнал тревоги"; +"bark" = "Звук лая собаки"; +"beacon" = "Маяк"; +"bulletin" = "Звук объявления"; +"duck" = "Звук утки"; +"illuminate" = "Освещение"; +"marimba" = "Маримба"; +"oldPhone" = "Мелодия звонка телефона"; +"pianoRiff" = "Фортепианный рифф"; +"pinball" = "Пинбол"; +"presto" = "Престо"; +"radar" = "Радар"; +"reflection" = "Отражение"; +"сенча" = "зеленый чай"; +"signal" = "Сигнал"; +"stargaze" = "Звездочет"; +"strum" = "гитара"; +"trill" = "трель"; +"xylophone" = "ксилофон"; diff --git a/Multimer/Multimer/Localizing/vi.lproj/Localizable.strings b/Multimer/Multimer/Localizing/vi.lproj/Localizable.strings index 48172a0..7c62b95 100644 --- a/Multimer/Multimer/Localizing/vi.lproj/Localizable.strings +++ b/Multimer/Multimer/Localizing/vi.lproj/Localizable.strings @@ -29,7 +29,7 @@ "noTimerActivatedMessage" = "Không có bộ hẹn giờ nào\nđang chạy"; "onlyActiveTimersAppearMessage" = "Đồng hồ hẹn giờ có trạng thái\nđang chạy / tạm dừng / kết thúc sẽ xuất hiện."; "settingTimer" = "%@ Cài đặt"; -"timerExpired" = "Hẹn giờ kết thúc - %@"; +"timerExpired" = "[Hẹn giờ kết thúc] %@"; "appTitle" = "Multi Timer"; "defaultName" = "Tên mặc định"; "swipeRightToStop" = "Vuốt sang phải để dừng hẹn giờ"; @@ -37,3 +37,36 @@ "checkNotificationPermissions" = "Kiểm tra quyền thông báo"; "allowNotificationAuthorizationAlert" = "Thông báo không hiện vì bạn chưa cho phép quyền thông báo. Vui lòng cho phép quyền thông báo trong Cài đặt."; "goToSettings" = "Tới thiết lập"; + +// MARK: - Ringtones + +"alertTones" = "ALERT TONES"; +"ringtones" = "RINGTONES"; + +"default1" = "Alert Sound 1"; +"default2" = "Alert Sound 2"; +"default3" = "Alert Sound 3"; +"default4" = "Alert Sound 4"; +"default5" = "Alert Sound 5"; +"default6" = "Alert Sound 6"; +"default7" = "Alert Sound 7"; +"default8" = "Alert Sound 8"; +"alarm" = "Alarm"; +"bark" = "Bark"; +"beacon" = "Beacon"; +"bulletin" = "Bulletin"; +"duck" = "Duck"; +"illuminate" = "Illuminate"; +"marimba" = "Marimba"; +"oldPhone" = "Old Phone"; +"pianoRiff" = "Piano Riff"; +"pinball" = "Pinball"; +"presto" = "Presto"; +"radar" = "Radar"; +"reflection" = "Reflection"; +"sencha" = "Sencha"; +"signal" = "Signal"; +"stargaze" = "Stargaze"; +"strum" = "Strum"; +"trill" = "Trill"; +"xylophone" = "Xylophone"; diff --git a/Multimer/Multimer/Localizing/zh-Hans.lproj/Localizable.strings b/Multimer/Multimer/Localizing/zh-Hans.lproj/Localizable.strings index 2aed914..f5f8599 100644 --- a/Multimer/Multimer/Localizing/zh-Hans.lproj/Localizable.strings +++ b/Multimer/Multimer/Localizing/zh-Hans.lproj/Localizable.strings @@ -29,7 +29,7 @@ "noTimerActivatedMessage" = "无活化中的计时器"; "onlyActiveTimersAppearMessage" = "只显示正在活化中或暂停的计时器。"; "settingTimer" = "设定 %@"; -"timerExpired" = "计时器 完了 '%@'"; +"timerExpired" = "[计时器 完了] %@"; "appTitle" = "多重计时器"; "defaultName" = "默认名称"; "swipeRightToStop" = "向右滑动以停止计时器。"; @@ -37,3 +37,36 @@ "checkNotificationPermissions" = "检查通知权限"; "allowNotificationAuthorizationAlert" = "警报未响是因为您未允许通知权限。请在设置中允许通知权限。"; "goToSettings" = "转到设置"; + +// MARK: - Ringtones + +"alertTones" = "警报音调"; +"ringtones" = "铃声"; + +"default1" = "默认警报声 1"; +"default2" = "默认警报声 2"; +"default3" = "默认警报声 3"; +"default4" = "默认警报声 4"; +"default5" = "默认警报声 5"; +"default6" = "默认警报声 6"; +"default7" = "默认警报声 7"; +"default8" = "默认警报声 8"; +"alarm" = "警报声"; +"bark" = "吠叫"; +"beacon" = "渐弱的蜂鸣声"; +"bulletin" = "公告声音"; +"duck" = "鸭子"; +"illuminate" = "照明"; +"marimba" = "马林巴琴"; +"oldPhone" = "旧电话"; +"pianoRiff" = "钢琴"; +"pinball" = "弹球"; +"presto" = "普雷斯托"; +"radar" = "雷达"; +"reflection" = "反照"; +"sencha" = "绿茶"; +"signal" = "信号音"; +"stargaze" = "音调"; +"strum" = "吉他弹奏"; +"trill" = "颤音"; +"xylophone" = "木琴"; diff --git a/Multimer/Multimer/Presentation/Coordinator/Home/DefaultHomeCoordinator.swift b/Multimer/Multimer/Presentation/Coordinator/Home/DefaultHomeCoordinator.swift index a3b7a0a..eb3c855 100644 --- a/Multimer/Multimer/Presentation/Coordinator/Home/DefaultHomeCoordinator.swift +++ b/Multimer/Multimer/Presentation/Coordinator/Home/DefaultHomeCoordinator.swift @@ -29,6 +29,8 @@ final class DefaultHomeCoordinator: HomeCoordinator { presentTimerCreateViewController(createdTimerRelay: createdTimerRelay) case .showTimerEditScene(let initialTimer, let editedTimerRelay): pushTimerEditViewController(initialTimer: initialTimer, editedTimerRelay: editedTimerRelay) + case .showRingtoneSelectScene(let selectedRingtoneRelay): + presentRingtoneSelectViewController(selectedRingtoneRelay: selectedRingtoneRelay) case .finishTimerCreateScene: navigationController.dismiss(animated: true) case .finishTimerEditScene: @@ -54,7 +56,7 @@ private extension DefaultHomeCoordinator { func presentTimerCreateViewController(createdTimerRelay: PublishRelay) { let timerCreateViewController = TimerCreateViewController() - let timerCreateReactor = TimerCreateViewModel( + let timerCreateReactor = TimerCreateReactor( coordinator: self, createdTimerRelay: createdTimerRelay ) @@ -64,7 +66,7 @@ private extension DefaultHomeCoordinator { func pushTimerEditViewController(initialTimer: Timer, editedTimerRelay: PublishRelay) { let timerEditingViewController = TimerEditingViewController() - let timerEditingReactor = TimerEditingViewModel( + let timerEditingReactor = TimerEditingReactor( initialTimer: initialTimer, coordinator: self, editedTimerRelay: editedTimerRelay @@ -72,4 +74,11 @@ private extension DefaultHomeCoordinator { timerEditingViewController.reactor = timerEditingReactor navigationController.pushViewController(timerEditingViewController, animated: true) } + + func presentRingtoneSelectViewController(selectedRingtoneRelay: BehaviorRelay) { + let ringtoneSelectViewController = RingtoneSelectViewController() + let ringtoneSelectReactor = RingtoneSelectReactor(selectedRingtoneRelay: selectedRingtoneRelay) + ringtoneSelectViewController.reactor = ringtoneSelectReactor + navigationController.visibleViewController?.present(ringtoneSelectViewController, animated: true) + } } diff --git a/Multimer/Multimer/Presentation/Coordinator/Home/HomeCoordinatorAction.swift b/Multimer/Multimer/Presentation/Coordinator/Home/HomeCoordinatorAction.swift index db83ea5..730f7e4 100644 --- a/Multimer/Multimer/Presentation/Coordinator/Home/HomeCoordinatorAction.swift +++ b/Multimer/Multimer/Presentation/Coordinator/Home/HomeCoordinatorAction.swift @@ -11,6 +11,7 @@ enum HomeCoordinatorAction { case appDidStart case showTimerCreateScene(createdTimerRelay: PublishRelay) case showTimerEditScene(initialTimer: Timer, editedTimerRelay: PublishRelay) + case showRingtoneSelectScene(selectedRingtoneRelay: BehaviorRelay) case finishTimerCreateScene case finishTimerEditScene } diff --git a/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneCellModel.swift b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneCellModel.swift new file mode 100644 index 0000000..b1bc58b --- /dev/null +++ b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneCellModel.swift @@ -0,0 +1,13 @@ +// +// RingtoneCellModel.swift +// Multimer +// +// Created by 김상혁 on 2023/08/02. +// + +import Foundation + +struct RingtoneCellModel: Hashable { + var isSelected: Bool = false + var ringtone: Ringtone +} diff --git a/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectReactor.swift b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectReactor.swift new file mode 100644 index 0000000..df3f1e9 --- /dev/null +++ b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectReactor.swift @@ -0,0 +1,86 @@ +// +// RingtoneSelectReactor.swift +// Multimer +// +// Created by 김상혁 on 2023/07/31. +// + +import RxRelay +import ReactorKit + +final class RingtoneSelectReactor: Reactor { + + enum Action { + case viewDidLoad + case ringtoneDidSelect(IndexPath) + } + + enum Mutation { + case selectRingtone(Ringtone) + case playRingtoneSound(Ringtone) + } + + struct State { + var ringtoneCellModelMap: [RingtoneType: [RingtoneCellModel]] = [:] + var ringtoneToPlay: Ringtone? + } + + var initialState = State() + private let selectedRingtoneRelay: BehaviorRelay + + init(selectedRingtoneRelay: BehaviorRelay) { + self.selectedRingtoneRelay = selectedRingtoneRelay + } + + func mutate(action: Action) -> Observable { + switch action { + case .viewDidLoad: + let selectedRingtone = selectedRingtoneRelay.value + return .just(.selectRingtone(selectedRingtone)) + + case .ringtoneDidSelect(let indexPath): + let ringtoneType = RingtoneType.allCases[indexPath.section] + guard let ringtoneCellModels = currentState.ringtoneCellModelMap[ringtoneType] else { + return .empty() + } + let selectedRingtone = ringtoneCellModels[indexPath.row].ringtone + return .concat( + .just(.playRingtoneSound(selectedRingtone)), + .just(.selectRingtone(selectedRingtone)), + acceptSelectedRingtone(selectedRingtone) + ) + } + } + + func reduce(state: State, mutation: Mutation) -> State { + var newState = state + switch mutation { + case .selectRingtone(let selectedRingtone): + newState.ringtoneCellModelMap = generateRingtoneCellModelMap(selectedRingtone: selectedRingtone) + case .playRingtoneSound(let ringtoneToPlay): + newState.ringtoneToPlay = ringtoneToPlay + } + return newState + } +} + +// MARK: - Side Effect Methods + +private extension RingtoneSelectReactor { + func acceptSelectedRingtone(_ ringtone: Ringtone) -> Observable { + selectedRingtoneRelay.accept(ringtone) + return .empty() + } +} + +// MARK: - Supporting Methods + +private extension RingtoneSelectReactor { + func generateRingtoneCellModelMap(selectedRingtone: Ringtone) -> [RingtoneType: [RingtoneCellModel]] { + let ringtoneCellModels = Ringtone.allCases.map { + RingtoneCellModel(isSelected: $0 == selectedRingtone, ringtone: $0) + } + let ringtoneCellModelMap = Dictionary(grouping: ringtoneCellModels) { $0.ringtone.type } + return ringtoneCellModelMap + } +} diff --git a/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectTableViewDiffableDataSource.swift b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectTableViewDiffableDataSource.swift new file mode 100644 index 0000000..5a3b604 --- /dev/null +++ b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectTableViewDiffableDataSource.swift @@ -0,0 +1,42 @@ +// +// RingtoneSelectTableViewDiffableDataSource.swift +// Multimer +// +// Created by 김상혁 on 2023/08/02. +// + +import UIKit + +final class RingtoneSelectTableViewDiffableDataSource: UITableViewDiffableDataSource { + + typealias Snapshot = NSDiffableDataSourceSnapshot + + init(tableView: UITableView) { + super.init(tableView: tableView) { (tableView, indexPath, cellViewModel) -> UITableViewCell? in + let cell = tableView.dequeueReusableCell( + withIdentifier: RingtoneViewCell.identifier, + for: indexPath + ) as? RingtoneViewCell + let ringtoneName = LocalizableString.ringtoneName(ringtone: cellViewModel.ringtone).localized + cell?.configure(title: ringtoneName, isSelected: cellViewModel.isSelected) + return cell + } + } + + // MARK: - DiffableDataSource Methods + + func applySnapshot(for ringtoneCellModelMap: [RingtoneType: [RingtoneCellModel]]) { + var snapshot = Snapshot() + snapshot.appendSections(RingtoneType.allCases) + for type in RingtoneType.allCases { + if let cellModels = ringtoneCellModelMap[type] { + snapshot.appendItems(cellModels, toSection: type) + } + } + apply(snapshot, animatingDifferences: false) + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return RingtoneType.allCases[section].title + } +} diff --git a/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectViewController.swift b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectViewController.swift new file mode 100644 index 0000000..4218d2b --- /dev/null +++ b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneSelectViewController.swift @@ -0,0 +1,97 @@ +// +// SoundSettingViewController.swift +// Multimer +// +// Created by 김상혁 on 2023/07/30. +// + +import AVFoundation +import UIKit + +import ReactorKit +import RxCocoa + +final class RingtoneSelectViewController: UIViewController, View { + private lazy var tableViewDiffableDataSource = RingtoneSelectTableViewDiffableDataSource(tableView: tableView) + private lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .insetGrouped) + tableView.register(RingtoneViewCell.self, forCellReuseIdentifier: RingtoneViewCell.identifier) + tableView.backgroundColor = .systemGray5 + return tableView + }() + + + private let closeBarButton = UIBarButtonItem(systemItem: .close) + + private var audioPlayer: AVAudioPlayer? + var disposeBag = DisposeBag() + + override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + + navigationItem.rightBarButtonItem = closeBarButton + } + + func bind(reactor: RingtoneSelectReactor) { + bindAction(reactor: reactor) + bindState(reactor: reactor) + } +} + +// MARK: - Bind Reactor + +private extension RingtoneSelectViewController { + func bindAction(reactor: RingtoneSelectReactor) { + rx.viewDidLoad + .map { Reactor.Action.viewDidLoad } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + tableView.rx.itemSelected + .map { Reactor.Action.ringtoneDidSelect($0) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + } + + func bindState(reactor: RingtoneSelectReactor) { + reactor.state.map { $0.ringtoneCellModelMap } + .distinctUntilChanged() + .observe(on: MainScheduler.asyncInstance) + .bind(onNext: tableViewDiffableDataSource.applySnapshot) + .disposed(by: disposeBag) + + reactor.state.map { $0.ringtoneToPlay } + .distinctUntilChanged() + .compactMap { $0 } + .bind(with: self) { `self`, ringtoneToPlay in + self.play(ringtone: ringtoneToPlay) + } + .disposed(by: disposeBag) + } +} + +// MARK: - UI Layout + +private extension RingtoneSelectViewController { + func setupTableView() { + view.addSubview(tableView) + + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 6).isActive = true + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 6).isActive = true + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -6).isActive = true + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + } +} + +// MARK: - Supporting Methods + +private extension RingtoneSelectViewController { + func play(ringtone: Ringtone) { + if let url = Bundle.main.url(forResource: ringtone.name, withExtension: Constant.Ringtone.extension) { + audioPlayer = try? AVAudioPlayer(contentsOf: url) + audioPlayer?.play() + } + } +} diff --git a/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneViewCell.swift b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneViewCell.swift new file mode 100644 index 0000000..fb47088 --- /dev/null +++ b/Multimer/Multimer/Presentation/RingtoneSelect/RingtoneViewCell.swift @@ -0,0 +1,45 @@ +// +// RingtoneViewCell.swift +// Multimer +// +// Created by 김상혁 on 2023/08/02. +// + +import UIKit + +final class RingtoneViewCell: UITableViewCell, CellIdentifiable { + + private let titleLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + layoutUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(title: String, isSelected: Bool) { + titleLabel.text = title + accessoryType = isSelected ? .checkmark : .none + } + + func toggleSelection() { + accessoryType = isSelected ? .none : .checkmark + } +} + +// MARK: - UI Layout + +private extension RingtoneViewCell { + func layoutUI() { + contentView.addSubview(titleLabel) + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true + titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true + } +} diff --git a/Multimer/Multimer/Presentation/Support/Common/RingtoneButton.swift b/Multimer/Multimer/Presentation/Support/Common/RingtoneButton.swift new file mode 100644 index 0000000..44038bc --- /dev/null +++ b/Multimer/Multimer/Presentation/Support/Common/RingtoneButton.swift @@ -0,0 +1,39 @@ +// +// RingtoneButton.swift +// Multimer +// +// Created by 김상혁 on 2023/08/02. +// + +import UIKit + +final class RingtoneButton: UIButton { + + init() { + super.init(frame: .zero) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + super.init(coder: coder) + } +} + +// MARK: - UI Configuration + +private extension RingtoneButton { + func configureUI() { + var configuration = UIButton.Configuration.plain() + configuration.image = UIImage(systemName: Constant.SFSymbolName.bellFill) + configuration.imagePadding = 8 + configuration.imagePlacement = .leading + configuration.titleAlignment = .trailing + configuration.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: .zero, bottom: 12, trailing: .zero) + self.configuration = configuration + tintColor = .label + layer.borderWidth = 1 + layer.cornerRadius = 8 + layer.borderColor = UIColor.systemGray.cgColor + } +} diff --git a/Multimer/Multimer/Presentation/Support/Constant.swift b/Multimer/Multimer/Presentation/Support/Constant.swift index dd7ac47..0798a43 100644 --- a/Multimer/Multimer/Presentation/Support/Constant.swift +++ b/Multimer/Multimer/Presentation/Support/Constant.swift @@ -30,6 +30,8 @@ enum Constant { static let hourglassTophalfFilled = "hourglass.tophalf.filled" static let stopwatchFill = "stopwatch.fill" + + static let bellFill = "bell.fill" } enum AssetImageName { @@ -48,4 +50,8 @@ enum Constant { static let isFirstLaunch = "isFirstLaunch" static let isNotificationAllowed = "isNotificationAllowed" } + + enum Ringtone { + static let `extension` = "aiff" + } } diff --git a/Multimer/Multimer/Presentation/Support/Enum/Ringtone.swift b/Multimer/Multimer/Presentation/Support/Enum/Ringtone.swift new file mode 100644 index 0000000..21b70c5 --- /dev/null +++ b/Multimer/Multimer/Presentation/Support/Enum/Ringtone.swift @@ -0,0 +1,51 @@ +// +// Ringtone.swift +// Multimer +// +// Created by 김상혁 on 2023/07/30. +// + +import Foundation + +enum Ringtone: String, CaseIterable, Hashable { + case alarm + case bark + case beacon + case bulletin + case default1 + case default2 + case default3 + case default4 + case default5 + case default6 + case default7 + case default8 + case duck + case illuminate + case marimba + case oldPhone + case pianoRiff + case pinball + case presto + case radar + case reflection + case sencha + case signal + case stargaze + case strum + case trill + case xylophone + + var name: String { + return rawValue + } + + var type: RingtoneType { + switch self { + case .default1, .default2, .default3, .default4, .default5, .default6, .default7, .default8: + return .alertTone + default: + return .ringtone + } + } +} diff --git a/Multimer/Multimer/Presentation/Support/Enum/RingtoneType.swift b/Multimer/Multimer/Presentation/Support/Enum/RingtoneType.swift new file mode 100644 index 0000000..ca71602 --- /dev/null +++ b/Multimer/Multimer/Presentation/Support/Enum/RingtoneType.swift @@ -0,0 +1,22 @@ +// +// RingtoneType.swift +// Multimer +// +// Created by 김상혁 on 2023/08/02. +// + +import Foundation + +enum RingtoneType: String, CaseIterable { + case alertTone + case ringtone + + var title: String { + switch self { + case .alertTone: + return LocalizableString.alertTones.localized + case .ringtone: + return LocalizableString.ringtones.localized + } + } +} diff --git a/Multimer/Multimer/Presentation/Support/Extension/UIButton+Rx+configurationTitle.swift b/Multimer/Multimer/Presentation/Support/Extension/UIButton+Rx+configurationTitle.swift new file mode 100644 index 0000000..c09b3a2 --- /dev/null +++ b/Multimer/Multimer/Presentation/Support/Extension/UIButton+Rx+configurationTitle.swift @@ -0,0 +1,18 @@ +// +// UIButton+Rx+configurationTitle.swift +// Multimer +// +// Created by 김상혁 on 2023/08/02. +// + +import RxSwift + +extension Reactive where Base: UIButton { + var configurationTitle: Binder { + return Binder(self.base) { button, title in + var configuration = button.configuration + configuration?.title = title + button.configuration = configuration + } + } +} diff --git a/Multimer/Multimer/Presentation/TimerCreate/TimerCreateViewModel.swift b/Multimer/Multimer/Presentation/TimerCreate/TimerCreateReactor.swift similarity index 79% rename from Multimer/Multimer/Presentation/TimerCreate/TimerCreateViewModel.swift rename to Multimer/Multimer/Presentation/TimerCreate/TimerCreateReactor.swift index 4bf3f32..6ff32b7 100644 --- a/Multimer/Multimer/Presentation/TimerCreate/TimerCreateViewModel.swift +++ b/Multimer/Multimer/Presentation/TimerCreate/TimerCreateReactor.swift @@ -1,5 +1,5 @@ // -// TimerCreateViewModel.swift +// TimerCreateReactor.swift // Multimer // // Created by 김상혁 on 2022/12/21. @@ -8,7 +8,7 @@ import ReactorKit import RxRelay -final class TimerCreateViewModel: Reactor { +final class TimerCreateReactor: Reactor { enum Action { case cancelButtonDidTap @@ -18,12 +18,14 @@ final class TimerCreateViewModel: Reactor { case tagDidSelect(Tag?) case timePickerViewDidEdit(Time) case timerTypeDidSelect(TimerType) + case ringtoneButtonDidTap } enum Mutation { case setTimePickerViewHidden(Bool) case setTimer(Timer) case setTimerNamePlaceholder(String) + case updateSelectedRingtone(Ringtone) } struct State { @@ -31,12 +33,14 @@ final class TimerCreateViewModel: Reactor { var isTimePickerViewHidden: Bool = false var timer = Timer(tag: Tag(isSelected: true, color: .label)) var timerNamePlaceholder: String = "" + var selectedRingtone: Ringtone = .default1 } let initialState = State() private weak var coordinator: HomeCoordinator? private let createdTimerRelay: PublishRelay + private let selectedRingtoneRelay = BehaviorRelay(value: .default1) init(coordinator: HomeCoordinator?, createdTimerRelay: PublishRelay) { self.coordinator = coordinator @@ -87,6 +91,9 @@ final class TimerCreateViewModel: Reactor { .just(.setTimerNamePlaceholder(selectedType.placeholder)), .just(.setTimer(timer)) ]) + + case .ringtoneButtonDidTap: + return showRingtoneSelectScene(selectedRingtoneRelay: selectedRingtoneRelay) } } @@ -103,6 +110,10 @@ final class TimerCreateViewModel: Reactor { case .setTimerNamePlaceholder(let placeholder): newState.timerNamePlaceholder = placeholder + + case .updateSelectedRingtone(let ringtone): + newState.selectedRingtone = ringtone + newState.timer.ringtone = ringtone } return newState } @@ -110,24 +121,28 @@ final class TimerCreateViewModel: Reactor { // MARK: - Supporting Methods -private extension TimerCreateViewModel { +private extension TimerCreateReactor { func validateCompleteButtonIsEnable(for timer: Timer) -> Bool { - let isNameEmpty = timer.name.isEmpty let isTypeCountUp = timer.type == .countUp let isTotalSecondsBiggerThanZero = timer.totalSeconds > .zero - let isCompleteButtonEnable = !isNameEmpty && (isTypeCountUp ? true : isTotalSecondsBiggerThanZero) + let isCompleteButtonEnable = isTypeCountUp ? true : isTotalSecondsBiggerThanZero return isCompleteButtonEnable } } // MARK: - Side Effect Methods -private extension TimerCreateViewModel { +private extension TimerCreateReactor { func exitScene() -> Observable { coordinator?.coordinate(by: .finishTimerCreateScene) return .empty() } + func showRingtoneSelectScene(selectedRingtoneRelay: BehaviorRelay) -> Observable { + coordinator?.coordinate(by: .showRingtoneSelectScene(selectedRingtoneRelay: selectedRingtoneRelay)) + return selectedRingtoneRelay.map { Mutation.updateSelectedRingtone($0) } + } + func acceptCreatedTimerRelay(createdTimer: Timer) -> Observable { createdTimerRelay.accept(createdTimer) return .empty() diff --git a/Multimer/Multimer/Presentation/TimerCreate/TimerCreateViewController.swift b/Multimer/Multimer/Presentation/TimerCreate/TimerCreateViewController.swift index 3486d98..a5eee42 100644 --- a/Multimer/Multimer/Presentation/TimerCreate/TimerCreateViewController.swift +++ b/Multimer/Multimer/Presentation/TimerCreate/TimerCreateViewController.swift @@ -77,12 +77,18 @@ final class TimerCreateViewController: UIViewController, View { }() private lazy var timePickerButtonStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [timePickerView, buttonStackView]) + let stackView = UIStackView(arrangedSubviews: [timePickerView, ringtoneButton, buttonStackView]) stackView.axis = .vertical stackView.spacing = 24 return stackView }() + private let ringtoneButton: RingtoneButton = { + let button = RingtoneButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: ViewSize.buttonFont) + return button + }() + var disposeBag = DisposeBag() override func touchesBegan(_ touches: Set, with event: UIEvent?){ @@ -96,7 +102,7 @@ final class TimerCreateViewController: UIViewController, View { layout() } - func bind(reactor: TimerCreateViewModel) { + func bind(reactor: TimerCreateReactor) { bindAction(reactor: reactor) bindState(reactor: reactor) } @@ -105,7 +111,7 @@ final class TimerCreateViewController: UIViewController, View { // MARK: - Bind Reactor private extension TimerCreateViewController { - func bindAction(reactor: TimerCreateViewModel) { + func bindAction(reactor: TimerCreateReactor) { timerTypeSegmentControl.rx.selectedSegmentIndex .compactMap { TimerType(rawValue: $0) } .map { Reactor.Action.timerTypeDidSelect($0) } @@ -151,9 +157,14 @@ private extension TimerCreateViewController { .map { Reactor.Action.timePickerViewDidEdit($0) } .bind(to: reactor.action) .disposed(by: disposeBag) + + ringtoneButton.rx.tap + .map { Reactor.Action.ringtoneButtonDidTap } + .bind(to: reactor.action) + .disposed(by: disposeBag) } - func bindState(reactor: TimerCreateViewModel) { + func bindState(reactor: TimerCreateReactor) { reactor.state.map { $0.isCompleteButtonEnabled } .distinctUntilChanged() .bind(to: completeButton.rx.isEnabled) @@ -177,7 +188,13 @@ private extension TimerCreateViewController { reactor.state.map { $0.isTimePickerViewHidden } .distinctUntilChanged() - .bind(to: timePickerView.rx.animated.flip(.top, duration: 0.35).isHidden) + .bind(to: timePickerView.rx.animated.flip(.top, duration: 0.35).isHidden, ringtoneButton.rx.isHidden) + .disposed(by: disposeBag) + + reactor.state.map { $0.selectedRingtone } + .distinctUntilChanged() + .map { LocalizableString.ringtoneName(ringtone: $0).localized } + .bind(to: ringtoneButton.rx.configurationTitle) .disposed(by: disposeBag) } } diff --git a/Multimer/Multimer/Presentation/TimerEditing/TimerEditingViewModel.swift b/Multimer/Multimer/Presentation/TimerEditing/TimerEditingReactor.swift similarity index 70% rename from Multimer/Multimer/Presentation/TimerEditing/TimerEditingViewModel.swift rename to Multimer/Multimer/Presentation/TimerEditing/TimerEditingReactor.swift index e72c431..9f50fb7 100644 --- a/Multimer/Multimer/Presentation/TimerEditing/TimerEditingViewModel.swift +++ b/Multimer/Multimer/Presentation/TimerEditing/TimerEditingReactor.swift @@ -1,5 +1,5 @@ // -// TimerEditingViewModel.swift +// TimerEditingReactor.swift // Multimer // // Created by 김상혁 on 2022/11/07. @@ -8,7 +8,7 @@ import ReactorKit import RxRelay -final class TimerEditingViewModel: Reactor { +final class TimerEditingReactor: Reactor { enum Action { case cancelButtonDidTap @@ -17,11 +17,13 @@ final class TimerEditingViewModel: Reactor { case tagDidSelect(Tag?) case timePickerViewDidEdit(Time) case viewDidLoad + case ringtoneButtonDidTap } enum Mutation { case setTimePickerViewIsHidden(Bool) case editTimer(Timer) + case updateSelectedRingtone(Ringtone) } struct State { @@ -36,6 +38,7 @@ final class TimerEditingViewModel: Reactor { private weak var coordinator: HomeCoordinator? private let editedTimerRelay: PublishRelay + private let selectedRingtoneRelay: BehaviorRelay init( initialTimer: Timer, @@ -45,6 +48,7 @@ final class TimerEditingViewModel: Reactor { initialState = State(editedTimer: initialTimer, initialTimer: initialTimer) self.coordinator = coordinator self.editedTimerRelay = editedTimerRelay + self.selectedRingtoneRelay = BehaviorRelay(value: initialTimer.ringtone ?? .default1) } func mutate(action: Action) -> Observable { @@ -78,6 +82,9 @@ final class TimerEditingViewModel: Reactor { let initialTimer = currentState.initialTimer let isTimePickerViewHidden = !initialTimer.type.shouldSetTime return .just(.setTimePickerViewIsHidden(isTimePickerViewHidden)) + + case .ringtoneButtonDidTap: + return showRingtoneSelectScene(selectedRingtoneRelay: selectedRingtoneRelay) } } @@ -90,8 +97,15 @@ final class TimerEditingViewModel: Reactor { case .editTimer(let editedTimer): newState.editedTimer = editedTimer newState.isCompleteButtonEnable = validateCompleteButtonIsEnable( - currentTimer: state.initialTimer, - newTimer: editedTimer + initialTimer: state.initialTimer, + newTimer: newState.editedTimer + ) + + case .updateSelectedRingtone(let ringtone): + newState.editedTimer.ringtone = ringtone + newState.isCompleteButtonEnable = validateCompleteButtonIsEnable( + initialTimer: state.initialTimer, + newTimer: newState.editedTimer ) } return newState @@ -100,25 +114,30 @@ final class TimerEditingViewModel: Reactor { // MARK: - Supporting Methods -private extension TimerEditingViewModel { - func validateCompleteButtonIsEnable(currentTimer: Timer, newTimer: Timer) -> Bool { +private extension TimerEditingReactor { + func validateCompleteButtonIsEnable(initialTimer: Timer, newTimer: Timer) -> Bool { switch newTimer.type { case .countDown: - return currentTimer != newTimer && newTimer.totalSeconds > 0 + return initialTimer != newTimer && newTimer.totalSeconds > 0 case .countUp: - return currentTimer != newTimer + return initialTimer != newTimer } } } // MARK: - Side Effect Methods -private extension TimerEditingViewModel { +private extension TimerEditingReactor { func exitScene() -> Observable { coordinator?.coordinate(by: .finishTimerEditScene) return .empty() } + func showRingtoneSelectScene(selectedRingtoneRelay: BehaviorRelay) -> Observable { + coordinator?.coordinate(by: .showRingtoneSelectScene(selectedRingtoneRelay: selectedRingtoneRelay)) + return selectedRingtoneRelay.map { Mutation.updateSelectedRingtone($0) } + } + func acceptEditedTimerRelay(editedTimer: Timer) -> Observable { editedTimerRelay.accept(editedTimer) return .empty() diff --git a/Multimer/Multimer/Presentation/TimerEditing/TimerEditingViewController.swift b/Multimer/Multimer/Presentation/TimerEditing/TimerEditingViewController.swift index 53c637c..a617cb6 100644 --- a/Multimer/Multimer/Presentation/TimerEditing/TimerEditingViewController.swift +++ b/Multimer/Multimer/Presentation/TimerEditing/TimerEditingViewController.swift @@ -64,12 +64,18 @@ final class TimerEditingViewController: UIViewController, View { }() private lazy var timePickerButtonStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [timePickerView, buttonStackView]) + let stackView = UIStackView(arrangedSubviews: [timePickerView, ringtoneButton, buttonStackView]) stackView.axis = .vertical stackView.spacing = 36 return stackView }() + private let ringtoneButton: RingtoneButton = { + let button = RingtoneButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: ViewSize.buttonFont) + return button + }() + var disposeBag = DisposeBag() override func touchesBegan(_ touches: Set, with event: UIEvent?){ @@ -83,7 +89,7 @@ final class TimerEditingViewController: UIViewController, View { layout() } - func bind(reactor: TimerEditingViewModel) { + func bind(reactor: TimerEditingReactor) { bindAction(reactor: reactor) bindState(reactor: reactor) } @@ -92,7 +98,7 @@ final class TimerEditingViewController: UIViewController, View { // MARK: - Bind Reactor private extension TimerEditingViewController { - func bindAction(reactor: TimerEditingViewModel) { + func bindAction(reactor: TimerEditingReactor) { rx.viewDidLoad .map { Reactor.Action.viewDidLoad } .bind(to: reactor.action) @@ -133,9 +139,14 @@ private extension TimerEditingViewController { .map { Reactor.Action.timePickerViewDidEdit($0) } .bind(to: reactor.action) .disposed(by: disposeBag) + + ringtoneButton.rx.tap + .map { Reactor.Action.ringtoneButtonDidTap } + .bind(to: reactor.action) + .disposed(by: disposeBag) } - func bindState(reactor: TimerEditingViewModel) { + func bindState(reactor: TimerEditingReactor) { reactor.state.map { $0.initialTimer } .distinctUntilChanged() .observe(on: MainScheduler.asyncInstance) @@ -158,7 +169,13 @@ private extension TimerEditingViewController { reactor.state.map { $0.isTimePickerViewHidden } .distinctUntilChanged() - .bind(to: timePickerView.rx.isHidden) + .bind(to: timePickerView.rx.isHidden, ringtoneButton.rx.isHidden) + .disposed(by: disposeBag) + + reactor.state.map { $0.editedTimer.ringtone } + .distinctUntilChanged() + .map { LocalizableString.ringtoneName(ringtone: $0 ?? .default1).localized } + .bind(to: ringtoneButton.rx.configurationTitle) .disposed(by: disposeBag) } } diff --git a/Multimer/Multimer/Ringtones/alarm.aiff b/Multimer/Multimer/Ringtones/alarm.aiff new file mode 100644 index 0000000..20effb4 Binary files /dev/null and b/Multimer/Multimer/Ringtones/alarm.aiff differ diff --git a/Multimer/Multimer/Ringtones/bark.aiff b/Multimer/Multimer/Ringtones/bark.aiff new file mode 100644 index 0000000..2235e5b Binary files /dev/null and b/Multimer/Multimer/Ringtones/bark.aiff differ diff --git a/Multimer/Multimer/Ringtones/beacon.aiff b/Multimer/Multimer/Ringtones/beacon.aiff new file mode 100644 index 0000000..6376256 Binary files /dev/null and b/Multimer/Multimer/Ringtones/beacon.aiff differ diff --git a/Multimer/Multimer/Ringtones/bulletin.aiff b/Multimer/Multimer/Ringtones/bulletin.aiff new file mode 100644 index 0000000..47d84b3 Binary files /dev/null and b/Multimer/Multimer/Ringtones/bulletin.aiff differ diff --git a/Multimer/Multimer/Ringtones/default1.aiff b/Multimer/Multimer/Ringtones/default1.aiff new file mode 100644 index 0000000..55ac241 Binary files /dev/null and b/Multimer/Multimer/Ringtones/default1.aiff differ diff --git a/Multimer/Multimer/Ringtones/default2.aiff b/Multimer/Multimer/Ringtones/default2.aiff new file mode 100644 index 0000000..a2a937b Binary files /dev/null and b/Multimer/Multimer/Ringtones/default2.aiff differ diff --git a/Multimer/Multimer/Ringtones/default3.aiff b/Multimer/Multimer/Ringtones/default3.aiff new file mode 100644 index 0000000..008ce28 Binary files /dev/null and b/Multimer/Multimer/Ringtones/default3.aiff differ diff --git a/Multimer/Multimer/Ringtones/default4.aiff b/Multimer/Multimer/Ringtones/default4.aiff new file mode 100644 index 0000000..c816260 Binary files /dev/null and b/Multimer/Multimer/Ringtones/default4.aiff differ diff --git a/Multimer/Multimer/Ringtones/default5.aiff b/Multimer/Multimer/Ringtones/default5.aiff new file mode 100644 index 0000000..296fda9 Binary files /dev/null and b/Multimer/Multimer/Ringtones/default5.aiff differ diff --git a/Multimer/Multimer/Ringtones/default6.aiff b/Multimer/Multimer/Ringtones/default6.aiff new file mode 100644 index 0000000..8a256a0 Binary files /dev/null and b/Multimer/Multimer/Ringtones/default6.aiff differ diff --git a/Multimer/Multimer/Ringtones/default7.aiff b/Multimer/Multimer/Ringtones/default7.aiff new file mode 100644 index 0000000..c6ec822 Binary files /dev/null and b/Multimer/Multimer/Ringtones/default7.aiff differ diff --git a/Multimer/Multimer/Ringtones/default8.aiff b/Multimer/Multimer/Ringtones/default8.aiff new file mode 100644 index 0000000..19e6204 Binary files /dev/null and b/Multimer/Multimer/Ringtones/default8.aiff differ diff --git a/Multimer/Multimer/Ringtones/duck.aiff b/Multimer/Multimer/Ringtones/duck.aiff new file mode 100644 index 0000000..8a55e94 Binary files /dev/null and b/Multimer/Multimer/Ringtones/duck.aiff differ diff --git a/Multimer/Multimer/Ringtones/illuminate.aiff b/Multimer/Multimer/Ringtones/illuminate.aiff new file mode 100644 index 0000000..f0f5319 Binary files /dev/null and b/Multimer/Multimer/Ringtones/illuminate.aiff differ diff --git a/Multimer/Multimer/Ringtones/marimba.aiff b/Multimer/Multimer/Ringtones/marimba.aiff new file mode 100644 index 0000000..cc0707e Binary files /dev/null and b/Multimer/Multimer/Ringtones/marimba.aiff differ diff --git a/Multimer/Multimer/Ringtones/oldPhone.aiff b/Multimer/Multimer/Ringtones/oldPhone.aiff new file mode 100644 index 0000000..905152f Binary files /dev/null and b/Multimer/Multimer/Ringtones/oldPhone.aiff differ diff --git a/Multimer/Multimer/Ringtones/pianoRiff.aiff b/Multimer/Multimer/Ringtones/pianoRiff.aiff new file mode 100644 index 0000000..69f4130 Binary files /dev/null and b/Multimer/Multimer/Ringtones/pianoRiff.aiff differ diff --git a/Multimer/Multimer/Ringtones/pinball.aiff b/Multimer/Multimer/Ringtones/pinball.aiff new file mode 100644 index 0000000..34b29c1 Binary files /dev/null and b/Multimer/Multimer/Ringtones/pinball.aiff differ diff --git a/Multimer/Multimer/Ringtones/presto.aiff b/Multimer/Multimer/Ringtones/presto.aiff new file mode 100644 index 0000000..be17568 Binary files /dev/null and b/Multimer/Multimer/Ringtones/presto.aiff differ diff --git a/Multimer/Multimer/Ringtones/radar.aiff b/Multimer/Multimer/Ringtones/radar.aiff new file mode 100644 index 0000000..2dde54c Binary files /dev/null and b/Multimer/Multimer/Ringtones/radar.aiff differ diff --git a/Multimer/Multimer/Ringtones/reflection.aiff b/Multimer/Multimer/Ringtones/reflection.aiff new file mode 100644 index 0000000..01753b2 Binary files /dev/null and b/Multimer/Multimer/Ringtones/reflection.aiff differ diff --git a/Multimer/Multimer/Ringtones/sencha.aiff b/Multimer/Multimer/Ringtones/sencha.aiff new file mode 100644 index 0000000..abbc324 Binary files /dev/null and b/Multimer/Multimer/Ringtones/sencha.aiff differ diff --git a/Multimer/Multimer/Ringtones/signal.aiff b/Multimer/Multimer/Ringtones/signal.aiff new file mode 100644 index 0000000..c71a1b4 Binary files /dev/null and b/Multimer/Multimer/Ringtones/signal.aiff differ diff --git a/Multimer/Multimer/Ringtones/stargaze.aiff b/Multimer/Multimer/Ringtones/stargaze.aiff new file mode 100644 index 0000000..e341adc Binary files /dev/null and b/Multimer/Multimer/Ringtones/stargaze.aiff differ diff --git a/Multimer/Multimer/Ringtones/strum.aiff b/Multimer/Multimer/Ringtones/strum.aiff new file mode 100644 index 0000000..077d824 Binary files /dev/null and b/Multimer/Multimer/Ringtones/strum.aiff differ diff --git a/Multimer/Multimer/Ringtones/trill.aiff b/Multimer/Multimer/Ringtones/trill.aiff new file mode 100644 index 0000000..cba52d2 Binary files /dev/null and b/Multimer/Multimer/Ringtones/trill.aiff differ diff --git a/Multimer/Multimer/Ringtones/xylophone.aiff b/Multimer/Multimer/Ringtones/xylophone.aiff new file mode 100644 index 0000000..1390717 Binary files /dev/null and b/Multimer/Multimer/Ringtones/xylophone.aiff differ