- 맑음 / 흐림 / 비 / 눈 의 배경색을 다르게 하여 앱을 켜자마자 현재 날씨를 알 수 있도록 함
- UIKit
- UI - Code
- CLLocation, CLGeocoder
- URLSession
- MVC → MVVM 리팩토링
반복되는 API호출을 줄여, 간단한 코드로 줄일 수 없을까? (제네릭 문법 활용)
💡 문제상황: API 4개를 사용하고 있는 네트워킹 함수가 있지만, 반복되는 요소들이 눈에 띔. 어떻게 반복되는 코드를 줄일 수 있을까?
-
Result 결과값들이 모두 Decodable을 채택하는 공통점을 이용하여 제네릭 문법을 활용
-
URL을 생성하는 함수는 각각 만들고, 네트워킹하는 함수는 하나로 만들어 반복되는 요소를 줄임
// 각 API의 URL을 생성하는 함수들 func fetchCurrentWeather(lat: String, lon: String, completion: @escaping (Result<CurrentWeather, NetworkError>) -> Void) func fetch3DaysForecast(lat: String, lon: String, completion: @escaping (Result<Forecast3Days, NetworkError>) -> Void) func fetchWeekWeather(location: String, date: String, completion: @escaping (Result<WeekWeather, NetworkError>) -> Void) func fetchWeekWeatherRainAndImage(location: String, date: String, completion: @escaping (Result<WeekWeatherRainAndImage, NetworkError>) -> Void
// URLSession.shared.dataTask(with: request) 하는 함수 func performRequest<T: Decodable>(request: URLRequest, completion: @escaping (Result<T, NetworkError>) -> Void)
2개 이상의 비동기 함수가 종료되는 시점 확인하기 (DispatchGroup 활용)
💡 문제 상황: 날씨 API의 2개의 결과값을 동시에 사용(위도/경도 및 날씨 관련 이미지)해야 하는데, 어떻게 하면 끝나는 시점이 각기 다른 2개의 비동기 함수들이 모두 종료되는 시점을 어떻게 파악할 수 있을까?
-
디스패치 그룹을 활용하여, 비동기적으로 동작하는 2개의 함수 시점을 파악
-
DispatchGroup()을 생성하여.enter().leave()을 활용 -
해당 그룹이 종료되는 시점을 notify메서드를 활용, 해당 시점에 대해 노티를 받음
func fetchWeekWeatherDataWith(regionCode: String, imageRegionCode: String) { **let group = DispatchGroup() group.enter()** networkManager.fetchWeekWeather(location: regionCode, date: Date.currentDataForWeek()) { result in switch result { case .success(let weekWeather): guard let weekWeatherTemp = weekWeather.response?.weekWeatherResponse?.weekWeatherItems?.weekWeatherTempList?.first else { return } self.weekWeatherTemp = weekWeatherTemp **group.leave()** case .failure(let error): print(error.localizedDescription) } } **group.enter()** networkManager.fetchWeekWeatherRainAndImage(location: imageRegionCode, date: Date.currentDataForWeek()) { result in switch result { case .success(let weekWeatherImage): guard let weekWeatherRainAndImage = weekWeatherImage.response?.weekWeatherImageResponse?.weekWeatherImageItems?.weekWeatherRainAndImageList?.first else { return } self.weekWeatherRainAndImageData = weekWeatherRainAndImage **group.leave()** case .failure(let error): print(error.localizedDescription) } } **group.notify**(queue: .global()) { [weak self] in guard let temp = self?.weekWeatherTemp, let rainAndImage = self?.weekWeatherRainAndImageData else { return } self?.convertWeekWeather(temp: temp, rainAndImage: rainAndImage) } }
데이터 관리 계층(Layer)과 MVVM 아키텍처로 리팩토링 (향후 유지보수를 고려한 설계로 리팩토링)
💡 문제 상황: MVC구조에서, 3개의 탭이 존재하고 각 뷰컨트롤러(뷰)에 진입시마다 불필요한 네트워킹 데이터요청이 지속적으로 일어남. 불필요한 반복적인 네트워크 요청을 어떻게 줄일 수 있을까? 또한 (날씨) 데이터 관리를 보다 효율적으로 할 수 없을까?
- MVC 기존구조 - 각 탭에서 불필요하게 반복적으로 네트워크 요청 및 효율적이지 않은 (날씨) 데이터 관리
- MVVM + 데이터 관리 계층 추가 (불필요한 반복적인 네트워크 요청을 줄이고, 효율적으로 데이터를 관리)
- 데이터 관리 계층 추가와 MVVM의 뷰모델 추가로 인해 비대한 뷰컨트롤러에서 벗어나 보다 효율적인 프로젝트 관리로 유지 가능해졌음
반복되는 네트워킹 호출 줄이기, 비동기적인 데이터 호출 후에 데이터 생성 시점 파악하기 (데이터 관리 Layer 추가와 노티피케이션을 활용한 데이터 바인딩)
💡 문제상황: 각 탭마다 네트워킹 함수와 CoreLocation을 호출하여 켜지는데 시간이 소요되고, 반복되는 네트워킹 함수 호출이 많음 → 어떻게 하면 유저의 더 나은 경험이 되도록 하고, 반복되는 네트워킹 함수 호출을 줄일 수 있을까?
-
(각 화면이 아닌) 날씨 데이터를 관리하는 객체(
DataManager)를 따로 분리해서 날씨 데이터를 관리하도록 하고(한 번에 네트워킹 호출), 노티피케이션을 이용해서 데이터 생성 시점에 각 탭에 뷰 그리기- 배열들과 함수를 데이터 관리 객체 한 곳에 모아 가독성을 높이고
14개의 반복되는 함수를5개로 줄일 수 있었음 - 데이터 관리 객체 Layer에서 데이터 생성 시점을 노티피케이션을 활용해, 시점을 전달 했음 (노티피케이션을 활용한 데이터 바인딩)
// 변수마다 노티피케이션을 부여해, 데이터 변경시점에 노티피케이션을 통해 알리고, 유동적으로 뷰를 다시 그리도록 함 extension Notification.Name { static let cityName = Notification.Name("cityName") static let mainWeather = Notification.Name("mainWeather") static let todayWeatherList = Notification.Name("todayWeatherList") static let tomorrowWeatherList = Notification.Name("tomorrowWeatherList") static let dayAfterTomorrowWeatherList = Notification.Name("dayAfterTomorrowWeatehrList") static let weekWeatherList = Notification.Name("weekWeatherList") } private func setupNotification() { NotificationCenter.default.addObserver(self, selector: #selector(configureUI), name: .cityName, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(configureUI), name: .mainWeather, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(configureUI), name: .todayWeatherList, object: nil) } deinit { NotificationCenter.default.removeObserver(self) }
- 배열들과 함수를 데이터 관리 객체 한 곳에 모아 가독성을 높이고
정확한 위치기반이나 기준이 없는 날씨 API를 이용해야하는 문제점 (기상청 API 예보구역코드 매칭 문제)
💡 문제상황: 기상청 API를 호출할 때 필수로 예보구역코드가 필요한데 201개로 나누어진 코드를 어떻게 적용시켜야 할까?
-
예보구역코드는 대한민국을 (특별한 기준이 없이 임의로) 201개의 도시로 나누어 놓음 (이에 대해 나누는 정확한 기준이 나와있지 않아)
-
CLGeocoder의
placemark.administrativeArea를 기준으로 임의로 나눔 -
16개의 시/도로 나누고 예보구역코드에 나와있는 도시 중 해당 시/도에서 인구가 가장 많은 도시의 코드를 매치시킴
let geocoder = CLGeocoder() let local = Locale(identifier: "Ko-kr") geocoder.reverseGeocodeLocation(location, preferredLocale: local) { (placemarks, error) in if let error = error { print("Error: \(error.localizedDescription)") return } if let placemark = placemarks?.first { if let cityName = placemark.locality, let subCityName = placemark.subLocality { guard let city = placemark.administrativeArea else { return } let regionCode = self.getRegionCodeForWeek(city: city) } }
func getRegionCodeForWeek(city: String) -> String { switch city { case "서울특별시": return "11B10101" case "부산광역시": return "11H20201" case "인천광역시": return "11B20201" case "대구광역시": return "11H10701" case "대전광역시": return "11C20401" case "광주광역시": return "11F20501" case "경기도": return "11B20601" case "울산광역시": return "11H20101" case "충청남도": return "11C20301" case "충청북도": return "11C10301" case "경상남도": return "11H20301" case "경상북도": return "11H10201" case "전라남도": return "11F20401" case "전라북도": return "11F10201" case "제주특별자치도": return "11G00201" case "강원도": return "11D10401" default: return "11B10101" } }
런치스크린 시점 조절을 통한 자연스러운 UX구현 (앱 런칭시 네트워킹을 시작하고, 데이터 생성시점에 런치스크린 종료 구현)
💡 문제상황: 앱 런칭 시 필수적으로 날씨 데이터를 가져오기 위한 네트워킹이 진행되고, 네트워킹 동안 유저가 (하얀색 화면이 아닌) 런치스크린을 보다가 자연스럽게 “데이터 생성시점”에 앱의 첫번째 화면으로 넘어가려면 어떻게 구현해야 할까?
-
네트워킹이 끝나는 시점에 rootViewController를 첫 탭으로 할당해 런치스크린 종료 시점을 파악하여 이동시키도록 구현
// 선언부 func setupCoreLocation(completion: @escaping () -> Void) { locationManager.delegate = self locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingLocation() completion() }
// 실행부 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { WeatherDataManager.shared.setupCoreLocation { DispatchQueue.main.async { if let windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { if let window = windowScene.windows.first { window.rootViewController = TodayViewController() } } } return true } }