iOS: Fixing a Crash on Launch Issue on Apple Silicon Macs

My iOS app Mango Baby is made available for Apple Silicon Macs. Ever since macOS Sonoma, it would crash on launch.

A similar crash on launch issue happened for the release of macOS Ventura. It was because the ActivityKit is not available on macOS. I fixed it by protecting calls to the ActivityKit APIs against isiOSAppOnMac at runtime. But apparently a new issue has appeared for macOS Sonoma.

Attempt 1

First I asked on Mastodon, and Matt Massicotte very kindly provided tips and walked me through the debugging process.

TIP: For crashes on launch, Apple-captured crash logs are much more helpful than Crashlytics. They can be accessed in Xcode following these instructions.

Through the crash logs, I discovered the following stacktrace:

Termination Reason: Namespace DYLD, Code 1 Library missing
Library not loaded: /System/Library/Frameworks/ActivityKit.framework/ActivityKit
Referenced from: <9B5A3687-FF24-3E38-9007-1DE6A69EFEEB> /Volumes/VOLUME/*/Baby.app/PlugIns/BabyWidget.appex/BabyWidget
Reason: tried: '/System/105Support/System/Library/Frameworks/ActivityKit.framework/ActivityKit'
(terminated at launch; ignore backtrace)

Thread Crashed:
0 dyld 0x0000000189391b48 __abort_with_payload + 8
1 dyld 0x000000018939e108 abort_with_payload_wrapper_internal + 104 (terminate_with_reason.c:102)
2 dyld 0x000000018939e13c abort_with_payload + 16 (terminate_with_reason.c:124)
3 dyld 0x0000000189325518 dyld4::halt(char const, dyld4::StructuredError const*) + 304 (DyldProcessConfig.cpp: 2890)
4 dyld 0x00000001893221e8 dyld4::prepare(dyld4::APIs&, dyld3::Mach0Analyzer const) + 3884 (dyldMain.cpp:0)
5 dyld 0x0000000189320f44 start + 1948 (dyldMain.cpp:1238)

Then I remembered that I had to weak link the ActivityKit framework in order to make Mango Baby continue available on Macs. This is done by adding -weak_framework ActivityKit to Other Linker Flags. But this stacktrace is from my BabyWidget app extension for widgets, and I didn't have the flag for my Widget target. I submitted an update along with other features.

Since I didn't know any way of testing iOS apps running on Macs, I didn't verify the fix. And it turns out Mango Baby is still crashing.

Attempt 2

Later I learned from Franklin on Threads that you can enable the Test iPhone and iPad Apps on Apple Silicon Macs option on App Store Connect. Then, you can at least use TestFlight to test your iOS app running on Mac. This still isn't ideal, but it's already 100 times better than nothing.

I checked new crash logs again, and they look like:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Exception Codes: 0x0000000000000001, 0x0000000000000000

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   ???                           	       0x0 ???
1   Baby                          	       0x102c75b1c closure #1 in closure #1 in closure #1 in closure #2 in AppModel.init(storage:) + 32 (AppModel.swift:488) [inlined]
2   Baby                          	       0x102c75b1c partial apply for closure #1 in closure #1 in closure #1 in closure #2 in AppModel.init(storage:) + 60
3   SwiftUI                       	       0x1c96f4c38 0x1c7d86000 + 26668088
4   SwiftUI                       	       0x1c96f4f28 0x1c7d86000 + 26668840
5   libswiftCore.dylib            	       0x190f6025c withExtendedLifetime<A, B>(_:_:) + 28
6   SwiftUI                       	       0x1c96f3420 0x1c7d86000 + 26661920
7   SwiftUI                       	       0x1c96f2f74 0x1c7d86000 + 26660724

Thread 0 crashed with ARM Thread State (64-bit):
    x0: 0x0000000000000000   x1: 0x0000000000000000   x2: 0x0000000000000103   x3: 0x0000600000296400
    x4: 0x0000000000000000   x5: 0x0000000000002760   x6: 0x00000001d94074e8   x7: 0x00000000ffffffff
    x8: 0x000000016d4658f0   x9: 0x000000016d465910  x10: 0x0000000000002760  x11: 0x00000000982478cd
   x12: 0x00000000000007fb  x13: 0x00000000000007fd  x14: 0x00000000984480d3  x15: 0x00000000000000d3
   x16: 0x0000000000000000  x17: 0x00000001d98d6c30  x18: 0x0000000000000000  x19: 0x000000016d4659d0
   x20: 0x000000016d465950  x21: 0x0000600002336710  x22: 0x00006000039a3020  x23: 0x0000000000000000
   x24: 0x000000016d465930  x25: 0x000000010372ebe8  x26: 0x00000001d98d3ec8  x27: 0x000000000000000f
   x28: 0x0000000000000000   fp: 0x000000016d465d90   lr: 0x0000000102c4ca70
    sp: 0x000000016d4658f0   pc: 0x0000000000000000 cpsr: 0x20001000
   far: 0x0000000000000000  esr: 0x82000006 (Instruction Abort) Translation fault

Binary Images:
0x102998000 - 0x1035b3fff com.mangoumbrella.Baby (2023.12) <ca277200-885e-3b08-8ee2-93648cc69108> /private/var/folders/*/Baby.app/Baby

