ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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를 구현하고 날씨 정보를 가져오는 동안 로딩화면을 이를 통해 표현할 수 있도록 추가해 본다. 

    댓글

Designed by Tistory.