五个 Apple 平台,三个共享文件:Return 是如何真正做到跨平台 SwiftUI 的
我的冥想计时应用 Return 运行在五个 Apple 平台上:iPhone、iPad、Mac、Apple Watch 和 Apple TV。1 代码库共有 40 个 Swift 文件(不含测试)。其中只有 3 个被五个平台共享。 其余部分拆进各自独立的 Xcode target,TimerManager、AudioManager、ContentView 这些概念是重复实现的,而不是靠 #if os(...) 条件编译共用一份。
共享率大约 7.5%,而且是刻意为之。
这篇文章要讲的是:2026 年做一个跨平台 SwiftUI 应用,真实情况到底是什么样;为什么激进的代码共享被高估了;以及那三个确实共享了的文件到底有什么共同点。
Return 所面向的五个平台,取自 Apple 在 developer.apple.com 上的呈现方式。每一个在 Xcode 里都是独立的平台 target,而不是运行时的一条分支。
一句话总结
- Return:主 target 18 个 Swift 文件(iOS + iPadOS + macOS),tvOS target 10 个,watchOS target 7 个,widget 2 个(实时活动),以及
Return/Shared/里 3 个真正跨平台的文件。合计 40 个。 - 这 3 个共享文件都跟持久化有关:
MeditationSession、SessionStore、SessionHistoryView。它们承载的是通过 iCloud 流动的状态,而不是需要适配平台的 UI。 - tvOS 和 watchOS 是独立的 Xcode target,而不是主 target 里的
#if os(tvOS)分支。这两者的操控模型差异太大,塞不进同一个 ContentView。 - 即便在 iOS/iPadOS/macOS 这一个主 target 内部,
#if os块也在不断增殖:ContentView.swift里 10 处,LiveActivityManager.swift8 处,VideoBackgroundView.swift8 处,AudioManager.swift6 处。 - 说句实话:在五个 Apple 平台之间激进地共享代码,是一笔维护负债。一个小而稳的共享内核(持久化层)加上各平台独立的 UI,比一个塞满
#if的巨型文件交付更快、出错更少。
想看各平台的专题文章,请参阅Apple 平台对照矩阵、watchOS 执行环境的约定与Liquid Glass 的 SwiftUI 模式。
数字
剔除测试和 UI 测试之后,按 Swift 文件数看这个代码库的形状:
Return/ 18 files (iPhone + iPad + Mac, single target)
├── Shared/ 3 files ← cross-platform truth
│ ├── MeditationSession.swift
│ ├── SessionStore.swift
│ └── SessionHistoryView.swift
├── ContentView.swift (10 #if os branches)
├── TimerManager.swift (2 #if os branches)
├── AudioManager.swift (6 #if os branches)
├── HealthKitManager.swift
├── LiveActivityManager.swift (8 #if os branches, iOS-only)
├── ThemeManager.swift
├── VideoBackgroundView.swift (8 #if os branches)
├── GlassTextShape.swift (Liquid Glass, see prior post)
├── GlassTimerText.swift
└── … (settings, theme, audio assets, etc.)
ReturnTV/ 10 files (tvOS, separate target)
├── TVContentView.swift
├── TVTimerManager.swift ← duplicates main TimerManager
├── TVAudioManager.swift ← duplicates main AudioManager
├── TVDurationPicker.swift
├── TVFocusModifier.swift ← tvOS button styles for focus
├── TVSettingsView.swift
└── …
ReturnWatch Watch App/ 7 files (watchOS, separate target)
├── WatchContentView.swift
├── WatchTimerManager.swift ← duplicates main TimerManager
├── WatchAudioManager.swift ← duplicates main AudioManager
├── WatchHealthKitManager.swift ← duplicates main HealthKitManager (mostly)
├── WatchSettingsView.swift
└── …
ReturnWidgets/ 2 files (Live Activity + bundle)
├── ReturnLiveActivity.swift
└── ReturnWidgetsBundle.swift
五个平台,三个共享文件,两个独立的平台专属 target 外加一个 widget target,主 target 内部还有大量条件编译。共享比例大约 7.5%。多数“多平台 SwiftUI”教程给出的建议正好相反:写一个 ContentView,通过 @Environment(\.horizontalSizeClass) 和 #if os(...) 去适配每一个平台。2 这套做法在两个平台(iPhone + iPad)上行得通,到了五个平台就崩了。
那三个共享文件的共同点
Return/Shared/MeditationSession.swift 定义了与 SwiftData 相邻的值类型:3
struct MeditationSession: Codable, Identifiable, Equatable {
let id: UUID
let startDate: Date
let endDate: Date
let durationSeconds: Int
let sourceDevice: DeviceType
var syncedToHealthKit: Bool
enum DeviceType: String, Codable, CaseIterable {
case iPhone, iPad, mac, appleTV, appleWatch
}
}
文件开头那行注释起着承重作用:// Add this file to: Return, ReturnTV, ReturnWatch Watch App targets. 同一个源文件被三个 Xcode target 引用,既没有做符号链接,也没有塞进 Swift package。Apple 的构建系统很乐意把一个文件编译进三个二进制产物。
SessionStore.swift 是持久化层:对 NSUbiquitousKeyValueStore(Apple 的 iCloud 键值存储)的一层薄封装,负责读写 MeditationSession 数组。这个选择很关键:键值存储的同步让 Return 不必配置 CloudKit 容器就能拿到跨设备的历史记录,代价是整个存储上限只有 1 MB。12 而每条冥想记录平均不过几百字节,这个上限绰绰有余。SessionHistoryView.swift 是一个把这些记录渲染出来的 SwiftUI 列表。这两者在 iPhone、iPad、Mac、Watch 和 TV 的 target 里用法完全一致。
这三个文件的共同点是:它们描述的是状态,不是交互。 MeditationSession 在每台设备上都是同一个概念。过往记录列表在每台设备上的读法也一样。它们都不涉及操控界面、窗口管理器、音频路由决策、焦点引擎或数码表冠。一个文件一旦需要知道自己跑在什么平台上,它就不再具备共享的资格。
其余部分为什么没共享
以 TimerManager 为例。iOS/iPadOS/macOS 版本用 Timer.publish(every: 1, ...),通知走 UserNotifications。tvOS 版本(TVTimerManager)要处理用户用 Siri Remote 暂停之后屏保介入的情况。watchOS 版本(WatchTimerManager)把工作委托给 WKExtendedRuntimeSession(经由 WatchSessionManager),好让系统在息屏期间仍保持应用响应,输入也走数码表冠而非触摸。三个平台,三套差异极深的计时行为。
你当然可以把它们统一成 class TimerManager { #if os(watchOS) ... #elif os(tvOS) ... }。结果就是一个有三种模式的类,每种模式四十行被 #if 圈起来的代码,动一下 iOS 那条路径就有可能弄坏 watchOS 那条。这是维护上的噩梦。
三个独立的类、三个文件名,占的磁盘更多,占的脑子更少。看得懂的重复,胜过看不懂的抽象。
同样的逻辑也适用于:
ContentView与TVContentView、WatchContentView:导航模型不同(iPhone 是压栈式,TV 是焦点式,Watch 是列表式)。AudioManager与TVAudioManager、WatchAudioManager:音频会话类别不同,watchOS 的后台音频规则更严,tvOS 到 AirPlay 的路由方式也不一样。VideoBackgroundView在主 target 里有 8 处#if os(iOS)分支(外加一个#elseif os(macOS)配套分支),覆盖不同的视频素材(fire_phone.mp4对fire_mac.mp4)、不同的图层类型和不同的宽高比。4
有一点要说明:主 Return/ target 确实把 iOS、iPadOS 和 macOS 捆在了一起。这三个平台之间共享的代码多于不共享的。SwiftUI 的 NavigationStack 三个平台都能用,.glassEffect() 也是。窗口管理上的差异真实存在,但在一个 target 内部完全处理得了。tvOS 和 watchOS 才是我划下独立 target 这条线的地方。
tvOS 这一例:焦点引擎为什么逼出一个独立 target
Apple TV 的导航是围绕焦点引擎建立的。5 每一个用户可交互的 UI 元素都要声明自己可获得焦点;Siri Remote 上的方向操作让焦点在元素之间移动;按下选择键则激活当前获得焦点的元素。tvOS 上的 SwiftUI 通过 .focusable()、.focusEffect 以及响应 @Environment(\.isFocused) 的自定义 ButtonStyle 类型把这套机制暴露出来——Apple 自家应用那种视差倾斜效果就是这么做的。TVFocusModifier.swift 里的真实生产代码:6
struct TVCapsuleButtonStyle: ButtonStyle {
var accentColor: Color = .white
@Environment(\.isFocused) private var isFocused
func makeBody(configuration: Configuration) -> some View {
configuration.label
.colorMultiply(isFocused ? focusedTextColor : accentColor)
.background(
Capsule().fill(isFocused
? AnyShapeStyle(accentColor)
: AnyShapeStyle(.ultraThinMaterial))
)
.clipShape(Capsule())
.scaleEffect(isFocused ? 1.1 : 1.0)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.shadow(color: .black.opacity(isFocused ? 0.3 : 0.1),
radius: isFocused ? 20 : 5, y: isFocused ? 10 : 2)
.animation(.easeInOut(duration: 0.2), value: isFocused)
}
}
同一个文件还定义了用于方形/圆形控件的 TVCircleButtonStyle。两种样式都会在获得焦点时反转颜色与透明度:未获焦点的按钮落在 .ultraThinMaterial 上,获得焦点后填充强调色,并加大缩放与阴影。就这个应用而言,这套模式在结构上是 tvOS 专属的。@Environment(\.isFocused) 在 iOS、iPadOS、macOS、watchOS 和 tvOS 上都可用,13 但只有在 tvOS 上,焦点驱动的导航才是首要的交互模型——Siri Remote 既不产生指针事件,也不产生触摸事件。在 iPhone 或 iPad 上,同类控件靠点击命中测试;在 Mac 上则是悬停或点按。TVFocusModifier.swift 里的按钮样式默认焦点就是用户的主要可供性,并围绕它设计了全部视觉反馈。想在一个地方写出一个既处理 iOS 触摸、又处理 Mac 悬停、还处理 tvOS 焦点导航的 ContentView,没有什么好办法。视图结构本身就不一样:tvOS 的 ContentView 是一张由可获焦点的行构成的图,iOS 的 ContentView 则是一摞点击即执行的堆栈。
时长选择器也是同理。在 iPhone 上,它从底部滑出,接受点击。在 Apple TV 上,它是一横排可获焦点的单元格,用户用遥控器在其中移动。TVDurationPicker.swift 之所以自成一个文件,是因为这种基于单元格的焦点设计在 iPhone 上根本没有对应物。硬把两者塞进一个文件,无非是用 #if os(tvOS) 把两套毫不相干的 UI 粘在一起。
watchOS 这一例:扩展执行环境会话、HealthKit,以及更小的表面
watchOS 多出两条其他平台没有的结构性约束:
WKExtendedRuntimeSession,用来在手表息屏期间保持应用响应。8 没有它,watchOS 会在每一秒的滴答之间激进地挂起应用,计时随之漂移。Return 在 watchOS target 的Info.plist里声明了WKBackgroundModes: mindfulness,好让系统识别这一使用场景并授予相应的运行预算;会话本身则用默认的WKExtendedRuntimeSession()初始化方法创建。- 通过
NSUbiquitousKeyValueStore做 iCloud 同步,而不是 WatchConnectivity。7 Return 的历史记录同步跑在与 iPhone、iPad、Mac target 相同的键值存储上,因此在手表上完成的一次冥想,无需任何手表到手机的直接通信就会出现在 iPhone 的历史视图里。WatchConnectivity 未来或许可以用于实时状态同步,但 Return 选了更简单的模型:每台设备都往同一个 iCloud 键值存储里写,任何一台设备下一次读取时看到的就是并集。
WatchTimerManager.swift 是手表端的计时器,它把扩展执行环境相关的工作委托给 WatchSessionManager——后者定义在 ReturnWatchApp.swift 中,签名为 final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate。iOS 的 TimerManager 没有对应物,因为 iOS 应用在前台无需显式的执行环境会话就能保持响应。用 #if os(watchOS) 把手表逻辑塞进 iOS 的 TimerManager,只会让 iOS 那条路径导入自己永远用不到的 WatchKit 符号,而 watchOS 那条路径又需要 iOS 路径不需要的初始化流程。
WatchHealthKitManager.swift 是主 HealthKitManager 的一个精简版本。记录正念分钟数的方式相同,但授权提示的 UX 不同(手表上无法展示 HealthKitPermissionSheet)。手表版这个类的体量大约是主版本的一半。
iOS/iPadOS/macOS 主 target 内部发生了什么
即便在主 target 之内,共享也不是自动发生的。ContentView.swift 里有十处 #if os(macOS) 或 #if !os(macOS) 块;LiveActivityManager.swift 有八处;VideoBackgroundView.swift 有八处;AudioManager.swift 有六处。实时活动是 iPhone 独有的功能,所以整个 LiveActivityManager 都包在 #if os(iOS) 里。iPhone 上的时长选择器与 iPad、Mac 上的布局不同,于是 ContentView 里存在并行的布局分支。
行之有效的规律是:小的平台差异用 #if os(...)(键盘行为不同、间距不同、缺少某个 API),大的结构性差异用独立 target(焦点对触摸、锻炼会话对计时器)。 我最终采用的阈值是“分支超过约 10 行”。低于这个量,条件编译没问题;高于这个量,这个文件就是在同时干两件事,而第二件事属于另一个 target。
什么时候不该五个平台全上
说点实话。
如果你的应用信息密度高,就跳过 Apple Watch。 46mm 的屏幕装不下一个 30 项的列表、一个时长选择器再加一个设置页。Return 能在 watchOS 上活下来,是因为它的核心交互只有一个按钮(开始/停止计时)。生产力应用、理财应用或富媒体应用做不到。
如果你的应用交互频繁,就跳过 Apple TV。 电视适合的是环境式体验(计时器在房间另一头的屏幕上跑着、音乐播放)。任何需要用户频繁输入的东西都是在跟这个平台对着干。Return 上 tvOS,是因为“设一个 20 分钟的计时器,然后看着屏幕上的火焰”恰恰是最贴切的环境式场景。一个笔记应用在这儿会很难受。
如果你的应用是手机优先的界面,就跳过 Mac。 SwiftUI 在 Mac 上能跑,但 NavigationStack 那套压栈模型与真正的 Mac 侧边栏一比,就显得像个玩具。如果应用在 Mac 上会显得半成品,那就上 Catalyst(它把 iPad 应用转过来),或者干脆先不做 Mac,等你能做出原生 Mac UI 再说。
如果你还没做尺寸类适配,就跳过 iPad。 一个 iPhone 应用被拉伸铺满 iPad,读起来就是廉价。iPad 至少需要一个带侧边栏的 NavigationSplitView,理想情况下是真正的双栏布局。Return 在 iPad 上用分栏视图,在 iPhone 上用堆栈。代码在同一个 target 里,但 UI 是实打实不同的。
我给自己划的规矩是:当应用的核心交互与某个平台的输入模型对得上时,才上那个平台。冥想计时器上 Apple Watch(一按即开)。冥想计时器上 Apple TV(设好就不管)。看板工具两个都别上。
什么东西能不费力地跨过去
在 Return 里,真正做到五个平台共享的只有三样:
- 数据模型(
MeditationSession)。这个 struct 在每个平台上都完全一致,经NSUbiquitousKeyValueStore同步,任何平台都能读到其他平台写下的内容。 - 历史记录视图(
SessionHistoryView)。一个展示过往记录的List,在 iPhone、iPad、Mac、Apple Watch 和 Apple TV 上渲染效果一致。List是 SwiftUI 中少数能干净地适配全部五种形态因子的原语之一。 - 持久化封装(
SessionStore)。读写与平台无关;底层存储(NSUbiquitousKeyValueStore)在哪儿都是同一套 API。
三个概念。状态、列表渲染、持久化。凡是有状态且偏展示、又不牵涉硬件专属输入模型的东西,都可以共享。 凡是碰到输入、焦点、音频路由、屏幕尺寸或后台执行的,都不行。
这个规律在《用 AI 智能体开发 iOS 应用》指南里也出现过,我在那里用另一套说法讲了同一件事:iOS 应用中智能体能写的部分,与人来写的部分共享着大部分代码;而需要人的判断力的那些部分(签名、视觉打磨、性能)恰恰也是跨平台最难共享的部分。9 这两条边界是重合的。它们讲的都是:领域知识从哪里开始变得要紧。
多平台的代价
这笔 ROI 是不对称的。给一个 iPhone 应用加上 iPad,大概多出 20% 的代码(尺寸类分支、某些地方的分栏视图)。在同一个 target 上再加 Mac,又多出 15–20%(#if os(macOS) 分支、菜单栏、窗口管理)。对小型应用来说,每加一个主要 target 大约多出 10 个文件。
Apple Watch 和 Apple TV 才是贵的那两个。给 Return 加上 watchOS,需要在一个独立 target 里新增 11 个文件,其中包括专属的音频、计时和 HealthKit 管理器。加上 tvOS,则需要在另一个独立 target 里新增 10 个文件,包括焦点管理和一个自定义时长选择器。两者加起来,几乎让这个应用的 Swift 代码面积翻了一倍——而在用户功能层面,它还是同一个应用。
选择五个平台全上,动机并不是“我们就是想做多平台”。那是一连串各自独立的判断:上 Apple Watch,因为冥想计时器本就属于手腕;上 Apple TV,因为环境式的屏幕形态适合长时间的冥想;上 Mac,因为有些用户会在会议间隙于工位上冥想。每个平台都是靠一个真实场景挣来自己的 target 的。
如果一个功能挣不来自己的 target,更划算的做法是跳过这个平台,把力气加倍投在应用真正出色的那些平台上。
这对你的应用意味着什么
三点结论。
- 默认按主要平台族划分 target。 iOS + iPadOS + macOS 放一个 target 是可行的,因为核心交互(触摸 + 光标)相近。tvOS 单独一个 target。watchOS 单独一个 target。每个独立 target 大约要多花 10 个文件,但能省掉一个
#if分支无限膨胀的上帝类。 - 积极共享状态,别共享交互。 Codable 的模型 struct、持久化封装以及
List渲染几乎可以零成本迁移。计时管理器、音频管理器、内容视图不行。 - 让每个平台自己挣来资格。 别因为“能做”就上 watchOS。当应用的核心交互与该平台的输入模型对得上时再上,其余的跳过。
这套模式与我为同一族应用写过的另外三个层面彼此配合:面向 Apple Intelligence 的类型化 App Intents,面向跨 LLM 智能体的 MCP 服务器,以及面向设备前那个人的 Liquid Glass。同一套技术栈的最外层就是平台本身:这个应用究竟会跑在哪些屏幕上。选平台,要像选 AI 层面一样审慎。
常见问题
为什么不用 Swift package 来放共享代码?
我考虑过。就三个文件而言,Swift package 带来的繁琐多于它省下的功夫。当你勾上 Target Membership 复选框,Apple 的 Xcode 26 构建系统很乐意把一个源文件编译进多个 target。而一个 package 会多出一个 Package.swift、一个独立的测试 target,以及每次重构都得绕一圈的间接层。共享内核很小的时候,简单的答案胜出。10
SwiftData 在 watchOS 和 tvOS 上能用吗?
SwiftData 支持 iOS 17+、macOS 14+、watchOS 10+ 和 tvOS 17+,Return 面向的每个平台都覆盖到了。11 MeditationSession 这个 struct 只是普通的 Codable,不是 @Model,因为 Return 的历史记录同步用的是 NSUbiquitousKeyValueStore 而非 SwiftData 容器。对 @Model 类型来说,这套模式同样成立:模型文件共享,持久化容器则在必要时按平台各行其是。
我该用 Mac Catalyst 还是原生 Mac target?
当 iPad 应用本身足够好、好到 Catalyst 重建出的 Mac 版读起来就像原生时,Catalyst 就是对的工具。Return 的主 target 是真正的多平台 target(不是 Catalyst),用 SwiftUI 把 iOS、iPadOS 和 macOS 构建进同一个二进制。Mac 上的 UI 通过 #if os(macOS) 渲染出与 iPad 不同的样子:用侧边栏而不是 sheet,按钮带快捷键,等等。用 Catalyst 会更省事,但那样 Mac UI 就会像一个跑在 Mac 上的 iPad 应用——而这正是 Catalyst 最出名的那种失败方式。
小应用值得上 Apple TV 吗?
多半不值得。Apple TV 应用的适用场景非常具体(环境式、媒体、休闲游戏)。如果你的应用不属于其中之一,这个平台的受众规模撑不起每个应用 10 个 Swift 文件的投入。Return 之所以专门面向 tvOS,是因为“在房间另一头的屏幕上进行长时间冥想”正是少数与生产力沾边、又契合这个平台的场景之一。
五个平台全上要花多长时间?
很难给出精确数字,这取决于应用本身。Return 从第一天就是多平台交付,而不是后来一个个平台补上去——前者比改造要省力得多。粗略的经验法则是:一个只做 iPhone 的 MVP 再加上 iPad 支持和 Mac 支持,大约是纯 iPhone 工作量的 1.5 倍。加上 Apple Watch,再多 0.5 倍。加上 Apple TV,再多 0.5 倍。所以首发就覆盖五个平台,大致是纯 iPhone 投入的 2.5 倍——但要说明的是,这是一次由智能体协助的构建,那些重复代码大多由 Claude Code 批量编辑生成,而非手工敲出来的。
参考资料
-
作者的 Return,一款冥想计时应用,2026 年 4 月 21 日上架 App Store。原生 target:iOS 26+、iPadOS 26+、macOS 26+、watchOS 26+、tvOS 26+。全程 SwiftUI。跨设备的历史记录使用
NSUbiquitousKeyValueStore。 ↩ -
Apple Developer,“Configuring a Multi-Platform App” 以及 WWDC 2024 的 “SwiftUI essentials” 场次。Apple 的默认建议倾向于单一 target 加环境驱动的适配;本文所走的多 target 路线是一次刻意的背离。 ↩
-
生产代码见
Return/Return/Shared/MeditationSession.swift、SessionStore.swift、SessionHistoryView.swift。MeditationSession.swift的文件头注释写着:“Add this file to: Return, ReturnTV, ReturnWatch Watch App targets.” ↩ -
生产代码见
Return/Return/VideoBackgroundView.swift(8 处#if os(iOS)分支加 1 处#elseif os(macOS)分支)、Return/Return/ContentView.swift(10 处#if os分支)、Return/Return/AudioManager.swift(6 处#if os分支)、Return/Return/LiveActivityManager.swift(8 处#if os分支,该文件仅限 iOS)。分支数量由grep -Ec '^\s*#if os\\(' <file>统计得出。 ↩ -
Apple Developer,“Focus interactions” Human Interface Guidelines。tvOS 的焦点引擎与 iOS 的触摸或 Mac 的指针在导航模型上有根本区别。 ↩
-
生产代码见
Return/ReturnTV/TVFocusModifier.swift。它定义了两个ButtonStyle类型(TVCapsuleButtonStyle与TVCircleButtonStyle),二者都借助@Environment(\.isFocused)在获得焦点时反转颜色与透明度,并施加缩放与阴影。 ↩ -
Apple Developer,“WatchConnectivity”。这是用于 iPhone 与配对 Watch 之间通信的框架;Return 的记录同步并未使用它,而是依赖 iCloud 键值存储。 ↩
-
Apple Developer,“WKExtendedRuntimeSession” 以及 Info.plist 键 “WKBackgroundModes”。文档对
mindfulness值的说明是:”Enables extended runtime sessions for silent meditation”——正好契合一款冥想计时应用。Return 创建默认的WKExtendedRuntimeSession(),并在 watchOS target 的Info.plist中声明WKBackgroundModes: mindfulness。生产代码:Return/ReturnWatch Watch App/ReturnWatchApp.swift定义了WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate;WatchTimerManager.swift把扩展执行环境相关的工作委托给它。 ↩ -
作者的分析见用 AI 智能体开发 iOS 应用,一份基于 8 款生产应用、面向从业者的智能体协助 iOS 开发指南。 ↩
-
Apple Developer,“Configuring a Multi-Platform App”。Target membership 让一个源文件无需 Swift package 就能编译进多个 target。对小型共享内核来说是对的工具。 ↩
-
Apple Developer,“SwiftData” 平台可用性。支持 iOS 17+、iPadOS 17+、macOS 14+、watchOS 10+、tvOS 17+、visionOS 1+,覆盖全部五个 Apple 平台族。 ↩
-
Apple Developer,“NSUbiquitousKeyValueStore”。Apple 的 iCloud 键值存储,用于在用户的多台设备之间同步少量状态。按 Apple 公布的限制,所有键加起来的存储总量上限为 1 MB。生产代码:
Return/Return/Shared/SessionStore.swift。 ↩ -
Apple Developer,
EnvironmentValues.isFocused。支持 iOS 14+、iPadOS 14+、macOS 11+、tvOS 14+、watchOS 7+。这个 API 是跨平台的;有区别的是焦点是否为用户的主要导航可供性。 ↩