Struct và Class trong xây dựng giao diện với SwiftUI

SwiftUI 15 Th02 2025

structclass đóng vai trò rất quan trọng trong việc xây dựng giao diện người dùng với SwiftUI. Việc hiểu rõ sự khác biệt và cách sử dụng chúng sẽ giúp bạn viết code SwiftUI hiệu quả hơn.

Để làm rõ hơn, chúng ta sẽ đi qua các điểm sau:

1. Bản chất của Struct và Class trong Swift:

  • Struct (Kiểu giá trị - Value Type):
    • Khi bạn gán một struct cho một biến khác, hoặc truyền nó vào một hàm, một bản sao của struct sẽ được tạo ra.
    • Các struct lưu trữ dữ liệu của chúng trực tiếp.
    • So sánh bằng nhau dựa trên giá trị (nếu tất cả các thuộc tính bằng nhau thì hai struct bằng nhau).
    • Thường nhanh hơn class trong nhiều trường hợp vì không có overhead quản lý bộ nhớ phức tạp như class.
    • Mặc định là bất biến (immutable) trừ khi bạn khai báo thuộc tính là mutating trong các phương thức.
  • Class (Kiểu tham chiếu - Reference Type):
    • Khi bạn gán một class cho một biến khác, hoặc truyền nó vào một hàm, bạn đang làm việc với cùng một tham chiếu đến đối tượng class ban đầu.
    • Class lưu trữ một tham chiếu đến dữ liệu của chúng trên heap (vùng nhớ động).
    • So sánh bằng nhau dựa trên tham chiếu (hai biến class chỉ bằng nhau nếu chúng tham chiếu đến cùng một đối tượng trong bộ nhớ).
    • Có thể kế thừa (inheritance) và có các tính năng hướng đối tượng nâng cao hơn.
    • Cần quản lý bộ nhớ (ví dụ: ARC - Automatic Reference Counting) để tránh rò rỉ bộ nhớ.

2. Tại sao Struct và Class quan trọng trong SwiftUI?

SwiftUI được xây dựng dựa trên các nguyên tắc lập trình khai báo (declarative programming) và hướng dữ liệu (data-driven). Điều này có nghĩa là giao diện người dùng của bạn được mô tả bằng cách khai báo trạng thái mong muốn dựa trên dữ liệu, và SwiftUI sẽ tự động cập nhật giao diện khi dữ liệu thay đổi.

  • Struct và Tính Bất Biến (Immutability):

    • Struct là lựa chọn mặc định cho hầu hết các View và Model trong SwiftUI. Tính bất biến của struct rất phù hợp với triết lý của SwiftUI. Khi dữ liệu (struct) thay đổi, SwiftUI sẽ nhận biết sự thay đổi này và chỉ render lại những phần giao diện cần thiết một cách hiệu quả.
    • Dự đoán được (Predictable): Vì struct là kiểu giá trị, việc thay đổi một struct sẽ không ảnh hưởng đến các struct khác. Điều này giúp code dễ hiểu, dễ debug và tránh được các lỗi side-effect không mong muốn.
    • Hiệu suất: Struct thường hiệu quả hơn trong việc quản lý trạng thái giao diện nhỏ và đơn giản.
  • Class và Quản lý Trạng thái Phức tạp (State Management):

    • Class được sử dụng khi bạn cần quản lý trạng thái phức tạp, chia sẻ dữ liệu giữa nhiều View, hoặc cần các tính năng hướng đối tượng mạnh mẽ. Trong SwiftUI, class thường được sử dụng để tạo ra các ObservableObject (đối tượng quan sát được) để quản lý trạng thái ứng dụng.
    • Chia sẻ trạng thái: Khi bạn muốn nhiều View cùng chia sẻ và cập nhật chung một dữ liệu, class (thông qua ObservableObject) là lựa chọn phù hợp. Bất kỳ thay đổi nào trong đối tượng class sẽ được thông báo đến các View đang quan sát và giao diện sẽ được cập nhật theo.
    • Kế thừa và tái sử dụng: Nếu bạn cần xây dựng một hệ thống phức tạp với nhiều thành phần có quan hệ kế thừa, class có thể hữu ích.

