← 所有文章

单一可信源:SwiftData、MCP、iCloud

Get Bananas 有三个调用方可以写入同一份购物清单。人类在 iOS 应用中点击行项。Apple Intelligence 通过AppIntent路由 Siri 请求。Claude Code会话通过 stdio 调用MCP工具。在用户脑海中,这份清单是一个规范实体;问题在于它存储在哪里,以及当不同调用方发生分歧时由谁胜出。

综合性文章命名了 iOS 应用的三个表面:人类、Apple Intelligence、代理。每个表面都需要读取和写入相同的领域状态。这一需求催生了太多应用中出现的架构错误:每个表面拥有自己的存储,各表面随时间漂移,用户根据最近触碰过哪个表面而看到三种不同版本的清单。能够长期存活的模式是:单一可信源,加上表面与基础载体之间的显式同步路径。

本文命名了基础载体的几种选择、每种选择强制带来的冲突解决规则,以及当三类调用方都能写入时仍能站得住脚的架构。讲解使用 Get Bananas 的实际布局:SwiftData 用于应用内状态,iCloud Drive 中的一个JSON文件用于跨进程同步,一个MCP服务器从 iOS 沙盒外读写同一份文件。1

TL;DR

  • 三种基础载体可以组合使用:SwiftData(进程内、快速、模式类型化)、iCloud Drive(跨进程、基于文件、可同步)、NSUbiquitousKeyValueStore(跨设备、键值、仅限小载荷)。
  • 选择哪个基础载体作为某领域的可信源。冲突解决是这一选择的强制结果,而不是另一个独立问题。
  • 三个表面(人类、Apple Intelligence、代理)各自通过领域层与基础载体交互。冲突解决策略就存放在领域层。
  • 每个可变实体上的lastModified时间戳是廉价的冲突解决原语;”最后写入者胜出”是廉价策略。需要更强保证的应用必须用显式合并逻辑来支付代价。
  • 应用进程之外的MCP服务器无法读取 SwiftData。桥梁是序列化层(iCloud Drive 中的JSON文件、App Group 容器等),应用内的 SwiftData 读取器和进程外的MCP服务器都可以与之通信。

三种基础载体

Apple 为已发布的 iOS 应用提供了三种原生持久化基础载体,它们在跨进程和跨设备模式中均会出现。每一种都有特定形态;不加规划地混用就会产生漂移问题。

SwiftData。 由 Core Data 支撑的进程内持久化存储。2 快速、模式类型化、可通过@Query查询、与 SwiftUI 的观察系统集成。存储归应用进程所有。应用扩展(小组件、意图、共享扩展)可以通过 App Group 加上显式配置来共享 SwiftData 容器;运行在开发者机器上、位于应用签名上下文之外的任意外部MCP进程,无法安全地伸入 SwiftData 容器。行可以通过PersistentIdentifier(进程内)或@Attribute(.unique)自然键(跨进程、跨设备)来寻址。迁移通过VersionedSchemaMigrationPlan声明式定义(详见SwiftData 的真正代价是模式纪律)。

iCloud Drive。 基于文件的跨设备同步,通过FileManager暴露用户 iCloud 容器中的 URL。3 文件出现在用户登录的每台设备上。冲突解决在文件级别:iCloud 使用NSFileVersion跟踪并发编辑,应用通过冲突日志决定保留什么。iCloud Drive 中的文件可以从 iOS 应用进程之外寻址:Mac 上的MCP服务器可以打开 iOS 应用读取的同一个JSON文件。这种基础载体正是让 Get Bananas 的MCP集成得以工作的关键。

NSUbiquitousKeyValueStore 跨设备键值存储。Apple 当前的公开限制是每个应用总计 1MB、每个值 1MB、1024 个键、每个键 128 个 UTF-16 字符,且写入速率受限流约束。4 冲突解决内置:系统串行化写入并在变更时通知所有设备。适用于小型、低频状态(设置、上次选中的标签页、整数计数器);不适用于高写入率数据,或限流会成为瓶颈的工作负载。Return 用它来存储跨设备的计时器状态(用户在 iPhone 上启动计时器,在 Apple Watch 上看到它);详见五个 Apple 平台,三个共享文件

