为什么我要自己构建一个音频播放器:基于 SwiftUI 的原生 MP3 播放器实践
In 2025, Apple still makes it hard to play your own MP3s, so I wrote my own app
2025年5月21日 • Oleg Pustovit
在2025年,在 iPhone 上播放自己的音乐仍然非常困难,除非你向 Apple 付费或在重重限制中摸索。因此,我从头开始构建了自己的播放器,具有全文搜索、iCloud 支持和本地优先的体验。GitHub 链接
Why I Built My Own Audio Player
和许多人一样,我订阅了太多的服务,有些通过 Apple (iCloud, Apple Music),有些则遗忘在各种平台(比如 Netflix,我都忘记了还在付费)。我实际上经常使用 Apple Music(之前是 Spotify),但流媒体播放体验带来的便利性最终并没有那么重要。通过精心策划的本地音乐库,我并没有失去太多,只是摆脱了锁定。
最初,我以为可以继续使用 iCloud Music Library 进行跨设备音乐同步,但一旦我取消了 Apple Music 订阅,同步就停止工作了。 事实证明,此功能是需要付费才能使用的。 从技术上讲,您可以通过 iTunes Match ($24.99/年)重新获得此功能。 Match 只是在线存储 256-kbps AAC 副本; 除非您选择替换它们,否则原始文件将保留在原处。 在现代 Mac 上,您可以在 Music 应用程序中完成所有这些操作。 如果没有订阅,云同步将消失,并且您将回到电缆/Wi-Fi 同步。
由于对缺乏选择感到沮丧,我选择了自己构建的道路。 如果我购买了一台计算设备(在本例中为 iPhone),有什么能阻止我使用代码构建完全符合我需求的程序呢? 在本文中,我想分享我创建基本音乐播放器功能的完整过程:加载音频文件、组织和播放它们。 但最重要的是,我想提醒自己,这仍然是一台通用计算机,我应该能够让它做我想做的事情。
What Apple (and Others) Offer Today
在编写我自己的应用程序之前,我探索了官方和第三方提供的离线音乐播放选项。
Apple’s Built-in Apps
从技术上讲,Apple 允许您通过 Files 应用程序直接从 iCloud 播放音乐,但它的功能并非专为听音乐而设计。 它缺少基本功能,例如播放列表管理、元数据排序或播放队列。 虽然它支持音乐播放,但功能非常有限,并且总体上用户体验不佳。
Third-Party Apps
我去 App Store 寻找可以解决我的问题的优秀应用程序,虽然有很多,但许多都依赖于基于订阅的定价,对于一个仅仅播放用户已经拥有的文件的应用程序来说,这是一种有问题的模式。 我喜欢一个应用程序,Doppler。 我在试用期间使用了它,但 UX 是围绕管理专辑构建的。 搜索效果不是很好,并且从 iCloud 导入的功能速度很慢,并且很难在大量嵌套文件夹中使用。 优点是,它具有一次性付款的定价模式。
Going Builder Mode: My Technical Journey
综上所述,我决定创建自己的理想音乐播放器,以解决我的痛点:
- 跨 iCloud 文件夹的灵活全文搜索,因此我可以快速选择和导入包含音乐的文件夹或特定文件。
- 在管理音乐方面的功能至少与官方 Music App 相当:队列、播放列表管理以及按专辑等排序。
- 熟悉且友好的界面。
Trying React Native First
最初,由于我之前的经验,我避免使用 Swift。 几年前,我喜欢它的语法(感觉更接近 TypeScript),并且欣赏类似 Rust 的内存安全性,但在当时没有原生的 async
/ await
的情况下,与 Go 或 JS/TS 相比,编写并发代码感觉笨拙且样板代码过多。 这种经历让我感到沮丧,因此当我重新审视这个项目时,我最初选择了更熟悉的东西。
也就是说,我选择了 React Native 或 Expo,希望能够重用我的 Web 开发经验,并从现有模板中插入播放器 UI。 构建播放 UI 非常简单; 有很多开源示例和教程视频可以构建满足我需求的美观音乐播放器。 我选择了一个现有的Gionatha Sturba 的模板项目,因为它看起来具有我的应用程序所需的所有功能。
访问文件系统和同步云文件遇到了重大障碍:像 expo-filesystem
这样的库支持基本的文件选择,但是递归遍历深度嵌套的 iCloud 目录经常失败,甚至导致应用程序崩溃。 这清楚地表明,即使这意味着更陡峭的学习曲线,基于 JavaScript 的方法也比使用 Apple 的原生 API 引入了更多的复杂性。
iOS 沙盒阻止应用程序在没有明确用户许可的情况下读取文件,这意味着 React Native 无法可靠地访问外部文件夹。 切换到 Swift 使我可以更好地控制 iCloud 文件访问和沙盒权限。
Switching to SwiftUI
我选择了 SwiftUI 而不是 UIKit 或 storyboards,因为我想要一个干净且声明性的 UI 层,它可以让我在专注于域逻辑和数据同步时不受干扰。 借助 async/await 等现代功能以及与 Swift Actors 的集成,我发现更容易管理数据流和并发。 SwiftUI 绝对也更容易将应用程序构建为独立的 ViewModel 组件,这反过来帮助我从 OpenAI o1 和 DeepSeek 等 LLM 中获得更好的结果。 LLM 可以生成纯 UI 代码或数据绑定代码,而不会引入混乱的相互依赖关系。
App Architecture and Data Model
让我们回顾一下我创建的应用程序的架构:我使用 SQLite 进行持久数据存储,并将应用程序架构视为一个简单的服务器应用程序。 我避免使用 CoreData,因为我需要严格控制模式、原始查询,尤其是全文搜索。 SQLite 的内置 FTS5 支持使我可以添加快速模糊搜索,而无需引入繁重的外部搜索引擎或构建自己的索引层。
Three Main Screens
该应用程序由 3 个屏幕/模式组成:
- Library import. 在这里您可以添加您的 iCloud 库文件夹。 该应用程序扫描每个文件夹中的音频文件,并将每个路径插入到 SQLite 数据库中。 这样,您就可以在搜索、添加文件夹和子文件夹时具有完全的灵活性。 Apple 的原生文件选择器非常笨拙; 您无法选择通过关键字搜索的多个目录,然后一次选择一堆文件。 它根本不是为此而设计的。
- Library management. 在这里您可以管理添加的歌曲和组织播放列表。 在大多数情况下,我反映了 Apple 在他们的 Music 应用程序中的做法,这对我来说已经足够好了。
- Player and playback. 应用程序的这一部分管理队列管理(重复、随机播放)等,以及播放、停止和下一首歌曲功能。
此处显示了一个简单的用户流程图:
User flow in practice: 当应用程序在没有库的情况下启动时,它会停留在 Sync 选项卡上,显示一个大的“Add iCloud Source”按钮。 在那里选择一个文件夹,Sync 屏幕会显示一个进度条,同时遍历该树。 索引完成后,它会立即将您切换到 Library 选项卡,该选项卡的第一个屏幕列出了 Playlists / Artists / Albums / Songs。 进入任何列表,点击一首曲目,Mini-Player 就会从底部弹出; 点击该迷你栏以打开具有随机播放、重复、队列重新排序和音量的全屏 Player。 滑动或点击关闭图标,您将直接返回 Library,同时继续播放。 任何时候您需要更多音乐,都可以跳回 Sync,点击导航栏中的“+”,选择另一个文件夹,导入服务会在后台合并新歌曲,无需重新启动。
Backend-Like Logic Layer
拥有 Web/云背景并在初创公司工作期间发布了大量服务器代码,因此我为移动应用程序采用了类似后端的架构。 整个域/逻辑层与 View 和 View-Model 层分离,因为我必须完成应用程序的 cloud syncing, metadata parsing 方面,并且可以干净地访问 SQLite DB。 这是我在此处使用的近似分层架构图:
How the layers talk: SQLite 位于底部,存储原始歌曲行和 FTS 索引。 然后,存储库包装数据库并公开异步 API。 在这些之上是我的 domain actors,Swift actors,它们拥有所有业务规则(导入、搜索、队列逻辑),因此状态变化保持 线程安全。 ViewModels 订阅 actors,将数据转换为 UI 就绪的结构,而 SwiftUI 视图只是呈现它们获得的任何内容。 没有什么直接跨越层,从而使 iCloud 同步、播放和 UI 很好地解耦。
Implementing Full Text Search with SQLite
就像我之前提到的那样,您可以导入具有 FTS 功能的 SQLite 版本真是太好了:从 iOS 11 左右开始,它就可以开箱即用,无需额外设置。 这使得将模糊搜索集成到我的音乐库中变得很容易,而无需任何第三方依赖项。 此外,我使用 SQLite.swift 库进行常规查询(它用作具有编译时安全性的查询生成器); 但是,对于 FTS 查询,我不得不求助于常规 SQL 语句。
SQLite 的 FTS5 扩展最终成为架构中最有价值的部分之一。 它允许我跨文件名和元数据(如艺术家、专辑和标题)进行查询,而无需额外的索引基础设施。
Setting Up the FTS Tables
| Domain | Swift actor / repo | FTS5 table | Columns that get indexed |
| -------------- | ---------------------------- | ----------------- | ---------------------------- |
| Library songs | SQLiteSongRepository
| songs_fts
| artist
, title
, album
, albumArtist
|
| Source-browser paths | SQLiteSourcePathSearchRepository
| source_paths_fts
| fullPath
, fileName
|
我使用了两个 FTS5 表:一个用于索引歌曲(艺术家/标题/专辑),一个用于文件夹导入期间的文件路径。 两个表都与普通 B 树表(songs
、source_paths
)中的主行相邻。 对于 UI,FTS 是只读的; 所有写入都发生在存储库内部,因此没有任何东西会从中溜走。
Creating the search index
SQLite 的内置 FTS5 使快速搜索变得容易。 这是我使用的一个简单的表定义:
try db.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
songId UNINDEXED,
artist, title, album, albumArtist,
tokenize='unicode61'
);
""")
我使用 unicode61
tokenizer 来确保处理各种字符。 不可搜索的键用 UNINDEXED
标记,因此它们不会膨胀术语词典。
Updating data reliably
为了保持简单和安全,我将更新和插入包装在事务中。 这确保了即使应用程序崩溃或中断,搜索索引也永远不会不同步。
func upsertSong(_ song: Song) async throws {
db.transaction {
// insert or update main song data
// insert or update search index data
}
}
Querying with Fuzzy Search
为了方便用户使用搜索,我自动添加了通配符支持。 如果您键入“lumine”,它会在内部搜索“lumine*”,即使使用部分查询也能立即获得结果。
我还利用 SQLite 的内置智能排名 (bm25
) 来返回更相关的结果,而无需额外的复杂性:
SELECT s.* FROM songs s
JOIN songs_fts fts ON s.id = fts.songId
WHERE songs_fts MATCH ?
ORDER BY bm25(songs_fts)
LIMIT ? OFFSET ?;
总的来说,使用原始 SQLite 为我提供了我需要的灵活性:可预测的模式、本地优先访问以及强大的全文搜索,而无需引入任何网络依赖项或外部服务。 这种方法非常适合设计为私有和离线优先的应用程序。
Working with iOS Files and Bookmarks
在 iOS 上,应用程序可以将持久书签存储到文件位置,但是安全范围内的书签,它授予对应用程序沙箱外部文件的扩展访问权限,仅在 macOS 上可用。 iOS 应用程序可以使用常规书签来记住文件路径,并通过文档选择器再次请求访问,但无法保证该访问权限会默默地持久存在。 请参阅 Apple 的书签文档。
为了缓解这种情况,我实现了一种回退机制,将文件复制到应用程序自己的沙盒容器中。 这避免了安全范围内的书签的脆弱生命周期,如果 iOS 重置权限,这些书签可能会默默地中断。 通过在后台主动复制文件,同时书签有效,因此不存在访问无效音频文件引用的风险。
这种方法还可以提高索引速度。 我可以扫描文件夹结构一次(在访问处于活动状态时),仅导入相关的音频文件,并安全地遍历深度嵌套的目录。 但是从外部位置可靠地播放单个音频文件,尤其是在设备重启后,对我来说仍然是一个未解决的问题。 这突出了即使对于原生应用程序,这种用例也不受支持,并且在 iOS 上可靠地处理文件访问仍然是多么复杂。
Building the Playback and UI
Metadata Parsing
为了从音频文件中解析元数据,我使用了 Apple 的 AVFoundation framework,特别是 AVURLAsset 类,它允许检查媒体文件元数据,例如标题、专辑艺术家等。虽然元数据解析由原生 SDK 处理,但某些字段(如音轨编号)您必须手动从 ID3 标签中查找。 我依靠 GitHub search 来查找示例,因为官方文档缺乏对边缘情况的覆盖。
Audio Playback with AVFoundation
在索引库之后,实现音频播放器感觉非常简单:您只需初始化一个 AVAudioPlayer
实例并让音频播放即可。 此外,对于生活质量功能:从控制中心播放音乐,我必须实现 AVAudioPlayerDelegate
协议,并且还连接到 Apple 的 MPRemoteCommandCenter
,它允许开发人员响应系统级别的播放控件。
Reflections: Apple, Developer Lock-In, and the Future
以下是在开发过程中脱颖而出的内容:
The Bad
Xcode 的局限性仍然令人沮丧。 实时 SwiftUI 预览绝对是一个进步,但总体开发体验仍然无法与五年前 Flutter 提供的体验相媲美:紧密的 VSCode 集成、实时模拟器重新加载以及熟悉的调试工具。
缺乏编辑器灵活性。 在 Neovim 或 VSCode 中为 Swift 设置 Language Server Protocol (LSP) 支持需要额外的工具,例如 xcode-build-server
,并且仍然无法完全匹配 Web 优先生态系统的开发体验。
Apple SDK 的某些角落仍然存在于 Objective-C 领域。 例如,Spotlight 文件搜索仅通过 NSMetadataQuery
公开,它使用 Key-Value Observing (KVO) 和字符串键,还没有 Swift 友好的包装器。 文档通常很稀疏,这会加剧学习曲线。
SwiftUI 的声明性 UI 很棒,但调试 iCloud 交互仍然需要手动模拟。 SwiftUI 预览无法模拟涉及 iCloud 权利的完整应用程序行为,因此您必须手动模拟云交互,这有点令人讨厌,但值得注意。
The Good
Async/await. 最后,我可以像编写命令式代码一样编写 I/O 绑定并发代码,而无需烦人的回调。 这是一个巨大的胜利,我非常感谢在 Actors 中编写同步代码是多么容易,并像在 JavaScript 生态系统中一样调用它。
Plethora of native libs. 是的,您不受 React Native/Flutter 生态系统中开源绑定的限制。 在这里,您可以更自由地开发比您的公司/产品网站替代品“更严肃”的东西(因为移动优先体验不佳)。 许多 Apple 的 API 都有示例,这使得入门变得容易。
SwiftUI 本身。 是的,React 风格的 UI 构建方法提供了更多的生产力和探索空间。 Apple 采用它真是太好了。
Summary: Building Should Be Easier
经过 1.5 周的破解,我能够获得完全满足我需求的软件:一个本地/离线音乐播放器,可以从云存储导入音频文件。
但是,开发人员很快意识到,他们现在无法轻松地将应用程序部署到自己的设备上并忘记它:应用程序只能在 7 天内运行,而无需开发证书,此后,您必须重新构建它,除非您向 Apple 支付 99 美元以注册开发计划。
即使在 欧盟的 DMA 法案 之后,侧载仍然没有完全开放。 欧盟用户现在可以直接从开发人员的网站安装来自第三方市场的应用程序,但前提是该开发人员仍然注册了 Apple 的 99 美元/年的计划,并且同意 Apple 的替代条款。 对于个人/业余爱好者使用,这仍然不会消除 7 天开发构建限制。
这最终没有任何意义。 一家创新的技术公司积极地为民主化的应用程序开发设置障碍。 即使是渐进式 Web 应用程序 (PWA) 在 iOS 上也面临着显着的限制:即使在 Apple 的 16-18.x 更新之后,iOS PWA 仍然在 Safari 的沙箱中运行。 它们获得了 WebGL2 和 Web 推送,但它们没有获得 Web 蓝牙/USB/NFC、后台同步或超过约 50MB 的保证存储空间。 WebGL 通过 Metal shim 运行,因此实际帧速率通常落后于原生 Metal 应用程序; 这对于 UI 来说已经足够好,但不适用于 AAA 3D 游戏。
如今,AI 通过以可访问的方式提供所有必要的知识,使任何人都可以应对未知的技术,从而降低了现代软件开发的复杂性。 您可以清楚地看到,Web 开发如何从非技术人员那里获得更多兴趣,他们有一种方法来构建他们的想法,而无需专门研究大量技术。 但是,当涉及到移动应用程序时,您只需遵守人为的规则。 即使您自己构建了它,为了自己,Apple 仍然拥有最终决定权,然后您才能运行它超过一周。 同一家曾经授权独立开发人员的公司现在施加了严格的限制,这些限制阻碍了个人应用程序的开发和分发。 AI 使构建新工具比以往任何时候都更容易,除非您为 iOS 构建,否则门仍然是锁着的。
Related Links
- iTunes Match – Apple Support
- Security-Scoped Bookmarks – Apple Docs
- FTS5 – SQLite Documentation
- Doppler Music Player – App Store
- Expo FileSystem Documentation
- Apple Developer Program Info (7-day builds)
- Apple Community: Files App & MP3 Playback
#swift#sqlite#icloud#ios#apple#xcode
Join the Newsletter
Get the latest technical deep dives and startup insights. Subscribe Previous Microservices Are a Tax Your Startup Probably Can’t Afford Back to top Copyright © Oleg Pustovit 2025 | Privacy Policy 07:20 PM GMT+3 follow me on