VesselWheel

정해진 지역의 날씨를 호출하여 날씨 정보 표시하기(feat. OpenWeatherMap API) 본문

Xcode Study

정해진 지역의 날씨를 호출하여 날씨 정보 표시하기(feat. OpenWeatherMap API)

JasonYang 2024. 2. 6. 19:18

MVC 패턴으로 만든 OpenWeatherMap API의 특정지역(서울)의 날씨정보 호출 및 출력

시뮬레이터에 출력된 날씨 정보

1. OpenWeatherMap에서 가져온 데이터로 구조체 만들기 

//
//  WeatherModel.swift
//  Weather777
//
//  Created by Jason Yang on 2/5/24.
//

import Foundation

// MARK: - WeatherData
struct WeatherData: Codable {
    let coord: Coord
    let weather: [Weather]
    let base: String
    let main: Main
    let visibility: Int
    let wind: Wind
    let clouds: Clouds
    let dt: Int
    let sys: Sys
    let timezone, id: Int
    let name: String
    let cod: Int
}

// MARK: - Clouds
struct Clouds: Codable {
    let all: Int
}

// MARK: - Coord
struct Coord: Codable {
    let lon, lat: Double
}

// MARK: - Main
struct Main: Codable {
    let temp, feelsLike, tempMin, tempMax: Double
    let pressure, humidity: Int

    enum CodingKeys: String, CodingKey {
        case temp
        case feelsLike = "feels_like"
        case tempMin = "temp_min"
        case tempMax = "temp_max"
        case pressure, humidity
    }
}

// MARK: - Sys
struct Sys: Codable {
    let type, id: Int
    let country: String
    let sunrise, sunset: Int
}

// MARK: - Weather
struct Weather: Codable {
    let id: Int
    let main, description, icon: String
}

// MARK: - Wind
struct Wind: Codable {
    let speed: Double
    let deg: Int
}

 

2. 데이터 모델인 WeatherData를 활용하여 WeatherManager 클래스로 API 호출하기

//
//  WeatherManager.swift
//  Weather777
//
//  Created by Jason Yang on 2/5/24.
//

import Foundation

// 에러 정의
enum NetworkError: Error {
    case badUrl
    case noData
    case decodingError
}

class WeatherManager {
    
    static let shared = WeatherManager()
    
    private init() {}
    
    // MARK: - Properties
    //plist를 통해 숨긴 API 호출을 위한 프로퍼티
    private var apiKey: String {
        get {
            // 생성한 .plist 파일 경로 불러오기
            guard let filePath = Bundle.main.path(forResource: "WeatherKey", ofType: "plist") else {
                fatalError("Couldn't find file 'WeatherKey.plist'.")
            }
            
            // .plist를 딕셔너리로 받아오기
            let plist = NSDictionary(contentsOfFile: filePath)
            
            // 딕셔너리에서 값 찾기
            guard let value = plist?.object(forKey: "OPENWEATHERMAP_KEY") as? String else {
                fatalError("Couldn't find key 'OPENWEATHERMAP_KEY' in 'KeyList.plist'.")
            }
            return value
        }
    }
    
    //API 호출을 위한 매소드 // 서울지역이 아닌 곳을 하려면 어떻게 해야할까?
    // 위경도 기준 : URL(string: "https://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)&appid=\(apiKey)")
    // 파라미터 추가 latitude: Double, longitude: Double,
    // 서울지역 : URL(string: "https://api.openweathermap.org/data/2.5/weather?q=seoul&appid=\(apiKey)")
    func getWeather(completion: @escaping(Result<WeatherData, NetworkError>) -> Void) {
        let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=seoul&appid=\(apiKey)")
        guard let url = url else {
            return completion(.failure(.badUrl))
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data, error == nil else {
                return completion(.failure(.noData))
            }
            
            // Data 타입으로 받은 리턴을 디코드
            let weatherData = try? JSONDecoder().decode(WeatherData.self, from: data)
            
            // 성공 시 성공한 데이터 저장
            if let weatherData = weatherData {
                completion(.success(weatherData))
            } else {
                completion(.failure(.decodingError))
            }
        }.resume()  // dataTask 시작
        print("Success")
    }
}

 

3. ViewController에서 WeatherManager 클래스를 싱글톤 패턴 방식으로  데이터를 호출하여 비동기로 UI 출력하기

//
//  ViewController.swift
//  Weather777
//
//  Created by Jason Yang on 2/5/24.
//

import UIKit
import SwiftUI

class ViewController: UIViewController {
    
