SwiftUI 框架

在 iOS 應用 SQLite 來處理數據 大大提高 App 效能

不論你是希望數據庫的儲存量可以更大,或是可以更好地控制數據,SQLite 和 SQL 都絕對不會讓你失望!在 App 中使用 SQLite,不但可以提高 App 的效能,在使用 CloudKit 同步數據也會變得更容易。在這篇文章中,Mattia 會帶大家簡單了解如何在 Swift 專案中使用 SQL。
在 iOS 應用 SQLite 來處理數據 大大提高 App 效能
在 iOS 應用 SQLite 來處理數據 大大提高 App 效能
In: SwiftUI 框架
本篇原文(標題:SQLite on iOS: The MVVM Way)刊登於作者 Medium,由 Sarah 所著,並授權翻譯及轉載。

上星期,我在 NetNewsWire 想要找些有趣的東西在專案上實作。

我看到一個不是使用 CoreData 的專案,而是使用 SQLite 的。這個專案的作者在 Sundell 播客第 95 期上,就解釋過為什麼採用 SQLite 可以大大提高效能。

如果你希望數據庫的儲存量可以更大,或可以更好地控制數據,又或是你只是想試試使用 Table 和 SQL lite 語句 (statement),這篇文章都很適合你!

我想實作這個專案有幾個原因。首先我不是很熟識 CoreData 和 CloudKit 的整合,加上我對 GUIs 沒有興趣。另外,我的工作主要圍繞後端服務,因此我比較喜歡使用 data layer,然後自己改善索引和查詢,並好好記下來。

如果你想先看看完整的程式碼,可以參考這個連結

FMDB 簡介

FMDB 是一個建基於 SQLite 的 Objective-C 包裝器 (wrapper),它是開源的,而且設置非常容易,可以說是唯一一個這麼好的程式庫 (Library)。(如果你知道有其他更好的程式庫,歡迎留言與我分享,我也很想試試使用!)

設置

讓我們創建一個新的 Xcode 專案,我把它命名為 SQLiteIntro

這個 App 不會很複雜,因為我只是想簡單介紹 SQLite,讓大家簡單了解如何在 Swift 專案中使用 SQL。

包裝器

我們應該保持一個好習慣,就在專用的類別或結構中分開邏輯。在這個範例中,我們使用的是 SQL 數據庫 (database),因此我們要創建一個類別來抽像化 (abstract) 一些數據層邏輯,讓程式碼更加簡潔。

final class DataWrapper: ObservableObject {
    private let db: FMDatabase
    
    init(fileName: String = "test") {
        // 1 - Get filePath of the SQLite file
        let fileURL = try! FileManager.default
            .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent("\(fileName).sqlite")
        
        // 2 - Create FMDatabase from filePath
        let db = FMDatabase(url: fileURL)
        
        // 3 - Open connection to database
        guard db.open() else {
            fatalError("Unable to open database")
        }
        
        // 4 - Initial table creation
        do {
            try db.executeUpdate("create table if not exists users(username varchar(255) primary key, age integer)", values: nil)
        } catch {
            fatalError("cannot execute query")
        }
        
        self.db = db
    }
}

這就是我們程式碼的開頭了,很簡單吧!

程式碼非常直接:在 DataWrapper 類別初次被創建後,它就會查找數據庫檔案,如果檔案不存在,FMDB 就會以該路徑 (path) 創建一個數據庫。最後,它會打開數據庫的連接,並創建 user table。

模型 (Model)

接下來我們會建立一個 User 結構,來處理數據庫紀錄。在範例 App 中,我們會添加一些其他與 JSON 相關的內容。我們稍後會使用一些 Web API 來創建一些隨機名稱的 User。

struct User: Hashable, Decodable {
    let username: String
    let age: Int
    
    init(username: String, age: Int) {
        self.username = username
        self.age = age
    }
    
    init?(from result: FMResultSet) {
        if let username = result.string(forColumn: "username") {
            self.username = username
            self.age = Int(result.int(forColumn: "age"))
        } else {
            return nil
        }
    }
    
    private enum CodingKeys : String, CodingKey {
        case username = "first_name"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        username = try container.decode(String.self, forKey: .username)
        age = Int.random(in: 1..<100)
    }
}

