SwiftVietnam News Tin Apple cho iOS/macOS Developers Việt Nam
← Quay lại danh sách

AttributeGraph — Cỗ máy vận hành đằng sau mọi SwiftUI View

28 tháng 3, 2026 · SwiftyPlace · IOS · Nguồn GitHub Issue

AttributeGraph là runtime engine của SwiftUI — hệ thống đồ thị phụ thuộc quyết định khi nào view được cập nhật, dữ liệu nào được lưu trữ và khi nào chúng bị giải phóng. Hiểu được model này giúp SwiftUI trở nên dự đoán được thay vì 'phép màu'.

AttributeGraph — Cỗ máy vận hành đằng sau mọi SwiftUI View

Bối cảnh

SwiftUI tồn tại từ năm 2019. Apple đã có hàng chục buổi WWDC nói về nó. Vậy mà hầu hết developer — kể cả người có kinh nghiệm — vẫn gặp phải những vấn đề lặp đi lặp lại: tại sao @State initializer bị bỏ qua? Tại sao state của view reset khi không mong đợi? Tại sao toàn bộ list re-render khi chỉ thay đổi một item?

Những vấn đề này không phải edge case. Chúng là behavior cơ bản, và tất cả đều bắt nguồn từ một thứ mà Apple gần như không đề cập: AttributeGraph — runtime engine của SwiftUI.

Tại sao quan trọng với dev Việt Nam

AttributeGraph quyết định toàn bộ cách SwiftUI hoạt động: khi nào view được cập nhật, dữ liệu tồn tại ở đâu, và khi nào bị giải phóng. Nắm được model này là chìa khóa để:

  • Debug các bug state khó hiểu (view không update, state bị reset không mong muốn)
  • Tối ưu performance — tránh re-render không cần thiết
  • Chọn đúng property wrapper: @State, @Binding, @StateObject, @ObservedObject, hay @Observable
  • Hiểu tại sao SwiftUI hoạt động khác hoàn toàn UIKit

Chi tiết kỹ thuật

AttributeGraph là gì?

Hãy hình dung một bảng tính (spreadsheet): ô A3 = A1 + A2. Khi A1 thay đổi, chỉ A3 được tính lại — A2 không bị ảnh hưởng. AttributeGraph hoạt động theo nguyên lý tương tự.

Graph được tạo thành từ:

  • Nodes (attributes) — mỗi node lưu một thông tin cụ thể: giá trị @State, một custom view, các SwiftUI view như VStack, Text, Button
  • Edges (dependencies) — kết nối giữa các node, nói lên "node này đọc dữ liệu từ node kia"

Khi một state node thay đổi, graph đi theo các edge và đánh dấu mọi node phụ thuộc là dirty — cần tính lại. Các node không phụ thuộc vào giá trị thay đổi không bao giờ bị đụng tới.

View struct không phải là UI

Đây là sự thay đổi tư duy quan trọng nhất khi chuyển từ UIKit sang SwiftUI.

Trong UIKit, UIViewController chứa data, chứa views, có vòng đời rõ ràng. Trong SwiftUI, view struct chỉ là một bản mô tả — một blueprint. SwiftUI đọc nó, trích xuất thông tin, đưa vào AttributeGraph, rồi vứt struct đi.

struct CounterView: View {
    @State var count = 0
    var body: some View {
        VStack {
            Text("\(count)")
            Button("+1") { count += 1 }
        }
    }
}

Khi count thay đổi:

  1. Attribute count được đánh dấu dirty
  2. CounterView.body được gọi và so sánh (diff) với giá trị cũ
  3. Text phụ thuộc vào output của body → ghi nhận thay đổi
  4. Button — input có thay đổi không? Không → bỏ qua
  5. Commit phase: chỉ các thay đổi thực sự mới được truyền xuống UIKit bên dưới

Sơ đồ AttributeGraph với các nodes và dependency edges

@State thực sự sống ở đâu?

@State là một property wrapper. Bên trong nó có một reference — Apple gọi là _location — trỏ tới một attribute trong graph. Giá trị thực sự sống ở đó, không phải trong struct.

Khi SwiftUI gặp view lần đầu tiên:

  1. Parent's body chạy và mention YourView(...)
  2. SwiftUI gọi YourView.init() — struct tạm thời được tạo trên stack; @State chứa State<String>(initialValue: "") — chỉ là MÔ TẢ, chưa có storage
  3. SwiftUI kiểm tra vị trí của view trong cây (Structural Identity)
  4. Lần đầu → allocate một attribute mới trong graph, lưu initial value, kết nối @State handle với attribute này
  5. SwiftUI gọi body trên struct → body ĐỌC name → Graph ghi lại dependency: "body phụ thuộc vào name"
  6. SwiftUI vứt struct đi → struct biến mất, attribute trong graph vẫn còn

Struct là disposable. Attribute trong graph mới là source of truth.

Single Source of Truth — Topology của Graph

Vấn đề: Hai @State = Hai Attribute

struct ParentView: View {
    @State var count = 0
    var body: some View {
        VStack {
            Button("+1") { count += 1 }
            ChildView(name: "Updated: \(count)")
        }
    }
}

struct ChildView: View {
    @State var name: String  // Luôn hiển thị "Updated: 0"
    var body: some View { Text(name) }
}

Graph tạo hai attribute riêng biệt. Khi count thay đổi, ChildView.name không được cập nhật vì không có edge nối chúng sau lần init đầu tiên.

Fix: Một @State + let = Một Attribute

struct ChildView: View {
    let name: String  // Nhận giá trị mới mỗi khi parent re-render
    var body: some View { Text(name) }
}

Chỉ có một @State declaration → graph tạo một attribute duy nhất → cả ParentView lẫn ChildView đều phản ánh đúng giá trị.

Topology của AttributeGraph: một @State duy nhất so với hai @State riêng biệt

Dependencies — Graph biết cập nhật gì

Value types với @State và @Binding:

struct MyView: View {
    @State var name = ""         // tạo node và edge/dependency
    @Binding var title: String   // không có edge riêng, được phát hiện khi walk view tree
    let age: Int                 // không có edge, được phát hiện khi walk view tree
    var body: some View {
        Text(name)  // body ĐỌC `name` nhưng KHÔNG ĐỌC `age`
    }
}

ObservableObject@StateObject tạo và sở hữu storage trong graph; @ObservedObject gắn vào node đã tồn tại:

struct ParentView: View {
    @StateObject var myObject = MyObject()    // tạo node + edge, graph sở hữu storage
    @StateObject var otherObject = OtherObject() // tạo node + edge, graph sở hữu storage
    var body: some View {
        MyView(myObject: myObject, otherObject: otherObject)
    }
}

struct MyView: View {
    @ObservedObject var myObject: MyObject   // edge tới existing node
    var otherObject: OtherObject             // không có edge → changes không được track
    var body: some View {
        Text(myObject.name)      // được update
        Text(otherObject.count)  // KHÔNG được update
    }
}

@StateObject nói với graph: "Allocate persistent attribute cho object này và giữ nó sống chừng nào identity của view còn tồn tại." @ObservedObject chỉ tạo edge tới node đã có — không allocate storage mới. Nếu không dùng property wrappers, graph không có pointer/edge và không biết phải update view này.

@Observable (macro mới) — edges được tạo theo từng property được truy cập trong body:

struct MyView: View {
    var myObject: MyObject  // không cần wrapper
    var body: some View {
        Text(myObject.name)  // read access → edge tới node "myObject.name"
        // chỉ khi myObject.name thay đổi, view này mới update
    }
}

Luồng dữ liệu từ trên xuống trong SwiftUI view tree với AttributeGraph

Khi nào State "chết"?

Trong SwiftUI không có deinit trên view struct (value type). State bị giải phóng khi identity của view bị xóa khỏi cây.

struct ParentView: View {
    @State var showChild = true
    var body: some View {
        VStack {
            Toggle("Show", isOn: $showChild)
            if showChild { ChildView() }
        }
    }
}

struct ChildView: View {
    @State var count = 0
    var body: some View {
        Button("Count: \(count)") { count += 1 }
    }
}
  • Toggle showChild thành falseChildView bị xóa khỏi cây → graph hủy attribute count (dù đang là 5)
  • Toggle lại thành true → graph tạo attribute mới với count = 0

count = 5 biến mất mãi mãi. Đây là "deinit" của SwiftUI.

Identity — Graph biết "View nào" qua cơ chế nào?

Structural Identity (mặc định): SwiftUI dùng vị trí trong code để xác định view.

var body: some View {
    VStack {
        Text("Hello") // identity: VStack/child-0
        Text("World") // identity: VStack/child-1
    }
}

If/else tạo identity khác nhau hoàn toàn:

var body: some View {
    if isLoggedIn {
        HomeView()   // identity: if-true-branch
    } else {
        LoginView()  // identity: if-false-branch
    }
}

Khi isLoggedIn flip, graph hủy toàn bộ attributes của LoginView và tạo mới cho HomeView.

Explicit Identity — bạn cung cấp qua .id() hoặc ForEach:

ForEach(items, id: \.id) { item in
    RowView(item: item)
}

Mỗi RowView được định danh bởi item.id. Nếu xóa một item khỏi array, graph hủy attributes của identity đó. Nếu thêm item mới, graph tạo attributes mới.

.id() còn cho phép reset state thủ công:

ChildView()
    .id(someValue)  // khi someValue thay đổi → view cũ bị hủy, view mới với state mới được tạo

Chu kỳ Update đầy đủ

  1. Set count = 5@State handle ghi vào attribute trong graph
  2. Attribute tự đánh dấu DIRTY
  3. Graph đi xuống qua dependency edges → mọi attribute phụ thuộc vào count được đánh dấu dirty
  4. SwiftUI lên lịch re-evaluation (batch — không tức thì)
  5. Trong render pass tiếp theo: gọi body cho các view dirty → diffing → đánh dấu custom views invalid nếu input thay đổi → tiếp tục gọi body cho subviews bị invalidate
  6. Commit phase: chuyển các thay đổi xuống rendering engine
  7. Frame tiếp theo: UI hiển thị state mới

Bảng đối chiếu UIKit ↔ SwiftUI

Khái niệm UIKit SwiftUI
Dữ liệu sống ở đâu Trong object của bạn Trong AttributeGraph
Ai tạo UI Bạn (addSubview, NSLayoutConstraint) Graph (từ body description)
Ai cập nhật UI Bạn (label.text = ...) Graph (dependency tracking)
Data được tạo khi nào init / viewDidLoad Lần đầu identity xuất hiện trong cây
Data chết khi nào deinit Identity bị xóa khỏi cây
Cập nhật lan truyền thế nào Bạn gọi methods thủ công Graph đi theo dependency edges tự động
Code của bạn là gì Cỗ máy Blueprint

Các quy tắc thực tiễn

Những điều sau không phải gợi ý — chúng là hệ quả tất yếu từ cách graph hoạt động:

  • Không dùng @State cho data truyền từ parent — graph allocate attribute một lần và bỏ qua các init value sau đó. Dùng @Binding hoặc let
  • Không sync hai @State thủ công — hai @State = hai attribute. Dùng một @State và truyền @Binding xuống
  • Đặt source of truth thấp nhất có thể nhưng đủ cao để các view cần có thể truy cập. @State ở root → thay đổi có thể trigger re-evaluation toàn bộ cây bên dưới
  • @StateObject cho object bạn tạo. @ObservedObject cho object bạn nhận. Tương ứng trực tiếp với việc graph có allocate storage hay chỉ observe
  • init() không phải điểm setup — nó chạy mỗi lần parent re-evaluate. Graph quyết định có dùng initial values hay không

Kết luận

Khi bạn hiểu AttributeGraph — rằng view struct chỉ là blueprint, rằng state sống trong graph chứ không phải struct, rằng identity quyết định vòng đời của data — SwiftUI ngừng cảm giác như phép màu và trở nên hoàn toàn dự đoán được. Mọi behavior "lạ" đều có một lý giải rõ ràng trong graph: attribute nào tồn tại, nó kết nối với gì, và identity của nó có còn trong cây hay không.