Giter Club home page Giter Club logo

chajabwa's People

Contributors

yeahg-dev avatar

Stargazers

 avatar

Watchers

 avatar

chajabwa's Issues

CountryCodeAPIService ์‘๋‹ต ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„ 

๐Ÿšจ ๋ฌธ์ œ ์„ค๋ช…

  • CountryCodeAPIService์—์„œ apikey์˜ค๋ฅ˜ response๋„ status code: 200, Content-Type: xml ํƒ€์ž…์œผ๋กœ ์‘๋‹ต
  • ๋”ฐ๋ผ์„œ ๊ธฐ์กด APIService์˜ request๋ฅผ ์š”์ฒญํ•˜๋Š” ๋ฉ”์„œ๋“œ์—์„œ ํ•ด๋‹น ์˜ค๋ฅ˜๋ฅผ ๊ฑธ๋Ÿฌ๋‚ผ ์ˆ˜ ์—†์—ˆ๊ณ , ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์—†์—ˆ์Œ
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
    }
}

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฌธ์ œ ๋ถ„์„

  • response์˜ header์˜ Content-Type ํ•„๋“œ ๊ฐ’์„ ์กฐํšŒํ•˜๋ฉด ์˜ค๋ฅ˜ ์ฒดํฌ ๋ฐ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  • Content-Type์ด 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
    }
    
}
  • ๊ธฐ์กด APIService์˜ getResponse๋ฉ”์„œ๋“œ๋Š” ํ”„๋กœํ† ์ฝœ ๊ธฐ๋ณธ ๊ตฌํ˜„์œผ๋กœ ๋‹ค๋ฅธ Service์—์„œ๋„ ์‚ฌ์šฉ๋˜๋ฏ€๋กœ, ๋‹จ์ผ์ฑ…์ž„์›์น™๊ณผ ๊ฐœ๋ฐฉํ์‡„์›์น™์„ ์œ„๋ฐ˜ํ•œ ์ฝ”๋“œ์ž„
  • ๋”ฐ๋ผ์„œ getResponse๋ฅผ ํ”„๋กœํ† ์ฝœ์— ์ •์˜, ์ถ”์ƒํ™”ํ•˜์—ฌ ํ™•์žฅ์— ์šฉ์ดํ•˜๋„๋ก ๋ฆฌํŒฉํ„ฐ๋ง
  • Combine ํ”„๋ ˆ์ž„์›Œํฌ ๋˜ํ•œ ๋ถˆํ•„์š”ํ•˜๊ฒŒ APIService์— ์ข…์†๋˜์–ด์žˆ์œผ๋ฏ€๋กœ, CombineAPIService ํ”„๋กœํ† ์ฝœ์„ ์ •์˜ํ•˜์—ฌ ์—ญํ• ์„ ๋ถ„๋ฆฌ
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>
    
}

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป๊ฒฐ๊ณผ์™€ ํ”ผ๋“œ๋ฐฑ

  • APIService์˜ ํ™•์žฅ์„ฑ ํ–ฅ์ƒ๋จ
  • ๋ถˆํ•„์š”ํ•œ Combine์„ import๋ฅผ ์ œ๊ฑฐํ•จ์œผ๋กœ์จ ์ตœ์ ํ™”ํ•จ

SearchBar ์ž‘๋™ ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์šด ๋ฌธ์ œ

๐Ÿšจ ๋ฌธ์ œ ์„ค๋ช…

ํ™˜๊ฒฝ

  • Xcode 14.0
  • target iOS 15.5

