UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Day 73 - Need Help!! Bucket List Challenge

Forums > 100 Days of SwiftUI

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()
                }
            }
        }
    }
}

2      

The issue is in this line

func update(location: Location) {
            guard let selectedPlace else { return }

            if let index = locations.firstIndex(of: selectedPlace) {
                locations[index] = location // in your code you assign selectedPlace insead of location
                save()
            }
        }

2      

@ygeras. thanks that solved it. I can't believe I missed out that detail. Was pretty sure the view model code was fine. But missed out that crucial detail. Thanks, I can sleep peacefully tonight.

2      

Hello,

I just completed the challenges for Project 14. There are a couple of things I noticed while doing these tasks and I'm hoping someone here would have an explanation for it.

  1. In ContentView-ViewModel.swift we import CoreLocation to work with CLLocationCoordinate2D. Why wasn't CoreLocation imported in Location.swift?
  2. In Part 3 of the project (Selecting and editing map annotations), we change the id property of the Location struct to a variable, so as to assign to a new id while updating the location. Paul provides an explanation for it in the video. However, I changed it back to a constant property and the location gets updated just fine. Has anyone else noticed this?

I'm using Xcode 15.2 and deploying for iOS 17.0.1.

2      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.