iTunes Search API๋ฅผ ํตํด
์ฑ ID
๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์์ธ ํ๋ฉด์ ๋ํ๋ ๋๋ค.
- Deployment Target : iOS 13.0
- Architecture : MVVM-C
- ํ๋ ์์ํฌ : Combine
- ์ฐธ๊ณ - ์ฝ๋ฉ/์ปค๋ฐ ์ปจ๋ฒค์
AppStoreClone
โโโ App
โโโ Presentation
โ โโโ LookupScene
โ โ โโโ ViewModel
โ โ โโโ View
โ โโโ DetailScene
โ โ โโโ ViewModel
โ โ โโโ View
โ โโโ ScreenshotScene
โ โโโ ViewModel
โ โโโ View
โโโ Model
โโโ Network
โ โโโ Response
โโโ Protocols
โโโ Extensions
โโโ Utilities
โโโ Resources
AppStoreCloneTests
โโโ Mock
- ์ฝ๋๋ก UI๋ฅผ ๊ตฌํํ์ต๋๋ค.
MockURLSession
์ ํ์ฉํ ๋คํธ์ํฌ ํ ์คํธ ๋ฑ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ต๋๋ค.- ์์ธํ๋ฉด์ ์๋จ์
๊ณต์ ๋ฒํผ
์ ๊ตฌํํ์ต๋๋ค. - ์์ธํ๋ฉด์
SummaryScrollView
๋ฅผ ๊ตฌํํ์ฌ ํ์ , ์ฐ๋ น, ์นดํ ๊ณ ๋ฆฌ ๋ฑ ์ฃผ์ ์ ๋ณด๋ฅผ ๋ํ๋์ต๋๋ค. - ์ฑ ํ์ ์
๋ณ ์ด๋ฏธ์ง
๋ก ๋ํ๋๊ณ , ํ๊ฐ ๊ฐ์๋ฅผ์ฒ/๋ง
๋จ์๋ก ํ์ฐํ์ฌ ๋ณด์ฌ์ค๋๋ค. - ์์ธํ๋ฉด ์คํฌ๋ฆฐ์ท ํญ๋ชฉ์์ ํญํ
indexPath
์ ํญ๋ชฉ์ ์คํฌ๋ฆฐ์ท ํ๋ ํ๋ฉด์์ ๊ฐ์ฅ ๋จผ์ ๋ณด์ด๋๋ก ๋ํ๋๋๋ก ํ์ต๋๋ค. - ์์ธํ๋ฉด ์ ๋ณด ํญ๋ชฉ์
InfoTableView
๋ก ๊ตฌํํ์ฌ ์ฑ์ ์ธ๋ถ์ ๋ณด๋ฅผ ๋ํ๋์ต๋๋ค. - ์ด๋ฏธ์ง
Cache
๋ฅผ ๊ตฌํํ์ต๋๋ค. ๊ฐ๋ก/์ธ๋ก ๋ชจ๋
์ ํ์ ๋์ํ์ต๋๋ค.๋คํฌ/๋ผ์ดํธ ๋ชจ๋
์ ํ์ ๋์ํ์ต๋๋ค.
- Feature-1. ๋คํธ์ํฌ ๋ฐ ๊ฒ์ํ๋ฉด ๊ตฌํ
- Feature-2. ์์ธํ๋ฉด ๊ตฌํ
- ๊ตฌํ ํ๋ฉด
- ๋ณด์ํ ์
- ์ฌ์ฉ์๊ฐ
TextField
์ ์ ๋ ฅํ์ฑ ID
๋ฅผ ํตํด ์๋ฒ์์ ์ฑ ์ ๋ณด๋ฅผ ๋ฐ์์ต๋๋ค. - ์ฑ ID๊ฐ ์ ํจํ์ง ์์ผ๋ฉด Label์ ๋ํ๋ ๋๋ค.
URLSession
์ ํตํด ๋คํธ์ํฌ ํต์ ์ ๊ตฌํํ์ต๋๋ค. (MockURLSession
์ ํตํ ํ ์คํธ ์คํ)
Coordinator
๋ฅผ ํตํด ์์กด์ฑ ์ฃผ์
์ ๊ด๋ฆฌํ๊ณ , ํ๋ฉด์ ํ ์ญํ ์ ์ ๋ดํ๋๋ก ํ์ต๋๋ค. ์ด๋ฅผ ์ํด navigationController
๋ฅผ ์์ฑ์ ์ฃผ์
์ผ๋ก ํ์ ChildCoordinator์ ์ ๋ฌํ๊ณ , ํ๋ฉด์ ํ ์ ํด๋น navigationController
๊ฐ ๋ค์ ํ๋ฉด์ push ํ๋๋ก ํ์ต๋๋ค.
๊ฒ์ํ๋ฉด -> ์์ธํ๋ฉด -> ์คํฌ๋ฆฐ์ท ํ๋ ํ๋ฉด
์์ผ๋ก ์ฐ๊ฒฐ๋๋ฏ๋ก ๊ฐ ์์ Coordinator์ childCoordinators
์ ํ์ Coordinator๋ฅผ ์ถ๊ฐํ์ต๋๋ค. ์ด๋ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๋ฅผ ์ํด ํ์ ํ๋ฉด์ด pop๋ ๋, ViewModel์ deinit
์์ ํด๋น Coordinator๊ฐ finish๋๋๋ก ์ค์ ํ์ต๋๋ค. ๋ํ Delegate ํจํด
์ ์ ์ฉํ์ฌ ์์ Coordinator๊ฐ removeFromChildCoordinators(coordinator:)
๋ฅผ ํธ์ถํ์ฌ ํ์ Coordinator๊ฐ childCoordinator์์ ์ ๊ฑฐํ๋๋ก ํ์ต๋๋ค.
Debug Memory Graph
๋ฅผ ํตํ ๋๋ฒ๊น
์ผ๋ก Coordinator/ViewModel ๋ฑ์ด ๋ฉ๋ชจ๋ฆฌ์์ ์ ์์ ์ผ๋ก ํด์ ๋๋์ง ํ์ธํ์ต๋๋ค.
๋น๋๊ธฐ ์์
์ ์ฒ๋ฆฌํ๊ธฐ ์ํด Combine
ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ์ต๋๋ค. ์๋ฒ์์ ๋ฐ์์จ ๋ฐ์ดํฐ๋ Publisher
ํ์
์ผ๋ก ๋ฐํํ๊ณ , ViewModel ๋ฐ ViewController๋ฅผ Binding
ํ์ฌ ํ๋ฉด์ ๋ํ๋ด๋๋ก ๊ตฌํํ์ต๋๋ค. ๋ํ ์ฌ์ฉ์ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด ViewController์์ PassthroughSubject
ํ์
์ ํ์ฉํ์ต๋๋ค.
๋ํ API๋ฅผ ์ด๊ฑฐํ
์ผ๋ก ๊ด๋ฆฌํ๋ ๊ฒฝ์ฐ, API๋ฅผ ์ถ๊ฐํ ๋๋ง๋ค ์๋ก์ด case๊ฐ ํ์ํ์ฌ ์ด๊ฑฐํ์ด ๋น๋ํด์ง๊ณ , ์ด๊ฑฐํ ๊ด๋ จ switch๋ฌธ์ ๋งค๋ฒ ์์ ํด์ผ ํ๋ ๋ฒ๊ฑฐ๋ก์์ด ์์์ต๋๋ค. ๋ฐ๋ผ์ API๋ฅผ ๊ตฌ์กฐ์ฒด
ํ์
์ผ๋ก ๋ณ๊ฒฝํ๊ณ , URL ํ๋กํผํฐ ์ธ์๋ HttpMethod
ํ๋กํผํฐ๋ฅผ ์ถ๊ฐํ APIProtocol
ํ์
์ ์ฑํํ๋๋ก ๊ฐ์ ํ์ต๋๋ค. ํด๋น ๊ตฌ์กฐ๋ ์ฝ๋ ์ ์ง๋ณด์๊ฐ ์ฉ์ดํ๋ฉฐ, ํ์
์ ํ์์ด ๊ฐ์ ๋ด๋นํ API ๊ตฌ์กฐ์ฒด ํ์
์ ๋
๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํ์ฌ ์ถฉ๋์ ๋ฐฉ์งํ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
์๋์ ๋ชฉ์ ์ ์ํด MockURLSession
์ ๊ตฌํํ์ต๋๋ค.
- ์ค์ ์๋ฒ์ ํต์ ํ ๊ฒฝ์ฐ ํ ์คํธ์ ์๋๊ฐ ๋๋ ค์ง
- ์ธํฐ๋ท ์ฐ๊ฒฐ์ํ์ ๋ฐ๋ผ ํ ์คํธ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง๋ฏ๋ก ํ ์คํธ ์ ๋ขฐ๋๊ฐ ๋จ์ด์ง
- ์ค์ ์๋ฒ์ ํต์ ์ ํ๋ฉฐ ์๋ฒ์ ํ ์คํธ ๋ฐ์ดํฐ๊ฐ ๋ถํ์ํ๊ฒ ์ ๋ก๋๋๋ Side-Effect๊ฐ ๋ฐ์ํจ
๋ํ ํฅํ ํ
์คํธ ๋์ ํ์ผ์ด ๋์ด๋ ๊ฒ์ ๋๋นํ์ฌ Mock ๋ฐ์ดํฐ๋ก JSON
ํ์ผ์ ์ถ๊ฐํ๊ณ , Bundle(for: type(of: self))
๋ก ๋ฐ์ดํฐ์ ์ ๊ทผํ์ต๋๋ค.
- AppStore์ ์ฑ ์๊ฐ ํ์ด์ง์ ์ ์ฌํ ๋์์ธ์ผ๋ก ๊ตฌํํ์ต๋๋ค.
- ํ๋ฉด ์์๋ฅผ
Main
,Summary ScrollView
,Screenshot CollectionView
,Description Label
,Info TableView
๋ก ๋๋์ด ๊ตฌํํ์ต๋๋ค. - ์ฑ์ ์คํฌ๋ฆฐ์ท ์ด๋ฏธ์ง๋ฅผ
CollectionView
๋ก ๋ณด์ฌ์ฃผ๊ณ , Cell์ ํญํ๋ฉด ์ด๋ฏธ์ง๋ฅผ ํฌ๊ฒ๋ณผ ์ ์๋ ํ๋ฉด์ ๋ํ๋ ๋๋ค. - ์ฑ ์ค๋ช
์์ญ์
ํผ์น๊ธฐ ๋ฒํผ
์ ํญํ๋ฉด ์ค๋ช ์ด ํผ์ณ์ง๋๋ค. ํผ์ณ์ง ์ํ์์์ ๊ธฐ ๋ฒํผ
์ ํญํ๋ฉด ๋ค์ ์ค๋ช ์ด ์ค์ด๋ญ๋๋ค.
ScrollView ๋ด๋ถ์ ContainerStackView
๋ฅผ ์ถ๊ฐํ๊ณ , ๊ทธ ๋ด๋ถ์ ๋ณ์ /์ฐ๋ น/์นดํ
๊ณ ๋ฆฌ/๊ฐ๋ฐ์/์ธ์ด ๋ฑ ๊ฐ ํญ๋ชฉ์ StackView๋ฅผ ์ถ๊ฐํ์ต๋๋ค. ์ด๋ ๊ฐ ํญ๋ชฉ์ EqualSpacing
์ผ๋ก ๋ฐฐ์นํด์ผ ํ์ผ๋ฏ๋ก Separator
๋ฅผ ContainerStackView์ ๋ฃ์ผ๋ฉด Layout์ด ๊นจ์ง๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
๋ฐ๋ผ์ StackView Extension
์ ํตํด ArrangedSubview๊ฐ ์๋ Subview
์ Separator๋ฅผ ๋ฃ๊ณ , ๊ธฐ์กด ์ปจํ
์ธ ์ trailingAnchor
์์น์ Separator๋ฅผ ๋ฐฐ์นํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ต๋๋ค.
func addVerticalSeparators(color: UIColor = .systemGray3, heightRatio: CGFloat = 0.5, spacing: CGFloat = 0) {
let separatorCount = arrangedSubviews.count - 1
(0..<separatorCount).forEach { index in
guard let subview = subviews[safe: index] else { return }
let separatorView = createVerticalSeparatorView(color: color)
addSubview(separatorView)
NSLayoutConstraint.activate([
separatorView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: heightRatio),
separatorView.trailingAnchor.constraint(equalTo: subview.trailingAnchor, constant: spacing),
separatorView.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
}
๋ณ์ ์ ๋ํ๋ผ StarImageView
์ข
๋ฅ๋ฅผ filled/halfFilled/empty๋ก ๊ตฌ๋ถํ๊ณ ,
ํ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ํ์ํ ๊ฐ์๋งํผ ์ข
๋ฅ๋ณ ImageView๋ฅผ ์์ฑํ์ฌ StackView์ ์ถ๊ฐํ๋๋ก ํ์ต๋๋ค. ์๋ ๋ก์ง๊ณผ ๊ฐ์ด ์ ์๋ฅผ ๋บ ์์์ ์์ญ์ ๊ฐ์ด 0.5 ์ด์์ด๋ฉด halfFilledStar
์ด๋ฏธ์ง๋ทฐ๊ฐ 1๊ฐ ์์ฑ๋๋๋ก ํ์ต๋๋ค.
private func configureStarImageView(with rating: Double) -> [StarImageView] {
let starCountByKind = calculateStarCountByKind(with: rating)
var starImageViews = [StarImageView]()
starImageViews += (0..<starCountByKind.filled).map { _ in StarImageView(kind: .filled) }
starImageViews += (0..<starCountByKind.halfFilled).map { _ in StarImageView(kind: .halfFilled) }
starImageViews += (0..<starCountByKind.empty).map { _ in StarImageView(kind: .empty) }
return starImageViews
}
private func calculateStarCountByKind(with rating: Double) -> (filled: Int, halfFilled: Int, empty: Int) {
let filledStarCount = Int(rating)
let remainder = rating.truncatingRemainder(dividingBy: 1)
let halfFilledStarCount = remainder >= 0.5 ? 1 : 0
let emptyStarCount = maxStarCount - filledStarCount - halfFilledStarCount
return (filledStarCount, halfFilledStarCount, emptyStarCount)
}
์์ธํ๋ฉด ๋ฐ ์คํฌ๋ฆฐ์ท ํ๋ ํ๋ฉด์์ CollectionView์ Compositional Layout
์ ํ์ฉํ์ฌ Cell์ ๋ฐฐ์นํ์ต๋๋ค. ๋ ํ๋ฉด์ Cell ๊ตฌ์ฑ์ด ๊ฐ์ผ๋ฏ๋ก ๋ชจ๋ ScreenshotCell์ ์ฌ์ฉํ์ต๋๋ค.
Section์ orthogonalScrollingBehavior๋ ๊ฐ๊ฐ groupPaging
, groupPagingCentered
์ผ๋ก ์ค์ ํ์ฌ AppStore์ ๋์ผํ๊ฒ Pagination์ ๊ตฌํํ์ต๋๋ค.
๊ฒ์ ํ๋ฉด | ์์ธ ํ๋ฉด | ๊ฐ๋ก/์ธ๋ก ๋ชจ๋ ๋์ | ๋คํฌ/๋ผ์ด๋ ๋ชจ๋ ๋์ |
---|---|---|---|
![]() ![]() |
![]() ![]() |
![]() ![]() |
![]() ![]() |
- ์ถ๊ฐ ๊ตฌํํ ๋ด์ฉ์ ์ฃผ์์ผ๋ก ๋จ๊ฒจ๋์์ต๋๋ค.
- ๊ณต์ ๋ฒํผ์ ๊ตฌํํ ์์ ์ ๋๋ค.
- ์์ธํ๋ฉด์ ์คํฌ๋ฆฐ์ท์์ ํญํ ํญ๋ชฉ์ ์คํฌ๋ฆฐ์ท ํ๋ ํ๋ฉด์์ ๋ํ๋๋๋ก ๊ตฌํํ ์์ ์ ๋๋ค.
- ์คํฌ๋ฆฐ์ท ํ๋ ํ๋ฉด์ modal๋ก ๋์ฐ๋๋ก ์์ ํ ์์ ์ ๋๋ค.
- SummaryScrollView์์ ์นดํ ๊ณ ๋ฆฌ ์ข ๋ฅ์ ๋ฐ๋ผ ImageView๋ฅผ ๋ณ๊ฒฝํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ ์์ ์ ๋๋ค.
- Localization์ ๊ตฌํํ ์์ ์ ๋๋ค. (SummaryScrollView์ ์ธ์ด, ์นดํ ๊ณ ๋ฆฌ๋ช , InfoTableView์ ์ธ์ด ํญ๋ชฉ ๋ฑ)
- Quick/Numble์ ํ์ฉํ์ฌ ViewModel ํ ์คํธ ์ฝ๋๋ฅผ ์งํํ ์์ ์ ๋๋ค.
- ๋ฆฌ๋ทฐ ๋ ธํธ : Lookup API ํน์ฑ์ ์๋ฒ ๋ฐ์ดํฐ ์ค ํ๊ฐ ๊ฐ์ (userRatingCount), ์ฑ ํฌ๊ธฐ (fileSize) ๋ฑ์ด ์ค์ AppStore์์ ํ์ธ๋๋ ๊ฐ๊ณผ ์ผ์นํ์ง ์๋ ๊ฒฝ์ฐ๊ฐ ์์ต๋๋ค.