Hey guys, I'm kind of pulling my hair out here. I've completed the challenge. And I'm almost 100% sure my implementation is correct
but I'm stuck at getting the last part of the challenge to work. I even took a peek at Paul's solution to verify and his code and mine are practically the same! I've debugged too many times.
Basically stuck at this part:
Create another view model, this time for EditView. What you put in the view model is down to you, but I would recommend leaving dismiss and onSave in the view itself – the former uses the environment, which can only be read by the view, and the latter doesn’t really add anything when moved into the model.
I've moved everything to a new ViewModel. The issue is pressing save does not actually update the location name or description. Please help!
Here's my code. (PS dont mind that Location is spelled as Locations in my code (its a stupid typo error but Locations is actually Location)
EditView
//
// EditView.swift
// Bucket List
//
import SwiftUI
struct EditView: View {
@Environment(\.dismiss) var dismiss
@State private var viewModel: ViewModel
var onSave: (Locations) -> Void
var body: some View {
NavigationStack {
Form {
Section {
TextField("Place name", text: $viewModel.name)
TextField("Description", text: $viewModel.description)
}
Section("Nearby") {
switch viewModel.loadingState {
case .loading:
Text("Loading...")
case .loaded:
ForEach(viewModel.pages, id: \.pageid) { page in
Text(page.title)
.font(.headline)
+ Text(": ")
+ Text(page.description)
.italic()
}
case .failed:
Text("Please try again later")
}
}
}
.navigationTitle("Place details")
.toolbar {
Button("Save") {
let newLocation = viewModel.save()
onSave(newLocation)
dismiss()
}
}
.task {
await viewModel.fetchNearbyPlaces()
}
}
}
init(location: Locations, onSave: @escaping (Locations) -> Void) {
self.onSave = onSave
_viewModel = State(initialValue: ViewModel(location: location))
}
}
#Preview {
EditView(location: .example, onSave: { _ in })
}
EditView-ViewModel
//
// EditView-ViewModel.swift
// Bucket List
//
import Foundation
extension EditView {
@Observable
class ViewModel {
enum LoadingState {
case loading, loaded, failed
}
var name: String
var description: String
var location : Locations
var pages = [Page]()
var loadingState = LoadingState.loading
init(location: Locations) {
self.name = location.name
self.description = location.description
self.location = location
}
func fetchNearbyPlaces() async {
let urlString = "https://en.wikipedia.org/w/api.php?ggscoord=\(location.latitude)%7C\(location.longitude)&action=query&prop=coordinates%7Cpageimages%7Cpageterms&colimit=50&piprop=thumbnail&pithumbsize=500&pilimit=50&wbptterms=description&generator=geosearch&ggsradius=10000&ggslimit=50&format=json"
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let items = try JSONDecoder().decode(Result.self, from: data)
self.pages = items.query.pages.values.sorted()
loadingState = .loaded
} catch {
loadingState = .failed
}
}
func save() -> Locations {
var newLocation = location
newLocation.name = name
newLocation.description = description
newLocation.id = UUID()
return newLocation
}
}
}
Also, this is my existing save code in the bucket list view and view model from the tutorial. Looks correct to me.
BucketListView
//
// BucketListView.swift
// Bucket List
//
import MapKit
import SwiftUI
struct BucketListView: View {
@State private var startPosition = MapCameraPosition.region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 56,longitude: -3),
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)
)
)
@State private var viewModel = ViewModel()
var body: some View {
if viewModel.isUnlocked {
NavigationStack {
MapReader { proxy in
Map(initialPosition: startPosition) {
ForEach(viewModel.locations) { location in
Annotation(location.name, coordinate: location.coordinate ) {
Image(systemName: "star.circle")
.resizable()
.foregroundStyle(.red)
.frame(width: 44, height: 44)
.background(.white)
.clipShape(.circle)
.onLongPressGesture {
viewModel.selectedPlace = location
}
}
}
}
.mapStyle(viewModel.mapStyle)
.onTapGesture { position in
if let coordinate = proxy.convert(position, from: .local) {
viewModel.addLocation(at: coordinate)
}
}
.sheet(item: $viewModel.selectedPlace) { place in
EditView(location: place) {
viewModel.update(location: $0)
}
}
}
.toolbar {
Button("Switch view") {
viewModel.isHybrid.toggle()
}
}
}
} else {
Button("Unlock Places") {
Task {
await viewModel.authenticate()
}
}
.padding()
.background(.blue)
.foregroundStyle(.white)
.clipShape(.capsule)
.alert(isPresented: $viewModel.alertShown) {
Alert(
title: Text(viewModel.alertTitle),
message: Text(viewModel.alertMessage)
)
}
}
}
}
#Preview {
BucketListView()
}
BucketList-ViewModel
//
// BucketListView-ViewModel.swift
// Bucket List
//
import CoreLocation
import MapKit
import Foundation
import LocalAuthentication
import _MapKit_SwiftUI
extension BucketListView {
@Observable
class ViewModel {
private(set) var locations: [Locations]
var selectedPlace: Locations?
var isHybrid: Bool = false
let savePath = URL.documentsDirectory.appending(path: "SavedPlaces")
var isUnlocked = false
var alertShown = false
var alertTitle = "Error"
var alertMessage = ""
var mapStyle: MapStyle {
guard isHybrid else {
return .hybrid
}
return .standard
}
init() {
do {
let data = try Data(contentsOf: savePath)
locations = try JSONDecoder().decode([Locations].self, from: data)
} 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.")
}
}
func addLocation(at point: CLLocationCoordinate2D) {
let newLocation = Locations(id: UUID(), name: "New Location", description: "", latitude: point.latitude, longitude: point.longitude)
locations.append(newLocation)
save()
}
func update(location: Locations) {
guard let selectedPlace else { return }
if let index = locations.firstIndex(of: selectedPlace) {
locations[index] = selectedPlace
save()
}
}
func authenticate() async {
let context = LAContext()
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
let reason = "Please authenticate yourself to unlock your places."
do {
let success = try await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason)
if success {
self.isUnlocked = true
}
} catch( let error) {
alertMessage = error.localizedDescription
alertShown.toggle()
}
} else {
if let error = error {
alertMessage = error.localizedDescription
alertShown.toggle()
}
}
}
}
}