Spotify Web API๋ฅผ ํ์ฉํ ๋๋ง์ ์์ ์ฌ์ ์ฑ
5-์ ๋ฐ์ดํธ ๋ฐ ๋ฆฌํฉํ ๋ง ์ฌํญ
Spotify Web API๋ฅผ ํ์ฉํ ๋๋ง์ ์์ ์ฌ์ ์ฑ
- ๊ฐ๋ฐ๊ธฐ๊ฐ : 2023.06.10 ~ 2023.07.28 (์ฝ 6์ฃผ)
- ์ฐธ์ฌ์ธ์ : 1์ธ (๊ฐ์ธ ํ๋ก์ ํธ)
- ์ฃผ์ํน์ง
- ์ธ๊ณ ์ต๋ ์์ ์คํธ๋ฆฌ๋ฐ ์๋น์ค์ธ Spotify๋ฅผ ์ปค์คํ UI ๋์์ธ์ผ๋ก ๊ตฌํ
- ์๋ก๋์จ ์จ๋ฒ, ํ๋ ์ด๋ฆฌ์คํธ ๋ฐ ์ฅ๋ฅด๋ณ ์์ , ์ํฐ์คํธ ๋ฐ ์จ๋ฒ ์ฐพ๊ธฐ, ํ๋ ์ด๋ฆฌ์คํธ ์์ฑ ์ธ ๋ค์ํ ๊ธฐ๋ฅ ์ ๊ณต
- OAuth2.0์ ๋์ ๋ฉ์ปค๋์ฆ(Resource Owner - Client - Authorization & Resource Server) ์ดํด
- Spotify Web API์์ ์ ๊ณตํ๋ ๋ฌธ์๋ฅผ ๋ฐํ์ผ๋ก RESTFul API ๊ตฌํ
- Code-base UI AutoLayout ๊ตฌํ
-
ํ์ฉ๊ธฐ์ ์ธ ํค์๋
- iOS : swift 5.8, xcode 14.3.1, UIKit
- Network: URLSession, OAuth(RESTful API)
- UI : ScrollView, TableView, CollectionView, TabBar, StackView
- Layout : AutoLayout(Code-base), Compositional Layout
-
๋ผ์ด๋ธ๋ฌ๋ฆฌ
- KingFisher (7.0.0)
- ๐ฃ๏ธ ๋ฐ๋์ ์๋ ์ ์ฐจ์ ๋ฐ๋ผ ๊ตฌ๋ํด์ฃผ์๊ธธ ๋ฐ๋๋๋ค.
Spotify ์ ์ฉ ํ์๊ฐ์๋ฐ๊ฐ๋ฐ์ ์ค์ ์ด ํ์ํฉ๋๋ค (Google, Facebook, Apple ๋ฏธ ์ง์)- ๊ตฌ๋์ด ์ ๋์ง ์๊ฑฐ๋, ๋ก๊ทธ์ธ ํ ํ๋ฉด์ ์๋ฌด๊ฒ๋ ๋ณด์ด์ง ์๋๋ค๋ฉด ์๋ ๋ฉ์ผ๋ก ๋ฌธ์ ๋ถํ๋๋ฆฝ๋๋ค
- [email protected]
| ์์ | ๋ด์ฉ | ๋น๊ณ |
|---|---|---|
| 1 | ์คํฌํฐํ์ด ๊ณ์ ์ ์์ฑ ํ, ๋ก๊ทธ์ธํฉ๋๋ค(Google ์ธ ๊ธฐํ ๊ฒฝ๋ก ๊ฐ์ X) | ์คํฌํฐํ์ด ๊ฐ๋ฐ์ ๋งํฌ |
| 2 | ์ค๋ฅธ์ชฝ ์๋จ์ ์๋ ๋ด ์์ด๋ ๋ฅผ ํด๋ฆญํ ํ, Dashboard๋ก ์ด๋ํฉ๋๋ค |
์ฝ๊ด ๋์ ํ, ์ด๋ฉ์ผ ์ธ์ฆ์ ์ค์ |
| 3 | ์ฑ ์์ฑ ๋ฒํผ์ ๋๋ฅธ ํ, ์ฑ ์ด๋ฆ๊ณผ ์๊ฐ, RedirectURL์ ์์ฑํ ํ ์ ์ฅํฉ๋๋ค | RedirectURL(์์ ์น ํ์ด์ง ์ฃผ์) |
| 4 | ์ฑ์ด ์์ฑ๋์๋ค๋ฉด, ์ค๋ฅธ์ชฝ ์๋จ์ Settings ๋ฒํผ์ ํด๋ฆญํฉ๋๋ค |
- |
| 5 | Base Infromation ํญ์์, ClientID ์ Client secret ์ฝ๋๋ฅผ ๋ณต์ฌํฉ๋๋ค |
- |
| 6 | ํด๋ก ๋ฐ์ ํ๋ก์ ํธ ํ์ผ ๋ด ClientID+Secret.swift ํ์ผ๋ก ์ด๋ํฉ๋๋ค |
ClientID ํด๋ ๋ด ์์น |
| 7 | ๋น์ด์๋ ๋ฌธ์์ด์ ์์๋๋ก ClientID, ClientSecret, RedirectURL ์ ๊ธฐ์
ํฉ๋๋ค. |
- |
| 8 | ๋น๋ ํ, ์์ ์์ฑํ ์คํฌํฐํ์ด ๊ณ์ ์ผ๋ก ์ง์ ๋ก๊ทธ์ธ ํฉ๋๋ค | Google, Facebook, Apple ๋ก๊ทธ์ธ X |
MVC Architecture
- ์์
์ฑ์ ํน์ฑ์, Track ๋ฆฌ์คํธ, ์ฌ์ ํ๋ ์ด์ด ๋ฑ ์ ์ฌํ View๋ฅผ ์ง์์ ์ผ๋ก ์ฌ ์ฌ์ฉํ๋ฏ๋ก,
MVC ํจํดํ์ฉ - AuthManager ๊ฐ์ฒด์์ API Fetching ๋ฉ์๋ ๊ตฌํ ๋ก์ง์ ๋ด๋น, ViewController์์ ์ง์ ํธ์ถ
- OAuth2.0 ์ธ์ฆ์ ์ํด ์ฝ๋ ๋ฐ ํ ํฐ๋ฐํ์ ์ํ AuthManager ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ , UserDefault ๊ฐ์ผ๋ก ์ ์ฅ
Presenter ํ์ฉ
- Music Player์ ๊ฒฝ์ฐ,
2๊ฐ์ง ๊ฒฝ์ฐ์ ์(ํ๋์ ํธ๋ ํน์ ์ ์ฒด ํธ๋)๋ฅผ ๊ฐ์ง๊ณ ์์ - ํ๋์ VC์์ ์ญํ ์ด ๋ค์ ๊ณผ์ค๋๋ค ํ๋จ,
Presenter๋ฅผ ํตํด View ๋ฐ Model์ ์ํ๋ฅผ ํ์ธ, ์ ๋ฐ์ดํธ ๋ก์ง์ ๊ตฌํ
Spotify_App
โฃ ๐App
โฃ ๐ClientID
โฃ ๐Controllers
โ โฃ ๐Core
โ โ โฃ ๐HomeViewController.swift
โ โ โฃ ๐LibraryViewController.swift
โ โ โฃ ๐SearchViewController.swift
โ โ โ ๐TabBarViewController.swift
โ โ ๐Others
โฃ ๐Managers
โ โฃ ๐APICaller.swift
โ โฃ ๐AuthManager.swift
โ โ ๐HapticManager.swift
โฃ ๐Models
โ โฃ ๐Auth
โ โฃ ๐Common
โ โฃ ๐Response
โ โ ๐User
โฃ ๐View
โ โฃ ๐HomeTab
โ โฃ ๐LibraryTab
โ โฃ ๐Player
โ โ โฃ ๐PlayBackPresenter
โ โ โ โ ๐PlayBackPresenter.swift
โ โ ๐SearchTab
- Spotify Web API ๋ฐ์ดํฐ ํ์ฑ์ ๋ฐ๋ผ, ์ค์๊ฐ์ผ๋ก ์์ ์ ๋ณด๋ฅผ ํ์ธํจ
- ๋จ์ผ ์จ๋ฒ์ ๋น๋กฏ, ์ฃผ์ ์ ๋ฐ๋ผ ๋ค์ํ ํธ๋์ด ํผํฉ๋ ํ๋ ์ด๋ฆฌ์คํธ ์ ๊ณต
![]() |
![]() |
![]() |
|---|---|---|
Home |
Album |
Playlist |
- Search API ํ์ฑ ๋ฐ์ดํฐ๋ฅผ SearchResults ๋ชจ๋ธ ๊ฐ์ฒด๋ก ๋ณํํ์ฌ ์์ , ์ํฐ์คํธ, ์จ๋ฒ ๊ฒ์๊ฒฐ๊ณผ๋ฅผ ์น์ ๋ณ๋ก ๋ํ๋
- ์ฃผ์ (์นดํ ๊ณ ๋ฆฌ)์ ๋ฐ๋ผ ๋์ผํ ์์ฑ๊ฐ(category.id)์ ๊ฐ์ง๊ณ ์๋ ํ๋ ์ด๋ฆฌ์คํธ๋ฅผ ์ ๊ณต
![]() |
![]() |
![]() |
|---|---|---|
Search |
Keyword |
Category Playlists |
- ๋ก๊ทธ์ธ ์ ์ ๊ฐ ์ง์ ํ๋ ์ด๋ฆฌ์คํธ์ ๋ช ์นญ๊ณผ ํฌํจ๋ ํธ๋์ ์ถ๊ฐํ ์ ์์
- Long Tap Gesture๋ฅผ ํ์ฉํ์ฌ ํ๋ ์ด๋ฆฌ์คํธ ๋ด ํธ๋ ์ถ๊ฐ (ํ์ฌ๋ HomeTab ํ๋จ ์ถ์ฒ ํธ๋๋ง ์ถ๊ฐ ๊ฐ๋ฅ)
- ๋ง์์ ๋๋ ์จ๋ฒ์ด ์์ ๊ฒฝ์ฐ, ์จ๋ฒ ํ์ด์ง ์ฐ์ธก ์๋จ์ (+) ๋ฒํผ์ ๋๋ฌ ์ฆ๊ฒจ์ฐพ๋ ์จ๋ฒ ์ถ๊ฐ
![]() |
![]() |
![]() |
|---|---|---|
Create Playlist |
Playlists tracks |
Favorite Album |
- AVPlayer Framework๋ฅผ ํ์ฉํ์ฌ ํธ๋ ๋ฐ ํ๋ ์ด๋ฆฌ์คํธ(์จ๋ฒ) ์ ์ฒด ์ฌ์ ๊ฐ๋ฅ
- ๐๐ป ์๋ฎฌ๋ ์ดํฐ๋ก ๊ตฌ๋์, delay๊ฐ ๋ฐ์ํ๋ฉฐ, ์ค์ ๋๋ฐ์ด์ค์์๋ ํ ์คํธ ์๋ฃ)
- ์์/์ ์ง(PlayPause), ๋ค์ ํธ๋(Forward), ์ด์ ํธ๋(Backward) ๊ธฐ๋ฅ ๊ตฌํ
![]() |
![]() |
![]() |
|---|---|---|
Audio Player |
Play All Button |
Volume Slider |
UIKit ๊ฐ๋ฐ ํ๋ ์์ํฌ๋ฅผ ํ์ฉ, OPEN API ๋คํธ์ํฌ ๊ตฌํ, ๊ตฌ์ํ ๋ชฉํ
- ์คํ ๋ฆฌ๋ณด๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ AutoLayout์ ๊ตฌํํ๋
Code-base๋ฅผ ํ์ฉ - ๋จ์ํ๋ฉด์, ๊ธฐ์ด์ ์ผ๋ก ์ฑ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถํ๊ณ ์
MVC ํจํด์ ์ฉ - ํ์ฌ ์์ฉํ ๋ ์ฑ์ ์ ๋ฐ์ ์ธ ๋ชจ์ต์ ๋ฐ๋ผ๊ฐ๋, 1์ฐจ์ ์ผ๋ก UI ์ธก๋ฉด๋ณด๋ค๋
๊ธฐ๋ฅ ์ค์ฌ์ ๊ตฌํ ๋ชฉํ
OAuth 2.0 ๋ก๊ทธ์ธ ๊ณผ์ ์ ๋ํ ํ์ต์ ๊ธฐ๋ฐ์ผ๋ก User Authmetication ๊ตฌํ
- Spotify Web API ๊ฐ์ด๋์ ๋ฐ๋ผ ๋ก๊ทธ์ธ ์์ฒญ โ ํ์ด์ง ์ ๊ณต โ Auth Code ๋ฐ๊ธ ๋ฐ Token ๊ตํ โ DB ์ ์ฅ โ API ํธ์ถ(Finish!)
- UserDefaults๋ฅผ ํ์ฉํ์ฌ Token ์ ์ฅ โ ์ฒ์ ๋ก๊ทธ์ธ ์ดํ, ์ฑ์ ์ฌ ์คํํ์ ๋ ์ฌ ๋ก๊ทธ์ธํ์ง ์๋๋ก ํจ
- TroubleShooting : 401 repsponse Error ๋ฐ์ ๋ฐ ํด๊ฒฐ๊ณผ์
HomeTab(์๋ก๋์จ ์จ๋ฒ, ์ถ์ฒ ์ฌ์๋ชฉ๋ก, ์ ์ฌํ ์ํฐ์คํธ&ํธ๋)
- ๊ฐ๊ฐ์ Section๋ณ ๊ตฌ๋ถ์ ๋ชฉํ๋ก, ํด๋น View์์ ํ์ฉ๋๋ ViewModel์ Associated Values์ผ๋ก ์ค์ ํ๋ BrowseSectionType์ ์์ฑ
enum BrowseSectionType {
case newRelease(viewModels: [NewReleasesCellViewModel])
case featuredPlaylists(viewModels: [FeaturedPlaylistsCellViewModel])
case recommendedTracks(viewModels: [RecommendedTrackCellViewModel])
...- ์์ง ์คํฌ๋กค ํ๋ฉด ๋ด, ์ํ ์คํฌ๋กค๋ก ๊ตฌ์ฑ๋ ์๋ก๋์จ ์จ๋ฒ, ์ถ์ฒ ์ฌ์๋ชฉ๋ก์ ๊ตฌํํ๊ธฐ ์ํด
Compositional Layout์ ์ฌ์ฉ
Search Tab(์ํฐ์คํธ, ์จ๋ฒ ์ธ ๊ฒ์๊ธฐ๋ฅ, ํ ๋ง๋ณ ํ๋ ์ด๋ฆฌ์คํธ)
- ๊ฒ์ ๊ฒฐ๊ณผ(Query ์ ๋ ฅ ๋ฐ Search ์๋ฃ ๋ฒํผ) ์ ๋ ฅ๊ฐ์ ๋ฐ๋ผ, UI๋ฅผ 4๊ฐ์ ์น์ ์ผ๋ก ๊ตฌ๋ถ(SearchResult)ํ์ฌ ์ ๋ฐ์ดํธ
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let resultsController = searchController.searchResultsController as? SearchResultViewController,
let query = searchBar.text,
!query.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
resultsController.delegate = self
APICaller.shared.search(with: query) { result in
DispatchQueue.main.async {
switch result {
case .success(let result):
resultsController.update(with: result)
case .failure(let error):
print(error.localizedDescription)
}
}
}
}Player Music(์์ ์ฌ์)
AVPlayerframework๋ฅผ ํ์ฉํ์ฌ ์ฌ์&์ผ์์ ์ง, ์ด์ ํธ๋, ๋ค์ ํธ๋ ๊ธฐ๋ฅ ๊ตฌํ (AVPlayer, AVQueuePlayer)- PlayerController์ ๊ณผ๋ํ ์ญํ ๋ถ๋ด์ ์ค์ด๊ธฐ ์ํด,
Presenter๋ฅผ ์์ฑ, ํ์ฉ - Track(Cell), Album&Playlists(Play All Button) ์ ํ์ ๋ฐ๋ผ ๊ธฐ๋ฅ ๋ถ๊ธฐ์ฒ๋ฆฌ ์ค์
var player: AVPlayer? // single Track
var playerQueue: AVQueuePlayer? // playlist or album Player
// ํ์ฌ ์ฌ์๋๊ณ ์๋ ํธ๋์ ์ฑ๊ฒฉ(ํน์ ์ ํ๋ ์์ดํ
)์ ๋ฐ๋ผ track(single) ํน์ tracks(album, playlists)๋ฅผ ๋ฐํ
var currentTrack: AudioTrack? {
if let track = track, tracks.isEmpty {
return track
}
else if !tracks.isEmpty {
return tracks[index]
}
return nil
}
...Library Tab (์ ์ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋๋ง์ Playlist ์์ฑ ๋ฐ ์ญ์ , Album ์ ์ฅ๊ธฐ๋ฅ)
- Child ViewController(Playlists, Albums)๋ด ํฌํจ๋ ๋ฐ์ดํฐ ์ฌ๋ถ๋ฅผ ํ์ธ(GET), 'ActionLabelView(๋ฐ์ดํฐ๊ฐ ์์)'๋ฅผ ํ ๊ธํจ
- Playlists : ๋ฐ์ดํฐ๊ฐ ์์ ๊ฒฝ์ฐ ์์ฑ(POST) ๋ฉ์๋๋ฅผ ํตํด ๋ง๋ค๊ณ , 'UILongPressGestureRecognizer'๋ฅผ ํ์ฉํด ์ ์ฅ(POST), ์ญ์ (DELETE)ํจ
- Album : ๊ธฐ์กด ์๋ฒ ๋ฐ์ดํฐ์์ ์กด์ฌํ๋ฏ๋ก, ์ ์ฅ(PUT)์ ์ค์ํจ
private func addChildren() {
addChild(playlistVC)
scrollView.addSubview(playlistVC.view)
playlistVC.view.frame = CGRect(x: 0, y: 0, width: scrollView.width, height: scrollView.height) // frame์ ํตํ paging
playlistVC.didMove(toParent: self)
addChild(albumsVC)
scrollView.addSubview(albumsVC.view)
albumsVC.view.frame = CGRect(x: view.width, y: 0, width: scrollView.width, height: scrollView.height) // frame์ ํตํ paging
albumsVC.didMove(toParent: self)
}- API Caller ๋ฉ์๋ ๋ก์ง ์์
- Library ๋ด ์ ์ฅ๋๋ Playlists์ Albums์ ๊ฒฝ์ฐ, ๋์ผํ ๋ฐ์ดํฐ๋ฅผ ์ค๋ณต์ ์ผ๋ก ์ ์ฅํ ์ ์๋ ์ด์ ํด๊ฒฐํ์
- SNS ๋ฐ Google ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํ
- ๊ฐ๋ณ SDK ์ ์ฉ ๋ฐ ๊ธฐ์กด ์ธ์น(WKWeb)์ SFSafariview๋ก ๋์ฒดํ๋ ๋์ ๋น๊ต ๋ฐ ์ ์ฉ
- Player ๊ธฐ๋ฅ
- Playlists๋ Albums์ ์ ์ฒด ์ฌ์ํ ๋, ๋๊ฐ๊ธฐ ํน์ ๋ฐ๋ณต๊ธฐ๋ฅ ์ถ๊ฐ (AudioTrack ๋ฐ์ดํฐ ๊ตฌ์กฐ ๋ด ์ฌ์์๊ฐ ๊ด๋ จ ๊ฐ์ฒด ํ์ธ)
- Volume์ ๋ด๋นํ๋ UISilder ๊ธฐ๋ฅ ์ ๊ฑฐ, ์ฌ์ ์๊ฐ์ ๋ฐ๋ฅธ UISlider ์ ๋ฐ์ดํธ ๋ฐฉ์์ผ๋ก ๋ฆฌํฉํ ๋ง ํ์
- ์ผ๋ถ Track์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ฌ์(Preview_urls)๊ฐ์ด ์์์๋ ๋ถ๊ตฌํ๊ณ , ์ฌ์์ด ๋์ง ์๋ ๋ฌธ์
- Tracks์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ฌ์์ด ์๋ ๊ฒฝ์ฐ ๋ถ๊ธฐ์ฒ๋ฆฌ๋ฅผ ํตํ์ฌ UI ์ ๋ฐ์ดํธ๋ฅผ ํ์ง ์๋๋ก ์ ํ
- UI ๊ฐ์
- frame-base layout์ AutoLayout์ผ๋ก ์ ๋ถ ๋์ฒดํ๊ธฐ
- Color Palette๋ฅผ ํ์ฉํ์ฌ ๋ณด๋ค ํต์ผ๊ฐ ์๋ ๋์์ธ ๊ตฌ์ฑํ๊ธฐ
- Architecture ์ฌ ๊ฒํ
- ViewController ๋ฐ API Caller ๋ด ๊ณผ๋ํ ์ญํ ์ง์ค์ผ๋ก ์ธํ ๋ถ์ฐจ์ ์ธ ๋ฌธ์ ์ฐ๋ ค, MVVM ํจํด์ผ๋ก์ ๋ฆฌํฉํ ๋ง ๊ณ ๋ ค












