14 Posts under Mango Snippets
How to Use SwiftFormat with SweetPad
SweetPad is a VS Code extension for Swift/iOS development, with swift-format
formatting built in. However, I prefer using SwiftFormat
by Nick Lockwood for its comprehensive formatting options.
To use SwiftFormat with SweetPad, update the formatter path and remove the default --in-place
argument in VS Code settings:
Here's my settings.json
:
{
"[swift]": {
"editor.formatOnSave": true,
"files.autoSaveWorkspaceFilesOnly": true,
"editor.defaultFormatter": "sweetpad.sweetpad"
},
"sweetpad.format.args": [
"${file}"
],
"sweetpad.format.path": "swiftformat"
}
A Small Interaction in Cursor
A lot of us are familiar with GitHub Copilot's multi-line auto-completions. As LLM models improve, they're becoming better productivity multipliers. However, auto-completion is just one way IDEs can leverage LLMs.
I'd like to share a very small interaction I had in Cursor today.
I wrote my website's backend in Go myself. At the bottom of my blog posts, there's a page navigation section that looks like:
« Previous page ---- Page 2 ---- Next page »
I wanted to add the total number of pages:
« Previous page - Page 2 of 51 - Next page »
I originally wrote the following code with a bug (both totalPosts
and numPostsPerPage
are int
in Go):
data.TotalPosts = totalPosts
data.NumPages = totalPosts / numPostsPerPage
Instead of directly fixing it, I added a leading comment: // This should be rounded up:
. Cursor immediately suggested a fix.
Cursor suggesting an edit after adding a leading comment.
This is not an auto-completion, but a suggested edit!
Git: Passwordless Push to GitHub
Every time I need to set up passwordless push to GitHub on a new machine, I had to perform many Google searches and read many instructions on different pages. I can't simply find simple instructions on a single page. So this snippet was born.
Here are the three steps to set up passwordless push to GitHub:
- Generate an SSH key:
ssh-keygen -t ed25519 -C "me@domain.com"
. - Add the public key from
~/.ssh/id_ed25519.pub
to your GitHub account. - Change your git remote url
git remote set-url origin git@github.com:<org>/<repo>.git
.
AppIntents: How to Set a Background Color for Your App Shortcuts
In WWDC22's Design App Shortcuts session, Lynn mentions:
And one last thing -- pick a color for your shortcuts in the Shortcuts app. We have a bunch of great colors for you to choose from, and all your shortcuts will use this color in the app. So pick one that complements your app icon nicely and don't just stick with the default.
To do that, you will override the shortcutTileColor
in your AppShortcutsProvider
like this:
struct MangoBabyAppShortcuts: AppShortcutsProvider {
static var shortcutTileColor: ShortcutTileColor = .red
static var appShortcuts: [AppShortcut] = []
}
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.
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)
}
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:
- Install Go 1.20 locally from https://go.dev/doc/install.
- Change
runtime: go115
toruntime: go120
in theapp.yaml
file. - Make sure the
app-engine-python
andapp-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.
iOS: How to Enable the Save Image Option in Share Sheet
When you want to present a share sheet to share an image, it isn't hard to figure out how to use UIActivityViewController
. All you need is to prepare an UIImage
or a URL
pointing to an image:
let controller = UIActivityViewController(
activityItems: [image], applicationActivities: nil)
present(controller, animated: true)
However, you won't see the "Save Image" option in the share sheet by default. The reason you don't see this option is unintuitive, but it actually requires the host app to have the permission writing to the photo library.
Since this action directly saves the image in user's photo library, you want to enable this convenient option. To do this, open your project's Info.plist
file, add a "Privacy - Photo Library Additions Usage Description", and give it a value like "Support saving images to the photo library".
SwiftUI: How to Add an Inverted Mask
SwiftUI's mask(alignment:_:)
method allows you to mask the view using the alpha channel of a given view. However, sometime you want to add an inverted mask. There are a few ways to achieve this depending on how your mask view is constructed.
Example of inverted mask effect.
Use eoFill
If your mask is a shape, you can create another shape by adding a Rectangle
path and applying a FillStyle(eoFill: true)
:
struct CircleInRectangle: Shape {
let padding: CGFloat
func path(in rect: CGRect) -> Path {
var shape = Rectangle().path(in: rect)
let paddedRect = rect.insetBy(dx: padding, dy: padding)
shape.addPath(Circle().path(in: paddedRect))
return shape
}
}
struct ContentView: View {
var body: some View {
Image(uiImage: #imageLiteral(resourceName: "img.jpeg"))
.resizable()
.scaledToFill()
.clipped()
.frame(width: 400, height: 225)
.background(.white)
.mask(
CircleInRectangle(padding: 24)
.fill(style: .init(eoFill: true))
)
)
}
}
Use .luminanceToAlpha()
SwiftUI's .luminanceToAlpha()
modifier creates a mask by making dark colors transparent and bright colors opaque.
struct ContentView: View {
var body: some View {
Image(uiImage: #imageLiteral(resourceName: "pattern.jpeg"))
.resizable()
.scaledToFill()
.clipped()
.frame(width: 400, height: 225)
.background(.white)
.mask(
Circle()
.padding()
.foregroundColor(.black)
.background(.white)
.compositingGroup()
.luminanceToAlpha()
)
}
}
Use .blendMode(.destinationOut)
SwiftUI's .blendMode(.destinationOut)
modifier allows you to erase any of the background that is covered by opaque source pixels.
struct ContentView: View {
var body: some View {
Image(uiImage: #imageLiteral(resourceName: "pattern.jpeg"))
.resizable()
.scaledToFill()
.clipped()
.frame(width: 400, height: 225)
.background(.white)
.mask(
Color.black
.overlay(
Circle()
.padding()
.blendMode(.destinationOut)
)
.compositingGroup()
)
}
}
Swift: The Direction of Date.distance(to:)
The official document of Date.distance(to:)
says it:
Returns the distance from this date to another date, specified as a time interval.
But it has never been clear to me positive distance means whether the left side date is earlier or later.
So this tip is dedicated to myself: Date.distance(to:)
is the opposite of minus. The distance positive when the left side date is earlier.
$ swift
1> let now = Date()
now: Date = {}
2> let oneHourLater = now.advanced(by: 3600)
oneHourLater: Date = {}
3> let distance = now.distance(to: oneHourLater)
distance: TimeInterval = 3600