Eyan Yehya
Eyan Yehya

Reputation: 33

Accessing value from ObservableObject

Note: I'm a beginner to Swift and MapKit so bear with me please. I appreciate it. I have a SwiftUI view that takes an ObservableObject as input

@ObservedObject var viewModel: PostRowViewModel

And within that ObservableObject there is a @Published field:

@Published var post: Post

Now what I want to do is to display a map of using the lat and long values that are fields in this post object. Now the Map view in MapKit is as follows:

            Map(coordinateRegion: Binding<MKCoordinateRegion>)

So I need to provide it with a binding of the region of the post I'm trying to display. What I tried to do is to initialize the region as follows:

@State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: viewModel.post.location.coordinate.latitude, longitude: viewModel.post.location.coordinate.latitude), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))

Using the info from the viewModel. However I get this error:

Cannot use instance member 'viewModel' within property initializer; property initializers run before 'self' is available.

So I searched online and found that to solve the issue you need to use an init function however since the viewModel is given as input to the view and I have Environment variable I dont want to include an init function. I've also tried to create a function inside the viewModel that returns a Binding as follows:


   func createMapRegion() -> Binding<MKCoordinateRegion> {
        @State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: post.location.coordinate.latitude, longitude: post.location.coordinate.longitude), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
        return $region
    }

But I get the warning: Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update. Because the state is being accessing outside since when I then create the map I do it like so:

            Map(coordinateRegion: viewModel.createMapRegion())

So I'm not sure how I can access this info from the post published object using the viewModel and create a Map when the viewModel is given as input to the view.

Any help would be very much appreciated!

Code:

ViewModel:



import Foundation
import SwiftUI
import MapKit

@MainActor
@dynamicMemberLookup
class PostRowViewModel: ObservableObject, StateManager {
..
 
    @Published var post: Post

..
    init(post: Post...) {
        self.post = post
        ...
    }
    
    subscript<T>(dynamicMember keyPath: KeyPath<Post, T>) -> T {
        post[keyPath: keyPath]
    }
}


View:



import SwiftUI
import MapKit

struct PostRowView: View {
.
.
.            
    @ObservedObject var viewModel: PostRowViewModel
    

.
.

    
        
    var body: some View {
       // I want to create a map here that uses the post stored in the viewModel. 
       Map(coordinateRegion: ...)
}


}
        

    
}





Where I want to access the values for lat and long using the Post struct which is:

struct Post: Identifiable, Codable, Equatable {
    .
    .
    .
    var location: LocationInfo
    .
    .
    .
}


Where LocationInfo is:

struct LocationInfo: Codable, Equatable, Identifiable {
    var name: String
    var countryCode: String
    var coordinate:  Coordinate
    var id = UUID()
}


And coordinate is:

struct Coordinate: Codable, Hashable {
    let latitude, longitude: Double
}


Upvotes: 2

Views: 637

Answers (1)

jnpdx
jnpdx

Reputation: 52565

As suggested in the comments, the path of least resistance is probably to put region in a @Published variable on your ObservableObject:

class PostRowViewModel: ObservableObject {
    
    @Published var post: Post
    @Published var region: MKCoordinateRegion
    
    init(post: Post) {
        self.post = post
        self.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: post.location.coordinate.latitude, longitude: post.location.coordinate.longitude), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
    }
}

struct PostRowView: View {
    @ObservedObject var viewModel: PostRowViewModel
    
    var body: some View {
        Map(coordinateRegion: $viewModel.region)
    }
}

Note that you don't actually need a view model for this. You could also do something like:

struct PostRowView: View {
    var post: Post
    @State private var region : MKCoordinateRegion = .init()
    
    var body: some View {
        Map(coordinateRegion: $region)
            .onAppear {
                region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: post.location.coordinate.latitude, longitude: post.location.coordinate.longitude), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))
            }
    }
}

Upvotes: 1

Related Questions