Boutique

Boutique

轻量级Swift数据持久化框架

Boutique是一个轻量级Swift数据持久化框架,支持SwiftUI、UIKit和AppKit。它采用内存和磁盘双层缓存架构,通过简洁的API实现实时更新和离线存储。框架提供@Stored等属性包装器,简化了状态管理。开发者无需关注数据库细节,即可构建具备实时更新和离线功能的应用。

Boutique数据存储SwiftUI离线存储实时更新Github开源项目

精品店标志

一个简单但出人意料的强大数据存储,而且远不止于此

"我抛弃了Core Data,这才是它应该工作的方式"

Josh Holtz

"Boutique 实现起来非常简单,让持久化变得轻而易举。它已成为我每个新项目的首选。"

Tyler Hillsman

"Boutique 已经变得不可或缺,我现在的每个个人项目都在使用它。不用关心持久化是很棒的,而且上手的成本几乎为零。"

Romain Pouclet

如果你觉得 Boutique 有价值,我将非常感谢你考虑赞助我的开源工作,这样我就可以继续开发像 Boutique 这样的项目来帮助像你这样的开发者。


Boutique 是一个简单但功能强大的持久化库,它是一组小型的属性包装器和类型,可以为 SwiftUI、UIKit 和 AppKit 构建incredibly简单的状态驱动应用。通过其双层内存+磁盘缓存架构,Boutique 提供了一种方法,只需几行代码就可以使用incredibly简单的 API 构建实时更新并具有完整离线存储功能的应用。Boutique 构建在 Bodega 之上,你可以在这个仓库中找到基于 Model View Controller Store 架构构建的演示应用,该应用展示了如何仅用几行代码就构建出一个支持离线的 SwiftUI 应用。你可以在这篇探讨 MVCS 架构的博文中了解更多关于这种架构背后的思考。



入门

Boutique 只有一个你需要理解的概念。当你将数据保存到 Store 时,你的数据会自动为你持久化,并以常规 Swift 数组的形式暴露出来。@StoredValue@AsyncStoredValue 属性包装器的工作方式相同,但它们不是处理数组,而是处理单个 Swift 值。你永远不需要考虑数据库,你的应用中的所有内容都是使用应用模型的常规 Swift 数组或值,代码简单直观,看起来就像任何其他应用一样。

你可能熟悉 ReduxThe Composable Architecture 中的 Store,但与这些框架不同,你不需要担心添加 Actions 或 Reducers。使用这个 Store 实现,所有数据都会自动为你持久化,无需额外代码。这允许你以一种incredibly简单直接的方式构建具有完整离线支持的实时更新应用。

你可以在下面阅读 Boutique 的高级概述,但 Boutique 也有完整的文档


Store

我们将在下面对 Store 进行高级概述,但 Store这里有完整的文档,包括上下文、用例和示例。

实现完整离线支持和整个应用的实时模型更新的 API 表面积仅包含三个方法:.insert().remove().removeAll()

// 创建一个 Store ¹ let store = Store<Animal>( storage: SQLiteStorageEngine.default(appendingPath: "Animals"), cacheIdentifier: \.id ) // 将一个项目插入 Store ² let redPanda = Animal(id: "red_panda") try await store.insert(redPanda) // 从 Store 中移除一个动物 try await store.remove(redPanda) // 向 Store 插入另外两个动物 let dog = Animal(id: "dog") let cat = Animal(id: "cat") try await store.insert([dog, cat]) // 你可以直接读取项目 print(store.items) // 打印 [dog, cat] // 你也不需要担心维护唯一性,Store 会为你处理唯一性 let secondDog = Animal(id: "dog") try await store.insert(secondDog) print(store.items) // 打印 [dog, cat] // 通过一次性移除所有项目来清空你的 store store.removeAll() print(store.items) // 打印 [] // 你甚至可以将命令链接在一起 try await store .insert(dog) .insert(cat) .run() print(store.items) // 打印 [dog, cat] // 这是清除陈旧缓存数据的好方法 try await store .removeAll() .insert(redPanda) .run() print(store.items) // 打印 [redPanda]

如果你正在构建一个 SwiftUI 应用,你不需要改变任何东西,Boutique 是为 SwiftUI 设计的,并与之兼容。(当然,它在 UIKit 和 AppKit 中也能很好地工作。😉)

// 由于 items 是一个 @Published 属性 // 你可以实时订阅任何更改。 store.$items.sink({ items in print("Items 已更新", items) }) // 对于更复杂的管道,可以很好地与 SwiftUI 配合使用。 .onReceive(store.$items, perform: { self.allItems = $0.filter({ $0.id > 100 }) })

¹ 你可以根据需要拥有任意多或任意少的 Store。对于应用中下载的所有图片使用一个 Store 可能是一个好策略,但你可能也想为每种需要缓存的模型类型创建一个 Store。你甚至可以为测试创建单独的 store,Boutique 不做规定,你可以自由选择如何建模你的数据。你还会注意到,这是来自 Bodega 的概念,你可以在 Bodega 的 StorageEngine 文档中了解更多信息。

