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

SOLVED: SwiftUI How to toggle a Bool in a Struct from an ObservableObject class and show alert to notify user.

Forums > SwiftUI

I have a method in my class that opens a map when given an address string. Trying to show an alert in a view by toggling a boolean in a method in the class. If addressString is inValid. I can't figure out how to toggle the boolean in the class method. This is what I tried. The Published bool in class method updates but does not update in the View. I did put up a repo of just this feature if anybody wants to play around with it.

https://github.com/Ongomobile/OpenMapInSwiftUI

import SwiftUI

@main
struct LocationTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(location: LocationManager())
        }
 }

}

Here is my Class:

import UIKit
import MapKit
import CoreLocation
import Combine

class LocationManager: NSObject, ObservableObject {
    var locationManager = CLLocationManager()
    lazy var geocoder = CLGeocoder()

    @Published var locationString = "1140"
    // @Published var locationString = "1 apple park way cupertino"
    @Published var currentAddress = ""
    @Published var isValid: Bool = true

    func openMapWithAddress () {

        geocoder.geocodeAddressString(locationString) { placemarks, error in
            if let error = error {
                self.isValid = false
                // prints false but does not update
                print("isValid")
                print(error.localizedDescription)
            }

            guard let placemark = placemarks?.first else {
                return
            }

            guard let lat = placemark.location?.coordinate.latitude else{return}

            guard let lon = placemark.location?.coordinate.longitude else{return}

            let coords = CLLocationCoordinate2DMake(lat, lon)

            let place = MKPlacemark(coordinate: coords)

            let mapItem = MKMapItem(placemark: place)
            mapItem.name = self.locationString
            mapItem.openInMaps(launchOptions: nil)
        }

    }
}

Here is the view:

import SwiftUI

struct ContentView: View {
@ObservedObject  var locationManager = LocationManager()
@State private var showingAlert = false

var body: some View {
    Button {
        locationManager.openMapWithAddress()

    } label: {
        Text("Get Map")
        }
        .alert(isPresented: $showingAlert) {
                Alert(title: Text("Important message"), message: 
                Text("Enter a valid address"), dismissButton: 
                .default(Text("OK")))
            }
    }
}

3      

the alert in your view needs to be isPresented from your Bool in your class not the one in your view

change: .alert(isPresented: $showingAlert)

to:

.alert(isPresented: locationManager.!isValid)

(exclamation mark may go in fromt of locationManager rather than isValid, I can't remember :)

3      

Hi @rlong405 thanks for the help but that did not work for me. I did put up a repo of just this feature if anybody wants to play around with it.

https://github.com/Ongomobile/OpenMapInSwiftUI

3      

This is what I can up with that worked could be refactored to be more efficient.

struct ContentView: View {
@State private var showingAlert = false
@State private var addressString = ""
@ObservedObject var locationManager = LocationManager.instance

var body: some View {
    VStack{
        Form{
            Section {
                Text("Enter Address")
                TextField("", text: $addressString)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(.horizontal)
            }
            Button {
                print(addressString)
                getMap()

            } label: {
                Text("Get Map")
            }
            .alert(isPresented: $showingAlert) {
                Alert(title: Text("Important message"), message: Text("Enter a valid address"), dismissButton: .default(Text("OK"), action:{
                    getMap()
                }))
            }
        }
    }

}
func getMap() {
    if locationManager.isValid == false {
        self.showingAlert.toggle()
    } else {
        self.showingAlert = false
        locationManager.openMapWithAddress(address: addressString)
        self.addressString = ""
    }
    locationManager.openMapWithAddress(address: addressString)
    self.addressString = ""
    self.showingAlert.toggle()
}

}

Here is the updated ViewModel

import UIKit
import MapKit
import CoreLocation
import Combine

class LocationManager: NSObject, ObservableObject {
    var locationManager = CLLocationManager()
    lazy var geocoder = CLGeocoder()

    @Published var locationString = "1140"
    @Published var isValid: Bool = true
    static let instance = LocationManager()

    func openMapWithAddress (address: String) {

    geocoder.geocodeAddressString(address) { placemarks, error in
        if let error = error {
            DispatchQueue.main.async { self.isValid = false }
            print(error.localizedDescription)
        }

        guard let placemark = placemarks?.first else {
            return
        }

        guard let lat = placemark.location?.coordinate.latitude else{return}

        guard let lon = placemark.location?.coordinate.longitude else{return}

        let coords = CLLocationCoordinate2DMake(lat, lon)

        let place = MKPlacemark(coordinate: coords)

        let mapItem = MKMapItem(placemark: place)
        mapItem.name = address
        mapItem.openInMaps(launchOptions: nil)
       }

   }
}

3      

How about this.....

puts everything back in your class and don't need the getMap func any more. The issue with my original suggestion is that (as I've now found out) - you can't bind the isPresented of a .alert to a negative value. So getting it to fire when isValid = false doesn't seem to be possible (someone else may know a way).

Anyway, changing your "isValid" bool so it's called "invalid" and then modifying your logic so it works the opposite way around (i.e. if it can't find the address then it sets the invalid bool to true) seems to work. Alert gets fired or maps get loaded as I would expect. I've also bound your textfield directly to the locationString in your class rather than the one in your view.

hope that helps - yours to adopt or trash at your leisure but I think this fits your original ask without having the additional function you created to set a second bool value which as you say is a little unecessary


class LocationManager: NSObject, ObservableObject {
    lazy var geocoder = CLGeocoder()

    @Published var locationString = "1140"
    @Published var invalid: Bool = false
    static let instance = LocationManager()

   func openMapWithAddress () {

        geocoder.geocodeAddressString(locationString) { placemarks, error in
            if let error = error {
                DispatchQueue.main.async
                {
                  self.invalid = true               }
                print(error.localizedDescription)
            }

            guard let placemark = placemarks?.first else {
                return
            }

            guard let lat = placemark.location?.coordinate.latitude else{return}

            guard let lon = placemark.location?.coordinate.longitude else{return}

            let coords = CLLocationCoordinate2DMake(lat, lon)

            let place = MKPlacemark(coordinate: coords)

            let mapItem = MKMapItem(placemark: place)
         mapItem.name = self.locationString
            mapItem.openInMaps(launchOptions: nil)
        }

    }
}

and your view:

struct ContentView: View {
    @ObservedObject var locationManager = LocationManager()

    var body: some View {
        VStack{
            Form{
                Section {
                    Text("Enter Address")
                  TextField("", text: $locationManager.locationString)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding(.horizontal)
                }
                Button {
                  locationManager.openMapWithAddress()

                } label: {
                    Text("Get Map")
                }
                .alert(isPresented: $locationManager.invalid ) {
                    Alert(title: Text("Important message"), message: Text("Enter a valid address"), dismissButton: .default(Text("OK"), action:{
                     locationManager.invalid = false
                     locationManager.locationString = ""
                    }))
                }
            }
        }
    }
}

4      

@rlong405 Thank you very much my friend very nice refactor. This really helps me to get a better grip on SwiftUI. I replaced the repository with a new one and commited changes giving you credit for the refactor https://github.com/Ongomobile/OpenMapInSwiftUI Maybe others could use this.

Cheers, Mike Haslam

3      

Hey Mike

thats very kind of you thankyou for the github credit (my first).

still learning swiftui myself and self taught but ive been doing quite a bit of View Model work recently so felt I could add some help here.

plus I learnt something too about the isPresented thing so thats a bonus

best of luck with your project

Rich

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.