Following this helpful Investigating memory access crashes article from Apple, I ran the following command to discover the exact line it's crahsing:

$ atos -arch arm64 -o atos -arch arm64 -o /Applications/Mango\ Baby.app/Wrapper/Baby.app/Baby -l 0x102998000 0x0000000102c4ca70
closure #2 in AppModel.processRemoteRecordEvents(events:) (in Baby) (AppModel.swift:1009)

And the line is:

if !ProcessInfo.processInfo.isiOSAppOnMac {
    LiveActivitiesManager.shared.syncAll()  // Crash here.
}

This doesn't make sense to me since this line shouldn't execute at all. It's protected by the isiOSAppOnMac check, and all the ActivityKit API calls are isolated in my LiveActivitiesManager class.

My only guess right now is that this might have something to do with branch predictions and the runtime will load the instructions even if they aren't executed at runtime in the end. With this guess, I have one solution in my mind: introduce a Protocol and two concrete classes for all my ActivityKit related functions. Something like:

protocol LiveActivitiesManagerProtocol {
    func syncAll()
}

class LiveActivitiesManager {
    static let shared: LiveActivitiesManagerProtocol = {
        if ProcessInfo.processInfo.isiOSAppOnMac {
            return NoopLiveActivitiesManager()
        } else {
            return RealLiveActivitiesManager()
        }
    }()
}

fileprivate class NoopLiveActivitiesManager: LiveActivitiesManagerProtocol {
    func syncAll() {}
}

fileprivate class RealLiveActivitiesManager: LiveActivitiesManagerProtocol {
    func syncAll() {
        // Use ActivityKit APIs.
    }
}

This code is now released in Mango Baby v2023.14 and it no longer crashes on Sonoma.

∞ Devon Dundee’s Apps of 2023

Devon has a great list of apps:

But one of Devon's Apps of 2023 is not like the other:

Mango Baby

The app that’s had the most impact on my life over the past year has to be Mango Baby, a newborn tracker by Yilei Yang. It’s not a new release, but since my son Noah was born, Mango Baby has been an indispensable tool for keeping track of his care.

The app is so well-designed and makes it simple for me to log feedings, diapers, and more. The information I need most is always right at hand, often not even requiring that I open the app thanks to Mango Baby’s extensive set of widgets. Shortcuts actions allow me to log diaper changes hands-free, and the iCloud-based collaboration is rock-solid, keeping everything in sync between my devices and my wife’s.

I’m a fan of anything that helps my kid and makes my life a bit easier. Mango Baby does both of those things every day.

I'm always inspired to hear how Mango Baby is helping parents. It has motivated me to keep improving the app for years.

Leaving Substack

After reading the note, I decided to leave Substack. This is an easy decision.

However, I'm also pausing Mango Newsletter for now. Even though I could move it somewhere else, I couldn't make it a priority. I need to spend more time on the actual development of Mango Baby and my next app.

