• Embedding Godot games in iOS apps is easy now

    May 31, 2025

    The Swift and Godot logos side by side over top a blurred image of Hyrule Field from Ocarina of Time

    Recently there’s been very exciting developments in the Godot game engine, that have allowed really easy and powerful integration into an existing normal iOS or Mac app. I couldn’t find a lot of documentation or discussion about this, so I wanted to shine some light on why this is so cool, and how easy it is to do!

    What’s Godot?

    For the uninitiated, Godot is an engine for building games, other common ones you might know of are Unity and Unreal Engine.

    It’s risen in popularity a lot over the last couple years due to its open nature: it’s completely open source, MIT licensed, and worked on in the open. But beyond that, it’s also a really well made tool for building games with (both 2D and 3D), with a great UI, beautiful iconography, a ton of tutorials and resources, and as a bonus, it’s very lightweight.

    I’ve had a lot of fun playing around with it (considering potentially integrating it into Pixel Pals), and while Unity and Unreal Engine are also phenomenal tools, Godot has felt lightweight and approachable in a really nice way. As an analogy, Godot feels closer to Sketch and Figma whereas Unity and Unreal feel more like Photoshop/Illustrator or other kinda bulky Adobe products.

    Even Apple has taken interest in it, contributing a substantial pull request for visionOS support in Godot.

    Why use it with iOS?

    You’ve always been able to build a game in Godot and export it to run on iOS, but recently thanks to advancements in the engine and work by amazing folks like Miguel de Icaza, you can now embed a Godot game in an existing normal SwiftUI or UIKit app just as you would an extra UITextView or ScrollView.

    Why is this important? Say you want to build a game or experience, but you don’t want it to feel just like another port, you want it to integrate nicely with iOS and feel at home there through use of some native frameworks and UI here and there to anchor the experience (share sheets, local notifications, a simple SwiftUI sheet for adding a friend, etc.). Historically your options have been very limited or difficult.

    You no longer have to have “a Godot game” or “an iOS app”, you can have the best of both worlds. A fun game built entirely in Godot, while having your share sheets, Settings screens, your paywall, home screen widgets, onboarding, iCloud sync, etc. all in native Swift code. Dynamically choosing which tool you want for the job.

    (Again, this was technically possible before and with other engines, but was much, much more complicated. Unity’s in particular seems to have been last updated during the first Obama administration.)

    And truly, this doesn’t only benefit “game apps”. Heck, if the user is doing something that will take awhile to complete (uploading a video, etc.) you could give them a small game to play in the interim. Or just for some fun you could embed a little side scroller easter egg in one of your Settings screens to delight spelunking users. Be creative!

    SpriteKit?

    A quick aside. It wouldn’t be an article about game dev on iOS without mentioning SpriteKit, Apple’s native 2D game framework (Apple also has SceneKit for 3D).

    SpriteKit is well done, and actually what I built most of Pixel Pals in. But it has a lot of downsides versus a proper, dedicated game engine:

    • Godot has a wealth of tutorials on YouTube and elsewhere, bustling Discord communities for help, where SpriteKit being a lot more niche can be quite hard to find details on
    • The obvious one: SpriteKit only works on Apple platforms, so if you want to port your game to Android or Windows you’re probably not going to have a great time, where Godot is fully cross platform
    • Godot being a full out game engine has a lot more tools for game development than can be handy, from animation tools, to sprite sheet editors, controls that make experimenting a lot easier, handy tools for creating shaders, and so much more than I could hope to go over in this article. If you ever watch a YouTube video of someone building a game in a full engine, the wealth of tools they have for speeding up development is bonkers.
    • Godot is updated frequently by a large team of employees and volunteers, SpriteKit conversely isn’t exactly one of Apple’s most loved frameworks (I don’t think it’s been mentioned at WWDC in years) and kinda feels like something Apple ins’t interested in putting much more work into. Maybe that’s because it does everything Apple wants and is considered “finished” (if so I think that would be incorrect, see previous point for many things that it would be helpful for SpriteKit to have), but if you were to encounter a weird bug I’d feel much better about the likelihood of it getting fixed in Godot than SpriteKit

    I’m a big fan of using the right tool for the job. For iOS apps, most of the time that’s building something incredible in SwiftUI and UIKit. But for building a game, be it small or large, using something purpose built to be incredible at that seems like the play to me, and Godot feels like a great candidate there.

    Setup

    Simply add the SwiftGodotKit package to your Xcode project by selecting your project in the sidebar, ensuring your project is selected in the new sidebar, selecting the Package Dependencies tab, click the +, then paste the GitHub link.

    After adding it, you will also need to select the target that you added it to in the sidebar, select the Build Settings tab, then select “Other Linker Flags” and add -lc++.

    Lastly, with that same target, under the General tab add MetalFX.framework to Frameworks, Libraries, and Embedded Content. (Yeah you got me, I don’t know why we have to do that.)

    After that, you should be able to import SwiftGodotKit.

    Usage

    Now we’re ready to use Godot in our iOS app! What excites me most and I want to focus on is embedding an existing Godot game in your iOS app and communicating back and forth with it from your iOS app.

    This way, you can do the majority of the game development in Godot without even opening Xcode, and then sprinkle in delightful iOS integration by communicating between iOS and Godot where needed.

    Screenshot of the Godot editor with the Cassette Beats game visible inside

    To start, we’ll build a very simple game called IceCreamParlor, where we select from a list of ice cream options in SwiftUI, which then gets passed into Godot. Godot will have a button the user can tap to send a message back to SwiftUI with the total amount of ice cream. This will not be an impressive “game” by any stretch of the imagination, but should be easy to set up and understand the concepts so you can apply it to an actual game.

    To accomplish our communication, in essence we’ll be recreating iOS’ NotificationCenter to send messages back and forth between Godot and iOS, and like NotificationCenter, we’ll create a simple singleton to accomplish this.

    Those messages will be sent via Signals. This is Godot’s system for, well, signaling an event occurred, and can be used to signify everything from a button press, to a player taking damage, to a timer ending. Keeping with the NotificationCenter analogy, this would the be Notification that gets posted (except in Godot, it’s used for everything, where in iOS land you really wouldn’t use NotificationCenter for a button press.)

    And similar to Notification that has a userInfo field to provide more information about the notification, Godot signals can also take an argument that provides more information. (For example if the notification was “player took damage” the argument might be an integer that includes how much damage they took.) Like userInfo, this is optional however and you can also fire off a signal with no further information, something like “userUnlockedPro” for when they activate Pro after your SwiftUI paywall.

    For our simple example, we’re going to send a “selectedIceCream” signal from iOS to Godot, and a “updatedIceCreamCount” signal from Godot to iOS. The former will have a string argument for which ice cream was selected, and the latter will have an integer argument with the updated count.

    Setting up our Godot project

    Open Godot.app (available to download from their website) and create a new project, I’ll type in IceCreamParlor, choose the Mobile renderer, then click Create.

    Godot defaults to a 3D scene, so I’ll switch to 2D at the top, and then in the left sidebar click 2D Scene to create that as our root node. I’ll right-click the sidebar to add a child node, and select Label. We’ll set the text to the “Ice cream:”. In the right sidebar, we’ll go to Theme Overrides and increase the font size to 80 to make it more visible, and we’ll also rename it in the left sidebar from Label to IceCreamLabel.

    We’ll also do the same to add a Button to the scene, which we’ll call UpdateButton and sets its text to “Update Ice Cream Count”.

    The Godot canvas with the aforementioned label and button added

    If you click the Play button in the top right corner of Godot, it will run and you can click the button, but as of now it doesn’t do anything.

    We’ll select our root node (Node2D) in the sidebar, right click, and select “Attach Script”. Leave everything as default, and click Create. This will now present us with an area where we can actually code in GDScript, and we can refer to the objects in our scene by prefixing their name with a dollar sign.

    Inside our script, we’ll implement the _ready function, which is essentially Godot’s equivalent of viewDidLoad, and inside we’ll connect to our simple signal we discussed earlier. We’ll do this by grabbing a reference to our singleton, then reference the signal we want, then connect to it by passing a function we want to be called when the signal is received. And of course the function takes a String as a parameter because our signal includes what ice cream was selected.

    extends Node2D
    
    var ice_cream: Array[String] = []
    
    func _ready() -> void:
        var singleton = Engine.get_singleton("GodotSwiftMessenger")
        singleton.ice_cream_selected.connect(_on_ice_cream_selected_signal_received)
    
    func _on_ice_cream_selected_signal_received(new_ice_cream: String) -> void:
        # We received a signal! Probably should do something…
        pass
    

    Note that we haven’t actually created the singleton yet, but we will shortly. Also note that normally in Godot, you have to declare custom signals like the ones we’re using, but we’re going to declare them in Swift. As long as they’re declared somewhere, Godot is happy!

    Let’s also hook up our button by going back to our scene, selecting our button in the canvas, selecting the “Node” tab in the right sidebar, and double-clicking the pressed() option. We can then select that same Node2D script and name the function _on_update_button_pressed to add a function that executes when the button is pressed (fun fact: the button being pressed event is also powered by signals).

    func _on_update_button_pressed() -> void:
        pass
    

    Setting up our iOS/Swift project

    Let’s jump over to Xcode and create a new SwiftUI project there as well, also calling it IceCreamParlor. We’ll start by adding the Swift package for SwiftGodotKit to Swift Package Manager, and as mentioned above we’ll add -lc++ to our “Other Linker Flags” under “Build Settings”, add MetalFX, then go to ContentView.swift and add import SwiftGodotKit at the top.

    Adding SwiftGodotKit to Xcode via the Swift Package Manager

    From here, let’s create a simple SwiftUI view so we can choose from some ice cream options.

    var body: some View {
        HStack {
            Button {
            } label: {
                Text("Chocolate")
            }
            
            Button {
            } label: {
                Text("Strawberry")
            }
            
            Button {
            } label: {
                Text("Vanilla")
            }
        }
        .buttonStyle(.bordered)
    }
    

    We’ll also create a new file in Xcode called GodotSwiftMessenger.swift. This will be where we implement our singleton that is akin to NotificationCenter.

    import SwiftGodot
    
    @Godot
    class GodotSwiftMessenger: Object {
        public static let shared = GodotSwiftMessenger()
        @Signal var iceCreamSelected: SignalWithArguments<String>
        @Signal var iceCreamCountUpdated: SignalWithArguments<Int>
    }
    

    We first import SwiftGodot (minus the Kit), essentially because this part is purely about interfacing with Godot through Godot, and doesn’t care about whether or not it’s embedded in an iOS app. For more details on SwiftGodot see its section below.

    Then, we annotate our class with the @Godot Swift Macro, which basically just says “Hey make Godot aware that this class exists”. The class is a subclass of Object as everything in Godot needs to inherit from Object, it’s essentially the parent class of everything.

    Following that is your bog standard Swift singleton initialization.

    Then, with another Swift Macro, we annotate a variable we want to be our signal which signifies that it’s a Signal to Godot. You can either specify its type as Signal or SignalWithArguments<T> depending on whether or not the specific signal also sends any data alongside it. We’ll use that “somethingHappened” signal we mentioned early, which includes a string for more details on what happened.

    Note that we used “ice_cream_selected” in Godot but “iceCreamSelected” in Swift, this is because the underscore convention is used in Godot, and SwiftGodotKit will automatically map the camelCase Swift convention to it.

    Now we need to tell Godot about this singleton we just made. We want Godot to know about it as soon as possible, otherwise if things aren’t hooked up, Godot might emit a signal that we wouldn’t receive in Swift, or vice-versa.

    So, we’ll hook it up very early in our app cycle. In SwiftUI, you might do this in the init of your main App struct as I’ll show below, and in UIKit in applicationDidFinishLaunching.

    @main
    struct IceCreamParlor: App {
        init() {
            initHookCb = { level in
                guard level == .scene else { return }
    
                register(type: GodotSwiftMessenger.self)
                Engine.registerSingleton(name: "GodotSwiftMessenger", instance: GodotSwiftMessenger.shared)
            }
        }
        
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    

    In addition to the boilerplate code Xcode gives us, we’ve added an extra step to the initializer, where we set a callback on initHookCb. This is just a callback that fires as Godot is setup, and it specifies what level of setup has occurred. We want to wait until the level setup is reached, which means the game is ready to go (you could set it up at an even earlier level if you see that as beneficial).

    Then, we just tell Godot about this type by calling register, and then we register the singleton itself with a name we want it to be accessible under.

    Again, we want to do this early, as if Godot was already setup in our app, and then we set initHookCb, its contents would never fire and thus we wouldn’t register anything. But don’t worry, this hook won’t fire until we first initialize our Godot game in iOS ourself, so as long as this code is called before then, we’re golden.

    Lastly, everything is registered in iOS land, but there’s still nothing that emits or receives signals. Let’s change that by going to ContentView.swift, and change our body to the following:

    import SwiftUI
    import SwiftGodotKit
    import SwiftGodot
    
    struct ContentView: View {
        @State var totalIceCream = 0
        @State var godotApp: GodotApp = GodotApp(packFile: "main.pck")
        
        var body: some View {
            VStack {
                GodotAppView()
                    .environment(\.godotApp, godotApp)
                
                Text("Total Ice Cream: \(totalIceCream)")
                
                HStack {
                    Button {
                        GodotSwiftMessenger.shared.iceCreamSelected.emit("chocolate")
                    } label: {
                        Text("Chocolate")
                    }
                    
                    Button {
                        GodotSwiftMessenger.shared.iceCreamSelected.emit("strawberry")
                    } label: {
                        Text("Strawberry")
                    }
                    
                    Button {
                        GodotSwiftMessenger.shared.iceCreamSelected.emit("vanilla")
                    } label: {
                        Text("Vanilla")
                    }
                }
                .buttonStyle(.bordered)
            }
            .onAppear {
                GodotSwiftMessenger.shared.iceCreamCountUpdated.connect { newTotalIceCream in
                    totalIceCream = newTotalIceCream
                }
            }
        }
    }
    

    There’s quite a bit going on here, but let’s break it down because it’s really quite simple.

    We have two new state variables, the first is to keep track of the new ice cream count. Could we just do this ourselves purely in SwiftUI? Totally, but for fun we’re going to be totally relying on Godot to keep us updated there, and we’ll just reflect that in SwiftUI to show the communication. Secondly and more importantly, we need to declare a variable for our actual game file so we can embed it.

    We do this embedding at the top of the VStack by creating a GodotAppView, a handy SwiftUI view we can now leverage, and we do so by just setting its environment variable to the game we just declared.

    Then, we change our buttons to actually emit the selections via signals, and when the view appears, we make sure we connect to the signal that keeps us updated on the count so we can reflect that in the UI. Note that we don’t also connect to the iceCreamSelected signal, because we don’t care to receive it in SwiftUI, we’re just firing that one off for Godot to handle.

    Communicating

    Let’s update our gdscript in Godot to take advantage of these changes.

    func _on_ice_cream_selected_signal_received(new_ice_cream: String) -> void:
        ice_cream.append(new_ice_cream)
        $IceCreamLabel.text = "Ice creams: " + ", ".join(ice_cream)
    
    func _on_update_button_pressed() -> void:
        var singleton = Engine.get_singleton("GodotSwiftMessenger")
        singleton.ice_cream_count_updated.emit(ice_cream.size())
    

    Not too bad! We now receive the signal from SwiftUI and update our UI and internal state in Godot accordingly, as well as the UI by making our ice cream into a comma separated list. And then when the user taps the update button, we then send (emit) that signal back to SwiftUI with the updated count.

    Running

    To actually see this live, first make sure you have an actual iOS device plugged in. Unfortunately Godot doesn’t work with the iOS simulator.

    Secondly, in Godot, select the Project menu bar item, then Export, then click the Add button and select “iOS”. This will bring you to a screen with a bunch of options, but my understanding is that this is 99% geared toward if you’re building your app entirely in Godot (which we are not): you can plug in all the things you’d otherwise plug into Xcode here instead, and Godot will handle them for you. That doesn’t apply to us, we’re going to do all that normally in Xcode anyway, we just want the juicy game files, so ignore all that and select “Export PCK/ZIP…” at the bottom. It’ll ask you where you want to save it, and I just keep it in the Godot project directory, make sure “Godot Project Pack (*.pck)” is selected in the dropdown, and then save it as main.pck.

    Godot's export screen with iOS preselected

    That’s our “game” bundled up, as meager as it is!

    We’ll then drop that into Xcode, making sure to add it to our target, then we can run it on the device!

    Here we’ll see choosing the ice cream flavor at the bottom in SwiftUI beams it into the Godot game that’s just chilling like a SwiftUI view, and then we can tap the update button in Godot land to beam the new count right back to SwiftUI to be displayed.

    Screenshot of the simplistic game running on an iPhone with the Godot game on top, and the SwiftUI buttons at the bottom.
    Not exactly a AAA game but enough to show the basics of communication 😄

    Look at you go! Take this as a leaping off point for all the cool SwiftUI and Godot interoperability that you can accomplish, be it tappings a Settings icon in Godot to bring up a beautifully designed, native SwiftUI settings screen, or confirmation to you your game when the user updated to the Pro version of your game through your SwiftUI paywall.

    Bonus: SwiftGodot (minus the “Kit”)

    An additional fun option (that sits at the heart of SwiftGodotKit) is SwiftGodot, which allows you to actually build your entire Godot game with Swift as the programming language if you so choose. Swift for iOS apps, Swift on the server, Swift for game dev. Swift truly is everywhere.

    For me, I’m liking playing around in GDScript, which is Godot’s native programming language, but it’s a really cool option to know about.

    Embed size

    A fear might be that embedding Godot into your app might bloat the binary and result in an enormous app download size. Godot is very lightweight, adding it to your codebase adds a relatively meager (at least by 2025 standards) 30MB to your binary size. That’s a lot larger than SpriteKit’s 0MB, but for all the benefits Godot offers that’s a pretty compelling trade.

    (30MB was measured by handy blog sponsor, Emerge Tools.)

    Tips

    Logging

    If you log something in Godot/GDScript via print("something") that will also print to the Xcode console, handy!

    Quickly embedding the pck into iOS

    Exporting the pck file from Godot to Xcode is quite a few clicks, so if you’re doing it a lot it would be nice to speed that up. We can use the command line to make this a lot nicer.

    Godot.app also has a headless mode you can use by going inside the .app file, then Contents > MacOS > Godot. But typing the full path to that binary is no fun, so let’s symlink the binary to /usr/local/bin.

    sudo ln -s "/Applications/Godot.app/Contents/MacOS/Godot" /usr/local/bin/godot
    

    Now we can simply type godot anywhere in the Terminal to either open the Godot app, or we can use godot --headless for some command line goodness.

    My favorite way to do this, is to do something like the following within your Godot project directory:

    godot --headless --export-pack "iOS" /path/to/xcodeproject/target/main.pck
    

    This will handily export the pck and add it to our Xcode project, overwriting any existing pck file, from which point we can simply compile our iOS app.

    Wrapping it up

    I really think Godot’s new interoperability with iOS is an incredibly exciting avenue for building games on iOS, be it a full fledged game or a small little easter egg integrated into an existing iOS app, and hats off to all the folks who did the hard work getting it working.

    Hopefully this serves as an easy way to get things up and running! It might seem like a lot at first glance, but most of the code shown above is just boilerplate to get an example Godot and iOS project up and running, the actual work to embed a game and communicate across them is so delightfully simple!

    (Also big shout out to Chris Backas and Miguel de Icaza for help getting this tutorial off the ground.)


  • Curing Mac mini M4 fomo with 3D printing

    November 12, 2024

    A 3D printed Mac mini sitting on an oak desk with a Stitch plushy sitting next to it
    Spoiler: 3D printed! The colored ports really sell the effect

    If you’re anything like me, you’ve found the new, tinier Mac mini to be absolutely adorable. But you might also be like me that you either already have an awesome M1 Mac mini that you have no real reason to replace, or the new Mac mini just isn’t something you totally need.

    While that logic might be sound, but it doesn’t make you want one any less.

    To help cure this FOMO, I made a cute little 3D printable Mac mini that can sit on your desk and be all cute. But then I had an even better idea, the new Mac mini is powerful sure, but it can’t hold snacks. Or a plant. Or your phone. Or pens/pencils. So I also made some versions you can print that add some cute utility to your desk in the form of the new Mac mini. They’re free of course! Just chuck ’em into your (or your friend’s) 3D printer.

    My hand holding the 3D printed Mac mini showing its back

    It even has all the little details modeled, like the power button, ports (including rear), and fan holes!

    They’re pretty easy to print, it’s in separate parts for ease of printing the bottom a different color (black) versus the top, then just put a dab of glue (or just use gravity) to keep them together. If you have a multi-color 3D printer, you can color the ports and power LED to make it look extra cool (or just do it after the fact with paint).

    Here are the different options for your desk!

    Secret item stash

    A 3D printed Mac mini with a removable top that is holding red keyboard switches, with a screwdriver visible in the background

    The possibilities for what you can store on your desk are now truly endless. Individually wrapped mints? Key switches? Screws? Paper clips? Rubber bands? Flash drives?

    Download link: https://makerworld.com/en/models/793456

    A very green sorta Mac

    A 3D printed Mac mini plant pot with a Haworthia succulent inside

    First carbon neutral Mac is cool and all but what if your Mac mini literally had a plant in it?

    A close-up of a 3D printed Mac mini plant pot with a Haworthia succulent inside

    Every desk needs a cute little plant.

    Download link: https://makerworld.com/en/models/793464

    Phone holder

    A 3D printed Mac mini with a groove in the top surface to hold a phone upright

    A phone/tablet holder is an essential item on my desk for debugging things, watching a video, or just keeping an eye on an Uber Eats order. Before, guests came over and saw my boring phone stand and judged me, now they come over and think I’m exciting and well-traveled.

    You can even charge your phone/tablet in portrait mode by pushing the cable through a tunnel made through the Ethernet port that then snakes up to the surface.

    Download link: https://makerworld.com/en/models/793495

    Pen holder

    A 3D printed Mac mini plant pot with a Haworthia succulent inside

    The Playdate had the cutest little pen/pencil holder accessory but it unfortunately never shipped and my desk is sad. This will be a nice stand in for your beloved pens, pencils, markers, and Apple Pencils.

    Download link: https://makerworld.com/en/models/793470

    A solid model

    A 3D printed Mac mini plant pot with a Haworthia succulent inside

    Or if you just want to stare at it without any frills, you can just print the normal model too!

    Download link: https://makerworld.com/en/models/793447

    Printer recommendation

    Whenever I post about 3D printing I understandably get a bunch of “Which 3D printer should I buy??” questions. This isn’t sponsored (I do use affiliate links on this blog when available though), but I’ve found over the last few years the answer has been pretty easy: something from Bambu Lab. Their printers are somehow super easy to use, well designed, and reasonably priced. Prusas are great too, but I think Bambu is hard to beat for the price. Don’t get an Ender.

    So if you’re looking for a printer now, Black Friday deals are aplenty so it’s pretty much the best time to pick one up. I’d grab something in their A series if you’re on a budget, or the P1S for a bit more if you can swing it (that’s what I use).

    On the other hand if you just want to print one thing now and again, a lot of local libraries are starting to have 3D printers so that might be worth looking into! And online services exist too (eg: JLCPCB and PCBWay), but if you do it with any regularity a 3D printer is a really fun thing to pick up.

    Enjoy! ❤️

    Learning 3D modeling over the last year has been a ton of fun so I love a good excuse to practice, and shout out to Jerrod Hofferth and his amazing 3D printable Mac mini tower (that you should totally download) for the idea to solve my desire with some 3D printing! Also, the models are almost certainly not accurate down to the micrometer as I don’t actually have one, they’re based off Apple’s measurements as well as measuring screenshots. But it should be close!

    If you have a multi-color 3D printer, the linked models have the colors built-in for your ready to go, but if you want to print it in single-colors I also made versions available with the top and bottom separate as well as the logo, so you can print them separately in the individual colors then connect them with a touch of super glue or something.


  • Introducing Tiny Storage: a small, lightweight UserDefaults replacement

    October 8, 2024

    A cute, purple floppy disk style animated character with their hands in the air with 'Tiny Storage' to their right
    Hey I'm a developer not an artist

    Following my last blog post about difficulties surrounding UserDefaults and edge cases that lead to data loss (give it a read if you haven’t, it’s an important precursor to this post!), I wanted to build something small and lightweight that would serve to fix the issues I was encountering with UserDefaults and thus TinyStorage was born! It’s open source so you can use it in your projects too if would like.

    GitHub link 🐙

    Overview

    As mentioned in that blog post, UserDefaults has more and more issues as of late with returning nil data when the device is locked and iOS “prelaunches” your app, leaving me honestly sort of unable to trust what UserDefaults returns. Combined with an API that doesn’t really do a great job of surfacing whether it’s available, you can quite easily find yourself in a situation with difficult to track down bugs and data loss. This library seeks to address that fundamentally by not encrypting the backing file, allowing more reliable access to your saved data (if less secure, so don’t store sensitive data), with some niceties sprinkled on top.

    This means it’s great for preferences and collections of data like bird species the user likes, but not for sensitive details. Do not store passwords/keys/tokens/secrets/diary entries/grammy’s spaghetti recipe, anything that could be considered sensitive user information, as it’s not encrypted on the disk. But don’t use UserDefaults for sensitive details either as UserDefaults data is still fully decrypted when the device is locked so long as the user has unlocked the device once after reboot. Instead use Keychain for sensitive data.

    As with UserDefaults, TinyStorage is intended to be used with relatively small, non-sensitive values. Don’t store massive databases in TinyStorage as it’s not optimized for that, but it’s plenty fast for retrieving stored Codable types. As a point of reference I’d say keep it under 1 MB.

    This reliable storing of small, non-sensitive data (to me) is what UserDefaults was always intended to do well, so this library attempts to realize that vision. It’s pretty simple and just a few hundred lines, far from a marvel of filesystem engineering, but just a nice little utility hopefully!

    (Also to be clear, TinyStorage is not a wrapper for UserDefaults, it is a full replacement. It does not interface with the UserDefaults system in any way.)

    Features

    • Reliable access: even on first reboot or in application prewarming states, TinyStorage will read and write data properly
    • Read and write Swift Codable types easily with the API
    • Similar to UserDefaults uses an in-memory cache on top of the disk store to increase performance
    • Thread-safe through an internal DispatchQueue so you can safely read/write across threads without having to coordinate that yourself
    • Supports storing backing file in shared app container
    • Uses NSFileCoordinator for coordinating reading/writing to disk so can be used safely across multiple processes at the same time (main target and widget target, for instance)
    • When using across multiple processes, will automatically detect changes to file on disk and update accordingly
    • SwiftUI property wrapper for easy use in a SwiftUI hierarchy (Similar to @AppStorage)
    • Can subscribe to to TinyStorage.didChangeNotification in NotificationCenter, and includes the key that changed in userInfo
    • Uses OSLog for logging
    • A function to migrate your UserDefaults instance to TinyStorage

    Limitations

    Unlike UserDefaults, TinyStorage does not support mixed collections, so if you have a bunch of strings, dates, and integers all in the same array in UserDefaults without boxing them in a shared type, TinyStorage won’t work. Same situation with dictionaries, you can use them fine with TinyStorage but the key and value must both be a Codable type, so you can’t use [String: Any] for instance where each string key could hold a different type of value.

    Installation

    Simply add a Swift Package Manager dependency for https://github.com/christianselig/TinyStorage.git

    Usage

    First, either initialize an instance of TinyStorage or create a singleton and choose where you want the file on disk to live. To keep with UserDefaults convention I normally create a singleton for the app container:

    extension TinyStorage {
        static let appGroup: TinyStorage = {
            let appGroupID = "group.com.christianselig.example"
            let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)!
            return .init(insideDirectory: containerURL)
        }()
    }
    

    (You can store it wherever you see fit though, in URL.documentsDirectory is also an idea for instance!)

    Then, decide how you want to reference your keys, similar to UserDefaults you can use raw strings, but I recommend a more strongly-typed approach, where you simply conform a type to TinyStorageKey and return a var rawValue: String and then you can use it as a key for your storage without worrying about typos. If you’re using something like an enum, making it a String enum gives you this for free, so no extra work!

    After that you can simply read/write values in and out of your TinyStorge instance:

    enum AppStorageKeys: String, TinyStorageKey {
        case likesIceCream
        case pet
        case hasBeatFirstLevel
    }
    
    // Read
    let pet: Pet? = TinyStorage.appGroup.retrieve(type: Pet.self, forKey: AppStorageKeys.pet)
    
    // Write
    TinyStorage.appGroup.store(true, forKey: AppStorageKeys.likesIceCream)
    

    (If you have some really weird type or don’t want to conform to Codable, just convert the type to Data through whichever means you prefer and store that, as Data itself is Codable.)

    If you want to use it in SwiftUI and have your view automatically respond to changes for an item in your storage, you can use the @TinyStorageItem property wrapper. Simply specify your storage, the key for the item you want to access, and specify a default value.

    @TinyStorageItem(key: AppStorageKey.pet, storage: .appGroup)
    var pet: = Pet(name: "Boots", species: .fish, hasLegs: false)
    
    var body: some View {
        Text(pet.name)
    }
    

    You can even use Bindings to automatically read/write.

    @TinyStorageItem(key: AppStorageKeys.message, storage: .appGroup)
    var message: String = ""
    
    var body: some View {
        VStack {
            Text("Stored Value: \(message)")
            TextField("Message", text: $message)
        }
    }
    

    It also addresses some of the annoyances of @AppStorage, such as not being able to store collections:

    @TinyStorageItem(key: "names", storage: .appGroup)
    var names: [String] = []
    

    Or better support for optional values:

    @TinyStorageItem(key: "nickname", storage: .appGroup)
    var nickname: String? = nil // or "Cool Guy"
    

    You can also migrate from a UserDefaults instance to TinyStorage with a handy helper function:

    let keysToMigrate = ["favoriteIceCream", "appFontSize", "useCustomTheme", "lastFetchDate"]
    TinyStorage.appGroup.migrate(userDefaults: .standard, keys: keysToMigrate, overwriteIfConflict: true)
    

    (Read the migrate function documentation for more details.)

    If you want to migrate multiple keys manually or store a bunch of things at once, rather than a bunch of single store calls you can consolidate them into one call with bulkStore which will only write to disk the once:

    let item1 = TinyStorageBulkStoreItem(key: AppGroup.pet, value: pet)
    let item2 = TinyStorageBulkStoreItem(key: AppGroup.theme, value: "sunset")
    
    TinyStorage.appGroup.bulkStore(items: [item1, item2])
    

    Hope it’s handy!

    If you like it or have any feedback let me know! I’m going to start slowly integrating it into Pixel Pals and hopefully solve a few bugs in the process.


  • Beware UserDefaults: a tale of hard to find bugs, and lost data

    October 5, 2024

    An iPhone sitting on an open book showing its Lock Screen.

    Excuse the alarmist title, but I think it’s justified, as it’s an issue that’s caused me a ton of pain in both support emails and actually tracking it down, so I want to make others aware of it so they don’t similarly burned.

    Brief intro

    For the uninitiated, UserDefaults (née NSUserDefaults) is the de facto iOS standard for persisting non-sensitive, non-massive data to “disk” (AKA offline). In other words, are you storing some user preferences, maybe your user’s favorite ice cream flavors? UserDefaults is great, and used extensively from virtually every iOS app to Apple sample code. Large amount of data, or sensitive data? Look elsewhere! This is as opposed to just storing it in memory where if the user restarts the app all the data is wiped out.

    It’s a really handy tool with a ton of nice, built-in things for you:

    • No needing to mess with writing to files yourself, and better yet, no need to coordinate when to persist values back to the disk
    • Easy to share data between your app’s main target and secondary targets (like a widget target)
    • Automatic serialization and deserialization: just feed in a String, Date, Int, and UserDefaults handles turning it into bytes and back from bytes
    • Thread-safe!

    So it’s no wonder it’s used extensively. But yeah, keep the two limitations in mind that Apple hammers home:

    Okay, so what’s the problem

    Turns out, sometimes you can request your saved data back from UserDefaults and it… just won’t have it! That’s a pretty big issue for a system that’s supposed to reliably store data for you.

    This can amount to an even bigger issue that leads to permanent data loss.

    Imagine a situation where a user has been meticulously opening your app for 364 days in a row. On day 365, your app promised a cool reward! When the user last closed the app, you stored 364 to UserDefaults.

    The user wakes up on day 365, excited for their reward:

    1. App launches
    2. App queries UserDefaults for how many days in a row the user has opened the app
    3. App returns 0 (UserDefaults is mysteriously unavailable so its API returns the default integer value of 0)
    4. It’s a new day, so you increment that value by 1, so that 0 changes to 1
    5. Save that new value back to UserDefaults

    Now, instead of your user having a fun celebration, their data has been permanently overwritten and reset! They are having a Sad Day™.

    It basically means, if at any point you trust UserDefaults to accurately return your data (which you know, sounds like a fair assumption) you might just get incorrect data, which you then might make worse by overwriting good data with.

    And remember, you’re not meant to store sensitive data in UserDefaults, but even if it’s not sensitive data it might be valuable. The user’s day streak above is not sensitive data that would be bad if leaked online like a password, but it is valuable to that user. In fact I’d argue any data persisted to the disk is valuable, otherwise you wouldn’t be saving it. And you should be always be able to trust an API to reliably save your data.

    What??? How is this happening? 😵‍💫

    As I understand it, there’s basically two systems coming together (and working incorrectly, if you ask me) to cause this:

    1. Sensitive data encryption

    When using Keychain or files directly, as a developer you can mark data that should be encrypted until the device is unlocked by Face ID/Touch ID/passcode. This way if you’re storing a sensitive data like a token or password on the device, the contents are encrypted and thus unreadable until the device is unlocked.

    This meant if the device was still locked, and you, say, had a Lock Screen Widget that performed an API request, you would have to show placeholder data until the user unlocked the device, because the sensitive data, namely the user’s API token, was encrypted and unable to be used by the app to fetch and show data until the user unlocked the device. Not the end of the world, but something to keep in mind for secure data like API tokens, passwords, secrets, etc.

    2. Application prewarming

    Starting with iOS 15, iOS will sometimes wake up your application early so that when a user launches it down the road it launches even quicker for them, as iOS was able to do some of the heavy lifting early. This is called prewarming. Thankfully per Apple, your application doesn’t fully launch, it’s just some processes required to get your app working (spoiler: this is wrong, see below):

    Prewarming executes an app’s launch sequence up until, but not including, when main() calls UIApplicationMain(::::).

    Okay, so what happened with these two?

    It seems at some point, even though UserDefaults is intended for non-sensitive information, it started getting marked as data that needs to be encrypted and cannot be accessed until the user unlocked their device. I don’t know if it’s because Apple found developers were storing sensitive data in there even when they shouldn’t be, but the result is even if you just store something innocuous like what color scheme the user has set for your app, that theme cannot be accessed until the device is unlocked.

    Again, who cares? Users have to unlock the device before launching my app, right? I thought so too! It turns out, even though Apple’s prewarming documentation states otherwise, developers have been reporting for years that that’s just wrong, and your app can effectively be fully launched at any time, including before the device is even unlocked.

    Combining this with the previous UserDefaults change, you’re left with the above situation where the app is launched with crucial data just completely unavailable because the device is still locked.

    UserDefaults also doesn’t make this clear at all, which it could do by for instance returning nil when trying to access UserDefaults.standard if it’s unavailable. Instead, it just looks like everything is as it should be, except none of your saved keys are available anymore, which can make your app think it’s in a “first launch after install” situation.

    The whole point of UserDefaults is that it’s supposed to reliably store simple, non-sensitive data so it can be accessed whenever. The fact that this has now changed drastically, and at the same time your app can be launched effectively whenever, makes for an incredibly confusing, dangerous, and hard to debug situation.

    And it’s getting worse with Live Activities

    If you use Live Activities at all, the cool new API that puts activities in your Dynamic Island and Lock Screen, it seems if your app has an active Live Activity and the user reboots their device, virtually 100% of the time the above situation will occur where your app is launched in the background without UserDefaults being available to it. That means the next time your user actually launches the app, if at any point during your app launching you trusted the contents of UserDefaults, your app is likely in an incorrect state with incorrect data.

    This bit me badly, and I’ve had users email me over time that they’ve experienced data loss, and it’s been incredibly tricky to pinpoint why. It turns out it’s simply because the app started up, assuming UserDefaults would return good data, and when it transparently didn’t, it would ultimately overwrite their good data with the returned bad data.

    I’ve talked to a few other developers about this, and they’ve also reported random instances of users being logged out or losing data, and after further experimenting been able to now pinpoint that this is what caused their bug. It happened in past apps to me as well (namely users getting signed out of Apollo due to a key being missing), and I could never figure out why, but this was assuredly it.

    If you’ve ever scratched your head at a support email over a user’s app being randomly reset, hopefully this helps!

    I don’t like this ☹️

    I can’t overstate what a misstep I think this was. Security is always a balance with convenience. Face ID and Touch ID strike this perfectly; they’re both ostensibly less secure per Apple’s own admission than, say, a 20 digit long password, but users are much more likely to adopt biometric security so it’s a massive overall win.

    Changing UserDefaults in this way feels more on the side of “Your company’s sysadmin requiring you to change your password every week”: dubious security gains at the cost of user productivity and headaches.

    Further, it’s not as if UserDefaults is truly secure with this change. It’s only encrypted between the time the device is rebooted and first unlocked. So sure, if you shut down your phone and someone powers it back up, the name of your pet cow might be knowable to the attacker, but literally any time after you unlock the device after first boot, even if you re-lock it, Mr. Moo will be unencrypted.

    Update: After more investigating it seems like this change has been around for years and years, but the results of it have just has gotten more prevalent with things like prewarming and Live Activities.

    I mean no shade to the nice folks at Apple working on UserDefaults, I’m sure there’s a lot of legacy considerations to take into account that I cannot fathom that makes this a complex machine. I’m just sad that this lovely, simple API has some really sharp edges in 2024.

    But enough moaning, let’s fix it.

    Solution 1

    Because iOS is now seemingly encrypting UserDefaults, the easiest solution is to check UIApplication.isProtectedDataAvailable and if it returns false, subscribe to NotificationCenter for when protectedDataDidBecomeAvailableNotification is fired. This was previously really useful for knowing when Keychain or locked files were accessible once the device was unlocked, but it now seemingly applies to UserDefaults (despite not being mentioned anywhere in its documentation or UserDefault’s documentation 🙃).

    I don’t love this solution, because it effectively makes UserDefaults either an asynchronous API (“Is it available? No? Okay I’ll wait here until it is.”), or one where you can only trust its values sometimes, because unlike the Keychain API for instance, UserDefaults API itself does not expose any information about this when you try to access it when it’s in a locked state.

    Further, some developers have reported UserDefaults still being unavailable even once isProtectedDataAvailable returns true, presumably where (for one reason or another) UserDefaults isn’t reading the disk store back into memory even though the file became available.

    Further further, UIApplication.isProtectedDataAvailable is just not a great API for this use case. For one, it’s weirdly only available in a main app context, so if you’re executing code from a widget extension and want to check if protected data is available, you’ll have to resort to some other method, like trying to write to disk and read it back. And on top of this, isProtectedDataAvailable returns false when the device is locked, which will cause you to think UserDefaults is also unavailable. But remember, UserDefaults is only unavailable before the first lock upon reboot, any time after that it’s totally accessible, and this API will still return false in those cases as UserDefaults doesn’t follow the strict protection mechanism that that API tracks.

    Solution 2

    For the mentioned reasons, I don’t really like/trust Solution 1. I want a version of UserDefaults that acts like what it says on the tin: simply, quickly, and reliably retrieve persisted, non-sensitive values. This is easy enough to whip up ourselves, we just want to keep in mind some of the things UserDefaults handles nicely for us, namely thread-safety, shared between targets, and an easy API where it serializes data without us having to worry about writing to disk. Let’s quickly show how we might approach some of this.

    UserDefaults is fundamentally just a plist file stored on disk that is read into memory, so let’s create our own file, and instead of marking it as requiring encryption like iOS weirdly does, we’ll say that’s not required:

    // Example thing to save
    let favoriteIceCream = "chocolate"
    
    // Save to your app's shared container directory so it can be accessed by other targets outside main
    let appGroupID = ""
    
    // Get the URL for the shared container
    guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else {
        fatalError("App Groups not set up correctly")
    }
    
    // Create the file URL within the shared container
    let fileURL = containerURL.appendingPathComponent("Defaults")
        
    do {
        let data = favoriteIceCream.data(using: .utf8)
        try data.write(to: fileURL)
    
        // No encryption please I'm just storing the name of my digital cow Mister Moo
        try FileManager.default.setAttributes([.protectionKey: .none], ofItemAtPath: fileURL.path)
        print("File saved successfully at \(fileURL)")
    } catch {
        print("Error saving file: \(error.localizedDescription)")
    }
    

    (Note that you could theoretically modify the system UserDefaults file in the same way, but Apple documentation recommends against touching the UserDefaults file directly.)

    Next let’s make it thread safe by using a DispatchQueue.

    private static let dispatchQueue = DispatchQueue(label: "DefaultsQueue")
    
    func retrieveFavoriteIceCream() -> String? {
       return dispatchQueue.sync { 
          guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "app-group-id") else { return nil }
    
          let fileURL = containerURL.appendingPathComponent(fileName)
                
          do {
             let data = try Data(contentsOf: fileURL)
             return String(data: data, encoding: .utf8)
          } catch {
             print("Error retrieving file: \(error.localizedDescription)")
             return nil
          }
       }
    }
    
    func save(favoriteIceCream: String) {
       dispatchQueue.sync { 
          guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "app-group-id") else { return }
    
          let fileURL = containerURL.appendingPathComponent(fileName)
    
          do {
             let data = favoriteIceCream.data(using: .utf8)
             try data.write(to: fileURL)
             try FileManager.default.setAttributes([.protectionKey: .none], ofItemAtPath: fileURL.path)
             print("File saved successfully at \(fileURL)")
          } catch {
             print("Error saving file: \(error.localizedDescription)")
          }
       }
    }
    

    (You probably don’t need a concurrent queue for this, so I didn’t.)

    But with that we have to worry about data types, let’s just make it so long as the type conforms to Codable we can save or retrieve it:

    func saveCodable(_ codable: Codable, forKey key: String) {
        do {
            let data = try JSONEncoder().encode(codable)
            // Persist raw data bytes to a file like above
        } catch {
            print("Unable to encode \(codable): \(error)")
        }
    }
    
    func codable<T: Codable>(forKey key: String, as type: T.Type) -> T? {
        let data = // Fetch raw data from disk as done above
        
        do {
            return try JSONDecoder().decode(T.self, from: data)
        } catch {
            print("Error decoding \(T.self) for key \(key) with error: \(error)")
            return nil
        }
    }
    
    // Example usage:
    let newFavoriteIceCream = "strawberry"
    saveCodable(newFavoriteIceCream, forKey: "favorite-ice-cream")
    
    let savedFavoriteIceCream = codable(forKey: "favorite-ice-cream", as: String.self)
    

    Put those together, wrap it in a nice little library, and bam, you’ve got a UserDefaults replacement that acts as you would expect. In fact if you like the encryption option you can add it back pretty easily (don’t change the file protection attributes) and you could make it clear in the API when the data is inaccessible due to the device being locked, either by throwing an error, making your singleton nil, awaiting until the device is unlocked, etc.

    Solution 3

    Update! I wrote a little library to kinda wrap all of the above into a handy little tool! It’s called TinyStorage.

    End

    Maybe this is super obvious to you, but I’ve talked to enough developers where it wasn’t, that I hope in writing this it can save you the many, many hours I spent trying to figure out why once in a blue moon a user would be logged out, or their app state would look like it reset, or worst of all: they lost data.


  • Juno for YouTube has been removed from the App Store

    October 1, 2024

    Screenshot from App Store Connect showing Juno as 'Removed from App Store'

    For those not aware, a few months ago after reaching out to me, YouTube contacted the App Store stating that Juno does not adhere to YouTube guidelines and modifies the website in a way they don’t approve of, and alludes to their trademarks and iconography.

    I don’t personally agree with this, as Juno is just a web view, and acts as little more than a browser extension that modifies CSS to make the website and video player look more “visionOS” like. No logos are placed other than those already on the website, and the “for YouTube” suffix is permitted in their branding guidelines. Juno also doesn’t block ads in any capacity, for the curious.

    I stated as much to YouTube, they wouldn’t really clarify or budge any, and as a result of both parties not being able to come to a conclusion I received an email a few minutes ago from Apple that Juno has been removed from the App Store.

    Juno was a fun hobby project for me to build. As a developer I wanted to get some experience building for the Vision Pro, and as a user I wanted a nice way to watch YouTube on this cool new device. As a result I really enjoyed building Juno, but it was always something I saw as fundamentally an app I built for fun.

    Because of that, I have zero desire to spin this into a massive fight (at least more than I’ve fought in emails over the last few months) akin to what happened with Reddit years ago. That’s kind of the opposite of fun. I hope that’s understandable.

    For those who have Juno, to my knowledge it should continue to work fine until/unless the YouTube website updates in some fashion that breaks stuff. Sorry it had to end this way, I had some really cool stuff planned for it that I think would have been a lot of fun! It’s been genuinely awesome hearing all the kind words from Vision Pro users who have loved the app.

    🫡