macos-notch-ui について
macos-notch-uiは、MacBookのノッチに沿うDynamic Island風オーバーレイを作る開発スキルです。SwiftUIのシェイプとNSPanelを使う方法に加え、配置、アニメーション、ノッチのない画面での代替表示を説明します。
主な機能
- 01SwiftUIによるノッチ形状
- 02NSPanelによる透明オーバーレイ
- 03画面上端とノッチへの位置合わせ
- 04形状サイズと表示状態のアニメーション
- 05ノッチのない画面を検出して代替表示
活用シーン
- 01ノッチ領域へのステータス表示
- 02録音インジケーターや短いテキストの表示
- 03オーバーレイの展開・収縮アニメーション
- 04外部ディスプレイでの代替表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ fayazara/ macos-app-skills --skill macos-notch-uiインストール後、Claude Code で/macos-notch-uiを入力して読み込まれたか確認します。
同梱ファイルreferences/NotchShape.swiftreferences/NotchWindow.swift
詳細
macOS ノッチ UI(Dynamic Island スタイル)
このスキルは、MacBook のハードウェアノッチから広がる Dynamic Island スタイルのオーバーレイを作成します。オーバーレイは画面上端にぴったりと配置される、透明なフローティングパネルです。物理的なノッチの切り欠きに自然になじむ、凹状の「耳」のカーブを持つカスタムシェイプを使用します。
アーキテクチャ
実装は 3 つの部分で構成されます。
NotchWindow(NSPanel サブクラス) --CGShieldingWindowLevelに配置され、メニューバーを含むすべての要素の手前に表示される、境界線なし、透明、クリック透過のパネルNotchShape(SwiftUI Shape) -- 上部の角に凹状の二次ベジェ曲線、下部に凸状の丸角を使用して Dynamic Island のシルエットを描画します- コンテンツビュー -- ノッチ内に表示するもの(ステータスインジケーター、波形、テキスト、アイコンなど)
仕組み
MacBook のノッチは、画面上部中央にある黒い長方形です。黒く塗りつぶした NotchShape を最高のウィンドウレベルでその正確な位置に配置すると、ノッチの領域が視覚的に広がります。シェイプ内のコンテンツは、ハードウェアノッチから「現れる」ように見えます。
位置を決める重要な計算式:
// Use full screen frame (not visibleFrame) to include the menu bar / notch area
let screen = NSScreen.main!
let x = screen.frame.origin.x + (screen.frame.width - totalWidth) / 2
let y = screen.frame.origin.y + screen.frame.height - totalHeight
ノッチがあるメニューバー領域を visibleFrame は除外するため、screen.visibleFrame ではなく screen.frame を使用することが重要です。
参考ファイル
references/NotchWindow.swift-- 表示/非表示と位置指定に対応した、そのまま使える NSPanel サブクラスreferences/NotchShape.swift-- アニメーション可能な角の半径を備えた Dynamic Island シェイプ
統合手順
1. NotchShape を追加する
references/NotchShape.swift をコピーします。これは、設定可能な角の半径を 2 つ持つ SwiftUI の Shape です。
topCornerRadius(デフォルト 10)-- ハードウェアノッチの逆向きの角を模した、上部の凹状の「耳」のカーブbottomCornerRadius(デフォルト 16)-- 下部の標準的な凸状の丸角
どちらも animatableData でアニメーション可能なため、SwiftUI はシェイプの変化を滑らかに補間できます。
2. コンテンツビューを作成する
ノッチ内に表示するものを作成します。コンテンツは NotchShape でクリップし、背景を黒で塗りつぶしてください。
struct NotchContentView: View {
var isVisible: Bool
var body: some View {
HStack {
Image(systemName: "mic.fill")
.foregroundStyle(.red)
Text("Recording")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white)
}
.frame(width: isVisible ? 200 : 0)
.frame(height: 32)
.background(NotchShape().fill(.black))
.clipShape(NotchShape())
.animation(.spring(response: 0.35, dampingFraction: 0.75), value: isVisible)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
3. NotchWindow を追加する
references/NotchWindow.swift をコピーします。これは、次の機能を持つ汎用 NSPanel です。
- 境界線なし、透明、非アクティブ化のパネルを作成します
CGShieldingWindowLevel(すべての要素の手前)に配置します- 画面上部中央の、上端にぴったりの位置に配置します
- マウスイベントを無視します(完全にクリック透過)
- すべてのスペースに表示され、フルスクリーンでも維持されます
4. 表示と非表示
// Create once and reuse
let notchWindow = NotchWindow()
// Show with your content
let content = NotchContentView(isVisible: true)
notchWindow.showNotch(content: content)
// Hide with spring collapse animation
notchWindow.hideNotch()
スプリングアニメーションの構成
Dynamic Island の効果は、特定のアニメーションシーケンスによって生まれます。
表示:
- ウィンドウを即座に表示します(
orderFront) - 次の実行ループのタイミングで、
isVisibleをtrueに切り替えます - 幅を
.spring(response: 0.35, dampingFraction: 0.75)で 0 から目標の幅までアニメーションさせます
この 2 段階の方法(ウィンドウを即座に表示してからコンテンツをアニメーションさせる)は、SwiftUI がアニメーションを実行する前にビューを階層に追加しておく必要があるため不可欠です。
非表示(3 段階の構成):
- 展開されたコンテンツ(テキスト、詳細情報)をクリアし、コンパクトなシェイプに折りたたみます
- 0.25 秒後に
isVisible = falseを設定し、幅を 0 に折りたたむスプリングアニメーションを開始します - 0.65 秒後にウィンドウを削除します(
orderOut)
各アニメーションが次のアニメーションの開始前に完了するよう、遅延時間を調整しています。これにより、ノッチに滑らかに「縮んで戻る」効果が生まれます。
// Step 1: collapse content
state.isExpanded = false
// Step 2: shrink width
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
state.isVisible = false
}
// Step 3: remove window
DispatchQueue.main.asyncAfter(deadline: .now() + 0.65) {
window.orderOut(nil)
}
画面ジオメトリの詳細
| プロパティ | 値 | 理由 |
|---|---|---|
| ウィンドウレベル | CGShieldingWindowLevel() | メニューバーを含むすべての要素の手前 |
| 位置の原点 | NSScreen.main.frame(visibleFrame ではない) | ノッチ/メニューバー領域を含める必要があります |
| 水平方向 | 中央揃え:(screen.width - totalWidth) / 2 | ハードウェアノッチと位置を合わせます |
| 垂直方向 | 上端にぴったり:screen.height - totalHeight | 上端が画面の端に接します |
| コレクション動作 | .stationary, .canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle | スペースとともに移動せず、フルスクリーンでも維持され、Cmd+Tab には表示されません |
ノッチがない Mac 向けのフォールバック
すべての Mac にノッチがあるわけではありません(外部ディスプレイや旧型の MacBook など)。次のように検出できます。
var hasNotch: Bool {
guard let screen = NSScreen.main else { return false }
// Notch Macs have a safe area inset at the top
return screen.safeAreaInsets.top > 0
}
ノッチがないディスプレイでは、screen.visibleFrame と標準の .floating ウィンドウレベルを使い、画面下部にフローティングピルを表示する方法に切り替えます。
デザインガイドライン
- 形状を黒一色で塗りつぶす — ハードウェアのノッチになじませるポイントです
- 黒い背景に白またはカラーのテキストやアイコンを使うと、コントラストが際立ちます
- コンテンツはコンパクトに保つ — ノッチの領域は小さいため、アイコンと短いテキストを組み合わせた1行表示が最適です
- 録画中のインジケーターには赤を使う — iOS の慣例に合っています
- 不透明度ではなく幅をアニメーションさせる — Dynamic Island の効果は、形状を拡大・縮小させるものであり、フェードさせるものではありません