-
SwiftUI 에서 CoreLocation 사용SwiftUI/MVVM 알아보기 2020. 8. 27. 15:49
이 전 블러그에서 텍스트 필드에서 입력받은 도시의 날씨를 표현하는 앱을 구현하였다. 이미지 location.fill 위치에 버튼을 추가하고 CoreLocation을 이용해 현재 좌표에 대한 날씨를 조회하도록 기능을 추가해 보자.
이 보다 먼저 ContentView의 body에서 상단 바와 날씨 정보를 보여주는 위젯을 Extract Subview를 이용해 두 영역을 하위 뷰로 분리한다.
위의 그림처럼 분리하고자 하는 VStack에 커서를 두고 ⌘ 누르고 마우스의 왼쪽키를 클릭하면 메뉴바가 나오는데 여기서 Extract Subview를 통해서 쉽게 분리할 수 있다. 뷰는 아래와 같은 구조를 가진다.
struct ContentView: View { ... var body: some View { ZStack { ... } VStack { //SearchBar 영역 WeatherSearchBar(viewModel: viewModel) //날씨 위젯 if viewModel.isLoading == false { WeatherWidget(viewModel: viewModel) } // 위젯을 위로 올리기 위해 스페이서 설정 Spacer() } } } } struct WeatherWidget: View { @ObservedObject var viewModel : WeatherViewModel var body: some View { ... } } struct WeatherSearchBar: View { @ObservedObject var viewModel : WeatherViewModel @State private var searchTerm = "" var body: some View { ... } }
LocationService 클래스
CoreLocation 프레임워크를 사용해 현재 위치를 가져오기 위해서는 info.plist에 'Privacy - Location When In Use Usage Description'
를 추가해 해당 앱이 위치정보를 요청할 경우 사용자에게 이를 승인받기 위한 문구를 추가한다.
LocationService는 CLLocationManager를 생성하고 해당 객체의 delegate 프로토콜을 구현해 CLLocationManager의 delegate로 설정한다. 로케이션 매니저에 위치정보를 요청한 경우 locationManager(_manager:didUpdateLocations locations:) delegate 함수를 통해서 위치 정보를 가져올 수 있다. LocationService는 LocationServiceDelegate를 통해서 이를 다른 클래스에 전달하는 대신 위치 정보 요청 시 completionHandler를 등록해 이를 통해 위치 정보를 전달하도록 구현해 구현을 간결하게 한다.
이를 아래와 같이 구현할 수 있다.
LocationService.swift
import Foundation import CoreLocation class LocationService : NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() var completionHandler: ((CLLocationCoordinate2D) -> (Void))? override init() { super.init() //CLLocationManager의 delegate 설정 manager.delegate = self //manager.desiredAccuracy = kCLLocationAccuracyBest //위치 정보 승인 요청 manager.requestWhenInUseAuthorization() } //위치 정보 요청 - 정보 요청이 성공하면 실행될 completionHandler를 등록 func requestLocation(completion: @escaping ((CLLocationCoordinate2D) -> (Void))) { completionHandler = completion manager.requestLocation() } //위치 정보는 주기적으로 업데이트 되므로 이를 중단하기 위한 함수 func stopUpdatingLocation() { manager.stopUpdatingHeading() } //위치 정보가 업데이트 될 때 호출되는 delegate 함수 func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } //requestLocation 에서 등록한 completion handler를 통해 위치 정보를 전달 if let completion = self.completionHandler { completion(location.coordinate) } //위치 정보 업데이트 중단 manager.stopUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print(error) } }
URL+Extensions.swift 에 위치 좌표 값을 통해 현재 날씨 정보를 로드하기 위한 REST API를 아래와 같이 추가한다.
import Foundation import CoreLocation let appid = "" let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=\(appid)&units=metric" extension URL { //도시 이름을 통해 날씨 정보를 가져오는 GET Rest API static func urlWith(city: String) -> URL? { return URL(string: "\(weatherURL)&q=\(city)") } //위치 값을 통해 날씨 정보를 가져오는 GET Rest API static func urlWith(coordinate : CLLocationCoordinate2D) -> URL? { let urlString = "\(weatherURL)&lat=\(coordinate.latitude)&lon=\(coordinate.longitude)" print(urlString) return URL(string: urlString) } }
LocationService를 통해 WeatherViewModel은 현재 위치에 대한 정보를 가져오는 함수 fetchWeatherDataWithCurrentLocation() 함수는 아래와 같다.
WeatherViewModel.swift
func fetchWeatherDataWithCurrentLocation() { //locationService의 requestLocation 호출 locationService.requestLocation { coordinate in //좌표 값을 수신한 경우 fetchWeatherData를 통해서 api 호출 self.fetchWeatherData(coordinate: coordinate) } } private func fetchWeatherData(coordinate: CLLocationCoordinate2D) { let url = URL.urlWith(coordinate: coordinate) guard let safeUrl = url else { return } load(resource: Resource<WeatherData>(url: safeUrl)) } private func load(resource: Resource<WeatherData>) { isLoading = true URLRequest.load(resource: resource) { (result) in switch result { case .success(let weatherData) : self.model = weatherData case .failure(let error) : print(error) } self.isLoading = false } }
이제 뷰에서 초기화 시 뷰 모델의 fetechWeatherDataWithCurrentLocation() 함수를 호출해 현재 위치의 날씨를 표현 하도록 하고 location 아이콘을 클릭 시 현재 위치의 날씨를 보여주도록 아래와 같이 추가해준다.
struct ContentView: View { ... init() { print("content view init") viewModel = WeatherViewModel() viewModel.fetchWeatherDataWithCurrentLocation() } } struct WeatherSearchBar: View { @ObservedObject var viewModel : WeatherViewModel //TextField에 바인딩 - TextField 의 text 값을 얻는 데 사용됨 @State private var searchTerm = "" var body: some View { VStack { HStack(spacing: 16.0) { //현재 위치에 대한 날씨 정보 요청 버튼 Button (action: { viewModel.fetchWeatherDataWithCurrentLocation() }, label: { Image(systemName: "location.fill") .font(.headline) .frame(width: 30, height: 30) .foregroundColor(.white) .background( Circle() .stroke(Color.white, lineWidth: 3.0) .shadow(color: Color.black.opacity(0.8), radius: 10, x: 10, y: 10) ) }) ... //텍스트 필드 에 입력된 도시이름으로 날씨 정보 요청 Button(action: { viewModel.fetchWeatherData(city: searchTerm) }, label: { Image(systemName: "magnifyingglass") .font(.headline) .frame(width: 30, height: 30) .foregroundColor(.white) .background( Circle() .stroke(Color.white, lineWidth: 3.0) .shadow(color: Color.black.opacity(0.8), radius: 10, x: 10, y: 10) ) }) }.padding() } } }
다음 글에서는 Lottie 이미지를 출력하는 LottieView를 구현하고 날씨 정보를 가져오는 동안 로딩화면을 이를 통해 표현할 수 있도록 추가해 본다.
'SwiftUI > MVVM 알아보기' 카테고리의 다른 글
lottie-ios 프레임워크와 UIViewRepresentable 을 이용한 LoadingView 구현 (0) 2020.08.27 SwiftUI MVVM 모델 (0) 2020.08.27 iOS 의 MVC 모델 (0) 2020.08.26