Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
65
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. It even has all the little details modeled, like the power button, ports (including rear), and fan holes! They’re pretty easy to print,...
7 months ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from Christian Selig

A slept on upscaling tool for macOS

I uploaded YouTube videos from time to time, and a fun comment I often get is “Whoa, this is in 8K!”. Even better, I’ve had comments from the like, seven people with 8K TVs that the video looks awesome on their TV. And you guessed it, I don’t record my videos in 8K! I record them in 4K and upscale them to 8K after the fact. There’s no shortage of AI video upscaling tools today, but they’re of varying quality, and some are great but quite expensive. The legendary Finn Voorhees created a really cool too though, called fx-upscale, that smartly leverages Apple’s built-in MetalFX framework. For the unfamiliar, this library is an extensive of Apple’s Metal graphics library, and adds functionality similar to NVIDIA’s DLSS where it intelligently upscales video using machine learning (AI), so rather than just stretching an image, it uses a model to try to infer what the frame would look like at a higher resolution. It’s primarily geared toward video game use, but Finn’s library shows it does an excellent job for video too. I think this is a really killer utility, and use it for all my videos. I even have a license for Topaz Video AI, which arguably works better, but takes an order of magnitude longer. For instance my recent 38 minute, 4K video took about an hour to render to 8K via fx-upscale on my M1 Pro MacBook Pro, but would take over 24 hours with Topaz Video AI. # Install with homebrew brew install finnvoor/tools/fx-upscale # Outputs a file named my-video Upscaled.mov fx-upscale my-video.mov --width 7680 --codec h265 Anyway, just wanted to give a tip toward a really cool tool! Finn’s even got a [version in the Mac App Store called Unsqueeze](https://apps.apple.com/ca/app/unsqueeze/id6475134617 Unsqueeze) with an actual GUI that’s even easier to use, but I really like the command line version because you get a bit more control over the output. 8K is kinda overkill for most use cases, so to be clear you can go from like, 1080p to 4K as well if you’re so inclined. I just really like 8K for the future proofing of it all, in however many years when 8K TVs are more common I’ll be able to have some of my videos already able to take advantage of that. And it takes long enough to upscale that I’d be surprised to see TVs or YouTube offering that upscaling natively in a way that looks as good given the amount of compute required currently. Obviously very zoomed in to show the difference easier If you ask me, for indie creators, even when 8K displays are more common, the future of recording still probably won’t be in native 8K. 4K recording gives so much detail still that have more than enough details to allow AI to do a compelling upscale to 8K. I think for my next camera I’m going to aim for recording in 6K (so I can still reframe in post), and then continue to output the final result in 4K to be AI upscaled. I’m coming for you, Lumix S1ii.

a week ago 16 votes
Embedding Godot games in iOS is easy

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 presidency.) 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. 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”. 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, add -lc++ to our “Other Linker Flags” under “Build Settings”, add MetalFX, then go to ContentView.swift and add import SwiftGodotKit at the top. 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% if you’re building your app entirely in Godot, 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 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. 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. 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.)

a month ago 25 votes
Introducing Tiny Storage: a small, lightweight UserDefaults replacement

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) 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" 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.

