์ํ์งํฅ์์ํ์ Daum ๊ฒ์ API๋ฅผ ํตํด ๋ ์ง๋ณ ๋ฐ์ค์คํผ์ค, ์ํ ์์ธ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ฑ์ ๋๋ค.
ํ๋ก์ ํธ ๊ธฐ๊ฐ : 23/07/24~23/08/18
- ํ์ ์๊ฐ
- ํ์ ๋ผ์ธ
- ์๊ฐํ ๊ตฌ์กฐ
- ์คํ ํ๋ฉด
- ํต์ฌ ๊ฒฝํ
- ํธ๋ฌ๋ธ ์ํ
- ์ฐธ๊ณ ์๋ฃ
- ํ ํ๊ณ
Yetti | maxhyunm |
๋ ์ง | ๋ด์ฉ |
---|---|
2023.07.24. | json ํ์ผ ํ์ฑ์ ์ํ BoxOfficeEntity ํ์ ์์ฑ, ๋์ฝ๋ฉ ์ ๋ํ ์คํธ ์งํ |
2023.07.25. | DecodingManager์์ ๋์ฝ๋ฉ๋ง ์งํํ ์ ์๋๋ก ๊ธฐ๋ฅ ๋ถ๋ฆฌ |
2023.07.26. | NetworkingManager, BoxOfficeError, MoviewDetailEntity ํ์ ์์ฑ |
2023.07.27. | extension์ผ๋ก ์ค์ฒฉํ์ ๋ถ๋ฆฌ, URLNamespace ํ์ ์์ฑ |
2023.07.31. | ํ๋ก์ ํธ ์งํ์ ์ํ ๊ฐ์ธ ๊ณต๋ถ์๊ฐ |
2023.08.01. | ํ๋ก์ ํธ ์งํ์ ์ํ ๊ฐ์ธ ๊ณต๋ถ์๊ฐ |
2023.08.02. | CollectionView์ธํ , BoxOfficeRankingCell ์์ฑ ๋ฐ ์ ๊ตฌ์ฑ ์ธํ , DiffableDataSource ์ธํ ๋ฐ ์ฐ๊ฒฐ, ๋ญํน ์ฆ๊ฐ๋ถ AttributedString ์ฒ๋ฆฌ, collectionView์ refreshControl ์ถ๊ฐ |
2023.08.07. | ๋ฆฌ๋ทฐ์ ๋ฐ๋ฅธ ๋ฆฌํฉํ ๋ง ์งํ |
2023.08.08. | DaumImageEntity ํ์ผ ์ถ๊ฐ ๋ฐ DAUM_API_KEY ์ถ๊ฐ, MovieDetailViewController ํ์ผ ์ถ๊ฐ ๋ฐ view ์ ํ ๋ฉ์๋ ์ถ๊ฐ, MoviewDetailViewController์ MoviewDetail ๋คํธ์ํฌ ์ฐ๊ฒฐ |
2023.08.09. | ํ๋ก์ ํธ ์งํ์ ์ํ ๊ฐ์ธ ๊ณต๋ถ์๊ฐ |
2023.08.10. | ์คํ๋ทฐ์ text์ค์ ๋ฉ์๋ ๊ธฐ๋ฅ ๋ถ๋ฆฌ ๋ฐ Namespace ์์ฑ, MovieDetailView ๋ก๋ฉํ๋ฉด ์์ |
2023.08.11. | README ์์ฑ |
2023.08.14. | url ํธ์ถ ์คํจ์ completion์ผ๋ก ์๋ฌ์ฒ๋ฆฌ ์ถ๊ฐ, MovieDetailViewController์์ isDataLoading, isImageLoading ํธ์ถ ์์น ๋ณ๊ฒฝ |
2023.08.15. | ํ๋ก์ ํธ ์งํ์ ์ํ ๊ฐ์ธ ๊ณต๋ถ์๊ฐ |
2023.08.16. | ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ ๋ฒํผ ์ถ๊ฐ BoxOfficeRankingIconCell ํ์ ์์ฑ, ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ์ CollectionView์ ๋ ์ด์์ ์ค์ ์ ๋ฐ๊พธ๋ ๊ธฐ๋ฅ ๊ตฌํ, ํ ์คํธ์ ๋ค์ด๋๋ฏน ํ์ ์ ์ฉ ๋ฐ auto layout ์์ |
2023.08.17. | APIKey ๊ฒ์ฆ๊ณผ์ ๋ฐ ์๋ฌ์ฒ๋ฆฌ ์ถ๊ฐ, ๋ ์ง ๋ณ๊ฒฝ์ ์ปฌ๋ ์ ๋ทฐ ๋ ์ด์์ ํ์ด์ง๋ ์ค๋ฅ ์์ |
2023.08.18. | README ์์ฑ |
โโโ BoxOffice
โย ย โโโ Extension
โย ย โย ย โโโ DateFormatter+.swift
โย ย โย ย โโโ String+.swift
โย ย โโโ Model
โย ย โย ย โโโ BoxOfficeEntity.swift
โย ย โย ย โโโ MovieDetailEntity.swift
โย ย โย ย โโโ DaumImageEntity.swift
โย ย โย ย โโโ DecodingManager.swift
โย ย โย ย โโโ Error.swift
โย ย โย ย โโโ NetworkingManager.swift
โย ย โย ย โโโ URLSessionProtocol.swift
โย ย โโโView
โย ย โ โโโ BoxOfficeRankingListCell.swift
โย ย โ โโโ BoxOfficeRankingIconCell.swift
โย ย โ โโโ MovieDetailStackView.swift
โย ย โโโ Controller
โย ย โย ย โโโ BoxOfficeViewController.swift
โย ย โย ย โโโ CalendarViewController.swift
โย ย โย ย โโโ MovieDetailViewController.swift
โย ย โโโ Resource
โย ย โย ย โโโ AppDelegate.swift
โย ย โย ย โโโ SceneDelegate.swift
โย ย โย ย โโโ NetworkConfiguration.swift
โย ย โย ย โโโ Assets.xcassets
โย ย โย ย โโโ box_office_sample.json
โย ย โโโInfo.plist
โโโ BoxOffice.xcodeproj
โโโ BoxOfficeTests
โย ย โโโ BoxOffice.xctestplan
โย ย โโโ BoxOfficeDecodingTests.swift
โย ย โโโ BoxOfficeNetworkingTest.swift
โย ย โโโ TestDouble.swift
โโโ README.md
์ํ ์์ธ์ ๋ณด | ๋ ์ง ๋ณ๊ฒฝ | ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ |
---|---|---|
ํ๊ฒฝ ํ์ผ์ ํ์ฉํด ์๊ฒฉ ์ ์ฅ์์ ๊ณต์ ๋์ง ์์์ผ ํ๋ key ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ์์ต๋๋ค.
Nested Type
์ ํ์ฉํ์ฌ ์ฌ๋ฌ ๋จ๊ณ๋ก ์ค์ฒฉ๋ ํํ์ json์ ํ์ฑํ ์ ์๋๋ก ํ์๊ณ , CodingKeys
๋ฅผ ํ์ฉํด ์ดํดํ๊ธฐ ์ด๋ ค์ด ํ๋ผ๋ฏธํฐ๋ช
์ ๋ณ๊ฒฝํ์์ต๋๋ค.
์์ธ์ฝ๋
extension BoxOfficeEntity {
struct BoxOfficeResult: Decodable {
let boxOfficeType, showRange: String
let dailyBoxOfficeList: [DailyBoxOffice]
enum CodingKeys: String, CodingKey {
case boxOfficeType = "boxofficeType"
case showRange, dailyBoxOfficeList
}
}
}
๋ค์ํ ํ์
์ Entity๋ฅผ ๋ฐํํด์ผ ํ๋ DecodingManager
์ ๋ฉ์๋๋ฅผ Generic
์ผ๋ก ๊ตฌํํ์์ต๋๋ค.
์์ธ์ฝ๋
func decode<T: Decodable>(_ data: Data?) throws -> T {
guard let data = data,
let decodedData = try? decoder.decode(T.self, from: data) else {
throw DecodingError.decodingFailure
}
return decodedData
}
Modern Collection View
๋ฅผ ํตํด ๋ฐ์ค์คํผ์ค ๋ญํน ๋ฆฌ์คํธ๋ฅผ ๊ตฌํํ๊ธฐ ์ํ์ฌ Diffable Data Source
์ Collection View List Cell
๋ฅผ ํ์ฉํ์์ต๋๋ค.
์์ธ์ฝ๋
private let collectionView: UICollectionView = {
let configuration = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
...
}
private var dataSource: UICollectionViewDiffableDataSource<NetworkNamespace, BoxOfficeEntity.BoxOfficeResult.DailyBoxOffice>?
...
UICalendarView
๋ฅผ ํ์ฉํด Calendar
๋ฅผ ๊ตฌํํ๊ณ ๊ณผ๊ฑฐ ๋ ์ง์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์๋๋ก ํ์ฉํ์์ต๋๋ค.
์์ธ์ฝ๋
private let calendarView: UICalendarView = {
var calendarView = UICalendarView()
calendarView.translatesAutoresizingMaskIntoConstraints = false
calendarView.backgroundColor = .systemBackground
let endDate = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date()
let calendarViewDateRange = DateInterval(start: Date(timeIntervalSince1970: 0), end: endDate)
calendarView.availableDateRange = calendarViewDateRange
return calendarView
}()
๋ฐ์ดํฐ fetch ์ํ์ ๋ฐ๋ผ UIActivityIndicatorView
์ ์ํ๊ฐ์ ๋ณ๊ฒฝํ์ฌ ๋ก๋ฉ ๋งํฌ๊ฐ ํ์ฑํ/๋นํ์ฑํ ๋๋๋ก ๊ตฌํํ์์ต๋๋ค.
์์ธ์ฝ๋
private let indicatorView: UIActivityIndicatorView = {
let indicatorView = UIActivityIndicatorView()
indicatorView.style = .large
indicatorView.translatesAutoresizingMaskIntoConstraints = false
return indicatorView
}()
private var isLoading: Bool = true {
willSet(newValue) {
if newValue == true {
indicatorView.isHidden = false
indicatorView.startAnimating()
} else {
indicatorView.isHidden = true
indicatorView.stopAnimating()
}
}
}
Collection View
์ UIRefreshColtrol
๊ฐ์ฒด๋ฅผ ์ถ๊ฐํ์ฌ, ์๋๋ก ๋น๊ฒผ์ ๋ ์๋ก๊ณ ์นจ์ ์งํํ ์ ์๋๋ก ํ์์ต๋๋ค.
์์ธ์ฝ๋
collectionView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
UIToolBar
์ flexibleSpace
๋ฅผ ํ์ฉํ์ฌ ํ๋ฉด ํ๋จ ๋ฒํผ์ ๊ตฌํํ์์ต๋๋ค.
์์ธ์ฝ๋
let modeChangeButton = UIBarButtonItem(title: "ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ", style: .plain, target: self, action: #selector(hitChangeModeButton))
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
...
self.toolbarItems = [flexibleSpace, modeChangeButton, flexibleSpace]
ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ์ UIAlertController
์ actionSheet
์คํ์ผ๋ก ์์ด์ฝ ๋ชจ๋์ ๋ฆฌ์คํธ ๋ชจ๋ ํ๋ฉด์ ์ ํ์ ์ผ๋ก ์ ์ฉํ ์ ์๋๋ก ๊ตฌํํ์์ต๋๋ค.
์์ธ์ฝ๋
@objc func hitChangeModeButton() {
let mode: String = isListMode == true ? "์์ด์ฝ" : "๋ฆฌ์คํธ"
let alert = UIAlertController(title: "ํ๋ฉด ๋ชจ๋ ๋ณ๊ฒฝ", message: nil, preferredStyle: .actionSheet)
let modeChangeAction = UIAlertAction(title: mode, style: .default) { _ in
self.isListMode.toggle()
}
let cancelAction = UIAlertAction(title: "์ทจ์", style: .cancel)
alert.addAction(modeChangeAction)
alert.addAction(cancelAction)
present(alert, animated: true)
}
ํ๋์ ๋ ์ด๋ธ ์์์ ์ฌ๋ฌ ๊ฐ์ง ์์์ ํ์ํ๊ธฐ ์ํ์ฌ Attributed String
์ ํ์ฉํ์์ต๋๋ค.
์์ธ์ฝ๋
attributedString.addAttribute(.foregroundColor, value: UIColor.systemRed, range: (fixedIntensity as NSString).range(of: "โฒ"))
preferredFont
๋ฅผ ํ์ฉํ์ฌ ํฐํธ์ DynamicType
์ ์ ์ฉํ์๊ณ , adjustsFontSizeToFitWidth
์ค์ ์ ํตํด ๊ฐ๋ก ๋๋น์ ๋ง์ถฐ ํ
์คํธ ํฌ๊ธฐ๋ฅผ ์๋ ์กฐ์ ํ ์ ์๋๋ก ๊ตฌํํ์์ต๋๋ค.
์์ธ์ฝ๋
private let audienceLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .body)
label.translatesAutoresizingMaskIntoConstraints = false
label.adjustsFontForContentSizeCategory = true
label.adjustsFontSizeToFitWidth = true
return label
}()
๐จ ๋ฌธ์ ์
๊ธฐ์กด์๋ dataTask
๋ฉ์๋์ URL
ํ์
์ ์ ๋ฌํ์ฌ ๋คํธ์ํฌ ์ฐ๊ฒฐ์ ์งํํ๋๋ก ๊ตฌํํ์์ต๋๋ค. ํ์ง๋ง ์ด๋ฒ์ Daum API
๊ฐ ์ถ๊ฐ๋๋ฉด์ Authorization
ํค๋๋ฅผ ์ถ๊ฐํด์ผ ํ๊ธฐ ๋๋ฌธ์ ํด๋น ์ ๋ณด๋ฅผ ์ด๋ป๊ฒ ์ ๋ฌํ ์ง ๊ณ ๋ฏผํ์์ต๋๋ค.
๐ก ํด๊ฒฐ ๋ฐฉ๋ฒ
URLRequest
๋ฅผ ํ์ฉํ๋ฉด setValue
๋ฅผ ํตํด ํค๋ ์ ๋ณด๋ฅผ ์ถ๊ฐํ ์ ์์์ ํ์ธํ์์ต๋๋ค. ์ด์ ๋ฐ๋ผ ๊ธฐ์กด ํ
์คํธ๋๋ธ ๋ฐ ํ
์คํธ์ฝ๋ ๋ชจ๋ URLRequest
์ ๋ง๊ฒ ์์ ์ ์งํํ์์ต๋๋ค.
var request = URLRequest(url: url)
request.setValue("KakaoAK \(NetworkNamespace.daumApiKey)", forHTTPHeaderField: "Authorization")
๋ํ Daum API
์ ์ ๋ฌํ ์ฟผ๋ฆฌ ๋ด์ฉ์๋ ๋์ด์ฐ๊ธฐ๊ฐ ์ถ๊ฐ๋์ด ์์ด, URL
์ ๋ถ์ฌ์ ๋ณด๋ด๊ธฐ๋ณด๋ค๋ URLComponents
๋ฅผ ์ฌ์ฉํด ํ์ํ ์ฟผ๋ฆฌ ์์ดํ
์ ์ถ๊ฐํ๋ ๊ฒ ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์์ต๋๋ค.
ํด๋น ๋ด์ฉ์ ์๋์ ๊ฐ์ด ๊ตฌํํ์์ต๋๋ค.
var urlComponents = URLComponents(string: NetworkNamespace.daumImage.url)
urlComponents?.queryItems = [URLQueryItem(name: "query", value: "\(movieName) ์ํ ํฌ์คํฐ")]
๐จ ๋ฌธ์ ์
NetworkingManager
์ load()
๋ฉ์๋๋ฅผ ํธ์ถํ ์์น์์ dataTask
๋ฅผ ํตํด ๋ฐ์ ์จ ๋ฐ์ดํฐ๋ฅผ ํ์ฉํ ์ ์๋ ๋ฐฉ๋ฒ์ ๋ํด ๊ณ ๋ฏผ์ด ์์์ต๋๋ค.
์ฒ์์๋ ๋ ํ์
์ ๋ธ๋ฆฌ๊ฒ์ดํธ๋ก ์ฐ๊ฒฐํ์ฌ ์ ๋ฌํ๋ ๋ฑ์ ๋ฐฉ๋ฒ์ ์๊ฐํ์ต๋๋ค. ํ์ง๋ง ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์ํด ๋คํธ์ํน์ ์ฌ์ฉํ๋ ๋ชจ๋ ํ์
์ ๋ธ๋ฆฌ๊ฒ์ดํธ๋ก ์ฐ๊ฒฐํ๋ ๊ฒ์ ๊ถ์ฅ๋๋ ๋ฐฉ์๋ ์๋๊ณ , ํจ์จ์ ์ด์ง ๋ชปํ ๊ฒ ๊ฐ์์ต๋๋ค.
๐ก ํด๊ฒฐ ๋ฐฉ๋ฒ
@escaping
ํด๋ก์ ์ Result
ํ์
์ ํ์ฉํ์ฌ ํด๊ฒฐํ์์ต๋๋ค. Result
๋ ์ฑ๊ณต/์คํจ ๋ ๊ฐ์ง ๊ฐ๋ฅ์ฑ์ ๋ํ ๋ฐ์ดํฐํ์
์ ๋ฐ๋ก ์ง์ ํด์ค ์ ์์ด CompletionHandler
์ ํ์ฉํ๊ธฐ์ ์ ์ ํ๋ค๊ณ ํ๋จํ์ต๋๋ค.
func load(_ urlString: String, completion: @escaping (Result<Data, BoxOfficeError>) -> Void) {
guard let url = URL(string: urlString) else {
return
}
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
completion(.failure(BoxOfficeError.connectionFailure))
return
}
...
๐จ ๋ฌธ์ ์
API๋ฅผ ๋ฐ์์์ผ ํ๋ ๋๋ฉ์ธ์ด https
๊ฐ ์๋ http
๋ฅผ ํ์ฉํ๊ณ ์์ด ๋คํธ์ํฌ ์ฐ๊ฒฐ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ์์ต๋๋ค.
๐ก ํด๊ฒฐ ๋ฐฉ๋ฒ
ํด๋น ๋๋ฉ์ธ ๋ฐ ํ์ ๋๋ฉ์ธ ์ ๋ณด๋ฅผ ATS์ Exception Domains
๋ก ์ถ๊ฐํ์ฌ ์ ์์ ์ผ๋ก ๋คํธ์ํน์ด ๊ฐ๋ฅํ๋๋ก ๊ตฌํํ์์ต๋๋ค.
๐จ ๋ฌธ์ ์
๋คํธ์ํน์ ํตํด ๋ฐ์ ์จ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ณผ์ ์์ ์ฑ๊ณตํ ๊ฒฝ์ฐ์๋ง ๋ก๋ฉ์ ๋๋ด๊ณ ๋น ์ ธ๋๊ฐ ์ ์๋๋ก ์ฒ๋ฆฌํ์๋๋, ์๋ฌ๊ฐ ๋ฌ์ ๋๋ ์๋์ ๊ฐ์ด ๊ณ์ ๋ก๋ฉ์ด ๋์๊ฐ๊ณ ์๋ ๊ฒ์ฒ๋ผ ๋ณด์ด๋ ๊ฒ์ ํ์ธํ์ต๋๋ค.
๋ํ RefreshControl์ ์ข
๋ฃ ์ฒ๋ฆฌ๋ฅผ ์๋์ ๊ฐ์ด ์งํํ๋, ๋ฐ์ดํฐ ๋ก๋ฉ์ด ์๋ฃ๋๊ธฐ ์ ์ ๋น๋๊ธฐ๋ก ๋ค์ ํธ์ถ์ด ์งํ๋์ด ์๋ก๊ณ ์นจ์ด ๋ฐ๋ก ๋๋๋ฒ๋ฆฌ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.
@objc private func refresh() {
passFetchedData()
refreshControl.endRefreshing()
}
๐ก ํด๊ฒฐ ๋ฐฉ๋ฒ
๋คํธ์ํน๊ณผ ๋์ฝ๋ฉ ์ฌ๋ถ์ ์๊ด์์ด ๋ก๋ฉ๊ณผ ์๋ก๊ณ ์นจ์ ์๋ฃํ ์ ์๋๋ก ํด๋น ํธ์ถ๋ถ๋ฅผ switch๋ฌธ ๋ฐ์ผ๋ก ์ด๋ํ์์ผ๋ฉฐ,
isLoading ๋ณ์๊ฐ false์ผ ๋ endRefreshing๋ ํธ์ถํ ์ ์๋๋ก ๋ณ๊ฒฝํด์ฃผ์์ต๋๋ค.
networkingManager?.load(url) { [weak self] (result: Result<Data, NetworkingError>) in
switch result {
case .success(let data):
...
case .failure(let error):
...
}
DispatchQueue.main.async {
self?.isLoading = false
self?.refreshControl.endRefreshing()
}
๐จ ๋ฌธ์ ์
Collection View์์ ์
์ ์ฌ์ฌ์ฉํ๋ฉด์, ๊ฒ์ ์์ผ๋ก ๋ค์ด๊ฐ์ผ ํ๋ ์์ ํ
์คํธ๊ฐ ๋นจ๊ฐ์์ผ๋ก ์๋ชป ๋ค์ด๊ฐ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
๐ก ํด๊ฒฐ ๋ฐฉ๋ฒ
PrepareForReuse()
๋ฅผ ํตํด ์๋์ ๊ฐ์ด ํฐํธ ์์์ ์ด๊ธฐํํด ์ฃผ์์ต๋๋ค.
override func prepareForReuse() {
rankIntensityLabel.textColor = .black
}
๐จ ๋ฌธ์ ์
์ธํฐ๋ท ์ฐ๊ฒฐ์ด ์๋ ์ํ์์ ๋คํธ์ํฌ ํต์ ์ ํ
์คํธํ๊ธฐ ์ํด Test Double
์ ์์ฑํ์์ต๋๋ค. ์ด ๊ณผ์ ์์ ํ
์คํธ์ฉ Stub Session
๊ณผ ์ค์ Session
์ฌ์ด์ ํธํ์ด ๊ฐ๋ฅํ๋๋ก ํ๊ธฐ ์ํด URLSessionProtocol
์ ๊ตฌํํ์๋๋ฐ, URLSession
์์ ์ด๋ฅผ ์์ํ๋ ค ํ๋ ์๋์ ๊ฐ์ ๊ฒฝ๊ณ ๊ฐ ๋ฐ์ํ์์ต๋๋ค.
๐ก ํด๊ฒฐ ๋ฐฉ๋ฒ
CompletionHandler typealias
์ @Sendable
์ ์ฑํํ์ฌ ํด๊ฒฐํ์์ต๋๋ค.
typealias CompletionHandler = @Sendable (Data?, URLResponse?, Error?) -> Void
๐จ ๋ฌธ์ ์
์ธ ๊ฐ์ง ๋ค๋ฅธ API๋ฅผ ํธ์ถํ๋ ํ๋ก๊ทธ๋จ์ด๋ค ๋ณด๋, ๊ฐ API ๋ด์ฉ์ ๋ฐ๋ผ ๋งค๋ฒ ์๋ก์ด ํ
์คํธ ํ๊ฒฝ์ ๋ง๋ค์ด์ฃผ์ด์ผ ํด์ ๋ฒ๊ฑฐ๋ก์ ์ต๋๋ค.
๐ก ํด๊ฒฐ ๋ฐฉ๋ฒ
์๋์ ๊ฐ์ด ํ
์คํธ ํ์
์ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ๋ฐ์ ๊ฐ ํ์
์ ๋ง๋ ํ
์คํธํ๊ฒฝ์ ๊ตฌ์ฑํ ์ ์๋๋ก ๊ตฌํํ์์ต๋๋ค.
func setUpSUT(isSuccess: Bool, apiType: NetworkNamespace) {
...
switch apiType {
case .boxOffice:
urlString = String(format: NetworkNamespace.boxOffice.url, NetworkNamespace.apiKey, "20230801")
asset = "box_office_sample"
case .movieDetail:
urlString = String(format: NetworkNamespace.movieDetail.url, NetworkNamespace.apiKey, "20230801")
asset = "movie_detail_sample"
case .daumImage:
urlString = String(format: NetworkNamespace.daumImage.url)
asset = "daum_image_sample"
}
...
if isSuccess {
...
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
...
} else {
let response = HTTPURLResponse(url: url, statusCode: 400, httpVersion: nil, headerFields: nil)
...
}
}
- URLSession ๊ณต์๋ฌธ์ ๐
- Fetching Website Data into Memory ๊ณต์๋ฌธ์ ๐
- UICollectionView ๊ณต์๋ฌธ์ ๐
- UICollectionViewListCell ๊ณต์๋ฌธ์ ๐
- UICollectionViewDiffableDataSource ๊ณต์๋ฌธ์ ๐
- URLRequest ๊ณต์๋ฌธ์ ๐
- URLComponents ๊ณต์๋ฌธ์ ๐
- AttributedString ๊ณต์๋ฌธ์ ๐
- UIRefreshControl ๊ณต์๋ฌธ์ ๐
- UIActivityIndicatorView ๊ณต์๋ฌธ์ ๐
- UICalendarView ๊ณต์๋ฌธ์ ๐
- UIAlertController ๊ณต์๋ฌธ์ ๐
- ์ผ๊ณฐ ๋ท๋ท - Unit Test