MVVM은 Model View View-Model의 약자로써 간단히 레이아웃과 로직을 분리하는 것을 의미한다고 이해했다.
이번 강의에서는 ContentView.swift의 로직을 ContentView-ViewModel.swift로 옮기는 작업을 배웠다. 순서대로 기록한다.
ContentView-ViewModel.swift
import Foundation
import MapKit
extension ContentView {
@Observable
class ViewModel {
var locations = [Location]()
var selectedPlace: Location?
}
}
ContentView.swift
@State private var viewModel = ViewModel()
viewModel 생성 후 locations, selectedPlace을 viewModel의 속성으로 변경했다.
ContentView-ViewModel.swift
import CoreLocation
// ...
func addLocation(at coordinate: CLLocationCoordinate2D) {
locations.append(Location(id: UUID(), name: "new location", longitude: coordinate.longitude, latitude: coordinate.latitude))
}
func update(location: Location) {
guard let selectedPlace else { return }
if let index = locations.firstIndex(of: selectedPlace) {
locations[index] = location
}
}
ContentView.swift
.onTapGesture { position in
if let coordinate = proxy.convert(position, from: .local) {
viewModel.addLocation(at: coordinate)
}
}
// ...
.sheet(item: $viewModel.selectedPlace) { place in
EditPlaceView(location: place) {
viewModel.update(location: $0)
}
}
원래 ContentView에서 설정한 로직을 ContentView-ViewModel.swift에서 함수로 설정하고, ContentView에서는 함수로 호출했다.
추가로 사용자가 Annotation으로 지정한 장소를 파일에 저장하고 다시 불러올 수 있도록 설정했다.
init() {
do {
let data = try Data(contentsOf: savePath)
let decoded = try JSONDecoder().decode([Location].self, from: data)
locations = decoded
} catch {
locations = []
}
}
func save() {
do {
let data = try JSONEncoder().encode(locations)
try data.write(to: savePath, options: [.atomic, .completeFileProtection])
} catch {
print("Unable to save data.")
}
}
생성자로 이전에 저장된 locations
를 불러오고, 장소를 새로 추가하거나 수정할 때 save()
를 실행하도록 설정했다.
마지막으로 face id를 설정했다.
app의 info에 'Privacy - Face ID Usage Description' 항목과 함께 "Please authenticate yourself to unlock your places"를 설정한다.
그리고 ContentView-ViewModel.swift 파일에서 아래 작업을 실행한다.
import LocalAuthentication
LocalAuthentication을 import하고,
func authenticate() {
let context = LAContext()
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
let reason = "Please authenticate yourself to unlock your places"
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
if success {
self.isUnlocked = true
} else {
print("Unable to unlock: \(String(describing: error))")
}
}
} else {
print("Unable to use biometrics")
}
}
authenticate()
함수를 설정한다. 이전 강의를 참고해서 작성했다. 잠금이 풀리지 않는 경우에 그와 관련한 이유를 출력하도록 설정했다.
그리고 ContentView의 모든 View를 아래 조건문 안으로 넣어 잠금이 풀리면 지도와 함께 보이도록 하고, 아니라면 authenticate()
을 실행할 수 있는 버튼을 설정했다.
if viewModel.isUnlocked {
// ...
} else {
Button("Unlock places") {
viewModel.authenticate()
}
.buttonStyle(.borderedProminent)
}
휴대폰으로도 연결하여 테스트해보니 잘 작동하는 것을 확인할 수 있었다.