第四种基础载体CloudKit,看起来是显而易见的选择,但本系列应用在跨进程MCP集成场景下明确将其拒之门外。CloudKit 提供强大的跨设备同步和具备冲突感知的记录,Apple 也确实发布了 CloudKit JS 和 CloudKit Web Services,使非 Apple 环境能够与公共和私有 CloudKit 数据库通信。诚实的权衡是集成成本,而非不可能性:Node.js 的MCP服务器要访问私有 CloudKit 数据库,必须接入 CloudKit Web Services 的认证、模式定义和请求签名,相比”打开一个JSON文件”,这是实质性的工程工作量。Get Bananas 选择 iCloud Drive 加JSON文件,是因为MCP服务器是一个 Node.js 进程,需要读写与 iOS 应用所见相同的数据,而普通的文件 I/O 是阻力最小的路径。1

决策:由哪个基础载体承载真相

问题不在于用哪个基础载体。问题在于哪个基础载体是某个领域的可信源。其他基础载体要么缓存它,要么镜像它,要么避让出去。

在本系列应用的生产环境中能够立得住的决策矩阵:

领域形态 可信源 原因
设置、偏好、简单标志 NSUbiquitousKeyValueStore 跨设备同步自动完成;冲突被串行化;小载荷适合
每设备的瞬态状态 UserDefaults(不同步) 设备本地;不应在另一台设备上的全新安装中存活
进程内可查询集合 SwiftData 快速的@Query、SwiftUI 观察、模式类型化;仅限进程内
必须触达外部进程的进程内集合 iCloud Drive JSON文件(导出到磁盘) iOS 端的 SwiftData 读取器和外部的MCP服务器都能读取该文件
大型用户内容(照片、音频、文档) iCloud Drive(按文件) 用户的 iCloud 是天然存储;CloudKit 可在其上分层以实现更丰富的同步
跨设备会话级状态(iPhone 上正在运行的计时器,可在 Watch 上查看) NSUbiquitousKeyValueStore 适合大小范围;需要跨设备推送语义

决策塑造了冲突解决策略。对于本地应用加外部MCP的桥接,SwiftData 在进程之间没有内在的冲突解决机制;如果两个调用方写入同一行,最后调用try context.save()者胜出。SwiftData 由 CloudKit 支撑并使用持久化历史时,可以承载更丰富的跨设备语义,但该表面位于 iOS 端,对外部 Node.js MCP情形并无帮助。iCloud Drive 将冲突暴露为NSFileVersion条目;应用必须遍历它们并选出胜出者。NSUbiquitousKeyValueStore在值层面有内置的冲突解决。

Get Bananas 的架构

Get Bananas 的实际布局:

                     ┌────────────────────────────────────┐
                     │           User's mental model       │
                     │         "my shopping list"          │
                     └─────────────────┬──────────────────┘
                                       │
                          ┌────────────┴───────────┐
                          │                        │
                  ┌───────▼────────┐      ┌───────▼─────────┐
                  │   iOS app      │      │  MCP server      │
                  │  (Get Bananas) │      │  (Node.js)       │
                  └───────┬────────┘      └───────┬─────────┘
                          │                        │
              ┌───────────┴────────┐               │
              │                    │               │
       ┌──────▼──────┐    ┌────────▼──────────┐   │
       │  SwiftData   │    │  iCloud Drive     │◀──┘
       │ (in-process) │◀──▶│  shopping_list.   │
       │              │    │       json        │
       └──────────────┘    └───────────────────┘

  In-app reads/writes flow through SwiftData.
  Cross-process reads/writes flow through the JSON file.
  An iCloud sync layer (iCloudBackupManager) reconciles the two.

该架构有三条规则。

SwiftData 是进程内查询的可信源。 iOS 应用从 SwiftData 读取每一次 UI 渲染、每一个由@Query支撑的列表、每一次搜索。写入首先经过 SwiftData;模型上下文保存;SwiftUI 重新渲染。

JSON文件是跨进程状态的可信源。 每当 iOS 应用保存到 SwiftData 时,iCloud 备份管理器都会将当前状态导出为用户 iCloud Drive 容器中的一个JSON文件。每当MCP服务器写入时,它写入的是同一个JSON文件。这个文件就是桥梁。

同步过程会在 iOS 应用启动时以及每次跨进程写入之后运行。 当前生产环境中的同步逻辑(SyncManager.applyExport)在每次同步过程中都将JSON备份视为权威:它读取JSON文件,通过 UUID 将每一行与 SwiftData 匹配,用备份中的值覆盖现有行,添加备份中存在但 SwiftData 中没有的行,并删除 SwiftData 中存在但备份中没有的行(并设有损坏防护,以避免空备份文件抹除本地数据库)。该策略是备份在同步时胜出,而不是按时间戳的逐行最后写入者胜出。结合 iOS 应用在每次保存后都重新导出,实践中稳态收敛很快:无论哪个进程最近写入,都会产生下一次同步要读取的JSON。

