├── .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 |
--------------------------------------------------------------------------------