SwiftUI 에서 CoreLocation 사용
이 전 블러그에서 텍스트 필드에서 입력받은 도시의 날씨를 표현하는 앱을 구현하였다. 이미지 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를 구현하고 날씨 정보를 가져오는 동안 로딩화면을 이를 통해 표현할 수 있도록 추가해 본다.