每當我們從 database 進行查詢時,即使結果只有一個,或是沒有結果,我們都會得到一個 FMResultSet。因此,在這種情況下一個專用的 init 函式就十分有用,可以幫我們處理所有設置邏輯。

Combine 和 MVVM

因為我使用的是 SwiftUI,我希望 DataWrapper 可以是響應式 (reactive) 的,並就數據庫中可能發生的變化通知視圖。 讓我們回到 DataWrapper,添加 @Published User 陣列,這樣就可以在一個 List 中顯示 User。

final class DataWrapper: ObservableObject {
    private let db: FMDatabase
    @Published var users = [User]()
    ...
}

我們想要從數據庫中獲取 User,並在數據庫打開後立即進行發佈。因此,我們需要創建一個方法來查詢所有 User,並在數據庫初始化後,將它們設置為 DataWrapper 的 Users 變數。

func getAllUsers() -> [User] {
    var users = [User]()
    do {
        let result = try db.executeQuery("select username, age from users", values: nil)
        while result.next() {
            if let user = User(from: result) {
                users.append(user)
            }
        }
        return users
    } catch {
        return users
    }
}

然後,把這段程式碼放在 DataWrapperinit 方法的最後:

users = getAllUsers()

現在,當我們第一次啟動 DataWrapper 時,DataWrapper 就會自動獲取所有 User,而且這些 User 是可用於 SwiftUI 的。

接著,讓我們建立一個 insert 函式。我們稍後會用到它。

func insert(_ user: User) {
    do {
        try db.executeUpdate(
            """
            insert into users (username, age)
            values (?, ?)
            """,
            values: [user.username, user.age]
        )
        users.append(user)
    } catch {
        fatalError("cannot insert user: \(error)")
    }
}

簡單的 SwiftUI 視圖

我想創建一個 List,來顯示數據庫中的所有使用者,並創建一個簡單的函式,來向 Web API 獲取隨機的使用者名稱,並將新使用者插入到數據庫。

struct ContentView: View {
    @EnvironmentObject var db: DataWrapper
    
    var body: some View {
        NavigationView {
            List(db.users, id: \.self) { user in
                HStack {
                    Text(user.username)
                    Spacer()
                    Text("\(user.age)")
                }
            }
            
            .navigationTitle("Users")
            .toolbar {
                ToolbarItem(id: "plus", placement: .navigationBarTrailing, showsByDefault: true) {
                    Button(action: {
                        createRandomUser()
                    }, label: {
                        Image(systemName: "plus")
                    })
                }
            }
        }
    }
    
    private func createRandomUser() {
        let url = URL(string: "https://random-data-api.com/api/name/random_name")!
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else {
                fatalError("No data")
            }
            
            DispatchQueue.main.async {
                let user = try! JSONDecoder().decode(User.self, from: data)
                db.insert(user)
            }
        }
        task.resume()
    }
}

如果我們現在執行 App,會看到一個空的列表。但只要點擊右上的加號,就可以在數據庫中加入內容,而列表的名稱也會實時在你的列表中出現。

總結

這篇文章是一個非常簡單的範例,用另一種方式來在熟悉的 SQLite 數據庫中存儲數據,你可以看到 App 的效能比 CoreData 版本大大提高。

如果你想更好地控制數據,SQLite 和 SQL 絕對不會讓你失望!對於需要精密控制和查詢優化器 (query optimization) 的 App 來說,SQLite 可以大大提高效能。使用 CloudKit 同步數據也會變得更容易,因為現在我們只需要同步 SQLite 檔案,而無需處理其他 CoreData Table 和不同的版本。

接下來,我會寫一篇關於 SQLite 遷移 (migration) 的文章。請大家密切留意,不要錯過這個有趣的主題!

你也可以在我的網站上閱讀這篇文章。

本篇原文(標題:SQLite on iOS: The MVVM Way)刊登於作者 Medium,由 Mattia 所著,並授權翻譯及轉載。
作者簡介:Mattia Righetti,軟體工程師,熱血的 iOS 和 macOS App 開發者。歡迎到我的網站閱讀其他文章。
譯者簡介:Kelly Chan-AppCoda 編輯小姐。

作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。