Các Protocol Quan Trọng trong SwiftUI

SwiftUI 15 Th02 2025

Hiểu rõ các Protocol trong SwiftUI là chìa khóa để bạn làm chủ framework này và xây dựng giao diện một cách hiệu quả. Bài viết sẽ giới thiệu một số Protocol quan trọng nhất trong SwiftUI theo cách dễ hiểu nhất, kèm ví dụ và giải thích chi tiết.

1. View Protocol - "Viên gạch" cơ bản của giao diện:

  • Định nghĩa: View là protocol cốt lõi nhất trong SwiftUI. Mọi thứ bạn nhìn thấy trên màn hình SwiftUI (Text, Image, Button, VStack, HStack, ...) đều là View hoặc được tạo thành từ các View. Nói cách khác, bất kỳ thứ gì bạn muốn hiển thị giao diện người dùng đều phải tuân theo protocol View.

  • Yêu cầu chính: Protocol View yêu cầu bạn phải cung cấp một thuộc tính (property) có tên là body. Thuộc tính body này phải trả về một View khác (hoặc một tổ hợp các View) để mô tả giao diện mà View của bạn sẽ hiển thị.

  • Ví dụ:

    import SwiftUI
    
    struct MyTextView: View { // Struct MyTextView tuân theo protocol View
        var body: some View { // Thuộc tính 'body' bắt buộc
            Text("Xin chào SwiftUI!") // 'body' trả về một View khác (Text View)
        }
    }
    
    struct ContentView: View {
        var body: some View {
            MyTextView() // Sử dụng MyTextView bên trong ContentView
        }
    }
    
  • Giải thích:

    • struct MyTextView: View: Chúng ta khai báo một struct tên MyTextView và nói rằng nó tuân theo protocol View bằng cách viết : View.
    • var body: some View: Đây là yêu cầu bắt buộc của protocol View. body là nơi bạn "mô tả" giao diện của MyTextView. some View có nghĩa là body phải trả về một kiểu dữ liệu nào đó tuân theo View protocol.
    • Text("Xin chào SwiftUI!"): Bên trong body, chúng ta trả về một Text View. Text là một View có sẵn trong SwiftUI, dùng để hiển thị chữ.
    • ContentViewMyTextView(): ContentView cũng là một View, và trong body của nó, chúng ta sử dụng (tạo instance) MyTextView() để hiển thị MyTextView bên trong ContentView.
  • Tóm lại: View protocol giống như việc bạn phải có "bản vẽ thiết kế" (body) cho bất kỳ "ngôi nhà" (View) nào bạn muốn xây dựng trong SwiftUI.

2. Identifiable Protocol - "Định danh" cho danh sách:

  • Định nghĩa: Identifiable protocol được sử dụng khi bạn làm việc với các danh sách dữ liệu trong SwiftUI, đặc biệt là khi bạn dùng ForEach để hiển thị danh sách đó. Protocol này yêu cầu kiểu dữ liệu của bạn phải có một thuộc tính id để SwiftUI có thể xác định duy nhất từng phần tử trong danh sách. Điều này rất quan trọng để SwiftUI có thể cập nhật danh sách một cách hiệu quả khi dữ liệu thay đổi.

  • Yêu cầu chính: Protocol Identifiable yêu cầu bạn phải cung cấp một thuộc tính có tên là id. Kiểu dữ liệu của id có thể là bất kỳ kiểu nào miễn là nó là duy nhất cho mỗi phần tử trong danh sách. Thông thường, người ta sử dụng UUID (Universally Unique Identifier) để tạo ra các ID duy nhất một cách tự động.

  • Ví dụ:

    import SwiftUI
    
    struct Task: Identifiable { // Struct Task tuân theo Identifiable
        let id = UUID() // Tạo ID duy nhất tự động
        var name: String
    }
    
    struct TaskListView: View {
        @State var tasks = [
            Task(name: "Mua sắm"),
            Task(name: "Làm bài tập"),
            Task(name: "Đi dạo")
        ]
    
        var body: some View {
            List {
                ForEach(tasks) { task in // Sử dụng ForEach để lặp qua mảng tasks
                    Text(task.name) // Hiển thị tên công việc
                }
            }
        }
    }
    
  • Giải thích:

    • struct Task: Identifiable: Struct Task tuân theo Identifiable protocol.
    • let id = UUID(): Chúng ta tạo một thuộc tính id kiểu UUID. UUID() sẽ tự động tạo ra một chuỗi ID duy nhất mỗi khi bạn tạo một instance của Task.
    • ForEach(tasks) { task in ... }: Khi bạn sử dụng ForEach để lặp qua mảng tasks (mảng các Task), SwiftUI sẽ sử dụng thuộc tính id của mỗi Task để theo dõi và quản lý các View được tạo ra. Nếu bạn thêm, xóa hoặc thay đổi thứ tự các Task trong mảng, SwiftUI sẽ biết cách cập nhật giao diện một cách chính xác và hiệu quả nhờ vào id.
  • Tóm lại: Identifiable giúp SwiftUI "biết" từng phần tử trong danh sách là ai, giống như việc bạn gán "số chứng minh thư" cho mỗi người trong danh sách lớp học để dễ dàng quản lý.

