How iOS Platform Bridges Work in Muse: FileUtils and DocumentPicker Architecture
Muse implements iOS platform bridges using Kotlin Multiplatform's expect/actual mechanism, delegating file sharing, opening, and document selection to native UIKit components while exposing a unified okio.Path-based API to shared Compose UI code.
The Muse repository demonstrates idiomatic Kotlin Multiplatform (KMP) architecture by isolating iOS-specific file operations in dedicated platform bridges. These iOS platform bridges—specifically FileUtils and DocumentPicker—enable the shared Compose UI to perform native file handling without importing platform-specific code into the common module.
Expect/Actual Pattern: The Foundation of iOS Platform Bridges
Muse structures its platform abstraction using Kotlin's expect/actual declarations. The common module (commonMain) defines contracts that describe required functionality, while the iOS source set (iosMain) supplies concrete implementations using UIKit and Foundation frameworks.
In muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/FileUtils.kt, the common code declares expect functions such as shareAudioFile, openFile, createCacheFile, and Path.toSink. The compiler links these to their actual counterparts in muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/FileUtils.ios.kt when building the iOS binary. This pattern ensures that shared business logic interacts with file operations through a consistent interface, while platform-specific implementation details remain isolated.
FileUtils Bridge: Native File Operations
The FileUtils bridge provides a unified API for sharing, opening, and creating cache files on iOS. The implementation relies on native UIKit controllers to present system interfaces.
Sharing Audio with UIActivityViewController
To share audio files, the shareAudioFile function in FileUtils.ios.kt constructs an NSURL from the provided okio.Path, then instantiates a UIActivityViewController. The implementation safely configures the pop-over location for iPad compatibility, preventing runtime crashes on tablet devices.
// FileUtils.ios.kt
actual fun shareAudioFile(path: Path): Result<Unit> = runCatching {
val url = NSURL.fileURLWithPath = path.toString()
val activityViewController = UIActivityViewController(listOf(url), null)
// Pop-over configuration for iPad...
uiViewController.presentViewController(activityViewController, true, null)
}
Opening Files with UIDocumentInteractionController
For file preview and "Open In" functionality, the bridge uses UIDocumentInteractionController. The controller is retained in a top-level variable activeDocumentInteractionController to prevent premature deallocation by the garbage collector—a critical consideration when bridging Objective-C objects to Kotlin/Native.
private var activeDocumentInteractionController: UIDocumentInteractionController? = null
actual fun openFile(path: Path): Result<Unit> = runCatching {
val url = NSURL.fileURLWithPath = path.toString()
activeDocumentInteractionController = UIDocumentInteractionController(url).apply {
delegate = documentInteractionControllerDelegate
presentPreviewAnimated(true)
}
}
Cache File Management
The createCacheFile function selects between the Library directory (for sensitive data) and Caches directory (for temporary files) based on a boolean flag. It uses NSFileManager.URLForDirectory to obtain the appropriate sandbox location before converting the resulting NSURL to an okio.Path.
actual fun createCacheFile(fileName: String, sensitive: Boolean): Path {
val directory = if (sensitive) NSLibraryDirectory else NSCachesDirectory
val url = NSFileManager.defaultManager.URLForDirectory(
directory,
NSUserDomainMask,
null,
true,
null
)!!.URLByAppendingPathComponent(fileName)!!
return url.toOkioPath()
}
Sink Conversion for Streaming Writes
To enable writing to files using Okio's streaming API, the bridge implements Path.toSink() by delegating to Okio's native iOS SystemFileSystem:
actual fun Path.toSink(): Sink {
return SystemFileSystem.sink(this)
}
DocumentPicker Bridge: UIDocumentPickerViewController Integration
The DocumentPicker bridge allows Compose UI to launch the native iOS document picker and receive selected file paths as okio.Path objects.
Composable Factory and UIViewController Access
The rememberDocumentPicker composable function in DocumentPicker.ios.kt accesses the current UIViewController through LocalUIViewController.current. This enables the bridge to present native controllers from within shared Compose code.
@Composable
actual fun rememberDocumentPicker(
mimeType: MimeType,
onResult: (Path?) -> Unit
): DocumentPicker {
val uiViewController = LocalUIViewController.current
return DocumentPicker(uiViewController, mimeType, onResult)
}
Type Safety with UTType Identifiers
The implementation maps the abstract MimeType enum to iOS Uniform Type Identifiers (UTIs). For audio files, it uses UTTypeAudio; for text, it uses UTTypeText or UTTypePlainText. This ensures the picker only presents relevant file types to the user.
Delegate Implementation and Memory Safety
A UIDocumentPickerDelegateProtocol implementation receives the didPickDocumentsAtURLs callback. The delegate is retained in a keepDelegate variable to prevent garbage collection during the picker's lifecycle. The selected NSURL is converted to okio.Path using toOkioPath().
private var keepDelegate: UIDocumentPickerDelegateProtocol? = null
actual fun launch() {
val picker = UIDocumentPickerViewController(
listOf(utType), // UTTypeAudio or UTTypeText
asCopy = true
)
keepDelegate = DocumentPickerDelegate { urls ->
val path = (urls.first() as? NSURL)?.toOkioPath()
onResult(path)
keepDelegate = null // Release after callback
}
picker.delegate = keepDelegate
uiViewController.presentViewController(picker, true, null)
}
Sandbox Security with asCopy
The picker is instantiated with asCopy = true, instructing iOS to copy the selected file into the app's temporary sandbox. This approach eliminates the need for persistent security-scope bookmarks, simplifying file access permissions while maintaining privacy compliance.
Practical Implementation Examples
Sharing an Audio File
import io.github.kkoshin.muse.platformbridge.shareAudioFile
import io.github.kkoshin.muse.platformbridge.createCacheFile
import okio.Path
fun exportRecording(data: ByteArray, fileName: String) {
// Create temporary cache file in Caches directory
val path: Path = createCacheFile(fileName, sensitive = false)
// Write data using okio
path.toSink().use { sink ->
sink.write(data)
}
// Launch native share sheet
shareAudioFile(path).onFailure { error ->
println("Share failed: ${error.message}")
}
}
Opening a File for Preview
import io.github.kkoshin.muse.platformbridge.openFile
fun previewDocument(path: Path) {
openFile(path).onFailure { error ->
// Handle case where no application can open the file
showErrorMessage("Unable to preview file")
}
}
Selecting Audio in Compose UI
import io.github.kkoshin.muse.platformbridge.rememberDocumentPicker
import io.github.kkoshin.muse.platformbridge.MimeType
@Composable
fun AudioImporter(onAudioSelected: (Path) -> Unit) {
val picker = rememberDocumentPicker(
mimeType = MimeType.Audio,
onResult = { path ->
path?.let { onAudioSelected(it) }
}
)
Button(onClick = { picker.launch() }) {
Text("Import Audio File")
}
}
Key Source Files
muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/FileUtils.kt— Declares cross-platform file utility contracts (expectdeclarations).muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/FileUtils.ios.kt— Implements utilities usingUIActivityViewController,UIDocumentInteractionController, andNSFileManager.muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/DocumentPicker.kt— Defines theDocumentPickerclass,launch()method, andrememberDocumentPickercomposable factory.muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/DocumentPicker.ios.kt— CreatesUIDocumentPickerViewControllerwith delegate management and UTI type mapping.
Summary
- Muse uses Kotlin Multiplatform's expect/actual pattern to abstract iOS file operations, keeping platform-specific UIKit code isolated in
iosMainsource sets. - FileUtils bridges provide native sharing via
UIActivityViewController, previewing viaUIDocumentInteractionController, and sandbox-compliant cache file creation usingNSFileManager. - DocumentPicker integrates
UIDocumentPickerViewControllerwith Compose throughLocalUIViewController, handling UTType identifiers and automatic sandbox copying viaasCopy = true. - Memory safety is enforced by retaining
UIDocumentInteractionControllerand picker delegates in top-level variables to prevent Kotlin/Native garbage collection from releasing Objective-C objects prematurely. - All bridges return okio.Path objects, ensuring consistent file path handling across Android and iOS platforms.
Frequently Asked Questions
What is the difference between the common and iOS actual implementations?
The common implementation in commonMain defines expect declarations—function signatures and class interfaces that represent the contract for file operations. The iOS actual implementation in iosMain provides the concrete actual functions using UIKit and Foundation APIs. This separation allows the shared Compose UI to call shareAudioFile() or rememberDocumentPicker() without knowing whether it is running on iOS or Android.
How does Muse prevent memory leaks when using UIKit controllers?
Muse prevents premature deallocation by storing references to UIDocumentInteractionController and UIDocumentPickerDelegateProtocol in top-level private variables (activeDocumentInteractionController and keepDelegate). Since Kotlin/Native uses a garbage collector while UIKit relies on reference counting, these retained references ensure the Objective-C objects stay alive while the user interacts with system dialogs. The delegates are explicitly set to null after callbacks complete to release memory.
Can these bridges be used with file types other than audio and text?
Yes, the architecture supports any file type that iOS can identify with a Uniform Type Identifier (UTI). The MimeType enum in the common code can be extended to support additional types (such as PDF or images), and the iOS actual implementation in DocumentPicker.ios.kt can map these to corresponding UTType constants (e.g., UTTypePDF, UTTypeImage). The FileUtils bridge is generic and works with any file path regardless of content type.
How does the DocumentPicker handle iOS sandbox restrictions?
The implementation uses UIDocumentPickerViewController with asCopy = true, which instructs the system to copy the selected file into the app's temporary container. This approach avoids the need for security-scoped bookmarks that would otherwise be required to persist access to files outside the app sandbox. The copied file is converted to an okio.Path and passed back to the shared code, allowing immediate read access without complex permission management.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →