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
Written by chuotfx on January 30, 2024

Observation Framework trong 10 phút

iOS & Swift . SwiftUI

Contents

  • Observation là gì?
  • Chuẩn bị
  • Khai báo Observable Object
  • The source of truth
  • Dependency Injection via Environment
    • Custom EnvironmentKey
    • Environment Object
  • Passing Observable Object
  • Bindable
  • Tạm kết

Chào mừng bạn đến với Fx Studio. Bài viết này sẽ nói về một thư viện mới của Apple, đó là Observation Framework. Để biết nó là gì? thì chúng ta tiếp tục tìm hiểu nhóe. Bài viết sẽ mang tính chất lý thuyết hàm lâm nhiều, nên bạn cần nuốt kiến thức từ từ nha.

Còn nếu mọi việc đã ổn rồi, thì …

Bắt đầu thôi!

Observation là gì?

Mọi thứ bắt đầu từ hư vô và lại trở lại từ hư vô mà thôi!

Observation Framework hay Observation cũng được, tùy cách bạn gọi sao cho dễ hiểu à. Nói về nó, đây là một thư viện mới mà Apple giới thiệu ở iOS 17 & Swift 5.9. Nó là một chuẩn mới giúp cho các lập trình viên giải quyết vấn đề lâu nay của họ đang bị rối bời.

Cụ thể hơn, ta đã biết trước khi có Swift 5.9. Các lập trình viên thiếu một chuẩn để thống nhất & cách hiệu quả để quan sát sự thay đổi đối với thuộc tính loại tham chiếu. Mô hình KƠV từ cái thời Objeactive-C, bị giới hạn trong phạm vị các lớp con NSObject. Còn về Combine, thì ta không thể quan sát tới cấp thuộc tính của đối tượng. Và cả 2 đều thiếu việc hỗ trợ đa nền tảng.

Do đó, đứa con cưng SwiftUI lại phải sống cùng lũ & về mặt hiệu năng lại không cao. Ngoài ra, nó là cho tư tưởng về The single source of truth cũng hơi rườm ra đi một chút. Bạn cần nhớ lại một chút về ObservableObject Protocol và các Property Wrapper, như @Published, @EnvironmentObject, @StateObject.

Dù sao đi nữa, bạn cũng phải bắn/lắng nghe với từng thuộc tính của một đối tượng kiểu tham chiếu mà thôi. Quá mợt mỏi đi mà!

Bây giờ với Swift 5.9, bạn đã có Observation Framework. Nó giải quyết các vấn đề rắc rối trên.

  • Áp dụng cho tất cả các loại tham chiếu của Swift, không chỉ các lớp con NSObject.
  • Cung cấp khả năng quan sát cấp thuộc tính chính xác mà không cần chú thích đặc biệt.
  • Giảm các cập nhật chế độ xem không cần thiết trong SwiftUI, cải thiện hiệu suất.

Chuẩn bị

Đối với bài viết này, bạn cũng cần phải nắm được kha khá kiến thức liên quan tới SwiftUI & The single source of truth. Các tư tưởng này sẽ là thành phần kiến thức nền tảng từ bây giờ cho tới về sau trong hệ sinh thái Apple. Và nếu bạn chưa biết qua chúng, thì có thể bắt đầu với các bài viết sau:

    • The single source of truth
    • Observation – SwiftUI Notes #35
    • Basic Environment values
    • Sharing in the Environment

Chúc bạn may mắn!

Khai báo Observable Object

Đọc qua trên chắc nhiều bạn sẽ nỗ não thôi. Mình đã việt hóa khá nhiều cho mớ lý thuyết mới. Để dễ hiểu hơn, thì bạn hãy tiếp tục đọc tiếp phần ví dụ cho việc khai báo nhóe.

Ít chữ, nhiều code chắc ổn hơn.

Với Combine, bạn khai báo một kiểu dữ liệu Observable theo dạng tham chiếu thì như sau:

class Store: ObservableObject {
    @Published var firstName: String
    @Published var lastName: String
    // ...
}

Với Observation thì nó đơn giản hơn một chút:

@Observable class Store {
    var firstName: String = "Fx"
    var lastName: String = "Studio"
    // ...
}

Trong đó:

  • Sử dụng duy nhất @Observable  để trước khai báo class
  • Không sử dụng các Wrapper @Published cho các thuộc tính loại lưu trữ (Store Properties) để bên ngoài có thể quan sát được
  • Các Computed Properties cũng có thể quan sát được
  • Nếu không muốn thuộc tính nào quan sát được thì thêm @ObservationIgnored trước khai báo thuộc tính

EZ Game!

Về @Observable, nó là một Macro. Khác với @Published, là một Wrapper Properties. Nó một cách mới và đơn giản hơn để thực hiện phản hồi dạng xem đối với các thay đổi dữ liệu. Cung cấp mọi công cụ cần thiết để làm việc mới Observer Design Pattern trong Swift. Và nó được cài đặt sẵn trong core rồi, nên cứ thế mà quất thôi.

