Contents
Chào mừng bạn đến với Fx Studio. Chúng ta tiếp tục tìm hiểu về chủ đề SwiftData, với 2 thao tác cũng khá thú vị & cần thiết. Đó là Sorts & Filters. Bên cạnh đó, mình cũng hướng dẫn các bạn áp dụng chúng vào dự án SwiftUI một cách đơn giản nhất.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Đây là bài viết thuộc dạng cơ bản và phần 2 của bài SwiftData – Hello world!. Do đó, nếu bạn chưa biết gì về SwiftData, thì hãy đọc Hello world
của nó trước nhóe. Và để đảm bảo mọi thứ hoạt động ổn định, bạn cũng phải chuẩn bị các versions OS & tools mới nhất:
-
- Xcode 15
- Swift 5.9
- SwiftUI 5
- iOS 17.0+
- iPadOS 17.0+
- macOS 14.0+
- tvOS 17.0+
- watchOS 10.0+
- visionOS 1.0+ (beta)
- …
Về mặt kiến thức chuyên môn, bạn cần nắm được lập trình iOS cơ bản hoặc lập trình iOS với SwiftUI. Nếu bạn chưa biết về chúng thì có thể bắt đầu bằng 2 link dưới đây:
Về project demo, bạn có thể tiếp tục với project ở bài viết Hello world
nhóe. Mình sẽ không thêm màn hình mới, công việc chúng ta vẫn làm Todo list mà thôi.
Tạo View
Bắt đầu, ta sẽ thực hiện việc tách code từ ContentView
ở bài viết trước nha. Gọi là TodoListView
nhóe. Code ví dụ như sau:
import SwiftUI import SwiftData struct TodoListView: View { //Query @Query var todoItems: [TodoItem] //Context @Environment(\.modelContext) private var modelContext var body: some View { List { ForEach(todoItems) { item in //Row HStack { VStack (alignment: .leading) { Text(item.name) Text(item.createDate.toString(dateFormat: "yyyy:MM:dd hh:mm:ss")) .fontWeight(.thin) .italic() } Spacer() if item.isComplete { Image(systemName: "checkmark") } } // select row .onTapGesture { item.isComplete.toggle() } } // Delete .onDelete(perform: { indexSet in for index in indexSet { let itemToDelete = todoItems[index] modelContext.delete(itemToDelete) } }) } } } #Preview { TodoListView() .modelContainer(for: TodoItem.self) }
Cũng khá đơn giản nha mọi người.
- Tách code phần UI cho
List
- Các
Navigation Title
và các thứ linh tinh khác ta để ởContentView
- Chỉnh lại chỗ
#Preview
để hiển thị lên màn hình Live Prevew bên cạnh
Còn với ContentView
, lúc này code cũng còn lại khá ít.
struct ContentView: View { @Query var todoItems: [TodoItem] @Environment(\.modelContext) private var modelContext @State var isShowAlert = false @State var taskContent = "" var body: some View { NavigationStack { TodoListView() // navigation bar .navigationTitle("To do list") // toolbar .toolbar{ Button("", systemImage: "plus") { // random add item //modelContext.insert(Helper.generateRandomToDoItem()) isShowAlert.toggle() } } .alert("Add new task", isPresented: $isShowAlert) { TextField("Content", text: $taskContent) .textInputAutocapitalization(.never) Button("OK", action: { addNewTask() }) Button("Cancel", role: .cancel) { } } } } func addNewTask() { let task = TodoItem(name: taskContent, isComplete: false) modelContext.insert(task) taskContent = "" isShowAlert = false } }
Bạn chỉ cần:
- Thêm một đối tượng
TodoListView
vào vị trí cần tách mà thôi - Mọi thứ vẫn hoạt động tốt & không cần thay đổi gì thêm
Sorts
Chúng ta có nhiều cách tiến hành Sort một danh sách trong SwiftData. Tùy thuộc vào yêu cầu của màn hình, mà bạn hãy chọn cách phù hợp.
Đơn giản
Cách đầu tiên, chính là bản đưa các tham số/tiêu chí cần thiết vào trực tiếp @Query
nhóe. Đây là câu lệnh Query cơ bản:
@Query var todoItems: [TodoItem]
Thay đổi một chút code cho việc khởi tạo @Query
@Query(sort: \TodoItem.createDate, order: .reverse) var todoItems: [TodoItem]
Trong đó:
- Chúng ta sẽ sắp xếp dựa theo tiêu chí thuộc tính
.createDate
trong lớpTodoList
. Dựa theo KeyPath để truy cập tới. - Sắp xếp theo chiều ngược lại, với giá trị
order
là.reverse
- Phần tạo biến vẫn không thay đổi gì
Gõ xong thì chờ tầm 5 giây, để xem màn hình Live Preview thay đổi như thế này.
Hoặc bạn muốn áp dụng 1 lúc nhiều tiêu chí sắp xếp dựa theo các thuộc tính mà bạn quan tâm. Ta sẽ dùng 1 biển thể khác của @Query
nha. Ví dụ, 2 tiêu chí cho createDate
& name
theo TodoItem
trong project demo.
@Query(sort: [SortDescriptor(\TodoItem.createDate), SortDescriptor(\TodoItem.name, order: .reverse)]) var todoItems: [TodoItem]
Vì đó là một mảng, nên bạn tha hồ thêm các đối tượng SortDescriptor. Cách dùng 1 tiêu chí chỉ là phiên bản thu gọn mà thôi.
Bạn có xem thêm về SortDescriptor từ tài liệu chính chủ của Apple nha.
Động
Tất nhiên là chúng ta sẽ ít khi nào fix cứng
như vậy cả. Do đó, việc sắp xếp này sẽ cần tùy thuộc vào tham số/điều kiện bên ngoài ảnh hưởng tới.
Bài toán lại khó hơn ở chỗ bản chất của @Query
là cần phải khởi tạo ngay. Chứ không theo kiểu Single source of truth được. Giải pháp cơ bản sẽ là:
- Tạo 1 hàm
init
để truyền tham số chosort
- Tham số truyền sẽ là một kiểu
State
Như vậy, khi tham số State thay đổi trạng thái. Nó kéo theo toàn bộ View sẽ render lại. Nghe qua cũng khá là EZ phải không nào. Quất tiếp thôi!
Setup
Bắt đầu, ta hãy thay đổi lại màn hình TodoListView với init
có tham số sort
. Code ví dụ như sau:
struct TodoListView: View { //Query @Query var todoItems: [TodoItem] init(sort: SortDescriptor<TodoItem>) { _todoItems = Query(sort: [sort]) } //... }
Trong đó:
- Khai báo Query đơn giản nhất để lấy dữ liệu từ TodoItem với thuộc tính
todoItem
- Thêm
init
với tham sốsort
- Gọi khởi tạo kiểu
Query
với tham sốsort
Code còn lại của TodoListView, chúng ta không thay đổi gì hết. Sang màn hình ContentView để thêm một thuộc tính State để quản lý trạng thái của tham số sort
@State var sort = SortDescriptor(\TodoItem.createDate)
Sau đó, thay đổi lại việc gọi TodoListView trong code của ContentView nhóe!
var body: some View { NavigationStack { TodoListView(sort: sort) .... } }
Khá EZ! Bạn hay thử reload lại màn hình Preview, nếu ổn thì sẽ hiển thị mình thường. Và dùng chức năng Add
từ bài trước, để kiểu tra việc thêm 1 đối tượng vào thì danh sách có thực sự sort
theo ý đồ của ta không.
Actions
Tiếp theo, chúng ta sẽ thêm các hành động. Mục đích đơn giản là với các hành động khác nhau, thì SwiftData sẽ như thế nào. View sẽ hiển thị danh sách lấy được với các sort
như thế nào?
Tại ContentView, bạn sẽ thay đổi các Bar Button tại .toolbar()
như sau:
.toolbar{ HStack { //Sort if sort.order == .forward { Button("", systemImage: "arrow.down.to.line") { sort = SortDescriptor(\TodoItem.createDate, order: .reverse) } } else { Button("", systemImage: "arrow.up.to.line") { sort = SortDescriptor(\TodoItem.createDate, order: .forward) } } //Add Button("", systemImage: "plus") { isShowAlert.toggle() } } }
Trong đó:
- Bỏ qua button
Add
vì nó ở bài viết trước rồi - Vì
sort
là biến trạng thái, nên ta có thểif else
để quyết định hiển thị nào phù hợp cho Buttor sắp xếp - Với mỗi điều kiện
order
ta sẽ khởi tạo lại biếnsort
Vì sort
là một State, nên bất kì lúc nào ghi nhận có sự thay đổi trạng thái. Thì các view mà sort
ảnh hưởng sẽ được render lại.
Bản chất vậy thôi. Giờ đợi Preview Live hiển thị xong, rồi chúng ta cùng test nhóe!
Filters
Tạm xong phần sắp xếp, qua phần lọc. Phần này cũng tương tự như sắp xếp. Nên mình sẽ không đề cập tới việc hướng dẫn từ cơ bản tới phức tạp. Quất luôn 1 phát ăn cả nha.
Predicate
Predicate là các điều kiện logic đánh giá một giá trị Boolean (đúng hoặc sai). Chúng thường được sử dụng để lọc và sắp xếp dữ liệu dựa trên các tiêu chí cụ thể. Với SwiftData, bạn có thể sử dụng các vị từ để xác định các điều kiện tìm nạp và thao tác dữ liệu từ kho lưu trữ dữ liệu của mình.
Ví dụ code nha
@Query(filter: #Predicate<Snippet> { $0.title.contains("test") }, sort: [SortDescriptor(\Snippet.creationDate)] ) var snippets: [Snippet]
Bạn sẽ thấy một Predicate sẽ được thêm vào tham số filter
của @Query
. Nội dung của chúng thì ta cũng bàn tới, tùy thuộc vào logic của bạn thôi.
Setup
Ta áp dụng vào project demo phần filter
cho todoItems
của màn hình TodoListView. Thay đổi tiếp init
của nó như sau:
init(sort: SortDescriptor<TodoItem>, keySearch: String = "") { //Query with Filter & Sort if keySearch.count > 0 { _todoItems = Query(filter: #Predicate<TodoItem> { $0.name.contains(keySearch)}, sort: [sort]) } else { _todoItems = Query(filter: #Predicate<TodoItem> { !$0.name.isEmpty}, sort: [sort]) } }
Mục đích logic là lọc các phần tử có tên ($0.name.contains()
) chứa keySearch
mà thôi. Về bản chất, cách làm này tương tự với cách sort
. Biến keySearch
sẽ được truyền từ bên ngoài vào và nó là một State. Nên khi nó thay đổi trạng thái, TodoListView sẽ render lại.
Actions
Ta sẽ thay đổi UI ở ContentView để có thể thêm hành động Filter vào cho TodoListView. Code như sau:
struct ContentView: View { //... //Search @State var keySearch = "" var body: some View { NavigationStack { TodoListView(sort: sort, keySearch: keySearch) // navigation bar .navigationTitle("To do list") //.. //search bar .searchable(text: $keySearch, prompt: "Seach") //... } } } //... }
Trong đó:
keySearch
là một thuộc tính State- Gọi lại khởi tạo cho TodoListView, với tham số
keySearch
mới - Thêm một
modifier
là.searchable()
cho TodoListView
Mặc dù, chúng ta không thấy được việc gán đối tượng này cho đối tượng khác. Vì đây là SwiftUI, cơ bản mọi thứ đã kết nối với nhau rồi. Thì chúng sẽ hoạt động ngon cơm thôi.
Giải thích cơ bản:
keySeach
là một State và ta sẽ truyền nó cho 2 phần tử TodoListItem & SearchBar- Chỉ cần trạng thái
keySearch
thay đổi hoặc bên này thay đổi thì sẽ ảnh hưởng bên kia. - Khi bạn gõ chữ ở SearchBar, kéo theo trạng thái của
keySearch
thay đổi. Nó sẽ tác động tới TodoListView thay đổi theo.
Build lên simulator để test hết toàn bộ lại nha. Chúc bạn thành công!
Tạm kết
- Tìm hiểu thêm về SwiftData
- Hai thao tác cơ bản là Sorts & Filter
- Áp dụng các SortDescriptor & Predicate để Query dữ liệu theo logic
Okay! Tới đây, mình xin kết thúc bài viết thứ hai về SwiftData với Sorts & Filters . Nếu có gì thắc mắc hay góp ý cho mình, thì bạn có thể để lại bình luận hoặc gửi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)