Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
SwiftData
Written by chuotfx on January 15, 2024

SwiftData – Sorts & Filters

iOS & Swift . SwiftUI

Contents

  • Chuẩn bị
  • Tạo View
  • Sorts
    • Đơn giản
    • Động
      • Setup
      • Actions
  • Filters
    • Predicate
    • Setup
    • Actions
  • Tạm kết

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:

    • Lập trình iOS cho mọi người
    • Làm quen với SwiftUI

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.

SwiftData

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ớp TodoList. 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ố cho sort
  • 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ến sort

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!

SwiftData

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!

SwiftData

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!

FacebookTweetPinYummlyLinkedInPrintEmailShares50

Related Posts:

  • SwiftData
    SwiftData - Hello world!
Tags: SwiftData, SwiftUI
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

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?
  • Khi Cô Đơn Gặp Python
  • Học vì tồn tại

You may also like:

  • SwiftData - Hello world!
    SwiftData

Archives

  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • December 2024 (4)
  • 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)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (43)
  • Code (10)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (101)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (86)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2025 Fx Studio - All rights reserved.