mcpField
M
macos-notch-ui

macos-notch-ui

github.com/fayazara设计与 UI685571 次安装22 周前更新
代码仓库

关于 macos-notch-ui

macos-notch-ui 是一项为 MacBook 刘海实现 Dynamic Island 风格浮层的开发技能,面向构建 macOS 界面的开发者。它提供 SwiftUI 形状和 NSPanel 窗口方案,并说明定位、动画及无刘海屏幕的回退设计。

主要功能

  1. 01提供 SwiftUI 刘海轮廓形状
  2. 02通过 NSPanel 创建透明置顶浮层
  3. 03对齐屏幕顶部与硬件刘海
  4. 04支持形状尺寸动画与显示隐藏
  5. 05检测无刘海屏幕并回退显示

适用场景

  1. 01在刘海区域显示状态信息
  2. 02显示录音指示或简短文本
  3. 03为浮层实现展开与收起动画
  4. 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(灵动岛风格)

此技能会创建一个灵动岛风格的浮层,从 MacBook 的硬件刘海处延伸出来。该浮层是一个透明的悬浮面板,与屏幕顶部齐平,并使用自定义形状;其顶部带有凹形“耳朵”曲线,可与实体刘海开孔无缝融合。

架构

实现分为 3 个部分:

  1. NotchWindow(NSPanel 子类)——一个无边框、透明、可穿透点击的面板,位于 CGShieldingWindowLevel 层级,显示在所有内容之上,包括菜单栏
  2. NotchShape(SwiftUI Shape)——绘制灵动岛轮廓,顶部角采用凹形二次贝塞尔曲线,底部采用凸形圆角
  3. 你的内容视图——你想在刘海区域中显示的任何内容(状态指示器、波形、文本、图标)

工作原理

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

必须使用 screen.frame(而不是 screen.visibleFrame)——visibleFrame 不包含刘海所在的菜单栏区域。

参考文件

  • references/NotchWindow.swift——可直接使用的 NSPanel 子类,支持显示、隐藏和定位
  • references/NotchShape.swift——灵动岛形状,支持可动画的圆角半径

分步集成

1. 添加 NotchShape

复制 references/NotchShape.swift。这是一个 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()

弹簧动画编排

灵动岛效果来自特定的动画序列:

显示:

  1. 窗口立即出现(orderFront)
  2. 在下一个运行循环周期,将 isVisible 切换为 true
  3. 宽度通过 .spring(response: 0.35, dampingFraction: 0.75) 从 0 动画到目标宽度

采用两步方式(先立即显示窗口,再为内容添加动画)是必要的,因为 SwiftUI 需要先将视图加入视图层级,才能对其执行动画。

隐藏(3 步编排):

  1. 清除所有展开的内容(文本、详情)——收起为紧凑形状
  2. 0.25s 后,将 isVisible 设为 false——触发弹簧动画,将宽度收缩至 0
  3. 0.65s 后,移除窗口(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 窗口级别,在屏幕底部显示悬浮胶囊作为回退方案。

设计指南

  • 用纯黑色填充形状——这样才能让它与硬件刘海融为一体
  • 在黑色背景上使用白色或彩色文字/图标,以形成对比
  • 保持内容简洁——刘海区域很小。最适合使用带图标和简短文字的单行布局
  • 录制指示器使用红色——符合 iOS 惯例
  • 对宽度而非不透明度进行动画处理——灵动岛效果的关键在于形状的扩展和收缩,而非淡入淡出

评论

支持 Markdown