ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • iOS 의 MVC 모델
    SwiftUI/MVVM 알아보기 2020. 8. 26. 00:21

    iOS 앱을 제작 시 스토리 보드를 이용하는 경우는 MVC 패턴에 따라 프로젝트를 구성하고 구현하게 된다. 이는 iOS의 프레임워크가 이를 기반으로 구성되어있기 때문이다. 

    openweather api 를 통해 도시에 대한 현재 날씨를 가져와 화면에 보여주는 간단한 예제를 통해 MVC 패턴에 대해 알아보고 SwiftUI 가 MVVM 패턴을 적용한 이유에 대해서 살펴보자.

     

    openweather api 는 REST API로 해당 요청에 대해 JSON 포맷에 대해 결과 값을 반환하는 구조로 이루어진다. 이를 사용하기 위해서는 openweather  웹사이트에서 무료로 키를 발급 받고 이 키값을 HTTP 요청에 다른 파라미터와 함께 전달해야한다. 

    urlString = https://api.openweathermap.org/data/2.5/weather?q=\(city)&appid=\(appid)&units=metric"
    

    위는 해당 api를 호출 시 결과 값을 보여준다. 우리는 URLSession을 통해 api를 호출하고 결과를 파싱해 WeatherData 객체를 생성하는데  프로그래밍 사용하기 위한 데이터나 이를 위한 로직을 Model 로 정의한다. 

     

    모델 - 데이터 정의 

    WeatherData.swift

    import UIKit
    
    /*
    *{
    *  "weather": [ //weather 는 배열
    *   {
    *   "id": 803, //id에 따라 사용할 아이콘 선택을 위해 필요
    *   "main": "Clouds",
    *   "description": "broken clouds",
    *   "icon": "04n"
    *   }
    *   ],
    *
    *   "main": {
    *   "temp": 28.7, //현재 온도 값
    *  "feels_like": 32.71,
    *   "temp_min": 28,
    *   "temp_max": 29,
    *   "pressure": 1005,
    *   "humidity": 74
    * },
    * "name": "Seoul",
    *}
    * 현재 WeatherData 에서 사용되는 데이터 정의
     */
    
    //JSON로 디코딩 인코딩 가능한 객체는 Codable 프로토콜을 따라야함 
    struct WeatherData : Codable {
        //도시 이름
        let name: String
        let main : WeatherMain
        let weather: [Weather]
        //편의성을 위해 image 프로퍼티 정의
        var image : UIImage? {
            return WeatherData.getWeatherImage(id: weather[0].id)
        }
    }
    
    //main 에 해당
    struct WeatherMain : Codable {
        //온도
        let temp: Double
        //습도
        let humidity: Double
    }
    
    //weather 에 해당
    struct Weather : Codable {
        let description: String
        //id 값에 따라 아이콘 출력을 위해 필요
        let id: Int
    }
    
    /*
     * Weather 구조체의 id 값에 따른 이미지 정보
     */
    extension WeatherData {
        static func getWeatherImage(id: Int) -> UIImage? {
            var imageName: String
            switch id {
            case 200...232:
                imageName = "cloud.bolt"
            case 300...321:
                imageName = "cloud.drizzle"
            case 500...531:
                imageName = "cloud.rain"
            case 600...622:
                imageName = "cloud.snow"
            case 701...781:
                imageName = "cloud.fog"
            case 800:
                imageName = "sun.max"
            case 801...804:
                imageName = "cloud.bolt"
            default:
                imageName = "cloud"
            }
            return UIImage(systemName: imageName)
        }
    }
    

    모델 - 로직 

    도시의 날씨 정보를 얻기위한 openweatherapi 

    URL+Extensions.swift

    import Foundation
    
    let appid = ""
    extension URL {
        static func urlWith(city: String) -> URL? {
            return URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(city)&appid=\(appid)&units=metric")
        }
        
    }

     

    모델 - 로직 

    URLSession과 JSONDecoder를 통해 WeatherData 값 얻기 

    URLRequest+Extensions.swift

    import Foundation
    
    //Resource 를 정의하면  JSonDecoder 에서 생성할 모델 객체에 대한 타입을 제너릭하게 설정할 수 있어
    //GET으로 요청하는 간단한 API에 대해서는 재사용성을 높일 수 있음
    struct Resource<T> {
        let url: URL
    }
    
    //간단한 에러 정의
    enum NetworkError: Error {
        case domainError
        case urlError
        case decodingError
    }
    
    extension URLRequest {
        static func load<T : Decodable> (resource: Resource<T>,
                                         completion: @escaping (Result<T, NetworkError>) -> Void)  {
            //1. URLSession 정의
            let urlSession = URLSession(configuration: .default)
            //2. URSSession 의 dataTask에 url을 전달하고 응답에 대한 completion 핸들러 등록
            // 람다로 전달된 completion 핸들러는 dataTask에서 완료시 호출됨
            urlSession.dataTask(with: resource.url) { (data , response, error) in
                //데이터가 nil 이고, error 값이 있는 경우 실패로 해당 결과 전달
                guard let data = data, error == nil else {
                    //UI에 대한 변경은 main 스레드에서만 가능하므로 해당 결과로 뷰를 수정하는 경우를
                    //대비해 main 큐에서 실행되도록 등록
                    DispatchQueue.main.async {
                        completion(.failure(.urlError))
                    }
                    return
                }
                // 정상적으로 데이터를 수신하는 경우. JSONDecoder를 통해 T 타입의 구조체 생성
                //여기서는 WeatherData 가 생성됨
                if let result = try? JSONDecoder().decode(T.self, from: data) {
                    DispatchQueue.main.async {
                        completion(.success(result))
                    }
                } else {
                    DispatchQueue.main.async {
                        completion(.failure(.decodingError))
                    }
                }
            }.resume() //3.URLSession에 dataTask 수행
        }
    }

    뷰 - Strory 보드로 관리 되고 ViewController 를 통해 이벤트가 들어온다. 

     

    ViewController.swift

     

    //
    //  ViewController.swift
    //  ClimaForStroyboard
    //
    //  Created by HYEONJUN PARK on 2020/08/25.
    //  Copyright © 2020 HYEONJUN PARK. All rights reserved.
    //
    
    import UIKit
    
    class ViewController: UIViewController {
        //Main Stroy 보드에서 생성한 UI 객체에 대한 레퍼런스
        @IBOutlet weak var weatherImage: UIImageView!
        @IBOutlet weak var cityNameLabel: UILabel!
        @IBOutlet weak var tempetureLabel: UILabel!
        @IBOutlet weak var searchTextField: UITextField!
        
        //뷰가 로드된 후 호출 되는 함수로 이 때 서울의 날씨 정보를 요청
        override func viewDidLoad() {
            super.viewDidLoad()
            fetchWeatherData(city: "seoul")
            
        }
        
        //모델에 대한 요청이 성공하면 여기서 UI 객체에 데이더 정보를 직접 전달해 뷰 갱신
        private func fetchWeatherData(city: String) {
            let url = URL.urlWith(city: city)
            if let url = url {
                let resoure = Resource<WeatherData>(url: url)
                URLRequest.load(resource: resoure) { (result) in
                    switch result {
                    case .success(let weatherData) :
                        self.weatherImage.image = weatherData.image
                        self.cityNameLabel.text = weatherData.name
                        self.tempetureLabel.text = String(format: "%.1f℃", weatherData.main.temp)
                        
                    case .failure(let error) :
                        print(error)
                    }
                }
            }
        }
    }
    
    /*
     * 도시이름을 입력받을 UITextFieldDelegate 의 구현으로 Story 보드에서 UITextField의 delegate
     * 를 현재 ViewController로 설정
     */
    extension ViewController : UITextFieldDelegate {
       
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            searchTextField.endEditing(true)
            return true
        }
        
        func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
            if searchTextField.text != "" {
                return true
            } else {
                textField.placeholder = "Enter a city name"
                return false
            }
        }
        // 실제 입력이 완료되고 리턴을 입력 받을 시 해당 도시에 대한 정보를 요청
        func textFieldDidEndEditing(_ textField: UITextField) {
            if let city = searchTextField.text {
                self.fetchWeatherData(city: city)
            }
            searchTextField.text = ""
        }
    }
    

     

     

    위처럼 MVC패턴은 ViewController에서 액션을 받고 여러 UI 객체를 관리하기 때문에 ViewController에 코드가 집중되는 경향이 있다. 

    또한 데이터가 변경 시 변경된 데이터를 직접 View 객체에 전달하게 되어 Model 과 View 간에 의존 성이 생겨 코드의 재 사용 가능성이 줄어든다. 프로젝트의 구조가 커지면 MVC 모델은 모델과 컨트롤러 뷰 간의 의존 성 때문에 단위 테스트가 힘들어 지는 등 여러 문제가 발생해 

     

    SwiftUI는 MVVM 모델을 도입하게 된다. MVVM 은. M(모델), V(뷰), 에 C(컨트롤러) 대신 VM( 뷰 모델)이 대체된 개념으로 

    Model 과 View객체는 의존성을 가지지 않는다. 모델은 뷰 모델에 정보를 전달하면 뷰 모델은 바인딩된 뷰 객체에 통지를 보내고 뷰는 해당 통지시 새롭게 뷰를 갱신하게된다. 마찬가지로 UI를 통애서 액션을 받고 해당 액션에 대한 정보를 참조하고 있는 뷰 모델 객체를 통해서 모델로 전달해 새롭게 데이터 값을 갱산하게 된다. 

     

     

    SwiftUI의 MVVM모델을 통한 날씨 앱은 다음 블러그에서 다루기로 한다. 

    댓글

Designed by Tistory.