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:
- Attribute
countđược đánh dấu dirty CounterView.bodyđược gọi và so sánh (diff) với giá trị cũTextphụ thuộc vào output của body → ghi nhận thay đổiButton— input có thay đổi không? Không → bỏ qua- Commit phase: chỉ các thay đổi thực sự mới được truyền xuống UIKit bên dưới

@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:
- Parent's body chạy và mention
YourView(...) - SwiftUI gọi
YourView.init()— struct tạm thời được tạo trên stack;@StatechứaState<String>(initialValue: "")— chỉ là MÔ TẢ, chưa có storage - SwiftUI kiểm tra vị trí của view trong cây (Structural Identity)
- Lần đầu → allocate một attribute mới trong graph, lưu initial value, kết nối
@Statehandle với attribute này - SwiftUI gọi
bodytrên struct →bodyĐỌCname→ Graph ghi lại dependency: "body phụ thuộc vào name" - 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ị.

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
}
}
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
showChildthànhfalse→ChildViewbị xóa khỏi cây → graph hủy attributecount(dù đang là 5) - Toggle lại thành
true→ graph tạo attribute mới vớicount = 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ạoChu kỳ Update đầy đủ
- Set
count = 5→@Statehandle ghi vào attribute trong graph - Attribute tự đánh dấu DIRTY
- Graph đi xuống qua dependency edges → mọi attribute phụ thuộc vào
countđược đánh dấu dirty - SwiftUI lên lịch re-evaluation (batch — không tức thì)
- Trong render pass tiếp theo: gọi
bodycho các view dirty → diffing → đánh dấu custom views invalid nếu input thay đổi → tiếp tục gọibodycho subviews bị invalidate - Commit phase: chuyển các thay đổi xuống rendering engine
- 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
@Statecho data truyền từ parent — graph allocate attribute một lần và bỏ qua các init value sau đó. Dùng@Bindinghoặclet - Không sync hai
@Statethủ công — hai@State= hai attribute. Dùng một@Statevà truyền@Bindingxuố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 @StateObjectcho object bạn tạo.@ObservedObjectcho object bạn nhận. Tương ứng trực tiếp với việc graph có allocate storage hay chỉ observeinit()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.