ScopedAnimation

0.2.2

Structural boundaries and DEBUG diagnostics for SwiftUI animation
9uiLe/swift-scoped-animation

What's New

0.2.2

2026-09-16T01:11:32Z

変更

  • 日本語を基本言語とし、文書・API コメント・診断・サンプル・開発テンプレートを統一。
    README・貢献ガイド・セキュリティ方針・行動規範には相互リンク付きの英語版を追加。
    用語、公開契約、文書の責務、検証・公開手順を、会話や過去の PR を前提としない構成で定義。
  • リリース準備で日本語・英語 README の導入バージョンを同時に更新し、公開前に両方の一致を検証。
  • バージョンを記載した文書の PR でリリースを準備し、対象 master コミットがパッケージと
    リリースツールの CI に成功した後、所有者のローカル認証で公開。
  • 注釈付き vX.Y.Z タグと、ソースのみの不変な GitHub Release を公開し、中断からの再開時も状態を検証。
  • 文書更新も含めて master のすべての push で CI を実行し、チェックアウトした認証情報の永続化を無効化。
  • 新規参加者向けに製品契約、構造、性能モデル、検証手順を文書化。
  • トリガー履歴を 1 回の比較で解決し、DEBUG 警告の整形をデバウンス判定後まで遅延。
  • 解決、生成、高価な値比較、抑制された競合警告を測る DEBUG/RELEASE マイクロベンチマークを追加。
  • スコープ構成、値の解決、トランザクション境界を分離し、スコープとバリアで除去・復元の実装を共有。
  • 値とアニメーション設定を同じスナップショットに保持し、選択したインデックスとアニメーションを組にする。
    不採用トリガーの記憶領域は RELEASE から除去。
  • トリガー履歴をメインアクターに隔離し、診断箇所のキーを一元化。
  • 厳格な lint により、フォーマット警告でも CI とリリース検証を失敗させる。
  • 振る舞いテストを契約ごとに直列の Swift Testing スイートへ整理。
    ホストウィンドウの保持・終了、空の観測の拒否、アニメーション値の直接比較を実施。
  • 連続更新、アニメーション無効時、挿入・削除、要素数・型の変更、ルート検出器の暗黙アニメーションの死角を検証。

修正

  • 型消去後も具象型を区別し、相互にキャスト可能な Int と Optional<Int> も別の値として扱う。
  • リスト検証の重複実行を防ぎ、画面を離れた場合は途中結果を確定せず保留中の処理をキャンセル。
  • 装飾用の DEBUG 境界線をアクセシビリティの移動対象から除外。

ScopedAnimation

日本語 | English

アニメーションを所有する領域を、SwiftUI のビュー構造で宣言するライブラリです。

CI 対応 Swift バージョン 対応プラットフォーム MIT ライセンス

名前付きスコープの境界を表示する DEBUG オーバーレイ

基本モデル

SwiftUI は、状態変更に伴うアニメーションを Transaction でビューへ伝えます。 ScopedAnimation は、その経路に境界を置きます。

  • スコープは外からのアニメーションを取り除き、値の変更や明示的な操作に応じて自身のアニメーションを与えます。
  • バリアは外からのアニメーションを取り除きます。自身ではアニメーションを与えません。
  • スタンプはアニメーションの所有者を示す内部情報です。DEBUG 診断は、観測点を通るスタンプのないアニメーションを報告します。

状態を読むビューは、スコープの内外を問わず更新されます。プロキシによる明示的な操作のトランザクションも、 境界のない領域へ届きます。外からのアニメーションを拒否する領域には、スコープかバリアを配置してください。

導入

  • iOS 17+、macOS 14+、tvOS 17+、watchOS 10+、visionOS 1+
  • Swift 6 言語モード、Swift tools 6.2+
  • Swift Package Manager、外部依存なし

Xcode に次のパッケージ URL を追加します。

https://github.com/9uiLe/swift-scoped-animation.git

Package.swift では依存を宣言し、対象ターゲットに ScopedAnimation プロダクトを追加します。

.package(url: "https://github.com/9uiLe/swift-scoped-animation.git", from: "0.2.2")

値の変更でアニメーションさせる

AnimationScope で囲んだ内容が、監視する値の変更に応じて動きます。

import ScopedAnimation
import SwiftUI

struct ExpandableCard: View {
    @State private var isExpanded = false

    var body: some View {
        VStack {
            Button("詳細を切り替え") {
                isExpanded.toggle()
            }

            AnimationScope(.spring(duration: 0.3), value: isExpanded, name: "Card") {
                VStack(alignment: .leading) {
                    Text("売上")
                    if isExpanded {
                        Text("月次の詳細")
                            .transition(.opacity)
                    }
                }
            }
        }
    }
}

初回表示では比較の基準を設定します。isExpanded が変わると、スコープが指定したアニメーションを内容へ渡します。 祖先のアニメーションは境界で取り除かれます。

同じ領域に複数の条件がある場合

値とアニメーションの組を、優先する順に並べます。

AnimationScope(
    name: "Board",
    triggers: [
        .animation(.easeOut(duration: 0.12), value: selectedPoints),
        .animation(.spring(response: 0.35, dampingFraction: 0.7), value: hintPoints),
    ]
) {
    BoardView()
}