该架构以复杂性换取了跨进程触达能力。纯 SwiftData 应用无需这一切;没有MCP服务器的应用不需要JSON桥梁;没有跨设备同步的应用不需要协调器。Get Bananas 三者都需要,因为三类调用方(通过 iOS 的人类、通过 iOS 上 App Intents 的 Apple Intelligence、通过 Mac 开发者机器上MCP的代理)全都触碰同一份购物清单。

升级路径:逐行最后写入者胜出

发布版的”备份在同步时胜出”策略很廉价,在单用户、同一时刻只有单一活跃写入者的情况下能够工作。当 iOS 应用和MCP服务器在短时间内先后写入JSON文件时,它就会出问题:无论哪个进程最近写入,都会覆盖另一方的更改,包括那些实际上并未冲突的行。当前的缓解措施是 iOS 应用在每次 SwiftData 保存后重新导出,使JSON文件大致与最新的应用内状态保持一致。稳态情况没问题;真正并发的情况则可能丢失工作。

最廉价的升级是基于lastModified: Date?列的逐行最后写入者胜出。ShoppingItem模型已经包含该字段以保障迁移安全(详见SwiftData 的真正代价是模式纪律),但JSON导出和MCP服务器目前并未对其进行序列化或遵循。将lastModified贯穿到导出和applyExport中,会把合并策略从”备份胜出”改为”较新的行胜出”:

  • 双方都有值,其中一方更近期。 最近者胜出。另一方的行被更新。
  • 双方都有值,且打平。 按主键打破平局,或按表面打破平局(iOS 应用在打平时胜出,以偏向用户最近的应用内交互)。
  • 一方有值,另一方没有。 有值的一方胜出。
  • 双方都没有值。 两行都是lastModified字段引入之前的数据。协调器为下一次戳上Date()

该策略廉价、易于推理,在大约 1% 的情况下会出错(对同一行不同字段的并发编辑)。对于购物清单,这 1% 无足轻重;对于文档编辑器则至关重要。需要更强保证的应用会在此基础之上叠加字段级合并、CRDT 或操作转换;Get Bananas 尚未需要这种复杂度,这就是为什么逐行 LWW 在路线图上,而更丰富的合并不在。

三类调用方与基础载体

三个表面中的调用方类别映射到基础载体的决策上:

人类表面通过 SwiftData 写入。 用户在 iOS 应用中点击复选框,事件通过 SwiftUI 层触发到一个领域函数,该函数变更 SwiftData 行,在模型上戳上lastModified = Date(),并保存模型上下文。iCloud 导出将当前状态写入JSON文件。MCP服务器在下次读取时获取新状态。

Apple Intelligence 表面通过 SwiftData 写入。 通过 Siri 调用的AppIntent在 iOS 应用进程中运行,触达人类表面所使用的同一个领域函数。SwiftData 状态变更,模型的lastModified更新,JSON导出捕获新状态。

代理表面通过JSON文件写入。 Mac 上Claude Code会话发起的MCP工具调用直接变更JSON文件(并使用文件锁以处理来自 iOS 应用的并发写入)。下次 iOS 应用启动或同步时,SyncManager.applyExport读取该文件,通过 UUID 遍历各项,用备份的值更新双方都存在的行,添加备份中存在的行,并删除备份中省略的行(并设有空备份防护)。发布策略是备份在同步时胜出;升级路径是把lastModified加进JSON,使策略可转变为较新的行胜出

这种不对称是真实的,也是有意为之。人类表面和 Apple Intelligence 表面都运行在 iOS 应用进程内,原生使用 SwiftData。代理表面运行在 iOS 应用进程之外,使用JSON文件,因为那是它能够触达的基础载体。协调器是把这两半粘在一起的部件。

何时这一模式不是正解

以下是几种JSON桥接模式不适用的情形。

高写入率数据。 每秒多次编辑的实时文档编辑器,无法承担在每次写入时把整个集合序列化为JSON文件的开销。正解是面向真实后端的操作转换或 CRDT。

强一致性需求。 金融交易账本无法容忍JSON文件上的最后写入者胜出。正解是 CloudKit(或服务端数据库)加上显式的事务语义。

用户实时看到彼此编辑的多用户协作。 iCloud Drive 同步是最终一致的,而非实时的。用户在一台设备上关闭应用、在另一台上打开,看到的是已同步的状态;用户在文档上看到另一位用户光标移动则做不到。正解是实时协作框架(yjs、automerge,或自定义的 WebSocket 层)。