3. Ví dụ minh họa:

Ví dụ 1: Sử dụng Struct cho View đơn giản và dữ liệu hiển thị

import SwiftUI

// Struct để đại diện cho dữ liệu sản phẩm
struct Product: Identifiable {
    let id = UUID() // Để sử dụng trong ForEach
    var name: String
    var price: Double
    var imageName: String
}

struct ProductCardView: View {
    let product: Product // Nhận dữ liệu Product

    var body: some View {
        VStack {
            Image(product.imageName)
                .resizable()
                .scaledToFit()
                .frame(height: 100)

            Text(product.name)
                .font(.headline)

            Text("Giá: \(product.price, specifier: "%.2f") VNĐ")
                .font(.subheadline)
                .foregroundColor(.gray)
        }
        .padding()
        .border(Color.gray.opacity(0.3))
    }
}

struct ContentView: View {
    let products = [
        Product(name: "iPhone 13", price: 25000000, imageName: "iphone13"),
        Product(name: "Samsung Galaxy S22", price: 23000000, imageName: "samsungS22"),
        Product(name: "Xiaomi 12 Pro", price: 18000000, imageName: "xiaomi12pro")
    ]

    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(products) { product in
                    ProductCardView(product: product)
                }
            }
            .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Giải thích:

  • Product struct: Được sử dụng để định nghĩa cấu trúc dữ liệu cho sản phẩm. Đây là một kiểu giá trị, dễ dàng tạo và truyền dữ liệu.
  • ProductCardView struct: Là một View hiển thị thông tin sản phẩm. Nó nhận một Product struct làm dữ liệu đầu vào. Khi dữ liệu product (bản sao) được truyền vào, View sẽ hiển thị thông tin tương ứng. Nếu bạn thay đổi product bên ngoài ProductCardView, View này sẽ không bị ảnh hưởng trừ khi bạn truyền một product struct mới vào.
  • ContentView struct: Sử dụng một mảng products kiểu [Product] và hiển thị danh sách sản phẩm bằng ForEachProductCardView.

Ví dụ 2: Sử dụng Class và ObservableObject để quản lý trạng thái ứng dụng

import SwiftUI
import Combine

// Class ObservableObject để quản lý dữ liệu giỏ hàng
class ShoppingCart: ObservableObject {
    @Published var items: [Product] = [] // @Published để thông báo thay đổi

    func addItem(product: Product) {
        items.append(product)
        objectWillChange.send() // Báo cho SwiftUI biết object đã thay đổi (cần thiết trong một số trường hợp phức tạp)
    }

    func removeItem(at index: Int) {
        items.remove(at: index)
        objectWillChange.send() // Tương tự
    }

    var totalPrice: Double {
        items.reduce(0) { $0 + $1.price }
    }
}

struct ShoppingCartView: View {
    @ObservedObject var cart: ShoppingCart // Quan sát đối tượng ShoppingCart

    var body: some View {
        VStack {
            Text("Giỏ hàng của bạn")
                .font(.title)

            List {
                ForEach(cart.items) { item in
                    HStack {
                        Text(item.name)
                        Spacer()
                        Text("\(item.price, specifier: "%.2f") VNĐ")
                    }
                }
                .onDelete(perform: { indexSet in
                    if let index = indexSet.first {
                        cart.removeItem(at: index)
                    }
                })
            }

            Text("Tổng cộng: \(cart.totalPrice, specifier: "%.2f") VNĐ")
                .font(.headline)
                .padding()
        }
    }
}

struct ProductListView: View {
    let products = [
        Product(name: "iPhone 13", price: 25000000, imageName: "iphone13"),
        Product(name: "Samsung Galaxy S22", price: 23000000, imageName: "samsungS22"),
        Product(name: "Xiaomi 12 Pro", price: 18000000, imageName: "xiaomi12pro")
    ]

