티스토리 뷰

새싹 과제로 날씨 API를 이용하여 날씨 앱을 만들어보았다.

 

현대카드 웨더 메인 화면 UI만 따라하고, 날씨를 보여주는 기능을 제외한 나머지는 구현하지 않았다.

 

 

 

구현한 기능은 간단히 아래와 같다.

 

 

현재 시간

 

-> 왼쪽 상단에 현재 시간 표시.

 

 

위치

 

카카오 로컬 API 사용

 

-> 위치 권한 요청

 

-> 위치 권한이 허용되지 않은 상태인 경우, Label에 서울만 표시

 

-> 위치 권한이 허용된 상태인 경우, 동까지 표시

 

 

날씨

 

openweather API 사용

 

-> 이미지 URL을 이용해 이미지 처리

 

-> 현재 날씨, 기온, 풍속, 습도, 기압 그리고 날씨에 따른 간단한 메세지 표시

 

-> 위치 정보를 받아왔을 땐, 현재 위치의 날씨 정보

 

-> 위치 정보를 받아오지 못했을 땐, 새싹 캠퍼스 위치의 날씨 정보

 

 

폰트

 

-> 눈누에서 커스텀 폰트를 받아와 적용.

 

 

 

이제 각각 세부적으로 어떤식으로 구현하였는지 작성해보겠다.

 

 

 

현재 시간

 

 

 

Timer를 이용하여 1초마다 현재 시간을 레이블에 보여주는 메서드를 반복해주었다.

func getCurrentTime() {
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector:#selector(self.currentTime), userInfo: nil, repeats: true)
}
    
@objc func currentTime() {
    let formatter = DateFormatter()
    formatter.dateFormat = "MM. dd(E) HH:mm a"
    formatter.amSymbol = "AM"
    formatter.pmSymbol = "PM"
    formatter.locale = Locale(identifier: "ko")
    timeLabel.text = formatter.string(from: Date())
}

 

dateFormat에서 요일과 am, pm을 나타내는 이니셜을 처음 알았다.

 

 

요일은 E를 사용하고, am과 pm은 a를 사용한다.

 

 

이때, amSymbol이나 pmSymbol을 이용하여 따로 작성해주지않으면 소문자로 출력이 된다.

(현대카드 웨더 앱과 동일하게 하기 위해 대문자로 바꿔준 것임.)

 

 

요일을 사용할때, locale을 작성해야 월, 화, 수, ...처럼 출력할 수 있다.

현재 시간이 변하면 레이블도 동시에 변하는 것을 확인할 수 있다.

 

 

위치

 

 

 

좌표를 이용하여 주소를 얻어오기 위해서는 위치 권한이 허용되어야 한다.

 

 

위치 권한 허용을 요청하는 과정은 아래 글에 따로 정리하였다.

-> https://heecheol.tistory.com/86

 

 

추가적인 메서드를 제외하고 기본적인 위치 권한 요청 코드가 작성된 상태에서 계속 진행해보겠다.

 

 

우선, 네트워크 통신은 Alamofire와 SwiftyJson 오픈 소스 라이브러리를 이용하였다.

// 주소 모델
struct AddressModel {
    let regionFirst: String
    let regionThird: String
}

// 네트워크 통신
import Alamofire
import SwiftyJSON

class AddressAPIManager {
    
    private init() {}
    
    static let shared = AddressAPIManager()
    
    func getLocationData(lat: Double, lon: Double, completionHandler: @escaping (AddressModel) -> ()) {
        
        let url = Endpoint.kakaoAddress + "x=\(lon)&y=\(lat)&input_coord=WGS84"
        
        let header: HTTPHeaders = [
            "Authorization": "KakaoAK \(APIKey.kakao)"
        ]
        
        AF.request(url, method: .get, headers: header).validate().responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let address = json["documents"].arrayValue[0]["address"]
                
                let first = address["region_1depth_name"].stringValue
                let third = address["region_3depth_name"].stringValue
                
                completionHandler(AddressModel(regionFirst: first, regionThird: third))
                            
            case .failure(let error):
                print(error)
            }
        }
        
    }
}