3. ObservableObject Protocol - "Báo động" khi dữ liệu thay đổi:

  • Định nghĩa: ObservableObject protocol được sử dụng cho các class (không phải struct) mà bạn muốn dùng để quản lý trạng thái của ứng dụng và thông báo cho SwiftUI biết khi dữ liệu trong class đó bị thay đổi. Khi một class tuân theo ObservableObject và có các thuộc tính được đánh dấu bằng @Published, SwiftUI sẽ có thể "quan sát" class đó và tự động cập nhật giao diện khi các thuộc tính @Published thay đổi.

  • Yêu cầu chính: Để một class trở thành ObservableObject, bạn chỉ cần khai báo nó tuân theo protocol này (class MyClass: ObservableObject { ... }). Tuy nhiên, để thực sự thông báo thay đổi dữ liệu, bạn cần sử dụng property wrapper @Published trước các thuộc tính mà bạn muốn SwiftUI theo dõi.

  • Ví dụ:

    import SwiftUI
    import Combine // Import module Combine để dùng ObservableObject và Published
    
    class TaskStore: ObservableObject { // Class TaskStore tuân theo ObservableObject
        @Published var tasks: [Task] = [] // 'tasks' là thuộc tính @Published
    
        init() {
            tasks = [
                Task(name: "Viết blog"),
                Task(name: "Học SwiftUI"),
                Task(name: "Uống cà phê")
            ]
        }
    
        func addTask(name: String) {
            let newTask = Task(name: name)
            tasks.append(newTask) // Khi tasks thay đổi, SwiftUI sẽ được thông báo
        }
    }
    
    struct ContentView: View {
        @StateObject var taskStore = TaskStore() // Tạo và giữ instance TaskStore
    
        var body: some View {
            NavigationView {
                List {
                    ForEach(taskStore.tasks) { task in
                        Text(task.name)
                    }
                    .onDelete { indexSet in
                        taskStore.tasks.remove(atOffsets: indexSet)
                    }
                }
                .navigationTitle("Công việc")
                .toolbar {
                    Button("Thêm công việc") {
                        taskStore.addTask(name: "Công việc mới")
                    }
                }
            }
        }
    }
    
  • Giải thích:

    • class TaskStore: ObservableObject: Class TaskStore tuân theo ObservableObject.
    • @Published var tasks: [Task] = []: Thuộc tính tasks được đánh dấu @Published. Điều này có nghĩa là bất cứ khi nào bạn thay đổi giá trị của tasks (ví dụ: thêm, xóa phần tử), SwiftUI sẽ tự động nhận ra sự thay đổi này và cập nhật lại giao diện của bất kỳ View nào đang "quan sát" TaskStore.
    • @StateObject var taskStore = TaskStore(): Trong ContentView, chúng ta sử dụng @StateObject để tạo và giữ một instance của TaskStore. @StateObject đảm bảo instance TaskStore chỉ được tạo ra một lần và tồn tại trong suốt vòng đời của ContentView.
    • Khi bạn nhấn nút "Thêm công việc" hoặc xóa công việc, mảng taskStore.tasks sẽ thay đổi. Vì tasks@Published, SwiftUI sẽ tự động cập nhật List để phản ánh những thay đổi này.
  • Tóm lại: ObservableObject@Published giống như một hệ thống "báo động" giữa dữ liệu và giao diện. Khi dữ liệu (@Published thuộc tính) thay đổi, "chuông báo động" sẽ reo lên và SwiftUI sẽ tự động cập nhật giao diện để hiển thị dữ liệu mới nhất.

