mapbox-maps-ios
mapbox-maps-ios copied to clipboard
Making MapBox easier to integrate into a SwiftUI app
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
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.
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.
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:
- mapView prints on the console that the frame size is too small and it sets a fallback size of 64x64
- 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.
Any updates on this? I think MapBox V10 should be ready for SwiftUI soon to keep state-of-the-art.
Any updates on this? MapBox V10 is ready for SwiftUI ? @knov @MartinMajewski @jmkiley @jumbopilot @zugaldia
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.
@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!