² 在底层,当你添加或删除项目时,Store 会完成将所有更改保存到磁盘的工作。

³ 在 SwiftUI 中,你甚至可以使用 $items 为你的 View 提供动力,并使用 .onReceive() 来更新和操作 Store 的 $items 发布的数据。

警告 在 Boutique 中存储图像或其他二进制数据技术上是支持的,但不推荐。原因是在 Boutique 中存储图像可能会使内存中的 store 膨胀,从而导致应用的内存也随之增加。出于类似的原因,不建议在数据库中存储图像或二进制 blob,同样也不建议在 Boutique 中存储图像或二进制 blob。


@Stored 的魔力

我们将在下面对 @Stored 属性包装器进行高级概述,但 @Stored这里有完整的文档,包括上下文、用例和示例。

这很简单,但我想向你展示一些让 Boutique 感觉非常神奇的东西。Store 是一种简单的方式来获得离线存储和实时更新的好处,但通过使用 @Stored 属性包装器,我们可以仅用一行代码就在内存和磁盘中缓存任何属性。

extension Store where Item == RemoteImage { // 初始化一个 Store 来保存我们的图像 static let imagesStore = Store<RemoteImage>( storage: SQLiteStorageEngine.default(appendingPath: "Images") ) } final class ImagesController: ObservableObject { /// 创建一个 @Stored 属性来处理图像的内存和磁盘缓存。⁴ @Stored(in: .imagesStore) var images /// 从 API 获取 `RemoteImage`,如果请求成功则为用户提供一个红熊猫。 func fetchImage() async throws -> RemoteImage { // 调用提供随机图像元数据的 API let imageURL = URL(string: "https://image.redpanda.club/random/json")! let randomImageRequest = URLRequest(url: imageURL) let (imageResponse, _) = try await URLSession.shared.data(for: randomImageRequest) return RemoteImage(createdAt: .now, url: imageResponse.url, width: imageResponse.width, height: imageResponse.height, imageData: imageResponse.imageData) } /// 将图像保存到内存和磁盘的`Store`中。 func saveImage(image: RemoteImage) async throws { try await self.$images.insert(image) } /// 从内存和磁盘的`Store`中移除一张图像。 func removeImage(image: RemoteImage) async throws { try await self.$images.remove(image) } /// 从内存和磁盘的`Store`中移除所有图像。 func clearAllImages() async throws { try await self.$images.removeAll() } }

就是这样,真的就这么简单。这种技术可以很好地扩展,在多个视图之间共享这些数据正是Boutique从简单到复杂应用程序的扩展方式,而无需增加API的复杂性。很难相信现在你的应用程序只需要一行代码就可以实现实时状态更新和完全离线存储。@Stored(in: .imagesStore) var images


⁴ (如果你更喜欢将存储与视图模型、控制器或管理器对象解耦,你可以像这样将存储注入到对象中。)

final class ImagesController: ObservableObject { @Stored var images: [RemoteImage] init(store: Store<RemoteImage>) { self._images = Stored(in: store) } }

StoredValue, SecurelyStoredValue, 和 AsyncStoredValue

我们将对@StoredValue@SecurelyStoredValue@AsyncStoredValue属性包装器进行概述,但它们在这里有详细的文档,包含上下文、用例和示例。

Store@Stored最初是为了存储数据数组而创建的,因为大多数应用程序渲染的数据都是以数组形式出现的。但有时我们需要存储单个值,这就是@StoredValue@SecurelyStoredValue@AsyncStoredValue派上用场的地方。

无论你是需要为下次应用程序启动保存重要信息,在钥匙串中存储认证令牌,还是想根据用户设置改变应用程序的外观,这些应用程序配置都是你想要持久化的单个值。

通常人们会选择在UserDefaults中存储这样的单个项目。如果你使用过@AppStorage,那么@StoredValue会让你感到很熟悉,它有非常相似的API,还有一些额外的功能。@StoredValue最终会存储在UserDefaults中,但它还暴露了一个publisher,方便你订阅变化。

// 设置一个`@StoredValue`,@AsyncStoredValue有相同的API。 @StoredValue(key: "hasHapticsEnabled") var hasHapticsEnabled = false // 你也可以存储nil值 @StoredValue(key: "lastOpenedDate") var lastOpenedDate: Date? = nil // 枚举也可以,只要它符合`Codable`和`Equatable`协议。 @StoredValue(key: "currentTheme") var currentlySelectedTheme = .light // 复杂对象也可以 struct UserPreferences: Codable, Equatable { var hasHapticsEnabled: Bool var prefersDarkMode: Bool var prefersWideScreen: Bool var spatialAudioEnabled: Bool } @StoredValue(key: "userPreferences") var preferences = UserPreferences() // 将lastOpenedDate设置为现在 $lastOpenedDate.set(.now) // currentlySelected现在是.dark $currentlySelectedTheme.set(.dark) // 由布尔值支持的StoredValues也有一个toggle()函数 $hasHapticsEnabled.toggle()

@SecurelyStoredValue属性包装器可以做@StoredValue能做的所有事情,但不是将值存储在UserDefaults中,@SecurelyStoredValue会将项目持久化到系统的钥匙串中。这非常适合存储敏感值,如密码或认证令牌,你不会想将这些存储在UserDefaults中。

你可能不想使用UserDefaults或系统钥匙串来存储值,在这种情况下,你可以使用自己的StorageEngine。为此,你应该使用@AsyncStoredValue属性包装器,它允许你在你提供的StorageEngine中存储单个值。这并不常用,但它提供了额外的灵活性,同时保持了Boutique的@StoredValue API的一致性。

文档

如果你有任何问题,我建议你首先查看文档,Boutique和Bodega都有非常详细的文档。此外,Boutique还附带了两个演示应用,每个应用都有不同的用途,但都展示了如何构建一个基于Boutique的应用。

在构建v1时,我注意到那些理解Boutique的人都很喜欢它,而那些认为它可能不错但有疑问的人一旦理解了如何使用它,也会爱上它。因此,我着手编写了大量文档,解释在构建iOS或macOS应用时会遇到的概念和常见用例。如果你仍有问题或建议,我非常欢迎反馈,如何贡献在本readme的反馈部分有讨论。


进一步探索

Boutique本身就非常有用,只需几行代码就可以构建实时离线就绪的应用程序,但当你使用我开发的Model View Controller Store架构时,它更加强大,如上面的ImagesController所示。MVCS将你熟悉和喜爱的MVC架构的简单性与Store的强大功能结合起来,为你的应用程序提供简单但定义明确的状态管理和数据架构。

如果你想了解更多关于它如何工作的信息,你可以阅读这篇博客文章,其中我探讨了SwiftUI的MVCS,你可以在这个仓库中找到由Boutique支持的离线就绪实时MVCS应用程序的参考实现。

我们在这里只是触及了Boutique能做什么的表面。利用Bodega的StorageEngine,你可以构建复杂的数据管道,从缓存数据到与API服务器接口。Boutique和Bodega不仅仅是库,它们是任何数据驱动应用程序的一组原语,所以我建议你尝试一下,玩玩演示应用,甚至构建你自己的应用程序!


反馈

本项目提供多种方式向维护者提供反馈。

