LeetCode 題目隨著我們一題一題實作過後,我們需要知道到底做過了多少題目,將它收藏起來,不想要打開 App 的時候發現怎麼跟剛下載的一樣,一點變化都沒有,也不記得上次做過哪幾題,本次主題就是要利用 SwiftUI 的 UserDefault
和 @AppStorage
來幫助記憶實作及收藏過的 LeetCode 題目。
這是 iOS 系統用來簡單數據持久化的解決方案,也就是說它會以鍵值對(Key-Value)的方式儲存在本機,建議儲存的是小量的資料,像是用戶的設置、偏好,應用程式的狀態訊息等等。
也就是說如果我們有把資料存在 UserDefaults
,當 App 開啟→結束→再開啟時,資料可以再次被讀取,而如果沒有存,再開啟時就會消失,恢復預設。
UserDefaults
可以用來儲存以下基本類型:
String
Int
Double
Float
Bool
URL
Data
Date
比較多元的資料,可以將自定義的類型轉換成 Data
後再存,或者實現 Codable
protocol 以便於物件之間的序列化和反序列化,然後再存。
// 實例化 UserDefaults
let defaults = UserDefaults.standard
// 儲存資料
defaults.set("John Doe", forKey: "userName")
// 讀取資料
if let name = defaults.string(forKey: "userName") {
print("User Name: \(name)")
}
以上為儲存以及讀取的 Swift 範例程式碼。
@AppStorage
是 SwiftUI 專屬用來儲存資料持久化的一個簡單的屬性包裝器,它只能在 SwiftUI 裡面的 View
使用,而且由於 @AppStorage
是一個綁定,它可以自動更新 View
,從而使 View
能夠反映儲存資料的最新更改。而它的底層也是保存到 UserDefaults
,只是有了它我們不用再調用落落長的語法。
SwiftUI 使用方式如下:
@AppStorage("userName") var userName: String = "John Doe"
var body: some View {
VStack {
Text("Welcome, \(userName)!")
TextField("Enter your name", text: $userName)
.padding()
}
}
一樣必須要注意的是 “userName”
需要是唯一 Key 值,它綁定於變數 userName,所以變數更新它也會跟著更新。如果它不是唯一 Key值,那就要注意其他地方如果也使用可能造成數值被修改,就要小心,但有了它讓 SwiftUI 在保存簡單數值時更加容易。
這兩個儲存方式都要注意不要存太大量的資料,否則開啟 App 會變得十分緩慢,因為它會把資料讀取後存在記憶體,而本文只簡單存 LeetCode 題目編號,紀錄實作過的題目。
首先無疑是先定義 LeetCode 題目資料物件,Identifiable 前面文章提過是為了要給 SwiftUI List
獨立識別 ID,這邊為了持久化資料,也就是存到 UserDefaults
,所以需要增加 Codable
的 protocol。
struct LeetCodeProblem: Identifiable, Codable {
var id = UUID()
var title: String
var isFavorited = false
}
接下來我們定義一個 Manager 來管理儲存的資料操作。LeetCodeManager
繼承 ObservableObject
且它的變數 problems
前面加上 @Published
是為了讓 SwiftUI 畫面取用資料且能夠同步更新。
這裡最重要的是,favoriteProblems
它就是我們自定義的 LeetCode 題目資料,需要存在手機本機,讓它再次打開 App 還能夠看到紀錄,所以前面加了 @AppStorage("favoriteProblems")
標記。
import SwiftUI
class LeetCodeManager: ObservableObject {
@AppStorage("favoriteProblems") var favoriteProblems: Data?
@Published var problems: [LeetCodeProblem] = [
LeetCodeProblem(title: "Two Sum"),
LeetCodeProblem(title: "Add Two Numbers"),
LeetCodeProblem(title: "Longest Substring Without Repeating Characters"),
// 加入更多題目...
]
init() {
if let data = favoriteProblems {
if let decoded = try? JSONDecoder().decode([LeetCodeProblem].self, from: data) {
problems = decoded
}
}
}
func toggleFavorite(for problem: LeetCodeProblem) {
if let index = problems.firstIndex(where: { $0.id == problem.id }) {
problems[index].isFavorited.toggle()
saveFavorites()
}
}
private func saveFavorites() {
if let encoded = try? JSONEncoder().encode(problems) {
favoriteProblems = encoded
}
}
}
再來我們的 SwiftUI 主畫面是顯示 LeetCode 題目列表,且可以讓使用者按收藏。
struct ContentView: View {
@ObservedObject var leetCodeManager = LeetCodeManager()
@State private var isSheetPresented = false
var body: some View {
NavigationView {
List(leetCodeManager.problems) { problem in
HStack {
Text(problem.title)
Spacer()
Button(action: {
leetCodeManager.toggleFavorite(for: problem)
}) {
Image(systemName: problem.isFavorited ? "heart.fill" : "heart")
}
}
}
.navigationTitle("LeetCode Problems")
.navigationBarItems(trailing: Button(action: {
// 打開Sheet
isSheetPresented.toggle()
}) {
Image(systemName: "heart.fill")
})
.sheet(isPresented: $isSheetPresented) {
FavoriteProblemsView(leetCodeManager: leetCodeManager)
}
}
}
}
而 @ObservedObject var leetCodeManager = LeetCodeManager()
表示leetCodeManager
中的數據發生變化時,畫面會自動更新。
最後打開 Sheet 另外一個 View 頁面,將會顯示我們收藏實作過的 LeetCode 題目。
struct FavoriteProblemsView: View {
@ObservedObject var leetCodeManager: LeetCodeManager
@Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
List(leetCodeManager.problems.filter { $0.isFavorited }) { problem in
Text(problem.title)
}
.navigationTitle("Favorite Problems")
.navigationBarItems(trailing: Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
Text("Close")
})
}
}
}
這裡持續使用 @ObservedObject
而不用另外一個 @StateObject
的原因是因為需要讓兩頁兩邊都能監聽到 leetCodeManager
的資料變化所以才如此使用。
@Environment
這個操作符表示連動這個 Sheet 系統原生的狀態,宣告了之後可以拿這個設定的變數去關掉 Sheet,是一個很特別的做法。
模擬器頁面收藏效果如圖(好不容易錄製 Gif ,畫質好像被壓縮了…)但是可以很明確看到收藏頁面的題目有連動我們原本 LeetCode 題目列表按的收藏項目。
在學 SwiftUI 的過程中發現很多東西都被大幅度的簡化,所以很多寫法沒辦法用以前寫程式的邏輯去想,有時候覺得不適應,但換個角度想,這樣的設計就是為了讓不懂寫程式的新手更好上手寫 App,只要知道怎麼調用語法就可以獲的想要的效果,不必學繁雜的程式碼運作,或許就是這套框架的精神,隨著一天一天學習過去,LeetCode SwiftUI App 知識也就越來越完整了,非常有成就感。