๋ฌธ์ œ

  • ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ™”๋ฉด์—์„œ searchBar๋ฅผ ํƒญํ•˜๊ณ  ํ•œ ๊ธ€์ž๋ฅผ ํƒ€์ดํ•‘ํ•ด์•ผ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด๋กœ ์ „ํ™˜๋˜์–ด ์‚ฌ์šฉ์ƒ์˜ ๋ถˆํŽธํ•จ ์•ผ๊ธฐ
  • ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ํ™”๋ฉด์—์„œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ™œ์„ฑํ™” ๋ทฐ(header)๊นŒ์ง€ ์Šคํฌ๋กค ๋˜์ง€ ์•Š๋Š” ๋ฒ„๊ทธ

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฌธ์ œ ๋ถ„์„ & ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ™”๋ฉด์—์„œ searchBar๋ฅผ ํƒญํ•˜๊ณ  ํ•œ ๊ธ€์ž๋ฅผ ํƒ€์ดํ•‘ํ•ด์•ผ ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด๋กœ ์ „ํ™˜
    • ๊ธฐ์กด์—๋Š” searchBar์˜ delegateMethod์—์„œ ๋ถ„๊ธฐ๋ฌธ์„ ํ†ตํ•ด RecenctSearchKeywordTableView๋ฅผ ๋ณด์—ฌ์ฃผ์—ˆ๋‹ค.
    • ๋ถ„๊ธฐ๋ฌธ์„ ํƒˆ ํ•„์š” ์—†์ด, SearchBar๊ฐ€ ํƒญ๋˜์—ˆ์„ ๋•Œ RecenctSearchKeywordTableView๋ฅผ ๋ณด์—ฌ์ฃผ๋ฉด ๋˜๋ฏ€๋กœ, searchBarTextDidBeginEditing์— showRecentSearchKeywordTableView๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ•ด๊ฒฐํ–ˆ๋‹ค
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
       searchController.showsSearchResultsController = true
       searchAppResultsController.showRecentSearchKeywordTableView()
   }
  1. ์ตœ๊ทผ ๊ฒ€์ƒ‰์–ด ํ™”๋ฉด์—์„œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ™œ์„ฑํ™” ๋ทฐ(header)๊นŒ์ง€ ์Šคํฌ๋กค ๋˜์ง€ ์•Š๋Š” ๋ฒ„๊ทธ
    • scrollToTop๋‚ด๋ถ€์—์„œ ์ฒซ๋ฒˆ์งธ cell๊นŒ์ง€ ์ด๋™ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— header๋Š” ๋ณด์—ฌ์ฃผ์ง€ ๋ชปํ–ˆ๋‹ค.
    • ๋”ฐ๋ผ์„œ headerView๊นŒ์ง€ ํฌํ•จ๋œ ๋ทฐ์ธ tableView์˜ bounds.origin.y๋ฅผ searchBar์˜ ๋†’์ด๋กœ ์ง€์ •ํ•ด์ฃผ์–ด headerView์˜ ์กด์žฌ ์œ ๋ฌด์— ์ƒ๊ด€ ์—†์ด tableView๋ฅผ ๋ชจ๋‘ ๋ณด์—ฌ์ฃผ๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค.
    func scrollToTop() {
        guard let searchBarHeight = navigationItem.searchController?.searchBar.bounds.height else {
            return
        }
        tableView.bounds.origin.y = searchBarHeight
    }

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป๊ฒฐ๊ณผ์™€ ํ”ผ๋“œ๋ฐฑ

  • ์˜๋„ํ•œ๋Œ€๋กœ searchBar์˜ ์‚ฌ์šฉ์„ฑ์ด ๊ฐœ์„ ๋˜์—ˆ๋‹ค.
  • UIKit์˜ API๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ, View์˜ ๊ณ„์ธต๊ตฌ์กฐ์™€ ์ขŒํ‘œ์ฒด๊ณ„๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐฐ์› ๋‹ค

๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ™•์žฅ : ์ด๋ฆ„์œผ๋กœ ๊ฒ€์ƒ‰/ํ”Œ๋žซํผ ์„ค์ •/๊ตญ๊ฐ€ ์„ค์ •

๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ™•์žฅ

  • ์ด๋ฆ„ ์œผ๋กœ ๊ฒ€์ƒ‰
  • ํ”Œ๋žซํผ ์„ค์ •
  • ๊ฒ€์ƒ‰ ๊ตญ๊ฐ€ ์„ค์ •

Model

  • iTunes Search API ์—ฐ๊ฒฐ
    • AppSerchAPIRequest ํƒ€์ž… ๊ตฌํ˜„
    • Entity ๋ชจ๋ธ๋ง
    • AppRepository Test
    • Parsing Test
  • ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ™•์žฅ
    • ๊ฒ€์ƒ‰ ๋ฒ”์œ„ ์„ค์ • ๊ตฌํ˜„ (software/iPad software/mac software)
    • input์— ๋”ฐ๋ผ ์–ด๋–ค API์‚ฌ์šฉํ• ์ง€ ํ•ธ๋“ค๋ง

UI

  • ScopeBar ๊ตฌํ˜„
  • AppSearchResultListView ๊ตฌํ˜„
  • AppSearchTableViewCell ๊ตฌํ˜„
  • App Search Scene ๋””์ž์ธ ๋ฆฌ๋‰ด์–ผ
    • Search Field UI ๋ณ€๊ฒฝ

  • SettingView๊ตฌํ˜„
    • ๊ตญ๊ฐ€ ์„ค์ •

Refactoring

  • ์ฝ”๋””๋„ค์ดํ„ฐ ํŒจํ„ด ์ ์šฉ

๋‹ค๊ตญ์–ด ์ง€์›

๐ŸŽฏ ๋ชฉ์ 

  • ํ•œ๊ตญ์–ด, ์˜์–ด, 2๊ฐ€์ง€ ์–ธ์–ด๋ฅผ ์ง€์›ํ•ด์„œ ํ•œ๊ตญ ์™ธ ๊ตญ๊ฐ€์—์„œ์˜ ์ ‘๊ทผ์„ฑ ํ™•๋ณด

๐Ÿ’ก ๊ตฌํ˜„ ์•„์ด๋””์–ด

์‚ฌ์šฉ Language & Region ์ถ”์ถœ ๋ฐฉ๋ฒ•

์ง€์—ญํ™” ์ ์šฉ ๋Œ€์ƒ

  • String : .strings ํŒŒ์ผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ localizedString์‚ฌ์šฉ
  • AssetCatalog : loacalization ๊ธฐ๋Šฅ ์‚ฌ์šฉ
  • Date : DateFormatter์‚ฌ์šฉํ•˜์—ฌ region์— ์•Œ๋งž์€ ๋‚ ์งœ ํฌ๋งท ํ‘œ์‹œ

