This specification is maintained by the AgentRules Working Group. Have enhancements or want to claim maintainership?Submit Specification →
mobilecritical PriorityID: kmp-compose-skiko-metal-surface-disposal-guardrail

KMP Compose Skiko Metal Surface Lifecycle & Native GPU Buffer Disposal

Enforces deterministic teardown of CAMetalLayer render surfaces and Skiko rendering contexts during iOS navigation transitions. Prevents unmanaged native GPU memory accumulation, command buffer starvation, and rapid Jetsam terminations caused by dangling ComposeUIViewController references in shared Kotlin UI runtimes.

Prohibited Anti-Pattern: Retaining CAMetalLayer Buffers and Skiko Render Loops Across iOS Navigation Transitions

Export Ready-to-Drop Configuration

Select format below to copy code or download file directly.

---
description: KMP Compose Skiko Metal Surface Lifecycle & Native GPU Buffer Disposal - Enforces deterministic teardown of CAMetalLayer render surfaces and Skiko rendering contexts during iOS navigation transitions. Prevents unmanaged native GPU memory accumulation, command buffer starvation, and rapid Jetsam terminations caused by dangling ComposeUIViewController references in shared Kotlin UI runtimes.
globs: **/iosMain/**/*.kt, **/iosApp/**/*.swift, **/composeApp/src/iosMain/**/*.kt
alwaysApply: true
verified_by: "agentrules.org"
license: "CC-BY-4.0"
source: "https://agentrules.org/rules/kmp-compose-skiko-metal-surface-disposal-guardrail"
---

# KMP Compose Skiko Metal Surface Lifecycle & Native GPU Buffer Disposal

> **Severity**: CRITICAL | **Category**: mobile
> **Anti-Pattern Warning**: Retaining CAMetalLayer Buffers and Skiko Render Loops Across iOS Navigation Transitions

## System Constraints
You are a Principal Mobile Systems Architect specializing in Kotlin Multiplatform (KMP), Compose Multiplatform, and Apple Metal graphics pipelines. Your role is to mandate strict native lifecycle boundaries and GPU surface disposal guards to eliminate iOS native memory fragmentation and Jetsam OOM crashes during rapid view hierarchy transitions.

## Strict Guidelines
- Engineers MUST ALWAYS implement deterministic teardown hooks in UIViewControllerRepresentable via dismantleUIViewController when hosting ComposeUIViewController in SwiftUI.
- Engineers MUST explicitly detach or invalidate the underlying SkikoUIView render delegate when a Compose view controller is popped from a UINavigationController stack.
- Engineers MUST NOT allow circular strong references between platform UIKit delegates, CADisplayLink wrappers, and Compose State containers across the Kotlin/Native boundary.
- Code MUST release and flush Skia GPU context resource caches (e.g., purgeSkiaFontCache, purgeGpuResources) during memory pressure warnings (applicationDidReceiveMemoryWarning).
- Components NEVER assume ARC will promptly collect CAMetalLayer drawable pools if ComposeUIViewController remains linked in a detached or hidden UINavigationController stack segment.
- Services MUST route navigation lifecycle events into the Compose runtime to pause recomposition and frame dispatch whenever viewDidDisappear is invoked with isMovingFromParent or isBeingDismissed.

## Bad vs Good Implementations
### SwiftUI ComposeUIViewController Lifecycle Bridge with Metal Surface Teardown

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: Failing to implement dismantleUIViewController causes ComposeUIViewController's Skiko Metal surface to retain GPU textures and keep display links active after view eviction, creating unmanaged native GPU memory leaks across rapid navigation pushes/pops.
```swift
// BAD: Creating ComposeUIViewController without dismantle lifecycle guarantees.
// When navigated away, SwiftUI drops the view reference, but the underlying
// Skiko CAMetalLayer and CADisplayLink remain bound in native memory,
// rapidly exhausting drawable pools and triggering Jetsam OOM on iOS.
import SwiftUI
import SharedKit

struct ComposeViewWrapper: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        return MainViewControllerKt.MainViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // Re-renders without disposal tracking
    }
}
```

#### Production Standard (Correct)
> **Standard**: Implements dismantleUIViewController to ensure the CAMetalLayer drawable queue is purged and superlayer links are detached immediately upon navigation exit, reclaiming native GPU heap memory before Jetsam thresholds are hit.
```swift
// GOOD: Explicit lifecycle coordination that notifies the Compose runtime,
// stops rendering passes, and detaches Skiko Metal context upon view dismantling.
import SwiftUI
import SharedKit

struct ComposeViewWrapper: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIViewController

    func makeUIViewController(context: Context) -> UIViewController {
        let controller = MainViewControllerKt.MainViewController()
        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // Regular state synchronization
    }

    static func dismantleUIViewController(_ uiViewController: UIViewController, coordinator: ()) {
        // Explicitly notify runtime of detachment to release CAMetalLayer resources
        if let composeController = uiViewController as? ComposeLifecycleOwner {
            composeController.onSurfaceDestroyed()
        }
        uiViewController.view.subviews.forEach { subview in
            subview.layer.sublayers?.forEach { layer in
                if layer is CAMetalLayer {
                    layer.contents = nil
                    layer.removeFromSuperlayer()
                }
            }
        }
    }
}
```

