Project Icon

Boutique

轻量级Swift数据持久化框架

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

精品店标志

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

"我抛弃了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这样的项目来帮助像你这样的开发者。


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

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号