โœ… Task

  • ์–ธ์–ด/์ง€์—ญ ์„ค์ • ๊ธฐ๋Šฅ
    • ์„ ํ˜ธ ์–ธ์–ด ์„ค์ • ๊ธฐ๋Šฅ (localization)
    • ๊ฒ€์ƒ‰ ๊ตญ๊ฐ€ ์„ค์ • ๊ธฐ๋Šฅ (SearchAPI์— ์ ์šฉ)
  • ํ˜„์žฌ ๊ธฐ๊ธฐ ์„ค์ •์„ ์ œ๊ณตํ•˜๋Š” API ๊ตฌํ˜„
    • DeviceSetting ํƒ€์ž… ๊ตฌํ˜„
  • Asset
    • ๋Ÿฐ์น˜์Šคํฌ๋ฆฐ
  • Date Format
    • DateFormatter

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป ๊ฒฐ๊ณผ์™€ ํ”ผ๋“œ๋ฐฑ

์•ฑ ์‹คํ–‰ ์ค€๋น„์™€ ์ฝ”๋””๋„ค์ดํ„ฐ ๋กœ์ง ๋ถ„๋ฆฌ

๐Ÿšจ ๋ฌธ์ œ ์„ค๋ช…

์ƒํ™ฉ

  • ์•ฑ์˜ ์ฒซ ํ™”๋ฉด์—์„œ ์„ค์ • ๋ฐ์ดํ„ฐ(๊ตญ๊ฐ€์™€ ๊ตญ๊ธฐ)๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” CountryCodeAPIService๋ฅผ ์ด์šฉํ•ด์„œ API call์ด ์„ ํ–‰๋˜์–ด์•ผํ•จ.
  • ๋ฃจํŠธ ๋ทฐ๋ฅผ ๋Ÿฐ์น˜์Šคํฌ๋ฆฐ์œผ๋กœ ์„ค์ •ํ•˜๊ณ , API call์„ ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ฐฉํ•˜๋ฉด ๋™๊ธฐ์ ์œผ๋กœ SearchVC๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์—ˆ์Œ

๋ฌธ์ œ

  • ๋‹ค์šด๋กœ๋“œ๋ฅผ ํ•˜๋Š” ๋™์•ˆ ๋Ÿฐ์น˜์Šคํฌ๋ฆฐ์ด ํ‘œ์‹œ๋˜๋ฏ€๋กœ, ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋Š” ๋Ÿฐ์น˜ ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง
  • ์ฝ”๋””๋„ค์ดํ„ฐ์—์„œ ์•ฑ ์ดˆ๊ธฐ๋ฐ์ดํ„ฐ๋ฅผ ์ค€๋น„ํ•˜๋Š” ์—ญํ• ๊นŒ์ง€ ๋งก๊ฒŒ๋จ
// ๊ธฐ์กด ์ฝ”๋“œ
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 ์œ„๋ฐ˜)

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฌธ์ œ ๋ถ„์„

  • ์ฝ”๋””๋„ค์ดํ„ฐ์—์„œ ์‹œ์Šคํ…œ์˜ ์ƒ์„ฑ๋…ผ๋ฆฌ๊นŒ์ง€ ์ฑ…์ž„์„ ๋งก๊ณ  ์žˆ์Œ
  • ์•ฑ์˜ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์ƒ์„ฑ์„ ์ฑ…์ž„์ง€๋Š” ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๋ฉด ์—ญํ• ์„ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. AppOrganizer ์ •์˜
  • CountryCodeAPIService๋ฅผ ์ด์šฉํ•ด ๊ตญ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์Œ
final class AppOrganizer {
    
    private let countryCodeAPIService = CountryCodeAPIService()

    func prepare(didEnd completion: @escaping (() -> Void)) {
        countryCodeAPIService.fetchCountryCodes(completion: completion)
    }
    
}
  1. LoadingViewController ๊ตฌํ˜„
  • ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋กœ๋“œ ํ•  ๋™์•ˆ ๋ณด์—ฌ์ค„ ๋ทฐ ๊ตฌํ˜„
  • activity Indicator๋ฅผ ์‚ฌ์šฉํ•ด ์ง„ํ–‰ ์ค‘์ž„์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ
  1. ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ์—†์„ ๋•Œ alert์„ ํ‘œ์‹œํ•ด ์žฌ์‹œ๋„ ์œ ๋„

image

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()
    }
    
}
  1. AppOrganizer์˜ completion handler๋กœ mainCoordinator.start()ํ˜ธ์ถœ
  • scene์ด ์—ฐ๊ฒฐ๋˜์—ˆ์„ ๋•Œ ๋จผ์ € loadingViewController๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ , AppOrganizer์˜ prepare๋ฅผ ํ˜ธ์ถœ
  • AppOrganizer์˜ 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)
  • ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•ด์ง

๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋„์ž…

๐ŸŽฏ ๋ชฉ์ 

  • ๋ฆฌ์†Œ์Šค(Image, Color, .strings)๋ฅผ Swift ํƒ€์ž…์œผ๋กœ ์ž๋™ ์ƒ์„ฑํ•˜๊ณ  ์ค‘๋ณต ์ž‘์—…์„ ์ œ๊ฑฐ
  • ์ปดํŒŒ์ผ ํƒ€์ž„์— ๋ฌธ์ž์—ด๋กœ ์ดˆ๊ธฐํ™” ํ•  ๋•Œ ์ƒ๊ธฐ๋Š” ํฌ๋ž˜์‹œ ์ฒดํฌ