  • 如果你对Boutique有疑问,我们建议你首先查阅文档,看看你的问题是否已经在那里得到解答。

  • 本项目有大量文档,但也包括多个示例项目。

    • 第一个应用是演示应用,展示了如何使用Model View Controller Store模式构建一个规范的Boutique应用。该应用有大量内联解释文档,帮助你建立对Boutique应用如何工作的直觉,并通过教你最佳实践来节省时间。
    • 第二个应用是性能分析器,同样使用Boutique首选的架构。如果你正在开发自定义StorageEngine,这个项目将作为测试你需要构建的操作性能的好方法。
  • 如果你仍有问题、改进建议或改进Boutique的方法,本项目利用GitHub的讨论功能。

  • 如果你发现了bug并希望报告,我们会感谢你提交issue


要求

  • iOS 13.0+
  • macOS 11.0+
  • Xcode 13.2+

安装

Swift Package Manager

Swift Package Manager是一个用于自动化分发Swift代码的工具,它集成在Swift构建系统中。

一旦你设置好了Swift包,将Boutique作为依赖项添加到你的Package.swift的dependencies值中就很容易了。

dependencies: [ .package(url: "https://github.com/mergesort/Boutique.git", .upToNextMajor(from: "1.0.0")) ]

手动集成

如果你不想使用SPM,你可以通过复制文件来手动将Boutique集成到你的项目中。


关于我

我是Joe,你可以在网上各处找到我,尤其是在Mastodon上。

许可证

查看许可证以了解更多关于如何使用Boutique的信息。

赞助

Boutique是一个充满爱的项目,旨在帮助开发者构建更好的应用,让你更容易释放创造力,为自己和用户打造出色的作品。如果你觉得Boutique有价值,我将非常感谢你考虑赞助我的开源工作,这样我就可以继续开发像Boutique这样的项目来帮助像你这样的开发者。


现在你已经知道了有什么"好货"在等着你,是时候开始了 🏪

编辑推荐精选

问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

Trae

Trae

字节跳动发布的AI编程神器IDE

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
咔片PPT

咔片PPT

AI助力,做PPT更简单!

咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。

讯飞绘文

讯飞绘文

选题、配图、成文,一站式创作,让内容运营更高效

讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。

热门AI辅助写作AI工具讯飞绘文内容运营AI创作个性化文章多平台分发AI助手
材料星

材料星

专业的AI公文写作平台,公文写作神器

AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。

openai-agents-python

openai-agents-python

OpenAI Agents SDK,助力开发者便捷使用 OpenAI 相关功能。

openai-agents-python 是 OpenAI 推出的一款强大 Python SDK,它为开发者提供了与 OpenAI 模型交互的高效工具,支持工具调用、结果处理、追踪等功能,涵盖多种应用场景,如研究助手、财务研究等,能显著提升开发效率,让开发者更轻松地利用 OpenAI 的技术优势。

下拉加载更多