Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotations don't update on map if they are updated in ContentView.swift #3

Closed
anark opened this issue Dec 30, 2019 · 28 comments · May be fixed by #9
Closed

Annotations don't update on map if they are updated in ContentView.swift #3

anark opened this issue Dec 30, 2019 · 28 comments · May be fixed by #9

Comments

@anark
Copy link

anark commented Dec 30, 2019

If I append an annotation to the annotation variable in ContentView.swift I expect it to show up on the map in the mapView.

Using a debugger I can see that the annotation variable is in fact updated in the mapView and updateUiView is called, however the marker never appears on the map. What needs to be done to make these changes appear on the mapView?

@zacharyblank
Copy link

Just stumbled into this same exact issue.

@folivi
Copy link

folivi commented Jan 4, 2020

Hi, I'm having the same issue!

@anark @zacharyblank, Have you managed to make it work?

my updateAnnotations function looks like this:

    private func updateAnnotations(){
        if let currentAnnotations = mapView.annotations {
            mapView.removeAnnotations(currentAnnotations)
        }
        mapView.addAnnotation(MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791434, longitude: -122.396267)))
                
        annotations.forEach({ (_annotation) in
            print("_annotation.coordinate \(_annotation)")
            mapView.addAnnotation(MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791034, longitude: -122.395267)))
        })
        
        mapView.addAnnotations(annotations)
    }

this part actually adds a marker on the map:
mapView.addAnnotation(MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791434, longitude: -122.396267)))
but neither adding through a foreach loop nor calling addAnnotations work, while I can see the annotations in the debug console.

When I follow the guide works perfectly for me but my annotations are fetched via a network request and passed down to the mapview via a @published variable.
I tried creating a custom annotationView but it doesn't work either. Only the annotation which is added manually is displayed

@CodyPChristian
Copy link

I also ran into this issue not just with annotations but with any of the @published or bindable data passed into the mapView. Ex: mapView.styleURL or mapView.UserTrackingMode. The map just never updates to reflect the changes dynamically.

@folivi
Copy link

folivi commented Jan 7, 2020

Looks like the team is not active at all.
This should a priority fix since Swiftui is gaining popularity everyday.
We are thinking of using Mapkit as fallback

@fabian-guerra
Copy link
Contributor

Hi. Thank you for looking at this demo. This repository is not a Mapbox framework; is the companion code for this tutorial: https://docs.mapbox.com/help/tutorials/ios-swiftui/

Mapbox is an UIKit based framework with a cocoa MVC architecture that relies on a view controller implementation to control the lifecycle of all the controller's views. This repository explains how the Mapbox Maps SDK for iOS can be integrated with the newly developed Apple's SwiftUI technology.

SwiftUI takes a react native architectural approach (or smalltalk like if you are familiar) to propagate changes.

In the ContentView I have declared:

@State var annotations: [MGLPointAnnotation] = [
        MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791434, longitude: -122.396267))
        
    ]

And in the MapView struct:

@Binding var annotations: [MGLPointAnnotation]

Which tells SwiftUI to observe any changes in ContentView.annotations to update the state in MapView; but is SwiftUI's binding model that controls the update lifecycle.

This repo is for reporting issues on the tutorial. For support or assistance with "how to" questions, please reach out to our support team. You can also ask other users on StackOverflow.

@CodyPChristian
Copy link

@fabian-guerra I have reached out to your support team countless times without replies. Using the code above just like in the tutorial does not work. Same goes for other parameters as well. The map never updates after its rendered the initial time. The only version that does work is the pure UIKit code without SwiftUI in the mix.

@fabian-guerra
Copy link
Contributor

Hi, @CodyPChristian. In my comment #3 (comment) I explained why is not working. SwiftUI has a different approach to update ui elements. The code in this comment #3 (comment) intends to follow the UIKit pattern.

That is why is not a Mapbox nor this tutorial issue but how you use SwiftUI. From Apple's documentation

States and bindings connect views to your app’s underlying data model. When you declare a state, SwiftUI stores it for you and manages the state’s connections to its view. When the state updates, the view invalidates its appearance and updates itself.

@folivi
Copy link

folivi commented Jan 8, 2020

Hi @fabian-guerra
I have the same issue as @CodyPChristian and sorry to say this, maybe you're not the one we should be reporting this to, but it's impossible to reach to the Mapbox team as they are not responsding. And your comment doesn't help either. I think we understood why it's not working but what we need the most is a solution or a workaround. For my company for example this is a critical issue, as in our case for us Mapbox doesn't work on SwiftUI.

@fabian-guerra
Copy link
Contributor

Hi @folivi.

Just to clarify:

maybe you're not the one we should be reporting this to

I wrote the tutorial and this companion code. What you are saying is that I don't know about the code I wrote?

