├── .gitattributes ├── .github ├── COMMIT_TEMPLATE │ └── message.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── task.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── JipJung ├── .swiftlint.yml ├── JipJung.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ ├── xcbaselines │ │ └── DD94DBFB2774B22600CE843B.xcbaseline │ │ │ ├── D2585D12-5BBB-4F99-BC0C-66E5C84BFC7E.plist │ │ │ └── Info.plist │ │ └── xcschemes │ │ └── JipJung.xcscheme ├── JipJung.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── JipJung │ ├── Application │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── 1024.png │ │ │ │ ├── 114.png │ │ │ │ ├── 120.png │ │ │ │ ├── 180.png │ │ │ │ ├── 29.png │ │ │ │ ├── 40.png │ │ │ │ ├── 57.png │ │ │ │ ├── 58.png │ │ │ │ ├── 60.png │ │ │ │ ├── 80.png │ │ │ │ ├── 87.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── app_icon.imageset │ │ │ │ ├── 1024.png │ │ │ │ └── Contents.json │ │ │ ├── focus_breath.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── focus_breath.png │ │ │ ├── focus_default.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── focus_default.png │ │ │ ├── focus_infinite.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── focus_infinite.png │ │ │ └── focus_pomodoro.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── focus_pomodoro.png │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ └── SceneDelegate.swift │ ├── Data │ │ └── Repositories │ │ │ ├── FavoriteRepository.swift │ │ │ ├── FocusTimeRepository.swift │ │ │ ├── MaximListRepository.swift │ │ │ ├── MediaListRepository.swift │ │ │ ├── MediaResourceRepository.swift │ │ │ ├── PlayHistoryRepository.swift │ │ │ └── SearchHistoryRepository.swift │ ├── Domain │ │ ├── Entities │ │ │ ├── BrightMedia.swift │ │ │ ├── DBStructureForJSON.swift │ │ │ ├── DarknessMedia.swift │ │ │ ├── DateFocusRecordDTO.swift │ │ │ ├── FavoriteMedia.swift │ │ │ ├── FocusRecord.swift │ │ │ ├── Maxim.swift │ │ │ ├── Media.swift │ │ │ └── PlayHistory.swift │ │ ├── Managers │ │ │ ├── Audio+Enums.swift │ │ │ └── AudioPlayManager.swift │ │ └── UseCases │ │ │ ├── AudioPlayUseCase.swift │ │ │ ├── FavoriteMediaUseCase.swift │ │ │ ├── FetchMediaURLUseCase.swift │ │ │ ├── LoadFocusTimeUseCase.swift │ │ │ ├── MaximListUseCase.swift │ │ │ ├── MediaListUseCase.swift │ │ │ ├── PlayHistoryUseCase.swift │ │ │ ├── SaveFocusTimeUseCase.swift │ │ │ ├── SearchHistoryUseCase.swift │ │ │ └── SearchMediaUseCase.swift │ ├── Dummy │ │ └── DummyFocusData.json │ ├── Info.plist │ ├── Infra │ │ ├── Bundle │ │ │ └── BundleManager.swift │ │ ├── File │ │ │ ├── LocalFileEnums.swift │ │ │ └── LocalFileManager.swift │ │ ├── LocalDB │ │ │ ├── LocalDBEnums.swift │ │ │ └── RealmDBManager.swift │ │ ├── Network │ │ │ ├── NetworkEnums.swift │ │ │ └── RemoteServiceProvider.swift │ │ └── UserDefaults │ │ │ └── UserDefaultsStorage.swift │ ├── Resource │ │ ├── .gitkeep │ │ └── ResourceData.json │ ├── UI │ │ ├── Animation │ │ │ ├── ClubSKScene.swift │ │ │ ├── CountdownView.swift │ │ │ ├── CycleAnimation.swift │ │ │ ├── PulseAnimation.swift │ │ │ ├── SlowPresent.swift │ │ │ └── WiggleAnimation.swift │ │ ├── Common │ │ │ ├── Enums │ │ │ │ └── Common+Enums.swift │ │ │ ├── Extensions │ │ │ │ ├── Array+Extension.swift │ │ │ │ ├── CALayer+Extension.swift │ │ │ │ ├── CGPoint+Extension.swift │ │ │ │ ├── Date+Extension.swift │ │ │ │ ├── Int+Extension.swift │ │ │ │ ├── Notification+Extension.swift │ │ │ │ ├── UIApplication+Extension.swift │ │ │ │ ├── UICollectionView+Extension.swift │ │ │ │ ├── UICollectionViewCell+Extension.swift │ │ │ │ ├── UIColor+Extension.swift │ │ │ │ ├── UILabel+Extension.swift │ │ │ │ ├── UIScreen+Extension.swift │ │ │ │ ├── UITableView+Extension.swift │ │ │ │ ├── UITableViewCell+Extension.swift │ │ │ │ └── UIView+Extension.swift │ │ │ └── Views │ │ │ │ ├── CloseButton.swift │ │ │ │ └── MediaCollectionViewCell.swift │ │ ├── Explore │ │ │ └── Main │ │ │ │ ├── ViewModels │ │ │ │ ├── ExploreViewModel.swift │ │ │ │ └── SearchViewModel.swift │ │ │ │ └── Views │ │ │ │ ├── ExploreViewController.swift │ │ │ │ ├── SearchTableViewCell.swift │ │ │ │ ├── SearchViewController.swift │ │ │ │ └── SoundTagCollectionViewCell.swift │ │ ├── Home │ │ │ ├── Focus │ │ │ │ ├── Enums.swift │ │ │ │ ├── Focus+Enums.swift │ │ │ │ ├── ViewModels │ │ │ │ │ ├── BreathFocusViewModel.swift │ │ │ │ │ ├── DefaultFocusViewModel.swift │ │ │ │ │ ├── InfinityFocusViewModel.swift │ │ │ │ │ └── PomodoroFocusViewModel.swift │ │ │ │ └── Views │ │ │ │ │ ├── BreathFocusViewController.swift │ │ │ │ │ ├── DefaultFocusViewController.swift │ │ │ │ │ ├── FocusButtons.swift │ │ │ │ │ ├── FocusViewController.swift │ │ │ │ │ ├── FocusViewControllerFactory.swift │ │ │ │ │ ├── InfinityFocusViewController.swift │ │ │ │ │ └── PomodoroFocusViewController.swift │ │ │ ├── Home+Enums.swift │ │ │ ├── Main │ │ │ │ ├── ViewModels │ │ │ │ │ ├── FavoriteViewModel.swift │ │ │ │ │ ├── HomeViewModel.swift │ │ │ │ │ ├── MediaPlayViewModel.swift │ │ │ │ │ └── PlayHistoryViewModel.swift │ │ │ │ └── Views │ │ │ │ │ ├── BlurCircleButton.swift │ │ │ │ │ ├── CarouselView.swift │ │ │ │ │ ├── FavoriteMusicViewController.swift │ │ │ │ │ ├── FocusButton.swift │ │ │ │ │ ├── HomeListHeaderView.swift │ │ │ │ │ ├── HomeViewController.swift │ │ │ │ │ ├── MediaPlayView.swift │ │ │ │ │ ├── PlayHistoryViewController.swift │ │ │ │ │ └── TouchTransferView.swift │ │ │ └── Maxim │ │ │ │ ├── Maxim+Enums.swift │ │ │ │ ├── ViewModels │ │ │ │ ├── MaximPresenterObject.swift │ │ │ │ └── MaximViewModel.swift │ │ │ │ └── Views │ │ │ │ ├── MaximCalendarHeaderCollectionViewCell.swift │ │ │ │ ├── MaximCollectionViewCell.swift │ │ │ │ ├── MaximHeaderCollectionViewCell.swift │ │ │ │ └── MaximViewController.swift │ │ ├── Me │ │ │ ├── Main │ │ │ │ ├── ViewModels │ │ │ │ │ └── MeViewModel.swift │ │ │ │ └── Views │ │ │ │ │ ├── GrassMapView.swift │ │ │ │ │ ├── GrassPresenterObject.swift │ │ │ │ │ ├── MeDailyStaticsView.swift │ │ │ │ │ ├── MeTableViewDailiyStaticsCell.swift │ │ │ │ │ └── MeViewController.swift │ │ │ └── Me+Enums.swift │ │ └── Sound │ │ │ ├── ViewModels │ │ │ └── MediaPlayerViewModel.swift │ │ │ └── Views │ │ │ ├── MediaDescriptionView.swift │ │ │ ├── MediaPlayerMaximView.swift │ │ │ ├── MediaPlayerViewController.swift │ │ │ ├── MusicPlayerButtons.swift │ │ │ └── TagCollectionViewCell.swift │ └── Utils │ │ ├── ApplicationLaunch.swift │ │ ├── ApplicationMode.swift │ │ ├── FeedbackGenerator.swift │ │ ├── LocalDBMigrator.swift │ │ ├── PushNotificationManager.swift │ │ └── Utils+Enums.swift ├── LocalFileManagerTests │ └── LocalFileManagerTests.swift ├── MaximTests │ ├── MaximListRepositoryStub.swift │ └── MaximListUseCaseTests.swift ├── MeTests │ ├── LoadFocusTimeRepositoryStub.swift │ └── LoadFocusTimeUseCaseTests.swift ├── Podfile ├── Podfile.lock ├── RealmTests │ └── FocusTimeRealmTests.swift └── UserDefaultsStorageTest │ └── UserDefaultsStorageTest.swift └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj binary merge=union -------------------------------------------------------------------------------- /.github/COMMIT_TEMPLATE/message.md: -------------------------------------------------------------------------------- 1 | # <타입> <제목> 의 형식으로 제목을 아래 공백줄에 작성 2 | # 제목은 72자 이내 / 변경사항이 "무엇"인지 명확히 작성 / 끝에 마침표 금지 3 | # 예) ✨ 로그인 기능 추가 4 | 5 | # 바로 아래 공백은 지우지 마세요 (제목과 본문의 분리를 위함) 6 | 7 | ################ 8 | # 본문(구체적인 내용)을 아랫줄에 작성 9 | # 여러 줄의 메시지를 작성할 땐 "-"로 구분 (한 줄은 72자 이내) 10 | # 1. 무엇을 수정했는지 11 | # 2. 왜 수정했는지 12 | # 3. 한글로 작성 13 | 14 | ################ 15 | # ✨:sparkles: 기능(새로운 기능) #이슈번호 16 | # 🐛:bug: 버그 (버그 수정) #이슈번호 17 | # ♻️:recycle: 리팩토링 #이슈번호 18 | # 💄:lipstick: UI 변경 #이슈번호 19 | # 🎨:art: 스타일 (코드 형식, 세미콜론 추가: 비즈니스 로직에 변경 없음) #이슈번호 20 | # 📝:memo: 문서 (문서 추가, 수정, 삭제) #이슈번호 21 | # ✅:white_check_mark: 테스트 (테스트 코드 추가, 수정, 삭제: 비즈니스 로직에 변경 없음) #이슈번호 22 | # 🔥:fire: 코드, 파일 삭제 #이슈번호 23 | # 📦️:package: 관련 라이브러리 추가 (빌드 스크립트 수정 등) #이슈번호 24 | # 🚀:rocket: 배포 관련 #이슈번호 25 | ################ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Task로 정하지 않은 예상치 않은 버그를 처리합니다. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 설명 11 | 12 | ## 상황 재현 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | ## 버그 해결 후 예상 결과 20 | 21 | ## 추가 자료(스크린샷 등) 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Task단위로 쪼갠 작업에 Issue를 할당합니다. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 설명 11 | 12 | ## To-do List 13 | - [ ] 14 | - [ ] 15 | - [ ] 16 | 17 | ## 사용할 기술 18 | 19 | ## 추가할 사항 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # 작업 내용 2 | - 내용을 적어주세요 3 | 4 | # 관련된 이슈 번호 5 | `#숫자, #숫자, #숫자 ...` 6 | `#숫자: 메인화면에 버튼 색상을 변경하는 부분` 7 | `#숫자: A버튼 터치할 때 넘어가는 화면 전환 애니메이션 부분` 8 | - 왜 관련되어있는지 간단한 설명도 적어주세요 9 | 10 | # 스크린 샷(optional) 11 | | 화면 제목을 적어주세요 | 12 | |--| 13 | | | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,swift,swiftpackagemanager,xcode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,swift,swiftpackagemanager,xcode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Swift ### 35 | # Xcode 36 | # 37 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 38 | 39 | ## User settings 40 | xcuserdata/ 41 | 42 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 43 | *.xcscmblueprint 44 | *.xccheckout 45 | 46 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 47 | build/ 48 | DerivedData/ 49 | *.moved-aside 50 | *.pbxuser 51 | !default.pbxuser 52 | *.mode1v3 53 | !default.mode1v3 54 | *.mode2v3 55 | !default.mode2v3 56 | *.perspectivev3 57 | !default.perspectivev3 58 | 59 | ## Obj-C/Swift specific 60 | *.hmap 61 | 62 | ## App packaging 63 | *.ipa 64 | *.dSYM.zip 65 | *.dSYM 66 | 67 | ## Playgrounds 68 | timeline.xctimeline 69 | playground.xcworkspace 70 | 71 | # Swift Package Manager 72 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 73 | # Packages/ 74 | # Package.pins 75 | # Package.resolved 76 | # *.xcodeproj 77 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 78 | # hence it is not needed unless you have added a package configuration file to your project 79 | # .swiftpm 80 | 81 | .build/ 82 | 83 | # CocoaPods 84 | # We recommend against adding the Pods directory to your .gitignore. However 85 | # you should judge for yourself, the pros and cons are mentioned at: 86 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 87 | Pods/ 88 | # Add this line if you want to avoid checking in source code from the Xcode workspace 89 | # *.xcworkspace 90 | 91 | # Carthage 92 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 93 | # Carthage/Checkouts 94 | 95 | Carthage/Build/ 96 | 97 | # Accio dependency management 98 | Dependencies/ 99 | .accio/ 100 | 101 | # fastlane 102 | # It is recommended to not store the screenshots in the git repo. 103 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 104 | # For more information about the recommended setup visit: 105 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 106 | 107 | fastlane/report.xml 108 | fastlane/Preview.html 109 | fastlane/screenshots/**/*.png 110 | fastlane/test_output 111 | 112 | # Code Injection 113 | # After new code Injection tools there's a generated folder /iOSInjectionProject 114 | # https://github.com/johnno1962/injectionforxcode 115 | 116 | iOSInjectionProject/ 117 | 118 | ### SwiftPackageManager ### 119 | # Packages 120 | # xcuserdata 121 | # *.xcodeproj 122 | 123 | 124 | ### Xcode ### 125 | # Xcode 126 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 127 | 128 | 129 | 130 | 131 | ## Gcc Patch 132 | /*.gcno 133 | 134 | ### Xcode Patch ### 135 | *.xcodeproj/* 136 | !*.xcodeproj/project.pbxproj 137 | !*.xcodeproj/xcshareddata/ 138 | !*.xcworkspace/contents.xcworkspacedata 139 | **/xcshareddata/WorkspaceSettings.xcsettings 140 | 141 | # End of https://www.toptal.com/developers/gitignore/api/macos,swift,swiftpackagemanager,xcode 142 | 143 | ## Firebase 144 | GoogleService-Info.plist 145 | 146 | ## Media Resource 147 | **/Resource/* 148 | 149 | ## gitkeep 150 | !.gitkeep -------------------------------------------------------------------------------- /JipJung/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Pods 3 | - MaximTests 4 | - RealmTests 5 | - MeTests 6 | disabled_rules: 7 | - switch_case_alignment 8 | - trailing_whitespace 9 | - type_body_length 10 | - function_body_length 11 | opt_in_rules: 12 | - force_unwrapping 13 | 14 | force_unwrapping: error 15 | force_cast: error 16 | force_try: error 17 | comma: error 18 | vertical_whitespace: 19 | severity: error 20 | colon: 21 | severity: error 22 | 23 | line_length: 24 | warning: 120 25 | ignores_function_declarations: true 26 | ignores_comments: true 27 | ignores_interpolated_strings: true 28 | ignores_urls: true 29 | identifier_name: 30 | excluded: 31 | - id 32 | -------------------------------------------------------------------------------- /JipJung/JipJung.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JipJung/JipJung.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JipJung/JipJung.xcodeproj/xcshareddata/xcbaselines/DD94DBFB2774B22600CE843B.xcbaseline/D2585D12-5BBB-4F99-BC0C-66E5C84BFC7E.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | FocusTimeRealmTests 8 | 9 | testPerformanceExample() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.009917 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /JipJung/JipJung.xcodeproj/xcshareddata/xcbaselines/DD94DBFB2774B22600CE843B.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | D2585D12-5BBB-4F99-BC0C-66E5C84BFC7E 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 0 13 | cpuCount 14 | 1 15 | cpuKind 16 | Apple M1 17 | cpuSpeedInMHz 18 | 0 19 | logicalCPUCoresPerPackage 20 | 8 21 | modelCode 22 | MacBookAir10,1 23 | physicalCPUCoresPerPackage 24 | 8 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | arm64 30 | targetDevice 31 | 32 | modelCode 33 | iPhone12,3 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /JipJung/JipJung.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /JipJung/JipJung.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/01. 6 | // 7 | 8 | import UIKit 9 | 10 | import Firebase 11 | 12 | @main 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | private let applicationLaunch = ApplicationLaunch() 15 | 16 | func application( 17 | _ application: UIApplication, 18 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 19 | ) -> Bool { 20 | applicationLaunch.start() 21 | return true 22 | } 23 | 24 | func application( 25 | _ application: UIApplication, 26 | configurationForConnecting connectingSceneSession: UISceneSession, 27 | options: UIScene.ConnectionOptions 28 | ) -> UISceneConfiguration { 29 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "1024.png", 71 | "idiom" : "ios-marketing", 72 | "scale" : "1x", 73 | "size" : "1024x1024" 74 | } 75 | ], 76 | "info" : { 77 | "author" : "xcode", 78 | "version" : 1 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/app_icon.imageset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/app_icon.imageset/1024.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/app_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "1024.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_breath.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "focus_breath.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_breath.imageset/focus_breath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/focus_breath.imageset/focus_breath.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_default.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "focus_default.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_default.imageset/focus_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/focus_default.imageset/focus_default.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_infinite.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "focus_infinite.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_infinite.imageset/focus_infinite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/focus_infinite.imageset/focus_infinite.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_pomodoro.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "focus_pomodoro.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Assets.xcassets/focus_pomodoro.imageset/focus_pomodoro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Application/Assets.xcassets/focus_pomodoro.imageset/focus_pomodoro.png -------------------------------------------------------------------------------- /JipJung/JipJung/Application/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /JipJung/JipJung/Application/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/01. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | var window: UIWindow? 12 | 13 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 14 | guard let windowScene = (scene as? UIWindowScene) else { return } 15 | window = UIWindow(frame: windowScene.coordinateSpace.bounds) 16 | window?.windowScene = windowScene 17 | window?.rootViewController = createTabBarController() 18 | window?.makeKeyAndVisible() 19 | } 20 | 21 | private func createTabBarController() -> UITabBarController { 22 | let tabBarController = UITabBarController() 23 | self.configure(tabBarController.tabBar) 24 | 25 | tabBarController.viewControllers = [ 26 | createHomeViewController(), 27 | createExploreNavigationViewController(), 28 | createMeViewController() 29 | ] 30 | return tabBarController 31 | } 32 | 33 | private func configure(_ tabBar: UITabBar) { 34 | tabBar.backgroundImage = UIImage() 35 | tabBar.barTintColor = .clear 36 | tabBar.tintColor = .black 37 | tabBar.unselectedItemTintColor = .gray 38 | tabBar.makeBlurBackground() 39 | } 40 | 41 | private func createHomeViewController() -> UIViewController { 42 | let tabBarItem = UITabBarItem( 43 | title: TabBarItems.Home.title, 44 | image: UIImage(systemName: TabBarItems.Home.image), 45 | tag: 0 46 | ) 47 | 48 | let homeViewController = HomeViewController() 49 | homeViewController.tabBarItem = tabBarItem 50 | return homeViewController 51 | } 52 | 53 | private func createExploreNavigationViewController() -> UIViewController { 54 | let tabBarItem = UITabBarItem( 55 | title: TabBarItems.Explore.title, 56 | image: UIImage(systemName: TabBarItems.Explore.image), 57 | tag: 1 58 | ) 59 | 60 | let exploreViewController = ExploreViewController() 61 | let exploreNavigationViewController = UINavigationController(rootViewController: exploreViewController) 62 | exploreNavigationViewController.tabBarItem = tabBarItem 63 | return exploreNavigationViewController 64 | } 65 | 66 | private func createMeViewController() -> UIViewController { 67 | let tabBarItem = UITabBarItem( 68 | title: TabBarItems.Me.title, 69 | image: UIImage(systemName: TabBarItems.Me.image), 70 | tag: 2 71 | ) 72 | 73 | let meViewController = MeViewController() 74 | let meNavigationController = UINavigationController(rootViewController: meViewController) 75 | meNavigationController.tabBarItem = tabBarItem 76 | meNavigationController.navigationBar.prefersLargeTitles = true 77 | return meNavigationController 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /JipJung/JipJung/Data/Repositories/FavoriteRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyFavoriteRepository.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class FavoriteRepository { 13 | private let localDBManager = RealmDBManager.shared 14 | 15 | func create(mediaID: String) -> Single { 16 | return Single.create { [weak self] single in 17 | guard let self = self else { 18 | single(.failure(RealmError.initFailed)) 19 | return Disposables.create() 20 | } 21 | 22 | do { 23 | let newFavorite = FavoriteMedia(mediaID: mediaID) 24 | try newFavorite.autoIncrease() 25 | try self.localDBManager.add(newFavorite) 26 | } catch { 27 | single(.failure(error)) 28 | } 29 | single(.success(true)) 30 | return Disposables.create() 31 | } 32 | } 33 | 34 | func read() -> Single<[Media]> { 35 | return Single.create { [weak self] single in 36 | guard let self = self else { 37 | single(.failure(RealmError.initFailed)) 38 | return Disposables.create() 39 | } 40 | 41 | do { 42 | let list = try self.localDBManager.objects(ofType: FavoriteMedia.self) 43 | 44 | var favoriteDict: [String: Int] = [:] 45 | list.forEach { element in 46 | favoriteDict[element.mediaID] = element.id 47 | } 48 | 49 | let favoriteIDs = Array(favoriteDict.keys) 50 | let predicate = NSPredicate.init(format: "id IN %@", favoriteIDs) 51 | let filteredMedia = try self.localDBManager.objects(ofType: Media.self, with: predicate) 52 | let result = filteredMedia.sorted { 53 | guard let lhs = favoriteDict[$0.id], 54 | let rhs = favoriteDict[$1.id] 55 | else { 56 | return false 57 | } 58 | return lhs > rhs 59 | } 60 | single(.success(result)) 61 | } catch { 62 | single(.failure(RealmError.searchFailed)) 63 | } 64 | 65 | return Disposables.create() 66 | } 67 | } 68 | 69 | func read(mediaID: String) -> Single<[FavoriteMedia]> { 70 | return Single.create { [weak self] single in 71 | guard let self = self else { 72 | single(.failure(RealmError.initFailed)) 73 | return Disposables.create() 74 | } 75 | 76 | do { 77 | let predicate = NSPredicate(format: "mediaID == %@", mediaID) 78 | let result = try self.localDBManager.objects( 79 | ofType: FavoriteMedia.self, 80 | with: predicate 81 | ) 82 | single(.success(result)) 83 | } catch { 84 | single(.failure(RealmError.searchFailed)) 85 | } 86 | return Disposables.create() 87 | } 88 | } 89 | 90 | func delete(mediaID: String) -> Single { 91 | return Single.create { [weak self] single in 92 | guard let self = self else { 93 | single(.failure(RealmError.initFailed)) 94 | return Disposables.create() 95 | } 96 | 97 | do { 98 | let predicate = NSPredicate(format: "mediaID = %@", mediaID) 99 | 100 | let deletingMedia = try self.localDBManager.objects( 101 | ofType: FavoriteMedia.self, 102 | with: predicate 103 | ).first 104 | 105 | if let deletingMedia = deletingMedia { 106 | try self.localDBManager.delete(deletingMedia) 107 | } 108 | 109 | single(.success(true)) 110 | } catch { 111 | single(.failure(RealmError.deleteFailed)) 112 | } 113 | return Disposables.create() 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /JipJung/JipJung/Data/Repositories/FocusTimeRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusTimeRepository.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class FocusTimeRepository: FocusTimeRepositoryProtocol { 13 | private let localDBManager = RealmDBManager.shared 14 | 15 | func create(record: FocusRecord) -> Single { 16 | return Single.create { [weak self] single in 17 | guard let self = self else { 18 | single(.failure(RealmError.initFailed)) 19 | return Disposables.create() 20 | } 21 | 22 | do { 23 | let dateKey = "\(record.year)\(record.month)\(record.day)" 24 | let dateFocusRecord: DateFocusRecord 25 | if let dateFocusRecordObject = try self.localDBManager.object(ofType: DateFocusRecord.self, forPrimaryKey: dateKey) { 26 | dateFocusRecord = dateFocusRecordObject 27 | try dateFocusRecord.realm?.write({ 28 | dateFocusRecord.focusTime.append(record) 29 | }) 30 | } else { 31 | dateFocusRecord = DateFocusRecord(id: dateKey) 32 | dateFocusRecord.focusTime.append(record) 33 | } 34 | try self.localDBManager.add(dateFocusRecord) 35 | } catch { 36 | single(.failure(error)) 37 | } 38 | single(.success(true)) 39 | return Disposables.create() 40 | } 41 | } 42 | 43 | func read(date: Date) -> Single { 44 | return Single.create { [weak self] single in 45 | guard let self = self else { 46 | single(.failure(RealmError.initFailed)) 47 | return Disposables.create() 48 | } 49 | 50 | do { 51 | if let result = try self.localDBManager.object( 52 | ofType: DateFocusRecord.self, 53 | forPrimaryKey: date.realmId 54 | ) { 55 | single(.success(result)) 56 | } else { 57 | single(.success(DateFocusRecord(id: date))) 58 | } 59 | } catch { 60 | single(.failure(RealmError.searchFailed)) 61 | } 62 | return Disposables.create() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /JipJung/JipJung/Data/Repositories/MaximListRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximListRepository.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MaximListRepository: MaximListRepositoriable { 13 | private let localDBManager = RealmDBManager.shared 14 | 15 | func read(from date: Date) -> Single<[Maxim]> { 16 | return Single.create { [weak self] single in 17 | guard let self = self else { 18 | single(.failure(RealmError.initFailed)) 19 | return Disposables.create() 20 | } 21 | 22 | do { 23 | let predicate = NSPredicate(format: "date < %@", date as CVarArg) 24 | let result = try self.localDBManager.objects( 25 | ofType: Maxim.self, 26 | with: predicate 27 | ) 28 | single(.success(result)) 29 | } catch { 30 | single(.failure(RealmError.searchFailed)) 31 | } 32 | return Disposables.create() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /JipJung/JipJung/Data/Repositories/MediaResourceRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaResourceRepository.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/08. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MediaResourceRepository { 13 | enum MediaResourceError: Error { 14 | case notFound 15 | } 16 | 17 | private let bundleManager = BundleManager.shared 18 | private let localFileManager = LocalFileManager.shared 19 | private let remoteServiceProvider = RemoteServiceProvider.shared 20 | 21 | func getMediaURL(fileName: String, type: MediaType) -> Single { 22 | let localFileManager = self.localFileManager 23 | 24 | if let fileURL = bundleManager.findURL(fileNameWithExtension: fileName) { 25 | return Single.just(fileURL) 26 | } 27 | 28 | if let fileURL = localFileManager.isExsit(fileName) { 29 | return Single.just(fileURL) 30 | } 31 | 32 | return remoteServiceProvider.request(key: fileName, type: type) 33 | .map { try localFileManager.move(from: $0, to: fileName) } 34 | } 35 | 36 | func getMediaURLFromLocal(fileName: String) -> Single { 37 | if let fileURL = BundleManager.shared.findURL(fileNameWithExtension: fileName) { 38 | return Single.just(fileURL) 39 | } 40 | if let fileURL = localFileManager.isExsit(fileName) { 41 | return Single.just(fileURL) 42 | } 43 | 44 | return Single.error(MediaResourceError.notFound) 45 | } 46 | 47 | func fetchData(fileName: String, type: MediaType) -> Single { 48 | let localFileManager = self.localFileManager 49 | 50 | if let fileURL = bundleManager.findURL(fileNameWithExtension: fileName) { 51 | return Single.just(fileURL) 52 | .map { try Data(contentsOf: $0) } 53 | } 54 | 55 | if let localFileData = try? localFileManager.read(fileName) { 56 | return Single.just(localFileData) 57 | } 58 | 59 | return remoteServiceProvider.request(key: fileName, type: type) 60 | .map { try localFileManager.move(from: $0, to: fileName) } 61 | .map { try localFileManager.read($0) } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /JipJung/JipJung/Data/Repositories/PlayHistoryRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayHistoryRepository.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class PlayHistoryRepository { 13 | private let localDBManager = RealmDBManager.shared 14 | 15 | func create(mediaID: String) -> Single { 16 | return Single.create { [weak self] single in 17 | guard let self = self else { 18 | single(.failure(RealmError.initFailed)) 19 | return Disposables.create() 20 | } 21 | 22 | do { 23 | let newHistory = PlayHistory(mediaID: mediaID) 24 | try newHistory.autoIncrease() 25 | try self.localDBManager.add(newHistory) 26 | } catch { 27 | single(.failure(error)) 28 | } 29 | single(.success(true)) 30 | return Disposables.create() 31 | } 32 | } 33 | 34 | func read() -> Single<[Media]> { 35 | return Single.create { [weak self] single in 36 | guard let self = self else { 37 | single(.failure(RealmError.initFailed)) 38 | return Disposables.create() 39 | } 40 | 41 | do { 42 | let list = try self.localDBManager.objects(ofType: PlayHistory.self) 43 | 44 | var playHistoryDict: [String: Int] = [:] 45 | list.forEach { element in 46 | playHistoryDict[element.mediaID] = element.id 47 | } 48 | 49 | let playHistoryIDs = Array(playHistoryDict.keys) 50 | let predicate = NSPredicate.init(format: "id IN %@", playHistoryIDs) 51 | let filteredMedia = try self.localDBManager.objects(ofType: Media.self, with: predicate) 52 | let result = filteredMedia.sorted { 53 | guard let lhs = playHistoryDict[$0.id], 54 | let rhs = playHistoryDict[$1.id] 55 | else { 56 | return false 57 | } 58 | return lhs > rhs 59 | } 60 | single(.success(result)) 61 | } catch { 62 | single(.failure(RealmError.searchFailed)) 63 | } 64 | 65 | return Disposables.create() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /JipJung/JipJung/Data/Repositories/SearchHistoryRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchHistoryRepository.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | final class SearchHistoryRepository { 11 | private let userDefaultStorage = UserDefaultsStorage.shared 12 | 13 | func save(searchHistory: [String]) { 14 | userDefaultStorage.save(for: UserDefaultsKeys.searchHistory, 15 | value: searchHistory) 16 | } 17 | 18 | func load() -> [String] { 19 | guard let searchHistory: [String] = userDefaultStorage.load( 20 | for: UserDefaultsKeys.searchHistory 21 | ) else { return [] } 22 | return searchHistory 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/BrightMedia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightMedia.swift.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | class BrightMedia: Object, Decodable { 13 | @Persisted(primaryKey: true) var id: Int 14 | @Persisted var mediaID: String 15 | 16 | private override init() { 17 | super.init() 18 | } 19 | 20 | convenience init(id: Int = -1, mediaID: String) { 21 | self.init() 22 | self.id = id 23 | self.mediaID = mediaID 24 | } 25 | 26 | func autoIncrease() throws { 27 | guard let realm = try? Realm() else { 28 | throw RealmError.initFailed 29 | } 30 | 31 | var idCount = 0 32 | if let lastHistory = realm.objects(Self.self).last { 33 | idCount = lastHistory.id + 1 34 | } 35 | 36 | id = idCount 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/DBStructureForJSON.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DBStructureForJSON.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DBStructureForJSON: Decodable { 11 | enum CodingKeys: String, CodingKey { 12 | case allMediaList = "Media" 13 | case brightModeList = "BrightMedia" 14 | case darknessModeList = "DarknessMedia" 15 | case playHistoryList = "PlayHistory" 16 | case favoriteMediaList = "FavoriteMedia" 17 | case maximList = "Maxim" 18 | } 19 | 20 | let allMediaList: [Media] 21 | let brightModeList: [BrightMedia] 22 | let darknessModeList: [DarknessMedia] 23 | let playHistoryList: [PlayHistory] 24 | let favoriteMediaList: [FavoriteMedia] 25 | let maximList: [Maxim] 26 | } 27 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/DarknessMedia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DarknessMedia.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | class DarknessMedia: Object, Decodable { 13 | @Persisted(primaryKey: true) var id: Int 14 | @Persisted var mediaID: String 15 | 16 | private override init() { 17 | super.init() 18 | } 19 | 20 | convenience init(id: Int = -1, mediaID: String) { 21 | self.init() 22 | self.id = id 23 | self.mediaID = mediaID 24 | } 25 | 26 | func autoIncrease() throws { 27 | guard let realm = try? Realm() else { 28 | throw RealmError.initFailed 29 | } 30 | 31 | var idCount = 0 32 | if let lastHistory = realm.objects(Self.self).last { 33 | idCount = lastHistory.id + 1 34 | } 35 | 36 | id = idCount 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/DateFocusRecordDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFocusRecordDTO.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DateFocusRecordDTO: Comparable { 11 | static func < (lhs: DateFocusRecordDTO, rhs: DateFocusRecordDTO) -> Bool { 12 | return lhs.date < rhs.date 13 | } 14 | 15 | let date: Date 16 | let focusSecond: Int 17 | 18 | internal init(date: Date, focusSecond: Int) { 19 | self.date = date.midnight 20 | self.focusSecond = focusSecond 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/FavoriteMedia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteMusic.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | class FavoriteMedia: Object, Decodable { 13 | @Persisted(primaryKey: true) var id: Int 14 | @Persisted var mediaID: String 15 | 16 | private override init() { 17 | super.init() 18 | } 19 | 20 | convenience init(id: Int = -1, mediaID: String) { 21 | self.init() 22 | self.id = id 23 | self.mediaID = mediaID 24 | } 25 | 26 | func autoIncrease() throws { 27 | guard let realm = try? Realm() else { 28 | throw RealmError.initFailed 29 | } 30 | 31 | var idCount = 0 32 | if let lastHistory = realm.objects(Self.self).last { 33 | idCount = lastHistory.id + 1 34 | } 35 | 36 | id = idCount 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/FocusRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusRecord.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | class FocusRecord: Object, Decodable { 13 | @Persisted(primaryKey: true) var id: String 14 | @Persisted var focusTime: Int // unit : second 15 | @Persisted var year: Int 16 | @Persisted var month: Int 17 | @Persisted var day: Int 18 | @Persisted var hour: Int 19 | 20 | private override init() { 21 | super.init() 22 | } 23 | 24 | convenience init(id: String, focusTime: Int, year: Int, month: Int, day: Int, hour: Int) { 25 | self.init() 26 | self.id = id 27 | self.focusTime = focusTime 28 | self.year = year 29 | self.month = month 30 | self.day = day 31 | self.hour = hour 32 | } 33 | } 34 | 35 | class DateFocusRecord: Object { 36 | @Persisted(primaryKey: true) var id: String 37 | @Persisted var focusTime = List() 38 | 39 | private override init() { 40 | super.init() 41 | } 42 | 43 | convenience init(id: Date) { 44 | self.init(id: id.realmId) 45 | } 46 | 47 | convenience init(id: String) { 48 | self.init() 49 | self.id = id 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/Maxim.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Maxim.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | class Maxim: Object, Decodable { 13 | @Persisted(primaryKey: true) var id: String 14 | @Persisted var date: Date 15 | @Persisted var thumbnailImageFileName: String 16 | @Persisted var content: String 17 | @Persisted var speaker: String 18 | 19 | private override init() { 20 | super.init() 21 | } 22 | 23 | convenience init(id: String, date: Date, thumbnailImageFileName: String, content: String, speaker: String) { 24 | self.init() 25 | self.id = id 26 | self.date = date 27 | self.thumbnailImageFileName = thumbnailImageFileName 28 | self.content = content 29 | self.speaker = speaker 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/Media.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Media.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | class Media: Object, Decodable { 13 | @Persisted(primaryKey: true) var id: String 14 | @Persisted var name: String 15 | @Persisted var explanation: String 16 | @Persisted var maxim: String 17 | @Persisted var speaker: String 18 | @Persisted var color: String 19 | @Persisted var mode: Int 20 | @Persisted var tag: String 21 | @Persisted var thumbnailImageFileName: String 22 | @Persisted var videoFileName: String 23 | @Persisted var audioFileName: String 24 | 25 | private override init() { 26 | super.init() 27 | } 28 | 29 | convenience init( 30 | id: String, 31 | name: String, 32 | explanation: String, 33 | maxim: String, 34 | speaker: String, 35 | color: String, 36 | mode: Int, 37 | tag: String, 38 | thumbnailImageFileName: String, 39 | videoFileName: String, 40 | audioFileName: String 41 | ) { 42 | self.init() 43 | self.id = id 44 | self.name = name 45 | self.explanation = explanation 46 | self.maxim = maxim 47 | self.speaker = speaker 48 | self.color = color 49 | self.mode = mode 50 | self.tag = tag 51 | self.thumbnailImageFileName = thumbnailImageFileName 52 | self.videoFileName = videoFileName 53 | self.audioFileName = audioFileName 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Entities/PlayHistory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayHistory.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | class PlayHistory: Object, Decodable { 13 | @Persisted(primaryKey: true) var id: Int 14 | @Persisted var mediaID: String 15 | 16 | private override init() { 17 | super.init() 18 | } 19 | 20 | convenience init(id: Int = -1, mediaID: String) { 21 | self.init() 22 | self.id = id 23 | self.mediaID = mediaID 24 | } 25 | 26 | func autoIncrease() throws { 27 | guard let realm = try? Realm() else { 28 | throw RealmError.initFailed 29 | } 30 | 31 | var idCount = 0 32 | if let lastHistory = realm.objects(Self.self).last { 33 | idCount = lastHistory.id + 1 34 | } 35 | 36 | id = idCount 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Managers/Audio+Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Audio+Enums.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AudioError: Error { 11 | case initFailed 12 | case badURL 13 | } 14 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/Managers/AudioPlayManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioPlayManager.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/15. 6 | // 7 | 8 | import AVFoundation 9 | import Foundation 10 | import MediaPlayer 11 | 12 | import RxRelay 13 | import RxSwift 14 | 15 | typealias PlayState = AudioPlayManager.PlayState 16 | 17 | class AudioPlayManager { 18 | enum PlayState { 19 | case manual(Bool) 20 | case automatics 21 | } 22 | 23 | static let shared = AudioPlayManager() 24 | private init() { 25 | remoteCommandCenter.playCommand.addTarget { [weak self] _ in 26 | guard let audioPlayer = self?.audioPlayer else { 27 | return .noSuchContent 28 | } 29 | 30 | audioPlayer.play() 31 | 32 | NotificationCenter.default.post( 33 | name: .checkCurrentPlay, 34 | object: nil, 35 | userInfo: nil 36 | ) 37 | 38 | return .success 39 | } 40 | 41 | remoteCommandCenter.pauseCommand.addTarget { [weak self] _ in 42 | guard let audioPlayer = self?.audioPlayer else { 43 | return .noSuchContent 44 | } 45 | 46 | audioPlayer.pause() 47 | 48 | NotificationCenter.default.post( 49 | name: .checkCurrentPlay, 50 | object: nil, 51 | userInfo: nil 52 | ) 53 | 54 | return .success 55 | } 56 | } 57 | 58 | private let remoteCommandCenter = MPRemoteCommandCenter.shared() 59 | private let remoteCommandInfoCenter = MPNowPlayingInfoCenter.default() 60 | private let mediaResourceRepository = MediaResourceRepository() 61 | private let disposeBag = DisposeBag() 62 | 63 | private(set) var audioPlayer: AVAudioPlayer? 64 | 65 | func ready(url: URL) throws { 66 | do { 67 | audioPlayer = try AVAudioPlayer(contentsOf: url) 68 | } catch { 69 | throw AudioError.badURL 70 | } 71 | audioPlayer?.numberOfLoops = -1 72 | audioPlayer?.prepareToPlay() 73 | } 74 | 75 | func play(media: Media, restart: Bool) throws -> Bool { 76 | guard let audioPlayer = audioPlayer, 77 | let fileName = audioPlayer.url?.lastPathComponent 78 | else { 79 | throw AudioError.initFailed 80 | } 81 | 82 | if fileName != media.audioFileName { 83 | return false 84 | } 85 | 86 | if restart { 87 | audioPlayer.currentTime = 0 88 | } 89 | 90 | configureRemoteCommandInfo(media: media) 91 | 92 | return audioPlayer.play() 93 | } 94 | 95 | func pause() throws { 96 | guard let audioPlayer = audioPlayer else { 97 | throw AudioError.initFailed 98 | } 99 | 100 | audioPlayer.pause() 101 | } 102 | 103 | func isEqaul(with media: Media) -> Bool { 104 | guard let audioPlayer = audioPlayer, 105 | let currentAudiofileName = audioPlayer.url?.lastPathComponent 106 | else { 107 | return false 108 | } 109 | 110 | return currentAudiofileName == media.audioFileName 111 | } 112 | 113 | func isEqaul(with audioFileName: String) -> Bool { 114 | guard let audioPlayer = audioPlayer, 115 | let currentAudiofileName = audioPlayer.url?.lastPathComponent 116 | else { 117 | return false 118 | } 119 | 120 | return currentAudiofileName == audioFileName 121 | } 122 | 123 | func isPlaying() -> Bool { 124 | guard let audioPlayer = audioPlayer else { 125 | return false 126 | } 127 | 128 | return audioPlayer.isPlaying 129 | } 130 | 131 | private func configureRemoteCommandInfo(media: Media) { 132 | var nowPlayingInfo = remoteCommandInfoCenter.nowPlayingInfo ?? [String: Any]() 133 | nowPlayingInfo[MPMediaItemPropertyTitle] = media.name 134 | 135 | if let image = UIImage(named: "app_icon") { 136 | nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork( 137 | boundsSize: image.size, 138 | requestHandler: { _ in 139 | return image 140 | } 141 | ) 142 | } 143 | 144 | remoteCommandInfoCenter.nowPlayingInfo = nowPlayingInfo 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/AudioPlayUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioPlayUseCase.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/09. 6 | // 7 | 8 | import AVKit 9 | import Foundation 10 | 11 | import RxSwift 12 | 13 | final class AudioPlayUseCase { 14 | private let mediaResourceRepository = MediaResourceRepository() 15 | private let playHistoryRepository = PlayHistoryRepository() 16 | private let audioPlayManager = AudioPlayManager.shared 17 | 18 | private let disposeBag = DisposeBag() 19 | 20 | func control(media: Media, autoPlay: Bool = false, restart: Bool = false, isContinue: Bool = false) -> Single { 21 | return Single.create { [weak self] single in 22 | guard let self = self else { 23 | single(.failure(AudioError.initFailed)) 24 | return Disposables.create() 25 | } 26 | 27 | do { 28 | if self.audioPlayManager.isEqaul(with: media.audioFileName) { 29 | if self.audioPlayManager.isPlaying() { 30 | if isContinue { 31 | single(.success(true)) 32 | } else { 33 | let result = try self.pause() 34 | single(.success(result)) 35 | } 36 | } else { 37 | let result = try self.play(media: media, restart: restart) 38 | single(.success(result)) 39 | } 40 | } else { 41 | self.mediaResourceRepository.getMediaURL(fileName: media.audioFileName, type: .audio) 42 | .observe(on: MainScheduler.asyncInstance) 43 | .map { [weak self] in 44 | try self?.audioPlayManager.ready(url: $0) 45 | } 46 | .subscribe { 47 | if autoPlay { 48 | let result = try? self.play(media: media, restart: restart) 49 | single(.success(result ?? false)) 50 | } else { 51 | single(.success(true)) 52 | } 53 | } onFailure: { error in 54 | single(.failure(error)) 55 | } 56 | .disposed(by: self.disposeBag) 57 | } 58 | } catch { 59 | single(.failure(error)) 60 | } 61 | 62 | return Disposables.create() 63 | } 64 | } 65 | 66 | func control(media: Media, state: Bool, restart: Bool = false) -> Single { 67 | return Single.create { [weak self] single in 68 | guard let self = self else { 69 | single(.failure(AudioError.initFailed)) 70 | return Disposables.create() 71 | } 72 | do { 73 | if self.audioPlayManager.isEqaul(with: media.audioFileName) { 74 | if state { 75 | let result = try self.play(media: media, restart: restart) 76 | single(.success(result)) 77 | } else { 78 | let result = try self.pause() 79 | single(.success(result)) 80 | } 81 | } 82 | } catch { 83 | single(.failure(error)) 84 | } 85 | return Disposables.create() 86 | } 87 | } 88 | 89 | func play(media: Media, restart: Bool) throws -> Bool { 90 | do { 91 | if try audioPlayManager.play(media: media, restart: restart) { 92 | if let mediaID = media.audioFileName.components(separatedBy: ".")[safe: 0] { 93 | playHistoryRepository.create(mediaID: mediaID) 94 | .subscribe(onSuccess: { state in 95 | if !state { 96 | return 97 | } 98 | 99 | NotificationCenter.default.post( 100 | name: .refreshHome, 101 | object: nil, 102 | userInfo: ["RefreshType": [RefreshHomeData.playHistory]] 103 | ) 104 | }) 105 | .disposed(by: disposeBag) 106 | } 107 | return true 108 | } else { 109 | return false 110 | } 111 | } catch { 112 | throw error 113 | } 114 | } 115 | 116 | func pause() throws -> Bool { 117 | do { 118 | try audioPlayManager.pause() 119 | return false 120 | } catch { 121 | throw error 122 | } 123 | } 124 | 125 | func isPlaying(using audioFileName: String) -> Bool { 126 | let isEqaul = audioPlayManager.isEqaul(with: audioFileName) 127 | let isPlaying = audioPlayManager.isPlaying() 128 | return isEqaul && isPlaying 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/FavoriteMediaUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteMediaUseCase.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class FavoriteUseCase { 13 | private let myFavoriteRepository: FavoriteRepository = FavoriteRepository() 14 | 15 | func read(id: String) -> Single<[FavoriteMedia]> { 16 | return myFavoriteRepository.read(mediaID: id) 17 | } 18 | 19 | func fetchAll() -> Single<[Media]> { 20 | return myFavoriteRepository.read() 21 | } 22 | 23 | func save(id: String, date: Date) -> Single { 24 | return myFavoriteRepository.create(mediaID: id) 25 | .do(onSuccess: { _ in 26 | NotificationCenter.default.post( 27 | name: .refreshHome, 28 | object: nil, 29 | userInfo: ["RefreshType": [RefreshHomeData.favorite]] 30 | ) 31 | }) 32 | } 33 | 34 | func delete(id: String) -> Single { 35 | return myFavoriteRepository.delete(mediaID: id) 36 | .do(onSuccess: { _ in 37 | NotificationCenter.default.post( 38 | name: .refreshHome, 39 | object: nil, 40 | userInfo: ["RefreshType": [RefreshHomeData.favorite]] 41 | ) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/FetchMediaURLUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckMusicDownloadedUseCase.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class FetchMediaURLUseCase { 13 | private let mediaResourceRepository = MediaResourceRepository() 14 | 15 | func getMediaURL(fileName: String, type: MediaType) -> Single { 16 | return mediaResourceRepository.getMediaURL(fileName: fileName, type: type) 17 | } 18 | 19 | func getMediaURLFromLocal(fileName: String) -> Single { 20 | return mediaResourceRepository.getMediaURLFromLocal(fileName: fileName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/LoadFocusTimeUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadFocusTimeUseCase.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol FocusTimeRepositoryProtocol { 13 | func create(record: FocusRecord) -> Single 14 | func read(date: Date) -> Single 15 | } 16 | 17 | final class LoadFocusTimeUseCase { 18 | private let focusTimeRepository: FocusTimeRepositoryProtocol 19 | 20 | init(focusTimeRepository: FocusTimeRepositoryProtocol) { 21 | self.focusTimeRepository = focusTimeRepository 22 | } 23 | 24 | func loadHistory(from date: Date, nDays: Int) -> Observable<[DateFocusRecordDTO]> { 25 | let oneDay = TimeInterval(86400) 26 | let dateFormatter = DateFormatter() 27 | dateFormatter.dateFormat = "yyyy-MM-dd" 28 | dateFormatter.timeZone = NSTimeZone(name: "UTC") as TimeZone? 29 | let dateObservable = Observable.from(Array(0.. DateFocusRecordDTO in 39 | let focusSecond = focusRecords.focusTime.reduce(0) { $0 + $1.focusTime } 40 | return DateFocusRecordDTO(date: date, focusSecond: focusSecond) 41 | } 42 | .toArray() 43 | .map({$0.sorted(by: <)}) 44 | .asObservable() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/MaximListUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximListUseCase.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/08. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol MaximListRepositoriable { 13 | func read(from date: Date) -> Single<[Maxim]> 14 | } 15 | 16 | final class MaximListUseCase { 17 | private let maximListRepository: MaximListRepositoriable 18 | 19 | init(maximListRepository: MaximListRepositoriable) { 20 | self.maximListRepository = maximListRepository 21 | } 22 | 23 | func fetchWeeksMaximList() -> Single<[Maxim]> { 24 | return maximListRepository.read(from: makeTomorrow()).map { 25 | $0.dropLast($0.count % 7) 26 | } 27 | .map { 28 | $0.sorted { lhs, rhs in 29 | lhs.date > rhs.date 30 | } 31 | } 32 | } 33 | 34 | private func makeTomorrow() -> Date { 35 | let date = Date() 36 | let dateFormatter = DateFormatter() 37 | dateFormatter.dateFormat = "yyyy-MM-dd" 38 | return dateFormatter.date(from: [date.year, date.month, date.day + 1].map({"\($0)"}).joined(separator: "-")) ?? Date(timeIntervalSinceNow: 86400) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/MediaListUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaListUseCase.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MediaListUseCase { 13 | private let mediaListRepository = MediaListRepository() 14 | 15 | func fetchAllMediaList() -> Single<[Media]> { 16 | return mediaListRepository.read() 17 | } 18 | 19 | func fetchMediaMyList(mode: MediaMode) -> Single<[Media]> { 20 | return mediaListRepository.read(for: mode) 21 | } 22 | 23 | func removeMediaFromMode(media: Media) -> Single { 24 | return mediaListRepository.delete(mediaID: media.id, mode: media.mode) 25 | } 26 | 27 | func removeMediaFromMode(id: String, mode: Int) -> Single { 28 | return mediaListRepository.delete(mediaID: id, mode: mode) 29 | } 30 | 31 | func saveMediaFromMode(id: String, mode: Int) -> Single { 32 | return mediaListRepository.create(mediaID: id, mode: mode) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/PlayHistoryUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayHistoryUseCase.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class PlayHistoryUseCase { 13 | private let playHistoryRepository = PlayHistoryRepository() 14 | 15 | func fetchPlayHistory() -> Single<[Media]> { 16 | return playHistoryRepository.read() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/SaveFocusTimeUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaveFocusTimeUseCase.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class SaveFocusTimeUseCase { 13 | private let focusTimeRepository = FocusTimeRepository() 14 | 15 | func execute(seconds time: Int) -> Single { 16 | let currentDate = Date() 17 | return focusTimeRepository.create( 18 | record: FocusRecord( 19 | id: UUID().uuidString, 20 | focusTime: time, 21 | year: currentDate.year, 22 | month: currentDate.month, 23 | day: currentDate.day, 24 | hour: currentDate.hour 25 | ) 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/SearchHistoryUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchHistoryUseCase.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | final class SearchHistoryUseCase { 11 | private let searchHistoryRepository = SearchHistoryRepository() 12 | 13 | func save(_ searchHistory: [String]) { 14 | return searchHistoryRepository.save(searchHistory: searchHistory) 15 | } 16 | 17 | func load() -> [String] { 18 | return searchHistoryRepository.load() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /JipJung/JipJung/Domain/UseCases/SearchMediaUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchMediaUseCase.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class SearchMediaUseCase { 14 | let allMediaList = BehaviorRelay<[Media]>(value: []) 15 | let searchResult = BehaviorRelay<[Media]>(value: []) 16 | 17 | private let mediaListRepository = MediaListRepository() 18 | private var disposeBag: DisposeBag = DisposeBag() 19 | 20 | func search(keyword: String) { 21 | allMediaList.bind { [weak self] in 22 | guard let self = self else { return } 23 | var searchResult: [Media] = [] 24 | $0.forEach { media in 25 | if media.name.contains(keyword) { 26 | searchResult.append(media) 27 | } 28 | } 29 | self.searchResult.accept(searchResult) 30 | } 31 | .disposed(by: disposeBag) 32 | 33 | fetchAllMediaList() 34 | } 35 | 36 | func search(tag: String) { 37 | allMediaList.bind { [weak self] in 38 | guard let self = self else { return } 39 | var searchResult: [Media] = [] 40 | $0.forEach { media in 41 | if media.tag.contains(tag) { 42 | searchResult.append(media) 43 | } 44 | } 45 | self.searchResult.accept(searchResult) 46 | } 47 | .disposed(by: disposeBag) 48 | 49 | fetchAllMediaList() 50 | } 51 | 52 | private func fetchAllMediaList() { 53 | mediaListRepository.read() 54 | .subscribe { [weak self] in 55 | self?.allMediaList.accept($0) 56 | } onFailure: { error in 57 | print(error.localizedDescription) 58 | } 59 | .disposed(by: disposeBag) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /JipJung/JipJung/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | UIBackgroundModes 23 | 24 | audio 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/Bundle/BundleManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleManager.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/14. 6 | // 7 | 8 | import Foundation 9 | 10 | class BundleManager { 11 | static let shared = BundleManager() 12 | 13 | private init() {} 14 | 15 | func findURL(fileNameWithExtension path: String) -> URL? { 16 | return Bundle.main.url(forResource: path, withExtension: "") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/File/LocalFileEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalFileEnums.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LocalFileError: Error { 11 | case notFound 12 | case copyFailed 13 | case writeFailed 14 | } 15 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/File/LocalFileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalFileManager.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/02. 6 | // 7 | 8 | import Foundation 9 | 10 | final class LocalFileManager { 11 | static let shared = LocalFileManager() 12 | private init() {} 13 | 14 | private let cachePath = FileManager.default.urls( 15 | for: .cachesDirectory, 16 | in: .userDomainMask 17 | )[safe: 0] 18 | 19 | func isExsit(_ fileName: String) -> URL? { 20 | guard let fileURL = cachePath?.appendingPathComponent(fileName) else { 21 | return nil 22 | } 23 | 24 | if FileManager.default.fileExists(atPath: fileURL.path) { 25 | return fileURL 26 | } else { 27 | return nil 28 | } 29 | } 30 | 31 | func isExsit(_ fileURL: URL) -> URL? { 32 | if FileManager.default.fileExists(atPath: fileURL.path) { 33 | return fileURL 34 | } else { 35 | return nil 36 | } 37 | } 38 | 39 | func read(_ fileName: String) throws -> Data { 40 | guard let fileURL = cachePath?.appendingPathComponent(fileName) else { 41 | throw LocalFileError.notFound 42 | } 43 | 44 | do { 45 | return try Data(contentsOf: fileURL) 46 | } catch { 47 | throw LocalFileError.notFound 48 | } 49 | } 50 | 51 | func read(_ fileURL: URL) throws -> Data { 52 | do { 53 | return try Data(contentsOf: fileURL) 54 | } catch { 55 | throw LocalFileError.notFound 56 | } 57 | } 58 | 59 | func write(_ data: Data, at fileName: String) throws { 60 | guard let fileURL = cachePath?.appendingPathComponent(fileName) else { 61 | throw LocalFileError.notFound 62 | } 63 | 64 | do { 65 | try data.write(to: fileURL) 66 | } catch { 67 | throw LocalFileError.writeFailed 68 | } 69 | } 70 | 71 | func move(from url: URL, to fileName: String) throws -> URL { 72 | guard let fileURL = cachePath?.appendingPathComponent(fileName) else { 73 | throw LocalFileError.notFound 74 | } 75 | 76 | do { 77 | try FileManager.default.moveItem(at: url, to: fileURL) 78 | } catch { 79 | throw LocalFileError.copyFailed 80 | } 81 | return fileURL 82 | } 83 | 84 | func delete(_ fileName: String) throws { 85 | guard let fileURL = cachePath?.appendingPathComponent(fileName) else { 86 | throw LocalFileError.notFound 87 | } 88 | 89 | do { 90 | try FileManager.default.removeItem(at: fileURL) 91 | } catch { 92 | throw LocalFileError.notFound 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/LocalDB/LocalDBEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalDBEnums.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RealmError: Error { 11 | case initFailed 12 | case searchFailed 13 | case addFailed 14 | case deleteFailed 15 | } 16 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/LocalDB/RealmDBManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmDBManager.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | 12 | final class RealmDBManager { 13 | static let shared = RealmDBManager() 14 | private init() {} 15 | 16 | func object(ofType: Entity.Type, forPrimaryKey: String) throws -> Entity? { 17 | guard let realm = try? Realm() else { 18 | throw RealmError.initFailed 19 | } 20 | 21 | return realm.object(ofType: ofType, forPrimaryKey: forPrimaryKey) 22 | } 23 | 24 | func objects(ofType: Entity.Type, with predicate: NSPredicate? = nil) throws -> [Entity] { 25 | guard let realm = try? Realm() else { 26 | throw RealmError.initFailed 27 | } 28 | 29 | if let predicate = predicate { 30 | return Array(realm.objects(Entity.self).filter(predicate)) 31 | } else { 32 | return Array(realm.objects(Entity.self)) 33 | } 34 | } 35 | 36 | func add(_ value: Object) throws { 37 | guard let realm = try? Realm() else { 38 | throw RealmError.initFailed 39 | } 40 | 41 | do { 42 | try realm.write({ 43 | realm.add(value, update: .all) 44 | }) 45 | } catch { 46 | throw RealmError.addFailed 47 | } 48 | } 49 | 50 | func delete(_ value: Object) throws { 51 | guard let realm = try? Realm() else { 52 | throw RealmError.initFailed 53 | } 54 | 55 | do { 56 | try realm.write({ 57 | realm.delete(value) 58 | }) 59 | } catch { 60 | throw RealmError.deleteFailed 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/Network/NetworkEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkEnums.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MediaType: String { 11 | case audio 12 | case image 13 | case video 14 | } 15 | 16 | enum RemoteServiceError: Error { 17 | case transportFailed 18 | case client 19 | case server 20 | case unknown 21 | case badURL 22 | } 23 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/Network/RemoteServiceProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteManager.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/06. 6 | // 7 | 8 | import Foundation 9 | 10 | import FirebaseStorage 11 | import RxSwift 12 | 13 | final class RemoteServiceProvider { 14 | static let shared = RemoteServiceProvider() 15 | private init() {} 16 | 17 | private let storage = Storage.storage() 18 | 19 | func request(key: String, type: MediaType) -> Single { 20 | let fullPath = type.rawValue + "/" + key 21 | let reference = storage.reference(withPath: fullPath) 22 | 23 | return Single.create { single in 24 | reference.downloadURL { url, error in 25 | guard let url = url, 26 | error == nil 27 | else { 28 | single(.failure(RemoteServiceError.badURL)) 29 | return 30 | } 31 | 32 | URLSession.shared.downloadTask(with: url) { (url, response, error) in 33 | guard let url = url, 34 | error == nil 35 | else { 36 | single(.failure(RemoteServiceError.transportFailed)) 37 | return 38 | } 39 | 40 | guard let response = response as? HTTPURLResponse else { 41 | single(.failure(RemoteServiceError.unknown)) 42 | return 43 | } 44 | 45 | if (200 ..< 300) ~= response.statusCode { 46 | single(.success(url)) 47 | } else if (400 ..< 500) ~= response.statusCode { 48 | single(.failure(RemoteServiceError.client)) 49 | return 50 | } else if (500 ..< 600) ~= response.statusCode { 51 | single(.failure(RemoteServiceError.server)) 52 | return 53 | } else { 54 | single(.failure(RemoteServiceError.unknown)) 55 | return 56 | } 57 | }.resume() 58 | } 59 | 60 | return Disposables.create() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /JipJung/JipJung/Infra/UserDefaults/UserDefaultsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsStorage.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | final class UserDefaultsStorage { 11 | static let shared = UserDefaultsStorage() 12 | private init() {} 13 | 14 | private let userDefaults = UserDefaults.standard 15 | 16 | func save(for key: String, value: Int) { 17 | userDefaults.set(value, forKey: key) 18 | userDefaults.synchronize() 19 | } 20 | 21 | func save(for key: String, value: [String]) { 22 | userDefaults.set(value, forKey: key) 23 | userDefaults.synchronize() 24 | } 25 | 26 | func load(for key: String) -> Int? { 27 | let value = userDefaults.integer(forKey: key) 28 | return value == 0 ? nil : value 29 | } 30 | 31 | func load(for key: String) -> [String]? { 32 | guard let value = userDefaults.object(forKey: key) as? [String] else { return nil } 33 | return value 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /JipJung/JipJung/Resource/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS10-JipJung/4fee394a93fd40d9d2b08d88d8864dd78537c126/JipJung/JipJung/Resource/.gitkeep -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Animation/CountdownView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountdownView.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CountdownView: UIView { 11 | private let numberLabel: UILabel = { 12 | let label = UILabel() 13 | label.textColor = .gray 14 | label.text = "3" 15 | label.font = .preferredFont(forTextStyle: .title1) 16 | return label 17 | }() 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | configure() 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | override func layoutSubviews() { 29 | super.layoutSubviews() 30 | layer.cornerRadius = frame.width / 2.0 31 | } 32 | 33 | private func configure() { 34 | backgroundColor = .white 35 | 36 | addSubview(numberLabel) 37 | numberLabel.snp.makeConstraints { 38 | $0.center.equalToSuperview() 39 | } 40 | } 41 | 42 | func animate(countdown: Int, completion: @escaping () -> Void) { 43 | guard countdown > 0 else { 44 | completion() 45 | return 46 | } 47 | 48 | self.numberLabel.text = "\(countdown)" 49 | self.numberLabel.sizeToFit() 50 | UIView.animate(withDuration: 1.0) { 51 | self.layoutIfNeeded() 52 | self.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) 53 | self.layer.opacity = 0.0 54 | } completion: { _ in 55 | self.transform = .identity 56 | self.layer.opacity = 1.0 57 | self.animate(countdown: countdown - 1, completion: completion) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Animation/CycleAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CycleAnimation.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/11. 6 | // 7 | 8 | import QuartzCore 9 | 10 | final class CycleAnimation: CABasicAnimation { 11 | override init() { 12 | super.init() 13 | self.cycleAnimation() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | self.cycleAnimation() 19 | } 20 | 21 | private func cycleAnimation() { 22 | keyPath = "transform.rotation" 23 | fromValue = 0.0 24 | toValue = Double.pi * 2 25 | duration = 31 26 | repeatCount = .greatestFiniteMagnitude 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Animation/PulseAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulseAnimation.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/10. 6 | // 7 | 8 | import QuartzCore 9 | 10 | final class PulseAnimation: CAAnimationGroup { 11 | override init() { 12 | super.init() 13 | self.scaleAnimation() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | self.scaleAnimation() 19 | } 20 | 21 | private func scaleAnimation() { 22 | let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") 23 | scaleAnimation.fromValue = 1.0 24 | scaleAnimation.toValue = 1.6 25 | 26 | let opacityAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity)) 27 | opacityAnimation.fromValue = 0.9 28 | opacityAnimation.toValue = 0 29 | 30 | self.animations = [scaleAnimation, opacityAnimation] 31 | self.duration = 4 32 | self.timingFunction = CAMediaTimingFunction(name: .easeOut) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Animation/SlowPresent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlowPresent.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/13. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SlowPresent: NSObject { 11 | enum AnimationType { 12 | case present, dismiss 13 | } 14 | 15 | private let duration: Double 16 | private let animationType: AnimationType 17 | 18 | init(duration: Double, animationType: AnimationType) { 19 | self.duration = duration 20 | self.animationType = animationType 21 | } 22 | } 23 | 24 | extension SlowPresent: UIViewControllerAnimatedTransitioning { 25 | func transitionDuration( 26 | using transitionContext: UIViewControllerContextTransitioning? 27 | ) -> TimeInterval { 28 | return duration 29 | } 30 | 31 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 32 | guard let fromVC = transitionContext.viewController(forKey: .from), 33 | let fromView = fromVC.view, 34 | let toVC = transitionContext.viewController(forKey: .to), 35 | let toView = toVC.view 36 | else { 37 | transitionContext.completeTransition(false) 38 | return 39 | } 40 | 41 | switch animationType { 42 | case .present: 43 | transitionContext.containerView.addSubview(toView) 44 | presentAnimation(with: transitionContext, toView: toView) 45 | case .dismiss: 46 | dismissAnimation(with: transitionContext, fromView: fromView) 47 | } 48 | } 49 | 50 | private func presentAnimation( 51 | with transitionContext: UIViewControllerContextTransitioning, 52 | toView: UIView 53 | ) { 54 | let animationDuration = transitionDuration(using: transitionContext) 55 | toView.alpha = 0 56 | UIView.animate( 57 | withDuration: animationDuration, 58 | animations: { 59 | toView.alpha = 1 60 | }, 61 | completion: { 62 | transitionContext.completeTransition($0) 63 | } 64 | ) 65 | } 66 | 67 | private func dismissAnimation( 68 | with transitionContext: UIViewControllerContextTransitioning, 69 | fromView: UIView 70 | ) { 71 | let animationDuration = transitionDuration(using: transitionContext) 72 | UIView.animate( 73 | withDuration: animationDuration, 74 | animations: { 75 | fromView.alpha = 0 76 | }, 77 | completion: { 78 | transitionContext.completeTransition($0) 79 | } 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Enums/Common+Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Common+Enums.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | enum MediaCell { 11 | static let ratio: CGFloat = 1.25 12 | } 13 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/Array+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extension.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/14. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array { 11 | subscript (safe index: Int) -> Element? { 12 | return indices ~= index ? self[index] : nil 13 | } 14 | 15 | subscript(loop index: Int) -> Element? { 16 | return isEmpty ? nil : self[(index + count) % count] 17 | } 18 | 19 | func elements(in range: Range) -> [Element] { 20 | var result: [Element] = [] 21 | range.forEach { index in 22 | if indices ~= index { 23 | result.append(self[index]) 24 | } 25 | } 26 | return result 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/CALayer+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/10. 6 | // 7 | 8 | import UIKit 9 | 10 | // 참고 : https://stackoverflow.com/questions/33994520/how-to-pause-and-resume-uiview-animatewithduration 11 | 12 | extension CALayer { 13 | func pauseLayer() { 14 | let pausedTime = convertTime(CACurrentMediaTime(), from: nil) 15 | speed = 0.0 16 | timeOffset = pausedTime 17 | } 18 | 19 | func resumeLayer() { 20 | let pausedTime = timeOffset 21 | speed = 1.0 22 | timeOffset = 0.0 23 | beginTime = 0.0 24 | let timeSincePause = convertTime(CACurrentMediaTime(), from: nil) - pausedTime 25 | beginTime = timeSincePause 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/CGPoint+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+Extension.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/18. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGPoint { 11 | static func - (lhs: CGPoint, rhs: CGPoint) -> Self { 12 | return CGPoint( 13 | x: lhs.x - rhs.x, 14 | y: lhs.y - rhs.y 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/Date+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | var hour: Int { 12 | return Calendar.current.component(.hour, from: self) 13 | } 14 | 15 | var day: Int { 16 | return Calendar.current.component(.day, from: self) 17 | } 18 | 19 | var month: Int { 20 | return Calendar.current.component(.month, from: self) 21 | } 22 | 23 | var weekdayEng: String { 24 | return ["S", "M", "T", "W", "T", "F", "S"] [weekday - 1] 25 | } 26 | 27 | var weekday: Int { 28 | return Calendar.current.component(.weekday, from: self) 29 | } 30 | 31 | var year: Int { 32 | return Calendar.current.component(.year, from: self) 33 | } 34 | } 35 | 36 | extension Date { 37 | var midnight: Date { 38 | Calendar.current.startOfDay(for: self) 39 | } 40 | } 41 | 42 | extension Date { 43 | var realmId: String { 44 | "\(year)\(month)\(day)" 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/Int+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Int { 11 | var digitalClockFormatted: String { 12 | let minutes = self / 60 13 | let seconds = self - (minutes * 60) 14 | return String(format: "%02d", minutes) + ":" + String(format: "%02d", seconds) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/Notification+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification+Extension.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Notification.Name { 11 | static let refreshHome = Notification.Name("RefreshHome") 12 | static let checkCurrentPlay = Notification.Name("CheckCurrentPlay") 13 | static let controlForFocus = Notification.Name("ControlForFocus") 14 | } 15 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UIApplication+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+Extension.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/14. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIApplication { 11 | static var statusBarHeight: CGFloat { 12 | if #available(iOS 13.0, *) { 13 | let window = UIApplication.shared.windows.first 14 | return window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 15 | } else { 16 | return UIApplication.shared.statusBarFrame.height 17 | } 18 | } 19 | 20 | static var bottomIndicatorHeight: CGFloat { 21 | if #available(iOS 11.0, *) { 22 | let window = UIApplication.shared.windows.first 23 | return window?.safeAreaInsets.bottom ?? 0 24 | } else { 25 | return -1 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UICollectionView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/14. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionView { 11 | func register(_ cellClass: T.Type) { 12 | register(T.self, forCellWithReuseIdentifier: T.identifier) 13 | } 14 | 15 | func dequeueReusableCell(indexPath: IndexPath) -> T? { 16 | return self.dequeueReusableCell(withReuseIdentifier: T.identifier, for: indexPath) as? T 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UICollectionViewCell+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionViewCell+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionViewCell { 11 | static var identifier: String { 12 | return String(describing: self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UIColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/18. 6 | // 7 | import UIKit 8 | 9 | // 참고: https://stackoverflow.com/questions/24263007/how-to-use-hex-color-values 10 | 11 | extension UIColor { 12 | convenience init(rgb: Int, alpha: CGFloat = 1.0) { 13 | self.init( 14 | red: CGFloat((rgb >> 16) & 0xFF)/255, 15 | green: CGFloat((rgb >> 8) & 0xFF)/255, 16 | blue: CGFloat(rgb & 0xFF)/255, 17 | alpha: alpha 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UILabel+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | // 참고자료: https://stackoverflow.com/questions/39158604/how-to-increase-line-spacing-in-uilabel-in-swift 11 | 12 | extension UILabel { 13 | func setLineSpacing(lineSpacing: CGFloat = 0.0, lineHeightMultiple: CGFloat = 0.0) { 14 | let paragraphStyle = NSMutableParagraphStyle() 15 | paragraphStyle.lineSpacing = lineSpacing 16 | paragraphStyle.lineHeightMultiple = lineHeightMultiple 17 | 18 | let attributedString: NSMutableAttributedString 19 | if let labelattributedText = self.attributedText { 20 | attributedString = NSMutableAttributedString(attributedString: labelattributedText) 21 | } else if let labelText = self.text { 22 | attributedString = NSMutableAttributedString(string: labelText) 23 | } else { 24 | attributedString = NSMutableAttributedString(string: "") 25 | } 26 | 27 | attributedString.addAttribute( 28 | .paragraphStyle, 29 | value: paragraphStyle, 30 | range: NSRange(location: 0, length: attributedString.length) 31 | ) 32 | attributedText = attributedString 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UIScreen+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScreen+Extension.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/14. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIScreen { 11 | static var deviceScreenSize: CGSize { 12 | CGSize( 13 | width: UIScreen.main.bounds.width, 14 | height: UIScreen.main.bounds.height 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UITableView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableViewCell+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/14. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UITableView { 11 | func register(_ cellClass: T.Type) { 12 | return register(T.self, forCellReuseIdentifier: T.identifier) 13 | } 14 | 15 | func dequeueReusableCell() -> T? { 16 | return self.dequeueReusableCell(withIdentifier: T.identifier) as? T 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UITableViewCell+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableViewCell+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UITableViewCell { 11 | static var identifier: String { 12 | return String(describing: self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Extensions/UIView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extension.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/07. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | func makeCircle() { 12 | layer.cornerRadius = bounds.height / 2 13 | layer.masksToBounds = true 14 | } 15 | 16 | func makeBlurBackground(style: UIBlurEffect.Style = .light) { 17 | let blurEffect = UIBlurEffect(style: style) 18 | let blurView = UIVisualEffectView(effect: blurEffect) 19 | blurView.isUserInteractionEnabled = false 20 | insertSubview(blurView, at: 0) 21 | blurView.snp.makeConstraints { 22 | $0.edges.equalToSuperview() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Views/CloseButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CloseButton.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CloseButton: UIButton { 11 | override init(frame: CGRect) { 12 | super.init(frame: frame) 13 | configure() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | configure() 19 | } 20 | 21 | private func configure() { 22 | setBackgroundImage(UIImage(systemName: "xmark"), for: .normal) 23 | var font: UIFont = .preferredFont(forTextStyle: .title2) 24 | if let fontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold) { 25 | font = UIFont(descriptor: fontDescriptor, size: 0) 26 | } 27 | 28 | titleLabel?.font = font 29 | tintColor = UIColor.white 30 | backgroundColor = .clear 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Common/Views/MediaCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaCollectionViewCell.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/07. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class MediaCollectionViewCell: UICollectionViewCell { 13 | lazy var imageView: UIImageView = { 14 | let imageView = UIImageView(frame: frame) 15 | imageView.image = UIImage(systemName: "photo") 16 | imageView.clipsToBounds = true 17 | return imageView 18 | }() 19 | lazy var titleView: UILabel = { 20 | let label = UILabel(frame: frame) 21 | label.text = "몰디브 자연음" 22 | label.textColor = .white 23 | label.textAlignment = .center 24 | label.font = .systemFont(ofSize: 16, weight: .semibold) 25 | return label 26 | }() 27 | 28 | override func layoutSubviews() { 29 | super.layoutSubviews() 30 | imageView.makeCircle() 31 | } 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | self.configureUI() 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | super.init(coder: coder) 40 | self.configureUI() 41 | } 42 | 43 | private func configureUI() { 44 | backgroundColor = .white 45 | layer.cornerRadius = 10 46 | 47 | addSubview(imageView) 48 | let frame = self.frame 49 | imageView.snp.makeConstraints { 50 | $0.width.equalToSuperview().multipliedBy(0.6) 51 | $0.centerX.equalToSuperview() 52 | $0.height.equalTo(imageView.snp.width) 53 | $0.top.equalToSuperview().offset(frame.width * 0.2) 54 | } 55 | addSubview(titleView) 56 | titleView.snp.makeConstraints { 57 | $0.width.equalToSuperview().multipliedBy(0.9) 58 | $0.centerX.equalToSuperview() 59 | $0.top.equalTo(imageView.snp.bottom).offset(frame.width * 0.2) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Explore/Main/ViewModels/ExploreViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExploreViewModel.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class ExploreViewModel { 14 | private let searchMediaUseCase = SearchMediaUseCase() 15 | private let disposeBag = DisposeBag() 16 | 17 | let categoryItems = BehaviorRelay<[Media]>(value: []) 18 | let soundTagList = SoundTag.allCases 19 | var selectedTagIndex = 0 20 | 21 | func categorize(by tag: String) { 22 | guard !tag.isEmpty else { return } 23 | 24 | searchMediaUseCase.searchResult 25 | .bind { [weak self] in 26 | self?.categoryItems.accept($0) 27 | } 28 | .disposed(by: disposeBag) 29 | searchMediaUseCase.search(tag: tag) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Explore/Main/ViewModels/SearchViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewModel.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class SearchViewModel { 14 | let searchHistory = BehaviorRelay<[String]>(value: []) 15 | let searchResult = BehaviorRelay<[Media]>(value: []) 16 | 17 | private let disposeBag = DisposeBag() 18 | private let searchHistoryUseCase = SearchHistoryUseCase() 19 | private let searchMediaUseCase = SearchMediaUseCase() 20 | 21 | func saveSearchKeyword(keyword: String) { 22 | var searchHistoryValue = searchHistoryUseCase.load() 23 | searchHistoryValue.append(keyword) 24 | searchHistoryUseCase.save(searchHistoryValue) 25 | 26 | searchHistoryValue = searchHistoryValue.reversed() 27 | .elements(in: 0..<5) 28 | searchHistory.accept(searchHistoryValue) 29 | } 30 | 31 | func loadSearchHistory() { 32 | let searchHistoryValue = searchHistoryUseCase.load() 33 | .reversed() 34 | .elements(in: 0..<5) 35 | searchHistory.accept(searchHistoryValue) 36 | } 37 | 38 | func removeSearchHistory(at index: Int) { 39 | guard self.searchHistory.value.count > index else { return } 40 | var searchHistoryValue = searchHistoryUseCase.load() 41 | searchHistoryValue.remove(at: searchHistoryValue.count-1-index) 42 | searchHistoryUseCase.save(searchHistoryValue) 43 | } 44 | 45 | func search(keyword: String) { 46 | searchMediaUseCase.searchResult 47 | .bind { [weak self] in 48 | self?.searchResult.accept($0) 49 | } 50 | .disposed(by: disposeBag) 51 | 52 | searchMediaUseCase.search(keyword: keyword) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Explore/Main/Views/SearchTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchTableViewCell.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class SearchTableViewCell: UITableViewCell { 13 | lazy var searchHistory = UILabel() 14 | lazy var deleteButton: UIButton = { 15 | let button = UIButton(type: .custom) 16 | button.tintColor = .darkGray 17 | button.setImage(UIImage(systemName: "xmark"), for: .normal) 18 | button.backgroundColor = .clear 19 | return button 20 | }() 21 | lazy var searchHistoryStackView: UIStackView = { 22 | let stackView = UIStackView(arrangedSubviews: [searchHistory, deleteButton]) 23 | stackView.axis = .horizontal 24 | stackView.spacing = 20 25 | return stackView 26 | }() 27 | 28 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 29 | super.init(style: style, reuseIdentifier: reuseIdentifier) 30 | configureUI() 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | super.init(coder: coder) 35 | configureUI() 36 | } 37 | 38 | private func configureUI() { 39 | selectionStyle = .none 40 | 41 | contentView.addSubview(searchHistoryStackView) 42 | searchHistoryStackView.snp.makeConstraints { 43 | $0.top.bottom.equalToSuperview() 44 | $0.leading.trailing.equalToSuperview().inset(20) 45 | } 46 | } 47 | 48 | func changeColor() { 49 | switch ApplicationMode.shared.mode.value { 50 | case .bright: 51 | backgroundColor = .white 52 | searchHistory.textColor = .black 53 | case .dark: 54 | backgroundColor = .black 55 | searchHistory.textColor = .white 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Explore/Main/Views/SoundTagCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryCollectionViewCell.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/08. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class SoundTagCollectionViewCell: UICollectionViewCell { 13 | lazy var soundTagLabel: UILabel = { 14 | let label = UILabel() 15 | label.textColor = .lightGray 16 | label.font = .systemFont(ofSize: 17, weight: .semibold) 17 | return label 18 | }() 19 | 20 | override var isSelected: Bool { 21 | didSet { 22 | if isSelected { 23 | backgroundColor = UIColor(rgb: 0x00FF80, alpha: 1.0) 24 | soundTagLabel.textColor = .white 25 | soundTagLabel.font = .systemFont(ofSize: 17, weight: .bold) 26 | } else { 27 | switch ApplicationMode.shared.mode.value { 28 | case .bright: 29 | backgroundColor = .white 30 | case .dark: 31 | backgroundColor = .black 32 | } 33 | soundTagLabel.textColor = .lightGray 34 | soundTagLabel.font = .systemFont(ofSize: 17, weight: .semibold) 35 | } 36 | } 37 | } 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | configureUI() 42 | } 43 | 44 | required init?(coder: NSCoder) { 45 | super.init(coder: coder) 46 | configureUI() 47 | } 48 | 49 | private func configureUI() { 50 | layer.cornerRadius = frame.height/2 51 | 52 | contentView.addSubview(soundTagLabel) 53 | soundTagLabel.snp.makeConstraints { 54 | $0.center.equalToSuperview() 55 | } 56 | 57 | switch ApplicationMode.shared.mode.value { 58 | case .bright: 59 | backgroundColor = .white 60 | case .dark: 61 | backgroundColor = .black 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enums.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/08. 6 | // 7 | 8 | import CoreGraphics 9 | import UIKit 10 | 11 | enum MediaMode: Int { 12 | case bright, darkness 13 | } 14 | 15 | enum FocusMode: CaseIterable { 16 | case normal, pomodoro, infinity, breath 17 | 18 | enum Normal { 19 | static let title = "기본" 20 | static let image = "house" 21 | } 22 | enum Pomodoro { 23 | static let title = "뽀모도로" 24 | static let image = "house" 25 | } 26 | enum Infinity { 27 | static let title = "무한" 28 | static let image = "house" 29 | } 30 | enum Breath { 31 | static let title = "심호흡" 32 | static let image = "house" 33 | } 34 | 35 | static func getValue(from mode: FocusMode) -> (title: String, image: String) { 36 | switch mode { 37 | case .normal: 38 | return (Normal.title, Normal.image) 39 | case .pomodoro: 40 | return (Pomodoro.title, Pomodoro.image) 41 | case .infinity: 42 | return (Infinity.title, Infinity.image) 43 | case .breath: 44 | return (Breath.title, Breath.image) 45 | } 46 | } 47 | 48 | func getFocusViewController() -> UIViewController { 49 | switch self { 50 | case .normal: 51 | return FocusViewControllerFactory.makeDefaultTimer() 52 | case .pomodoro: 53 | return FocusViewControllerFactory.makePomodoroTimer() 54 | case .infinity: 55 | return FocusViewControllerFactory.makeInfinityTimer() 56 | case .breath: 57 | return UIViewController() 58 | } 59 | } 60 | } 61 | 62 | enum FocusViewButtonSize { 63 | static let startButton = CGSize(width: 115, height: 50) 64 | static let pauseButton = CGSize(width: 100, height: 50) 65 | static let continueButton = CGSize(width: 115, height: 50) 66 | static let exitButton = CGSize(width: 115, height: 50) 67 | } 68 | 69 | enum TimerState: Equatable { 70 | case ready 71 | case running(isContinue: Bool) 72 | case paused 73 | } 74 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/Focus+Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Focus+Enums.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | enum FocusViewControllerSize { 11 | static let timerViewLength = UIScreen.deviceScreenSize.width * 0.56 12 | } 13 | 14 | enum BreathMode { 15 | static let media = Media( 16 | id: "", 17 | name: "Breath", 18 | explanation: "", 19 | maxim: "", 20 | speaker: "", 21 | color: "", 22 | mode: 0, 23 | tag: "", 24 | thumbnailImageFileName: "", 25 | videoFileName: "", 26 | audioFileName: "breath.WAV" 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/ViewModels/BreathFocusViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BreathViewModel.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxCocoa 11 | import RxRelay 12 | import RxSwift 13 | 14 | enum BreathFocusState { 15 | case running 16 | case stop 17 | } 18 | 19 | final class BreathFocusViewModel { 20 | let clockTime = BehaviorRelay(value: -1) 21 | let isFocusRecordSaved = BehaviorRelay(value: false) 22 | let focusState = BehaviorRelay(value: .stop) 23 | let timerState = BehaviorRelay(value: .ready) 24 | let focusTimeList = [Int](1...15) 25 | var focusTime = 7 26 | 27 | private let disposeBag = DisposeBag() 28 | private let saveFocusTimeUseCase = SaveFocusTimeUseCase() 29 | private let audioPlayUseCase = AudioPlayUseCase() 30 | 31 | private var runningStateDisposeBag = DisposeBag() 32 | 33 | func changeState(to state: BreathFocusState) { 34 | self.focusState.accept(state) 35 | } 36 | 37 | func startClockTimer() { 38 | audioPlayUseCase.control(media: BreathMode.media, autoPlay: true, restart: true) 39 | .subscribe(onFailure: { error in 40 | print(error.localizedDescription) 41 | }).disposed(by: disposeBag) 42 | 43 | clockTime.accept(0) 44 | Observable.interval(.seconds(1), scheduler: MainScheduler.instance) 45 | .subscribe { [weak self] _ in 46 | guard let self = self else { return } 47 | self.clockTime.accept(self.clockTime.value + 1) 48 | } 49 | .disposed(by: runningStateDisposeBag) 50 | } 51 | 52 | func resetClockTimer() { 53 | audioPlayUseCase.control(media: BreathMode.media, state: false) 54 | .subscribe(onFailure: { error in 55 | print(error.localizedDescription) 56 | }).disposed(by: disposeBag) 57 | 58 | clockTime.accept(-1) 59 | runningStateDisposeBag = DisposeBag() 60 | } 61 | 62 | func setFocusTime(seconds: Int) { 63 | focusTime = seconds 64 | } 65 | 66 | func saveFocusRecord() { 67 | saveFocusTimeUseCase.execute(seconds: clockTime.value) 68 | .subscribe { [weak self] in 69 | self?.isFocusRecordSaved.accept($0) 70 | } onFailure: { [weak self] _ in 71 | self?.isFocusRecordSaved.accept(false) 72 | } 73 | .disposed(by: disposeBag) 74 | } 75 | 76 | func alertNotification() { 77 | let clockTime = clockTime.value 78 | let angryEmpjis = ["😡", "🤬", "🥵", "🥶", "😰"] 79 | let happyEmojis = ["☺️", "😘", "😍", "🥳", "🤩"] 80 | let times = clockTime / 7 81 | let message = times > 0 82 | ? "\(times)회 호흡 운동하셨습니다." + (happyEmojis.randomElement() ?? "") 83 | : "\(times)회... 반복했습니다. 집중합시다!" + (angryEmpjis.randomElement() ?? "") 84 | PushNotificationMananger.shared.presentFocusStopNotification( 85 | title: .focusFinish, 86 | body: message 87 | ) 88 | FeedbackGenerator.shared.impactOccurred() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/ViewModels/DefaultFocusViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultFocusViewModel.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class DefaultFocusViewModel { 14 | let clockTime = BehaviorRelay(value: 0) 15 | let isFocusRecordSaved = BehaviorRelay(value: false) 16 | let timerState = BehaviorRelay(value: .ready) 17 | let focusTimeList = [1, 5, 8, 10, 15, 20, 25, 30, 35, 18 | 40, 45, 50, 55, 60, 70, 80, 90, 19 | 100, 110, 120, 130, 140, 150, 160, 170, 180] 20 | var focusTime = 60 21 | 22 | private let disposeBag = DisposeBag() 23 | private let saveFocusTimeUseCase = SaveFocusTimeUseCase() 24 | private let audioPlayUseCase = AudioPlayUseCase() 25 | 26 | private var runningStateDisposeBag = DisposeBag() 27 | 28 | func changeTimerState(to timerState: TimerState) { 29 | self.timerState.accept(timerState) 30 | } 31 | 32 | func startClockTimer() { 33 | NotificationCenter.default.post( 34 | name: .controlForFocus, 35 | object: nil, 36 | userInfo: [ 37 | "PlayState": true 38 | ] 39 | ) 40 | 41 | Observable.interval(RxTimeInterval.seconds(1), 42 | scheduler: MainScheduler.instance) 43 | .subscribe { [weak self] _ in 44 | guard let self = self else { return } 45 | self.clockTime.accept(self.clockTime.value + 1) 46 | } 47 | .disposed(by: runningStateDisposeBag) 48 | } 49 | 50 | func pauseClockTimer() { 51 | NotificationCenter.default.post( 52 | name: .controlForFocus, 53 | object: nil, 54 | userInfo: [ 55 | "PlayState": false 56 | ] 57 | ) 58 | 59 | runningStateDisposeBag = DisposeBag() 60 | } 61 | 62 | func resetClockTimer() { 63 | NotificationCenter.default.post( 64 | name: .controlForFocus, 65 | object: nil, 66 | userInfo: [ 67 | "PlayState": false 68 | ] 69 | ) 70 | 71 | clockTime.accept(0) 72 | runningStateDisposeBag = DisposeBag() 73 | } 74 | 75 | func setFocusTime(seconds: Int) { 76 | focusTime = seconds 77 | } 78 | 79 | func saveFocusRecord() { 80 | saveFocusTimeUseCase.execute(seconds: clockTime.value) 81 | .subscribe { [weak self] in 82 | self?.isFocusRecordSaved.accept($0) 83 | } onFailure: { [weak self] _ in 84 | self?.isFocusRecordSaved.accept(false) 85 | } 86 | .disposed(by: disposeBag) 87 | } 88 | 89 | func alertNotification() { 90 | let clockTime = clockTime.value 91 | let sadEmojis = ["🥶", "😣", "😞", "😟", "😕"] 92 | let happyEmojis = ["☺️", "😘", "😍", "🥳", "🤩"] 93 | let minuteString = clockTime / 60 == 0 ? "" : "\(clockTime / 60)분 " 94 | let secondString = clockTime % 60 == 0 ? "" : "\(clockTime % 60)초 " 95 | let message = focusTime - clockTime > 0 96 | ? "완료시간 전에 종료되었어요." + (sadEmojis.randomElement() ?? "") 97 | : minuteString + secondString + "집중하셨어요!" + (happyEmojis.randomElement() ?? "") 98 | PushNotificationMananger.shared.presentFocusStopNotification( 99 | title: .focusFinish, 100 | body: message 101 | ) 102 | 103 | FeedbackGenerator.shared.impactOccurred() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/ViewModels/InfinityFocusViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfinityFocusViewModel.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/09. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class InfinityFocusViewModel { 14 | let clockTime = BehaviorRelay(value: 0) 15 | let isFocusRecordSaved = BehaviorRelay(value: false) 16 | let timerState = BehaviorRelay(value: .ready) 17 | 18 | private let disposeBag = DisposeBag() 19 | private let saveFocusTimeUseCase = SaveFocusTimeUseCase() 20 | private let audioPlayUseCase = AudioPlayUseCase() 21 | 22 | private var runningStateDisposeBag = DisposeBag() 23 | 24 | func changeTimerState(to timerState: TimerState) { 25 | self.timerState.accept(timerState) 26 | } 27 | 28 | func startClockTimer() { 29 | NotificationCenter.default.post( 30 | name: .controlForFocus, 31 | object: nil, 32 | userInfo: [ 33 | "PlayState": true 34 | ] 35 | ) 36 | 37 | Observable.interval(RxTimeInterval.seconds(1), 38 | scheduler: MainScheduler.instance) 39 | .subscribe { [weak self] _ in 40 | guard let self = self else { return } 41 | self.clockTime.accept(self.clockTime.value + 1) 42 | } 43 | .disposed(by: runningStateDisposeBag) 44 | } 45 | 46 | func pauseClockTimer() { 47 | NotificationCenter.default.post( 48 | name: .controlForFocus, 49 | object: nil, 50 | userInfo: [ 51 | "PlayState": false 52 | ] 53 | ) 54 | 55 | runningStateDisposeBag = DisposeBag() 56 | } 57 | 58 | func resetClockTimer() { 59 | NotificationCenter.default.post( 60 | name: .controlForFocus, 61 | object: nil, 62 | userInfo: [ 63 | "PlayState": false 64 | ] 65 | ) 66 | 67 | clockTime.accept(0) 68 | runningStateDisposeBag = DisposeBag() 69 | } 70 | 71 | func saveFocusRecord() { 72 | saveFocusTimeUseCase.execute(seconds: clockTime.value) 73 | .subscribe { [weak self] in 74 | self?.isFocusRecordSaved.accept($0) 75 | } onFailure: { [weak self] _ in 76 | self?.isFocusRecordSaved.accept(false) 77 | } 78 | .disposed(by: disposeBag) 79 | } 80 | 81 | func alertNotification() { 82 | let clockTime = clockTime.value 83 | let happyEmojis = ["☺️", "😘", "😍", "🥳", "🤩"] 84 | let minuteString = clockTime / 60 == 0 ? "" : "\(clockTime / 60)분 " 85 | let secondString = clockTime % 60 == 0 ? "" : "\(clockTime % 60)초 " 86 | let message = minuteString + secondString + "집중하셨어요!" + (happyEmojis.randomElement() ?? "") 87 | PushNotificationMananger.shared.presentFocusStopNotification( 88 | title: .focusFinish, 89 | body: message 90 | ) 91 | FeedbackGenerator.shared.impactOccurred() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/ViewModels/PomodoroFocusViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PomodoroFocusViewModel.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | enum PomodoroMode { 14 | case work 15 | case relax 16 | } 17 | 18 | final class PomodoroFocusViewModel { 19 | let clockTime = BehaviorRelay(value: 0) 20 | let isFocusRecordSaved = BehaviorRelay(value: false) 21 | let timerState = BehaviorRelay(value: .ready) 22 | let mode = BehaviorRelay(value: .work) 23 | let focusTimeList = [1, 5, 8, 10, 15, 20, 25, 30, 35, 24 | 40, 45, 50, 55, 60, 70, 80, 90, 25 | 100, 110, 120, 130, 140, 150, 160, 170, 180] 26 | let timeUnit = 5 27 | 28 | var totalFocusTime = 0 29 | 30 | lazy var focusTime = timeUnit 31 | 32 | private let disposeBag = DisposeBag() 33 | private let saveFocusTimeUseCase = SaveFocusTimeUseCase() 34 | 35 | private var runningStateDisposeBag = DisposeBag() 36 | 37 | func changeTimerState(to timerState: TimerState) { 38 | self.timerState.accept(timerState) 39 | } 40 | 41 | func startClockTimer() { 42 | if mode.value == .work { 43 | NotificationCenter.default.post( 44 | name: .controlForFocus, 45 | object: nil, 46 | userInfo: [ 47 | "PlayState": true 48 | ] 49 | ) 50 | 51 | } 52 | 53 | Observable.interval(RxTimeInterval.seconds(1), 54 | scheduler: MainScheduler.instance) 55 | .subscribe { [weak self] _ in 56 | guard let self = self else { return } 57 | self.clockTime.accept(self.clockTime.value + 1) 58 | if self.mode.value == .work { 59 | self.totalFocusTime += 1 60 | } 61 | print(self.totalFocusTime) // 62 | } 63 | .disposed(by: runningStateDisposeBag) 64 | } 65 | 66 | func pauseClockTimer() { 67 | NotificationCenter.default.post( 68 | name: .controlForFocus, 69 | object: nil, 70 | userInfo: [ 71 | "PlayState": false 72 | ] 73 | ) 74 | 75 | runningStateDisposeBag = DisposeBag() 76 | } 77 | 78 | func resetClockTimer() { 79 | NotificationCenter.default.post( 80 | name: .controlForFocus, 81 | object: nil, 82 | userInfo: [ 83 | "PlayState": false 84 | ] 85 | ) 86 | 87 | clockTime.accept(0) 88 | runningStateDisposeBag = DisposeBag() 89 | } 90 | 91 | func setFocusTime(value: Int) { 92 | focusTime = value * timeUnit 93 | } 94 | 95 | func saveFocusRecord() { 96 | saveFocusTimeUseCase.execute(seconds: totalFocusTime) 97 | .subscribe { [weak self] in 98 | self?.isFocusRecordSaved.accept($0) 99 | } onFailure: { [weak self] _ in 100 | self?.isFocusRecordSaved.accept(false) 101 | } 102 | .disposed(by: disposeBag) 103 | } 104 | 105 | func changeMode() { 106 | switch mode.value { 107 | case .work: 108 | mode.accept(.relax) 109 | case .relax: 110 | mode.accept(.work) 111 | } 112 | } 113 | 114 | func changeToWorkMode() { 115 | mode.accept(.work) 116 | } 117 | 118 | func resetTotalFocusTime() { 119 | totalFocusTime = 0 120 | } 121 | 122 | func alertNotification() { 123 | let clockTime = clockTime.value 124 | let sadEmojis = ["🥶", "😣", "😞", "😟", "😕"] 125 | let happyEmojis = ["☺️", "😘", "😍", "🥳", "🤩"] 126 | let relaxEmojis = ["👍", "👏", "🤜", "🙌", "🙏"] 127 | switch mode.value { 128 | case .work: 129 | let minuteString = clockTime / 60 == 0 ? "" : "\(clockTime / 60)분 " 130 | let secondString = clockTime % 60 == 0 ? "" : "\(clockTime % 60)초 " 131 | let message = focusTime - clockTime > 0 132 | ? "완료시간 전에 종료되었어요." + (sadEmojis.randomElement() ?? "") 133 | : minuteString + secondString + "집중하셨어요!" + (happyEmojis.randomElement() ?? "") 134 | PushNotificationMananger.shared.presentFocusStopNotification( 135 | title: .focusFinish, 136 | body: message 137 | ) 138 | case .relax: 139 | let message = "휴식시간이 끝났어요! 다시 집중해볼까요?" + (relaxEmojis.randomElement() ?? "") 140 | PushNotificationMananger.shared.presentFocusStopNotification( 141 | title: .relaxFinish, 142 | body: message 143 | ) 144 | } 145 | FeedbackGenerator.shared.impactOccurred() 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/Views/FocusButtons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusButtons.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FocusStartButton: UIButton { 11 | override init(frame: CGRect) { 12 | super.init(frame: frame) 13 | configure() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | private func configure() { 21 | tintColor = .gray 22 | let playImage = UIImage(systemName: "play.fill")?.withRenderingMode(.alwaysTemplate) 23 | setImage(playImage, for: .normal) 24 | imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12) 25 | setTitle("Start", for: .normal) 26 | titleLabel?.font = UIFont.boldSystemFont(ofSize: 17) 27 | titleEdgeInsets = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 0) 28 | setTitleColor(UIColor.gray, for: .normal) 29 | layer.cornerRadius = 25 30 | backgroundColor = .white 31 | } 32 | } 33 | 34 | final class FocusContinueButton: UIButton { 35 | override init(frame: CGRect) { 36 | super.init(frame: frame) 37 | configure() 38 | } 39 | 40 | required init?(coder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | private func configure() { 45 | tintColor = .gray 46 | setTitle("Continue", for: .normal) 47 | titleLabel?.font = UIFont.boldSystemFont(ofSize: 17) 48 | titleLabel?.textAlignment = .center 49 | setTitleColor(UIColor.gray, for: .normal) 50 | layer.cornerRadius = 25 51 | backgroundColor = .white 52 | } 53 | } 54 | 55 | final class FocusPauseButton: UIButton { 56 | override init(frame: CGRect) { 57 | super.init(frame: frame) 58 | configure() 59 | } 60 | 61 | required init?(coder: NSCoder) { 62 | fatalError("init(coder:) has not been implemented") 63 | } 64 | 65 | private func configure() { 66 | setTitle("Pause", for: .normal) 67 | titleLabel?.font = UIFont.boldSystemFont(ofSize: 17) 68 | setTitleColor(UIColor.white, for: .normal) 69 | layer.cornerRadius = 25 70 | backgroundColor = .gray 71 | layer.borderColor = UIColor.white.cgColor 72 | layer.borderWidth = 2 73 | } 74 | } 75 | 76 | final class FocusExitButton: UIButton { 77 | override init(frame: CGRect) { 78 | super.init(frame: frame) 79 | configure() 80 | } 81 | 82 | required init?(coder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | private func configure() { 87 | setTitle("Exit", for: .normal) 88 | titleLabel?.font = UIFont.boldSystemFont(ofSize: 17) 89 | setTitleColor(UIColor.white, for: .normal) 90 | layer.cornerRadius = 25 91 | backgroundColor = .gray 92 | layer.borderColor = UIColor.white.cgColor 93 | layer.borderWidth = 2 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/Views/FocusViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusViewController.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/11. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxCocoa 11 | import RxSwift 12 | 13 | class FocusViewController: UIViewController { 14 | let closeButton = CloseButton() 15 | let disposeBag = DisposeBag() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | configureCloseButton() 20 | bindCloseButton() 21 | } 22 | 23 | private func configureCloseButton() { 24 | view.addSubview(closeButton) 25 | closeButton.snp.makeConstraints { 26 | $0.width.equalTo(30) 27 | $0.top.equalTo(view.snp.topMargin).offset(10) 28 | $0.leading.equalToSuperview().offset(10) 29 | $0.height.equalTo(30) 30 | } 31 | } 32 | 33 | private func bindCloseButton() { 34 | closeButton.rx.tap.bind { [weak self] _ in 35 | self?.dismiss(animated: true) { 36 | // NotificationCenter.default.post( 37 | // name: .checkCurrentPlay, 38 | // object: nil, 39 | // userInfo: nil 40 | // ) 41 | } 42 | } 43 | .disposed(by: disposeBag) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Focus/Views/FocusViewControllerFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusViewControllerFactory.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FocusViewControllerFactory { 11 | static func makeDefaultTimer() -> DefaultFocusViewController { 12 | return DefaultFocusViewController() 13 | } 14 | static func makePomodoroTimer() -> PomodoroFocusViewController { 15 | return PomodoroFocusViewController() 16 | } 17 | static func makeInfinityTimer() -> InfinityFocusViewController { 18 | return InfinityFocusViewController() 19 | } 20 | static func makeBreathTimer() -> BreathFocusViewController { 21 | return BreathFocusViewController() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Home+Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enums.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/08. 6 | // 7 | 8 | import CoreGraphics 9 | import UIKit 10 | 11 | enum MediaMode: Int { 12 | case bright, dark 13 | } 14 | 15 | enum FocusMode: CaseIterable { 16 | case normal, pomodoro, infinity, breath 17 | 18 | enum Normal { 19 | static let title = "기본" 20 | static let image = "focus_default" 21 | } 22 | enum Pomodoro { 23 | static let title = "뽀모도로" 24 | static let image = "focus_pomodoro" 25 | } 26 | enum Infinity { 27 | static let title = "무한" 28 | static let image = "focus_infinite" 29 | } 30 | enum Breath { 31 | static let title = "심호흡" 32 | static let image = "focus_breath" 33 | } 34 | 35 | static func getValue(from mode: FocusMode) -> (title: String, image: String) { 36 | switch mode { 37 | case .normal: 38 | return (Normal.title, Normal.image) 39 | case .pomodoro: 40 | return (Pomodoro.title, Pomodoro.image) 41 | case .infinity: 42 | return (Infinity.title, Infinity.image) 43 | case .breath: 44 | return (Breath.title, Breath.image) 45 | } 46 | } 47 | 48 | func getFocusViewController() -> FocusViewController { 49 | switch self { 50 | case .normal: 51 | return FocusViewControllerFactory.makeDefaultTimer() 52 | case .pomodoro: 53 | return FocusViewControllerFactory.makePomodoroTimer() 54 | case .infinity: 55 | return FocusViewControllerFactory.makeInfinityTimer() 56 | case .breath: 57 | return FocusViewControllerFactory.makeBreathTimer() 58 | } 59 | } 60 | } 61 | 62 | enum HomeMainViewSize { 63 | static let topViewHeight: CGFloat = 100 64 | static let bottomViewHeight = UIScreen.deviceScreenSize.height 65 | static let focusButtonSize = CGSize(width: 60, height: 90) 66 | } 67 | 68 | enum FocusViewButtonSize { 69 | static let startButton = CGSize(width: 115, height: 50) 70 | static let pauseButton = CGSize(width: 100, height: 50) 71 | static let continueButton = CGSize(width: 115, height: 50) 72 | static let exitButton = CGSize(width: 115, height: 50) 73 | } 74 | 75 | enum TimerState: Equatable { 76 | case ready 77 | case running(isResume: Bool) 78 | case paused 79 | } 80 | 81 | enum FileExtension { 82 | enum Image: String, CaseIterable { 83 | case png, jpeg, jpg, bmp 84 | } 85 | 86 | enum Video: String, CaseIterable { 87 | case mp4, avi 88 | } 89 | } 90 | 91 | enum RefreshHomeData { 92 | case brightMode, darkMode, playHistory, favorite 93 | } 94 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/ViewModels/FavoriteViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteViewModel.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class FavoriteViewModel { 14 | private let favoriteUseCase = FavoriteUseCase() 15 | private let disposeBag = DisposeBag() 16 | 17 | let favoriteSoundList = BehaviorRelay<[Media]>(value: []) 18 | 19 | func viewDidLoad() { 20 | favoriteUseCase.fetchAll() 21 | .subscribe { [weak self] in 22 | self?.favoriteSoundList.accept($0) 23 | } onFailure: { error in 24 | print(error.localizedDescription) 25 | } 26 | .disposed(by: disposeBag) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/ViewModels/MediaPlayViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaPlayViewModel.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MediaPlayViewModel { 13 | private let fetchMediaURLUseCase = FetchMediaURLUseCase() 14 | 15 | func didSetMedia(fileName: String, type: MediaType) -> Single { 16 | return fetchMediaURLUseCase.getMediaURL(fileName: fileName, type: type) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/ViewModels/PlayHistoryViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayHistoryViewModel.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class PlayHistoryViewModel { 14 | private let playHistoryUseCase = PlayHistoryUseCase() 15 | private let disposeBag = DisposeBag() 16 | 17 | let playHistory = BehaviorRelay<[Media]>(value: []) 18 | 19 | func viewDidLoad() { 20 | playHistoryUseCase.fetchPlayHistory() 21 | .subscribe { [weak self] in 22 | self?.playHistory.accept($0) 23 | } onFailure: { error in 24 | print(error.localizedDescription) 25 | } 26 | .disposed(by: disposeBag) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/Views/BlurCircleButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlurCircleButton.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/08. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | class BlurCircleButton: UIView { 13 | let iconBackground: UIView = { 14 | let view = UIView() 15 | view.layer.masksToBounds = true 16 | view.makeBlurBackground() 17 | return view 18 | }() 19 | 20 | typealias Listener = () -> Void 21 | var buttonClickListener: Listener? 22 | 23 | override func layoutSubviews() { 24 | super.layoutSubviews() 25 | iconBackground.layer.cornerRadius = bounds.width / 2 26 | } 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | configure() 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | super.init(coder: coder) 35 | configure() 36 | } 37 | 38 | private func configure() { 39 | configureIconBackground() 40 | let gesture = UITapGestureRecognizer(target: self, action: #selector(buttonClicked(_:))) 41 | gesture.numberOfTapsRequired = 1 42 | addGestureRecognizer(gesture) 43 | } 44 | 45 | private func configureIconBackground() { 46 | addSubview(iconBackground) 47 | iconBackground.snp.makeConstraints { 48 | $0.top.leading.trailing.equalToSuperview() 49 | $0.height.equalTo(iconBackground.snp.width) 50 | } 51 | } 52 | 53 | @objc private func buttonClicked(_ sender: UIGestureRecognizer) { 54 | buttonClickListener?() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/Views/FavoriteMusicViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteViewController.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/08. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxCocoa 11 | import RxSwift 12 | 13 | final class FavoriteViewController: UIViewController { 14 | private lazy var favoriteCollectionView: UICollectionView = { 15 | let layout = UICollectionViewFlowLayout() 16 | layout.scrollDirection = .vertical 17 | let cellWidth = (UIScreen.deviceScreenSize.width - 32) / 2 - 6 18 | layout.itemSize = CGSize(width: cellWidth, height: cellWidth * MediaCell.ratio) 19 | layout.sectionInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) 20 | layout.minimumInteritemSpacing = 8 21 | layout.minimumLineSpacing = 8 22 | 23 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 24 | collectionView.backgroundColor = .clear 25 | collectionView.showsHorizontalScrollIndicator = false 26 | collectionView.register(MediaCollectionViewCell.self) 27 | return collectionView 28 | }() 29 | 30 | private let viewModel = FavoriteViewModel() 31 | private let disposeBag = DisposeBag() 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | configureUI() 37 | bindUI() 38 | 39 | viewModel.viewDidLoad() 40 | } 41 | 42 | private func configureUI() { 43 | view.backgroundColor = .white 44 | navigationItem.title = "좋아요 누른 음원" 45 | navigationItem.largeTitleDisplayMode = .always 46 | navigationController?.navigationBar.prefersLargeTitles = true 47 | navigationController?.navigationBar.largeTitleTextAttributes = [ 48 | NSAttributedString.Key.foregroundColor: UIColor.black 49 | ] 50 | 51 | view.addSubview(favoriteCollectionView) 52 | favoriteCollectionView.snp.makeConstraints { 53 | $0.top.equalTo(view.snp.topMargin) 54 | $0.leading.trailing.bottom.equalToSuperview() 55 | } 56 | } 57 | 58 | private func bindUI() { 59 | bindUIwithView() 60 | bindUIWithViewModel() 61 | } 62 | 63 | private func bindUIwithView() { 64 | favoriteCollectionView.rx.modelSelected(Media.self) 65 | .subscribe(onNext: { [weak self] media in 66 | let musicPlayerView = MediaPlayerViewController( 67 | viewModel: MediaPlayerViewModel( 68 | media: media 69 | ) 70 | ) 71 | self?.navigationController?.pushViewController(musicPlayerView, animated: true) 72 | }) 73 | .disposed(by: disposeBag) 74 | } 75 | 76 | private func bindUIWithViewModel() { 77 | viewModel.favoriteSoundList 78 | .bind( 79 | to: favoriteCollectionView.rx.items( 80 | cellIdentifier: MediaCollectionViewCell.identifier 81 | ) 82 | ) { (_, element, cell) in 83 | guard let cell = cell as? MediaCollectionViewCell else { return } 84 | 85 | cell.titleView.text = element.name 86 | cell.imageView.image = UIImage(named: element.thumbnailImageFileName) 87 | cell.backgroundColor = UIColor( 88 | rgb: Int(element.color, radix: 16) ?? 0xFFFFFF, 89 | alpha: 1.0 90 | ) 91 | }.disposed(by: disposeBag) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/Views/FocusButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusButton.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/08. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class FocusButton: BlurCircleButton { 13 | private lazy var icon: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.contentMode = .scaleAspectFit 16 | imageView.tintColor = .white 17 | return imageView 18 | }() 19 | private lazy var titleLabel: UILabel = { 20 | let label = UILabel() 21 | label.textColor = .white 22 | label.textAlignment = .center 23 | label.adjustsFontSizeToFitWidth = true 24 | return label 25 | }() 26 | 27 | private(set) var mode: FocusMode? 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | configure() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | configure() 37 | } 38 | 39 | func set(mode: FocusMode) { 40 | self.mode = mode 41 | let modeValue = FocusMode.getValue(from: mode) 42 | icon.image = UIImage(named: modeValue.image)?.withRenderingMode(.alwaysTemplate) 43 | titleLabel.text = modeValue.title 44 | } 45 | 46 | private func configure() { 47 | configureIcon() 48 | configureTitle() 49 | } 50 | 51 | private func configureIcon() { 52 | iconBackground.addSubview(icon) 53 | icon.snp.makeConstraints { 54 | $0.center.equalToSuperview() 55 | $0.width.height.equalToSuperview().multipliedBy(0.5) 56 | } 57 | } 58 | 59 | private func configureTitle() { 60 | addSubview(titleLabel) 61 | titleLabel.snp.makeConstraints { 62 | $0.top.equalTo(iconBackground.snp.bottom).offset(12) 63 | $0.leading.trailing.equalToSuperview() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/Views/HomeListHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeListHeaderView.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/25. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class HomeListHeaderView: UIView { 13 | private(set) lazy var titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.textColor = .white 16 | label.font = .preferredFont(forTextStyle: .title3) 17 | return label 18 | }() 19 | private(set) lazy var allButton: UIButton = { 20 | let button = UIButton() 21 | button.setTitle("더보기", for: .normal) 22 | button.setTitleColor(.white.withAlphaComponent(0.6), for: .normal) 23 | button.titleLabel?.font = .preferredFont(forTextStyle: .callout) 24 | return button 25 | }() 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | 30 | configure() 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | super.init(coder: coder) 35 | 36 | configure() 37 | } 38 | 39 | private func configure() { 40 | configureTitleLabel() 41 | configureAllButton() 42 | } 43 | 44 | private func configureTitleLabel() { 45 | addSubview(titleLabel) 46 | titleLabel.snp.makeConstraints { 47 | $0.top.bottom.equalToSuperview() 48 | $0.leading.equalToSuperview().offset(16) 49 | } 50 | } 51 | 52 | private func configureAllButton() { 53 | addSubview(allButton) 54 | allButton.snp.makeConstraints { 55 | $0.top.bottom.equalToSuperview() 56 | $0.trailing.equalToSuperview().offset(-16) 57 | $0.width.equalTo(50) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/Views/MediaPlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaPlayView.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/18. 6 | // 7 | 8 | import AVKit 9 | import UIKit 10 | 11 | import RxRelay 12 | import RxSwift 13 | 14 | final class MediaPlayView: UIView { 15 | private lazy var playButton: UIButton = { 16 | let button = UIButton() 17 | let configuration = UIImage.SymbolConfiguration(font: .systemFont(ofSize: 45)) 18 | button.setImage(UIImage(systemName: "play.fill", withConfiguration: configuration), for: .normal) 19 | button.tintColor = .white.withAlphaComponent(0.5) 20 | button.isUserInteractionEnabled = false 21 | return button 22 | }() 23 | private lazy var thumbnailImageView: UIImageView = { 24 | let imageView = UIImageView() 25 | imageView.contentMode = .scaleAspectFill 26 | imageView.backgroundColor = .clear 27 | imageView.layer.masksToBounds = true 28 | return imageView 29 | }() 30 | private var videoPlayer = AVQueuePlayer() 31 | private var playerLooper: AVPlayerLooper? 32 | 33 | private let viewModel = MediaPlayViewModel() 34 | private let disposeBag = DisposeBag() 35 | 36 | let media = BehaviorRelay(value: nil) 37 | 38 | required init?(coder: NSCoder) { 39 | super.init(coder: coder) 40 | } 41 | 42 | init() { 43 | super.init(frame: .zero) 44 | 45 | configureUI() 46 | bindUI() 47 | } 48 | 49 | init(media: Media) { 50 | super.init(frame: .zero) 51 | 52 | configureUI() 53 | bindUI() 54 | 55 | self.media.accept(media) 56 | } 57 | 58 | func replaceMedia(media: Media) { 59 | self.media.accept(media) 60 | } 61 | 62 | func playVideo() { 63 | playButton.isHidden = true 64 | guard videoPlayer.currentItem != nil else { return } 65 | videoPlayer.currentItem?.seek(to: .zero, completionHandler: nil) 66 | videoPlayer.play() 67 | } 68 | 69 | func pauseVideo() { 70 | playButton.isHidden = false 71 | guard videoPlayer.currentItem != nil else { return } 72 | videoPlayer.pause() 73 | } 74 | 75 | private func configureUI() { 76 | addSubview(thumbnailImageView) 77 | thumbnailImageView.snp.makeConstraints { 78 | $0.edges.equalToSuperview() 79 | } 80 | 81 | thumbnailImageView.addSubview(playButton) 82 | playButton.snp.makeConstraints { 83 | $0.top.equalToSuperview().offset(UIScreen.deviceScreenSize.height * 0.4) 84 | $0.centerX.equalToSuperview() 85 | $0.width.height.equalTo(60) 86 | } 87 | } 88 | 89 | private func bindUI() { 90 | media 91 | .distinctUntilChanged() 92 | .compactMap { $0 } 93 | .bind(onNext: { [weak self] media in 94 | self?.pauseVideo() 95 | self?.setMedia(media: media) 96 | }) 97 | .disposed(by: disposeBag) 98 | } 99 | 100 | private func setMedia(media: Media) { 101 | guard media.mode == ApplicationMode.shared.mode.value.rawValue else { return } 102 | 103 | switch ApplicationMode.shared.mode.value { 104 | case .bright: 105 | viewModel.didSetMedia(fileName: media.videoFileName, type: .video) 106 | .subscribe { [weak self] url in 107 | guard let self = self else { return } 108 | 109 | let playerItem = AVPlayerItem(url: url) 110 | self.videoPlayer.replaceCurrentItem(with: playerItem) 111 | self.playerLooper = AVPlayerLooper(player: self.videoPlayer, templateItem: playerItem) 112 | 113 | let avPlayerLayer = AVPlayerLayer(player: self.videoPlayer) 114 | avPlayerLayer.videoGravity = .resizeAspectFill 115 | avPlayerLayer.frame = UIScreen.main.bounds 116 | 117 | self.thumbnailImageView.image = nil 118 | self.layer.sublayers = [avPlayerLayer, self.thumbnailImageView.layer] 119 | } onFailure: { error in 120 | print(error.localizedDescription) 121 | } 122 | .disposed(by: disposeBag) 123 | case .dark: 124 | viewModel.didSetMedia(fileName: media.thumbnailImageFileName, type: .image) 125 | .subscribe { [weak self] url in 126 | guard let self = self else { return } 127 | 128 | self.videoPlayer.replaceCurrentItem(with: nil) 129 | self.layer.sublayers = [self.thumbnailImageView.layer] 130 | if let imageData = try? Data(contentsOf: url) { 131 | self.thumbnailImageView.image = UIImage(data: imageData) 132 | } 133 | self.layer.sublayers = [self.thumbnailImageView.layer] 134 | } onFailure: { error in 135 | print(error.localizedDescription) 136 | } 137 | .disposed(by: disposeBag) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/Views/PlayHistoryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayHistoryViewController.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/08. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxCocoa 11 | import RxSwift 12 | 13 | final class PlayHistoryViewController: UIViewController { 14 | private lazy var playHistoryCollectionView: UICollectionView = { 15 | let layout = UICollectionViewFlowLayout() 16 | layout.scrollDirection = .vertical 17 | let cellWidth = (UIScreen.deviceScreenSize.width - 32) / 2 - 6 18 | layout.itemSize = CGSize(width: cellWidth, height: cellWidth * MediaCell.ratio) 19 | layout.sectionInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) 20 | layout.minimumInteritemSpacing = 8 21 | layout.minimumLineSpacing = 8 22 | 23 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 24 | collectionView.backgroundColor = .clear 25 | collectionView.showsHorizontalScrollIndicator = false 26 | collectionView.register(MediaCollectionViewCell.self) 27 | return collectionView 28 | }() 29 | 30 | private let viewModel = PlayHistoryViewModel() 31 | private let disposeBag = DisposeBag() 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | configureUI() 37 | bindUI() 38 | 39 | viewModel.viewDidLoad() 40 | } 41 | 42 | private func configureUI() { 43 | view.backgroundColor = .white 44 | navigationItem.title = "재생 기록" 45 | navigationItem.largeTitleDisplayMode = .always 46 | navigationController?.navigationBar.prefersLargeTitles = true 47 | navigationController?.navigationBar.largeTitleTextAttributes = [ 48 | NSAttributedString.Key.foregroundColor: UIColor.black 49 | ] 50 | 51 | view.addSubview(playHistoryCollectionView) 52 | playHistoryCollectionView.snp.makeConstraints { 53 | $0.top.equalTo(view.snp.topMargin) 54 | $0.leading.trailing.bottom.equalToSuperview() 55 | } 56 | } 57 | 58 | private func bindUI() { 59 | bindUIwithView() 60 | bindUIWithViewModel() 61 | } 62 | 63 | private func bindUIwithView() { 64 | playHistoryCollectionView.rx.modelSelected(Media.self) 65 | .subscribe(onNext: { [weak self] media in 66 | let musicPlayerView = MediaPlayerViewController( 67 | viewModel: MediaPlayerViewModel( 68 | media: media 69 | ) 70 | ) 71 | self?.navigationController?.pushViewController(musicPlayerView, animated: true) 72 | }) 73 | .disposed(by: disposeBag) 74 | } 75 | 76 | private func bindUIWithViewModel() { 77 | viewModel.playHistory 78 | .bind( 79 | to: playHistoryCollectionView.rx.items( 80 | cellIdentifier: MediaCollectionViewCell.identifier 81 | ) 82 | ) { (_, element, cell) in 83 | guard let cell = cell as? MediaCollectionViewCell else { return } 84 | 85 | cell.titleView.text = element.name 86 | cell.imageView.image = UIImage(named: element.thumbnailImageFileName) 87 | cell.backgroundColor = UIColor( 88 | rgb: Int(element.color, radix: 16) ?? 0xFFFFFF, 89 | alpha: 1.0 90 | ) 91 | }.disposed(by: disposeBag) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Main/Views/TouchTransferView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TouchTransferView.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | final class TouchTransferView: UIView { 11 | var transferView: UIView? 12 | 13 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 14 | super.touchesBegan(touches, with: event) 15 | transferView?.touchesBegan(touches, with: event) 16 | } 17 | 18 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 19 | super.touchesMoved(touches, with: event) 20 | transferView?.touchesMoved(touches, with: event) 21 | } 22 | 23 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 24 | super.touchesEnded(touches, with: event) 25 | transferView?.touchesEnded(touches, with: event) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Maxim/Maxim+Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Maxim+Enums.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | enum MaximCollectionViewSize { 11 | static let nocheHeight = UIApplication.statusBarHeight 12 | static let headerHeight = CGFloat(50) 13 | static let cellSize = UIScreen.main.bounds.size 14 | static let lineSpacing = CGFloat(2) 15 | static let pageWidth = (cellSize.width + lineSpacing) 16 | } 17 | 18 | enum MaximCalendarCollectionViewSize { 19 | static let contentWidth = UIScreen.deviceScreenSize.width 20 | static let size = CGRect( 21 | origin: .zero, 22 | size: CGSize( 23 | width: Self.contentWidth / 14, 24 | height: Self.contentWidth / 14 25 | + MaximCollectionViewSize.nocheHeight 26 | + MaximCollectionViewSize.headerHeight * 2 27 | ) 28 | ) 29 | static let cellWidth = Self.contentWidth / 14 30 | static let lineSpacing = cellWidth 31 | static let pageWidth = (cellWidth + lineSpacing) * 7 32 | } 33 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Maxim/ViewModels/MaximPresenterObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximPresenterObject.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MaximPresenterObject { 11 | let day: String 12 | let weekDay: String 13 | let monthYear: String 14 | let content: String 15 | let speaker: String 16 | let thumbnailImageAssetPath: String 17 | 18 | init( 19 | day: String, 20 | weekDay: String, 21 | monthYear: String, 22 | content: String, 23 | speaker: String, 24 | thumbnailImageAssetPath: String 25 | ) { 26 | self.day = day 27 | self.weekDay = weekDay 28 | self.monthYear = monthYear 29 | self.content = content 30 | self.speaker = speaker 31 | self.thumbnailImageAssetPath = thumbnailImageAssetPath 32 | } 33 | 34 | init(maxim: Maxim) { 35 | let monthList = [ 36 | "JAN", "FEB", "MAR", 37 | "APR", "MAY", "JUN", 38 | "JUL", "AUG", "SEP", 39 | "OCT", "NOV", "DEC" 40 | ] 41 | let date = maxim.date 42 | let day = "\(date.day)" 43 | let weekDay = "\(date.weekdayEng)" 44 | let monthYear = "\(monthList[date.month - 1]) \(date.year)" 45 | let content = maxim.content 46 | let speaker = maxim.speaker.isEmpty ? "미상" : maxim.speaker 47 | let thumbnailImageAssetPath = maxim.thumbnailImageFileName 48 | self.init( 49 | day: day, 50 | weekDay: weekDay, 51 | monthYear: monthYear, 52 | content: content, 53 | speaker: speaker, 54 | thumbnailImageAssetPath: thumbnailImageAssetPath 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Maxim/ViewModels/MaximViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximViewModel.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class MaximViewModel { 14 | let maximList = BehaviorRelay<[MaximPresenterObject]>(value: []) 15 | let isHeaderPresent = BehaviorRelay(value: false) 16 | let imageURLs = BehaviorRelay<[String]>(value: []) 17 | let selectedDate = BehaviorRelay(value: 0) 18 | let jumpedDate = PublishRelay() 19 | let jumpedWeek = PublishRelay() 20 | 21 | private let disposeBag = DisposeBag() 22 | private let maximListUseCase = MaximListUseCase( 23 | maximListRepository: MaximListRepository() 24 | ) 25 | 26 | func fetchMaximList() { 27 | maximListUseCase.fetchWeeksMaximList() 28 | .map { 29 | $0.map { 30 | MaximPresenterObject(maxim: $0) 31 | } 32 | } 33 | .subscribe { [weak self] in 34 | self?.maximList.accept($0) 35 | } 36 | .disposed(by: disposeBag) 37 | } 38 | 39 | func presentHeader() { 40 | isHeaderPresent.accept(true) 41 | } 42 | 43 | func dismissHeader() { 44 | isHeaderPresent.accept(false) 45 | } 46 | 47 | func scrollDate(to index: Int) { 48 | let index = compressIndex(index) 49 | jumpedWeek.accept(index / 7) 50 | selectedDate.accept(index) 51 | } 52 | 53 | func scrollWeek(to index: Int) { 54 | let index = (selectedDate.value % 7 + index * 7) 55 | jumpDate(to: index) 56 | } 57 | 58 | func jumpDate(to index: Int) { 59 | let index = compressIndex(index) 60 | jumpedDate.accept(index) 61 | selectedDate.accept(index) 62 | } 63 | 64 | // 참고: https://stackoverflow.com/questions/31656642/lesser-than-or-greater-than-in-swift-switch-statement 65 | private func compressIndex(_ index: Int) -> Int { 66 | switch index { 67 | case ..<0: 68 | return 0 69 | case maximList.value.count...: 70 | return maximList.value.count - 1 71 | default: 72 | return index 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Maxim/Views/MaximCalendarHeaderCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximHeaderCollectionViewCell.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MaximCalendarHeaderCollectionViewCell: UICollectionViewCell { 11 | private(set) lazy var weekdayLabel: UILabel = { 12 | let label = UILabel() 13 | label.font = .preferredFont(forTextStyle: .title3) 14 | label.textColor = .gray 15 | label.text = " " 16 | return label 17 | }() 18 | private(set) lazy var dayLabel: UILabel = { 19 | let label = UILabel() 20 | label.text = "15" 21 | label.font = .preferredFont(forTextStyle: .headline) 22 | label.textColor = .white 23 | return label 24 | }() 25 | private(set) lazy var indicatorPointView: UIView = { 26 | let view = UIView() 27 | view.backgroundColor = .red 28 | view.isHidden = true 29 | return view 30 | }() 31 | private(set) lazy var dayButton: UIButton = { 32 | let button = UIButton() 33 | button.isUserInteractionEnabled = false 34 | button.makeCircle() 35 | button.backgroundColor = .black 36 | button.alpha = 0.8 37 | return button 38 | }() 39 | 40 | override init(frame: CGRect) { 41 | super.init(frame: frame) 42 | configureUI() 43 | } 44 | 45 | required init?(coder: NSCoder) { 46 | super.init(coder: coder) 47 | configureUI() 48 | } 49 | 50 | override func prepareForReuse() { 51 | indicatorPointView.isHidden = true 52 | } 53 | 54 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 55 | if dayButton.frame.contains(point) { 56 | return super.hitTest(point, with: event) 57 | } 58 | return nil 59 | } 60 | 61 | override func layoutSubviews() { 62 | super.layoutSubviews() 63 | indicatorPointView.makeCircle() 64 | dayButton.makeCircle() 65 | } 66 | 67 | private func configureUI() { 68 | addSubview(weekdayLabel) 69 | weekdayLabel.snp.makeConstraints { 70 | $0.centerX.equalToSuperview() 71 | $0.top.equalToSuperview() 72 | } 73 | 74 | addSubview(dayButton) 75 | dayButton.snp.makeConstraints { 76 | $0.width.equalToSuperview() 77 | $0.height.equalTo(dayButton.snp.width) 78 | $0.leading.equalToSuperview() 79 | $0.top.equalTo(weekdayLabel.snp.bottom).offset(8) 80 | } 81 | 82 | addSubview(indicatorPointView) 83 | indicatorPointView.snp.makeConstraints { 84 | $0.width.equalTo(5) 85 | $0.height.equalTo(indicatorPointView.snp.width) 86 | $0.centerX.equalToSuperview() 87 | $0.top.equalTo(dayButton.snp.bottom).offset(10) 88 | } 89 | 90 | addSubview(dayLabel) 91 | dayLabel.snp.makeConstraints { 92 | $0.center.equalTo(dayButton) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Maxim/Views/MaximCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximCollectionViewCell.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MaximCollectionViewCell: UICollectionViewCell { 11 | private lazy var seperateLine: UIView = { 12 | let view = UIView() 13 | view.backgroundColor = .systemGray3 14 | return view 15 | }() 16 | 17 | private(set) lazy var closeButton: UIButton = { 18 | let button = CloseButton() 19 | button.tintColor = .white 20 | return button 21 | }() 22 | private(set) lazy var calendarButton: UIButton = { 23 | let button = UIButton() 24 | button.setBackgroundImage(UIImage(systemName: "calendar"), for: .normal) 25 | button.tintColor = .white 26 | button.imageView?.frame = .init(x: 0, y: 0, width: 100, height: 100) 27 | return button 28 | }() 29 | private(set) lazy var dayLabel: UILabel = { 30 | let label = UILabel() 31 | label.font = .systemFont(ofSize: 70) 32 | label.textColor = .white 33 | label.text = "15" 34 | return label 35 | }() 36 | private(set) lazy var monthYearLabel: UILabel = { 37 | let label = UILabel() 38 | label.font = .preferredFont(forTextStyle: .title3) 39 | label.textColor = .white 40 | label.text = "NOV 2021" 41 | return label 42 | }() 43 | private(set) lazy var contentLabel: UILabel = { 44 | let label = UILabel() 45 | label.font = .preferredFont(forTextStyle: .title1) 46 | label.textColor = .white 47 | label.text = "In fact, in order to understand the real Chinaman, and the Chinese civilisation, a man must be depp, broad and simple." 48 | label.numberOfLines = 0 49 | label.setLineSpacing(lineSpacing: 10) 50 | return label 51 | }() 52 | private(set) lazy var speakerLabel: UILabel = { 53 | let label = UILabel() 54 | var font = UIFont.preferredFont(forTextStyle: .subheadline) 55 | if let fontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) { 56 | font = UIFont(descriptor: fontDescriptor, size: 0) 57 | } 58 | label.font = font 59 | label.textColor = .systemGray3 60 | label.text = "Schloar, Gu Hongming" 61 | return label 62 | }() 63 | 64 | var isShown = false { 65 | willSet { 66 | if newValue { 67 | UIView.animate(withDuration: 2) { 68 | self.contentLabel.alpha = 1 69 | } 70 | } else { 71 | self.contentLabel.alpha = 0 72 | } 73 | } 74 | } 75 | 76 | override init(frame: CGRect) { 77 | super.init(frame: frame) 78 | configureUI() 79 | } 80 | 81 | required init?(coder: NSCoder) { 82 | super.init(coder: coder) 83 | configureUI() 84 | } 85 | 86 | private func configureUI() { 87 | backgroundColor = .black 88 | 89 | addSubview(speakerLabel) 90 | speakerLabel.snp.makeConstraints { 91 | $0.leading.equalToSuperview().offset(30) 92 | $0.bottom.equalTo(snp.bottomMargin).offset(-20) 93 | } 94 | 95 | addSubview(seperateLine) 96 | seperateLine.snp.makeConstraints { 97 | $0.leading.equalToSuperview().offset(30) 98 | $0.bottom.equalTo(speakerLabel.snp.top).offset(-20) 99 | $0.width.equalTo(30) 100 | $0.height.equalTo(5) 101 | } 102 | 103 | addSubview(contentLabel) 104 | contentLabel.snp.makeConstraints { 105 | $0.leading.equalToSuperview().offset(30) 106 | $0.bottom.equalTo(seperateLine.snp.top).offset(-30) 107 | $0.trailing.equalToSuperview().offset(-30) 108 | } 109 | 110 | addSubview(monthYearLabel) 111 | monthYearLabel.snp.makeConstraints { 112 | $0.leading.equalToSuperview().offset(30) 113 | $0.bottom.equalTo(contentLabel.snp.top).offset(-30) 114 | } 115 | 116 | addSubview(dayLabel) 117 | dayLabel.snp.makeConstraints { 118 | $0.leading.equalToSuperview().offset(30) 119 | $0.bottom.equalTo(monthYearLabel.snp.top) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Home/Maxim/Views/MaximHeaderCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximHeaderCollectionViewCell.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | class MaximHeaderCollectionViewCell: UICollectionViewCell { 11 | private(set) lazy var weekLabel: UILabel = { 12 | let weekLabel = UILabel() 13 | weekLabel.font = .systemFont(ofSize: 70) 14 | weekLabel.textColor = .black 15 | weekLabel.text = "15" 16 | return weekLabel 17 | }() 18 | 19 | private(set) lazy var dateButton: UIButton = { 20 | let dateButton = UIButton() 21 | dateButton.setBackgroundImage(UIImage(systemName: "calendar"), for: .normal) 22 | dateButton.setTitle("17", for: .normal) 23 | dateButton.setTitleColor(.black, for: .normal) 24 | return dateButton 25 | }() 26 | 27 | private(set) lazy var indicatorPointView: UIView = { 28 | let indicatorPointView = UIView() 29 | indicatorPointView.backgroundColor = .red 30 | return indicatorPointView 31 | }() 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | configureUI() 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | super.init(coder: coder) 40 | configureUI() 41 | } 42 | 43 | private func configureUI() { 44 | addSubview(weekLabel) 45 | weekLabel.snp.makeConstraints { 46 | $0.width.equalToSuperview() 47 | $0.leading.equalToSuperview() 48 | $0.top.equalToSuperview() 49 | } 50 | 51 | addSubview(dateButton) 52 | dateButton.snp.makeConstraints { 53 | $0.width.equalToSuperview() 54 | $0.height.equalTo(dateButton.snp.width) 55 | $0.leading.equalToSuperview() 56 | $0.top.equalTo(weekLabel.snp.bottom) 57 | } 58 | 59 | addSubview(indicatorPointView) 60 | indicatorPointView.snp.makeConstraints { 61 | $0.width.equalToSuperview() 62 | $0.height.equalTo(dateButton.snp.width) 63 | $0.leading.equalToSuperview() 64 | $0.top.equalTo(dateButton.snp.bottom) 65 | } 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Me/Main/ViewModels/MeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeViewModel.swift 3 | // JipJung 4 | // 5 | // Created by Soohyeon Lee on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | import RxRelay 12 | 13 | final class MeViewModel { 14 | let grassPresenterObject = BehaviorRelay(value: nil) 15 | let monthIndex = BehaviorRelay<[(index: Int, month: String)]>(value: []) 16 | 17 | private let disposeBag = DisposeBag() 18 | private let loadFocusTimeUseCase = LoadFocusTimeUseCase( 19 | focusTimeRepository: FocusTimeRepository() 20 | ) 21 | 22 | func fetchFocusTimeLists() { 23 | let nDay = MeGrassMap.dayCount - 7 + Date().weekday 24 | let historyObservable = loadFocusTimeUseCase.loadHistory(from: Date(), nDays: nDay) 25 | historyObservable.bind { [weak self] in 26 | let grassPresenterObject = GrassPresenterObject(dailyFocusTimes: $0, nDay: nDay) 27 | self?.grassPresenterObject.accept(grassPresenterObject) 28 | } 29 | .disposed(by: disposeBag) 30 | 31 | historyObservable 32 | .flatMap { 33 | return Observable.from($0.enumerated()) 34 | } 35 | .filter { 36 | $0.offset % 7 == 0 37 | } 38 | .distinctUntilChanged { 39 | $0.element.date.month 40 | } 41 | .map { 42 | (index: $0.offset / 7, month: "\($0.element.date.month)월") 43 | } 44 | .toArray() 45 | .asObservable() 46 | .bind(to: monthIndex) 47 | .disposed(by: disposeBag) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Me/Main/Views/GrassMapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeGrassView.swift 3 | // JipJung 4 | // 5 | // Created by 윤상진 on 2021/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class GrassMapView: UIView { 11 | private lazy var weeksStackView = UIStackView() 12 | 13 | private var monthLabelLists = [UILabel]() 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | configureUI() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | configureUI() 23 | } 24 | 25 | private func configureUI() { 26 | weeksStackView.distribution = .fillEqually 27 | weeksStackView.axis = .horizontal 28 | addSubview(weeksStackView) 29 | weeksStackView.snp.makeConstraints { 30 | $0.bottom.equalToSuperview() 31 | $0.leading.equalToSuperview() 32 | $0.trailing.equalToSuperview() 33 | $0.height.equalTo(MeGrassMapViewSize.height) 34 | } 35 | 36 | for _ in 1...MeGrassMap.weekCount { 37 | let weekStackView = UIStackView() 38 | weekStackView.axis = .vertical 39 | for _ in 1...7 { 40 | let dayView = UIView( 41 | frame: CGRect( 42 | origin: .zero, 43 | size: CGSize( 44 | width: MeGrassMapViewSize.cellLength, 45 | height: MeGrassMapViewSize.cellLength 46 | ) 47 | ) 48 | ) 49 | dayView.backgroundColor = .systemGray3 50 | dayView.layer.cornerRadius = 5 51 | weekStackView.addArrangedSubview(dayView) 52 | weekStackView.distribution = .fillEqually 53 | weekStackView.spacing = MeGrassMapViewSize.cellSpacing 54 | } 55 | weeksStackView.addArrangedSubview(weekStackView) 56 | weeksStackView.distribution = .fillEqually 57 | weeksStackView.spacing = MeGrassMapViewSize.cellSpacing 58 | 59 | let monthLabel = makeMonthLabel() 60 | monthLabelLists.append(monthLabel) 61 | addSubview(monthLabel) 62 | monthLabel.snp.makeConstraints { 63 | $0.bottom.equalTo(weekStackView.snp.top) 64 | $0.centerX.equalTo(weekStackView) 65 | } 66 | } 67 | 68 | } 69 | 70 | private func makeMonthLabel() -> UILabel { 71 | let monthLabel = UILabel() 72 | monthLabel.text = "" 73 | monthLabel.textColor = MeGrassMap.tintColor 74 | monthLabel.textAlignment = .center 75 | monthLabel.font = .systemFont(ofSize: 16) 76 | return monthLabel 77 | } 78 | 79 | func setMonthLabel(index: Int, month: String) { 80 | guard (0.. UIView? { 89 | guard (0..<7) ~= index.day, 90 | (0.. FocusStage { 35 | 36 | return FocusStage(rawValue: Int(ceil(Float(second) / 3600.0))) ?? FocusStage.five 37 | } 38 | 39 | var greenColor: UIColor { 40 | switch self { 41 | case .zero: 42 | return MeGrassMap.tintColor 43 | case .one: 44 | return UIColor(rgb: 0xd2f2d4) 45 | case .two: 46 | return UIColor(rgb: 0x7be382) 47 | case .three: 48 | return UIColor(rgb: 0x26cc00) 49 | case .four: 50 | return UIColor(rgb: 0x22b600) 51 | case .five: 52 | return UIColor(rgb: 0x009c1a) 53 | } 54 | } 55 | 56 | var description: String { 57 | switch self { 58 | case .zero: 59 | return "0H" 60 | case .one: 61 | return "~2" 62 | case .two: 63 | return "~4" 64 | case .three: 65 | return "~6" 66 | case .four: 67 | return "~8" 68 | case .five: 69 | return "8+" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Sound/Views/MediaDescriptionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaDescriptionView.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class MediaDescriptionView: UIView { 13 | var titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.font = .preferredFont(forTextStyle: .title1) 16 | label.textColor = .white 17 | return label 18 | }() 19 | var explanationLabel: UILabel = { 20 | let label = UILabel() 21 | label.textColor = .white.withAlphaComponent(0.8) 22 | return label 23 | }() 24 | var plusButton: PlusCircleButton = { 25 | let button = PlusCircleButton() 26 | button.contentHorizontalAlignment = .fill 27 | button.contentVerticalAlignment = .fill 28 | button.imageEdgeInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6) 29 | return button 30 | }() 31 | let tagView = UIView() 32 | let gradientLayer = CAGradientLayer() 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | configure() 37 | } 38 | 39 | required init?(coder: NSCoder) { 40 | super.init(coder: coder) 41 | configure() 42 | } 43 | 44 | override func layoutSubviews() { 45 | super.layoutSubviews() 46 | gradientLayer.frame = bounds 47 | } 48 | 49 | private func configure() { 50 | let colors: [CGColor] = [ 51 | .init(gray: 0.0, alpha: 0.5), 52 | .init(gray: 0, alpha: 0.0) 53 | ] 54 | gradientLayer.colors = colors 55 | gradientLayer.startPoint = CGPoint(x: 0.5, y: 1.0) 56 | gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.0) 57 | layer.addSublayer(gradientLayer) 58 | 59 | let contentView = UIView() 60 | addSubview(contentView) 61 | contentView.snp.makeConstraints { 62 | $0.top.bottom.equalToSuperview() 63 | $0.leading.trailing.equalToSuperview().inset(12) 64 | } 65 | 66 | contentView.addSubview(titleLabel) 67 | contentView.addSubview(explanationLabel) 68 | contentView.addSubview(plusButton) 69 | contentView.addSubview(tagView) 70 | 71 | plusButton.isEnabled = false 72 | plusButton.snp.makeConstraints { 73 | $0.top.trailing.equalToSuperview() 74 | $0.size.equalTo(titleLabel.snp.height) 75 | } 76 | 77 | titleLabel.snp.makeConstraints { 78 | $0.top.leading.equalToSuperview() 79 | $0.trailing.equalTo(plusButton.snp.leading) 80 | $0.centerY.equalTo(plusButton) 81 | } 82 | 83 | explanationLabel.snp.makeConstraints { 84 | $0.top.equalTo(titleLabel.snp.bottom).offset(8) 85 | $0.leading.trailing.equalToSuperview() 86 | } 87 | 88 | tagView.snp.makeConstraints { 89 | $0.top.equalTo(explanationLabel.snp.bottom).offset(8) 90 | $0.leading.trailing.bottom.equalToSuperview() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Sound/Views/MediaPlayerMaximView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaPlayerMaximView.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class MediaPlayerMaximView: UIView { 13 | let maximLabel: UILabel = { 14 | let label = UILabel() 15 | let text = """ 16 | At night, 17 | The moon draws faint tides from ear to ear. 18 | """ 19 | label.font = .systemFont(ofSize: 14, weight: .semibold) 20 | label.numberOfLines = 0 21 | label.textColor = .white 22 | label.text = text 23 | label.setLineSpacing(lineSpacing: 24) 24 | 25 | label.lineBreakMode = .byWordWrapping 26 | if #available(iOS 14.0, *) { 27 | label.lineBreakStrategy = .hangulWordPriority 28 | } 29 | return label 30 | }() 31 | 32 | let speakerNameLabel: UILabel = { 33 | let label = UILabel() 34 | let words = "- J.M. Coetzee" 35 | label.text = words 36 | label.font = .systemFont(ofSize: 18) 37 | label.textColor = UIColor(white: 1, alpha: 0.35) 38 | return label 39 | }() 40 | 41 | override init(frame: CGRect) { 42 | super.init(frame: frame) 43 | configure() 44 | } 45 | 46 | required init?(coder: NSCoder) { 47 | super.init(coder: coder) 48 | configure() 49 | } 50 | 51 | private func configure() { 52 | addSubview(maximLabel) 53 | addSubview(speakerNameLabel) 54 | 55 | maximLabel.snp.makeConstraints { 56 | $0.leading.trailing.equalToSuperview() 57 | $0.center.equalToSuperview() 58 | } 59 | 60 | speakerNameLabel.snp.makeConstraints { 61 | $0.trailing.equalToSuperview() 62 | $0.top.equalTo(maximLabel.snp.bottom).offset(32) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Sound/Views/MusicPlayerButtons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MediaPlayerButtons.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/11. 6 | // 7 | 8 | import UIKit 9 | 10 | class MediaPlayerBaseButton: UIButton { 11 | override func layoutSubviews() { 12 | super.layoutSubviews() 13 | self.makeCircle() 14 | } 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | configure() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | super.init(coder: aDecoder) 23 | configure() 24 | } 25 | 26 | func configure() { 27 | self.backgroundColor = UIColor(white: 1, alpha: 0.35) 28 | } 29 | } 30 | 31 | final class BackCircleButton: MediaPlayerBaseButton { 32 | override func configure() { 33 | super.configure() 34 | setImage(UIImage(systemName: "chevron.backward"), for: .normal) 35 | tintColor = .white 36 | } 37 | } 38 | 39 | final class FavoriteCircleButton: MediaPlayerBaseButton { 40 | override func configure() { 41 | super.configure() 42 | setImage(UIImage(systemName: "heart"), for: .normal) 43 | setImage(UIImage(systemName: "heart.fill"), for: .selected) 44 | tintColor = .red 45 | } 46 | } 47 | 48 | final class PlusCircleButton: MediaPlayerBaseButton { 49 | override func configure() { 50 | super.configure() 51 | self.backgroundColor = .clear 52 | setImage(UIImage(systemName: "plus.circle.fill"), for: .normal) 53 | setImage(UIImage(systemName: "checkmark.circle.fill"), for: .selected) 54 | tintColor = .lightGray 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /JipJung/JipJung/UI/Sound/Views/TagCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TagView.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class TagCollectionViewCell: UICollectionViewCell { 13 | let tagLabel: UILabel = { 14 | let label = UILabel() 15 | label.font = .systemFont(ofSize: 14) 16 | label.textColor = .white 17 | return label 18 | }() 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | cofigure() 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | super.init(coder: coder) 27 | cofigure() 28 | } 29 | 30 | private func cofigure() { 31 | self.backgroundColor = .gray 32 | self.layer.cornerRadius = 4 33 | contentView.addSubview(tagLabel) 34 | 35 | tagLabel.snp.makeConstraints { 36 | $0.center.equalToSuperview() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /JipJung/JipJung/Utils/ApplicationMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationMode.swift 3 | // JipJung 4 | // 5 | // Created by 오현식 on 2021/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | 12 | final class ApplicationMode { 13 | static let shared = ApplicationMode() 14 | private init() {} 15 | 16 | let mode = BehaviorRelay(value: .bright) 17 | 18 | func convert() { 19 | switch mode.value { 20 | case .bright: 21 | mode.accept(.dark) 22 | case .dark: 23 | mode.accept(.bright) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /JipJung/JipJung/Utils/FeedbackGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedbackGenerator.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/25. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FeedbackGenerator { 11 | static let shared = FeedbackGenerator() 12 | private init() {} 13 | 14 | private let lightGenerator = UIImpactFeedbackGenerator(style: .light) 15 | 16 | func impactOccurred() { 17 | lightGenerator.impactOccurred() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /JipJung/JipJung/Utils/LocalDBMigrator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalDBMigrator.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/14. 6 | // 7 | 8 | import Foundation 9 | 10 | import RealmSwift 11 | import RxSwift 12 | 13 | final class LocalDBMigrator { 14 | static let shared = LocalDBMigrator() 15 | private var disposeBag = DisposeBag() 16 | 17 | private init() {} 18 | 19 | func migrateJsonData(dataList: [T]) throws { 20 | for data in dataList { 21 | do { 22 | try RealmDBManager.shared.add(data) 23 | } catch { 24 | throw error 25 | } 26 | } 27 | } 28 | 29 | func migrateSchema() throws { 30 | let config = Realm.Configuration( 31 | schemaVersion: 1, 32 | migrationBlock: { migration, oldSchemaVersion in 33 | if oldSchemaVersion < 1 { 34 | DispatchQueue.main.async { 35 | guard let focusRecords = try? RealmDBManager.shared.objects(ofType: FocusRecord.self) else { 36 | return 37 | } 38 | for focusRecord in focusRecords { 39 | let dateKey = "\(focusRecord.year)\(focusRecord.month)\(focusRecord.day)" 40 | let dateFocusRecord: DateFocusRecord 41 | if let dateFocusRecordObject = try? RealmDBManager.shared.object( 42 | ofType: DateFocusRecord.self, 43 | forPrimaryKey: dateKey 44 | ) { 45 | dateFocusRecord = dateFocusRecordObject 46 | try? dateFocusRecord.realm?.write({ 47 | dateFocusRecord.focusTime.append(focusRecord) 48 | }) 49 | } else { 50 | dateFocusRecord = DateFocusRecord(id: dateKey) 51 | dateFocusRecord.focusTime.append(focusRecord) 52 | } 53 | try? RealmDBManager.shared.add(dateFocusRecord) 54 | } 55 | } 56 | } 57 | } 58 | ) 59 | Realm.Configuration.defaultConfiguration = config 60 | _ = try? Realm() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /JipJung/JipJung/Utils/PushNotificationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PushNotificationManager.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/25. 6 | // 7 | 8 | import Foundation 9 | import UserNotifications 10 | 11 | final class PushNotificationMananger { 12 | static let shared = PushNotificationMananger() 13 | private init() {} 14 | 15 | enum NotificationTitleType: String { 16 | case focusFinish = "집중 모드 종료" 17 | case relaxFinish = "휴식 종료" 18 | } 19 | 20 | func presentFocusStopNotification(title: NotificationTitleType, body: String) { 21 | UNUserNotificationCenter.current().getNotificationSettings { status in 22 | if status.authorizationStatus == UNAuthorizationStatus.authorized { 23 | let content = UNMutableNotificationContent() 24 | content.badge = 0 25 | content.title = title.rawValue 26 | content.body = body 27 | content.sound = .default 28 | 29 | let trigger = UNTimeIntervalNotificationTrigger( 30 | timeInterval: 0.2, 31 | repeats: false 32 | ) 33 | let request = UNNotificationRequest( 34 | identifier: "stopFocus", 35 | content: content, 36 | trigger: trigger 37 | ) 38 | 39 | UNUserNotificationCenter.current().add(request) { error in 40 | if let error = error { 41 | print(#function, #line, error) 42 | } 43 | } 44 | } else { 45 | print("알림 권한이 없음") 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /JipJung/JipJung/Utils/Utils+Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils+Enums.swift 3 | // JipJung 4 | // 5 | // Created by turu on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ApplicationLaunchError: Error { 11 | case resourceJsonFileNotFound 12 | } 13 | 14 | enum ApplicationModeType: Int { 15 | case bright, dark 16 | } 17 | 18 | enum TabBarItems { 19 | enum Home { 20 | static let title = "Home" 21 | static let image = "music.note.house" 22 | } 23 | 24 | enum Explore { 25 | static let title = "Explore" 26 | static let image = "slash.circle" 27 | } 28 | 29 | enum Me { 30 | static let title = "Me" 31 | static let image = "person" 32 | } 33 | } 34 | 35 | enum UserDefaultsKeys { 36 | static let searchHistory = "SearchHistory" 37 | static let wasLaunchedBefore = "WasLaunchedBefore" 38 | } 39 | 40 | enum SoundTag: CaseIterable { 41 | case all, nature, urban, relax, focus, cafe, lounge, club 42 | 43 | var value: String { 44 | switch self { 45 | case .all: 46 | return "All" 47 | case .nature: 48 | return "Nature" 49 | case .urban: 50 | return "Urban" 51 | case .relax: 52 | return "Relax" 53 | case .focus: 54 | return "Focus" 55 | case .cafe: 56 | return "Cafe" 57 | case .lounge: 58 | return "Lounge" 59 | case .club: 60 | return "Club" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /JipJung/LocalFileManagerTests/LocalFileManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalFileMangerTests.swift 3 | // LocalFileMangerTests 4 | // 5 | // Created by Soohyeon Lee on 2021/11/04. 6 | // 7 | 8 | import XCTest 9 | 10 | class LocalFileMangerTests: XCTestCase { 11 | 12 | var localFileManager: LocalFileManager! 13 | 14 | let testDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] 15 | 16 | let testFileName = "Test.txt" 17 | let testFileContents = "test" 18 | 19 | override func setUpWithError() throws { 20 | localFileManager = LocalFileManager.shared 21 | 22 | let filePath = testDirectory.appendingPathComponent(testFileName) 23 | FileManager.default.createFile(atPath: filePath.path, contents: testFileContents.data(using: .utf8)) 24 | } 25 | 26 | override func tearDownWithError() throws { 27 | localFileManager = nil 28 | 29 | let filePath = testDirectory.appendingPathComponent(testFileName) 30 | try? FileManager.default.removeItem(at: filePath) 31 | } 32 | 33 | func test_LocalFileManager_Read_Success() { 34 | // Given 35 | let fileName = testFileName 36 | // When 37 | let fileData = localFileManager.read(fileName) 38 | // Then 39 | XCTAssertNotNil(fileData) 40 | 41 | guard let fileData = fileData else { 42 | XCTFail("Error: FileData is Nil") 43 | return 44 | } 45 | 46 | // Given 47 | let contents = testFileContents 48 | // When 49 | let fileString = String(data: fileData, encoding: .utf8) 50 | // Then 51 | XCTAssertEqual(fileString, contents) 52 | } 53 | 54 | func test_LocalFileManager_Read_Fail() { 55 | // Given 56 | let newFileName = "a" 57 | // When 58 | let fileData = localFileManager.read(newFileName) 59 | // Then 60 | XCTAssertNil(fileData) 61 | } 62 | 63 | func test_LocalFileManager_Write_Success() { 64 | // Given 65 | let fileName = testFileName 66 | // When 67 | let fileData = localFileManager.read(fileName) 68 | // Then 69 | XCTAssertNotNil(fileData) 70 | 71 | guard let fileData = fileData else { 72 | XCTFail("Error: FileData is Nil") 73 | return 74 | } 75 | 76 | // Given 77 | let contents = testFileContents 78 | // When 79 | let fileString = String(data: fileData, encoding: .utf8) 80 | // Then 81 | XCTAssertEqual(fileString, contents) 82 | } 83 | 84 | func test_LocalFileManager_Delete_Success() { 85 | // Given 86 | let fileName = testFileName 87 | // When 88 | let fileData = localFileManager.delete(fileName) 89 | // Then 90 | XCTAssertTrue(fileData) 91 | 92 | // When 93 | let filePath = testDirectory.appendingPathComponent(fileName) 94 | let isExist = FileManager.default.fileExists(atPath: filePath.path) 95 | // Then 96 | XCTAssertFalse(isExist) 97 | } 98 | 99 | func test_LocalFileManager_Delete_Fail() { 100 | // Given 101 | let newFileName = "a" 102 | // When 103 | let fileData = localFileManager.delete(newFileName) 104 | // Then 105 | XCTAssertFalse(fileData) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /JipJung/MaximTests/MaximListRepositoryStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximListRepositoryStub.swift 3 | // MaximTests 4 | // 5 | // Created by 윤상진 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | struct MaximListRepositoryStub: MaximListRepositoriable { 13 | let testData: [Maxim] 14 | 15 | func read(from date: Date) -> Single<[Maxim]> { 16 | return Observable.of(testData).asSingle() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JipJung/MaximTests/MaximListUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaximTests.swift 3 | // MaximTests 4 | // 5 | // Created by 윤상진 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | 10 | import Nimble 11 | import RxSwift 12 | import RxBlocking 13 | import RealmSwift 14 | 15 | class MaximListUseCaseTests: XCTestCase { 16 | func test_Fetch의결과는_항상7의배수() throws { 17 | var testMaximData = [Maxim]() 18 | for i in 1...7 { 19 | let maximUseCase = MaximListUseCase(maximListRepository: MaximListRepositoryStub(testData: testMaximData)) 20 | let fetchObservable = maximUseCase.fetchWeeksMaximList() 21 | .asObservable() 22 | .toBlocking() 23 | let maximList = try! fetchObservable.first()! 24 | expect(maximList.count % 7).to(equal(0)) 25 | testMaximData.append(Maxim(id: "\(i)", date: Date(), thumbnailImageFileName: "", content: "", speaker: "")) 26 | } 27 | } 28 | 29 | func test_Fetch결과는_시간역순배열() { 30 | let forwardTestMaximData: [Maxim] = (1...10).map({ 31 | let date = dateGenerator(baseDate: Date(), dateInterval: $0) 32 | return testMaximGenerator(date: date) 33 | }) 34 | let maximUseCase = MaximListUseCase(maximListRepository: MaximListRepositoryStub(testData: forwardTestMaximData)) 35 | let maximObservable = maximUseCase.fetchWeeksMaximList().asObservable() 36 | 37 | let maximData = try! maximObservable.toBlocking().single() 38 | if maximData.count == 0 { 39 | return 40 | } 41 | for cur in 1.. Maxim { 70 | return Maxim(id: "", date: date, thumbnailImageFileName: "", content: "", speaker: "") 71 | } 72 | 73 | private func dateGenerator(baseDate: Date, dateInterval: Int) -> Date { 74 | return baseDate.addingTimeInterval(Double(dateInterval) * TimeInterval(86400)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /JipJung/MeTests/LoadFocusTimeRepositoryStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusTimeRepositoryStub.swift 3 | // MeTests 4 | // 5 | // Created by 윤상진 on 2022/01/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | struct LoadFocusTimeRepositoryStub: FocusTimeRepositoryProtocol { 13 | var focusTimes = [Date: DateFocusRecord]() 14 | 15 | func create(record: FocusRecord) -> Single { 16 | return Single.create { _ in return Disposables.create()} 17 | } 18 | 19 | func read(date: Date) -> Single { 20 | return Single.create { single in 21 | if let dateFocusRecord = focusTimes[date] { 22 | single(.success(dateFocusRecord)) 23 | } else { 24 | single(.success(DateFocusRecord(id: date))) 25 | } 26 | return Disposables.create() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /JipJung/MeTests/LoadFocusTimeUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadFocusTimeUseCaseTests.swift 3 | // MeTests 4 | // 5 | // Created by 윤상진 on 2022/01/02. 6 | // 7 | 8 | import XCTest 9 | 10 | import Nimble 11 | import RxBlocking 12 | 13 | class LoadFocusTimeUseCaseTests: XCTestCase { 14 | var loadFocusTimeUseCase: LoadFocusTimeUseCase! = nil 15 | let date = Date() 16 | let nDays = 140 17 | let oneDay = TimeInterval(86400) 18 | var dates: [Date] { (0.. 8.9.0) 7 | - FirebaseCore (8.9.1): 8 | - FirebaseCoreDiagnostics (~> 8.0) 9 | - GoogleUtilities/Environment (~> 7.6) 10 | - GoogleUtilities/Logger (~> 7.6) 11 | - FirebaseCoreDiagnostics (8.9.0): 12 | - GoogleDataTransport (~> 9.1) 13 | - GoogleUtilities/Environment (~> 7.6) 14 | - GoogleUtilities/Logger (~> 7.6) 15 | - nanopb (~> 2.30908.0) 16 | - FirebaseStorage (8.9.0): 17 | - FirebaseCore (~> 8.0) 18 | - GTMSessionFetcher/Core (~> 1.5) 19 | - GoogleDataTransport (9.1.2): 20 | - GoogleUtilities/Environment (~> 7.2) 21 | - nanopb (~> 2.30908.0) 22 | - PromisesObjC (< 3.0, >= 1.2) 23 | - GoogleUtilities/Environment (7.6.0): 24 | - PromisesObjC (< 3.0, >= 1.2) 25 | - GoogleUtilities/Logger (7.6.0): 26 | - GoogleUtilities/Environment 27 | - GTMSessionFetcher/Core (1.7.0) 28 | - nanopb (2.30908.0): 29 | - nanopb/decode (= 2.30908.0) 30 | - nanopb/encode (= 2.30908.0) 31 | - nanopb/decode (2.30908.0) 32 | - nanopb/encode (2.30908.0) 33 | - Nimble (9.2.1) 34 | - PromisesObjC (2.0.0) 35 | - Realm (10.19.0): 36 | - Realm/Headers (= 10.19.0) 37 | - Realm/Headers (10.19.0) 38 | - RealmSwift (10.19.0): 39 | - Realm (= 10.19.0) 40 | - RxBlocking (6.2.0): 41 | - RxSwift (= 6.2.0) 42 | - RxCocoa (6.2.0): 43 | - RxRelay (= 6.2.0) 44 | - RxSwift (= 6.2.0) 45 | - RxRelay (6.2.0): 46 | - RxSwift (= 6.2.0) 47 | - RxSwift (6.2.0) 48 | - RxTest (6.2.0): 49 | - RxSwift (= 6.2.0) 50 | - SnapKit (5.0.1) 51 | - SwiftLint (0.45.0) 52 | 53 | DEPENDENCIES: 54 | - Firebase/Storage 55 | - Nimble 56 | - RealmSwift 57 | - RxBlocking 58 | - RxCocoa 59 | - RxRelay 60 | - RxSwift 61 | - RxTest 62 | - SnapKit 63 | - SwiftLint 64 | 65 | SPEC REPOS: 66 | trunk: 67 | - Firebase 68 | - FirebaseCore 69 | - FirebaseCoreDiagnostics 70 | - FirebaseStorage 71 | - GoogleDataTransport 72 | - GoogleUtilities 73 | - GTMSessionFetcher 74 | - nanopb 75 | - Nimble 76 | - PromisesObjC 77 | - Realm 78 | - RealmSwift 79 | - RxBlocking 80 | - RxCocoa 81 | - RxRelay 82 | - RxSwift 83 | - RxTest 84 | - SnapKit 85 | - SwiftLint 86 | 87 | SPEC CHECKSUMS: 88 | Firebase: fb5114cd2bf96e2ff7bcb01d0d9a156cf5fd2f07 89 | FirebaseCore: c5aab092d9c4b8efea894946166b04c9d9ef0e68 90 | FirebaseCoreDiagnostics: 5daa63f1c1409d981a2d5007daa100b36eac6a34 91 | FirebaseStorage: 452c98c31ccb40b819764bf3039426c4388d9939 92 | GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 93 | GoogleUtilities: 684ee790a24f73ebb2d1d966e9711c203f2a4237 94 | GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91 95 | nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 96 | Nimble: e7e615c0335ee4bf5b0d786685451e62746117d5 97 | PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 98 | Realm: 869ab2a7e3bdfa6bf06d8723251539f504e6f602 99 | RealmSwift: f1f51c4a02fe826e5cea623b5441992f06bdc942 100 | RxBlocking: 0b29f7d2079109a8de49c411381bed7c33ef1eeb 101 | RxCocoa: 4baf94bb35f2c0ab31bc0cb9f1900155f646ba42 102 | RxRelay: e72dbfd157807478401ef1982e1c61c945c94b2f 103 | RxSwift: d356ab7bee873611322f134c5f9ef379fa183d8f 104 | RxTest: 0c692fa672b694373ba86d2b00033595297a2831 105 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 106 | SwiftLint: e5c7f1fba68eccfc51509d5b2ce1699f5502e0c7 107 | 108 | PODFILE CHECKSUM: 4d29fbcea3ec55b846d86dd1f215f0fea32e299d 109 | 110 | COCOAPODS: 1.11.2 111 | -------------------------------------------------------------------------------- /JipJung/RealmTests/FocusTimeRealmTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusTimeRealmTests.swift 3 | // RealmTests 4 | // 5 | // Created by 윤상진 on 2021/12/23. 6 | // 7 | 8 | import XCTest 9 | @testable import JipJung 10 | 11 | import Nimble 12 | import RealmSwift 13 | import RxBlocking 14 | import RxSwift 15 | 16 | class FocusTimeRealmTests: XCTestCase { 17 | var loadFocusTimeUseCase: LoadFocusTimeUseCase! = nil 18 | 19 | override func setUpWithError() throws { 20 | Realm.Configuration.defaultConfiguration.inMemoryIdentifier = name 21 | try! ApplicationLaunch().makeDebugLaunch() 22 | loadFocusTimeUseCase = LoadFocusTimeUseCase(focusTimeRepository: FocusTimeRepository()) 23 | } 24 | 25 | func test_realm을_사용하는_loadHistory의_성능측정() throws { 26 | self.measure { 27 | let historyObservable = loadFocusTimeUseCase.loadHistory(from: Date(), nDays: 140).asObservable().toBlocking() 28 | _ = try! historyObservable.first()! 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /JipJung/UserDefaultsStorageTest/UserDefaultsStorageTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsStorageTest.swift 3 | // UserDefaultsStorageTest 4 | // 5 | // Created by 오현식 on 2021/11/04. 6 | // 7 | 8 | import XCTest 9 | @testable import JipJung 10 | 11 | class UserDefaultsStorageTest: XCTestCase { 12 | 13 | var userDefaultsStorage: UserDefaultsStorage! 14 | 15 | override func setUpWithError() throws { 16 | userDefaultsStorage = UserDefaultsStorage() 17 | } 18 | 19 | override func tearDownWithError() throws { 20 | userDefaultsStorage = nil 21 | } 22 | 23 | func testuserDefaultsStorage_saveIntValue() { 24 | userDefaultsStorage.save(for: "test", value: 6) 25 | 26 | let value = UserDefaults.standard.integer(forKey: "test") 27 | 28 | XCTAssertEqual(value, 6) 29 | } 30 | 31 | func testuserDefaultsStorage_loadIntValue() { 32 | UserDefaults.standard.set(6, forKey: "test") 33 | 34 | let value: Int? = userDefaultsStorage.load(for: "test") 35 | 36 | XCTAssertNotNil(value) 37 | XCTAssertEqual(6, value) 38 | } 39 | 40 | func testuserDefaultsStorage_saveStringArrayValue() { 41 | userDefaultsStorage.save(for: "test", value: ["test1", "test2", "test3"]) 42 | 43 | let value = UserDefaults.standard.object(forKey: "test") 44 | 45 | XCTAssertNotNil(value) 46 | guard let value = value as? [String] else { return } 47 | 48 | XCTAssertEqual(value, ["test1", "test2", "test3"]) 49 | } 50 | 51 | func testuserDefaultsStorage_loadStringArrayValue() { 52 | UserDefaults.standard.set(["test1", "test2", "test3"], forKey: "test") 53 | 54 | let value: [String]? = userDefaultsStorage.load(for: "test") 55 | 56 | XCTAssertNotNil(value) 57 | XCTAssertEqual(["test1", "test2", "test3"], value) 58 | } 59 | } 60 | --------------------------------------------------------------------------------