← Back to Plan

Swift Pitfalls

Swift / iOS Build Pitfalls

Lessons learned from getting the Steel Notes iOS app to compile and run. Every item here caused a real build failure or runtime crash. Read this before touching iosApp/.

XcodeGen & Info.plist

INFOPLIST_KEY_* build settings are ignored with custom plists

When a target specifies info: path: SomeDir/Info.plist in project.yml, Xcode does not apply INFOPLIST_KEY_* build settings. Those only work when Xcode auto-generates the plist.

Fix: Put required keys in project.yml under info: properties: alongside path:. XcodeGen merges properties into the plist at the given path.

```yaml

info:

path: SteelNotesShare/Info.plist

properties:

CFBundleDisplayName: "Steel Notes"

NSExtension:

NSExtensionPointIdentifier: com.apple.share-services

```

App extensions MUST have NSExtension in their Info.plist

Without the NSExtension dictionary, the simulator refuses to install with extensionDictionary must be set in placeholder attributes. This applies to Share extensions, Widget extensions, etc.

  • **Share extension:** needs NSExtensionPointIdentifier: com.apple.share-services, NSExtensionPrincipalClass, and NSExtensionActivationRule
  • **Widget extension:** needs NSExtensionPointIdentifier: com.apple.widgetkit-extension
  • App extensions MUST have CFBundleDisplayName

    Without it: does not have a CFBundleDisplayName key with a non-zero length string value.

    UILaunchScreen must be in the plist or the app runs zoomed

    If the main app's Info.plist is missing UILaunchScreen, iOS runs the app in a legacy compatibility mode where everything appears zoomed in and doesn't extend to screen edges. Add an empty dict:

    ```yaml

    info:

    properties:

    UILaunchScreen: {}

    ```

    After fixing, delete the app from the simulator before rebuilding — the launch screen config is cached.

    Running xcodegen generate resets plists

    XcodeGen regenerates plist files from project.yml. Any manual edits to Info.plist files will be overwritten. Always put plist content in project.yml info: properties:, never edit the plist files directly.


    Kotlin/Native ↔ Swift Interop

    Kotlin Map cannot be cast to CFDictionaryRef

    Casting a Kotlin mapOf<Any?, Any?>() directly to CFDictionaryRef compiles but crashes at runtime. Kotlin maps don't bridge to CoreFoundation dictionaries.

    Fix: Use NSMutableDictionary to build the dictionary, then cast that to CFDictionaryRef. For Keychain queries, the simplest working approach is NSUserDefaults for development, with a proper Keychain implementation for production.

    Security framework constants are CFStringRef, not String

    kSecClass, kSecAttrService, etc. are CFStringRef types. Methods like NSMutableDictionary.setValue(forKey:) may not accept them directly.

    alloc<CFDictionaryRef?>() is invalid

    CFDictionaryRef is a pointer typealias, not a struct. Use alloc<CFTypeRefVar>() for SecItemCopyMatching's out parameter.

    KMP framework loading blocks the main thread

    The first access to ANY type from SteelNotesShared triggers Kotlin/Native runtime initialization, which can take 10-30+ seconds. If this happens during AppContainer.init(), the app shows a blank screen.

    Fix: Keep AppContainer.init() empty — no KMP type references. Use .task {} in SwiftUI to trigger initialization after the first frame renders. Construct all KMP objects inside Task.detached {} to run off the main thread.

    ```swift

    // BAD — blocks main thread before any UI renders

    @StateObject private var container = AppContainer() // init() creates KMP objects

    // GOOD — UI renders immediately, KMP loads in background

    .task { await container.start() } // start() creates KMP objects in Task.detached

    ```

    SKIE renames final to ` final ` (backtick-escaped)

    The Kotlin enum case FINAL becomes a Swift reserved keyword conflict. With SKIE, access it as ` SynthesisStatus.final , not .final_. Without SKIE, Kotlin/Native exports it as .final_`. Check which is correct for your SKIE version.

    expect/actual class warnings

    These produce Beta warnings. Suppress with -Xexpect-actual-classes compiler flag or kotlin.suppressExpectActualClasses=true in gradle.properties. They are warnings, not errors.


    Swift / SwiftUI

    ShapeStyle does not inherit Color static members

    Color extensions like static let _steelAccent on Color are not visible in .foregroundStyle(._steelAccent) because foregroundStyle expects ShapeStyle, not Color.

    Fix: Add a ShapeStyle extension:

    ```swift

    extension ShapeStyle where Self == Color {

    static var _steelAccent: Color { Color._steelAccent }

    // ... repeat for all custom colors

    }

    ```

    @EnvironmentObject does not observe nested object properties

    If AppNavigation has @EnvironmentObject var container: AppContainer, changing container.authService.isAuthenticated does NOT trigger a re-render. SwiftUI only observes @Published properties on the object itself.

    Fix: Inject the nested ObservableObject as its own @EnvironmentObject:

    ```swift

    .environmentObject(container)

    .environmentObject(container.authService)

    ```

    Then observe it directly in the view:

    ```swift

    @EnvironmentObject var authService: AuthService

    ```

    @MainActor isolation for ObservableObject

    If a class like AuthService is @MainActor, its initializer can only be called from a @MainActor context. Calling it from a non-isolated init() produces: Call to main actor-isolated initializer in a synchronous nonisolated context.

    Fix: Mark the containing class @MainActor too, or create the @MainActor objects inside await MainActor.run {}.

    Implicitly unwrapped optionals become Optional when captured

    let core = container.core where core is SharedCoreService! infers the local as SharedCoreService?, not SharedCoreService.

    Fix: Force-unwrap at the capture site: let core = container.core!. This is safe when the code path is gated by an isReady check.

    Redundant as? [String] casts on KMP-exported List<String>

    Kotlin List<String> exports to Swift as [String]. Casting with as? [String] produces a warning: Conditional cast from '[String]' to '[String]' always succeeds.

    Fix: Remove the cast. Use the property directly.

    String.init is ambiguous in .map(String.init)

    String has many initializers. Using .map(String.init) on a [Substring] or [String] triggers Ambiguous use of 'init'.

    Fix: If the elements are already strings, remove the .map() entirely. Otherwise use an explicit closure: .map { String($0) }.

    .joined() on Data.map works without separator

    Data.map { ... }.joined() is valid — the default separator is "". Don't add separator: "" unnecessarily.


    Gradle / Build Environment

    Java 25 is too new for Gradle 8.x

    If the system defaults to Java 25, Gradle fails with a cryptic error message that is just the version number (e.g., 25.0.2).

    Fix: Always set JAVA_HOME to JDK 21 before running Gradle:

    ```bash

    export JAVA_HOME=$(/usr/libexec/java_home -v 21)

    ```

    The Xcode pre-build script already handles this, but CLI Gradle invocations need it explicitly.

    SQLDelight getMeta returns Query<String>, not a data class

    When a SQLDelight query returns a single column, the generated code maps directly to that type (e.g., Query<String>). Don't access .value_ on the result — it's already the string.


    Simulator

    Delete app before rebuilding if launch screen / zoom changes

    The simulator caches the app's launch screen configuration. If you add UILaunchScreen to fix the zoomed display, you must delete the app from the simulator and do a fresh install.

    simctl commands hang on iOS 26.x beta

    xcrun simctl listapps, simctl launch, and other commands may hang indefinitely on iOS 26 beta simulators. Use Xcode's Run button (Cmd+R) instead of CLI tools for launching.

    Simulator console noise is harmless

    These errors appear on every iOS 26 beta simulator launch and can be ignored:

  • Failed to send CA Event for app launch measurements
  • NSMapGet: map table argument is NULL
  • Class UIAccessibilityLoaderWebShared is implemented in both...
  • AX Safe category class was not found