싱글턴 패턴을 이용해 API Manager를 작성해주었다.

 

 

받아오는 데이터 중 지역만 사용할 것이므로 1depth와 3depth만을 사용하였다.

 

 

네트워크 통신은 위치 권한이 허용된 상태일때와 허용되지 않은 상태일때를 나눠주었다.

 

 

허용되지 않은 상태는 CLAuthorizationStatus의 값이 denied이거나 restricted일 것이다.

 

 

그래서 두 경우에 기본적으로 설정해둔 새싹 캠퍼스의 좌표로 네트워크 통신을 하도록 하였다.

func checkLocationServiceAuthorizationStatus() {
        
    let authorizationStatus: CLAuthorizationStatus
        
    authorizationStatus = locationManager.authorizationStatus
        
    if CLLocationManager.locationServicesEnabled() {
        checkCurrentLocationAuthorizationStatus(authorizationStatus)
    } else {
        print("위치 권한 확인하세요.")
    }
}
    
func checkCurrentLocationAuthorizationStatus(_ authorizationStatus: CLAuthorizationStatus) {
    
    switch authorizationStatus {
    case .notDetermined:
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
    case .restricted:
        //기본적으로 새싹캠퍼스의 날씨지만, 레이블은 서울만 표시(현대카드 웨더처럼)
        allHidden()
        locationButton.image = UIImage(systemName: "location")
        hud.show(in: view)
        AddressAPIManager.shared.getLocationData(lat: lat, lon: lon) { value in
            self.locationLabel.text = "\(value.regionFirst)"
            
            WeatherAPIManager.shared.getWeatherData(lat: self.lat, lon: self.lon) { value in
                //첫번째 뷰
                let imageURL = URL(string: Endpoint.imageURL + "\(value.iconId)@2x.png")
                self.weatherImageView.kf.setImage(with: imageURL!)
                self.currentTempLabel.text = "\(WeatherModel.getWeather(weather: value.weather)) \(value.temp)°"
                self.maxMinTempLabel.text = "최고 \(value.temp_max)° · 최저 \(value.temp_min)°"
                
                //두번째 뷰
                self.windLabel.text = "풍속    \(value.wind)m/s"
                
                //세번째 뷰
                self.humidityLabel.text = "습도    \(value.humidity)%"
                
                //네번째 뷰
                self.pressureLabel.text = "기압    \(value.pressure)hPa"
                
                //다섯번째 뷰
                self.messageLabel.text = WeatherModel.getMessage(weather: value.weather)
                
                self.hud.dismiss(animated: true)
                self.allShow()
            }
        }
        showRequestLocationServiceAlert()
    case .denied:
        //기본적으로 새싹캠퍼스의 날씨지만, 레이블은 서울만 표시(현대카드 웨더처럼)
        allHidden()
        locationButton.image = UIImage(systemName: "location")
        hud.show(in: view)
        AddressAPIManager.shared.getLocationData(lat: lat, lon: lon) { value in
            self.locationLabel.text = "\(value.regionFirst)"
            
            WeatherAPIManager.shared.getWeatherData(lat: self.lat, lon: self.lon) { value in
                //첫번째 뷰
                let imageURL = URL(string: Endpoint.imageURL + "\(value.iconId)@2x.png")
                self.weatherImageView.kf.setImage(with: imageURL!)
                self.currentTempLabel.text = "\(WeatherModel.getWeather(weather: value.weather)) \(value.temp)°"
                self.maxMinTempLabel.text = "최고 \(value.temp_max)° · 최저 \(value.temp_min)°"
                
                //두번째 뷰
                self.windLabel.text = "풍속    \(value.wind)m/s"
                
                //세번째 뷰
                self.humidityLabel.text = "습도    \(value.humidity)%"
                
                //네번째 뷰
                self.pressureLabel.text = "기압    \(value.pressure)hPa"
                
                //다섯번째 뷰
                self.messageLabel.text = WeatherModel.getMessage(weather: value.weather)
                
                self.hud.dismiss(animated: true)
                self.allShow()
            }
        }
        showRequestLocationServiceAlert()
    case .authorizedWhenInUse:
        locationManager.startUpdatingLocation()
    default:
        print("항상 허용")
    }
}

