mousebird-consulting-inc / WhirlyGlobe

WhirlyGlobe Development
Other
828 stars 255 forks source link

Recommended way of Using WhirlyGlobe from a SwiftUI setup #1549

Closed Niklas81 closed 2 years ago

Niklas81 commented 2 years ago

I'm building an app based on SwiftUI, which means I need to display the UIView that WhirlyGlobe generates inside a SwiftUI view. Could you please provide a recommended way of doing this, preferably with some sample code? I'm guessing it would involve bridging WhirlyGlobe with something like UIViewControllerRepresentable or UIViewRepresentable, but I can't quite get it to work all the way.

sjg-wdw commented 2 years ago

I know we have users who have done it, but we don't have any clients who use SwiftUI so we're not familiar with it ourselves.

paulgee31 commented 2 years ago

This should give you some idea of what is required. So using the attached files and adding a tile source you should be able to get it going. I use the Cocoapod version WG for SwiftUI.

There are two files; WhirlyGlobalManager which provides a singleton to the underling WG ViewController which is where you can control the view directly, add layers etc.

The WhirlyGloobeSwiftUIView file is the control you place in your Swift Views (see below) and also where you tap into the WGController delegate methods.

struct PreviewView: View {
    var whirlyGlobe = WhirlyGlobeSwiftUIView()

