The interactive globe that almost broke my app
Atlas: Global Intelligence is a geopolitical news app. Wars, elections, sanctions, maritime incidents -- stuff that's inherently spatial. When I started building it, I knew the primary navigation had to be a globe. Not a flat map with markers. A 3D sphere you could spin and explore.
A border dispute between two countries is clearer when you can see the border. A naval standoff means more when you can see how close the ships are to a chokepoint. Geopolitics is geography, and a globe makes geography feel real in a way that a scrollable list of headlines never will.
SceneKit seemed right
I built the globe with SceneKit. Create an SCNSphere, wrap it in an Earth texture, drop it in an SCNView, and you've got a planet. Event pins are SCNNode objects positioned using latitude/longitude converted to 3D coordinates on the sphere's surface. Each pin has a color indicating event type -- red for conflict, orange for economic disruption, blue for diplomatic activity.
Getting a basic version working took a weekend. Getting it to not destroy the user experience took months.
500 pins will ruin your frame rate
First time I loaded real data, the globe had over 500 active event pins. Frame rate dropped to single digits. SceneKit was rendering every pin as its own node with geometry, materials, and a label. On an iPhone 13 this was already rough. On anything older, unusable.
Fix had two parts. First, geographic clustering. Pins within a configurable distance threshold get merged into a single cluster node that displays a count badge. I used a simple grid-based spatial hash -- divide the globe's surface into cells, group pins by cell, merge cells that exceed a density threshold. Second, level of detail rendering. When the camera is zoomed out, even unclustered pins render as flat colored dots. Full 3D pin geometry with labels only kicks in once you're zoomed in close enough to read them. Those two changes together brought the frame rate back to 60fps with thousands of events loaded.
Rotation vs. taps
Gesture handling on a 3D globe is surprisingly annoying. User puts their finger down and moves it -- are they rotating the globe or trying to tap a pin that's slightly off center? I tried a few approaches before landing on one that felt right. The pan gesture recognizer feeds into a delegate that tracks velocity. If the finger moves faster than a threshold before lifting, it's a rotation. If it stays roughly stationary, it's a tap. I also added a short delay before committing to rotation so that slow drags starting near a pin still register as taps if they don't travel far.
The texture problem
A detailed Earth texture at 8K resolution looks gorgeous on a new iPhone. It also uses a staggering amount of memory. On older devices with 3GB of RAM, loading the texture alongside the rest of the app triggered memory warnings and sometimes crashes. I ended up shipping three texture resolutions -- 2K, 4K, and 8K -- and selecting one at launch based on ProcessInfo.processInfo.physicalMemory. Devices with 4GB or less get 2K. 6GB gets 4K. Everything above that gets the full 8K. Unglamorous but it works.
I almost killed it
There was a point, maybe two months in, where I seriously considered replacing the globe with a flat Mercator map using MapKit. Would've solved the performance issues, the gesture issues, the memory issues, all of it. But the globe was the whole identity of the app. The App Store screenshots, the name, the way users talked about it in reviews -- all of it centered on spinning this thing around and discovering what's happening in the world. Switching to a flat map would've made Atlas just another news app with pins.
So I kept debugging. The clustering, the LOD system, the texture tiers, the gesture thresholds -- none of it was elegant, but together they made the globe fast enough and reliable enough to ship. Glad I didn't cut it. But I understand why people cut features like this.
-- Daniel