위에서 네트워크 통신을 메서드로 따로 빼서 호출하니 제대로 동작하지않았다. 그래서 네트워크 통신 코드를 그대로 사용했다.

 

여기서 AddressAPIManager부분만 보면, regionFirst인 지역(서울)만 레이블에 표시하는 것을 확인할 수 있다.

 

 

위치 권한이 허용되지 않으면 아래처럼 표시된다.

(비어있는 Location 이미지)

 

 

만약 위치 권한을 허용한다면 didUpdateLocations메서드에서 받아온 coordinate를 이용해 위치 정보를 구했다.

 

 

위와 다르게 regionThird까지 표시하였고, 위치를 가져온 상태인 경우 locationButton의 이미지를 바꿔주었다.

 

 

 

날씨

 

 

 

좌표를 이용해 네트워크 통신을 진행하여 날씨 정보를 가져와 UI에 표시해주었다.

 

 

우선, 모델은 아래와 같다.

struct WeatherModel {
    
    let temp: Int
    let temp_min: Int
    let temp_max: Int
    let pressure: Int
    let humidity: Int
    let wind: Double
    let iconId: String
    let weather: String
    
    static func getWeather(weather: String) -> String {
        
        switch weather {
        case Weather.thunderStorm:
            return "뇌우"
        case Weather.drizzle:
            return "이슬비"
        case Weather.rain:
            return "비"
        case Weather.snow:
            return "눈"
        case Weather.clear:
            return "맑음"
        case Weather.clouds:
            return "흐림"
        default :
            return "안개"
        }
    }
    
    static func getMessage(weather: String) -> String {
        switch weather {
        case Weather.thunderStorm:
            return "나무 아래에 있지 마세요."
        case Weather.drizzle:
            return "이 정도 비는 맞아도 돼요."
        case Weather.rain:
            return "비가 내리고 음악이 흐르면"
        case Weather.snow:
            return "많이 미끄러우니 조심하세요."
        case Weather.clear:
            return "맑은 날엔 외출이죠."
        case Weather.clouds:
            return "내일 비가 올 수도 있겠어요."
        default :
            return "안개가 있네요. 앞을 조심하세요."
        }
    }
}

enum Weather {
        
    static let thunderStorm = "Thunderstorm"
    static let drizzle = "Drizzle"
    static let rain = "Rain"
    static let snow = "Snow"
    static let clear = "Clear"
    static let clouds = "Clouds"
    
}

 

 

다음은 네트워크 통신 APIManager이다.

 

 

마찬가지로 싱글턴 패턴을 이용해 작성해주었다.

class WeatherAPIManager {
    
    private init() {}
    
    static let shared = WeatherAPIManager()
    
    func getWeatherData(lat: Double, lon: Double, completionHandler: @escaping (WeatherModel) -> ()) {
        
        let url = Endpoint.weatherURL + "lat=\(lat)&lon=\(lon)&appid=\(APIKey.openWeather)"
        
        AF.request(url, method: .get).validate().responseData { response in
            switch response.result {
            case .success(let value):
                let json = JSON(value)
                
                let main = json["main"]
                
                let temp = Int(round(main["temp"].doubleValue - 273.15))
                let tempMin = Int(round(main["temp_min"].doubleValue - 273.15))
                let tempMax = Int(round(main["temp_max"].doubleValue - 273.15))
                let pressure = main["pressure"].intValue
                let humidity = main["humidity"].intValue
                let wind = round(json["wind"]["speed"].doubleValue * 10) / 10
                let iconId = json["weather"][0]["icon"].stringValue
                let weatherName = json["weather"][0]["main"].stringValue
                
                let weather = WeatherModel(temp: temp, temp_min: tempMin, temp_max: tempMax, pressure: pressure, humidity: humidity, wind: wind, iconId: iconId, weather: weatherName)
                
                completionHandler(weather)
                
            case .failure(let error):
                print(error)
            }
        }
    }
}

 

 

