yeahg-dev / chajabwa Goto Github PK
View Code? Open in Web Editor NEWFind Apps in global App Store! ๐
License: MIT License
Find Apps in global App Store! ๐
License: MIT License
extension APIService {
func execute<T: APIRequest>(request: T) async throws -> T.APIResponse {
guard let urlRequest = request.urlRequest else {
throw APIError.invalidURL
}
let (data, response) = try await session.data(for: urlRequest)
guard let response = response as? HTTPURLResponse,
// ์ํ๋ json๋ฐ์ดํฐ๊ฐ ์๋ xml๋ฐ์ดํฐ๊ฐ 200์ผ๋ก ๋์ฐฉ
(200...299).contains(response.statusCode) else {
print("\(T.self) failed to receive success response(status code: 200-299)")
throw APIError.HTTPResponseFailure
}
guard let parsedData: T.APIResponse = parse(response: data) else {
print("parsing failed. type: \(T.APIResponse.Type.self) ")
throw APIError.invalidParsedData
}
return parsedData
}
}
application/json;charset=UTF-8
์ด ์๋๋ฉด ์๋ฌ๋ฅผ ๋ฐํstruct CountryCodeAPIService: APIService {
var session: URLSession = URLSession(configuration: .default)
func requestAllCountryCode() async throws -> [CountryCode] {
let request = CountryCodeListRequest(pageNo: 1, numOfRows: 240)
let countryCodeList: CountryCodeList = try await getResponse(request: request)
return countryCodeList.data
}
func getResponse<T: APIRequest>(request: T) async throws -> T.APIResponse {
guard let urlRequest = request.urlRequest else {
throw APIError.invalidURL
}
let (data, response) = try await session.data(for: urlRequest)
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode) else {
print("\(T.self) failed to receive success response(status code: 200-299)")
throw APIError.HTTPResponseFailure
}
guard let contentType = response.value(forHTTPHeaderField: "Content-Type"),
contentType == "application/json;charset=UTF-8" else {
print("\(T.self) : response Content-Type is \(String(describing: response.value(forHTTPHeaderField: "Content-Type")))")
throw APIError.HTTPResponseFailure
}
guard let parsedData: T.APIResponse = parse(response: data) else {
print("parsing failed. type: \(T.APIResponse.Type.self) ")
throw APIError.invalidParsedData
}
return parsedData
}
}
getResponse
๋ฉ์๋๋ ํ๋กํ ์ฝ ๊ธฐ๋ณธ ๊ตฌํ์ผ๋ก ๋ค๋ฅธ Service์์๋ ์ฌ์ฉ๋๋ฏ๋ก, ๋จ์ผ์ฑ
์์์น๊ณผ ๊ฐ๋ฐฉํ์์์น์ ์๋ฐํ ์ฝ๋์getResponse
๋ฅผ ํ๋กํ ์ฝ์ ์ ์, ์ถ์ํํ์ฌ ํ์ฅ์ ์ฉ์ดํ๋๋ก ๋ฆฌํฉํฐ๋งprotocol APIService {
var session: URLSession { get set }
func getResponse<T: APIRequest>(request: T) async throws -> T.APIResponse
}
protocol CombineAPIService: APIService {
func getResponse<T: APIRequest>(
request: T)
-> AnyPublisher<T.APIResponse, Error>
}
searchBarTextDidBeginEditing
์ showRecentSearchKeywordTableView
๋ฅผ ํธ์ถํ์ฌ ํด๊ฒฐํ๋คfunc searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchController.showsSearchResultsController = true
searchAppResultsController.showRecentSearchKeywordTableView()
}
scrollToTop
๋ด๋ถ์์ ์ฒซ๋ฒ์งธ cell๊น์ง ์ด๋ํ๊ธฐ ๋๋ฌธ์ header๋ ๋ณด์ฌ์ฃผ์ง ๋ชปํ๋ค.tableView
์ bounds.origin.y
๋ฅผ searchBar์ ๋์ด๋ก ์ง์ ํด์ฃผ์ด headerView์ ์กด์ฌ ์ ๋ฌด์ ์๊ด ์์ด tableView๋ฅผ ๋ชจ๋ ๋ณด์ฌ์ฃผ๋๋ก ์์ ํ๋ค. func scrollToTop() {
guard let searchBarHeight = navigationItem.searchController?.searchBar.bounds.height else {
return
}
tableView.bounds.origin.y = searchBarHeight
}
AppSerchAPIRequest
ํ์
๊ตฌํScopeBar
๊ตฌํAppSearchResultListView
๊ตฌํAppSearchTableViewCell
๊ตฌํSettingView
๊ตฌํ
localizedString
์ฌ์ฉloacalization
๊ธฐ๋ฅ ์ฌ์ฉDateFormatter
์ฌ์ฉํ์ฌ region์ ์๋ง์ ๋ ์ง ํฌ๋งท ํ์CountryCodeAPIService
๋ฅผ ์ด์ฉํด์ API call์ด ์ ํ๋์ด์ผํจ.// ๊ธฐ์กด ์ฝ๋
final class SearchCoordinator: NSObject, Coordinator {
var childCoordinator = [Coordinator]()
var launchScreenViewController: LaunchScreenViewController
var navigationController: UINavigationController!
init(rootViewController: LaunchScreenViewController) {
self.launchScreenViewController = rootViewController
}
func start() {
Task {
do {
// APIcall ์์ฒญ
try await AppSearchingConfiguration().downloadCountryCode()
await MainActor.run {
// ์ฑ๊ณตํ๋ฉด SearchVC present
_ = presentSearchViewController()
}
} catch {
await MainActor.run {
// ์คํจํ๋ฉด SearchVC Present, alert Present
let searchVC = presentSearchViewController()
searchVC.presentCountryCodeDownloadErrorAlert()
}
}
}
}
...
}
AppSearchingConfiguration
์ ์ฑ ๊ฒ์ ์ค์ ์ ๊ด๋ฆฌํ๋ ์ ์ค์ผ์ด์ค์ธ๋ฐ ์ฑ์ ์ด๊ธฐ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ก๋ํ๋ ์ญํ ๊น์ง ๋งก๊ฒ๋จ(SRP ์๋ฐ)AppOrganizer
์ ์CountryCodeAPIService
๋ฅผ ์ด์ฉํด ๊ตญ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ก๋ ๋ฐ์final class AppOrganizer {
private let countryCodeAPIService = CountryCodeAPIService()
func prepare(didEnd completion: @escaping (() -> Void)) {
countryCodeAPIService.fetchCountryCodes(completion: completion)
}
}
LoadingViewController
๊ตฌํactivity Indicator
๋ฅผ ์ฌ์ฉํด ์งํ ์ค์์ ์ฌ์ฉ์์๊ฒ ์๋ฆผfinal class CountryCodeAPIService: NSObject {
private let session: URLSession = URLSession(
configuration: .default,
delegate: nil,
delegateQueue: nil)
func fetchCountryCodes(completion: @escaping (() -> Void)) {
guard let request = CountryCodeListRequest(pageNo: 1, numOfRows: 240).urlRequest else {
return
}
let downloadTask = session.dataTask(with: request) { [weak self] ( data, urlResponse, error) in
if error != nil {
// ๋คํธ์ํฌ ์ฐ๊ฒฐ ์์์ ์๋ฆฌ๋ Alert ํ์
NetworkMonitor.shared.handleNetworkError {
DispatchQueue.global().async { [weak self] in
// `์ฌ์๋`๋ฒํผ์ ๋ฐ์ธ๋ฉ๋๋ ์ด๋ฒคํธ
self?.fetchCountryCodes(completion: completion)
}
}
return
}
guard let httpResponse = urlResponse as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("\(String(describing: self)) : failed to receive success response(status code: 200-299)")
return
}
guard let contentType = httpResponse.value(forHTTPHeaderField: "Content-Type"),
contentType == "application/json;charset=UTF-8" else {
print("\(String(describing: self)) : response Content-Type is \(String(describing: httpResponse.value(forHTTPHeaderField: "Content-Type")))")
return
}
guard let data,
let parsedData = try? JSONDecoder().decode(CountryCodeList.self, from: data) else {
print("parsing failed. type: \(CountryCodeList.self) ")
return
}
self?.appendCountries(countryCodeList: parsedData)
completion()
}
downloadTask.resume()
}
}
mainCoordinator.start()
ํธ์ถloadingViewController
๋ฅผ ๋ณด์ฌ์ฃผ๊ณ , AppOrganizer
์ prepare
๋ฅผ ํธ์ถprepare
๊ฐ ์ ์์ ์ผ๋ก ์๋ฃ๋์์ ๋ mainCoordinator
๋ฅผ ์์ํ์ฌ ์ฒซ๋ฒ์งธ ๋ทฐ๋ฅผ ๋ณด์ฌ์ฃผ๋๋ก ํจ// SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let navigationController = UINavigationController()
window = UIWindow(windowScene: windowScene)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
let mainCooordinator = SearchCoordinator(rootViewController: navigationController)
mainCoordinator = mainCooordinator
let loadingView = AppLoadingViewController()
loadingView.modalPresentationStyle = .fullScreen
navigationController.present(loadingView, animated: false)
appOrganizer.prepare {
DispatchQueue.main.async {
loadingView.dismiss(animated: false)
mainCooordinator.start()
}
}
...
}
์ฑ์ ์คํ์ ์ค๋นํ๋ ์ญํ
, ๋คํธ์ํฌ ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํ๋ ์ญํ
, ํ๋ฉด ์ ํ์ ์ฑ
์์ง๋ ์ญํ
๊ฐ๊ฐ ํ๋์ ์ฑ
์๋ง ๊ฐ์ง๋๋ก ๊ฐ์ (SRP)์ง์ํ๋ ํ์ | R.Swift | SwiftGen |
---|---|---|
Asset Catalog โ๏ธ | O | O |
Color โ๏ธ | O | O |
CoreData | X | O |
Files | O | O |
Font | O | O |
InterfaceBuilerFile (Storyboard) | O | O |
JSON and YAML files | X | O |
Plists โ๏ธ | X | O |
.strings โ๏ธ | O | O |
Nibs | O | X |
ReusableCell | O | X |
Segue | O | X |
์ต๊ทผ ๊ฒ์ ์ฑ์ ๊ฒ์ ์ฐฝ์ ๋ณด์ฌ์ค๋ค
SearchHistory
์ ์SearchResultController
SearchResultTableViewCell
x
๋ฒํผ๋ชจ๋ ์ญ์
๋ฒํผiTunesAPI๋ฅผ ํธ์ถํด ๋น๋ฒํ๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ์ ์์ต๋๋ค. ๊ทธ๋์ ์บ์๋ฅผ ์ฌ์ฉํด ํต์ ํธ๋ํฝ์ ์ค์ด๊ณ , ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋๋ฐ ํ์ํ ์๊ฐ์ ๋จ์ถ์ํค๊ณ ์ ํ์ต๋๋ค.
URLCache
๋ฅผ ์ฌ์ฉํ ์บ์ฑ ๋ฐฉ๋ฒ์ ์๊ฐํ์ต๋๋ค. URLRequest
์ response๊ฐ ์บ์์ ์กด์ฌํ๋ค๋ฉด ์บ์๋ response๋ฅผ ๋ฐํํ๊ณ , ์๋ค๋ฉด ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด๋ ค ํ์ต๋๋ค.
ํ์ง๋ง, ์ด ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํ์ ์๋ฒ์ ์บ์๋ ๋ฐ์ดํฐ ๊ฐ ๋ถ์ผ์น ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
HTTP header์ Cache-Control์ no-cache
๋ก ์ค์ ํ์ต๋๋ค. no-cache
๋ ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉ์์๊ฒ ๋ณด์ฌ์ฃผ๊ธฐ ์ด์ ์, ์๋ฒ์ ์บ์ ๋ฐ์ดํฐ๊ฐ ์๋ฒ์ ๋ฐ์ดํฐ์ ๋์ผํ์ง ์ ํจ์ฑ ๊ฒ์ฆ์ ํ๋๋ก ๊ฐ์ ํฉ๋๋ค. ๋งค ์์ฒญ ๋ง๋ค ํต์ ์ ํ๋ ๊ฒ์ ํผํ ์ ์์ง๋ง, ์บ์๊ฐ ์ ํจํ ๋ ๋ฐ์ดํฐ ๋ค์ด๋ก๋ ๋น์ฉ์ ์ ๊ฐํ ์ ์๊ธฐ ๋๋ฌธ์ ํจ์ฉ์ฑ์ด ์๋ค๊ณ ํ๋จํ์ต๋๋ค.
extension iTunesAPIRequest {
...
var header: [String: String] {
["Content-Type": "application/json",
"Accept": "application/json",
"Cache-control": "no-cache"]
}
}
URLSessionConfiguration.default
์ผ๋ก URLSession์ ์์ฑํ์ต๋๋ค.useProtocolCachePolicy
๋ก ์ค์ ํ์ฌ, request๋ง๋ค ์ ํจ์ฑ์ ๊ฒ์ฆํ๋ ๋ก์ง์ ์ฌ์ฉํ๋๋ก ํ์ต๋๋ค.extension iTunesAPIService {
static let sessionWithDefaultConfiguration: URLSession = {
let defaultConfiguration = URLSessionConfiguration.default
defaultConfiguration.requestCachePolicy = .useProtocolCachePolicy
return URLSession(configuration: defaultConfiguration)
}()
}
์๋ํ๋ฐ๋๋ก ์ค์ ๋ก ์ ๋์ํ๋์ง ํ์ธํ๊ธฐ ์ํด 2๊ฐ์ง ํ ์คํธ๋ฅผ ์งํํ์ต๋๋ค.
์บ์ ๋ฐ์ดํฐ์ ์๋ฒ ๋ฐ์ดํฐ๊ฐ ๋์ผํ๋ค๋ฉด cachedResponse๋ฅผ ๋ฆฌํดํ๋์ง
์บ์ ๋ฐ์ดํฐ์ ์๋ฒ ๋ฐ์ดํฐ๊ฐ ๋ค๋ฅด๋ค๋ฉด ์๋ฒ์ ์์ฒญํ์ฌ ๋ฐ์ ์ต์ Response๋ฅผ ๋ฆฌํดํ๋์ง
UICollectionViewFlowLayout
์ฌ์ฉlayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
UICollectionViewFlowLayout
์ estimatedItemSize
๋ฅผ automaticSize
๋ก ์ค์ ํ๋ค.private let contentCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.sectionInset = UIEdgeInsets(top: 10, left:0, bottom: 10, right: 0)
return UICollectionView(
frame: .zero,
collectionViewLayout: layout)
}()
AppDetailSummaryCollectionViewCell
์์ ์ ์ฝ ์ถฉ๋ ๋ฐ์์ถฉ๋์ด ๋ฐ์ํ๋ iconImageView์ ๋์ด ์ ์ฝ์ priority๋ฅผ ๋ฎ๊ฒ ์กฐ์ ํ๋ค.
let heightAnchor = iconImageView.heightAnchor.constraint(
equalToConstant: design.iconImageViewHeight)
heightAnchor.priority = .init(rawValue: 750)
SummaryCollectionViewCell
์ symbolImage์ ์ฌ์ฉ๋๋ ์ด๋ฏธ์ง๋ ๋ ๊ฐ์ง ํ์
์ด ์์ต๋๋ค
UIImage(systemName:)
: SFSymbol๋ก ์ด๊ธฐํํ๋ ์ด๋ฏธ์งTextSymbolView
(UIView
ํ์
) : Label์ ์ฌ์ฉํด Text๋ฅผ ์ด๋ฏธ์ง์ฒ๋ผ ๋ณด์ฌ์ฃผ๋ ๋ทฐ๋์ผํ ์ญํ ์ ์ด๋ฏธ์ง์ด์ง๋ง, ๋ ์ด๋ฏธ์ง์ ํ์ ์ด ๋ฌ๋ผ์ Cell์์ ๋ถ๊ธฐ์ฒ๋ฆฌ๋ฅผ ํตํด ์ด๋ฏธ์ง๋ฅผ ํธ๋ค๋งํด์ผํฉ๋๋ค.
// ๊ธฐ์กด ์ฝ๋
func bind(
primaryText: String?,
secondaryText: String?,
symbolImage: UIImage?) {
primaryTextLabel.text = primaryText
secondaryTextLabel.text = secondaryText
if let symbolImage = symbolImage {
self.symbolImageView.image = symbolImage
}
if let symbolTextView = symbolTextView {
self.symbolTextView = symbolTextView
}
}
๋ทฐ๋ ๋ก์ง์ด ์ ์ธ๋, UI๋ฅผ ํ์ํ๋ ์ญํ ๋ง ๊ฐ์ ธ๊ฐ๋ ๊ฒ์ด ์ ์ฐํ ์ค๊ณ๋ฅผ ์ํด ๋ง๋ค๊ณ ํ๋จํ๊ธฐ ๋๋ฌธ์,
TextSymbolView
์ ํ์
์ UIImage
๋ก ๋ณํํ๋๋ก ํ์ต๋๋ค.
UIView
์ ์ต์คํ
์
์ UIImage
๋ก ๋ณํํ๋ ๋ฉ์๋๋ฅผ ๊ตฌํํ์ต๋๋ค.
extension UIView {
func toImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}
TextSymbolView
์ size๋ ์์๊ฐ์ผ๋ก ํฝ์คํ, Label์ ์ฌ์ด์ฆ๋ intrinsicContentSize์ ์ํด ๊ฒฐ์ ๋๋๋กํ๊ณ , ์์น๋ view์ centerX, centerY์ ๋ง์ถฐ ์ค์์ ์ค๋๋ก ๊ตฌํํ์ต๋๋ค.
textAlignment์ center๋ก ์ค์ ํ์์๋, ์ ์ด๋ฏธ์ง์ ๊ฐ์ด ๋ผ๋ฒจ์ด ์ข์ธก ์ ๋ ฌ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
final class TextSymbolView: UIView {
private let text: String
private lazy var label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = self.text
label.font = Design.font
label.textColor = Design.color
label.textAlignment = .center
label.numberOfLines = 1
label.lineBreakMode = .byTruncatingTail
label.sizeToFit()
return label
}()
var image: UIImage {
return self.toImage()
}
init(_ text: String) {
self.text = text
super.init(frame: CGRect(x: 0, y: 0, width: Design.width, height: Design.height))
configureSubview()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureSubview(){
addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
label.centerYAnchor.constraint(equalTo: self.centerYAnchor)
])
}
}
๊ฐ์ ์ | ๊ฐ์ ํ |
---|---|
![]() |
![]() |
cellForRowAt
์์ ๋คํธ์ํฌ ๋ฐ ๋ ๋ฆ์์ CellModeld์ ๋น๋๊ธฐ์ ์ผ๋ก ๋ฐ์์ค๋ Publisher๋ฅผ ๋ง๋ ํ, cell์ bindํด์ฃผ๊ณ ์คํธ๋ฆผ์ ์์ํฉ๋๋ค.extension AppFolderDetailViewModel: UITableViewDataSource {
func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int)
-> Int
{
if let savedApps,
savedApps.isEmpty {
showEmptyView.send(true)
} else {
showEmptyView.send(false)
}
return savedApps?.count ?? 0
}
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell
{
let cell = tableView.dequeueReusableCell(
withClass: SavedAppDetailTableViewCell.self,
for: indexPath)
guard let savedApp = savedApps?[safe: indexPath.row] else {
return cell
}
// TODO: - Error Handling
let cellModel = appFolderUsecase.readSavedAppDetail(of: savedApp)
.map { savedAppDetail in
return SavedAppDetailTableViewCellModel(savedAppDetail: savedAppDetail)
}
.assertNoFailure()
.eraseToAnyPublisher()
cell.bind(cellModel)
return cell
}
}
UITableViewDataSourcePrefetching
์ฌ์ฉUITableViewDataSourcePrefetching
๋ฅผ ์ฑํํ์ฌ tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath])
๋ฉ์๋๋ด์์ [indexPath]์ ํด๋นํ๋ savedApp๋ค์ cellModel์ ๋ค์ด๋ก๋ํ์ฌ ๋ฐฐ์ด์ ์ ์ฅํฉ๋๋ค.extension AppFolderDetailViewModel: UITableViewDataSourcePrefetching {
func tableView(
_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath])
{
guard let savedApps else {
return
}
Publishers.Sequence<[IndexPath], Never>(sequence: indexPaths)
.map({ indexPath in
return (indexPath.row, savedApps[indexPath.row])
})
.flatMap { (index, savedApp) -> AnyPublisher<(Int, SavedAppDetail), Error> in
return self.appFolderUsecase.readSavedAppDetail(of: savedApp, index: index)
}
.map{ savedAppDetail in
return (savedAppDetail.0, SavedAppDetailTableViewCellModel(savedAppDetail: savedAppDetail.1))
}
.assertNoFailure()
.sink { [unowned self] cellModel in
self.fetchedCellModels[cellModel.0] = cellModel.1
}.store(in: &cancellable)
}
}
tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath)
์์ ๋ง์ฝ prefetchํ cellModel์ด ์๋ค๋ฉด ํด๋น cellModel์ ์ฌ์ฉํด ๋ฐ์ธ๋ํ๊ณ , ์๋ค๋ฉด ์คํธ๋ฆผ์ ์์ํฉ๋๋ค.extension AppFolderDetailViewModel: UITableViewDataSource {
func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int)
-> Int
{
if let savedApps,
savedApps.isEmpty {
showEmptyView.send(true)
} else {
showEmptyView.send(false)
}
return savedApps?.count ?? 0
}
func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell
{
let cell = tableView.dequeueReusableCell(
withClass: SavedAppDetailTableViewCell.self,
for: indexPath)
guard let savedApp = savedApps?[safe: indexPath.row] else {
return cell
}
if let cellModel = fetchedCellModels[indexPath.row] {
cell.bind(cellModel)
} else {
let cellModel = appFolderUsecase.readSavedAppDetail(of: savedApp)
.map { savedAppDetail in
return SavedAppDetailTableViewCellModel(savedAppDetail: savedAppDetail)
}
.assertNoFailure()
.eraseToAnyPublisher()
cell.bind(cellModel)
}
return cell
}
}
prefetchRowsAt
๋ฉ์๋๋ ์ฌ์ฉ์๊ฐ ์คํฌ๋กค์ ํด์ผ ํธ์ถ๋๊ธฐ ๋๋ฌธ์ ๋งจ ์ฒ์ ๋ณด์ฌ์ง๋ ์
์ ์ฌ์ ํ ์ง์ฐ์ด ๋ฐ์ํฉ๋๋ค.
AppSearchViewController
๊ตฌํAppDetailView
component ๊ตฌํAppDetailViewController
๊ตฌํ์ธ๊ต๋ถ_๊ตญ๊ฐยท์ง์ญ๋ณ ํ์ค์ฝ๋
CountryCodeList
, CountryCode
) & Country
ํ์
์์ CountryCodeAPIService
, CountrtyCodeListRequest
๊ตฌํdidFinishLaunchingWithOptions
์์ API ํธ์ถ์ ํตํด CountryCode
๋ฐ์ดํฐ๋ฅผ ๋ค์ด๋ก๋ ๋ฐ์ ํ Country
ํ์
์ผ๋ก ๋งคํํ์ฌ ํ์
ํ๋กํผํฐ(Country.list
)์ ์ ์ฅCountry
์ ๊ตฌํ๐ํด๊ฒฐ ๊ณผ์ ์ ๋ธ๋ก๊ทธ์ ์ ๋ฆฌํ์ฌ ๊ณต์
๋ฌธ์ ์ ์
ํฌ์คํธ๋งจ์์๋ ์ ์์ ์๋ต์ ๋ฐ๋ ๊ฒ์ด,
์ฑ์์ ์คํํ๋SERVICE_KEY_IS_NOT_REGISTERED_ERROR ๋ผ๋ ์ค๋ฅ xml์ด ์ด
๋ฌธ์ ์์ธ ๋ถ์
URLComponent
์ ๊ณต๊ณต๋ฐ์ดํฐํฌํธ์ด ๊ฐ๊ฐ ์ฌ์ฉํ๋ api key๋ฅผ ์ธ์ฝ๋ฉํ๋ ๋ฐฉ์์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋ฐ์ํด๊ฒฐ ๋ฐฉ๋ฒ
percentEncodedQuery
์ '+'๋ฅผ '%2B' ๋ณํ 5c7acb6var url: URL? {
var urlComponents = URLComponents(string: baseURLString + path)
urlComponents?.queryItems = query.map {
URLQueryItem(name: $0.key, value: "\($0.value)") }
let encodedQuery = urlComponents?.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
urlComponents?.percentEncodedQuery = encodedQuery
return urlComponents?.url
}
๋ฌธ์ ์ ์
MainCoordinator์ start()
์์ ๋ค์ด๋ก๋๋ฅผ ํจ
func start() {
Task {
await AppSearchingConfiguration().downloadCountryCode()
await MainActor.run {
let searchKeywordRepository = RealmSearchKeywordRepository()
let appSearchUseacase = AppSearchUsecase(
searchKeywordRepository: searchKeywordRepository)
let seachViewModel = SearchViewModel(appSearchUsecase: appSearchUseacase)
let searchVC = SearchViewController(searchViewModel: seachViewModel)
searchVC.coordinator = self
navigationController.pushViewController(searchVC, animated: false)
}
}
}
๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๋ฐ์์จ ๋ค, ์ฒซ ํ๋ฉด์ด ์์๋๊ธฐ ๋๋ฌธ์ ์ง์ ํด๋ ๊ตญ๊ฐ๋ฅผ ๋ถ๋ฌ์ค์ง๋ง..
๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๋์ ์ ๊น ๊ฒ์ ํ๋ฉด์ด ๋ณด์ด๋ ๋ฌธ์ ๊ฐ ์ฌ์ ํ ๋ฐ์
๊ณ ๋ฏผํ ๋ฐฉ๋ฒ
1. rootViewController์ ๋ฐฐ๊ฒฝ์ ๋ฐ์น์คํฌ๋ฆฐ์ผ๋ก ์ง์ ํ๋ค. โ
๊ฒ์ ํ๋ฉด์ ์์ ๊ณ ์ํ๋ ์๋์ ๋ถํฉ
2. stateRestoration ํจ์๋ฅผ ์ฌ์ฉํด๋ณธ๋ค? ํ๋ฉด ์ํ๋ณต๊ตฌ์ ์ฌ์ฉํ๋ฏ๋ก ์ ์ ํ์ง ์์
ํด๊ฒฐ ๋ฐฉ๋ฒ
ํ์ดํฌ LaunchScreenVC๋ฅผ ๋ง๋ค์ด์ ๋ค๋น๊ฒ์ด์ ์ปจํธ๋กค๋ฌ์ rootViewController๋ก ์ง์ ํจ 37d4c14
๊ทผ๋ฐ ์ด ๋ฐ์น์คํฌ๋ฆฐ์ด root๋ผ ๋ค๋น๊ฒ์ด์ ์คํ์ ๋ค์ด๊ฐ์, ๋ค๋น๊ฒ์ด์ ๋ฐ๊ฐ ์๊น
rootViewController๋ฅผ LaunchScreenVC์ผ๋ก ์ง์ ํ๊ณ
๋ค์ด์ด ์๋ฃ๋์์ ๋, SearchVC๋ฅผ root๋ก ํ๋ ๋ค๋น๊ฒ์ด์ ์ปจํธ๋กค๋ฌ๋ฅผ presentํจ์ผ๋ก ํด๊ฒฐ! 112cf13
// SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let launchScreen = LaunchScreenViewController()
window = UIWindow(windowScene: windowScene)
// ๋ฐ์น์คํฌ๋ฆฐ๋ทฐ์ปจ์ ๋ฃจํธ๋ทฐ๋ก ์ง์
window?.rootViewController = launchScreen
window?.makeKeyAndVisible()
let mainCooordinator = SearchCoordinator(rootViewController: launchScreen)
mainCoordinator = mainCooordinator
mainCooordinator.start()
}
// SearchCoordinator
func start() {
Task {
await AppSearchingConfiguration().downloadCountryCode()
await MainActor.run {
let searchKeywordRepository = RealmSearchKeywordRepository()
let appSearchUseacase = AppSearchUsecase(
searchKeywordRepository: searchKeywordRepository)
let seachViewModel = SearchViewModel(appSearchUsecase: appSearchUseacase)
let searchVC = SearchViewController(searchViewModel: seachViewModel)
searchVC.coordinator = self
// ์ฑ์ ์ง์ง ์ฒซํ๋ฉด์ธ SearchVC๋ฅผ ๋ฃจํธ๋ก ํ๋ ๋ค๋น๊ฒ์ด์
์ปจํธ๋กค๋ฌ ์์ฑ
navigationController = UINavigationController(rootViewController: searchVC)
// ๋ชจ๋ ํ๋ฉด์ ๋ฎ๋๋ก ์คํ์ผ fullScreen์ผ๋ก ์ง์
navigationController.modalPresentationStyle = .fullScreen
launchScreenViewController.present(navigationController, animated: false)
}
}
}
AppFolder
์ ์SavedApp
์ ์AppFolderRepository
๊ตฌํ
AppGroupListView
AppGroupView
AppTableViewCell
final class ScreenShotCollectionViewCell: UICollectionViewCell {
private let screenShotView = UIImageView(
frame: CGRect(x: 0, y: 0, width: 280, height: 500))
...
}
.estimated()
ํ๋กํผํฐ๋ก dimension์ ์ค์ ํ๋ค. private func createLayout() -> UICollectionViewLayout {
let sectionProvider = { [weak self] (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionKind = AppDetailViewModel.Section(rawValue: sectionIndex) else { return nil }
let section: NSCollectionLayoutSection
if sectionKind == .summary {
...
} else if sectionKind == .screenshot {
let itemSize = NSCollectionLayoutSize(
widthDimension: .estimated(280),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(500))
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item])
section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
} else if sectionKind == .descritption {
....
} else {
fatalError("Unknown section!")
}
return section
}
return UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)
nil
๋ก ๋ฐํ์งsearchingConturyISOCode
๋ก ๋ณ์๋ช
๋ช
ํํ๊ฒ ์์ SearchView
์ ํ์ดํ(AnimatableArrow)๊ฐ iPad, Mac์ ๊ฐ๋ฆฌํฌ ๋, ํ์ดํ๊ฐ 12์๋ฐฉํฅ์ผ๋ก ํ์๋์ด ๋ถ์์ฐ์ค๋ฌ์setPostition
์ ์ ๋๋ฉ์ด์
์์ ์๋์ ๊ฐ์ด ์ค์ ํ๊ธฐ ๋๋ฌธ์ ํ์ดํ์ ์์น๋ง ์ ํํ๊ณ , ํ์ดํ๋ ํ์ ํ์ง ์์์
rotationMode
์ ์ฉํ์ง ์์fillMode
์ ์ฉํ์ง ์์animate(to:)
์ ๋์ผํ๊ฒ ์ ๋๋ฉ์ด์
ํน์ง์ ์ถ๊ฐํ๊ณ , startAngle์ iPhone์ ๊ฐ๋ฆฌํค๋ 1.5 * .pi
๋ก ๊ณ ์ SearchView
๋ฅผ ์ฒ์ ๋์์ ๋, iPad ๋๋ mac์ผ์ ํ์ดํ ๋ฐ๋ฅ์ด ์์ ๋ถ์ด์ ์์ฐ์ค๋ฝ๊ฒ ํ์APIRequest
APIService
์ธํฐํ์ด์ค ์ ์ItunesAPIService
๊ตฌํA declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.