๐Ÿ’ก ๊ตฌํ˜„ ์•„์ด๋””์–ด

1. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ํƒ

์ง€์›ํ•˜๋Š” ํƒ€์ž… 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
  • ๋ณ€ํ™˜ํ•˜๊ณ ์žํ•˜๋Š” ํƒ€์ž…์„ ๋ชจ๋‘ ์ง€์›ํ•˜๊ณ , ์•ˆ์ •์ ์œผ๋กœ ์šด์šฉ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ SwiftGen์„ ์„ ํƒ

2. ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  • Homebrew๋กœ ์„ค์น˜
  • ConfigํŒŒ์ผ์— ๋ณ€ํ™˜ํ•˜๊ณ ์žํ•˜๋Š” ํŒŒ์ผ์„ YAML ๋ฌธ๋ฒ•์— ๋งž์ถฐ ์ž‘์„ฑ

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป ๊ฒฐ๊ณผ์™€ ํ”ผ๋“œ๋ฐฑ

  • ๋ณต์ ์ธ ๋‹จ์ˆœ ์ž‘์—… ์ œ๊ฑฐ์— ๋”ฐ๋ฅธ ๊ฐœ๋ฐœ ํŽธ๋ฆฌ์„ฑ ์ฆ๊ฐ€
  • ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ํŒŒ์ผ๋กœ ๋ถ€ํ„ฐ ํƒ€์ž…์„ ์ƒ์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋Ÿฐํƒ€์ž„ ํฌ๋ž˜์‹œ๋„ ๋ฐฉ์ง€ ํšจ๊ณผ

์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ๊ธฐ๋Šฅ ๊ตฌํ˜„

์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ๊ธฐ๋Šฅ ๊ตฌํ˜„

์ตœ๊ทผ ๊ฒ€์ƒ‰ ์•ฑ์„ ๊ฒ€์ƒ‰ ์ฐฝ์— ๋ณด์—ฌ์ค€๋‹ค

๊ตฌํ˜„ ์‚ฌํ•ญ

Model

  • SearchHistory ์ •์˜
  • ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์ €์žฅ
  • ์ตœ๊ทผ ๊ฒ€์ƒ‰ ๊ธฐ๋ก ์‚ญ์ œ
    • ๋ชจ๋‘ ์‚ญ์ œ
    • ์„ ํƒ ์‚ญ์ œ
  • ๊ฒ€์ƒ‰ ๊ธฐ๋ก ํ™œ์„ฑํ™” ๊ตฌํ˜„

UI

  • SearchResultController
    • SearchResultTableViewCell
    • ์‚ญ์ œx๋ฒ„ํŠผ
    • ๋ชจ๋‘ ์‚ญ์ œ ๋ฒ„ํŠผ
    • ๊ฒ€์ƒ‰ ๊ธฐ๋ก ํ™œ์„ฑํ™” ํ† ๊ธ€ ๋ฒ„ํŠผ

CacheControl ํ™œ์šฉํ•œ ํŠธ๋ž˜ํ”ฝ ์ตœ์ ํ™”

์บ์‹œ์˜ ํ•„์š”์„ฑ ์ธ์‹

iTunesAPI๋ฅผ ํ˜ธ์ถœํ•ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•ด ํ†ต์‹  ํŠธ๋ž˜ํ”ฝ์„ ์ค„์ด๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š”๋ฐ ํ•„์š”ํ•œ ์‹œ๊ฐ„์„ ๋‹จ์ถ•์‹œํ‚ค๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ ์ƒํ™ฉ

URLCache๋ฅผ ์‚ฌ์šฉํ•œ ์บ์‹ฑ ๋ฐฉ๋ฒ•์„ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. URLRequest์˜ response๊ฐ€ ์บ์‹œ์— ์กด์žฌํ•œ๋‹ค๋ฉด ์บ์‹œ๋œ response๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , ์—†๋‹ค๋ฉด ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ด๋ ค ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ, ์ด ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„์‹œ ์„œ๋ฒ„์™€ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ๊ฐ„ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

HTTP header์˜ Cache-Control์„ no-cache๋กœ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. no-cache๋Š” ์บ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ๊ธฐ ์ด์ „์—, ์„œ๋ฒ„์— ์บ์‹œ ๋ฐ์ดํ„ฐ๊ฐ€ ์„œ๋ฒ„์˜ ๋ฐ์ดํ„ฐ์™€ ๋™์ผํ•œ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ํ•˜๋„๋ก ๊ฐ•์ œํ•ฉ๋‹ˆ๋‹ค. ๋งค ์š”์ฒญ ๋งˆ๋‹ค ํ†ต์‹ ์„ ํ•˜๋Š” ๊ฒƒ์€ ํ”ผํ•  ์ˆ˜ ์—†์ง€๋งŒ, ์บ์‹œ๊ฐ€ ์œ ํšจํ•  ๋• ๋ฐ์ดํ„ฐ ๋‹ค์šด๋กœ๋“œ ๋น„์šฉ์„ ์ ˆ๊ฐํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํšจ์šฉ์„ฑ์ด ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