4. Shape Protocol - "Vẽ vời" hình dạng:

  • Định nghĩa: Shape protocol cho phép bạn tạo ra các hình dạng vector phức tạp trong SwiftUI (ví dụ: hình tam giác, ngôi sao, đường cong, ...). Các hình dạng này có thể được tô màu, viền, và sử dụng trong nhiều ngữ cảnh khác nhau trong giao diện người dùng.

  • Yêu cầu chính: Protocol Shape yêu cầu bạn phải cung cấp một phương thức path(in rect: CGRect) -> Path. Phương thức này sẽ được gọi bởi SwiftUI để yêu cầu bạn "vẽ" hình dạng của mình bên trong một hình chữ nhật cho trước (rect). Bạn cần trả về một Path (đường dẫn) mô tả hình dạng mà bạn muốn vẽ.

  • Ví dụ:

    import SwiftUI
    
    struct Triangle: Shape { // Struct Triangle tuân theo Shape
        func path(in rect: CGRect) -> Path { // Phương thức 'path(in:)' bắt buộc
            var path = Path() // Tạo một Path mới
    
            path.move(to: CGPoint(x: rect.midX, y: rect.minY)) // Điểm đỉnh trên
            path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) // Điểm trái dưới
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) // Điểm phải dưới
            path.addLine(to: CGPoint(x: rect.midX, y: rect.minY)) // Quay lại điểm đầu (đóng hình)
    
            return path // Trả về Path đã vẽ
        }
    }
    
    struct ContentView: View {
        var body: some View {
            Triangle() // Sử dụng Triangle Shape
                .fill(.blue) // Tô màu xanh dương
                .frame(width: 100, height: 100) // Đặt kích thước
        }
    }
    
  • Giải thích:

    • struct Triangle: Shape: Struct Triangle tuân theo Shape protocol.
    • func path(in rect: CGRect) -> Path: Phương thức path(in:) được triển khai để vẽ hình tam giác. rect là hình chữ nhật giới hạn không gian vẽ.
    • var path = Path(): Chúng ta tạo một Path mới để bắt đầu vẽ.
    • Các lệnh path.move(to:), path.addLine(to:): Các lệnh này "di chuyển bút vẽ" và "vẽ đường thẳng" để tạo thành hình tam giác.
    • Triangle().fill(.blue).frame(...): Trong ContentView, chúng ta sử dụng Triangle(), sau đó dùng .fill(.blue) để tô màu xanh dương và .frame(...) để đặt kích thước cho hình tam giác.
  • Tóm lại: Shape protocol cho phép bạn trở thành một "họa sĩ" trong SwiftUI, tự do vẽ ra các hình dạng vector theo ý muốn và sử dụng chúng để làm phong phú giao diện người dùng.