同時に複数の値が変わると、先頭に最も近い変更済みトリガーのアニメーションをサブツリー全体に使います。 DEBUG では不採用の変更も multiTriggerConflict として報告します。

比較は配列位置ごとに行い、具象型と値が両方等しければ変更なしとします。名前やアニメーション設定だけでは動きません。 要素数の変更は、内容の同一性とローカル状態を保ったまま、アニメーションなしで比較基準を設定し直します。 並べ替えは新しい位置での比較になります。意図した構成変更でなければ、要素数と順序を一定に保ってください。

明示的な操作でアニメーションさせる

内容のクロージャで受け取るプロキシの animate に、同期的な状態変更を渡します。

AnimationScope(.snappy, name: "Disclosure") { scope in
    VStack {
        Button("切り替え") {
            scope.animate {
                isOpen.toggle()
            }
        }
        DisclosureContent(isOpen: isOpen)
    }
}

scope.animate(.spring(duration: 0.4)) { ... } は、その操作に使うアニメーションを指定します。 プロキシが付けたスタンプは祖先の境界を通過し、所有者の ID が一致するスコープでアニメーションを復元します。

境界を配置する

目的 構成
異なる表示レイヤーを独立して動かす 兄弟スコープ
同じサブツリーの複数条件に優先順位を付ける 1 スコープの複数トリガー
子孫に独立したアニメーションの所有者を置く 入れ子のスコープ
外からのアニメーションを取り除く animationBarrier()
DEBUG に名前付き境界を表示し、値駆動アニメーションは与えない AnimationScope(name:triggers:content:) に空配列

入れ子のスコープは祖先のアニメーションを取り除き、自身のトリガーやプロキシに応じて動きます。 DEBUG の crossScopeAnimationStrip は、このスコープ間の遮断を示します。

バリアは、次のように適用します。

StatusPanel()
    .animationBarrier()

スタンプは子孫のために保持します。スタンプのないアニメーションを除去したときの DEBUG 警告は、 animationBarrier(warnsOnLeaks: false) で抑制できます。空のトリガー配列によるスコープには、このバリア警告はありません。

バリアはレイアウト領域を確保しません。固定の大きさが必要なら通常のフレーム指定を併用します。 また、境界より下の SwiftUI アニメーション修飾子は独自のアニメーションを生成できます。

DEBUG 診断

RootView()
    .detectAnimationLeaks()
    .animationScopeDebugOverlay()

検出器は、スタンプのないアニメーション付きトランザクションを報告します。オーバーレイはスコープの境界と名前を表示します。 診断実装は RELEASE ビルドから除去されます。

発生源 ルートの検出器 発生源より下流の検出器・バリア
スタンプのない withAnimation / アニメーション付き withTransaction 通過時に検出 通過時に検出
ルートより下の直接の .animation(_:value:) 観測不可 通過時に検出
スタンプ付きトランザクション リーク報告なし リーク報告なし

観測できる範囲は配置に依存します。画面ルートと調査対象のサブツリーに検出器を置き、 直接のアニメーション呼び出しも確認してください。コード検索では SwiftUI の修飾子と AnimationTrigger.animation(_:value:) ファクトリーを区別します。

性能と検証範囲

スコープは、動かすサブツリーを明示します。状態による無効化、body の評価回数、フレームレートは制御しません。 トリガーの生成と等価比較にはコストがあるため、条件を十分に表す小さな値を使ってください。 性能ガイドはアプリの計測を、 参考計測は内部の CPU コストを扱います。

トランザクション伝播は macOS / iOS のホストテストで検証します。 List 行への伝播、再利用、実際の描画はサンプル QAで環境ごとに確認します。 OS や Xcode のメジャー更新時は、互換性の前提を再検証してください。

サンプル

比較、オーバーレイ、リスト検証、複数トリガーを操作できます。サンプル UI は日本語です。 掲載 GIF は英語 UI で収録した動作例です。

直接のアニメーションとスコープの比較 List の伝播とバリアの動作確認

xcodebuild build \
  -project Examples/ScopedAnimationExample/ScopedAnimationExample.xcodeproj \
  -scheme ScopedAnimationExample \
  -destination 'platform=iOS Simulator,name=iPhone 17'

文書の案内

日本語を基本言語とし、README・貢献ガイド・セキュリティ方針・行動規範に英語版を提供します。 文書の責務と同期方法は貢献ガイドの言語方針に定義しています。

読みたいこと 文書
API の使い方 使い始める・組み合わせ方
トランザクションが届く仕組み 仕組み
製品契約・内部構造・ロードマップ 設計
開発環境・テスト・文書の管理 貢献ガイド
バージョンの準備と公開 リリース手順
実行環境・実出力・検証の限界 検証記録
報告と参加のルール セキュリティ方針・行動規範

DocC は Xcode でビルドします。

xcodebuild docbuild -scheme ScopedAnimation -destination 'generic/platform=iOS'

ライセンス

MIT

Description

  • Swift Tools 6.2.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Sep 27 2026 06:17:41 GMT-0900 (Hawaii-Aleutian Daylight Time)