    @StateObject var cart = ShoppingCart() // Tạo instance ShoppingCart và giữ nó trong View

    var body: some View {
        NavigationView {
            List(products) { product in
                HStack {
                    VStack(alignment: .leading) {
                        Text(product.name)
                            .font(.headline)
                        Text("Giá: \(product.price, specifier: "%.2f") VNĐ")
                            .font(.subheadline)
                            .foregroundColor(.gray)
                    }
                    Spacer()
                    Button("Thêm vào giỏ") {
                        cart.addItem(product: product)
                    }
                }
            }
            .navigationTitle("Danh sách sản phẩm")
            .toolbar {
                NavigationLink(destination: ShoppingCartView(cart: cart)) { // Truyền tham chiếu cart
                    Image(systemName: "cart.fill")
                    Text("(\(cart.items.count))")
                }
            }
        }
    }
}

struct ProductListView_Previews: PreviewProvider {
    static var previews: some View {
        ProductListView()
    }
}

Giải thích:

  • ShoppingCart class: Kế thừa ObservableObject để trở thành một đối tượng có thể quan sát được.
    • @Published var items: Thuộc tính items được đánh dấu @Published. Khi items thay đổi, SwiftUI sẽ tự động thông báo cho các View đang quan sát ShoppingCart và cập nhật giao diện.
    • addItem, removeItem, totalPrice: Các phương thức và thuộc tính để thao tác với giỏ hàng.
  • ShoppingCartView struct:
    • @ObservedObject var cart: ShoppingCart: Khai báo cart là một @ObservedObject. Điều này có nghĩa là ShoppingCartView sẽ "quan sát" đối tượng cart. Khi @Published thuộc tính trong cart thay đổi, ShoppingCartView sẽ được render lại để cập nhật giao diện.
  • ProductListView struct:
    • @StateObject var cart = ShoppingCart(): Sử dụng @StateObject để tạo và giữ instance của ShoppingCart trong ProductListView. @StateObject đảm bảo instance ShoppingCart chỉ được tạo một lần và tồn tại trong suốt vòng đời của ProductListView.
    • Khi bấm nút "Thêm vào giỏ", phương thức cart.addItem(product: product) được gọi. Vì items@Published, ShoppingCartView (và bất kỳ View nào khác quan sát cart) sẽ được cập nhật.
    • NavigationLink đến ShoppingCartView và truyền tham chiếu cart. Do đó, cả ProductListViewShoppingCartView đều đang làm việc với cùng một instance của ShoppingCart.

Tóm lại:

  • Sử dụng Struct khi:
    • Dữ liệu của bạn đơn giản, không cần chia sẻ trạng thái phức tạp.
    • Bạn muốn dữ liệu bất biến và dễ dự đoán.
    • Hầu hết các View và Model dữ liệu nên là struct.
  • Sử dụng Class (và ObservableObject) khi:
    • Bạn cần quản lý trạng thái phức tạp, chia sẻ dữ liệu giữa nhiều View.
    • Bạn cần các tính năng hướng đối tượng như kế thừa.
    • Để tạo ra các ViewModel hoặc đối tượng quản lý dữ liệu ứng dụng.

Lưu ý quan trọng:

  • SwiftUI khuyến khích sử dụng Struct. Class nên được sử dụng khi thực sự cần thiết để quản lý trạng thái phức tạp và chia sẻ dữ liệu.
  • Hiểu rõ sự khác biệt giữa kiểu giá trị và kiểu tham chiếu là rất quan trọng để viết code SwiftUI hiệu quả và tránh các lỗi không mong muốn.

Hy vọng những ví dụ và giải thích trên đã giúp bạn hiểu rõ hơn về vai trò của Struct và Class trong thiết kế giao diện SwiftUI.

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.