Using SQLite on iOS to process data - improving app performance

Introduction to FMDB

FMDB is an Objective-C wrapper based on SQLite. It is open source and easy to set up. It can be said that it is the only such good library (Library). (If you know of other better libraries, please leave a message to share with me, I'd love to try it out too!)

set up

Let's create a new Xcode project, I named it  SQLiteIntro.

This app will not be very complicated, because I just want to briefly introduce SQLite, so that everyone can easily understand how to use SQL in Swift projects.

wrapper

We should keep a good habit of separating logic in dedicated categories or structures. In this example, we are using a SQL database, so we need to create a class to abstract some of the data layer logic to make the code more concise.

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
    }
}
复制代码

This is the beginning of our code, simple enough!

The code is pretty straightforward: after the  DataWrapper class is first created, it looks for the database file, and if the file doesn't exist, FMDB creates a database at that path. Finally, it opens the connection to the database and creates the  user table.

Model

Next we will build a  User structure to handle database records. In the sample app, we'll add some other JSON-related content. We'll use some Web API later to create some random-named 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)
    }
}
复制代码

Whenever we query from the database, even if there is only one result, or no result, we will get one  FMResultSet. So in this case a dedicated init function is useful to handle all the setup logic for us.

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
    }
}
复制代码

然后,把这段程式码放在 DataWrapper 的 init 方法的最后:

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-](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 和不同的版本。

这里也推荐一些面试相关的内容!

Guess you like

Origin juejin.im/post/7083742883706044429