### Kotlin/Native ComposeUIViewController Host Disposal Guard

#### Bad Antipattern (AI Common Hallucination)
> **Why it fails**: Omits UINavigationController lifecycle integration and memory pressure handling. Popping this controller from a native navigation stack leaves Skiko's render target and Skia GPU cache allocated.
```kotlin
// BAD: Standard factory returning raw UIViewController.
// When UINavigationController pops the controller, Skiko continues listening
// to frame updates, leaving Metal buffers and native fonts uncollected.
package com.example.ui

import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController

fun MainViewController(): UIViewController = ComposeUIViewController {
    AppRootContent()
}
```

#### Production Standard (Correct)
> **Standard**: Wraps ComposeUIViewController in a lifecycle-aware UIViewController that hooks isMovingFromParentViewController/isBeingDismissed to fully detach child hierarchies and clear references, facilitating instant ARC cleanup of Skiko GPU context.
```kotlin
// GOOD: Custom hosting controller subclassing UIViewController to observe
// native dismissal and hook into platform memory pressure events.
package com.example.ui

import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController
import platform.UIKit.navigationController

interface ComposeLifecycleOwner {
    fun onSurfaceDestroyed()
}

fun MainViewController(): UIViewController {
    return ComposeHostViewController()
}

private class ComposeHostViewController : UIViewController(null, null), ComposeLifecycleOwner {
    private var composeController: UIViewController? = null

    override fun viewDidLoad() {
        super.viewDidLoad()
        val inner = ComposeUIViewController {
            AppRootContent()
        }
        composeController = inner
        addChildViewController(inner)
        view.addSubview(inner.view)
        inner.view.setFrame(view.bounds)
        inner.didMoveToParentViewController(this)
    }

    override fun viewDidDisappear(animated: Boolean) {
        super.viewDidDisappear(animated)
        if (isMovingFromParentViewController() || isBeingDismissed()) {
            onSurfaceDestroyed()
        }
    }

    override fun onSurfaceDestroyed() {
        composeController?.willMoveToParentViewController(null)
        composeController?.view?.removeFromSuperview()
        composeController?.removeFromParentViewController()
        composeController = null
    }

    override fun didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Force Skiko/Skia buffer reclamation
        platform.Foundation.NSNotificationCenter.defaultCenter.postNotificationName(
            "ComposePurgeGpuCaches",
            null
        )
    }
}
```

Recommended MCP Servers (mcp-serverlist.com)

Equip agent with system tools to verify code for this stack.

Recommended Agent Skills (askills.dev)

Ready-to-use executable skill functions tailored to this stack.

Architectural Specification & Invariant Rationale

Architectural Invariant Design: KMP Compose Skiko Metal Surface Lifecycle & Native GPU Buffer Disposal

KMP Compose Skiko Metal Surface Lifecycle & Native GPU Buffer Disposal defines an industrial-grade behavioral standard for mobile engineering. Enforces deterministic teardown of CAMetalLayer render surfaces and Skiko rendering contexts during iOS navigation transitions. Prevents unmanaged native GPU memory accumulation, command buffer starvation, and rapid Jetsam terminations caused by dangling ComposeUIViewController references in shared Kotlin UI runtimes. Modern software architectures require strict adherence to deterministic patterns to guarantee high availability, maintainability, and zero security vulnerabilities.

Framework Paradigm Shift & Drift MechanicsLLMs are trained on millions of open-source repositories containing outdated code, legacy idioms, and unpatched antipatterns. When coding assistants generate implementations for this stack, they default to common statistical tokens: Retaining CAMetalLayer Buffers and Skiko Render Loops Across iOS Navigation Transitions Without an explicit system instruction file, developers spend hours debugging silent failures.
Failure Modes & Production Blast Radius

Hazard Analysis: What Breaks Without This Rule

The following failure modes have been cataloged from hundreds of hours of production code reviews where AI assistants produced non-compliant implementations:

Failure ModeAI Symptom (The Anti-Pattern)SeverityProduction Impact & Remedy
AI Hallucination & Deprecation DriftRetaining CAMetalLayer Buffers and Skiko Render Loops Across iOS Navigation TransitionsCritical
Violating this industrial standard introduces regression bugs, security risks, and unexpected runtime failures in production.
Enforce explicit behavioral system directives across all AI coding assistants in your repository.
Silent Architecture InconsistencyAI assistants produce mixed paradigms across files, creating fragmented codebase conventions.Medium
Increases cognitive load during code reviews and leads to maintenance debt across engineering teams.
Pin project conventions in .cursor/rules, CLAUDE.md, and AGENTS.md so all team agents follow identical guardrails.
Architectural Directives in Detail

