-
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모델을 통한 날씨 앱은 다음 블러그에서 다루기로 한다.
'SwiftUI > MVVM 알아보기' 카테고리의 다른 글
lottie-ios 프레임워크와 UIViewRepresentable 을 이용한 LoadingView 구현 (0) 2020.08.27 SwiftUI 에서 CoreLocation 사용 (0) 2020.08.27 SwiftUI MVVM 모델 (0) 2020.08.27