M1 Mac 上优秀 iPad 和 iPhone app 的品质
Qualities of great iPad and iPhone apps on Macs with M1
2021年6月9日
一句话判断
你的 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)和右键点击需要额外处理。UIHoverGestureRecognizer 和 UIContextMenuInteraction 可以让 app 更好地支持鼠标操作。
值得深挖的点
窗口尺寸的极端情况
M1 Mac 的外接显示器可以让窗口宽度达到 5120pt。如果你的 app 使用了固定宽度或者最大宽度限制,在超宽窗口下会出现大量空白。建议用 viewIsAppearing 或者 viewDidLayoutSubviews 来动态调整布局列数和间距。
触控板手势的映射
iOS 的滑动手势在 Mac 触控板上可以正常工作,但需要调整灵敏度。Mac 触控板的惯性滚动比 iPhone 屏幕更灵敏,UIScrollView 的 decelerationRate 应该设置为 .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 上运行时,
UIScene的activationState会跟随窗口的焦点状态变化。 UIContextMenuInteraction在 Mac 上会自动映射为标准的右键菜单。UIDragInteraction和UIDropInteraction在 Mac 上支持跨 app 拖拽。