The source of truth

Sau khi có một đối tượng Observable, chúng ta phải quyết định ai sở hữu dữ liệu này. Tùy thuộc vào ý đồ logic của bạn mà bạn có thể đặt nó ở đâu trong phân cấp view ứng dụng. Cho dù đặt ở đâu, thì chúng ta cần có @State để xác định đó là nguồn duy nhất được sử dụng.

Ví dụ đặt tại Root:

@Observable class Store {
    ...
}

@main
struct ExampleApp: App {
    @State private var store = Store()

    var body: some Scene {
        ...
    }
}

Ví dụ đặt tại một View nào đó.

@Observable class Store {
    ...
}

struct ContentView: View {
    @State private var store = Store()

    var body: some View {
        ...
    }
}

Dễ chơi, dễ trúng thưởng!

Dependency Injection via Environment

Ngoài cái The single source of truth, chúng ta vẫn còn cách để sử dụng nữa là truyền vào các biến môi trường. Điều này cũng giúp cho việc truyền data model tới các view con. Nếu bạn chưa quen với các Environment trong SwiftUI, thì có thể hiểu việc này như là một đối tượng Singleton và bạn có thể dùng ở bất kỳ đâu trong phân cấp view của bạn.

Trong SwiftUI, chúng ta có tới 2 cách sử dụng biến môi trường nhóe. Bắt đầu, mình sử dụng một Store đơn giản như sau:

@Observable class Store {
    var count: Int = 0
    
    init(count: Int = 0) {
        self.count = count
    }
}

Custom EnvironmentKey

Với cách này, code của bạn sẽ trông chuyên nghiệp hơn. Bạn có thể quản lý tất cả chúng nó một cách đơn giản nữa. Việc đầu tiên, ta cần tạo ra một EnvironmentKey mới nha.

struct CustomStoreKey: EnvironmentKey {
    static var defaultValue = Store()
}

Quan trọng, struct Key mới của bạn cần conform với EnvironmentKey Protocol. Yêu cầu duy nhất cho giao thức này là bạn xác định giá trị mặc định cho Key. Sau đó, ta sẽ extension EnvironmentValues với một thuộc tính môi trường mới.

EnvironmentValues ​​là tập hợp các giá trị môi trường có thể truy cập được từ toàn bộ hệ thống phân cấp Views.

extension EnvironmentValues {

    var store: Store {
        get { self[CustomStoreKey.self] }
        set { self[CustomStoreKey.self] = newValue }
    }
    
}

Các View sẽ truy cập tới thông qua KeyPath là store nhóe! Còn cách dùng tại các View thì như sau:

struct ContentView: View {
    @Environment(\.store) private var store
    
    var body: some View {
        Text("Count: \(store.count)")
        Button("Tap +1") {
            store.count += 1
        }
        .buttonStyle(.bordered)
    }
}

Đây là cách bạn sử dụng giá trị mặc định của biến môi trường nhóe. Kết quả tại Preview như sau:

observation framework

Còn muốn cung cấp một giá trị khác từ một store cho biến môi trường, thì bạn có thể truyền nó từ Root.

@main
struct ObservationDemoApp: App {
    
    @State private var myStore = Store(count: 9999)
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.store, myStore)
        }
    }
}

Bây giờ, ContentView đã sử dụng giá trị mới là myStore. Bạn nhớ build lên để test nha, chứ đừng thắc mắc tại sao Preview của nó vẫn không thay đổi.

Observation

Environment Object

Các thứ 2 thì đơn giản hơn nhiều. Bớt tạo thêm file hay thêm các struct. Bạn có thể khai báo một thuộc tính của view là biến môi trường với wrapper properties @Environment. Ví dụ như sau:

struct SecondView: View {
    
    @Environment(Store.self) var store
    
    var body: some View {
        Text("Count: \(store.count)")
        Button("+1") {
            store.count += 1
        }
    }
}

#Preview {
    var testStore = Store(count: 8888)
    
    return SecondView()
        .environment(testStore)
}

Trong đó:

  • Thuộc tính store của SecondView là một thuộc tính với kiểu là biến môi trường
  • Khi muốn gán giá trị cho thuộc tính, bạn sử dụng modifier là .environment() và truyền giá trị vào

Tuy nhiên, cách này sẽ không an toàn, nếu bạn không gọi modifier & xét giá trị cho nó. Dẫn tới ứng dụng sẽ bị crash. Do đó, bạn cần thận trọng khi sử dụng cách này nha. Đây là giải pháp khi đưa chúng về Optional.

struct SecondView: View {
    
    @Environment(Store.self) var store: Store?
    
    var body: some View {
        if let count = store?.count {
            Text("Count: \(count)")
            Button("+1") {
                store?.count += 1
            }
        } else {
            Text("N/A")
        }
    }
}

