Giới Thiệu: UIKit Không Chết — Nó Đang Tiến Hóa
Nếu bạn là một iOS developer đã gắn bó với UIKit nhiều năm, chắc hẳn bạn đã nghe câu "UIKit sắp bị thay thế bởi SwiftUI" cỡ vài trăm lần rồi. Mình cũng thế. Nhưng thực tế lại đi ngược hoàn toàn — tại WWDC 2025, Apple không những không bỏ rơi UIKit mà còn mang đến những cải tiến có thể nói là lớn nhất trong nhiều năm qua.
Tâm điểm của bản cập nhật iOS 26 cho UIKit chính là Automatic Observation Tracking — khả năng tự động theo dõi và cập nhật UI khi dữ liệu @Observable thay đổi. Kèm theo đó là phương thức lifecycle hoàn toàn mới updateProperties(), tùy chọn animation .flushUpdates, và khả năng chia sẻ model giữa UIKit và SwiftUI mượt mà chưa từng có.
Nói thật, đây là tín hiệu rõ ràng nhất từ Apple: UIKit đang hội tụ với SwiftUI, không phải bị thay thế.
Và tin vui hơn nữa — bạn có thể backport phần lớn tính năng này về iOS 18. Trong bài viết này, mình sẽ đi chi tiết từng tính năng, kèm code chạy được ngay để bạn áp dụng vào dự án thực tế. Nào, bắt đầu thôi.
Nhắc Lại Nhanh: @Observable Macro Là Gì?
Trước khi nhảy vào phần UIKit, mình muốn chắc chắn bạn nắm rõ @Observable đã. Được giới thiệu trong Swift 5.9 cùng iOS 17, macro này biến một class thành kiểu có thể quan sát được (observable), với cơ chế theo dõi truy cập tự động thông qua ObservationRegistrar.
import Observation
@Observable
class UserProfile {
var name: String = ""
var avatarURL: URL?
var unreadCount: Int = 0
var hasUnread: Bool {
unreadCount > 0
}
}
Điểm khác biệt cốt lõi so với ObservableObject cũ: @Observable sử dụng mô hình pull-based và access-tracked. Nói đơn giản hơn, hệ thống chỉ theo dõi những property mà bạn thực sự đọc, không phải tất cả property trong class. Với các model phức tạp có hàng chục property, sự khác biệt về hiệu năng là đáng kể.
Trong SwiftUI, @Observable đã hoạt động hoàn hảo từ iOS 17. Nhưng UIKit thì sao? Trước iOS 26, bạn phải tự viết logic theo dõi bằng withObservationTracking() — rất rối và dễ lỗi (ai từng thử chắc hiểu cảm giác). iOS 26 thay đổi hoàn toàn câu chuyện này.
Automatic Observation Tracking — Cách UIKit Tự Động Cập Nhật UI
Đây là thay đổi quan trọng nhất: UIKit giờ đây tự động theo dõi các property của @Observable class khi bạn đọc chúng trong các phương thức lifecycle đặc biệt. Khi property thay đổi, UIKit tự invalidate view và chạy lại phương thức tương ứng — không cần NotificationCenter, không cần Combine, không cần didSet.
Nghe có vẻ đơn giản, nhưng tin mình đi — nó thay đổi cách bạn viết UIKit code hoàn toàn.
Cách Nó Hoạt Động Trong Thực Tế
Hãy xem ví dụ đơn giản nhất:
@Observable
class HitCounter {
var hits: Int = 0
}
class CounterViewController: UIViewController {
private let counter = HitCounter()
private let hitLabel = UILabel()
private let incrementButton = UIButton(type: .system)
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
incrementButton.addTarget(
self,
action: #selector(incrementTapped),
for: .touchUpInside
)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// UIKit tự động theo dõi counter.hits tại đây
hitLabel.text = "Số lần nhấn: \(counter.hits)"
}
@objc private func incrementTapped() {
counter.hits += 1
// Không cần gọi setNeedsLayout()!
// UIKit biết hitLabel.text phụ thuộc vào counter.hits
// và tự động gọi lại viewWillLayoutSubviews()
}
private func setupUI() {
hitLabel.font = .systemFont(ofSize: 24, weight: .bold)
hitLabel.textAlignment = .center
incrementButton.setTitle("Nhấn tôi!", for: .normal)
// ... Auto Layout setup
}
}
Bạn thấy chưa? Không một dòng setNeedsLayout(), không updateUI(), không didSet. Khi counter.hits thay đổi, UIKit tự biết rằng viewWillLayoutSubviews() cần được gọi lại vì nó đã đọc property đó. Sạch sẽ đến mức hơi khó tin.
Theo Dõi Chỉ Những Gì Thực Sự Được Đọc
Một điểm rất quan trọng cần hiểu: observation tracking là property-specific.
Nếu model có 10 property nhưng bạn chỉ đọc 2 trong viewWillLayoutSubviews(), thì chỉ 2 property đó được theo dõi. Thay đổi 8 property còn lại sẽ không trigger bất kỳ cập nhật nào. Hiệu năng cực tốt.
@Observable
class UserProfile {
var name: String = "" // được theo dõi
var email: String = "" // KHÔNG được theo dõi
var bio: String = "" // KHÔNG được theo dõi
var avatarURL: URL? // được theo dõi
}
class ProfileViewController: UIViewController {
let profile = UserProfile()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// Chỉ 2 property này được theo dõi
nameLabel.text = profile.name
avatarImageView.image = loadImage(from: profile.avatarURL)
}
}
Kể cả computed property cũng được theo dõi đúng cách — nếu hasUnread phụ thuộc vào unreadCount, thì thay đổi unreadCount vẫn trigger cập nhật UI khi bạn đọc hasUnread. Khá thông minh.
Danh Sách Các Phương Thức Hỗ Trợ Observation Tracking
Không phải mọi phương thức đều hỗ trợ automatic tracking. Đây là danh sách đầy đủ mà bạn nên bookmark lại:
UIViewController
updateProperties()— iOS 26+, phương thức mới được khuyên dùngviewWillLayoutSubviews()viewDidLayoutSubviews()updateViewConstraints()updateContentUnavailableConfiguration(using:)
UIView
updateProperties()— iOS 26+layoutSubviews()updateConstraints()draw(_:)
UICollectionViewCell / UITableViewCell
updateConfiguration(using:)- Thông qua
configurationUpdateHandler
UIPresentationController
containerViewWillLayoutSubviews()containerViewDidLayoutSubviews()
UIButton
updateConfiguration()- Thông qua
configurationUpdateHandler
Lưu ý quan trọng: Observation tracking chỉ xảy ra trong các phương thức nêu trên. Đọc property @Observable trong viewDidLoad() hay bất kỳ phương thức tùy chỉnh nào khác sẽ không được tự động theo dõi. Đây là lỗi mà mình thấy khá nhiều người mắc phải.
updateProperties() — Ngôi Sao Mới Của UIKit Lifecycle
Okay, đây là phần mình thích nhất. Phương thức updateProperties() mới được thêm vào cả UIView và UIViewController trong iOS 26, chạy trước layoutSubviews() nhưng hoạt động độc lập với chu trình layout.
Tại Sao Cần updateProperties() Khi Đã Có viewWillLayoutSubviews()?
Câu hỏi rất hay — và mình cũng tự hỏi điều này lúc đầu. Trước đây, developer thường "nhét" mọi thứ vào viewWillLayoutSubviews() — cả việc cập nhật text, color, image lẫn tính toán layout. Điều này dẫn đến hai vấn đề:
- Vi phạm Single Responsibility: Một phương thức làm quá nhiều việc
- Layout pass không cần thiết: Khi bạn chỉ thay đổi màu nền, UIKit vẫn chạy full layout cycle
updateProperties() giải quyết cả hai vấn đề đó. Nó được thiết kế riêng cho các thay đổi không ảnh hưởng đến kích thước hay vị trí — text, color, image, visibility, alpha, font. Thứ tự thực thi mới của UIKit lifecycle trông như sau:
- Update Traits — cập nhật trait collection
- Update Properties — cập nhật nội dung, styling (MỚI)
- Layout Subviews — tính toán kích thước và vị trí
Gọn gàng, rõ ràng, mỗi bước một trách nhiệm.
Ví Dụ Thực Tế: Tách Biệt Property và Layout
@Observable
class NotificationSettings {
var isEnabled: Bool = true
var badgeCount: Int = 0
var accentColor: UIColor = .systemBlue
}
class SettingsViewController: UIViewController {
let settings = NotificationSettings()
private let statusLabel = UILabel()
private let badgeView = UIView()
private let toggleSwitch = UISwitch()
override func updateProperties() {
super.updateProperties()
// Cập nhật nội dung — không ảnh hưởng layout
statusLabel.text = settings.isEnabled
? "Thông báo: Bật (\(settings.badgeCount) chưa đọc)"
: "Thông báo: Tắt"
statusLabel.textColor = settings.isEnabled
? settings.accentColor
: .secondaryLabel
badgeView.backgroundColor = settings.accentColor
badgeView.isHidden = settings.badgeCount == 0
toggleSwitch.isOn = settings.isEnabled
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// Chỉ xử lý layout ở đây
// Các tính toán frame, constraint nếu cần
}
}
Cấu trúc này clean hơn rất nhiều so với cách cũ. updateProperties() lo phần "nội dung gì hiển thị", viewWillLayoutSubviews() lo phần "hiển thị ở đâu". Và cả hai đều hỗ trợ observation tracking tự động — một sự kết hợp khá hoàn hảo.
setNeedsUpdateProperties() và updatePropertiesIfNeeded()
Giống như hệ thống layout có setNeedsLayout() và layoutIfNeeded(), hệ thống property update cũng có cặp phương thức tương ứng:
setNeedsUpdateProperties()— đánh dấu cần cập nhật, UIKit sẽ gọiupdateProperties()trong chu kỳ tiếp theoupdatePropertiesIfNeeded()— buộc cập nhật ngay lập tức nếu có pending update
// Khi bạn muốn trigger cập nhật thủ công
// (ngoài automatic observation tracking)
func userChangedTheme(_ newTheme: Theme) {
applyTheme(newTheme)
setNeedsUpdateProperties() // Lên lịch cập nhật
}
// Khi cần cập nhật ngay — ví dụ trước khi chụp screenshot
func captureScreenshot() -> UIImage {
updatePropertiesIfNeeded() // Đảm bảo mọi thứ đã cập nhật
return view.snapshot()
}
Animation với .flushUpdates — Tạm Biệt layoutIfNeeded()
Đây là một cải tiến nhỏ nhưng cực kỳ tiện lợi. iOS 26 giới thiệu tùy chọn animation .flushUpdates cho UIView.animate. Khi bật, UIKit tự động áp dụng các pending update trước animation bắt đầu và sau khi animation kết thúc.
Thành thật mà nói, mình không nhớ nổi đã viết bao nhiêu dòng layoutIfNeeded() trong sự nghiệp.
So Sánh Cách Cũ và Mới
// ❌ TRƯỚC iOS 26 — Cần gọi layoutIfNeeded() thủ công
UIView.animate(withDuration: 0.3) {
self.model.confirmationColor = validation.isValid ? .systemGreen : .systemRed
self.confirmationView.layoutIfNeeded()
}
// ✅ iOS 26 — Tự động với .flushUpdates
UIView.animate(options: .flushUpdates) {
self.model.confirmationColor = validation.isValid ? .systemGreen : .systemRed
// Không cần layoutIfNeeded()!
// UIKit biết confirmationView phụ thuộc vào model.confirmationColor
}
Tùy chọn .flushUpdates cũng hoạt động với các thay đổi constraint-based mà không cần giữ reference đến NSLayoutConstraint. Kết hợp với observation tracking, animation code gọn gàng hơn đáng kể.
Chia Sẻ @Observable Model Giữa UIKit và SwiftUI
Đây mới là phần mình thấy thú vị nhất, thật sự. Vì @Observable là Swift-native (không phải UIKit-specific hay SwiftUI-specific), bạn có thể dùng cùng một model object cho cả hai framework. Và với iOS 26, cả hai bên đều tự động phản ứng khi model thay đổi.
Đối với những team đang trong quá trình chuyển dần sang SwiftUI, đây là tin rất tốt.
Pattern: UIKit ViewController + SwiftUI Sheet
Một pattern cực kỳ phổ biến trong thực tế: màn hình chính viết bằng UIKit (vì đã có sẵn, đã ổn định), nhưng modal hay sheet mới viết bằng SwiftUI. Với @Observable, pattern này trở nên đơn giản đến bất ngờ:
// Model chung — dùng cho cả UIKit và SwiftUI
@Observable
class ImageSelection {
var selectedImage: String = "lake"
var filterIntensity: Double = 0.5
enum ImageName: String, CaseIterable {
case lake, mountain, ocean, forest
}
}
// UIKit ViewController — hiển thị ảnh đã chọn
class GalleryViewController: UIViewController {
private let selection = ImageSelection()
private let imageView = UIImageView()
private let titleLabel = UILabel()
override func updateProperties() {
super.updateProperties()
// Tự động cập nhật khi selection thay đổi
imageView.image = UIImage(named: selection.selectedImage)
titleLabel.text = selection.selectedImage.capitalized
}
@objc private func showSettings() {
// Tạo SwiftUI view, truyền cùng model
let settingsView = ImageSettingsView(selection: selection)
let hostingController = UIHostingController(rootView: settingsView)
if let sheet = hostingController.sheetPresentationController {
sheet.detents = [.medium()]
}
present(hostingController, animated: true)
}
}
// SwiftUI View — cho phép thay đổi selection
struct ImageSettingsView: View {
@Bindable var selection: ImageSelection
var body: some View {
NavigationStack {
List(ImageSelection.ImageName.allCases, id: \.self) { image in
Button {
selection.selectedImage = image.rawValue
} label: {
HStack {
Image(systemName: "photo")
Text(image.rawValue.capitalized)
Spacer()
if selection.selectedImage == image.rawValue {
Image(systemName: "checkmark")
.foregroundStyle(.blue)
}
}
}
}
.navigationTitle("Chọn Ảnh")
Slider(value: $selection.filterIntensity, in: 0...1) {
Text("Cường độ filter")
}
.padding()
}
}
}
Khi user chọn ảnh mới trong SwiftUI sheet, selection.selectedImage thay đổi → UIKit tự động gọi lại updateProperties() → imageView và titleLabel cập nhật ngay. Không một dòng notification hay callback nào cần viết. Zero.
Tích Hợp Với Custom Traits — Reactive State Toàn Ứng Dụng
Nếu bạn muốn chia sẻ state observable xuyên suốt toàn bộ view hierarchy (kiểu như Environment trong SwiftUI), hãy thử kết hợp @Observable với Custom Traits — một tính năng có từ iOS 17 mà không nhiều người biết.
// 1. Định nghĩa model
@Observable
class AppTheme {
var primaryColor: UIColor = .systemBlue
var isDarkMode: Bool = false
var fontSize: CGFloat = 16
}
// 2. Tạo custom trait
struct AppThemeTrait: UITraitDefinition {
static let defaultValue: AppTheme? = nil
}
extension UITraitCollection {
var appTheme: AppTheme? { self[AppThemeTrait.self] }
}
extension UIMutableTraits {
var appTheme: AppTheme? {
get { self[AppThemeTrait.self] }
set { self[AppThemeTrait.self] = newValue }
}
}
// 3. Inject ở root
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let theme = AppTheme()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
let rootVC = MainTabBarController()
rootVC.traitOverrides.appTheme = theme
window.rootViewController = rootVC
window.makeKeyAndVisible()
}
}
// 4. Dùng ở bất kỳ đâu trong view hierarchy
class AnyChildViewController: UIViewController {
override func updateProperties() {
super.updateProperties()
guard let theme = traitCollection.appTheme else { return }
view.backgroundColor = theme.isDarkMode ? .black : .white
titleLabel.font = .systemFont(ofSize: theme.fontSize)
accentView.backgroundColor = theme.primaryColor
}
}
Pattern này mạnh mẽ kinh khủng. Bạn inject theme một lần ở root, và mọi view controller trong hierarchy đều tự động phản ứng khi theme thay đổi — giống hệt @Environment trong SwiftUI. Theo ý kiến cá nhân của mình, đây là một trong những cách tốt nhất để quản lý global state trong UIKit hiện tại.
Observation Tracking Với UICollectionView
Một trong những ứng dụng thực tế nhất (và hay ho nhất) của observation tracking là với UICollectionView. Bạn có thể dùng configurationUpdateHandler để cell tự cập nhật khi model thay đổi:
@Observable
class TodoItem {
var title: String
var isCompleted: Bool = false
init(title: String) {
self.title = title
}
}
class TodoListViewController: UIViewController {
private var items: [TodoItem] = []
private var collectionView: UICollectionView!
private func makeCellRegistration()
-> UICollectionView.CellRegistration
{
UICollectionView.CellRegistration { cell, indexPath, item in
cell.configurationUpdateHandler = { cell, state in
var config = UIListContentConfiguration.cell()
config.text = item.title
config.textProperties.strikethroughStyle =
item.isCompleted ? .single : []
config.textProperties.color =
item.isCompleted ? .secondaryLabel : .label
cell.contentConfiguration = config
cell.accessories = [
.checkmark(displayed: .always,
options: .init(isHidden: !item.isCompleted))
]
}
}
}
}
Khi item.isCompleted thay đổi, chỉ cell tương ứng được cập nhật — không phải toàn bộ collection view. Trước đây để đạt được hiệu năng tương tự, bạn phải viết kha khá code. Giờ thì vài dòng là xong.
Backport Về iOS 18: Bật Observation Tracking Trên Phiên Bản Cũ
Tin tuyệt vời cho những ai chưa thể target iOS 26 ngay (chắc là đa số chúng ta): bạn có thể bật automatic observation tracking trên iOS 18+ bằng cách thêm một key vào Info.plist. Đúng vậy, chỉ một key thôi.
Bước 1: Thêm Key Vào Info.plist
<key>UIObservationTrackingEnabled</key>
<true/>
Với macOS (AppKit), key tương ứng là:
<key>NSObservationTrackingEnabled</key>
<true/>
Bước 2: Dùng viewWillLayoutSubviews() Thay Cho updateProperties()
Vì updateProperties() chỉ có từ iOS 26, trên iOS 18 bạn sẽ đặt logic cập nhật trong viewWillLayoutSubviews():
class ProfileViewController: UIViewController {
let profile = UserProfile()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// Observation tracking hoạt động trên iOS 18+
// khi UIObservationTrackingEnabled = YES
nameLabel.text = profile.name
avatarImageView.image = loadAvatar(profile.avatarURL)
}
}
Code Tương Thích Cả Hai Phiên Bản
Nếu bạn muốn tận dụng updateProperties() trên iOS 26 nhưng vẫn hỗ trợ iOS 18, đây là cách mình hay làm:
class AdaptiveViewController: UIViewController {
let model = SettingsModel()
// iOS 26+: Dùng updateProperties()
override func updateProperties() {
super.updateProperties()
applyModelToUI()
}
// iOS 18-25: Fallback sang viewWillLayoutSubviews()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if #unavailable(iOS 26) {
applyModelToUI()
}
}
private func applyModelToUI() {
titleLabel.text = model.title
subtitleLabel.text = model.subtitle
iconView.image = UIImage(systemName: model.iconName)
}
}
Pattern #unavailable(iOS 26) ở đây giúp tránh việc applyModelToUI() bị gọi hai lần trên iOS 26. Gọn và hiệu quả.
Bảng So Sánh Khả Năng Giữa Các Phiên Bản
| Tính năng | iOS 18 (opt-in) | iOS 26 (mặc định) |
|---|---|---|
| Automatic observation tracking | Cần Info.plist key | Bật sẵn |
| viewWillLayoutSubviews() tracking | Có | Có |
| updateProperties() | Không | Có |
| .flushUpdates animation | Không | Có |
| configurationUpdateHandler tracking | Có | Có |
Những Lỗi Thường Gặp Và Cách Tránh
Sau khi dùng thử observation tracking một thời gian và đọc khá nhiều phản hồi từ cộng đồng, mình tổng hợp lại mấy "bẫy" phổ biến mà bạn nên biết trước:
1. Đọc Property Ngoài Phương Thức Hỗ Trợ
Đây là lỗi phổ biến nhất, đặc biệt với những ai mới bắt đầu.
// ❌ SAI — viewDidLoad() KHÔNG hỗ trợ observation tracking
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = model.name // Sẽ không tự cập nhật!
}
// ✅ ĐÚNG — Đặt trong phương thức được hỗ trợ
override func updateProperties() {
super.updateProperties()
nameLabel.text = model.name // Tự cập nhật khi model.name thay đổi
}
2. Đọc Tất Cả Property "Cho Chắc"
// ❌ SAI — Anti-pattern: tạo dependency không cần thiết
override func updateProperties() {
super.updateProperties()
_ = model.property1 // Tại sao đọc nếu không dùng?
_ = model.property2
_ = model.property3
nameLabel.text = model.name
}
// ✅ ĐÚNG — Chỉ đọc những gì bạn thực sự dùng
override func updateProperties() {
super.updateProperties()
nameLabel.text = model.name
}
Nghe có vẻ hiển nhiên, nhưng mình đã thấy pattern này trong vài code review. Đừng đọc property chỉ vì "sợ thiếu" — nó tạo ra những update không cần thiết.
3. Tính Toán Nặng Trong Phương Thức Tracked
// ❌ SAI — Xử lý nặng có thể gây lag
override func updateProperties() {
super.updateProperties()
let processedImage = applyHeavyFilter(to: model.rawImageData) // Chậm!
imageView.image = processedImage
}
// ✅ ĐÚNG — Cache kết quả, xử lý nặng ở nơi khác
override func updateProperties() {
super.updateProperties()
imageView.image = cachedFilteredImage
}
// Xử lý nặng trong Task riêng
func processImageIfNeeded() {
Task {
let result = await processInBackground(model.rawImageData)
await MainActor.run {
cachedFilteredImage = result
setNeedsUpdateProperties()
}
}
}
4. Retain Cycle Với Observable
Observable object được retain bởi observation tracking khi đang được theo dõi. Hãy cẩn thận với reference cycle, đặc biệt khi model giữ reference ngược về view controller. Mình khuyên nên dùng [weak self] ở những chỗ cần thiết và tránh để model biết về view controller.
Câu Hỏi Thường Gặp (FAQ)
UIKit có bị thay thế bởi SwiftUI không?
Không. iOS 26 là bằng chứng rõ nhất rằng Apple vẫn tiếp tục đầu tư mạnh vào UIKit. Thay vì thay thế, Apple đang hội tụ hai framework — UIKit nhận những tính năng hay nhất của SwiftUI (observation tracking, reactive updates) và ngược lại. Trong thực tế, hầu hết team sẽ dùng cả hai: UIKit cho các màn hình phức tạp đã có sẵn, SwiftUI cho tính năng mới.
Có cần migrate từ Combine sang @Observable không?
Không bắt buộc ngay, nhưng mình khuyến nghị cho code mới. @Observable đơn giản hơn, hiệu năng tốt hơn, và hoạt động native với cả UIKit lẫn SwiftUI. Nếu đang dùng ObservableObject + @Published, hãy cân nhắc migrate dần khi có dịp refactor.
updateProperties() có thay thế viewWillLayoutSubviews() không?
Không hẳn — chúng phục vụ mục đích khác nhau. updateProperties() dành cho cập nhật nội dung (text, color, image), còn viewWillLayoutSubviews() dành cho layout (frame, constraint). Apple khuyên dùng updateProperties() để tách biệt concerns và tránh layout pass không cần thiết.
Observation tracking có hoạt động với struct không?
@Observable chỉ hoạt động với class, không phải struct. Lý do là cơ chế observation cần reference semantics — khi nhiều nơi cùng tham chiếu đến một object, thay đổi ở một nơi phản ánh ở tất cả nơi khác. Với struct (value semantics), mỗi nơi giữ bản copy riêng nên observation không có ý nghĩa.
Backport về iOS 18 có ổn định không?
Có, khá ổn định. Mặc dù chỉ được công bố chính thức với iOS 26, Apple đã tích hợp cơ chế observation tracking vào UIKit từ iOS 18 — chỉ tắt theo mặc định. Thêm key UIObservationTrackingEnabled vào Info.plist để bật. Nhiều developer (bao gồm cả mình) đã dùng trong production mà không gặp vấn đề gì đáng kể. Tuy nhiên, nhớ rằng updateProperties() và .flushUpdates chỉ có từ iOS 26.