    var weather: Weather?
    var main: Main?

    
    // MARK: - UI Properties
    let testLabel: UILabel = {
        let label = UILabel()
        label.text = "7팀 화이팅입니다.😃"
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    var weatherImage: UIImageView!
    var tempLabel: UILabel!
    var maxTempLabel: UILabel!
    var minTempLabel: UILabel!

    
    // MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setUI()
        setAddSubView()
        setLayout()
       
        // data fetch, url에서 싱글톤패턴으로 받아온 데이터를 비동기적으로 호출하고, UI 업데이트를 위해 메인스레드에서 수행, 클로져 내부에서 강한참조 방지 weak self
        WeatherManager.shared.getWeather { [weak self] result in
            switch result {
            case .success(let weatherResponse):
                DispatchQueue.main.async {
                    self?.weather = weatherResponse.weather.first
                    self?.main = weatherResponse.main
                    self?.setWeatherUI()
                }
            case .failure(_ ):
                print("error")
            }
        }
    }
}


extension ViewController {
    func setUI() {
        // 배경색 지정
        view.backgroundColor = .white
        
        weatherImage = UIImageView()
        weatherImage.translatesAutoresizingMaskIntoConstraints = false
        
        tempLabel = UILabel()
        tempLabel.translatesAutoresizingMaskIntoConstraints = false
        
        maxTempLabel = UILabel()
        maxTempLabel.translatesAutoresizingMaskIntoConstraints = false
        
        minTempLabel = UILabel()
        minTempLabel.translatesAutoresizingMaskIntoConstraints = false
        
    }
    
    func setAddSubView() {
        view.addSubview(testLabel)
        view.addSubview(weatherImage)
        view.addSubview(tempLabel)
        view.addSubview(maxTempLabel)
        view.addSubview(minTempLabel)
    }
    
    func setLayout() {
        NSLayoutConstraint.activate([
            testLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            testLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            
            // weatherImage의 제약 조건 설정
            weatherImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            weatherImage.topAnchor.constraint(equalTo: testLabel.bottomAnchor, constant: 100),
            weatherImage.widthAnchor.constraint(equalToConstant: 100),
            weatherImage.heightAnchor.constraint(equalToConstant: 100),
            
            // tempLabel의 제약 조건 설정
            tempLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            tempLabel.topAnchor.constraint(equalTo: weatherImage.bottomAnchor, constant: 20),
            
            // maxTempLabel의 제약 조건 설정
            maxTempLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            maxTempLabel.topAnchor.constraint(equalTo: tempLabel.bottomAnchor, constant: 20),
            
            // minTempLabel의 제약 조건 설정
            minTempLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            minTempLabel.topAnchor.constraint(equalTo: maxTempLabel.bottomAnchor, constant: 20)
        ])
    }
    
    private func setWeatherUI() {
        guard let icon = self.weather?.icon else {
            return
        }
        
        let urlString = "https://openweathermap.org/img/wn/\(icon)@2x.png"
        guard let url = URL(string: urlString) else {
            return
        }
        
        let session = URLSession.shared
        let task = session.dataTask(with: url) { (data, response, error) in
            guard let data = data, error == nil else {
                return
            }
            
            DispatchQueue.main.async {
                self.weatherImage.image = UIImage(data: data)
            }
        }
        task.resume()
        
        if let main = self.main {
            let tempCelsius = main.temp
            let tempMaxCelsius = main.tempMax
            let tempMinCelsius = main.tempMin
            
            DispatchQueue.main.async {
                if let celsiustempLabel = self.convertFahrenheitToCelsius(tempCelsius),
                   let celsiusmaxTempLabel = self.convertFahrenheitToCelsius(tempMaxCelsius),
                   let celsiusminTempLabel = self.convertFahrenheitToCelsius(tempMinCelsius) {
                    // Call to method 'convertFahrenheitToCelsius' in closure requires explicit use of 'self' to make capture semantics explicit
                    
                    self.tempLabel.text = "\(celsiustempLabel)"
                    self.maxTempLabel.text = "\(celsiusmaxTempLabel)"
                    self.minTempLabel.text = "\(celsiusminTempLabel)"
                }
            }
        }
    }
}


extension ViewController {
    func convertFahrenheitToCelsius(_ fahrenheit: Double) -> String? {
        let celsiusUnit = UnitTemperature.celsius
        let formatter = NumberFormatter()
        formatter.maximumFractionDigits = 1
        let celsius = celsiusUnit.converter.value(fromBaseUnitValue: fahrenheit)
        if let formattedCelsius = formatter.string(from: celsius as NSNumber) {
            return "\(formattedCelsius)°C"
        }
        return ""
    }
}

// MARK: - Preview
struct PreView: PreviewProvider {
    static var previews: some View {
        ViewController().toPreview()
    }
}

#if DEBUG
extension UIViewController {
    private struct Preview: UIViewControllerRepresentable {
        let viewController: UIViewController
        
        func makeUIViewController(context: Context) -> UIViewController {
            return viewController
        }
        
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        }
    }
    
    func toPreview() -> some View {
        Preview(viewController: self)
    }
}
#endif

OpenWeatherMap API에서 날씨 아이콘 호출 방법

https://openweathermap.org/weather-conditions

 

Weather Conditions - OpenWeatherMap

We use cookies to personalize content and to analyze our traffic. Please decide if you are willing to accept cookies from our website.

openweathermap.org