Qualities of great iPad and iPhone apps on Macs with M1
Design 入门 20m

M1 Mac 上优秀 iPad 和 iPhone app 的品质

Qualities of great iPad and iPhone apps on Macs with M1

2021年6月9日

在 Apple 官方观看视频

一句话判断

你的 iPad app 已经可以在 M1 Mac 上运行了,但「能运行」和「好用」之间差着一个窗口适配、键盘导航和鼠标交互——这场 Session 就是帮你跨过这个鸿沟的。

这场 Session 讲了什么

M1 芯片的 Mac 可以直接运行 iPad 和 iPhone app,这为 iOS 开发者打开了 Mac 用户群。但很多 app 在 Mac 上运行时体验不佳,因为它们没有针对 Mac 的输入方式和窗口管理做适配。

Session 从三个维度讲解了如何让 iOS app 在 M1 Mac 上表现更好。

窗口适配:iOS app 在 Mac 上运行时会自动获得窗口调整能力,但很多 app 的布局没有处理超大宽度的场景。Auto Layout 应该正确处理从 iPhone 竖屏到 Mac 全屏窗口的尺寸范围。

键盘导航:Mac 用户期望 Tab 键可以在控件之间跳转,每个按钮都应该可以用键盘激活。iOS app 通常只考虑了触摸交互,需要检查所有可交互元素的 isAccessibilityElement 和 accessibility traits。

鼠标和触控板:iOS 的触摸事件会被自动映射为鼠标点击,但悬停(Hover)和右键点击需要额外处理。UIHoverGestureRecognizerUIContextMenuInteraction 可以让 app 更好地支持鼠标操作。

值得深挖的点

窗口尺寸的极端情况

M1 Mac 的外接显示器可以让窗口宽度达到 5120pt。如果你的 app 使用了固定宽度或者最大宽度限制,在超宽窗口下会出现大量空白。建议用 viewIsAppearing 或者 viewDidLayoutSubviews 来动态调整布局列数和间距。

触控板手势的映射

iOS 的滑动手势在 Mac 触控板上可以正常工作,但需要调整灵敏度。Mac 触控板的惯性滚动比 iPhone 屏幕更灵敏,UIScrollViewdecelerationRate 应该设置为 .normal 而不是 .fast

代码片段

// 为 macOS 优化的自适应布局
class AdaptiveGridViewController: UIViewController {
    var collectionView: UICollectionView!
    var columnCount: Int = 2
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        updateColumnCount()
    }
    
    func updateColumnCount() {
        let width = view.bounds.width
        let minWidth: CGFloat = 320
        
        // 根据窗口宽度动态计算列数
        let newColumnCount = max(1, Int(width / minWidth))
        if newColumnCount != columnCount {
            columnCount = newColumnCount
            // 重新加载 collectionView 布局
            if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
                let itemWidth = (width - CGFloat(columnCount + 1) * layout.minimumInteritemSpacing) / CGFloat(columnCount)
                layout.itemSize = CGSize(width: itemWidth, height: itemWidth * 1.2)
            }
        }
    }
}
// 添加鼠标悬停效果
class HoverableCollectionViewCell: UICollectionViewCell {
    var isHovered = false
    
    override func awakeFromNib() {
        super.awakeFromNib()
        setupHoverGesture()
    }
    
    func setupHoverGesture() {
        let hoverGesture = UIHoverGestureRecognizer(
            target: self,
            action: #selector(handleHover(_:))
        )
        addGestureRecognizer(hoverGesture)
    }
    
    @objc func handleHover(_ gesture: UIHoverGestureRecognizer) {
        switch gesture.state {
        case .began, .changed:
            isHovered = true
            // 鼠标悬停时高亮
            layer.backgroundColor = UIColor.systemGray5.cgColor
        case .ended, .cancelled:
            isHovered = false
            layer.backgroundColor = nil
        default:
            break
        }
    }
}
// 检测运行环境并做平台特定适配
func configureForPlatform() {
    #if targetEnvironment(macCatalyst)
    // Mac 特定调整
    adjustForKeyboard()
    adjustForMouse()
    #else
    // iOS 保持原样
    adjustForTouch()
    #endif
}

最佳实践

不要假设 UI_USER_INTERFACE_IDIOM() 总是返回 .pad。在 M1 Mac 上,这个值可能返回 .mac(取决于运行模式)。使用 ProcessInfo.processInfo.isMacCatalystApp 来检测是否在 Mac 上运行。

如果你的 app 使用了 touchesBegan 等触摸事件方法,在 Mac 上它们仍然会被触发(通过鼠标点击映射)。但悬停、右键等 Mac 特有的交互需要额外的手势识别器。

还有什么值得关注

  • iOS app 在 M1 Mac 上运行时,UISceneactivationState 会跟随窗口的焦点状态变化。
  • UIContextMenuInteraction 在 Mac 上会自动映射为标准的右键菜单。
  • UIDragInteractionUIDropInteraction 在 Mac 上支持跨 app 拖拽。
WWDC 2021