HTTP Header

  • HTTP Header์— Cache-Control ํ•ญ๋ชฉ ์ถ”๊ฐ€
extension iTunesAPIRequest {
    ...
    var header: [String: String] {
        ["Content-Type": "application/json",
         "Accept": "application/json",
         "Cache-control": "no-cache"]
    }
}

URLSession์˜ configuration ๋ณ€๊ฒฝ

  • ์บ์‹œ๋ฅผ ์ง€์›ํ•˜๋Š” URLSessionConfiguration.default์œผ๋กœ URLSession์„ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์บ์‹œ ์ •์ฑ…์„ useProtocolCachePolicy๋กœ ์„ค์ •ํ•˜์—ฌ, request๋งˆ๋‹ค ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋กœ์ง์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.
extension iTunesAPIService {
    
    static let sessionWithDefaultConfiguration: URLSession = {
        let defaultConfiguration = URLSessionConfiguration.default
        defaultConfiguration.requestCachePolicy = .useProtocolCachePolicy
        return URLSession(configuration: defaultConfiguration)
    }()
    
}

์บ์‹œ ์ž‘๋™ ํ™•์ธ ํ…Œ์ŠคํŠธ

์˜๋„ํ•œ๋ฐ”๋Œ€๋กœ ์‹ค์ œ๋กœ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด 2๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. ์บ์‹œ ๋ฐ์ดํ„ฐ์™€ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋™์ผํ•˜๋‹ค๋ฉด cachedResponse๋ฅผ ๋ฆฌํ„ดํ•˜๋Š”์ง€

  2. ์บ์‹œ ๋ฐ์ดํ„ฐ์™€ ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅด๋‹ค๋ฉด ์„œ๋ฒ„์— ์š”์ฒญํ•˜์—ฌ ๋ฐ›์€ ์ตœ์‹  Response๋ฅผ ๋ฆฌํ„ดํ•˜๋Š”์ง€

performance test

ViewModel ๊ตฌํ˜„

  • SearchViewModel ๊ตฌํ˜„
  • AppDetailViewModel ๊ตฌํ˜„
  • ViewModel ๋™์ž‘ ํ…Œ์ŠคํŠธ

self-sizing CollectionViewCell ๊ตฌํ˜„

ContentView์— ๋”ฐ๋ฅธ Self-sizing CollectionViewCell ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

ํ™˜๊ฒฝ

  • UICollectionViewFlowLayout ์‚ฌ์šฉ

1. layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  • UICollectionViewFlowLayout์˜ estimatedItemSize๋ฅผ automaticSize๋กœ ์„ค์ •ํ•œ๋‹ค.
  • 0์ด ์•„๋‹Œ placeholder ๊ฐ’์ด๊ณ , ๊ฐ ์…€์˜ ์‹ค์ œ ์‚ฌ์ด์ฆˆ๋Š” preferredLayoutAttributesFitting(_:) ๋ฉ”์„œ๋“œ์— ์˜ํ•ด ๊ฒฐ์ •๋œ๋‹ค.
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)
    }()

2. AppDetailSummaryCollectionViewCell์—์„œ ์ œ์•ฝ ์ถฉ๋Œ ๋ฐœ์ƒ

image

`estimatedSize`์™€ ์‹ค์ œ ์‚ฌ์ด์ฆˆ ๊ฐ„ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ๋‹ค

ํ•ด๊ฒฐ

์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜๋Š” iconImageView์˜ ๋†’์ด ์ œ์•ฝ์˜ priority๋ฅผ ๋‚ฎ๊ฒŒ ์กฐ์ •ํ–ˆ๋‹ค.

    let heightAnchor = iconImageView.heightAnchor.constraint(
        equalToConstant: design.iconImageViewHeight)
    heightAnchor.priority = .init(rawValue: 750)

TextSymbolView๋ฅผ UIImage๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ

๋ฌธ์ œ ์ธ์‹ : ๋ทฐ์— ํฌํ•จ๋œ ๋กœ์ง

SummaryCollectionViewCell์˜ symbolImage์— ์‚ฌ์šฉ๋˜๋Š” ์ด๋ฏธ์ง€๋Š” ๋‘ ๊ฐ€์ง€ ํƒ€์ž…์ด ์žˆ์Šต๋‹ˆ๋‹ค

  1. UIImage(systemName:) : SFSymbol๋กœ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์ด๋ฏธ์ง€
  2. 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ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•˜๊ธฐ

UIView์˜ ์ต์Šคํ…์…˜์— UIImage๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

extension UIView {

    func toImage() -> UIImage {
        let renderer = UIGraphicsImageRenderer(bounds: bounds)
              return renderer.image { rendererContext in
                  layer.render(in: rendererContext.cgContext)
              }
      }
    
}

ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ… : Label์ด ๋ทฐ์˜ ์ค‘์•™์— ์˜ค์ง€ ์•Š๋Š” ๋ฌธ์ œ

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2022-09-05 แ„‹แ…ฉแ„’แ…ฎ 9 17 39

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)
        ])
        
    }
    
}

