# How iOS Platform Bridges Work in Muse: FileUtils and DocumentPicker Architecture

> Discover how Muse's iOS platform bridges FileUtils and DocumentPicker leverage expect/actual for native file sharing and document selection via okio.Path API.

- Repository: [Ko Shin/muse](https://github.com/kkoshin/muse)
- Tags: architecture
- Published: 2026-03-05

---

**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](https://github.com/kkoshin/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`](https://github.com/kkoshin/muse/blob/main/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`](https://github.com/kkoshin/muse/blob/main/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`](https://github.com/kkoshin/muse/blob/main/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.

```kotlin
// 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.

```kotlin
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`.

```kotlin
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`:

```kotlin
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`](https://github.com/kkoshin/muse/blob/main/DocumentPicker.ios.kt) accesses the current `UIViewController` through `LocalUIViewController.current`. This enables the bridge to present native controllers from within shared Compose code.

```kotlin
@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()`.

```kotlin
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

```kotlin
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

```kotlin
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

```kotlin
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`](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/FileUtils.kt)** — Declares cross-platform file utility contracts (`expect` declarations).
- **[`muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/FileUtils.ios.kt`](https://github.com/kkoshin/muse/blob/main/muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/FileUtils.ios.kt)** — Implements utilities using `UIActivityViewController`, `UIDocumentInteractionController`, and `NSFileManager`.
- **[`muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/DocumentPicker.kt`](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/platformbridge/DocumentPicker.kt)** — Defines the `DocumentPicker` class, `launch()` method, and `rememberDocumentPicker` composable factory.
- **[`muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/DocumentPicker.ios.kt`](https://github.com/kkoshin/muse/blob/main/muse/src/iosMain/kotlin/io/github/kkoshin/muse/platformbridge/DocumentPicker.ios.kt)** — Creates `UIDocumentPickerViewController` with 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 `iosMain` source sets.
- **FileUtils** bridges provide native sharing via `UIActivityViewController`, previewing via `UIDocumentInteractionController`, and sandbox-compliant cache file creation using `NSFileManager`.
- **DocumentPicker** integrates `UIDocumentPickerViewController` with Compose through `LocalUIViewController`, handling UTType identifiers and automatic sandbox copying via `asCopy = true`.
- **Memory safety** is enforced by retaining `UIDocumentInteractionController` and 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`](https://github.com/kkoshin/muse/blob/main/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.