代理与用户身份不同的情形。 Get Bananas 的模式假定代理(MCP调用方)和人类用户(iOS 应用用户)是同一个人,只是跨进程操作。如果代理代表不同的身份(共享清单、管理员、自动化机器人),用户 iCloud Drive 中的JSON文件就是错误的基础载体;需要的是带显式认证的多用户持久化。

该模式适合单用户、最终一致、跨进程的情形。带MCP集成的应用大多正是这种情形;有些则不是。

我会有哪些不同的构建方式

本系列应用要么已经发布、要么希望当初做到的三种模式。

让JSON序列化显式而非隐式。 Get Bananas 的第一个版本在保存钩子中将每次 SwiftData 写入都导出为JSON。第二个版本将导出改为应用在状态稳定后显式调用的步骤。这一变更减少了冗余写入,并清晰表明了跨进程状态何时被发布。隐式的”每次变更即保存”钩子,对任何非平凡集合都会产生过多的 I/O。

为JSON文件的模式打版本号。 JSON文件有自己的模式,独立于 SwiftData 的VersionedSchema。当 SwiftData 模式变化时(比如新增字段),JSON序列化必须随之变化。廉价的修复是在JSON顶部放一个schemaVersion: Int字段;协调器读取它并应用相应的解读。没有版本号,运行 v2 的 iOS 应用读取由旧MCP服务器写入的 v1 JSON文件,就会发生静默的数据损坏。

对JSON文件加文件锁,不要假定有协调机制。 iOS 应用和MCP服务器都要写入JSON文件。没有NSFileCoordinator(进程内、iOS 端)和文件锁(进程外、开发者机器上)的话,并发写入可能产生损坏的文件。Get Bananas 的MCP服务器在JSON上使用类flock式的文件锁;iOS 应用在写入时使用NSFileCoordinator;实践中文件很少出现争用,但这条安全带很廉价。

这一模式对在 iOS 26+ 上发布的应用意味着什么

三点要点。

  1. 每个领域只选一个可信源。其他基础载体负责缓存、镜像或避让出去。 SwiftData 用于进程内查询,iCloud Drive JSON用于跨进程桥接,NSUbiquitousKeyValueStore用于小型跨设备状态。冲突解决是这一选择的下游结果。

  2. lastModified加最后写入者胜出是廉价的基础情形。 大多数应用不需要更强的保证。需要字段级合并或 CRDT 的那 1% 情形,添加成本高昂;在数据形态确实要求之前,不要支付这一成本。

  3. 协调器是承重构件。 当 SwiftData 与JSON文件不一致时,由协调器决定。协调器在应用启动时、跨进程写入之后,以及 iCloud 同步事件之后运行。规则很简单;难点在于纪律——真的去运行它。

完整的 Apple 生态系统集群:针对 Apple Intelligence 表面的类型化App Intents;针对代理表面的MCP服务器;两者之间的路由问题;用于应用内端侧LLM功能的Foundation Models;运行时与工具LLM的区分;iOS 应用三个表面的综合;面向 iOS 锁屏状态机的Live Activities;Apple Watch 上的watchOS 运行时契约;支撑人类表面的SwiftUI 内部机制;visionOS 场景的RealityKit 空间心智模型;用于持久化的SwiftData 模式纪律;视觉层的Liquid Glass 模式;跨设备触达的多平台发布。集群入口在Apple 生态系统系列。要了解更广泛的 iOS 与 AI 代理结合的背景,请参阅iOS Agent 开发指南

FAQ

为什么不使用 CloudKit 进行跨进程同步?

CloudKit 提供强大的跨设备同步和具备冲突感知的记录,Apple 的 CloudKit JS / CloudKit Web Services 也确实让非 Apple 技术栈能够访问私有 CloudKit 数据库。约束在于集成成本:使用 CloudKit 的 Node.js MCP服务器必须处理 CloudKit 的认证(服务器对服务器密钥或用户级令牌)、模式声明和签名请求。iCloud Drive 加JSON文件就是普通的文件 I/O,这是通用译者。当团队愿意支付集成成本、并希望获得 CloudKit 更强的同步与冲突语义时,CloudKit 是正解;当”打开一个文件”已足以应对数据形态时,JSON桥梁是正解。

当两个调用方同时写入时,如何处理冲突?

