在移動應用開發(fā)日益依賴云服務與框架的今天,一位獨立開發(fā)者卻選擇回歸本地、離線優(yōu)先的設計初衷:用 1.5 周時間打造一款簡潔的 iOS 音樂播放器。
談及為什么要自己動手開發(fā)時,其在博客上表示:“2025 年,在 iPhone 上想聽自己收藏的音樂竟然變得挺麻煩的。要么掏錢給蘋果,要么得繞過一堆限制,挺煩的。于是我干脆從頭寫了一個自己的音樂播放器,能全文搜索、支持 iCloud,還優(yōu)先本地播放?!?/p>
與此同時,他還將這個項目在 GitHub 上開源了出來,供更多的開發(fā)者參考:https://github.com/nexo-tech/music-app
原文鏈接:https://nexo.sh/posts/why-i-built-a-native-mp3-player-in-swiftui/
作者 | Oleg Pustovit
編譯 | 蘇宓
出品 | CSDN(ID:CSDNnews)
以下為譯文:
為什么要自己動手開發(fā)一款播放器?
和很多人一樣,我是那種會不知不覺訂閱了一堆服務的人,有些是通過蘋果官方渠道買的(比如 iCloud、Apple Music),還有一些是在其他平臺上糊里糊涂續(xù)費的(像 Netflix,我居然都忘了還在付錢)。我其實也常用 Apple Music(之前也用過 Spotify),但后來發(fā)現(xiàn)流媒體聽歌其實只是圖個方便,真說不上是剛需。我自己整理了一個本地音樂庫后,也沒覺得損失什么,反倒不用被平臺綁定了。
一開始我以為,取消 Apple Music 后還能繼續(xù)用 iCloud 音樂庫同步歌曲,結(jié)果一取消訂閱,同步功能也沒了——原來這功能是要錢的。雖然可以通過 iTunes Match(每年 24.99 美元)重新開通,但本質(zhì)上只是把 256kbps 的 AAC 文件傳上云端,原始音質(zhì)的文件還是留在你設備上,除非你自己手動替換?,F(xiàn)在的 Mac 上,這一切都要在 Music App 里操作。如果你一個會員都不買,那同步功能就徹底沒戲了,只能靠連線或 Wi-Fi 手動同步。
我實在受夠了這些限制,就決定自己動手。
如果我買了個計算設備(這里是 iPhone),那我為什么不能用代碼把它變成我想用的樣子呢?我想做的其實很簡單:能加載音樂、整理它們、正常播放,順便也提醒一下自己——iPhone 其實還是一臺通用電腦,我應該有權(quán)自己決定怎么用它。
現(xiàn)在 Apple 和其他軟件的現(xiàn)狀
在開始寫自己的 App 之前,我也先去研究了一下官方和第三方有哪些聽本地音樂的方案。
Apple 自帶的應用
從技術(shù)上講,你可以在「文件」App 里直接播放 iCloud 里的音樂,但它根本不是為聽歌設計的。沒有播放列表、沒有標簽整理、沒有播放隊列……雖然勉強能放歌,但體驗非常差,幾乎沒法用。
第三方 App
我去 App Store 找了一圈,雖然有不少看起來不錯的 App,但很多都要訂閱付費。說實話,這種只播放本地音樂的 App,搞訂閱制真有點說不過去。
有一個叫 Doppler 的 App 我還挺喜歡的,試用了一下,它的界面主要圍繞「專輯」來組織,搜索功能不太行,從 iCloud 導入音樂也挺慢,而且處理層級多的文件夾時特別麻煩。不過好的一點是,它是一次性買斷,不搞訂閱。
自己動手:技術(shù)折騰過程
于是我決定親手做一個滿足我需求的理想播放器:
可以在 iCloud 文件夾里全文搜索音樂,快速選中并導入;
至少要有跟官方音樂 App 差不多的功能:播放隊列、播放列表、按專輯整理等等;
界面要順手、看著舒服。
一開始試了 React Native
一開始我沒用 Swift,因為之前用過一次,感覺不太好。當時雖然它的語法挺像 TypeScript,也有點 Rust 那種內(nèi)存安全的風格,但那時候沒有原生的 async/await,用起來比 Go 或 JS/TS 寫并發(fā)代碼麻煩多了,得寫一堆模板代碼,讓人挺沮喪的。所以這次我就想換個熟一點的技術(shù)棧。
我先用 React Native 或 Expo 來試試,想著可以復用以前做網(wǎng)頁的經(jīng)驗,還能套用現(xiàn)成的 UI 模板。播放界面做起來挺順利的,網(wǎng)上有很多開源項目和教學視頻。
我選了 Gionatha Sturba 做的一個模板項目(https://github.com/CodeWithGionatha-Labs/music-player),它幾乎具備我需要的所有功能。
我本來想通過現(xiàn)成的工具處理文件訪問和 iCloud 同步,但很快就遇到了大問題。像 expo-filesystem 這樣(https://docs.expo.dev/versions/latest/sdk/filesystem/)的庫,雖然可以用來選文件,但要遞歸掃描 iCloud 里層級很深的文件夾,經(jīng)常失敗,甚至直接把 App 弄崩了。這時候我意識到:JavaScript 的方案反而把事情搞復雜了,還不如直接用 Apple 的原生 API,雖然學起來難度大點,但更靠譜。
iOS 系統(tǒng)的沙盒機制限制很嚴,App 要訪問用戶的文件必須獲得明確授權(quán)。React Native 在這方面很不穩(wěn)定,想訪問 iCloud 里的文件夾不太現(xiàn)實。于是我決定轉(zhuǎn)向 Swift 開發(fā),這樣對文件訪問和權(quán)限控制能掌握得更細致。
轉(zhuǎn)向 SwiftUI
我選了 SwiftUI 而不是 UIKit 或 storyboard,因為它的語法更現(xiàn)代、聲明式風格更清爽,不會讓 UI 代碼干擾到我主要關(guān)注的邏輯處理和數(shù)據(jù)同步。Swift 的 async/await 和 actor 模型也很好用,讓我在處理并發(fā)和數(shù)據(jù)流時省了不少力。SwiftUI 還能把 App 分成更清晰的 ViewModel 結(jié)構(gòu),這對我用 LLM(比如 OpenAI o1 或 DeepSeek)寫 UI 代碼也有幫助——模型可以直接輸出干凈的界面或綁定代碼,不會出現(xiàn)各種亂七八糟的依賴。
應用架構(gòu)與數(shù)據(jù)模型
整個 App 的架構(gòu)我參考了寫后端服務的方式來做:用 SQLite 存儲數(shù)據(jù),設計成一個簡單但清晰的邏輯系統(tǒng)。我沒有用 Apple 的 CoreData,因為我需要更高的自由度,比如自己定義數(shù)據(jù)庫結(jié)構(gòu)、寫原生 SQL、做全文搜索等等。而 SQLite 原生支持 FTS5(全文搜索引擎),讓我能非常高效地做模糊查找,不用再額外集成 Elasticsearch 或自己造輪子。
三個主要界面
這個 App 一共分三個核心頁面:
導入音樂庫:用戶選擇 iCloud 文件夾后,App 會掃描所有子目錄,找出音頻文件,并把它們的路徑存到 SQLite 數(shù)據(jù)庫中。這樣你就可以自由地搜索、添加文件夾和子文件夾了。Apple 自帶的文件選擇器非常不好用,不能一次性選中多個目錄或按關(guān)鍵詞篩選一批文件,這就是它的硬傷。
音樂管理界面:這里可以瀏覽和管理已導入的歌曲,整理播放列表。我大致照搬了 Apple Music 的操作邏輯,對我來說已經(jīng)夠用了。
播放器:負責播放、暫停、切歌、重復、隨機播放、排隊等功能。
一個簡單的用戶使用流程圖如下:
第一次打開 App 時,如果還沒有導入音樂,會先進入 “同步” 頁面,中間有個很大的「添加 iCloud 文件夾」按鈕;
選好一個文件夾后,App 會開始掃描文件,進度條會顯示處理進度;
掃描完后,會自動跳轉(zhuǎn)到 “音樂庫” 頁面,展示播放列表 / 藝人 / 專輯 / 歌曲;
點進任意一項,播放條會出現(xiàn)在底部;點播放條可以展開全屏播放器,里面有隨機播放、重復、隊列排序、音量控制等功能;
你可以隨時返回音樂庫,音樂繼續(xù)播放不受影響;
想添加更多音樂,只需返回同步頁,點右上角的「+」,再選新的文件夾,系統(tǒng)會在后臺自動合并進當前音樂庫,無需重啟。
像做后端一樣設計邏輯層
我之前寫后端服務比較多,干脆把 App 的邏輯層也按后端思路來做。整個領(lǐng)域/邏輯層(處理同步、搜索、隊列等)完全跟 UI 分離,數(shù)據(jù)存取也用 SQLite 操作得很精細。
簡單說下架構(gòu)是這樣分層的:
最底層是 SQLite,存原始歌曲數(shù)據(jù)和全文搜索索引;
上面一層是 Repository,封裝數(shù)據(jù)庫訪問邏輯,提供異步 API;
然后是 Domain Actor(Swift actor),負責業(yè)務邏輯,比如導入、搜索、排隊等;
ViewModel 再訂閱這些 Actor,把數(shù)據(jù)轉(zhuǎn)換成適合 UI 展示的格式;
最上層的 SwiftUI 只負責“展示”數(shù)據(jù),所有狀態(tài)和邏輯處理都已經(jīng)在底層搞定,彼此之間不會耦合。
這樣一來,iCloud 同步、播放功能和界面展示都能分工明確、互不干擾。
在 SQLite 中實現(xiàn)全文搜索
我前面提到,iOS 從大概 iOS 11 開始,就原生集成了帶 FTS 功能的 SQLite。這太好了,我可以不依賴第三方搜索引擎,就實現(xiàn)模糊搜索。
我用 SQLite.swift 這個庫來寫一般的數(shù)據(jù)庫查詢(它有點類似于安全型 SQL 構(gòu)造器),但 FTS 搜索還是得用原生 SQL 語句來寫。
SQLite 的 FTS5 功能對我來說非常關(guān)鍵,它能快速搜索歌曲名、藝人、專輯這些字段,而且不需要額外的索引系統(tǒng)。
創(chuàng)建全文搜索索引表
我建了兩個 FTS 表:一個用于索引歌曲(藝術(shù)家/標題/專輯),另一個用于文件夾導入期間的文件路徑。兩個表都放在普通的 B-tree 表(songs、source_paths)旁邊。FTS 表在 UI 層是只讀的,所有寫入都通過 Repository 完成,保證不會漏掉任何數(shù)據(jù)。
創(chuàng)建搜索索引
一個簡單的建表語句如下:
try db.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS songs_fts USING fts5(
songId UNINDEXED,
artist, title, album, albumArtist,
tokenize='unicode61'
);
""")
這里用了 unicode61 分詞器,可以支持更多不同語言和字符類型。而像 songId 這樣的字段我標記為 UNINDEXED,防止它們占用太多索引空間。
數(shù)據(jù)可靠更新
為了保證簡單又安全,我把所有的更新和插入操作都包裹在事務中。這樣一來,即便應用崩潰或中斷,搜索索引也不會不同步。
func upsertSong(_ song: Song) async throws {
db.transaction {
// 插入或更新主歌曲數(shù)據(jù)
// 插入或更新搜索索引數(shù)據(jù)
}
}
模糊搜索查詢
為了讓搜索體驗更友好,我自動添加了通配符支持。比如你輸入“l(fā)umine”,系統(tǒng)內(nèi)部會搜索“l(fā)umine*”,即便是部分匹配也能立刻返回結(jié)果。
我還利用了 SQLite 內(nèi)置的智能排序算法(bm25),能在不增加額外復雜度的前提下返回更相關(guān)的結(jié)果:
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 提供了我所需的靈活性:可預期的 schema、本地優(yōu)先的訪問方式、強大的全文搜索功能,而且無需依賴網(wǎng)絡或外部服務。這種方式非常適合一個注重隱私、強調(diào)離線使用的應用。
與 iOS 文件系統(tǒng)和書簽交互
在 iOS 上,應用可以存儲對文件位置的持久書簽(bookmarks),但所謂的“安全作用域書簽”(security-scoped bookmarks),即允許訪問沙盒外部文件的權(quán)限機制,僅在 macOS 上可用。iOS 應用只能使用普通書簽記錄路徑,之后需通過文檔選擇器再次請求訪問權(quán)限,而且這類訪問不能悄無聲息地持續(xù)生效。
為了緩解這個問題,我實現(xiàn)了一種回退機制:將文件復制到應用自身的沙盒目錄中。這樣可以規(guī)避安全作用域書簽生命周期脆弱的問題——比如 iOS 重置權(quán)限后可能導致訪問失敗。我選擇在后臺主動復制文件,只要書簽仍然有效,就能確保不會訪問到無效音頻路徑。
這種方式還能提升索引速度。我可以在訪問權(quán)限仍然有效時一次性遍歷整個目錄結(jié)構(gòu),只導入相關(guān)音頻文件,并可靠地深入嵌套目錄。但要在設備重啟后仍能穩(wěn)定播放這些外部音頻文件,目前我還沒找到解決方案。這也說明,即便對原生開發(fā)者來說,iOS 文件訪問的這一用例仍然缺乏支持,處理起來也依然復雜。
構(gòu)建播放功能和用戶界面
元數(shù)據(jù)解析
為了從音頻文件中解析元數(shù)據(jù),我使用了 Apple 的 AVFoundation 框架,尤其是 AVURLAsset 類,可以用來檢查媒體文件的標題、專輯藝術(shù)家等元信息。
雖然大部分元數(shù)據(jù)由原生 SDK 處理,但比如曲目編號等字段仍需手動從 ID3 標簽中提取。我通過 GitHub 搜索(https://github.com/TastemakerDesign/Warper/blob/2af8c07ad8422f4dc3a539177d3a76ee8502e632/plugins/flutter_media_metadata/ios/Classes/Id3MetadataRetriever.swift)找到了一些處理邊緣情況的代碼示例,因為官方文檔在這方面覆蓋非常有限。
音頻播放功能
當音樂庫完成索引后,構(gòu)建一個播放器其實很簡單:初始化一個 AVAudioPlayer 實例播放音頻即可。為了支持系統(tǒng)控制中心播放功能,我實現(xiàn)了 AVAudioPlayerDelegate 協(xié)議,并接入了 Apple 的 MPRemoteCommandCenter,從而可以響應系統(tǒng)級的播放控制事件。
一些反思
不足之處
Xcode 的局限性仍然令人沮喪。SwiftUI 的實時預覽確實是進步,但整體開發(fā)體驗依舊無法與五年前的 Flutter 相提并論——Flutter 擁有緊密的 VSCode 集成、實時模擬器熱重載和熟悉的調(diào)試工具。
編輯器靈活性差。想要在 Neovim 或 VSCode 中配置 Swift 的 Language Server Protocol(LSP)支持,你還得安裝像 xcode-build-server 這樣的工具,效果依舊趕不上 web 開發(fā)那種輕快的體驗。
Apple 的 SDK 有些部分還停留在 Objective-C 時代。比如 Spotlight 文件搜索功能只能通過 NSMetadataQuery 使用,采用 KVO 和字符串鍵,沒有 Swift 友好的封裝。加上文檔稀缺,學習成本也就更高。
SwiftUI 的聲明式 UI 很棒,但調(diào)試 iCloud 相關(guān)功能仍需手動 mock。因為 SwiftUI 的預覽無法模擬涉及 iCloud 權(quán)限的完整行為,必須自行模擬云端交互,雖然只是個小問題,但確實麻煩。
優(yōu)點之處
async/await 真是福音。終于可以像寫同步代碼一樣寫并發(fā)邏輯,告別煩人的回調(diào)地獄。甚至可以在 Actor 里寫 I/O 密集的邏輯,像 JavaScript 那樣直接調(diào)用,非常絲滑。
豐富的原生庫支持。在 React Native/Flutter 等生態(tài)里你可能會受限于開源綁定質(zhì)量,但在 iOS 原生開發(fā)中你可以更自由地做“嚴肅一點”的應用。Apple 提供的許多 API 都附帶示例代碼,入門門檻反而降低了。
SwiftUI 本身就很棒。React 風格的 UI 構(gòu)建方式讓開發(fā)效率更高、更容易試驗和構(gòu)思。Apple 采納它實在是明智之舉。
總結(jié):構(gòu)建應用本應更簡單
折騰了 1.5 周之后,我做出了一個完全滿足個人需求的軟件 —— 一個可以從云端導入音頻文件的本地/離線音樂播放器。
但很快你會意識到,如今的開發(fā)者很難自由地把自己寫的 App 裝到設備上長期使用。沒有開發(fā)者證書,應用只能運行 7 天,之后你必須重新構(gòu)建。除非你每年向 Apple 支付 99 美元,注冊開發(fā)者計劃。
即便在歐盟《數(shù)字市場法》(DMA)生效后,sideloading(側(cè)載)也仍非完全開放。雖然 EU 用戶現(xiàn)在可以從第三方網(wǎng)站直接安裝 App,但前提是開發(fā)者已經(jīng)加入 Apple 的 $99/年開發(fā)者計劃,并接受 Apple 的“替代條款”。對于純粹的個人或愛好者來說,這并未真正解除“7 天使用期”的限制。
這根本說不通。一家聲稱推動技術(shù)創(chuàng)新的公司,反而在人為設置障礙,阻礙開發(fā)者自由創(chuàng)作。即便是漸進式 Web 應用(PWA)在 iOS 上也存在明顯限制:即使在 iOS 16~18.x 中,PWA 仍運行在 Safari 的沙盒里。它們獲得了 WebGL2 和 Web 推送,但仍然缺乏 Web 藍牙/USB/NFC、后臺同步,甚至連超過約 50MB 的穩(wěn)定存儲也不支持。WebGL 還通過 Metal 的中間層運行,實際幀率遠低于原生 Metal 應用——對 UI 來說勉強夠用,但無法支撐真正的 3A 級 3D 游戲。
現(xiàn)在 AI 已經(jīng)顯著降低了現(xiàn)代軟件開發(fā)的門檻,讓任何人都能快速上手未知技術(shù)、構(gòu)建自己的工具。我們也看到 Web 開發(fā)因其開放性吸引了大量非技術(shù)背景的創(chuàng)作者,他們無需掌握一堆技術(shù)就能實現(xiàn)自己的想法。但在移動開發(fā)領(lǐng)域,你還是得遵守一整套人為的規(guī)則。即使是你自己為自己做的 App,Apple 仍然握有決定權(quán),限制你運行超過 7 天。
這家公司曾經(jīng)點燃了獨立開發(fā)者的夢想,如今卻親手關(guān)上了那扇自由的大門。在 AI 讓一切變得更簡單的時代里,唯有 iOS 開發(fā)仍被死死鎖住。
——對話 IEEE 首位華人主席、美國雙院院士劉國瑞 | 萬有引力
2025 全球產(chǎn)品經(jīng)理大會
2025 年 8 月 15–16 日
北京·威斯汀酒店
2025 全球產(chǎn)品經(jīng)理大會將匯聚互聯(lián)網(wǎng)大廠、AI 創(chuàng)業(yè)公司、ToB/ToC 實戰(zhàn)一線的產(chǎn)品人,圍繞產(chǎn)品設計、用戶體驗、增長運營、智能落地等核心議題,展開 12 大專題分享,洞察趨勢、拆解路徑、對話未來。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.