I think we understood why it's not working but what we need the most is a solution or a workaround

What you are saying is that since you are a paid customer we are not providing a prompt technical support?

Reading again my comment #3 (comment) I realized that I was not clear enough to state that to apply any changes to the view, you have to update ContentView.annotations but I assumed you were familiar with the SwiftUI's update cycle. For any property you want to change in the MGLMapView you will have to provide the bindings (@State + @Bindings). The Mapbox Maps SDK for iOS is not a SwiftUI framework, this guide explains how you could integrate a cocoa based framework with SwiftUI.

For reference on learning how to control the view's appearance based on data changes I suggest you read Apple's documentation: https://developer.apple.com/documentation/swiftui/state_and_data_flow

I am waiting for your reply with the answers for the above questions.

@folivi
Copy link

folivi commented Jan 8, 2020

Hi @fabian-guerra
I have no problem with you code as it works fine

maybe you're not the one we should be reporting this to

I mean by this that we are here commenting a tutorial when maybe we should be reaching out to someone from the Mapbox development team to report the issue.
The issue is not about being a paid customer or whatsoever.

I contacted the Mapbox twitter, no response, created a ticket, still no reaction. This has nothing to do with your tutorial.
I'm very familiar to the state management in SwiftUI. We spent a lot of time playing with the integration in SwifUI. The data flow work perfectly. The changes on the binding are successfully sent to the MapView but some reason we can't figure out yet it doesn't react at all.
Have you succeeded yourself dynamically adding and removing annotations to your map and see it update accordingly?

@fabian-guerra
Copy link
Contributor

Hi, @folivi.

Thank you for clarifying. I am a team member of the Mapbox Maps SDK for iOS too.

Have you succeeded yourself dynamically adding and removing annotations to your map and see it update accordingly?

Yes, as I mentioned in my previous comments you have to update the ContentView.annotations property. This property has a binding to the MapView struct and when you update it (ContentView.annotations); SwiftUI's will propagate the change to the MapView and it will refresh the view.

In this example I am adding and removing an annotation updating ContentView.annotations instead of mapView.addAnnotation as you have in your example.

var body: some View {
        VStack {
            MapView(annotations: $annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16)
            Button(action: {
                self.annotations.append( MGLPointAnnotation(title: "Mapbox 1", coordinate: .init(latitude: 37.791400, longitude: -122.396200)))
            }) {
                Text("Add annotation")
            }
            Button(action: {
                self.annotations.removeLast()
            }) {
                Text("Remove annotation")
            }
        }
        
    }

@folivi
Copy link

folivi commented Jan 8, 2020

Thank again for your time.
It's like we're talking about the same thing but I can't see where I'm wrong
This is my implementation in an app my company is working on:

struct CoummunityScreen: View {
    @ObservedObject var communityVm: CommunityVM = CommunityVM()
    @State var annotations: [MGLPointAnnotation] = [
        MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 31.790234, longitude: -120.016267)),
        MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 27.191434, longitude: -123.296267)),
    ]
    
    init(){
        self.communityVm.getEvents()
    }
    
    var body: some View {
        VStack {
            Text(String(self.annotations.count))
            MapView(annotations: $annotations)
        }.onAppear {
            let delayInSeconds = 3.0
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayInSeconds) {
                //self.annotations = self.communityVm.eventsAnnotations
               self.annotations =      [
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.781434, longitude: -122.816267)),
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 36.771434, longitude: -122.096267)),
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.761434, longitude: -120.296267)),

                ]
            }
        }
        
    }
}

In the Mapview

@Binding var annotations: [MGLPointAnnotation]

    private func updateAnnotations(){
        if let currentAnnotations = mapView.annotations {
            mapView.removeAnnotations(currentAnnotations)
        }
        print("annotations.count \(annotations.count)")
        mapView.addAnnotations(annotations)
    }

After 3 seconds you will see the log in the console print 3 but the annotations won't change.
Have you tried something like this?

@CodyPChristian
Copy link

Thanks for the replies @fabian-guerra. Just like @folivi I’m still unable to get your sample above to work as you stated. Here is a gist of my code when I filed the original support ticket. In this case I was trying to change the map style and toggle user location tracking using SwiftUI. The annotations in this example are not bindable on purpose as I don’t change them after the map loads, however i have another version of this that does have them dynamic similar to @folivi

https://gist.github.com/CodyPChristian/3bfa0faa85bbc686278e7bffc57f5790

@fabian-guerra
Copy link
Contributor

Hi. I have modified my example and works too:

var body: some View {
        VStack {
            MapView(annotations: $annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16)
            Button(action: {
                self.annotations.append( MGLPointAnnotation(title: "Mapbox 1", coordinate: .init(latitude: 37.791400, longitude: -122.396200)))
            }) {
                Text("Add annotation")
            }
            Button(action: {
                self.annotations.removeLast()
            }) {
                Text("Remove annotation")
            }
        }.onAppear() {
            let delayInSeconds = 3.0
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayInSeconds) {
               self.annotations =      [
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.781434, longitude: -122.816267)),
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 36.771434, longitude: -122.096267)),
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.761434, longitude: -120.296267)),

                ]
            }
        }
        
    }

swiftui

Thank you all for using Mapbox. The above proves that this has nothing to do with our SDK but with SwiftUI. I will keep this ticket closed.

@CodyPChristian
Copy link

@fabian-guerra thanks I appreciate your help. I will give this latest code a test tonight. Can you confirm if the styleURL can be updated from SwiftUI in a similar approach?

@CodyPChristian
Copy link

@fabian-guerra I have spent some time tonight messing with your latest examples and I can not get them working. I can see in the console that the updateUIView does fire when I hit the button or when the timer fires, but there is no new annotations on the map. One difference though is you are using MGLPointAnnotation where I am using CustomPointAnnotation from one of the other Mapbox tutorials. Outside of that our setup is the same.

@CodyPChristian
Copy link

CodyPChristian commented Jan 9, 2020

@fabian-guerra okay I did some more testing. I took this repo and applied your demo above and upgraded it to 5.6.0 and it worked great. However the second I tried to duplicate your logic and set mapView.styleURL in a similar way to what you did with Annotations, the map view stopped updating for both the Annotations and the mapStyle.

Next I removed that code back to where annotations work and replaced the annotations with the CustomPointAnnotations found in https://docs.mapbox.com/ios/maps/examples/custom-callout/ which show up fine, until you try to update them like you are doing in your demo, once again the map doesnt update the annotations.

Can you please explain why this is the case? What key thing are we missing that is causing plain MGLAnnotations to work but once you extend them like CustomPointAnnotation or other properties on mapView like mapView.styleURL cause the map to suddenly no longer update?

One thing that does sorta work is instead of doing self.styleURL inside of a button I did it inside the DispatchQueue you added in your last example above and that time it did update the map style but at the same time it broke the annotations that was working before that timer fired. Something funky is going on for sure.

Thanks

@folivi
Copy link

folivi commented Jan 11, 2020

@CodyPChristian have you figure out how to solve your issue?

@CodyPChristian
Copy link

@folivi Not yet, did you? Hoping @fabian-guerra can take a look at my previous message and help out. Thanks

@CodyPChristian
Copy link

@folivi @fabian-guerra I made a discovery today as well. I managed to get the one issue working where I couldn't update the map style (mapView.styleURL), I called it within a DispatchQueue.main from a Button instead of an ActionSheet and it works fine. Now I wonder why the ActionSheet doesn't work. Very interesting. Here is what I changed.

From:

Button(action: {
    self.showingSheet = true
}){
    Text("Map Style")
}.actionSheet(isPresented: $showingSheet) {
    ActionSheet(title: Text("Map Settings"), message: Text("Select Map Style"), buttons: [
        .default(Text("Dark")) {
            DispatchQueue.main.async() {
                self.styleURL = URL(string: "mapbox://styles/REDACTED")!
            }
        },
        .default(Text("Satellite")) {
            DispatchQueue.main.async() {
                self.styleURL = URL(string: "mapbox://styles/REDACTED")!
            }
        },
        .default(Text("Default Streets")) {
            DispatchQueue.main.async() {
                self.styleURL = URL(string: "mapbox://styles/mapbox/streets-v11")!
            }
        },
        .cancel()
    ])
}

To:

Button(action: {
    DispatchQueue.main.async() {
        self.styleURL = URL(string: "mapbox://styles/mapbox/streets-v11")!
    }
}){
    Text("Streets")
}
Button(action: {
    DispatchQueue.main.async() {
        self.styleURL = URL(string: "mapbox://styles/REDACTEDr")!
    }
}){
    Text("Dark")
}
Button(action: {
    DispatchQueue.main.async() {
        self.styleURL = URL(string: "mapbox://styles/REDACTED")!
    }
}){
    Text("Satellite")
}

I also tested the user location stuff again using a Button like above and that also works.

I pass the @State variables into MapView like so:

MapView(annotations: $annotations, styleURL: $styleURL, trackUser: $trackUser)

and within MapView.swift I did the following to get everything working.

struct MapView: UIViewRepresentable {
    @Binding var annotations: [MGLPointAnnotation]
    @Binding var styleURL: URL
    @Binding var trackUser: Bool
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
        mapView.delegate = context.coordinator
        mapView.styleURL = self.styleURL
        trackingUser()
        return mapView
    }
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
        updateAnnotations()
        mapView.styleURL = self.styleURL
        trackingUser()
    }
private func trackingUser(){
        if self.trackUser {
            mapView.showsUserLocation = true
            mapView.userTrackingMode = .followWithHeading
        } else {
            mapView.showsUserLocation = false
            mapView.userTrackingMode = .none
        }
    }

@folivi
Copy link

folivi commented Jan 16, 2020

@CodyPChristian
Building my annotations from the data I get from my server does definetely not work. The updateUIView detects the changes on the annotations variable but doesn't rebuild the map.
We decided to use Mapkit instead.
Maybe I'll try Mapbox again if I have time to spend

@andersoncanteiro
Copy link

andersoncanteiro commented Feb 4, 2020

@CodyPChristian @folivi I was facing the same issue. When I bind something on content view, the map view stop refreshing. Then I use mapview that was created on makeUIView just on coordinator, to get delegate functions, to update annotations, user location, style I'm using uiView from updateUIView and everything is working fine.

@staticdreams
Copy link

I can confirm that to some extent example provided by @fabian-guerra works.

However, I found no way to populate the map with externally provided data (e.g after making a request to the API). Making an async call first seem to block all the consequent of altering of bound data on the map.
Perhaps one day Mapbox will be able to fix this or provide a workaround with an example.

@bilics
Copy link

bilics commented Mar 18, 2020

Hi @fabian-guerra!

Can you please clarify something:

The demo stops showing any new added annotation when a Text() is added above the MapView() call like bellow:

VStack {
         Text("\(annotations.count)")
         MapView(annotations: $annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16)
...
}

Does this have to do with the SwiftUI propagation and new UI update pattern? Or am I doing something wrong?

Thanks for any insights (or for pointing out what I am doing wrong).

Best regards,

Hi. I have modified my example and works too:

var body: some View {
        VStack {
            MapView(annotations: $annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16)
            Button(action: {
                self.annotations.append( MGLPointAnnotation(title: "Mapbox 1", coordinate: .init(latitude: 37.791400, longitude: -122.396200)))
            }) {
                Text("Add annotation")
            }
            Button(action: {
                self.annotations.removeLast()
            }) {
                Text("Remove annotation")
            }
        }.onAppear() {
            let delayInSeconds = 3.0
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayInSeconds) {
               self.annotations =      [
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.781434, longitude: -122.816267)),
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 36.771434, longitude: -122.096267)),
                    MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.761434, longitude: -120.296267)),

                ]
            }
        }
        
    }

swiftui

Thank you all for using Mapbox. The above proves that this has nothing to do with our SDK but with SwiftUI. I will keep this ticket closed.

@kennethlng
Copy link

kennethlng commented May 5, 2020

I was having the same issue. The following worked for me:

func updateUIView(_ uiView: MGLMapView, context: Context) {
        updateAnnotations(uiView)
        trackUser(uiView)
}

private func updateAnnotations(_ view: MGLMapView) {
        if let currentAnnotations = view.annotations {
            view.removeAnnotations(currentAnnotations)
        }
        view.addAnnotations(annotations)
}

private func trackUser(_ view: MGLMapView) {
        if isTrackingUser {
            view.userTrackingMode = .follow
        } else {
            view.userTrackingMode = .none
        }
    }

You have to use the uiView in updateUIView to update the annotations.

@bilics
Copy link

bilics commented May 7, 2020

Thanks! This does make it work!

@Novajox
Copy link

Novajox commented Jun 4, 2020

I was having the same issue. The following worked for me:

func updateUIView(_ uiView: MGLMapView, context: Context) {
        updateAnnotations(uiView)
        trackUser(uiView)
}

private func updateAnnotations(_ view: MGLMapView) {
        if let currentAnnotations = view.annotations {
            view.removeAnnotations(currentAnnotations)
        }
        view.addAnnotations(annotations)
}

private func trackUser(_ view: MGLMapView) {
        if isTrackingUser {
            view.userTrackingMode = .follow
        } else {
            view.userTrackingMode = .none
        }
    }

You have to use the uiView in updateUIView to update the annotations.

You saved my day!

@Matejy
Copy link

Matejy commented Feb 7, 2021

I was having the same issue. The following worked for me:

func updateUIView(_ uiView: MGLMapView, context: Context) {
        updateAnnotations(uiView)
        trackUser(uiView)
}

private func updateAnnotations(_ view: MGLMapView) {
        if let currentAnnotations = view.annotations {
            view.removeAnnotations(currentAnnotations)
        }
        view.addAnnotations(annotations)
}

private func trackUser(_ view: MGLMapView) {
        if isTrackingUser {
            view.userTrackingMode = .follow
        } else {
            view.userTrackingMode = .none
        }
    }

You have to use the uiView in updateUIView to update the annotations.

Thank you very much! You've just saved my final year project!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.