Contents
Chào mừng bạn đến với Fx Studio. Và chúng ta lại tiếp tục hành trình trong vũ trụ SwiftUI bất tận này. Chủ đề bài viết này sẽ về các trạng thái tương tác trên List. Bạn có thể thêm và xoá đi các Row của nó … và nhiếu thứ bạn có thể tương tác được. Nó được gọi là Editing Mode.
Nếu bạn chưa biết về List trong SwiftUI là như thế nào, thì bạn có thể tham khảo các bài viết sau:
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Về mặt tool và version, các bạn tham khảo như sau:
-
- SwiftUI 3.0
- Xcode 13
Về mặt kiến thức, bạn cần biết trước các kiến thức cơ bản với SwiftUI & SwiftUI App. Tham khảo các bài viết sau, nếu bạn chưa đọc qua SwiftUI:
(Mặc định, mình xem như bạn đã biết về cách tạo project với SwiftUI & SwiftUI App rồi.)
Về mặt demo, bạn chỉ cần thực hiện demo trên các SwiftUI View đơn giản. Mình sẽ thực hiện các View riêng biệt với nhau, nên bạn không cần lo lắng gì nhiều về tính liên kết của các View trong một Project. Về mặt giao diện thì khá là đơn giản à.
(Hoặc bạn có thể checkout project demo tại đây.)
Did selected Cell For Row At …
Thật là buồn khi cú pháp với SwiftUI 1.0 đã không còn nữa. Bạn đã từng làm việc này với List một cách đơn giản như sau:
List(data: <RandomAccessCollection>,
selection: <Binding<T>>,
action: <(Identifiable.IdentifiedValue) -> Void>,
rowContent: <(Identifiable.IdentifiedValue) -> View>)
Trong đó, tham số action sẽ là sự kiện trên một Row và đơn giản là bạn sẽ có được identified của Row đó và làm được nhiều thứ.
Tuy nhiên, nó đã bị xoá đi khỏi SwiftUI 2.0 và 3.0 rồi.
Chúng ta sẽ dùng tới cách truyền thống thôi.
Selection Cell
Bắt đầu, chúng ta sẽ tiến hành custom một Cell. Nó có khả năng phản hồi lại tương tác của người dùng, ví dụ khi tap vào sẽ đánh dấu là đã chọn. Tap lần nữa thì đánh dấu là không chọn.
Xem ví dụ code nhoé:
struct SelectionCell: View {
let title: String
@Binding var selectedItem: String?
var body: some View {
HStack {
Text(title)
Spacer()
if title == selectedItem {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
.contentShape(Rectangle())
.onTapGesture {
self.selectedItem = self.title
}
}
}
Trong đó:
- Sử dụng thuộc tính
selectedItemvới kiểu là@Binding. Nó sẽ trỏ tới thuộc tính đánh dấu Row nào được chọn ở List - Trong ví dụ, ta chọn kiểu dữ liệu cho
selectedItemlà String. Vì mình làm cho đơn giản ví dụ thôi, còn nếu phức tạp hơn thì bạn sẽ phải trả về cả một đối tượng - Nội dung
bodycủa SelectionCell sẽ tuỳ thuộc vào dữ liệu củaselectedItem. Nếu trùng vớiitemthì sẽ hiển thị dấucheckmark. Ngược lại thì sẽ không xuất hiện. .onTapGesture { }để thực hiện việc bắt sự kiện người dùng cho SelectionCell. Bạn chỉ cần xét lạiself.selectedItem = self.titlelà oke
Nếu chúng ta chọn Row khác, thì Row đó sẽ lại làm công việc self.selectedItem = self.title. Khi đó, Row hiện tại của chúng ta sẽ là self.selectedItem != self.title rồi. Nên dấu checkmark sẽ mất đi.
EZ phải không nào!
Bạn chú ý về dòng code .contentShape(Rectangle()) cho HStack, nó sẽ giúp bạn tap được vùng Spacer() à. Khá là có ích nhoé.
Config List
Tiếp tục với phần List. Theo ở trên chúng ta cần phải cấu hình thêm cho View chứa List một tí. Để đảm bảo việc Binding dữ liệu. Bạn xem ví dụ code sau cho List nhoé.
struct DidSelectedCellDemoView: View {
var weathers = Weather.dummyData()
@State var selectedItem: String? = nil
var body: some View {
NavigationView {
List {
ForEach(weathers) { item in
SelectionCell(title: item.city, selectedItem: $selectedItem)
}
}
.navigationTitle("Cities")
}
}
}
Trong đó:
weatherslà dữ liệu cho List. Chúng ta đã tiến hành custom với Identifiable Protocol trong các bài viết trước rồi.- Ta cần 1 biến State là
selectedItem, giúp lưu giữ giá trị được chọn khi người dùng chọn các Row
Khi người dùng chọn từng Row thì giá trị của selectedItem sẽ thay đổi. Vì áp dụng The single source of truth, nên các Row sẽ phản ứng theo giá trị của selectedItem nhoé.
Ta thử test thử View chúng ta ổn chưa. Bạn hãy bấm Live Preview và test nhoé.

Deselected Cell
Có một bugs là khi bạn chọn lại 1 Row đã chọn. Nó sẽ không bị mất đi. Đơn giản là vì chúng ta chưa xét trường hợp đó. Nếu:
selectedItemkhác vớititlethì sẽ đánh dấu là chọn, gánselectedItem = titleselectedItemtrùng vớititlethì sẽ đánh dấu là bỏ chọn, gán lạiselectedItem = nil
Bạn cập nhật lại phần onTapGesture { } ở View SelectionCell nhoé.
.onTapGesture {
if title == selectedItem {
selectedItem = nil
} else {
self.selectedItem = self.title
}
}
Như vậy, bạn sẽ thực hiện được công việc chọn và bỏ chọn khi tương tác cùng với 1 Row rồi. Hãy bấm Live Preview và test lại.
Callback Actions
Một chút hoài niệm với quá khứ UITableView nhoé. Ta sẽ tìm cách đưa sự kiện ở SelectionCell về List. Điều này giúp bạn chủ động hơn.
Hoặc có thể hiểu là Callback lại cho List từ Cell vậy.
Bạn tiến hành chỉnh sửa lại SelectionCell như sau:
struct SelectionCell: View {
typealias Action = (String) -> Void
let title: String
@Binding var selectedItem: String?
var action: Action?
var body: some View {
HStack {
Text(title)
Spacer()
if title == selectedItem {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
.contentShape(Rectangle())
.onTapGesture {
if title == selectedItem {
selectedItem = nil
} else {
self.selectedItem = self.title
}
if let action = action {
action(title)
}
}
}
}
Trong đó:
- Khai báo thêm một
typealiaslà Action là một closure(String) -> Void - Tham số String dùng để gởi
Identifiedvề cho List. Tuỳ với logic và dữ liệu bạn cần, thì có thể khác nhau kiểu dữ liệu. - Bạn khai báo thêm thuộc tính
actioncho SelectionCell nữa. Nó là kiểu Optional và sẽ được cung cấp giá trị lúc khởi tạo đối tượng từ bên ngoài. - Cuối cùng là bạn chỉ có việc gởi
titlevề thông qua việc gọiactionthực thi ở.onTapGesture { }
Tại List, bạn update lại việc tạo đối tượng SelectionCell như sau:
SelectionCell(title: item.city, selectedItem: $selectedItem) { item in
print(item)
}
Muốn test lại thì bạn hãy build project lên Simulator nhoé, để thấy được print ở Console.
Swipe Actions
Đã nói tới List hay UITableView thì chúng ta không thể nào quên đi các button ẩn bằng tương tác Swipe Action được. Và List cũng hỗ trợ bạn công việc này.
Đây là tính năng mới thêm vào cho iOS 15, Swift 5.5, SwiftUI 3.0 và bạn cần Xcode 13.0 để test nhoé
Bạn sẽ được cung cấp thêm một modifier .swipeActions(edge:_) cho List và ForEach và các View con, để bắt các sự kiện Swipe Actions
Điểm đặc biệt bạn có thể áp dụng cho bất cứ View nào để tương tác việc này. Không nhất thiết ở List nhoé!
Xem ví dụ code như sau:
List {
if #available(iOS 15.0, *) {
ForEach(weathers) { item in
SelectionCell(title: item.city, selectedItem: $selectedItem) { item in
print(item)
}
.swipeActions(edge: .trailing) {
Button {
print("Mark as favorite")
} label: {
Label("Favorite", systemImage: "star")
}
.tint(.yellow)
Button {
print("Delete")
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
.swipeActions(edge: .leading) {
Button {
print("Share")
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
.tint(.green)
}
}
} else {
// Fallback on earlier versions
}
}
Trong đó:
- Ta áp dụng
.swipeActionscho custom view của chúng ta - Sử dụng tham số
edgevới 2 hướng làtrailing&leading - Bạn sẽ khai báo các View vào trong closue của chúng
Bấm Live Preview để test nhoé!
- Trailing

- Leading

Mặc định với iOS 15.0 và Xcode 13, thì là Style cho List là Grouped nhoé. Muốn về kiểu trước kia thì thêm modifier sau vào.
.listStyle(InsetListStyle())
Edit Button
SwiftUI cung cấp cho chúng ta một cái Button khá là quyền lực. Được gọi là EditButton. Khi nó được triều hồi, thì List chúng ta đang dùng sẽ chuyển sang trạng thái Editing Mode. Nghĩa là bạn có thể:
- Delete đi một row
- Move một row đi tới vị trí khác
- Multi Selection để làm một số tác vụ với nhiều Row một lúc. (Cái này ta sẽ tìm hiểu sau nhoé)
Các kĩ thuật khó hơn, thì chúng ta sẽ tìm hiểu sau.
Chúng ta sẽ kích hoạt trạng thái Editing Mode cơ bản với EditButton như sau. Bạn tham khảo đoạn code sau:
struct BasicActionsForRowInListDemoView: View {
@State var weathers = Weather.dummyData()
var body: some View {
NavigationView {
List {
ForEach(weathers) { item in
Text(item.city)
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.listStyle(InsetListStyle())
.navigationBarItems(trailing: EditButton())
.navigationTitle("Cities")
}
}
// ....
}
Trong đó:
- Ta cần một nơi nào đó để người dùng nhấn sự kiện Edit. Trong ví dụ ta chọn BarButtonItem ở NavigationBar
- Đối tượng EditButton sẽ thêm vào modifier sau
.navigationBarItems(trailing: EditButton()). Bạn chú ýtrailinglà ở bên phía phải nhoé - Nếu ta không có các modifier như
onDelete&onMovethì không có hiện tượng gì xãy ra.
Khi bạn có đầy đủ các function cho việc tương tác với onDelete và onMove mới thấy được giao diện của trạng thái Editing Mode là như thế nào.

Basic actions for rows
Khi đã có trạng thái Editing Mode của List được kích hoạt, bạn sẽ tiến hành tương tác với các Actions cơ bản như sau
Nhớ khai báo danh sách dữ liệu chúng ta là kiểu @State hoặc @StateObject nhoé.
Delete Row
Bạn sẽ thêm function delete vào View. Bạn tham khảo ví dụ code sau:
func delete(at offsets: IndexSet) {
if let first = offsets.first {
weathers.remove(at: first)
}
}
Với nguyên lý của The single source of truth. Khi bạn xoá đi 1 item trong array weathers thì giao diện sẽ tự động biến đổi theo. Function delete phải tuân thử khai báo với các tham số như trên.

Move Row
Áp dụng tương tự, bạn sẽ có việc di chuyển một Row tới vị trí khác. Bạn sẽ hoàn thiện function move như sau:
func move(from source: IndexSet, to destination: Int) {
let reversedSource = source.sorted()
for index in reversedSource.reversed() {
weathers.insert(weathers.remove(at: index), at: destination)
}
}
Về logic bạn tự ngẫm nhoé. Cái này áp dụng được với đa số kiểu dữ liệu Array. Nên bạn cứ yên tâm mà dùng.
Add Row
Với việc thêm một Row nữa thì sẽ phức tạp hơn một chút. Trước tiên, bạn cần hoàn thiện function add.
func add() {
let item = Weather(city: "new city", country: "new country", temperature: 25, status: .sun)
weathers.append(item)
}
Đơn giản là tạo mới một đối tượng thêm nó vào array của chúng ta thôi.
Vì chế độ Edting Mode với EditButton không có actions add một item mới. Do đó, ta phải chế lại thôi. Bạn sẽ thêm một Bar Button Item nữa vào NavigationBar cho sự kiện thêm mới này. Tham khảo code nhoé.
var body: some View {
NavigationView {
List {
ForEach(weathers) { item in
Text(item.city)
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.listStyle(InsetListStyle())
.navigationBarItems(leading: Button(action: add, label: {
Text("Add")
}) ,trailing: EditButton())
.navigationTitle("Cities")
}
}
Bạn thêm một button add vào phía leading của NavigationBar là đẹp. Nó sẽ gọi action add và thêm mới 1 Row vào List.

Khi test, bạn sẽ thấy có nhiều row với title là new city. Đó là các Row mới được thêm vào.
Editting Mode
Nếu bạn thấy việc dùng EditButton khá là củ chuối và muốn custom lại cái Button đó, nhưng lại muốn kích hoạt được trạng thái Editing Mode của List. Hoặc bạn không muốn sử dụng EditButton. Thì bạn còn có một cách nữa để thử. Đó là
Kích hoạt trạng thái Editing Mode của List từ Environment
Sơ đồ hoạt động
Chúng ta sẽ có sơ đồ hoạt động của nó như sau:

Trong đó:
- Button kích hoạt thì có thể bất cứ button nào cũng được. Do đó, đây là ưu điểm của bạn sẽ dùng. Chúng ta thoải mái thiết kế giao diện cho nó.
- Bạn cần một biến @State để lưu giữ trạng thái đang Editing hay hết Editing của View
- Các View hay List sẽ auto cập nhật trạng thái từ biến @State bằng việc sử dụng Environment với keypath là
\.EditMode - Cuối cùng bạn thực hiện xong các thao tác và cập nhật lại các View.
- Tắt trạng thái Editing băng việc xét lại giá trị cho thuộc tính State
Demo code
Chúng ta sẽ thực hiện các bước sau:
- Thêm một thuộc tính @State để quản lý trạng thái Editing nhoé
@State var isEditing = false
- Xét lại giá trị của Environment với keypath là
\.EditMode
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive))
Để sử dụng được với Emvironment, thì bạn sẽ phải thêm modifier (như trên) đó cho List của bạn.
- Custom Button
.navigationBarItems(trailing: Button(action: {
// code for action
isEditing.toggle()
}, label: {
if self.isEditing {
Text("Done").foregroundColor(Color.red)
} else {
Text("Edit").foregroundColor(Color.blue)
}
}))
Mình sẽ làm mới một Bar Button item cho NaviagtionBar. Button này cũng phản ứng theo biến @State isEditing. Quan trọng chúng ta sẽ bật/tăt trạng thái tại action của Button. Bằng lệnh đơn giản sau isEditing.toggle()
- Để cho có hiệu ứng mợt hơn một tí, bạn thêm modifier
.animationcho List nhoé
.animation(Animation.spring())
Okay, bạn xoá đi EditButton trước đó và test lại với Custom Button mới. Xem thử trạng thái Editing Mode hoạt động tốt không nhoé. Chúc bạn thành công!
Tạm kết
- Thực hiện tương tác với một Row & Callback về List
- Xử lý các Swipe Actions trong Row/Cell
- Sử dụng EditButton để kích hoạt trạng thái Editing Mode
- Các tương tác cơ bản với List, như: delete, move, add … từng item
- Quản lý và tương tác với trạng thái Editing Mode với Environment
Okay! Tới đây, mình xin kết thúc bài viết về Editing Mode trong SwiftUI. Và 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
- Multi-Layer Prompt Architecture – Chìa khóa Xây dựng Hệ thống AI Phức tạp
- Khi “Prompt Template” Trở Thành Chiếc Hộp Pandora
- Vòng Lặp Ảo Giác
- Giàn Giáo Nhận Thức (Cognitive Scaffold) trong Prompt Engineering
- Bản Thể Học (Ontology) trong Prompt Engineering
- Hướng Dẫn Vibe Coding với Gemini CLI
- Prompt Bản Thể Học (Ontological Prompt) và Kiến Trúc Nhận Thức (Cognitive Architecture Prompt) trong AI
- Prompt for Coding – Code Translation Nâng Cao & Đối Phó Rủi Ro và Đảm Bảo Chất Lượng
- Tại sao cần các Chiến Lược Quản Lý Ngữ Cảnh khi tương tác với LLMs thông qua góc nhìn AI API
- Prompt for Coding – Code Translation với Kỹ thuật Exemplar Selection (k-NN)
You may also like:
Archives
- October 2025 (1)
- September 2025 (4)
- August 2025 (5)
- July 2025 (10)
- June 2025 (1)
- May 2025 (2)
- 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)