#Preview {
    return SecondView()    
}

Ta không cần gọi và truyền giá trị tại Preview, còn SecondView vẫn hiển thị đẹp và mướt lắm.

Observation

Passing Observable Object

Nếu bạn vẫn cảm thấy ở trên quá trời phức tạp, thì hãy quay về cách truyền dữ liệu truyền thống. Khi truyền một đối tượng Observable Object thì cũng tương tự như bao đối tượng khác. Xem ví dụ nha!

struct ThirdView: View {
    
    @State var store = Store()
    
    var body: some View {
        SubThirdView(store: store)
    }
}

struct SubThirdView: View {
    let store : Store
    
    var body: some View {
        Text("Count: \(store.count)")
        Button("Tap +1") {
            store.count += 1
        }
        .buttonStyle(.bordered)
    }
}

Trong đó:

  • Đối tượng Observable Object được truyền từ ThirdView sang SubThirdView trong hàm khởi tạo
  • Với khai báo @State cho store để đảm bảo việc dữ liệu tập trung ở một nguồn duy nhất

Khi bạn tương tác dữ liệu, bạn sẽ thấy chúng ta chỉ cần thay đổi store.count +=1 và không làm gì thêm cả. Kể cả việc hiển thị dữ liệu lên Text cũng không cần truy xuất ngược lại. Một khi giá trị thuộc tính count thay đổi, đối tượng Observable sẽ phát giá trị đi tới các View đang sử dụng. Khi đó, các View sẽ tự động thay đổi lại việc hiển thị.

Đây là cái hay của SwiftUI à. Quá đơn giản!

Bindable

Đây là một wrapper property mà hỗ trợ việc tạo liên kết 2 chiều (Binding two-way) với các thuộc tính có thể thay đổi được của các Observable Object. Bạn sẽ sử dụng cho những thành phần cần có loại liên kết (Binding) trước khi nó có thể thay đổi giá trị, ví dụ: TextField. Khi làm việc với các thành phần đó, bạn cần sử dụng Wrapper Property @Bindable trước var và let.

Ví dụ code:

struct CounterView: View {
    
    @Bindable var store = Store()
    
    var body: some View {
        VStack {
            VStack(alignment: .leading) {
                Text("Name: \(store.name)")
                Text("Count: \(store.count)")
                Button("Tap +1") {
                    store.count += 1
                }
                .buttonStyle(.bordered)
            }
            .padding(10)
            SubCounterView(store: store)
        }
        .padding(10)
    }
}

struct SubCounterView: View {
    
    @Bindable var store: Store
    
    var body: some View {
        TextField("", text: $store.name)
            .padding(10)
            .background(Color.accentColor)
    }
}

Thêm thuộc tính name kiểu String cho Store và tiến hành liên kết với một TextField ở một View con khác. Mọi thức vẫn hoạt động mượt lắm.

Còn ví dụ, bạn chỉ cần Binding ở cục bộ một vài chỗ và không muốn khai báo với @Bindable cho thuộc tính. Thì bạn vẫn có thể sử dụng chúng ở các View cục bộ nhóe. Xem qua ví dụ code tiếp nha:

struct ContentView: View {
    var store: Store
    
    var body: some View {
        @Bindable var bindableStore = store

        TextField("Name", text: $bindableStore.name)

    }
}

Phần cuối này, bạn tự suy nghĩ vào tạo demo code nha. Ahihi!

Tạm kết

  • Observation Framework tạo ra chuẩn và cách mạng hóa việc phát triển với các Observable Object cho kiểu tham chiếu
  • Đơn giản & giải pháp thay thế cho các cơ chế trước đây như KVO hay Combine
  • Kết hợp với SwiftUI để cải thiện hiệu năng & tương tác với người dùng tốt hơn

Tham khảo:

  • Migrating from the Observable Object protocol to the Observable macro
  • Managing model data in your app

Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Observation Framework . 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!

FacebookTweetPinYummlyLinkedInPrintEmailShares23

Related Posts:

  • feature_bg_swift_10
    Swift Optional trong 10 phút
  • Dart Defines
    Dart Defines trong Flutter và sức mạnh của nó
  • feature_bg_swiftui_4
    Regular Expression (Regex) trong Swift
  • Quick
    Quick trong 10 phút
Tags: Observation, Swift, 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

  • Role-playing vs. Persona-based Prompting
  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • 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?

You may also like:

  • Strategy Pattern trong 10 phút
    feature_bg_3
  • Dispatch Semaphore trong 10 phút
    Semaphore
  • Dart Defines trong Flutter và sức mạnh của nó
    Dart Defines
  • KeyPath trong 10 phút - Swift
    KeyPath
  • Raw String trong 10 phút
    Raw String

Archives

  • 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)

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 (44)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (87)

Newsletter

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

    Copyright © 2025 Fx Studio - All rights reserved.