SavedAppDetailTableViewCell ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ ์ง€์—ฐ ๊ฐœ์„ 

Before & After

๊ฐœ์„  ์ „ ๊ฐœ์„  ํ›„
แ„€แ…ขแ„‰แ…ฅแ†ซแ„Œแ…ฅแ†ซย  แ„€แ…ขแ„‰แ…ฅแ†ซแ„’แ…ฎ low27

๋ฌธ์ œ ์ƒํ™ฉ : Cell์— ๋ฐ์ดํ„ฐ ํ‘œ์‹œ ์ง€์—ฐ

  • cellForRowAt์—์„œ ๋„คํŠธ์›Œํฌ ๋ฐ ๋ ๋ฆ„์—์„œ CellModeld์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋ฐ›์•„์˜ค๋Š” Publisher๋ฅผ ๋งŒ๋“  ํ›„, cell์— bindํ•ด์ฃผ๊ณ  ์ŠคํŠธ๋ฆผ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.
  • cell์ด ๋””ํ๋˜๊ธฐ ์ง์ „์— ๋น„๋™๊ธฐ์ž‘์—…์ด ์‹œ์ž‘๋˜๋ฏ€๋กœ, cell์— ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ๊นŒ์ง€ ์ง€์—ฐ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
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๋ฉ”์„œ๋“œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กค์„ ํ•ด์•ผ ํ˜ธ์ถœ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋งจ ์ฒ˜์Œ ๋ณด์—ฌ์ง€๋Š” ์…€์€ ์—ฌ์ „ํžˆ ์ง€์—ฐ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

View ๊ตฌํ˜„

  • AppSearchViewController ๊ตฌํ˜„

  • AppDetailView component ๊ตฌํ˜„
  • AppDetailViewController ๊ตฌํ˜„

์˜คํ”ˆ API๋กœ๋ถ€ํ„ฐ ์„ธ๊ณ„ ๊ตญ๊ฐ€ ISO ์ฝ”๋“œ ๋ฐ ๊ตญ๊ฐ€๋ช…์„ ์ƒ์„ฑ

๋ชฉ์ 

  • ๊ธฐ์กด AssetData(JSON)๋Š” ์˜์–ด ๊ตญ๊ฐ€๋ช…๋งŒ์„ ์ œ๊ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ํ•œ๊ตญ์–ด ๊ตญ๊ฐ€๋ช…์„ ์ œ๊ณตํ•  ์ˆ˜ ์—†์—ˆ์Œ
  • ๋”ฐ๋ผ์„œ ๊ณต์ธ๋œ ๊ธฐ๊ด€์—์„œ ์ตœ์‹  ๊ตญ๊ฐ€ ISO ์ฝ”๋“œ ๋ฐ ๊ตญ๊ฐ€๋ช…(์˜์–ด,ํ•œ๊ตญ์–ด)๋ฅผ ์ œ๊ณต๋ฐ›๊ธฐ ์œ„ํ•ด ๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ์˜ API๋ฅผ ์‚ฌ์šฉ

API ์ •๋ณด

์™ธ๊ต๋ถ€_๊ตญ๊ฐ€ยท์ง€์—ญ๋ณ„ ํ‘œ์ค€์ฝ”๋“œ

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

  1. ๊ณต๊ณต๋ฐ์ดํ„ฐํฌํƒˆ์—์„œ API ํ™œ์šฉ ์‹ ์ฒญ
  2. API Call Test
  3. JSON ํŒŒ์‹ฑ ํƒ€์ž… ์ •์˜ (CountryCodeList, CountryCode) & Country ํƒ€์ž… ์ˆ˜์ •
  4. CountryCodeAPIService, CountrtyCodeListRequest ๊ตฌํ˜„
  5. AppDelegate์˜ didFinishLaunchingWithOptions์—์„œ API ํ˜ธ์ถœ์„ ํ†ตํ•ด CountryCode ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์€ ํ›„ Countryํƒ€์ž…์œผ๋กœ ๋งคํ•‘ํ•˜์—ฌ ํƒ€์ž… ํ”„๋กœํผํ‹ฐ(Country.list)์— ์ €์žฅ
  6. HTTP ํ†ต์‹ ์œผ๋กœ ์ธํ•œ ATS ์œ„๋ฐ˜ ์ด์Šˆ๋ฅผ ํ”„๋กœํผํ‹ฐ ๋ฆฌ์ŠคํŠธ ์ˆ˜์ •์œผ๋กœ ํ•ด๊ฒฐ
  7. ๊ตญ๊ฐ€ ์„ค์ •์— ๋”ฐ๋ผ ์•Œ๋งž์€ ์–ธ์–ด์˜ ๊ตญ๊ฐ€๋ช…์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ”„๋กœํผํ‹ฐ Country์— ๊ตฌํ˜„
  8. ๊ตญ๊ฐ€๋ช…์„ ์‚ฌ์šฉํ•˜๋Š” ๋ทฐ๋ชจ๋ธ์—์„œ ์œ„ ํ”„๋กœํผํ‹ฐ ์‚ฌ์šฉํ•˜๋„๋ก ์ˆ˜์ •

๊ตฌํ˜„ ์ค‘ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