    var body: some View {
        VStack(alignment:.leading){
                whirlyGlobe

Swift UI WhirlyGlobe.zip

Niklas81 commented 2 years ago

Thanks for your responses! I'm now doing something similar to @paulgee31, but I'm using a UIViewRepresentable instead of UIViewControllerRepresentable. I'm keeping mbTilesFetcher and imageLoader as properties of the coordinator, as keeping them in WhirlyGlobeView will require them to be @State or I won't be able to update them - also I then have to update wrapped within DispatchQueue.main.async {} which I'd rather want to avoid. I have a setup that allows me to switch map source depending on network connectivity.

I did have a lot of crashes related to metal, and my initial impression once I started playing with Whirly was quite dampened from the impression I got from a YouTube pitch. At first I tried to import it as a Cocoa Pod, which required me to hack the source or it wouldn't build. Then I switch to binary, but the instructions are outdated, and it wasn't until I found out that binaries must now be as .xcframework that things cleared out. I managed to find #1455 which included a link to a working binary.

Same frustration when following the tutorial that tells you to implement tap detection using func globeViewController(viewC: WhirlyGlobeViewController, didSelect selectedObj: NSObject) {}, which didn't work. Looking at the reference I discover this has been renamed to func globeViewController(_ viewC: WhirlyGlobeViewController, didSelect selectedObj: NSObject) {} (adding _ before viewC).

Because of this, I would like to ask you to update the tutorial so it describes current conditions, and the binary download should of course link to a working binary, not one that is certain to fail. The new binary (.xcframework format) also works without adding Search paths or bridging header.

This all may sound a bit harsh, but making these simply tutorial adjustments would have saved me many hours filled with frustration. I was honestly starting to doubt the quality of what now really seems like a great toolkit.

Keeping this issue open until I get a confirmation that it has been read by WhirlyGlobe maintainers.

Including a bit of my setup in case it helps someone else using this in a SwiftUI project. The following is simply called as WhirlyGlobeView(filtersReset: self.$aBindingBool):

struct WhirlyGlobeView: UIViewRepresentable {

    // Common state model
    @EnvironmentObject var state: CommonState

    @Binding var aBindingBool: Bool

    @State var viewC: WhirlyGlobeViewController? = WhirlyGlobeViewController()

    func makeUIView(context: Context) -> UIView {

                let aView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
                if self.viewC != nil {

                    viewC!.view.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
                    viewC!.frameInterval = 2
                    viewC!.rotateGesture = true
                    viewC!.keepNorthUp = true
                    viewC!.delegate = context.coordinator
                    viewC!.height(forMapScale: 0.5)
                    viewC!.clearColor = UIColor(Color("background"))

                    aView.addSubview(self.viewC!.view)
                }

                return aView
            }

            func updateUIView(_ uiView: UIView, context: Context) {

                if aBunchOfCustomConditions {
                            updateMap(uiView, context: context)
                }
            }

            func updateMap(_ uiView: UIView, context: Context){
            // Loads either offline or online map depending on network connection (omitted actual code)
            }

            func makeCoordinator() -> Coordinator {
                Coordinator(self)
            }

            class Coordinator: NSObject, WhirlyGlobeViewControllerDelegate, MaplyViewControllerDelegate {

                    let parent: WhirlyGlobeView
                    let control: WhirlyGlobeViewController
                    var initialised = false

                    var mbTilesFetcher : MaplyMBTileFetcher? = nil
                    var imageLoader : MaplyQuadImageLoader? = nil

                init(_ parent: WhirlyGlobeView)
                    {
                        self.parent = parent
                        self.control = parent.viewC!
                    }

                    func globeViewController(_ viewC: WhirlyGlobeViewController, didTapAt coord: MaplyCoordinate) {
                        if imageLoader == nil {
                            return
                        }

                            print("Tap at \(coord)")

                    }

            }

}
sjg-wdw commented 2 years ago

Thanks for taking the time to write this. I can understand your frustration.

On the subject of Metal, I'd recommend running on hardware rather than the simulator when you see that sort of problem. The simulator is often very weird, depending on your MacOS version and model. It's kind of amazing it works as well as it does! [Don't get me started on Metal debugging]

As for running on the main thread, WhirlyGlobe-Maply certainly doesn't require you to do that. We're multi-threaded to a frankly absurd degree on both iOS and Android. Perhaps that's some constraint of SwiftUI. Not familiar enough with it to say.

We would absolutely love to update the tutorials for both platforms, include an explicit SwiftUI version, fix up all the variants of Cocopods, make a Swift package... you name it. But let's focus on the tutorials.

I'm going to draw back the curtain on this open source project because it's a conversation I've had in private a lot recently.

Competent documentation is as good as gold and costs about as much. mousebird consulting paid for a couple of rounds over the years, each about $20k (USD). I also did one myself, which frankly cost a bit more. My time is very expensive.

What you're suggesting is a minimum of $10k (USD). A chunk of money, but worthwhile so we just need to take it out of one of our revenue streams which are.... really only consulting. Support contracts never made any money. In fact, they were a loss leader for consulting. So this stuff comes out of our consulting overhead.

A lot of things come out of our consulting overhead. Answering questions, which we really like to do, setting up and running the CI infrastructure, and fixing problems on new versions of the various OS's. Sometimes clients pay for that last one if they run into it first. Documentation is on our list, but we don't get to it all that often.

The last major update to the tutorials was when I hired Tim and put him on that for a couple of weeks. Actually much of the infrastructure work over the last couple of years was Tim's overhead time. That money comes right out of our pockets, by the way, but I was delighted to have him do it.

Will someone else do the tutorials for free? In my experience, no. And if you're up on open source literature you know that's true in general.

That's a bit worrying, right? Should you switch to another toolkit? Well, here's where it gets interesting because your choices are pretty limited anymore. MapboxGL used to be free, but now it's not and you have to drop back to an old version to use what's now called MapLibre. That old version.... doesn't support Metal. So, weirdly, you're more future-proof with WhirlyGlobe-Maply if you can figure out how to use it. MapLibre wants to upgrade to Metal, but that's gonna cost multiple six figures to get there. Best of luck, glad it's not my problem.

As to the future of our own toolkit, it's pretty solid, but very focused on the big paying users. One of those is us in another form (Wet Dog Weather). The others are big clients we picked up over the years who pay for instant responses and a week of work here and there. I've got nothing but love for those people; they really made the last few years work.

That's the state of things and certainly more information than you wanted. Obviously I'm going to reuse a lot of this. Best of luck on your project and navigating this weird world of open source geospatial.

Niklas81 commented 2 years ago

Thanks for the insights! As for metal stability, everything ran super smoothly once I built it using the working binary.

I have full respect for the balance you have to strike between keeping this open source whilst still not having it as a burden. From our end, the application I'm building is still in development and not being used by any clients. Should that change we may well become a paying customer one day. More important at the moment is the assurance that the project will remain alive in some form for the foreseeable future. Although it takes a while to figure things out, I'm getting there, and finding this framework really useful. I have previously implemented many of our intended features in MapKit, only to discover that overlays won't load in offline mode using a globe map, making it rather useless for our use case. Hopefully WhiryGlobe will take us all the way 😃

Much appreciate your elaboration on the state of the framework, and wishing you a great summer!