오늘은 MVVM 패턴에 대해서 배웠는데, 사실 아직 잘 모르겠습니다...
일단 이해한 대로 MVVM 패턴에 대해 한 번 써보려 합니다..!
1. What is MVVM?
MVVM 패턴이란 디자인을 Model, ViewModel, View로 나누는 것을 말합니다.
이렇게 나누는 이유는, UI와 실제 코드를 분리하여 로직과 디자인 간의 상호 영향을 줄여 개발을 편하게 만들기 위함입니다.
View는 UI를 맡고, Model엔 로직이 들어있고, ViewModel이 이 사이에서 중개자가 됩니다. 상사맨을 보는 것 같습니다..
1.
View는 user intent를 받아 ViewModel에게 명령하고,
2.
ViewModel은 필요한 데이터를 Model에게 요청,
3.
Model은 로직을 거치거나 등등 해서 요청받은 데이터를 응답,
4.
ViewModel은 Model로부터 받은 데이터를 저장한다.
5.
View는 ViewModel과 바인딩 되어있어 ViewModel에 저장된 데이터가 변함에 따라 갱신된다.
이렇게 돌아가는 것 같습니다..
왜이렇게 배우는 게 힘들까? 했더니 원래 혼자서 간단한 걸 만들때는 적용을 안하는게 더 쉬울 수 있다고 합니다.
2. 내가 적용해본 MVVM
Apus 동아리에서 진행하는 팀 프로젝트의 제가 맡은 부분입니다! 아직 완성된 건 아니지만요..
.padding(someValue)등 디자인을 위한 함수는 대체로 생략하고 올렸습니다.
<Model>
import Foundation
struct ProcessLocation {
private let availableDistance = 50.0
private let gaepoLatitude = 37.48815449911871
private let gaepoLongitude = 127.06476621423361
static func haversineDistance(la1: Double, lo1: Double, la2: Double, lo2: Double, radius: Double = 6367444.7) -> Double {
<...중략...>
}
/* 위도 경도를 두 세트 받아서 거리를 구해주는 공식입니다. 출처는 요기 https://github.com/raywenderlich/swift-algorithm-club/tree/master/HaversineDistance
무한한 감사를 올립니다.. */
func getDistanceFromCluster(lat userLatitude: Double, lon userLongitude: Double) -> Double {
return ProcessLocation.haversineDistance(la1: gaepoLatitude, lo1: gaepoLongitude,
la2: userLatitude, lo2: userLongitude)
}
func isNear(lat userLatitude: Double, lon userLongitude: Double) -> Bool {
if ProcessLocation.haversineDistance(la1: gaepoLatitude, lo1: gaepoLongitude,
la2: userLatitude, lo2: userLongitude) > availableDistance {
return false
} else {
return true
}
}
}
Swift
복사
<ViewModel>
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
@Published var locationStatus: CLAuthorizationStatus?
@Published var lastLocation: CLLocation?
<...중략...>
/* 이 위와 생략된 부분은 어디서 긁어온 건데, 현재 위치를 위도 경도로 받아올 수 있게 해줍니다. */
private var userLatitude: Double { lastLocation?.coordinate.latitude ?? 0 }
private var userLongitude: Double { lastLocation?.coordinate.longitude ?? 0 }
let processLocation = ProcessLocation()
/* 이 부분이 정말 필요한가..? 싶습니다. Model에서 메서드들을 다 타입 메서드로 만들어서 쓰는게 낫나라는
생각이 들어요. 하튼 잘 모르겠습니다.. 넘 어려워잉 */
var myDistanceFromCluster: Double {
processLocation.getDistanceFromCluster(lat: userLatitude, lon: userLongitude)
}
var isNear: Bool {
processLocation.isNear(lat: userLatitude, lon: userLongitude)
}
}
Swift
복사
<View>
import SwiftUI
struct FrontView: View {
@ObservedObject var locationManager: LocationManager
let intraId: String = "hakim"
var body: some View {
VStack {
Text("Apus Check-In")
Spacer()
EntranceButton(isInLocation: locationManager.isNear)
Spacer()
Text("Intra ID: \(intraId)")
Text("My distance from Cluster: \(Int(locationManager.myDistanceFromCluster)) meter")
}
}
}
struct EntranceButton: View {
var isInLocation: Bool
var body: some View {
Button {
//action has to be defined
} label: {
if isInLocation == true {
Image("coloredApus").resizable()
} else {
Image("uncoloredApus").resizable()
}
}
}
}
Swift
복사
1.
View의 역할
View에서는 UI를 만들어주기만 합니다. 텍스트와 버튼으로 구성했는데, 여기에 현재 좌표(위도, 경도)라는 실시간으로 변동되는 정보가 필요하고, 이 정보의 변경에따라 뷰가 업데이트 되어야 합니다.
현재 위치라는 정보는 ViewModel의 LocationManger을 인스턴스화한 locationManager가 가지고 있습니다. locationManager는 @ObservedObject라는 Property Wrapper를 가지고 있는데, 이는 View에서 locationManager를 지켜보다가 정보가 바뀌면 View를 갱신하겠다! 라는 것을 의미합니다.
user intent를 받아 ViewModel에게 정보를 내놓으라고 명령을 한다고 위에 썼는데, 제 생각에 여기서 user intent는 사용자의 이동입니다. 이동한다는게 내 현재 좌표를 내놓으라고 ViewModel에게 명령한다는 걸로 받아들였습니다. 맞는가는 모르겠습니다.
EntranceButton이나 마지막 Text같은 뷰가 이에 영향을 받아 갱신됩니다.
2.
ViewModel의 역할
ViewModel의 LocationManager이라는 클래스는 ObservableObject라는 프로토콜을 준수합니다. 이게뭐냐! 제 짧은 이해로는 View에서 쓰일떄 @ObservedObject라는 wrapper를 붙일 수 있게 해주고, 또 내부에서는 @Published wrapper를 쓰게 해주는 것 같습니다. 그럼 Published는 뭐냐! 이 wrapper가 붙은 변수의 값이 바뀔 때, 데이터가 변경되었음을 View에게 알리는 것입니다. @Published가 안 붙은 것들은 백날천날 바뀌어봐야 View가 모른체 합니다..
LocationManger의 var lastLocation에 @Published wrapper가 붙어있는데, 이렇기 때문에 View가 이걸 째리다가 내 현재 좌표가 바뀌면 (== lastLocation의 값이 변경되면) View를 갱신한다! 라고 이해했습니다.
근데 View에선 내 현재 좌표를 그냥 쓰지 않습니다. myDistanceFromCluster라는 변수와 isNear라는 변수로 바꿔서 써버리는디, 내 현재 좌표를 쟤네들로 바꿔주는 로직을 Model이 맡습니다! ViewModel은 Model에게 내 현재 좌표를 바꿔달라고 요청하고, 돌아온 데이터를 변수에 저장합니다.
3.
Model의 역할
제가 만든 거에서 좀 부족한게 이 부분 같습니다. 일단 저는 좌표라는 날것의 데이터를 가공하는 역할을 Model에게 주었습니다. 그래서 haversine formula를 통해 두 좌표 사이의 거리도 구하고, 이거를 정해놓은 거리와 비교해 클러스터 근처에 있는지 없는지라는 정보로 가공합니다. 그래서 그걸 ViewModel에게 돌려줍니다.
제 나름대로 MVVM으로 구성해서 ApusCheckIn 앱의 시작화면을 만들어보았습니다.. MVVM이란게 이렇게 하는게 맞으면 좋겠네요. 틀려먹어서 고쳐야할 부분을 보게 되시면 맘껏 말씀해주세요!! 열심히 듣겠습니다.