9 months ago 95 votes
Beware UserDefaults: a tale of hard to find bugs, and lost data

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: Don’t store sensitive data in UserDefaults, that’s what Keychain is for Don’t store large amounts of data in UserDefaults, use something like Core Data or Swift Data 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: App launches App queries UserDefaults for how many days in a row the user has opened the app App returns 0 (UserDefaults is mysteriously unavailable so its API returns the default integer value of 0) It’s a new day, so you increment that value by 1, so that 0 changes to 1 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: 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. 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. 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) { return 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 locked, etc. 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.

9 months ago 76 votes

More in technology

This Beyblade battlebot is like a whirling dervish of destruction

Battlebots are fascinating, because their builders always manage to come up with unique new ways of destroying opponents while avoiding damage at the same time. Like any good sport, it involves metagaming to develop a strategy based on expected opponent strategies. Ari’s strategy was to make his combat robot spin at insane speeds and that […] The post This Beyblade battlebot is like a whirling dervish of destruction appeared first on Arduino Blog.

5 hours ago 2 votes
The History of Acer

A Shy Kid Builds the Taiwanese Tech Industry

3 days ago 7 votes
Concept Bytes’ coffee table tracks people and walks itself across a room when called

The term “mmWave” refers to radio waves with wavelengths on the millimeter scale. When it comes to wireless communications technology, like 5G, mmWave allows for very fast data transfer — though that comes at the expense of range. But mmWave technology also has some very useful sensing and scanning applications, which you may have experienced […] The post Concept Bytes’ coffee table tracks people and walks itself across a room when called appeared first on Arduino Blog.

3 days ago 4 votes
2025-07-06 secret cellular phone numbers

A long time ago I wrote about secret government telephone numbers, and before that, secret military telephone buttons. I suppose this is becoming a series. To be clear, the "secret" here is a joke, but more charitably I could say that it refers to obscurity rather than any real effort to keep them secret. Actually, today's examples really make this point: they're specifically intended to be well known, but are still pretty obscure in practice. If you've been around for a while, you know how much I love telephone numbers. Here in North America, we have a system called the North American Numbering Plan (NANP) that has rigidly standardized telephone dialing practices since the middle of the 20th century. The US, Canada, and a number of Central American countries benefit from a very orderly system of area codes (more formally numbering plan areas or NPAs) followed by a subscriber number written in the format NXX-XXXX (this is a largely NANP-centric notation for describing phone number patterns, N represents the digits 2-9 and X any digit). All of these NANP numbers reside under the country code 1, allowing at least theoretically seamless international dialing within the NANP community. It's really a pretty elegant system. NANP is the way it is for many reasons, but it mostly reflects technical requirements of the telephone exchanges of the 1940s. This is more thoroughly explained in the link above, but one of the goals of NANP is to ensure that step-by-step (SxS) exchanges can process phone numbers digit by digit as they are dialed. In other words, it needs to be possible to navigate the decision tree of telephone routing using only the digits dialed so far. Readers with a computer science education might have some tidy way to describe this in terms of Chompsky or something, but I do not have a computer science education; I have an Information Technology education. That means I prefer flow charts to automata, and we can visualize a basic SxS exchange as a big tree. When you pick up your phone, you start at the root of the tree, and each digit dialed chooses the edge to follow. Eventually you get to a leaf that is hopefully someone's telephone, but at no point in the process does any node benefit from the context of digits you dial before, after, or how many total digits you dial. This creates all kinds of practical constraints, and is the reason, for example, that we tend to write ten-digit phone numbers with a "1" before them. That requirement was in some ways long-lived (The last SxS exchange on the public telephone network was retired in 1999), and in other ways not so long lived... "common control" telephone exchanges, which did store the entire number in electromechanical memory before making a routing decision, were already in use by the time the NANP scheme was adopted. They just weren't universal, and a common nationwide numbering scheme had to be designed to accommodate the lowest common denominator. This discussion so far is all applicable to the land-line telephone. There is a whole telephone network that is, these days, almost completely separate but interconnected: cellular phones. Early cellular phones (where "early" extends into CDMA and early GSM deployments) were much more closely attached to the "POTS" (Plain Old Telephone System). AT&T and Verizon both operated traditional telephone exchanges, for example 5ESS, that routed calls to and from their customers. These telephone exchanges have become increasingly irrelevant to mobile telephony, and you won't find a T-Mobile ESS or DMS anywhere. All US cellular carriers have adopted the GSM technology stack, and GSM has its own definition of the switching element that can be, and often is, fulfilled by an AWS EC2 instance running RHEL 8. Calls between cell phones today, even between different carriers, are often connected completely over IP and never touch a traditional telephone exchange. The point is that not only is telephone number parsing less constrained on today's telephone network, in the case of cellular phones, it is outright required to be more flexible. GSM also defines the properties of phone numbers, and it is a very loose definition. Keep in mind that GSM is deeply European, and was built from the start to accommodate the wide variety of dialing practices found in Europe. This manifests in ways big and small; one of the notable small ways is that the European emergency number 112 works just as well as 911 on US cell phones because GSM dictates special handling for emergency numbers and dictates that 112 is one of those numbers. In fact, the definition of an "emergency call" on modern GSM networks is requesting a SIP URI of "urn:service:sos". This reveals that dialed number handling on cellular networks is fundamentally different. When you dial a number on your cellular phone, the phone collects the entire number and then applies a series of rules to determine what to do, often leading to a GSM call setup process where the entire number, along with various flags, is sent to the network. This is all software-defined. In the immortal words of our present predicament, "everything's computer." The bottom line is that, within certain regulatory boundaries and requirements set by GSM, cellular carriers can do pretty much whatever they want with phone numbers. Obviously numbers need to be NANP-compliant to be carried by the POTS, but many modern cellular calls aren't carried by the POTS, they are completed entirely within cellular carrier systems through their own interconnection agreements. This freedom allows all kinds of things like "HD voice" (cellular calls connected without the narrow filtering and companding used by the traditional network), and a lot of flexibility in dialing. Most people already know about some weird cellular phone numbers. For example, you can dial *#06# to display your phone's various serial numbers. This is an example of a GSM MMI (man-machine interface) code, phone numbers that are handled entirely within your device but nonetheless defined as dialable numbers by GSM for compatibility with even the most basic flip phones. GSM also defined numbers called USSD for unstructured supplementary service data, which set up connections to the network that can be used in any arbitrary way the network pleases. Older prepaid phone services used to implement balance check and top-up operations using USSD numbers, and they're also often used in ways similar to Vertical Service Codes (VSCs) on the landline network to control carrier features. USSDs also enabled the first forms of mobile data, which involved a "special telephone call" to a USSD in order to download a cut-down form of ESPN in a weird mobile-specific markup language. Now, put yourself in the shoes of an enterprising cellular network. The flexibility of processing phone numbers as you please opens up all kinds of possibilities. Innovative services! Customer convenience! Sell them for money! Oh my god, sell them for money! It seems like this started with customer service. It is an old practice, dating to the Bell operating companies, to have special short phone numbers to reach the telephone company itself. The details varied by company (often based on technical constraints in their switching system), but a common early setup was that dialing 114 got you the repair service operator to report a problem with your phone line. These numbers were usually listed in the front of the phone book, and for the phone company the fact that they were "special" or nonstandard was sort of a feature, since they could ensure that they were always routed within the same switch. The selection of "911" as the US emergency number seems rooted in this practice, as later on several major telcos used the "N11" numbers for their service lines. This became immortalized in the form of 611, which will get you customer service for most phone carriers. So cellular companies did the same, allocating themselves "special" numbers for various service lines. Verizon offers #PMT to make a payment. Naturally, there's also room for upsell services: #ROAD for roadside assistance on Verizon. The odd thing about these phone numbers is that there's really no standard involved, they're just the arbitrary practices of specific cellular companies. The term "mobile dial code" (MDC) is usually used to refer to them, although that term seems to have arisen organically rather than by intent. Remember, these aren't a real thing! The carriers just make them up, all on their own. The only real constraint on MDCs is that they need to not collide with any POTS number, which is most easily achieved by prefixing them with some combination of * and #, and usually not "*#" because it's referenced by the GSM standard for MMI. MDCs are available for purchase, but the terms don't seem to be public and you have to negotiate separately with each carrier. That's because there is no centralization. This is where MDCs stand in clear contrast to the better known SMS Short Code, or SMSSC. Those are the five or six-digit numbers widely used in advertising campaigns. SMSSCs are centrally managed by the SMS Short Code Registry, which is a function of industry association CTIA but contracted to iConectiv. iConectiv is sort of like the SAIC of the communications industry, a huge company that dates back to the Bell System (where it became Bellcore after divestiture) and that no one has heard of but nonetheless is a critically important part of the telephone system. Providers that want to have an SMSSC (typically on behalf of one of their customers) pay a fee, and usually recoup it from the end user. That fee is not cheap, typical end-user rates for an SMSSC run over $10k a year. But at least it's straightforward, and your SMS A2P or marketing company can make it happen for you. MDCs have no such centralization, no standardized registration process. You negotiate with each carrier individually. That means it's pretty difficult to put together "complete coverage" on an MDC by getting the same one assigned by every major carrier. And this is one of those areas where "good enough" is seldom good enough; people get pissed off when something you advertise doesn't work. Putting a phone number that only works for some people on a billboard can quickly turn into an expensive embarrassment, so companies will be wary of using an MDC in marketing if they don't feel really confident that it works for the vast majority of cellphone users. Because of this fragmentation, adoption of MDCs for marketing purposes has been very low. The only going concern I know of is #250, operated by a company called Mobile Direct Response. The premise of #250 is very simple: users call 250 and are greeted by a simple IVR. They say a keyword, and they're either forwarded to the phone number of the business that paid for the keyword or they receive a text message response with more information. #250 is specifically oriented towards radio advertising, where asking people to remember a ten-digit phone number is, well, asking a lot. It's also made the jump to podcast advertising. #250 is priced in a very radio-centric way, by the keyword and the size of the market area in which the advertisement that gives the keyword is played. 250 was founded by Dave Robinett, who used to work on marketing at Sprint, presumably where he became aware that these MDCs were a possibility. He has negotiated for #250 to work across a substantial list of cellular carriers in the US and Canada, providing almost complete coverage. That wasn't easy, Robinett said in an interview that it took five years to get AT&T, T-Mobile, Verizon, and Sprint on board. 250 does not appear to be especially widely used. For one, the website is a little junky, with some broken links and other indications that it is not backed by a large communications department. Dave Robinett may be the entire company. They've been operating since at least 2017, and I've only ever heard it in an ad once---a podcast ad that ended with "Call #250 and say I need a dentist." One thing you quickly notice when you look into telephone marketing is that dentists are apparently about 80% of the market. He does mention success with shows like "Rush, Hannity, and Levin," so it's safe to say that my radio habits are a little different from Robinett's. That's not to say that #250 is a failure. In the same interview Robinett says that the company pays his mortgage and, well, that ain't too bad. But it's also nothing like the widespread adoption of SMSSCs. One wonders if the limitation of MDCs to one company that is so focused on radio marketing limits their potential. It might really open things up if some company created a registration service, and prenegotiated terms with carriers so that companies could pick up their own MDCs to use as they please. Well, yeah, someone's trying. Around 2006, a recently-founded mobile marketing company called Zoove announced StarStar dialing. I'm a little unclear on Zoove's history. It seems that they were originally founded as Teleractive in Rhode Island as an SMS short code keyword response service, and after an infusion of VC cash moved to Palo Alto and started looking for something bigger. In 2016, they were acquired by a call center technology company called Mindful. Or maybe Zoove sold the StarStar business to Mindful? Stick a pin in that. I don't love the name StarStar, which has shades of Spacestar Ordering. But it refers to their chosen MDC prefix, two stars. Well, that point is a little odd, according to their marketing material you can also get numbers with a # prefix or * prefix, but all of the examples use **. I would say that, in general, StarStar has it a little less together than #250. Their website is kind of broken, it only loads intermittently and some of the images are missing. At one point it uses the term "CADC" to describe these numbers but I can't find that expanded anywhere. Plus the "About" page refers repeatedly to Virtual Hold Technologies, which renamed to VHT in 2018 and Mindful 2022. It really feels like the vestigial website of a dead company. I know about StarStar because, for a time, trucks from moving franchise All My Sons prominently bore the number MOVE on the side. Indeed, this is still one of the headline examples on the StarStar website, but it doesn't work. I just get a loud click and then the call ends. And it's not that StarStar doesn't work with my mobile carrier, because StarStar's own number MOBILE does connect to their IVR. That IVR promises that a representative will speak with me shortly, plays about five seconds of hold music, and then dumps me on a voicemail system. Despite StarStar numbers apparently basically working, I'm finding that most of the examples they give on their website won't even connect. Perhaps results will vary depending on the mobile network. Well, perhaps not that much is lost. StarStar was founded by Steve Doumar, a serial telephone marketing entrepreneur with a colorful past founding various inbound call center companies. Perhaps his most famous venture is R360, a "lead acquisition" service memorialized by headlines like "Drug treatment referral service took advantage of addictions to make a quick buck" from the Federal Trade Commission. He's one of those guys whose bio involves founding a new company every two years, which he has to spin as entrepreneurial dynamism rather than some combination of fleeing dissatisfied investors and fleeing angered regulators. Today he runs whisp.io, a "customer activation platform" that appears to be a glorified SMS advertising service featuring something ominously called "simplified opt-in." Whisp has a YouTube channel which features the 48-second gem "Fun Fact We Absolutely Love About Steve Doumar". Description: Our very own CEO, Steve Doumar is a kind and generous person who has given back to the community in many ways; this man is absolutely a man with a heart of gold. Do you want to know the fun fact? Yes you do! Here it is: "He is an incredible philanthropist. He loves helping other people. Every time I'm with him he comes up with new ways and new ideas to help other people. Which I think is amazing. And he doesn't brag about it, he doesn't talk about it a lot." Except he's got his CMO making a YouTube video about it? From Steve Doumar's blog: American entrepreneur Ray Kroc expressed the importance of persisting in a busy world where everyone wants a bite of success. This man is no exception. An entrepreneur. A family man. A visionary. These are the many names of a man that has made it possible for opt-ins to be safe, secure, and accurate; Steve Doumar. I love this stuff, you just can't make it up. I'm pretty sure what's going on here is just an SEO effort to outrank the FTC releases and other articles about the R360 case when you search for his name. It's only partially working, "FTC Hits R360 and its Owner With $3.8 Million Civil ..." still comes in at Google result #4 for "Steve Doumar," at least for me. But hey, #4 is better than #1. Well, to be fair to StarStar, I don't think Steve Doumar has been involved for some years, but also to be fair, some of their current situation clearly dates to past behavior that is maybe less than savory. Zoove originally styled itself as "The National StarStar Registry," clearly trying to draw parallels to CTIA/iConectiv's SMSSC registry. Their largest customer was evidently a company called Sumotext, which leased a number of StarStar numbers to offer an SMS and telephone marketing service. In 2016, Sumotext sued StarStar, Zoove, VHT (now Mindful), and a healthy list of other entities all involved in StarStar including the intriguingly named StarSteve LLC. I'm not alone in finding the corporate history a little baffling; in a footnote on one ruling the court expressed confusion about all the different names and opted to call them all Zoove. In any case, Sumotext alleged that Zoove, StarSteve, and VHT all merged as part of a scheme to illegally monopolize the StarStar market by undercutting the companies that had been leasing the numbers and effectively giving VHT (Mindful) an exclusive ability to offer marketing services with StarStar numbers. The case didn't end up going anywhere for Sumotext, the jury found that Sumotext hadn't established a relevant market which is a key part of a Sherman act case. An appeal was made all the way to the Supreme Court, but they didn't take it up. What the case did do was publicize some pretty sketchy sounding details, like the seemingly uncontested accusation that VHT got Sumotext's customer list from the registry database and used it to convert them all into StarSteve customers. And yes, the Steve in StarSteve is Steve Doumar. As best I can tell, the story here is that Steve Doumar founded Zoove (or bought Teleractive and renamed it or something?) to establish the National StarStar Registry, then founded a marketing company called StarSteve that resold StarStar numbers, then merged StarSteve and the National StarStar Registry together and cut off all of the other resellers. Apparently not a Sherman act violation but it sure is a bad look, and I wonder how much it contributed to the lack of adoption of the whole StarStar idea---especially given that Sumotext seems to have been responsible for most of that adoption, including the All My Sons deal for MOVE. I wonder if All My Sons had to take MOVE off of their trucks because of the whole StarSteve maneuver? That seems to be what happened. Look, ten-digit phone numbers are had to remember, that much is true. But as is, the "MDC" industry doesn't seem stable enough for advertising applications where the number needs to continue to work into the future. I think the #250 service is probably here to stay, but confined to the niche of audio advertising. StarStar raised at least $30 million in capital in the 2010s, but seems to have shot itself in the foot. StarStar owner VHT/Mindful, now acquired by Medallia, doesn't even mention StarStar as a product offering. Hey, remember how Steve Doumar is such a great philanthropist? There are a lot of vestiges around of StarStar Inc., a nonprofit that made StarStar numbers available to charitable organizations. Their website, starstar.org, is now a Wix error page. You can find old articles about StarStar Me, also written **me, which sounds lewd but was a $3/mo offering that allowed customers to get a vanity short code (such as ** followed by their name)---the original form of StarStar, dating back to 2012 and the beginning of Zoove. In a press release announcing the StarStar Me, Zoove CEO Joe Gillespie said: With two-thirds of smartphone users having downloaded social networking apps to their phones, there’s a rapidly growing trend in today's on-the-go lifestyle to extend our personal communications and identity into the digital realm via our mobile phones. And somehow this leads to paying $3 for to get StarStarred? I love it! It's so meaningless! And years later it would be StarStar Mobile formerly Zoove by VHT now known as Mindful a Medallia company. Truly an inspiring story of industry, and just one little corner of the vast tapestry of phone numbers.

4 days ago 12 votes
July 4th Weekend Sale

Hurry in to save 40%!

6 days ago 9 votes