5. Gesture Protocol - "Lắng nghe" tương tác:

  • Định nghĩa: Gesture protocol cho phép bạn thêm khả năng tương tác người dùng vào các View trong SwiftUI (ví dụ: chạm, vuốt, kéo, ...). SwiftUI cung cấp nhiều loại gesture khác nhau (TapGesture, DragGesture, LongPressGesture, ...). Bạn có thể gắn các gesture này vào View để View có thể phản ứng lại các hành động của người dùng.

  • Yêu cầu chính: Protocol Gesture có một số yêu cầu phức tạp hơn, nhưng khi sử dụng các gesture có sẵn của SwiftUI, bạn thường không cần phải trực tiếp triển khai Gesture protocol. Thay vào đó, bạn sẽ sử dụng các concrete gesture types (kiểu gesture cụ thể) như TapGesture, DragGesture, ... và gắn chúng vào View bằng modifier .gesture().

  • Ví dụ:

    import SwiftUI
    
    struct ContentView: View {
        @State private var tapCount = 0
    
        var body: some View {
            VStack {
                Text("Bạn đã chạm vào đây \(tapCount) lần.")
                    .padding()
    
                Rectangle() // Vẽ một hình chữ nhật
                    .fill(.yellow)
                    .frame(width: 200, height: 100)
                    .gesture( // Gắn TapGesture vào Rectangle
                        TapGesture() // Tạo một TapGesture
                            .onEnded { // Hành động khi gesture kết thúc (tap)
                                tapCount += 1 // Tăng biến đếm khi chạm vào
                            }
                    )
            }
        }
    }
    
  • Giải thích:

    • Rectangle().gesture(...): Chúng ta gắn một gesture vào Rectangle View bằng modifier .gesture().
    • TapGesture(): Chúng ta tạo một instance của TapGesture, đây là một kiểu gesture có sẵn trong SwiftUI để nhận diện thao tác chạm.
    • .onEnded { ... }: Modifier .onEnded { ... } được gắn vào TapGesture. Đoạn code bên trong { ... } sẽ được thực thi khi gesture TapGesture kết thúc (khi người dùng nhấc ngón tay lên sau khi chạm vào hình chữ nhật). Trong ví dụ này, chúng ta tăng biến tapCount lên 1 mỗi khi chạm vào hình chữ nhật.
  • Tóm lại: Gesture protocol (thông qua các concrete gesture types) giúp bạn "lắng nghe" các hành động tương tác của người dùng trên giao diện và làm cho ứng dụng của bạn trở nên sống động và phản hồi tốt hơn.

6. Và các Protocol khác:

  • EnvironmentKeyEnvironmentValues: Cho phép bạn truyền dữ liệu "ngầm định" xuống cây View. Thường dùng để thiết lập theme, cấu hình ứng dụng, ...
  • PreviewProvider: Để cung cấp code preview cho View của bạn trong Canvas của Xcode.
  • Equatable, Hashable, Comparable: Các protocol chuẩn của Swift, cũng được sử dụng trong SwiftUI để so sánh, băm và sắp xếp dữ liệu.

Lời khuyên:

  • Bắt đầu với View, Identifiable, ObservableObject: Đây là những protocol quan trọng và cơ bản nhất bạn cần nắm vững khi mới bắt đầu với SwiftUI.
  • Thực hành với ví dụ: Hãy thử viết code ví dụ cho từng protocol để hiểu rõ hơn cách chúng hoạt động.
  • Xem tài liệu của Apple: Tài liệu chính thức của Apple về SwiftUI là nguồn thông tin tuyệt vời để tìm hiểu sâu hơn về các protocol và API của SwiftUI.

Hy vọng những giải thích và ví dụ trên đã giúp bạn hiểu rõ hơn về các Protocol quan trọng trong SwiftUI. Nếu bạn có bất kỳ câu hỏi nào khác, đừng ngần ngại hỏi nhé!

Tags

Tony Phạm

Là một người thích vọc vạch và tò mò với tất cả các lĩnh vực từ khoa học tự nhiên, lập trình, thiết kế đến ... triết học. Luôn mong muốn chia sẻ những điều thú vị mà bản thân khám phá được.