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/.
INFOPLIST_KEY_* build settings are ignored with custom plistsWhen 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
```
NSExtension in their Info.plistWithout the NSExtension dictionary, the simulator refuses to install with extensionDictionary must be set in placeholder attributes. This applies to Share extensions, Widget extensions, etc.
NSExtensionPointIdentifier: com.apple.share-services, NSExtensionPrincipalClass, and NSExtensionActivationRuleNSExtensionPointIdentifier: com.apple.widgetkit-extensionCFBundleDisplayNameWithout it: does not have a CFBundleDisplayName key with a non-zero length string value.
UILaunchScreen must be in the plist or the app runs zoomedIf 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.
xcodegen generate resets plistsXcodeGen 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.
Map cannot be cast to CFDictionaryRefCasting 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.
CFStringRef, not StringkSecClass, kSecAttrService, etc. are CFStringRef types. Methods like NSMutableDictionary.setValue(forKey:) may not accept them directly.
alloc<CFDictionaryRef?>() is invalidCFDictionaryRef is a pointer typealias, not a struct. Use alloc<CFTypeRefVar>() for SecItemCopyMatching's out parameter.
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
```
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 warningsThese produce Beta warnings. Suppress with -Xexpect-actual-classes compiler flag or kotlin.suppressExpectActualClasses=true in gradle.properties. They are warnings, not errors.
ShapeStyle does not inherit Color static membersColor 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 propertiesIf 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 ObservableObjectIf 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 {}.
Optional when capturedlet 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.
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 separatorData.map { ... }.joined() is valid — the default separator is "". Don't add separator: "" unnecessarily.
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.
getMeta returns Query<String>, not a data classWhen 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.
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 betaxcrun 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.
These errors appear on every iOS 26 beta simulator launch and can be ignored:
Failed to send CA Event for app launch measurementsNSMapGet: map table argument is NULLClass UIAccessibilityLoaderWebShared is implemented in both...AX Safe category class was not found