If you are still interested in a relatively low volume of the updates on my indie dev work, you can find Mango Umbrella at @MangoUmbrella@indieapps.space or @mango.umbrella@threads.net.

Default Apps 2023

I saw this from Jay Wilson and took it as a template.

This is what I’m using at the point and time of publishing.

📨 Mail Client: Gmail app on iOS and gmail.com on macOS

📮 Mail Server: Google

📝 Notes: Apple Notes

✅ To-Do: Apple Notes

📷 Photo Shooting: iPhone 14 Pro, Sony A7R III, Sony FE 70-200 F4 G

🎨 Photo Editing: Lightroom on macOS, Photomator on iOS

📆 Calendar: Google Calendar

📁 Cloud File Storage: Google Drive, iCloud Drive

🙍🏻‍♂️ Contacts: Apple Contacts

🌐 Browser: Safari on iOS, Chrome on macOS

💬 Chat: WeChat, iMessages, Discord, Google Talk or whatever it is called today

🔖 Bookmarks: Chrome

📑 Read It Later: Apple Notes

📜 Word Processing: Google Docs

📈 Spreadsheets: Google Sheets

📊 Presentations: Google Slides

🛒 Shopping Lists: Apple Notes

🍴 Meal Planning: Google Docs, physical paper on the fridge

💰 Budgeting and Personal Finance: Google Sheets

📰 News: Mastodon

🎵 Music: YouTube Music

🎤 Podcasts: Overcast

🔐 Password Management: Chrome, Apple Keychain, KeePassXC

🧑‍💻 Code Editor: VS Code, Xcode

🌲 Git Client: git, GitHub Desktop

🖥️ Terminal: iTerm2

📐 Design: Figma

📝 Blog: In-house Engine and Client

✈️ VPN: Google Fi

∞ BREAKING NEWS: Myke Hurley Posted a Thread Again

Myke Hurley, the British professional podcaster, co-founder of the podcast network Relay FM, Chief Product Designer and co-founder at Cortex Brand, logged in to Threads today and is asking how to get his threads on news:

Post by @imyke
View on Threads

Does this count?

∞ How To Make a 800 Square-feet Sidewalk Garden in San Francisco's Mission District

Zach Klein wrote a great article on how they made a 800 square feet garden in San Francisco's Mission District:

This garden was conceived to help expand San Francisco's urban forest and it was designed to be an example of xeriscaping. It is drought-tolerant and requires no additional water other than seasonal rain. It is also a permeable landscape that improves the city's sewage treatment capacity by reducing storm water runoff through absorption.

Look at how beautiful it is:

A Sidewalk Garden in San Francisco's Mission District

I would have never guessed it's in the SF Mission district:

A Sidewalk Garden in San Francisco's Mission District

∞ Ask vs Guess Culture

Jean Hsu's article on Ask vs Guess Culture is a great read:

If you’re more a guess-culture person, asking people for help without knowing their circumstances can feel rude or intrusive. Broadcasting publicly your need for help can feel awkward and vulnerable.

If you’re more of an ask-culture person, the guess-culture example of juggling everyone’s specific scenarios and the historical context of favors probably seems exhausting. Dropping hints in the hopes that you won’t even have to make your request can feel extra passive and manipulative.

And because I'm an Asian living in the U.S., I experience so much clash between the cultures:

I was raised deeply in guess culture, as many Asians and Asian-Americans are. The Japanese proverb that “the nail that sticks up gets nailed down” reinforces the idea of social collectivism and keeping your individual needs and wants to yourself — values that are shared by many Asian culture.

Western society is very much ask culture. A classic example can be found in proverbs. “A squeaky wheel gets the grease” is an American proverb, enforcing the ideas of individualism and that asking for what you want will benefit you.

SwiftUI: How to Show Subscript and Superscript Texts

SwiftUI gives us a .baselineOffset(_:) modifier to set the vertical offset for the text relative to its baseline. You can combine this with smaller fonts to show subscript and superscript texts. For example:

var body: some View {
    Text("Mango")
    + Text("sub").font(.caption).baselineOffset(-3)
    + Text(" Umbrella")
    + Text("sup").font(.caption).baselineOffset(6)
}

`xcrun simctl status_bar` Is Not Working Since Xcode 14.1

xcrun simctl status_bar is a great tool to control what you want to display on the status bar in simulators. It's often used to set the time to 9:41 AM for screenshots.

Since Xcode 14.1 in Oct 2022 though, it's no longer working if the simulator runs iOS 16.1+. Neither Xcode 14.2 and 14.3 fixed this, and it has been a mysterious since.

However, today @saagar just posted the reason and a workaround:

If you’ve been trying to use xcrun simctl status_bar recently to take pretty screenshots and found that it doesn’t work past simulators running iOS 16.1, you can use SIMCTL_CHILD_SIMULATOR_RUNTIME_VERSION=16.0 xcrun simctl boot as a temporary workaround to get this back.

Great, problem solvedmitigated!

In addition, if you use @twostraws's awesome ControlRoom app, I sent out a PR (to be reviewed and merged) to add this workaround.

App Engine: Fixing a Go Panic, Metadata Fetch Failed

The Umbrella engine that powers this website is built using App Engine's Go Standard Environment.

In September 2021, Google Cloud enabled most of the legacy App Engine API on the second-generation App Engine runtimes. This means you could now use newer Go versions even in the standard App Engine. I migrated to Go 1.15 then and it worked smoothly.

Today, I was trying to upgrade the runtime to Go 1.20 since it was made generally available on March 24, 2023. Here is what I did:

  1. Install Go 1.20 locally from https://go.dev/doc/install.
  2. Change runtime: go115 to runtime: go120 in the app.yaml file.
  3. Make sure the app-engine-python and app-engine-go components from the gcloud CLI are up to date: gcloud components install app-engine-go app-engine-python

But running dev_appserver.py results in the following panic (trace simplified) when serving a request:

CRITICAL: panic: Metadata fetch failed for
'instance/attributes/gae_backend_version': Get
"http://metadata/computeMetadata/v1/instance/attributes/gae_backend_version":
dial tcp: lookup metadata: no such host

goroutine 7 [running]:
google.golang.org/appengine/v2/panic(...)
	go/src/runtime/panic.go:884 +0x204
google.golang.org/appengine/v2/internal.mustGetMetadata(...)
	google.golang.org/appengine/v2/internal/metadata.go:34 +0xa8
google.golang.org/appengine/v2/internal.VersionID(...)
	google.golang.org/appengine/v2/internal/identity.go:124 +0xe8
google.golang.org/appengine/v2.VersionID(...)
	google.golang.org/appengine/v2/identity.go:60
mangosite/code/public/common.Handle.func1(...)
	mangoumbrella/public/common/common.go:67 +0x430
net/http.HandlerFunc.ServeHTTP(...)
	go/src/net/http/server.go:2122 +0x38
github.com/gorilla/mux.(*Router).ServeHTTP(...)
	github.com/gorilla/mux@v1.8.0/mux.go:210 +0x19c
net/http.(*ServeMux).ServeHTTP(...)
	go/src/net/http/server.go:2500 +0x140
google.golang.org/appengine/v2/internal.executeRequestSafely(...)
	google.golang.org/appengine/v2/internal/api.go:136 +0x68
google.golang.org/appengine/v2/internal.handleHTTP(...)
	google.golang.org/appengine/v2/internal/api.go:116 +0x374
net/http.HandlerFunc.ServeHTTP(...)
	go/src/net/http/server.go:2122 +0x38
net/http.serverHandler.ServeHTTP(...)
	go/src/net/http/server.go:2936 +0x2d8

Upon investigation, I have the following code in my http handler:

import "google.golang.org/appengine/v2"

func handler(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    _ = appengine.VersionID(c)
}

The panic happens at the appengine.VersionID(c) call as it doesn't work locally. Instead, you could check if it's running locally and use a different code path:

if appengine.IsDevAppServer() {
   // Do something else
} else {
   // This runs in production.
   _ = appengine.VersionID(c)
}

Hope this helps.