๐ŸšจResponse Type์ด xml๋กœ ์˜ค๋Š” ๋ฌธ์ œ

๐Ÿ“ํ•ด๊ฒฐ ๊ณผ์ •์„ ๋ธ”๋กœ๊ทธ์— ์ •๋ฆฌํ•˜์—ฌ ๊ณต์œ 

๋ฌธ์ œ ์ •์˜
ํฌ์ŠคํŠธ๋งจ์—์„œ๋Š” ์ •์ƒ์  ์‘๋‹ต์„ ๋ฐ›๋˜ ๊ฒƒ์ด,
์•ฑ์—์„œ ์‹คํ–‰ํ•˜๋‹ˆSERVICE_KEY_IS_NOT_REGISTERED_ERROR ๋ผ๋Š” ์˜ค๋ฅ˜ xml์ด ์˜ด

๋ฌธ์ œ ์›์ธ ๋ถ„์„

  • URLComponent์™€ ๊ณต๊ณต๋ฐ์ดํ„ฐํฌํ„ธ์ด ๊ฐ๊ฐ ์‚ฌ์šฉํ•˜๋Š” api key๋ฅผ ์ธ์ฝ”๋”ฉํ•˜๋Š” ๋ฐฉ์‹์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  • percentEncodedQuery์˜ '+'๋ฅผ '%2B' ๋ณ€ํ™˜ 5c7acb6
var 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
    }

๐Ÿšจย ๋Ÿฐ์น˜๊ฐ€ ๋๋‚œ ํ›„์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋„์ฐฉํ•ด์„œ ๊ธฐ์กด Country ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†๋Š” ๋ฌธ์ œ

๋ฌธ์ œ ์ •์˜

  • 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)
                }
            }
        }

์•ฑ ์ €์žฅ, ํด๋”๋ง ๊ธฐ๋Šฅ ๊ตฌํ˜„

์•ฑ ์ €์žฅ, ํด๋”๋ง ๊ธฐ๋Šฅ ๊ตฌํ˜„

  • ๊ด€์‹ฌ ์žˆ๋Š” ์•ฑ์„ ํด๋”์— ์ €์žฅ
  • ํด๋”๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์•ฑ์„ ๋ถ„๋ฅ˜ํ•ด์„œ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›

๊ตฌํ˜„ ์‚ฌํ•ญ

Model

  • AppFolder ์ •์˜
  • SavedApp ์ •์˜

Repository

  • AppFolderRepository๊ตฌํ˜„
    • ๊ทธ๋ฃน ์ƒ์„ฑ
    • ๋ชจ๋“  ๊ทธ๋ฃน, ํŠน์ • ๊ทธ๋ฃน ์ฝ๊ธฐ
    • ๊ทธ๋ฃน ์•„์ดํ…œ ์ถ”๊ฐ€/์‚ญ์ œ, ๊ทธ๋ฃน๋ช… ์ˆ˜์ •
    • ๊ทธ๋ฃน ์‚ญ์ œ

UI

  • AppGroupListView
    • ์…€ ์Šค์™€์ดํ”„ (์ขŒ: ๊ทธ๋ฃน๋ช… ์ˆ˜์ •, ์šฐ: ์‚ญ์ œ)
    • ํ…Œ์ด๋ธ” ๋ทฐ
  • AppGroupView
    • ์„ ํƒ ์‚ญ์ œ
    • AppTableViewCell

CompositionalLayout์—์„œ CustomCollectionViewCell ์‚ฌ์šฉ์‹œ ๋ ˆ์ด์•„์›ƒ ์ด์Šˆ

๋ฌธ์ œ ์ƒํ™ฉ

  • customCollectionViewCell์„ ๋งŒ๋“ค ๋•Œ, UIImageView์˜ ์‚ฌ์ด์ฆˆ๋ฅผ ์ƒ์ˆ˜๋กœ ์„ค์ •ํ–ˆ๋‹ค.
final class ScreenShotCollectionViewCell: UICollectionViewCell {
    
    private let screenShotView = UIImageView(
        frame: CGRect(x: 0, y: 0, width: 280, height: 500))
 ...
}
  • Cell์‚ฌ์ด์ฆˆ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ CompositionalLayout์˜ item๊ณผ group์‚ฌ์ด์ฆˆ๋ฅผ ๊ณ„์‚ฐํ•˜๊ธฐ ์œ„ํ•ด .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)
  • ์Šคํฌ๋กคํ•˜๋‹ˆ section์ด ์‚ฌ๋ผ์ง€๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.
    Simulator Screen Recording - iPhone 13 mini - 2022-08-29 at 16 58 42

์•ฑ ํด๋” CRUD ๋ฒ„๊ทธ

๐Ÿšจ ๋ฌธ์ œ ์„ค๋ช…

