{
  "version": "https://jsonfeed.org/version/1",
  "title": "Ian's Digital Garden",
  "home_page_url": "https://ianwwagner.com/",
  "feed_url": "https://ianwwagner.com//tag-apple.json",
  "description": "",
  "items": [
    {
      "id": "https://ianwwagner.com//reverse-engineering-better-mapkit-tile-overlays.html",
      "url": "https://ianwwagner.com//reverse-engineering-better-mapkit-tile-overlays.html",
      "title": "Reverse Engineering Better MapKit Tile Overlays",
      "content_html": "<p>Despite nearly 15 years of developing iOS apps on a daily basis,\nit’s occasionally jarring to go back to some of the older APIs.\nToday's blog is about <a href=\"https://developer.apple.com/documentation/mapkit/\">MapKit</a>,\nwhich isn't quite as old as CoreGraphics, but dates back to iOS 3.0!\nIt was a bit slimmer back then, but some of the APIs we'll be looking at today go back to iOS 7!</p>\n<p>To set the stage, <code>MKMapView</code>, the main &quot;view&quot; in MapKit,\nlets you add a sort of minimalist Apple Maps experience to your app in approximately one line of code.\nOr no code if you're using Storyboards / Interface Builder.\nThis is about 100x easier than it is on any other platform I can think of.</p>\n<p>But map-centric user experiences almost never stop with just a basemap.\nYou probably want to throw some data on top, like the outline of an area of interest,\nmaybe filled in with some color coding.\nOr maybe a route line, or a &quot;pin&quot; to show all the nearby hotels and how much they charge per night.\nThese are all <em>vector</em> data in industry jargon.</p>\n<p>One of my former clients was an agricultural tech startup that was later acquired\nby a company with a large business related to weather data.\nIt turns out weather is really important to farmers.\nAnd a lot of weather data, like radar and expected precipitation,\nare displayed as a <em>raster</em> overlay on the map.\nIn this case, usually transparently.</p>\n<p>A second use case for overlays is custom cartography.\nApple's maps are fine (superb, actually!) for many use cases,\nbut you can't customize very much about them.\nThis might be a dealbreaker if you need something outside the box\nlike hillshading or ocean depth,\nor if you want to swap out the satellite imagery for something more locally up to date.</p>\n<p>Overlays are the solution to this too; you can replace the entire base layer with your own!\n(Sadly MapKit can't, and probably won't ever support slick vector tile rendering from third-party sources.\nCheck out <a href=\"https://maplibre.org/\">MapLibre Native</a> instead for vector basemaps and a whole lot more.)</p>\n<p>Today's post is about overlays in MapKit,\nsome surprising behaviors that I found along the way,\nand maybe even a few bits of <a href=\"https://xkcd.com/979/\">ancient wisdom</a>\nto share on StackOverflow.</p>\n<h1><a href=\"#a-tale-of-two-mapkits\" aria-hidden=\"true\" class=\"anchor\" id=\"a-tale-of-two-mapkits\"></a>A Tale of two MapKits</h1>\n<p>MapKit has long had support for overlays.\nPer <a href=\"https://developer.apple.com/documentation/mapkit/mkoverlayrenderer\">Apple's docs</a>,\nit looks like user overlays were added in iOS 7.\nBut if you poke around closely,\nyou might notice that the MapKit docs are subtly split into two APIs:\n<em>MapKit for AppKit and UIKit</em>, and <em>MapKit for SwiftUI</em>.</p>\n<p>The SwiftUI docs aren't just about a nicer way to use <code>MKMapView</code> in your SwiftUI apps;\nit's about a completely different API, still under the MapKit umbrella.\nIn contrast to the old API where you have to implement a delegate just to add something to the map,\nthe new API is relatively modern.\nBut it's missing a few things.\nAnd the largest hole of them all is.... you guessed it! No overlays!</p>\n<p>To be fair to Apple, the new API is pretty new.\nAnd it probably seems like the use cases for raster overlays are fairly niche,\nbut I'm a bit confused why Apple dropped this functionality.\nSo if you're building a live weather radar app,\nyou need to use the AppKit and UIKit variant of MapKit.\nOr MapLibre.</p>\n<p>But let's say you actually <em>do</em> need to use MapKit for this purpose?\nThere are at least two good reasons you might want to:</p>\n<ul>\n<li><strong>One less dependency</strong>: if you absolutely can't afford a (very few) extra megabytes, MapKit makes sense since it's bundled with the OS.</li>\n<li><strong>Broad device support</strong>: it's probably possible to build maps with another framework on niche platforms like watchOS and visionOS, but MapKit just works ™.</li>\n</ul>\n<h1><a href=\"#diving-into-mktileoverlay\" aria-hidden=\"true\" class=\"anchor\" id=\"diving-into-mktileoverlay\"></a>Diving into <code>MKTileOverlay</code></h1>\n<p>Ok, let's take a look at the APIs here.\nThe first one we'll look at is <a href=\"https://developer.apple.com/documentation/mapkit/mktileoverlay\"><code>MKTileOverlay</code></a>.\nThis is a pretty straightforward class that describes a tile-based data source.\n(If you've used maps for a while, you may occasionally notice when the map fills in like a mosaic;\ninternally it's made up of these &quot;tiles&quot; that are stitched together client-side.)\nIt has properties describing the valid zoom range\nand a few ways of specifying where to get the tiles.</p>\n<p>The constructor takes a <code>urlTemplate</code> string argument.\nThe template looks like this: <code>https://tiles.stadiamaps.com/tiles/stamen_terrain/{z}/{x}/{y}.png?api_key=YOUR-API-KEY</code>.\nThis is supposed to &quot;just work.&quot;\nBut if you need something a bit more advanced,\nyou can implement <code>url(forTilePath:)</code> instead to return a URL.</p>\n<p>This sounds like an &quot;oh, that's nice&quot; sort of API,\nbut it's surprisingly useful.\nFor example, they support a <code>{scale}</code> placeholder,\nbut there is no <code>maximumScale</code> parameter in the public interface.\nFor reasons that should be obvious, few if any tile servers\nare investing in rendering out PNG tiles at triple the normal resolution,\nso <code>@2x</code> is the max that most will support.</p>\n<p>If you implement <code>url(forTilePath:)</code>, the constructor parameter is ignored.\nA rather clunky method of encapsulating things,\nbut I'll give the Apple engineers some slack,\nas this dates back to the era when object-oriented programming reigned supreme\nand we hadn't rediscovered the joy of protocols yet.</p>\n<p>Finally, if you want even <em>more</em> control,\nyou have <code>loadTile(at:result:)</code>, which asynchronously loads the tile data.\nThis gives you ultimate freedom in how you make your network request,\nif you make a network request at all, how you cache tiles, etc.\nWe'll revisit this in a bit.</p>\n<h1><a href=\"#adding-an-overlay-to-the-map\" aria-hidden=\"true\" class=\"anchor\" id=\"adding-an-overlay-to-the-map\"></a>Adding an overlay to the map</h1>\n<p>Adding an overlay to the map is not quite as straightforward as <code>mapView.addOverlay(overlay)</code>.\nThe design of <code>MKMapView</code> is quite flexible... so much so that you <em>have</em> to implement\n<code>mapView(_:rendererFor:)</code> on your delegate, or else no overlays will render!\nEnter <a href=\"https://developer.apple.com/documentation/mapkit/mktileoverlayrenderer\"><code>MKTileOverlayRenderer</code></a>.\nThe map view itself doesn't know what to do with the overlay.\nIt <em>requires</em> an overlay renderer to do that, and <code>MKTileOverlayRenderer</code> is built for tile overlays like this.</p>\n<p>A simple and &quot;obvious&quot; way to bring everything together looks something like this:</p>\n<pre><code class=\"language-swift\">import UIKit\nimport MapKit\n\nlet stadiaApiKey = &quot;YOUR-API-KEY&quot;  // TODO: Get one at client.stadiamaps.com\nclass ViewController: UIViewController {\n\n    @IBOutlet var mapView: MKMapView!\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        let overlay = MKTileOverlay(urlTemplate: &quot;https://tiles.stadiamaps.com/tiles/stamen_terrain/{z}/{x}/{y}.png?api_key=\\(stadiaApiKey)&quot;)\n        mapView.addOverlay(overlay)\n    }\n\n}\n\nextension ViewController: MKMapViewDelegate {\n    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -&gt; MKOverlayRenderer {\n        if let tileOverlay = overlay as? MKTileOverlay {\n            return MKTileOverlayRenderer(overlay: tileOverlay)\n        } else {\n            return MKOverlayRenderer(overlay: overlay)\n        }\n    }\n}\n</code></pre>\n<p>Not too bad, aside from the large amount of boilerplate (also ignoring for the moment that the images aren't scale optimized).\nThere's just one problem... <a href=\"https://stackoverflow.com/questions/79286875/mapkit-flashing-screen-each-time-a-zoom-level-is-changed-with-custom-map-tiles-w\">the UX is <em>awful</em></a>!</p>\n<blockquote>\n<p>[!bug] <code>addOverlay</code> docs bug</p>\n<p>At the time of this writing, the <a href=\"https://developer.apple.com/documentation/mapkit/mkmapview/addoverlay(_:)\">docs for <code>addOverlay</code></a>\nstate that &quot;The map view adds the specified object to the group of overlay objects in the <code>MKOverlayLevel.aboveLabels</code> level.&quot;\nThis is not what actually happens.\nSo the above code is theoretically correct, but will be even more surprisingly broken in practice.\nTo fix this bug, write <code>mapView.addOverlay(overlay, level: .aboveLabels)</code>.</p>\n</blockquote>\n<h1><a href=\"#fixing-the-flash\" aria-hidden=\"true\" class=\"anchor\" id=\"fixing-the-flash\"></a>Fixing the Flash</h1>\n<p>I initially thought the flashing behavior was a result of a poor cache implementation.\nSo the first thing I did was to write my own <code>MKTileOverlay</code> subclass.\nI knew I wanted to provide my own <code>loadTile(at:result:)</code> implementation anyways (to load the max <em>available</em> image scale).</p>\n<p>After digging into the cache behavior though (and replacing it with my own instance of <code>URLCache</code> which I could inspect),\nI realized the problem was <em>not</em> the cache responsiveness.</p>\n<p><code>MKTileRenderer</code> was the next suspect.\nRegrettably, this is a completely closed source library,\nso there's no way to know for sure what's going on under the hood,\nbut I was able to reverse engineer a few things.</p>\n<p>First, the problem is <em>related to</em> the way <code>MKTileOverlayRenderer</code>\nimplements <code>canDraw(_:zoomScale:)</code> and <code>draw(_:zoomScale:in:)</code>.\nThe class seems to indicate that it can't draw anything when the data is not available at this exact zoom level.\nThis is rather annoying,\nsince the entire map will disappear and then rapidly fill in every time you cross over a zoom boundary!</p>\n<p>Which means we have to go even deeper.\nTime to implement our own overlay renderer!</p>\n<p>The overall approach I settled on for the first (and fortunately only!) pass\nwas to leave <code>canDraw(_:zoomScale:)</code> unimplemented, so it will always try to draw something.\nFor the draw method, my goal was to put cache hits directly in the hot path,\nand kicking off async requests in case something wasn't in the cache.</p>\n<p>Here's most of the code:</p>\n<pre><code class=\"language-swift\">/// A generic protocol for MapKit tile overlays which implement their own queryable cache.\n///\n/// This is useful for making overlays more responsive, and allowing for fallback tiles\n/// to be fetched by the renderer while waiting for the higher zoom tiles to load over the network.\n/// While technically not required, it's easiest to just subclass `MKTileOverlay`.\npublic protocol CachingTileOverlay: MKOverlay {\n    /// Fetches a tile from the cache, if present.\n    ///\n    /// This method should retorn as quickly as possible.\n    func cachedData(at path: MKTileOverlayPath) -&gt; Data?\n    func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, (any Error)?) -&gt; Void)\n\n    var tileSize: CGSize { get }\n}\n\npublic class CachingTileOverlayRenderer: MKOverlayRenderer {\n    private var loadingTiles = AtomicSet&lt;String&gt;()\n\n    public init(overlay: any CachingTileOverlay) {\n        super.init(overlay: overlay)\n    }\n\n    public override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {\n        // Shift the type; our constructor ensures we can't get this wrong by accident though.\n        guard let tileOverlay = overlay as? CachingTileOverlay else {\n            fatalError(&quot;The overlay must implement MKCachingTileOverlay&quot;)\n        }\n\n        // (Snipped) Calculate the range of tiles the mapRect intersects\n\n        // Loop over the tiles that intersect mapRect...\n        for x in firstCol...lastCol {\n            for y in firstRow...lastRow {\n                // Create the tile overlay path\n                let tilePath = MKTileOverlayPath(x: x, y: y, z: currentZoom, contentScaleFactor: self.contentScaleFactor)\n\n                if let image = cachedTileImage(for: tilePath) {\n                    // (Snipped) Compute tile rect\n                    let drawRect = self.rect(for: tileRect)\n                    // If we have a cached image for this tile, just draw it!\n                    drawImage(image, in: drawRect, context: context)\n                } else {\n                    // Miss; load the tile\n                    loadTileIfNeeded(for: tilePath, in: tileRect)\n                }\n            }\n        }\n    }\n\n    func cachedTileImage(for path: MKTileOverlayPath) -&gt; ImageType? {\n        guard let overlay = self.overlay as? CachingTileOverlay else { return nil }\n        if let data = overlay.cachedData(at: path) {\n            return ImageType(data: data)\n        }\n        return nil\n    }\n\n    func loadTileIfNeeded(for path: MKTileOverlayPath, in tileMapRect: MKMapRect) {\n        guard let overlay = self.overlay as? CachingTileOverlay else { return }\n\n        // Create a unique key for the tile (MKTileOverlayPath is not hashable)\n        // and use this to avoid duplicate requests.\n        let tileKey = &quot;\\(path.z)/\\(path.x)/\\(path.y)@\\(path.contentScaleFactor)&quot;\n        guard !loadingTiles.contains(tileKey) else { return }\n\n        loadingTiles.insert(tileKey)\n\n        overlay.loadTile(at: path) { [weak self] data, error in\n            guard let self = self else { return }\n            self.loadingTiles.remove(tileKey)\n\n            // When the tile has loaded, schedule a redraw of the tile region.\n            DispatchQueue.main.async {\n                self.setNeedsDisplay(tileMapRect)\n            }\n        }\n    }\n}\n</code></pre>\n<p>It worked!\nWell, almost...</p>\n<p><figure><img src=\"media/mapkit-flipped-tiles.png\" alt=\"A map with elements upside down and badly stitched\" /></figure></p>\n<p>That was my first attempt at grabbing the <code>cgImage</code> property of a <code>UIImage</code>\nand slapping it onto the context.\nThe <code>drawImage</code> function ended up being rather annoying for both UIKit and AppKit.</p>\n<pre><code class=\"language-swift\">// At the top of your file\n#if canImport(UIKit)\ntypealias ImageType = UIImage\n#elseif canImport(AppKit)\ntypealias ImageType = NSImage\n#endif\n\n// Later, inside the overlay renderer...\n\nfunc drawImage(_ image: ImageType, in rect: CGRect, context: CGContext) {\n#if canImport(UIKit)\n    UIGraphicsPushContext(context)\n\n    image.draw(in: rect)\n\n    UIGraphicsPopContext()\n#elseif canImport(AppKit)\n    let graphicsContext = NSGraphicsContext(cgContext: context, flipped: true)\n\n    NSGraphicsContext.saveGraphicsState()\n    NSGraphicsContext.current = graphicsContext\n    image.draw(in: rect)\n    NSGraphicsContext.restoreGraphicsState()\n#endif\n}\n</code></pre>\n<p>Not very pretty (especially with the conditional compilation to support macOS), but it gets the job done.</p>\n<p>One more wart to acknowledge...this approach requires a bit of faith in <code>URLCache</code> being responsive.\nBut it got rid of the flicker, and I think that's the bigger win.</p>\n<blockquote>\n<p>[!question] What about the snipped code?!</p>\n<p>Yeah, I skipped over a bunch of math to calculate some rectangles.\nIt wasn't very interesting, and this is a LONG post.\nYou can find the <a href=\"https://github.com/stadiamaps/mapkit-caching-tile-overlay/blob/main/Sources/CachingMapKitTileOverlay/CachingTileOverlayRenderer.swift\">full code on GitHub</a>.</p>\n</blockquote>\n<h1><a href=\"#implementing-cachingtileoverlay\" aria-hidden=\"true\" class=\"anchor\" id=\"implementing-cachingtileoverlay\"></a>Implementing <code>CachingTileOverlay</code></h1>\n<p>After some minimal testing, it became clear that the default caching behavior\nwas not going to cut it.\nWe can't see the source code, but it looks like internally,\nApple either uses <code>URLSession.shared</code> or sets up a new session with caching behavior similar to <code>URLCache.shared</code>.\nThis, somewhat understandably, doesn't do much if any disk caching.\nBut map users expect data to be cached for snappy app relaunches!</p>\n<p>I ended up setting up a cache like this:</p>\n<pre><code class=\"language-swift\">let cache = URLCache(memoryCapacity: 25 * 1024 * 1024, diskCapacity: 100 * 1024 * 1024)\n</code></pre>\n<p>It's not entirely clear when exactly the memory contents are flushed to disk, but this is a big improvemnet already.\nDon't forget to configure your <code>URLSession</code> to use the new cache!\nI set up the session as in instance variable that's configured during <code>init</code>.</p>\n<pre><code class=\"language-swift\">self.urlSession = URLSession(configuration: configuration)\n</code></pre>\n<p>Next, we need to actually create the request and load it in <code>loadTile(at:result:)</code>.</p>\n<pre><code class=\"language-swift\">public override func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, (any Error)?) -&gt; Void) {\n    let url = self.url(forTilePath: path)\n    let request = URLRequest(url: url, cachePolicy: cachePolicy)\n\n    if let response = cache.cachedResponse(for: request) {\n        result(response.data, nil)\n        return\n    }\n\n    urlSession.dataTask(with: request) { data, _, error in\n        result(data, error)\n    }.resume()\n}\n</code></pre>\n<p>Nothing super special here.\nBut it's worth noting I also made <code>cachePolicy</code> an instance variable for extra configuability.\nAnd that's pretty much all the interesting bits in the overlay.</p>\n<div class=\"markdown-alert markdown-alert-tip\">\n<p class=\"markdown-alert-title\">`canReplaceMapContent`</p>\n<p>If you're implementing a basemap layer with this approach, make sure you set <code>canReplaceMapContent</code>!\nThis lets <code>MapKit</code> skip drawing all layers underneath yours.\nDon't do this if you're just adding a transparent overlay on top.</p>\n</div>\n<p>For a full implementation of an overlay,\ncheck out <a href=\"https://github.com/stadiamaps/mapkit-layers\">this one for Stadia Maps raster layers</a>.</p>\n<h1><a href=\"#going-for-gold-overzooming\" aria-hidden=\"true\" class=\"anchor\" id=\"going-for-gold-overzooming\"></a>Going for Gold: Overzooming</h1>\n<p>With the zoom transition &quot;flicker&quot; solved for cases where the tile was already in the cache,\nI noticed there was another problem with <code>MKTileOverlayRenderer</code>.\nIt refuses to show tiles from anything but the current zoom level.\nThis causes a jarring effect when zooming in, as the map is erased and slowly redrawn\nas new (non-cached) tiles are loaded.</p>\n<p><code>MapKit</code> is the first framework I can recall seeing with this behavior.\nOther frameworks will just &quot;overzoom&quot; the existing tiles.\nI was able to overcome this, but admittedly it required quite a lot of hackery.\nThe first thing we need to change is our drawing method.\nIt needs a fallback case.</p>\n<pre><code class=\"language-swift\">if let image = cachedTileImage(for: tilePath) {\n    // If we have a cached image for this tile, just draw it!\n    drawImage(image, in: drawRect, context: context)\n} else if let fallbackImage = fallbackTileImage(for: tilePath) {\n    // If we have a fallback image, draw that instead to start.\n    drawImage(fallbackImage, in: drawRect, context: context)\n\n    // Then, load the tile from the cache (if necessary)\n    loadTileIfNeeded(for: tilePath, in: tileRect)\n} else {\n    // Total cache miss; load the tile\n    loadTileIfNeeded(for: tilePath, in: tileRect)\n}\n</code></pre>\n<p>Nothing too surprising here.\nWe try to load a fallback image, and THEN kick off the tile fetch.\nThe majority of the logic lives in <code>fallbackTileImage(for:)</code>:</p>\n<pre><code class=\"language-swift\">/// Attempts to get a fallback tile image from a lower zoom level.\n///\n/// The idea is to try successively lower zoom levels until we find a tile we have cached,\n/// then use it until the real tile loads.\nfunc fallbackTileImage(for path: MKTileOverlayPath) -&gt; ImageType? {\n    var fallbackPath = path\n    var d = 0\n    while fallbackPath.z &gt; 0 &amp;&amp; d &lt; 2 {  // We'll go up to 2 levels higher\n        d += 1\n        fallbackPath = fallbackPath.parent\n\n        if let image = cachedTileImage(for: fallbackPath) {\n            let srcRect = cropRect(d: d, originalPath: path, imageSize: image.size)\n\n            return image.cropped(to: srcRect)\n        }\n    }\n    return nil\n}\n</code></pre>\n<p>This code looks for cached tiles up to 2 levels &quot;higher up.&quot;\nIf it finds one, it returns that image to be temporarily rendered as a stand-in.\nThis method in turn relies on two more methods:\nan extension on <code>MKTileOverlayPath</code> to get the parent tile,\nand a <code>cropRect</code> function which returns a sub-rectangle of the fallback image\nwhich we want to display.</p>\n<p>In digital maps, the map is subdivided into tiles.\nAt zoom level 0, the whole world is a single tile.\nEvery time you zoom in a level, each tile is subdivided into 4.\nThis property lets us use a previously loaded image from a lower zoom level as a stand-in.</p>\n<p>This took <strong>way</strong> too much trial and error to get right.\nFirst, we need to do some math to calculate which section of the cached image we should crop to and &quot;overzoom.&quot;\nThen we need to actually crop the image, which is easier said than done.\nNeither <code>UIImage</code> nor <code>NSImage</code> provide a cropping API directly,\nso we need to drop down to <code>CoreGraphics</code>.</p>\n<p>To make matters worse, AppKit and UIKit somewhat infamously use different coordinate systems,\nwith the origins in different spots.\nSo our cropping functions OR our rect calculation need to be aware of the difference.</p>\n<p>This code is not particularly interesting to be honest,\nbut here are links to the files on GitHub:</p>\n<ul>\n<li><a href=\"https://github.com/stadiamaps/mapkit-caching-tile-overlay/blob/main/Sources/CachingMapKitTileOverlay/CachingTileOverlayRenderer.swift\">Cropping rectangle</a> (search for <code>cropRect</code>)</li>\n<li><a href=\"https://github.com/stadiamaps/mapkit-caching-tile-overlay/blob/main/Sources/CachingMapKitTileOverlay/Cropping.swift\">Image extension</a></li>\n</ul>\n<blockquote>\n<p>[!question] What about zooming out?</p>\n<p>I currently don't apply the same tricks when zooming out.\nAs you zeem out, MapKit still clears tiles rather than showing smaller versions of what it has already.\nThis is a trickier problem since, while each child has exactly one parent tile,\nwhen you go in reverse, the task is to load 4 tiles and stitch them together.\nPRs welcome if anyone wants to take a swing!</p>\n</blockquote>\n<h1><a href=\"#conclusion\" aria-hidden=\"true\" class=\"anchor\" id=\"conclusion\"></a>Conclusion</h1>\n<p>Mapkit is full of surprises.\nWhile it works pretty well out of the box with a vanilla map style from Apple on a fast network,\nsomething as simple as adding raster overlays can be devilishly complicated.\nHere's to hoping that Apple eventually publishes the source code for MapKit.\nI would happily sobmit some PRs to improve it,\nincluding adding support for overlays in the SwiftUI API!</p>\n<p>In the meantime, I've published a Swift package\nwith the caching overlay and renderer outlined in this post.\n<a href=\"https://github.com/stadiamaps/mapkit-caching-tile-overlay/tree/main\">Check it out on GitHub</a>.</p>\n",
      "summary": "",
      "date_published": "2025-02-11T00:00:00-00:00",
      "image": "media/mapkit-flipped-tiles.png",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "ios",
        "swift",
        "apple",
        "maps"
      ],
      "language": "en"
    },
    {
      "id": "https://ianwwagner.com//kwdc-24.html",
      "url": "https://ianwwagner.com//kwdc-24.html",
      "title": "KWDC 24",
      "content_html": "<p>Yesterday I had the pleasure of attending KWDC 24,\nan Apple developer conference modeled after WWDC,\nbut for the Korean market.\nRegrettably, I only heard about it a few days prior\nthrough a friend at the <a href=\"https://www.meetup.com/seoul-ios-meetup/\">Seoul iOS Meetup</a>,\nso I wasn’t able to give a talk.</p>\n<h1><a href=\"#overall-impressions\" aria-hidden=\"true\" class=\"anchor\" id=\"overall-impressions\"></a>Overall impressions</h1>\n<p>The iOS meetup typically has 20-30 attendees.\nBut wow... the turnout at KWDC far exceeded my expectations.\nNearly 600 attendees showed up,\nand this is only its second year (I also didn’t hear about it last year;\nclearly I live under a rock)!\nThe staff were well organized and friendly,\nand the international participation was significantly better than I had expected.</p>\n<h1><a href=\"#the-challenge-of-multi-lingual-events\" aria-hidden=\"true\" class=\"anchor\" id=\"the-challenge-of-multi-lingual-events\"></a>The challenge of multi-lingual events</h1>\n<p>Surprisingly, most of the (30+) event staff also spoke English very well (that’s a first for a Korean conference that I’ve been to).\nI think the organizers did an excellent job in not only attracting an international audience\n(<a href=\"https://nerdyak.tech/\">one speaker</a> flew in from Czechia!),\nbut also making them feel welcome.\nHats off to the organizing team for that!\nThis makes me really happy, and I hope this is a big step in raising the level of conferences here.</p>\n<p>Not only did they have a mix of English and Korean talks,\nthey handled live translation much better than any other event I’ve seen.\nThey used a service called <a href=\"https://flitto.com\">Flitto</a>.\nApparently it’s a Korean company,\nand they were using some AI models to do the heavy lifting.\nIt had some hiccups to be sure,\nbut it did a surprisingly good job!\nThe main complaint I heard was that it would wait for half or 2/3 of a screenful of content, causing jumps that were hard to read.</p>\n<p>The speakers I talked with said they had to provide a script in advance,\nwhich we speculate were used to improve the quality of the translations\n(which were still live, even when the presenter went “off script”).\nThe model still failed to recognize technical terms on occasion,\nbut the hiccups were to be expected.\nOverall, the quality of the translation was excellent,\nand I think this will be the future of such events!\nI’ve been at half a dozen events where they give you a radio receiver\nand an earpiece, and it is never a great experience.\nEveryone I talked with said they liked the\ntext on screen approach was better too\n(especially since it was alongside the slides,\nwhich were honestly Apple quality at every single talk I saw!).</p>\n<p><figure><img src=\"media/IMG_8818.jpeg\" alt=\"View of the stage during a talk about Swift 6, showing the screen with live translation next to the slides\" /></figure></p>\n<h1><a href=\"#favorite-talks\" aria-hidden=\"true\" class=\"anchor\" id=\"favorite-talks\"></a>Favorite talks</h1>\n<p>My favorite talk was Pavel’s on “The Magic of SwiftUI Animations.”\nHe even walked up to the podium in a wizard robe 🧙\nI was blown away by the amount of effort that he put into the slides,\nand got a bunch of things to follow up on (like this <a href=\"https://m.youtube.com/watch?v=f4s1h2YETNY\">video on shaders</a>).\nTalking with him after, he said it was the culmination of around 4 years of effort.</p>\n<p><figure><img src=\"media/IMG_8820.jpeg\" alt=\"Pavel Zak on stage\" /></figure></p>\n<p><a href=\"https://x.com/riana_soumi\">Riana’s</a> talk on Swift Testing\ngot me fired up to switch.\nI wanted to shout with excitement when I heard that Swift <em>finally</em>\nsupports parameterized tests in a native testing framework!\nAnd it’s <a href=\"https://github.com/swiftlang/swift-testing\">open source</a>,\nso I hope it will be improve faster than XCTest.\nWho knows; maybe it’ll even get property testing (like QuickCheck, Hypothesis, etc.)!</p>\n<p>The third talk that stuck out to me was <a href=\"https://www.rudrank.com/\">Rudrank’s</a>\ntalk on widgets.\nHe was a GREAT presenter, with a number of Korean expressions woven throughout,\nwhich the audience loved.\nI also liked how he cleverly wove the Rime of the Ancient Mariner throughout\nthe talk (the title was “Widgets, Widgets Everywhere, and not a Pixel to Spare”).\nMy biggest learning was in the weird differences in the mental model for updates:\nit’s all about the timeline!</p>\n<h1><a href=\"#networking\" aria-hidden=\"true\" class=\"anchor\" id=\"networking\"></a>Networking</h1>\n<p>Networking at Korean conferences is typically a bit slow to be honest,\nas it is not normal in Korean culture to walk up to someone\nand start a conversation without much context.\nThis event also happened to be exceptional in a good way!</p>\n<p>The first big networking opportunity was lunch.\nBut there wasn't a very clear announcement of how lunch would work.\nEveryone was on their own, and the info was rather buried in some PDFs (which I didn’t get somehow)\nand Discord (which I had a hard time navigating).\nTogether with a Danish friend I met at the iOS meetup a few days prior,\nI suggested we wing it and just follow the crowd outside to see where we ended up,\nsince they were clearly better informed than us 🤣</p>\n<p>We ended up taking a few turns following a group in front of us,\nand eventually I asked if they would be cool with us crashing their party.\nWe ended up at a crowded Donkatsu buffet a few minutes later.\nThe two at our table were iOS engineers, one working at Hyundai AutoEver,\nand another at <a href=\"https://www.bucketplace.com/en/\">오늘의집</a>,\nand we had a great conversation over lunch!\n(They even bought us coffee after; so friendly!)</p>\n<p>One of them mentioned that we should check out the networking area in-between sessions, which I did later.\nIt was a bit hard to find, since it was in a narrow hall,\nafter you passed through a cafe on another floor.\nI think this could have been announced a bit better,\nsince not many people used it, but the conversations I had there were great!</p>\n<p>Aside: another cool thing that I haven’t seen done elsewhere is round-table Q&amp;A.\nAlong with the session times, each speaker was available for Q&amp;A around (literally)\nround tables near the networking zone.\nVery cool idea!</p>\n<p>The networking area had one room dedicated to local communities as well,\nincluding the Seoul iOS Meetup,\nthe Korean <a href=\"https://github.com/Swift-Coding-Club\">Swift Coding Club</a>,\nand the AWS Korea User group.\nThe Swift Coding Club in particular was a super cool group.\nSeveral were students,\nand one was working on some apps related to EV charging.\nThis naturally lead to a conversation about the geocoding,\nmaps, and navigation SDKs I’ve been working on at <a href=\"https://docs.stadiamaps.com/sdks/overview/\">Stadia Maps</a>.\nIt was a good time!</p>\n<p>Finally, there wasn’t a big after-party or anything,\nbut there was a small event at a bar organized for the speakers and sponsors.\nI didn’t speak, but they were fine letting me tag along.\nI ended up talking for well over an hour with Riana about everything from Swift\nto world cultures to under-representation of women in tech.\nAnd she 100% sold me on attending <a href=\"https://tryswift.jp/_en\"><code>try! Swift</code></a>\nin Tokyo next year.\nAnd Mark from <a href=\"https://www.revenuecat.com/\">RevenueCat</a>,\nwho I also met at the iOS meetup prior,\ntaught me a bunch of things I didn’t know about the history of MacRuby\n(turns out he built the first BaseCamp app using RubyMotion back in the day!).</p>\n<p>I ended up getting home at 1:30am for the second time this week.\nBut it was worth it!</p>\n",
      "summary": "",
      "date_published": "2024-10-26T00:00:00-00:00",
      "image": "media/IMG_8818.jpeg",
      "authors": [
        {
          "name": "Ian Wagner",
          "url": "https://fosstodon.org/@ianthetechie",
          "avatar": "media/avi.jpeg"
        }
      ],
      "tags": [
        "conferences",
        "AI",
        "translation",
        "apple",
        "swift"
      ],
      "language": "en"
    }
  ]
}