Get Bananas 的发布策略是”备份在同步时胜出”:SyncManager.applyExport通过 UUID 遍历各项并用备份覆盖本地行,并设有防护,避免空备份抹除良好的本地数据。升级路径是基于lastModified的逐行最后写入者胜出,该字段已存在于模型中,但尚未通过JSON桥梁序列化。加入它将解决约 99% 的冲突——其中一方确实更新;剩余 1%(对同一行不同字段的并发编辑)对目前这些应用来说足够罕见,因此可以略去字段级合并或 CRDT。具有更强一致性需求的应用会在其上叠加更丰富的合并机制。

如果 iCloud Drive 是跨进程状态的可信源,那 SwiftData 在哪里发挥作用?

SwiftData 是进程内查询的可信源。iOS 应用从 SwiftData 读取每一次 UI 渲染、每一次@Query、每一次搜索。SwiftData 速度快、模式类型化,并与 SwiftUI 的观察系统集成。当 iOS 应用写入时,变更先到 SwiftData,然后被导出到JSON文件。JSON文件是跨进程读取(MCP服务器视角)的可信源;SwiftData 是进程内读取(iOS UI 视角)的可信源。两者通过协调器保持对齐。

那购物清单本身用NSUbiquitousKeyValueStore怎么样?

NSUbiquitousKeyValueStore的上限是每个应用总计 1MB、每个值 1MB,写入受限流约束,并以 1024 键的字典进行序列化。包含数百个条目加历史记录的购物清单按字节数也许能装下,但通过限流来逐项写入变更是错误的形态;批量集合更新会与应用存储的所有其他东西争抢速率预算。集合类的正确基础载体要么是 SwiftData(进程内),要么是 iCloud Drive(跨进程)。请把NSUbiquitousKeyValueStore留给小而低频的键值状态:设置、上次选中的标签页、计数器、功能标志覆盖。

我如何为应用中的新领域选择基础载体?

按顺序问三个问题。第一:iOS 应用进程之外是否有任何东西需要读写这一领域?如果是,你需要 iCloud Drive(基于文件、普通文件 I/O)、CloudKit(通过 Apple 框架,或在非 Apple 技术栈中通过 CloudKit Web Services),或一个你掌控的服务器。如果不是,SwiftData 是默认选择。第二:这是否需要在用户的多台设备之间同步?如果是,基础载体必须支持(iCloud Drive 支持,SwiftData 不支持,除非与 iCloud 同步配对)。第三:载荷有多大,变化频率如何?小且低频的存放在NSUbiquitousKeyValueStore;其他都需要真正的持久化层。

参考资料


  1. 作者在两个代理生态,一份购物清单(2026 年 4 月 29 日)中的分析,以及 Get Bananas 项目位于Banana List/iCloudBackupManager.swift的 iCloud Drive JSON同步层。该架构将 SwiftData 与位于用户 iCloud Drive 容器中的JSON文件配对,外部MCP服务器读写该文件。 

  2. Apple Developer,“SwiftData”“在你的应用中添加和编辑持久数据”@Model宏、@Attribute约束以及与 Core Data 的NSManagedObjectModel的关系。作者在SwiftData 的真正代价是模式纪律中的分析涵盖了用于安全模式演进的VersionedSchemaMigrationPlan。 

  3. Apple Developer,“在 iCloud 环境中同步文档”。基于文件的跨设备同步、通过NSFileVersion的冲突解决,以及用于安全进程内写入共享文件的NSFileCoordinator API。 

  4. Apple Developer,“NSUbiquitousKeyValueStore”。跨设备键值存储。Apple 当前的限制:每个应用总计 1MB、每个值 1MB、1024 个键、每个键 128 个 UTF-16 字符,写入速率受限流约束。作者在五个 Apple 平台,三个共享文件中的分析涵盖了 Return 使用此API实现的跨设备计时器模式。 

相关文章

与 iOS 应用并存的 MCP 服务器:两个 __TERM_23__ 生态,一份清单

Get Bananas 运行于 iOS、macOS、watchOS 和 visionOS。它也作为 MCP 服务器存在于 Claude Desktop 中。桥梁:iCloud Drive 加一份 JSON 文件。

6 分钟阅读

App Intents 与 MCP:路由问题

两个协议,一个应用。App Intents 将您的应用暴露给 Apple Intelligence。MCP 将同一领域暴露给 Claude、ChatGPT 等其他工具。这是路由问题。

3 分钟阅读

你的Agent中间商你从未审查过

研究人员测试了28个LLM API路由器。17个接触了AWS金丝雀凭证。一个从私钥中抽走了ETH。路由器层是新的攻击面。

1 分钟阅读