모델과 매니저를 통해 didUpdateLocations메서드에 다음과 같이 구현해주었다.

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            
    if let coordinate = locations.last?.coordinate {
        lat = coordinate.latitude
        lon = coordinate.longitude
                    
        allHidden()
        hud.show(in: view)
        //위치 버튼 이미지 채우기
        self.locationButton.image = UIImage(systemName: "location.fill")
        
        AddressAPIManager.shared.getLocationData(lat: lat, lon: lon) { value in
            self.locationLabel.text = "\(value.regionFirst), \(value.regionThird)"

            WeatherAPIManager.shared.getWeatherData(lat: self.lat, lon: self.lon) { value in
                //첫번째 뷰
                let imageURL = URL(string: Endpoint.imageURL + "\(value.iconId)@2x.png")
                self.weatherImageView.kf.setImage(with: imageURL!)
                self.currentTempLabel.text = "\(WeatherModel.getWeather(weather: value.weather)) \(value.temp)°"
                self.maxMinTempLabel.text = "최고 \(value.temp_max)° · 최저 \(value.temp_min)°"

                //두번째 뷰
                self.windLabel.text = "풍속    \(value.wind)m/s"

                //세번째 뷰
                self.humidityLabel.text = "습도    \(value.humidity)%"

                //네번째 뷰
                self.pressureLabel.text = "기압    \(value.pressure)hPa"

                //다섯번째 뷰
                self.messageLabel.text = WeatherModel.getMessage(weather: value.weather)

                self.hud.dismiss(animated: true)
                self.allShow()
            }
        }
    }
    locationManager.stopUpdatingLocation()
}

이미지도 API response에서 받은 이미지 URL을 Kingfisher 라이브러리를 이용해 처리하였다.

 

 

HUD, isHidden

 

 

 

JGProgressHUD 오픈소스 라이브러리와 isHidden 프로퍼티를 이용하여 데이터가 한 번에 보여지도록 해주었다.

 

 

처음에는 SkeletonView 오픈소스 라이브러리를 사용하려고 했지만, 중간중간 뷰가 보이는 오류가 있어서 isHidden 프로퍼티를 이용하기로 했다.

 

 

어차피 SkeletonView를 이용하더라도 오브젝트들이 메모리 상에 올라와있는 것은 동일하기 때문에 isHidden으로 깔끔하게 한 번에 보여주는 것도 괜찮다고 생각했다.

func allHidden() {
    timeLabel.isHidden = true
    locationButton.isHidden = true
    locationView.isHidden = true
    weatherView.isHidden = true
    windView.isHidden = true
    humidityView.isHidden = true
    pressureView.isHidden = true
    messageView.isHidden = true
}

func allShow() {
    timeLabel.isHidden = false
    locationButton.isHidden = false
    locationView.isHidden = false
    weatherView.isHidden = false
    windView.isHidden = false
    humidityView.isHidden = false
    pressureView.isHidden = false
    messageView.isHidden = false
}

 

 

아래와 같이 사용하였다.

(사용한 부분 중 일부만 캡쳐)

 

 

 

 

결과 화면

 

 

위치 사용 권한을 허용하지 않았을 때

 

 

보이진않지만 처음에 위치 권한 허용 창에서 '허용 하지 않음'을 선택하느라 로딩 시간이 길어보이는 것이다.

 

허용하지 않으면 새싹 캠퍼스 위치의 날씨 정보가 표시된다.

 

 

위치 권한을 허용하였을 때

 

 

위치 권한 허용을 물어보는 창에서 허용을 선택한 화면이다.

 

현재 위치의 날씨 정보가 뜨는 것을 확인할 수 있다.

 

 

위치 권한을 변경하였을때

 

 

 

 

 

전체코드

 

https://github.com/Heecheoljjang/Sesac_Week6

댓글
최근에 올라온 글
Total
Today
Yesterday