๋ฌธ์ œ

  1. AppFolderSelectView - > AppFolderCreator present ์•ˆ๋˜๋Š” ๋ฒ„๊ทธ
  2. AppFolderSelectView ์ „ํ™˜์‹œ ๊ธฐ์กด ์„ ํƒ ํด๋” ์ฒดํฌ ์•ˆ๋จ
  3. savedApp 0๊ฐœ์ผ ๋•Œ defaultIconImage ํ‘œ์‹œ ์•ˆ๋จ

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฌธ์ œ ๋ถ„์„ & ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. AppFolderSelectView - > AppFolderCreator present ์•ˆ๋˜๋Š” ๋ฒ„๊ทธ
  • AppFolderSelector์˜ coordinator๊ฐ€ nil๋กœ ๋ฐํ˜€์ง
  • Coordinator๋ฅผ ํ• ๋‹นํ•ด์ฃผ์ง€ ์•Š์•„์„œ ๋ฐœ์ƒ
  • AppFolderSelectorCoordinator์—์„œ
  1. AppFolderSelectView ์ „ํ™˜์‹œ ๊ธฐ์กด ์„ ํƒ ํด๋” ์ฒดํฌ ์•ˆ๋จ
    • AppFolderRealmRepository์—์„œ savedApp์˜ AppFolder๋ฅผ ์ฝ์„ ๋•Œ, ์ €์žฅ๋˜์–ด์žˆ๋˜ savedApp์„ ์ฝ์–ด์˜ค์ง€ ๋ชปํ•˜๋Š” ๋ฌธ์ œ
    • savedApp์„ fetchํ•˜๊ณ  filterํ•  ๋•Œ ์‚ฌ์šฉํ•œ ๊ฒ€์ƒ‰ ๊ตญ๊ฐ€๋ช…(englsihName)๊ณผ savedApp์„ ์ €์žฅํ•  ๋•Œ ์‚ฌ์šฉํ•œ ๊ตญ๊ฐ€๋ช…(isoCode)๊ฐ€ ๋‹ฌ๋ž๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒ
    • filter ์กฐ๊ฑด์„ isoCode๋กœ ํ†ต์ผ
    • searchingConturyISOCode๋กœ ๋ณ€์ˆ˜๋ช… ๋ช…ํ™•ํ•˜๊ฒŒ ์ˆ˜์ •

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป๊ฒฐ๊ณผ์™€ ํ”ผ๋“œ๋ฐฑ

  • ์•ฑํด๋”์— CRUDํ•˜๋Š” ์ž‘์—… ์ •์ƒํ™”
  • SavedApp์„ identifyํ•˜๋Š” property๋“ค์„ ์บก์Šํ™”ํ•˜์—ฌ ์ด์ „๊ณผ ๊ฐ™์€ ํ˜ผ๋™์ด ์—†๋„๋ก ๊ฐœ์„  ํ•„์š”

AnimatableArrow ๋ฐฉํ–ฅ ์˜คํ‘œ์‹œ ๋ฌธ์ œ

๐Ÿšจ ๋ฌธ์ œ ์„ค๋ช…

๋ฌธ์ œ

  • SearchView์˜ ํ™”์‚ดํ‘œ(AnimatableArrow)๊ฐ€ iPad, Mac์„ ๊ฐ€๋ฆฌํ‚ฌ ๋•Œ, ํ™”์‚ดํ‘œ๊ฐ€ 12์‹œ๋ฐฉํ–ฅ์œผ๋กœ ํ‘œ์‹œ๋˜์–ด ๋ถ€์ž์—ฐ์Šค๋Ÿฌ์›€

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฌธ์ œ ๋ถ„์„

  • ๊ธฐ์กด setPostition์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ํ™”์‚ดํ‘œ์˜ ์œ„์น˜๋งŒ ์ •ํ™•ํ•˜๊ณ , ํ™”์‚ดํ‘œ๋Š” ํšŒ์ „ํ•˜์ง€ ์•Š์•˜์Œ
    • startAngle๊ณผ endAngle์ด ๋™์ผ
    • rotationMode ์ ์šฉํ•˜์ง€ ์•Š์Œ
    • fillMode์ ์šฉํ•˜์ง€ ์•Š์Œ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  • ๋”ฐ๋ผ์„œ animate(to:)์™€ ๋™์ผํ•˜๊ฒŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํŠน์ง•์„ ์ถ”๊ฐ€ํ•˜๊ณ , startAngle์€ iPhone์„ ๊ฐ€๋ฆฌํ‚ค๋Š” 1.5 * .pi ๋กœ ๊ณ ์ •
  • duratioin์„ ์•„์ฃผ ์งง์€ ์‹œ๊ฐ„์„ ํ• ๋‹นํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์—†์ด ์›๋ž˜ ๊ทธ ์œ„์น˜์ธ ๊ฒƒ์ฒ˜๋Ÿผ ๊ตฌํ˜„

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป๊ฒฐ๊ณผ์™€ ์„ฑ๊ณผ

  • SearchView๋ฅผ ์ฒ˜์Œ ๋„์—ˆ์„ ๋•Œ, iPad ๋˜๋Š” mac์ผ์‹œ ํ™”์‚ดํ‘œ ๋ฐ”๋‹ฅ์ด ์›์— ๋ถ™์–ด์„œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ‘œ์‹œ

APIService ๊ตฌํ˜„

  • HTTP ํ†ต์‹  ๋ชจ๋“ˆ APIRequest
  • APIService ์ธํ„ฐํŽ˜์ด์Šค ์ •์˜
  • ItunesAPIService ๊ตฌํ˜„
  • Parsing Test

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.