티스토리 뷰
1.MVVM이란?
Model-View-ViewModel의 약자로 소프트웨어 아키텍처 패턴이다
앱이 수정되고, 규모가 커짐에 따라 UI와 비즈니스 로직사이의 결합도가 커지게 되고
이는 UI수정에 대한 비용이 커지고 유닛테스트가 어렵게 만든다.
MVVM 패턴을 사용함으로써 비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리할 수 있고,
이는 앱의 개발, 유지보수, 테스트를 더 용이하게 해준다.
또한 코드 재사용이 가능하게 해주며 개발자와 UI디자이너가 각 부분을 개발할 때 더 쉽게 협력할 수 있게 해준다
1.1 MVC에서 MVVC(수정 중)
이 사진은 MVC의 전형적인 모습이다. Model은 data, View는 UI, View Controller는 이 두개의 중개자이다.
View와 View Controller가 분리된 컴포넌트이더라도 항상 짝지어 함께 작동한다.
프로젝트가 커지기 시작하면서 View Controller가 massive해졌다. 많은 코드들이 View Controller에 몰리게 됨.
1.2 MVVM의 구성요소
Model
- 앱의 데이터를 캡슐화 한 보여지지 않는 클래스이다.
- 앱의 데이터와 관련된 코드를 다룬다
View
- 유저가 화면을 통해 보게 되는 구조, 레이아웃. 즉 UI이다.
- 비즈니스 로직을 포함하지 않는다.
- View에서 받은 입력 값을 view model로 전달해준다
- ViewModel의 상태변화를 옵저빙한다.
View Model
- View와 독립적이다
- View와 Model을 중개한다
- model로부터 받은 데이터를 유저에게 쉽게 보여질 수 있도록 가공하여 view에 넘겨준다.
- view model의 책임
- model input: view의 input값을 model 에 없데이트해준다
- model output: mode outputs값을 view에 넘겨준다.
- formatting: model 데이터를 view에 보여주기 위한 적절한 데이터로 가공한다.
2.왜 써야하는가?
- View Model은 어댑터 역할을 하기 때문에 Model코드가 변경되는 것을 방지할 수 있다.
- 테스트의 용이성
- View를 사용하지 않고 View Model과 Model에 대한 유닛테스트를 작성할 수 있다.
- 디자이너와 개발자가 동시에 작업하는 것이 가능하다.
- 디자이너가 View를 작업하는 동안 개발자는 View Model을 작업하는 것이 가능하다.
3.MVC & MVVM 비교
3.1 각 패턴의 장단점
MVC
MVVM
- MVC에 비해 구현하기 어렵다.(뷰모델을 설계하는게 쉽지 않다.)
- 소형앱에서 사용하게 되면 오버헤드가 커진다
- 비즈니스 로직을 분리해줌으로써 View Controllder를 더 가볍게 만들어준다
- 규모가 작은 앱에 MVVM은 과하다.
- 규모가 커질수록 데이터 바인딩이 복잡해지므로 디버깅이 어렵다.
5.예제
이 링크의 예제는 스토리보드를 이용해 구현되어 있어서 Swift UI를 이용하여 MVVM을 구현해보았다.
Weatherbit라는 사이트에서 API를 받아와 화면에 보여준다.
완성화면
처음 실행시 default로 앵커리지라는 도시의 날씨를 보여주고
입력 창에 도시이름을 검색하면 해당도시의 오늘 날씨와 기온을 보여준다.
파일 구조
프로젝트는 크게 View, Model, View Model로 이루어져 있다.
도시 이름을 입력 후 버튼을 눌렀을 때 다음과 같은 순서로 View가 새로 그려지게 된다.
- View에서 도시이름을 입력 후 버튼을 누르면 ViewModel의 changeLoacation함수가 호출된다.
- ViewModel안의 changeLoacation 함수는 fetchWeatherForLocation함수를 통해 Model의 데이터를 변경한다.
- ViewModel의 Location관련 정보들이 변경된다.
- ViewModel에서는 View에 보여질 정보들을 가공한다.(ex.도시이름이 없을 경우, 온도관련 변수를 string으로 변경)
- ViewModel을 소유함으로써 View는 ViewModel의 상태를 관찰 할 수 있고, 데이터가 변경됨에 따라 View를 업데이트하게 된다
코드
ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
@State var newLoaction: String = ""
@StateObject var weatherViewModel = WeatherViewModel()
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
ZStack{
Color("rw-green").ignoresSafeArea()
VStack{
Text(weatherViewModel.locationName).font(.title)
Text(weatherViewModel.date)
Image(uiImage: weatherViewModel.icon!)
Text(weatherViewModel.summary)
Text(weatherViewModel.forecastSummary)
HStack(alignment:.center){
TextField("Enter your city", text: $newLoaction)
.textFieldStyle(.roundedBorder)
.cornerRadius(0.3)
.background(Color.white)
.opacity(0.5)
.frame(width: 200, height: 30)
Button(action: {
weatherViewModel.changeLocation(to: newLoaction)
}, label: {
Image(systemName: "location.fill").foregroundColor(Color.white)
})
}
}
}
}
}
WeatherViewModel.swift
import Foundation
import UIKit.UIImage
public class WeatherViewModel: ObservableObject {
private let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEEE, MMM d"
return dateFormatter
}()
private let tempFormatter: NumberFormatter = {
let tempFormatter = NumberFormatter()
tempFormatter.numberStyle = .none
return tempFormatter
}()
private static let defaultAddress = "Anchorage, AK"
private let geocoder = LocationGeocoder()
@Published var locationName = "Loading..."
@Published var date = " "
@Published var icon = UIImage(systemName: "magnifyingglass")
@Published var summary = " "
@Published var forecastSummary = " "
init() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.changeLocation(to: Self.defaultAddress)
}
// changeLocation(to: Self.defaultAddress)
}
func changeLocation(to newLocation: String) {
locationName = "Loading..."
geocoder.geocode(addressString: newLocation) { [weak self] locations in
guard let self = self else { return }
if let location = locations.first {
self.locationName = location.name
self.fetchWeatherForLocation(location)
return
}
self.locationName = "Not found"
self.date = ""
self.icon = UIImage(systemName: "magnifyingglass")
self.summary = ""
self.forecastSummary = ""
}
}
private func fetchWeatherForLocation(_ location: Location) {
WeatherbitService.weatherDataForLocation(
latitude: location.latitude,
longitude: location.longitude) { [weak self] (weatherData, error) in
guard
let self = self,
let weatherData = weatherData
else {
return
}
self.date = self.dateFormatter.string(from: weatherData.date)
self.icon = UIImage(named: weatherData.iconName)
let temp = self.tempFormatter
.string(from: weatherData.currentTemp as NSNumber) ?? ""
self.summary = "\(weatherData.description) - \(temp)℉"
self.forecastSummary = "\nSummary: \(weatherData.description)"
}
}
}
5.공부하면 좋은 개념들
state, observer
publish
property wrapper
callback
binding
[참고]
https://docs.microsoft.com/en-us/xamarin/xamarin-forms/enterprise-application-patterns/mvvm
https://www.raywenderlich.com/6733535-ios-mvvm-tutorial-refactoring-from-mvc#toc-anchor-007
https://seons-dev.tistory.com/84
https://lena-chamna.netlify.app/post/ios_design_pattern_mvvm/
'Swift' 카테고리의 다른 글
[Swift]Optional이란? 옵셔널 정리하기 (0) | 2022.08.09 |
---|---|
[Swift]Property Wrapper (0) | 2022.05.03 |
[swift]Closure란? + Closure축약 과정 (0) | 2021.11.25 |
[swift]어노테이션 (1) | 2021.10.26 |
[xcode]fail to prepare for communication with playground for an unknown reason (0) | 2021.10.26 |
- Total
- Today
- Yesterday
- closure
- 클로저
- ios
- property wrapper
- 애플 인증서
- Widget
- 백준
- New Group Without Folder
- 백준알고리즘
- ios mvvm
- New Group
- SWiFT
- provisioning key
- palera1n
- xcuserdata
- 애플 인증
- django
- xcsharedata
- 백준 1065번 swift
- readme ignore파일 포함한 repository
- Xcode
- mvvm in swiftui
- main branch
- 알고리즘
- 장고
- 백준 4673 swift
- 프로퍼티 래퍼
- 파이썬
- 이분탐색
- sileo
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |