mapbox-maps-ios icon indicating copy to clipboard operation
mapbox-maps-ios copied to clipboard

Making MapBox easier to integrate into a SwiftUI app

Open MartinMajewski opened this issue 4 years ago • 6 comments

Hello together,

so this is a direct follow-up thread to my previous issue at https://github.com/mapbox/mapbox-maps-ios/issues/596

The examples show a very overcomplicated way of getting MapBox run as a SwiftUI view. Generally, I think the Examples-App is not a good source of getting blueprints for your own implementations as it is very hard to analyze and cluttered. It is more of an overloaded demo app and I think this is sad for all newcomers that want to get their fingers wet with MapBox.

Anyhow, for a current project (it is a customer project where the customer has bought the license for MapBox) I need to use MapBox in an existing SwiftUI app in combination with SceneKit. First, I followed the sample code for MapBox in SwiftUI but found that it is super tricky getting it to work with custom layers and SceneKit. This is, because creating a CustomLayerHost is not straightforward and poorly documented. As a SwiftUI view is not a class but a struct, you cannot simply extend it. Extending MapView implies some other issues. On the other hand, the SceneKit example is UIKit only.

To understand my background a little bit better: I lastly developed for iOS over four years back when everything was UIKit. After some journeys to C++, Kotlin, and Java land I returned, and now most modern apps are in SwiftUI. Fine, so may it be this way.

But as I said, I had a hard time getting both examples mixed together until today I found a simple solution and I would hope that it would make its way into the documentation and also a simpler stand-alone examples code.

Instead of wrapping the MapView UIView into a UIViewRepresentable, I wrapped the UIViewController of the SceneKit example into an UIViewControllerRepresentable. This way I do not need to tediously and confusingly transform everything from the UIViewController into SwiftUI land. I could use the SceneKit example (after deleting some boilerplate code) nearly 1:1 in the SwiftUI app.

So to break it down:

The content view method is called as always and 100% inside SwiftUI land:

import SwiftUI
import MapboxMaps

struct ContentView: View {
    var body: some View {
        MapBoxViewController()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The MapBoxViewController is the wrapper struct:

import SwiftUI
import UIKit

struct MapBoxViewController: UIViewControllerRepresentable {

    func makeUIViewController(context: Context) -> MapBoxUiViewController {
        let mapBoxViewController = MapBoxUiViewController()
        
        return mapBoxViewController
    }
    
    func updateUIViewController(_ mapBoxViewController: MapBoxUiViewController, context: Context){
        
    }
    
}

And MapBoxUiViewController is the code taken from the SceneKit example

import UIKit
import SceneKit
import MapboxMaps

public class MapBoxUiViewController: UIViewController, CustomLayerHost {

    internal var mapView: MapView!

    let modelOrigin = CLLocationCoordinate2D(latitude: -35.39847, longitude: 148.9819)
    var renderer: SCNRenderer!
    var scene: SCNScene!
    var modelNode: SCNNode!
    var cameraNode: SCNNode!
    var textNode: SCNNode!
    var useCPUOcclusion = false

    override public func viewDidLoad() {
        super.viewDidLoad()

        let camera = CameraOptions(center: self.modelOrigin,
                                   zoom: 18,
                                   bearing: 180,
                                   pitch: 60)

...

So, from my current point of view, this is a far better way of explaining how to use a) MapBox in SwiftUI and b) it makes it possible to follow the MapBox documentation that is solely based around UIKit.

Moreover, to make it easier to follow the code, a minimal MapBox example, without any advanced features could be used. A tutorial or example for a certain topic should not introduce side-effects or dependencies that are not directly related or necessary for understanding that specific topic!

However, as I am currently digging into MapBox and I didn't have the overall picture, yet, I cannot judge if my approach will cause some complications along the way - like data injection from other parts of the app, or manipulation of the Map and its contents.

Therefore, I want to discuss this approach with you. What do you think about it?

I will also open another feature request where I want to ask for some information regarding SceneKit and the CustomLayerHost (LINK will be inserted here). The latter class is not documented very well and I don't get all the transformation matrices as of now. Why is it necessary. Why are SCNnodes flipped 90° while Collada files are not, etc?

Cheers, Martin

MartinMajewski avatar Aug 19 '21 14:08 MartinMajewski

Thank you for your ticket. Your feedback is very helpful. We are working to improve our documentation across the board, including our examples.

From your ticket, it looks like there are four main action items:

  • A tutorial or additional API documentation for CustomLayerHost.
  • A SwiftUI example and/or tutorial that uses UIViewControllerRepresentable.
  • Evaluate whether our current SwiftUI example can be simplified.
  • Update the current SwiftUI example title and description to provide more information about what the example accomplishes.

I will open additional tickets to track these tasks and link back to them in this comment.

jmkiley avatar Aug 25 '21 13:08 jmkiley

You're welcome!

I can second your action items!

  • CustomLayerHost: a much more detailed explanation of all the transformations is needed. Moreover, it would help if you split the tutorials/examples into smaller chunks. First, explain how to add a SceneKit Scene to the map and provide an example of adding as many objects into different locations as desired. Second, explain how to add terrain, shading, and a skybox. Don't mix everything as it makes reasoning about it very hard. This is why I wouldn't say I like the examples App.
  • UIViewControllerRepresentable: as MapBox is still deep in UiKit-land but also very feature-rich, it needs all those wrapper magic that is much more complex than using some old UIKit-widgets from Apple with SwiftUI. It wouldn't hurt to explain components like coordinators in a nutshell also. As I had a long pause from iOS development, I have difficulty getting all those information together.
  • Examples: As said before, all those examples have to be uncluttered, and all example code should be built as stand-alone apps.

From my example above, there is one major difference using the UIViewControllerRepresentable compared to the UIViewRepresentabel approach: the MapView is build on the viewDidLoad stage. This means that it is not available during the SwiftUI call to body. So you cannot pass mapView anything at this stage, or you get a nil exception.

This gave me headaches during the camera setup, as setCamera cannot be called. So I had to set the camera inside the mapInitOptions and pass this to the constructor of the UIViewControllerReperesentable-struct. And from here, I could pass that variable to the UIKit view controller.

Still, I have to learn how to use the coordinator, and the render call of the LayerHost is still a mystery for me.

MartinMajewski avatar Aug 25 '21 14:08 MartinMajewski

So, I found a workaround for the nill exception, and for some lifecycle issues.

When trying to use the UIKitViewController implementation the mapView gets initialized during the viewDidLoad phase. This is to late for the makeUIViewController method and, therefore, mapView is not available for important configurations like setting up the styleURI, calling the updateUIViewController method from within, or setting up a coordinator.

I guess a lot should be rewritten to get things like the coordinator work with the ViewController instead of the View itself, but I am currently trying to leave as much as possible in alignment with the SceneKit example code to prevent side-effects.

So the workaround is:

In order to call the `makeUIViewController' inside the UiViewControllerRepresentable similarly as in the SwiftUI example code, which is

...

    func makeUIViewController(context: UIViewControllerRepresentableContext<MapBoxSwiftUiViewController>) -> MapBoxUiKitViewController {
        let mapBoxUIKitViewController = MapBoxUiKitViewController()
        
        updateUIViewController(mapBoxUIKitViewController, context: context)
        context.coordinator.mapView = mapBoxUIKitViewController.mapView
        
        return mapBoxUIKitViewController
    }

...

one has to instantiate the mapView in the UIKitViewController before viewDidLoad.

This means that the constructors have to be overwritten:

...

    let screenHeight = UIScreen.main.bounds.height
    let screenWidth = UIScreen.main.bounds.width

    override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        
        mapView = MapView(frame: .init(x: 0.0, y: 0.0, width: screenWidth, height: screenHeight), mapInitOptions: .init())
        
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        mapView = MapView(frame: .init(x: 0.0, y: 0.0, width: screenWidth, height: screenHeight), mapInitOptions: .init())
        
        super.init(coder: aDecoder)
    }

...

However, as you can see I hacked in the frame size manually into the MapView-Initialization by reading the UIScreen.main.bounds properties.

This is not an elegant solution and works only if you have MapBox running on fullscreen.

The reason is that during the constructor call the frame size of this object inside the view tree is still unknown. Initializing the mapView with frame: .zero would make trouble during the mapView setup, which in turn is performed before SwiftUI can set proper frame size.

This .zero instantiation causes a lot of odd behavior:

  1. mapView prints on the console that the frame size is too small and it sets a fallback size of 64x64
  2. the camera cannot calculate a proper position and zoom level and drops (in my case) into the ocean at a ridiculously high zoom-level

Only after everything is loaded and e.g. the Camera is moved to another location everything went back to normal, as the view gets proper size information.

MartinMajewski avatar Aug 26 '21 14:08 MartinMajewski

Any updates on this? I think MapBox V10 should be ready for SwiftUI soon to keep state-of-the-art.

jumbopilot avatar Nov 17 '21 18:11 jumbopilot

Any updates on this? MapBox V10 is ready for SwiftUI ? @knov @MartinMajewski @jmkiley @jumbopilot @zugaldia

ghost avatar Jan 07 '22 14:01 ghost

Thanks for the continued feedback on our SwiftUI support. We will continue to evaluate options to improve the usability of the SDK with SwiftUI and welcome your feedback on specific pain points and use cases.

knov avatar Jan 12 '22 21:01 knov

@MartinMajewski Hi, we are happy to announce that experimental SwiftUI support is now part of the upcoming v11 release 🎉
You can try the first beta release with SwiftUI!

persidskiy avatar Aug 25 '23 11:08 persidskiy