Step-by-Step Production Directives

Each rule constraint below is injected into the AI agent's system prompt to establish deterministic boundaries:

1

Engineers MUST ALWAYS implement deterministic teardown hooks in UIViewControllerRepresentable via dismantleUIViewController when hosting ComposeUIViewController in SwiftUI.

Engineers MUST ALWAYS implement deterministic teardown hooks in UIViewControllerRepresentable via dismantleUIViewController when hosting ComposeUIViewController in SwiftUI.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
2

Engineers MUST explicitly detach or invalidate the underlying SkikoUIView render delegate when a Compose view controller is popped from a UINavigationController stack.

Engineers MUST explicitly detach or invalidate the underlying SkikoUIView render delegate when a Compose view controller is popped from a UINavigationController stack.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
3

Engineers MUST NOT allow circular strong references between platform UIKit delegates, CADisplayLink wrappers, and Compose State containers across the Kotlin/Native boundary.

Engineers MUST NOT allow circular strong references between platform UIKit delegates, CADisplayLink wrappers, and Compose State containers across the Kotlin/Native boundary.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
4

Code MUST release and flush Skia GPU context resource caches (e.g., purgeSkiaFontCache, purgeGpuResources) during memory pressure warnings (applicationDidReceiveMemoryWarning).

Code MUST release and flush Skia GPU context resource caches (e.g., purgeSkiaFontCache, purgeGpuResources) during memory pressure warnings (applicationDidReceiveMemoryWarning).

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
5

Components NEVER assume ARC will promptly collect CAMetalLayer drawable pools if ComposeUIViewController remains linked in a detached or hidden UINavigationController stack segment.

Components NEVER assume ARC will promptly collect CAMetalLayer drawable pools if ComposeUIViewController remains linked in a detached or hidden UINavigationController stack segment.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
6

Services MUST route navigation lifecycle events into the Compose runtime to pause recomposition and frame dispatch whenever viewDidDisappear is invoked with isMovingFromParent or isBeingDismissed.

Services MUST route navigation lifecycle events into the Compose runtime to pause recomposition and frame dispatch whenever viewDidDisappear is invoked with isMovingFromParent or isBeingDismissed.

Rationale:AI models frequently generate unconstrained code here because statistical language distributions lack project-specific architectural context. Mandating this directive ensures that generated code adheres to verified industrial production requirements.
Enforcement:Integrated into the Agent Pack rule file. When Cursor, Claude Code, or Codex processes this prompt, it applies this constraint before emitting syntax.
Technical Questions & Answers

Frequently Asked Technical Questions

Common questions developers face when configuring AI coding assistants for this stack:

Q:Why is KMP Compose Skiko Metal Surface Lifecycle & Native GPU Buffer Disposal critical for AI coding assistants?

Enforces deterministic teardown of CAMetalLayer render surfaces and Skiko rendering contexts during iOS navigation transitions. Prevents unmanaged native GPU memory accumulation, command buffer starvation, and rapid Jetsam terminations caused by dangling ComposeUIViewController references in shared Kotlin UI runtimes. Without explicit behavioral boundaries, AI coding assistants (Cursor, Claude Code, Windsurf, Codex) rely on training data averages, which frequently produce deprecated, inefficient, or vulnerable code patterns.

Q:How does adopting this rule prevent AI hallucinations?

This rule explicitly declares what the AI must avoid (Retaining CAMetalLayer Buffers and Skiko Render Loops Across iOS Navigation Transitions) and prescribes verified positive examples. Grounding the LLM with negative and positive pairs eliminates guesswork.

Q:Which AI coding environments are supported?

This specification is multi-platform compatible: Cursor (.cursor/rules/*.mdc & .cursorrules), Anthropic Claude Code (CLAUDE.md), Windsurf (.windsurfrules), Codex / Cline (AGENTS.md), and GitHub Copilot (.github/copilot-instructions.md).

Q:Can I combine this rule with other technology stacks?

Yes. Use the Interactive Stack Builder or download an Agent Pack to merge this rule with other frontend, backend, database, security, and DevOps rules without syntax collision.

Open Specification · CC BY 4.0·Free for Personal, Team & Commercial In-Repo Use

Citation & Open Source Attribution Guidelines

This specification is licensed under Creative Commons Attribution 4.0 International (CC BY 4.0). You are completely free to adapt and share this rule in your workspace. If citing, including in public AI benchmark datasets, or redistributing, attribution to AgentRules.org is required.

Related MOBILE Rules

View all rules →
1 Rule in Stack

KMP