# SwiftUI: Xay dung giao dien hien dai cho iOS > Huong dan xay dung giao dien hien dai voi SwiftUI: cu phap khai bao, thanh phan, hieu ung dong va cac phuong phap tot nhat cho iOS 18. - Published: 2026-01-05 - Updated: 2026-04-02 - Author: Anthony Fillion-Maillet - Tags: swiftui, ios, swift, ui, apple - Reading time: 12 min --- SwiftUI đã thay đổi hoàn toàn cách xây dựng giao diện trên các nền tảng Apple. Với cú pháp khai báo và khả năng tích hợp native, framework này cho phép tạo ra các ứng dụng tinh tế với lượng mã ít hơn đáng kể so với trước đây. iOS 18 mang đến những cải tiến quan trọng về hiệu năng cùng nhiều tính năng mới. > **Why SwiftUI in 2026?** > > SwiftUI đã đạt độ trưởng thành trên iOS 18, cung cấp khả năng render được tối ưu hóa, quản lý bộ nhớ tốt hơn và tích hợp UIKit đơn giản hơn. Đây là tiêu chuẩn cho các ứng dụng iOS mới. ## Hiểu về mô hình khai báo Trước khi viết mã, cần hiểu điều gì làm cho SwiftUI khác biệt. Với UIKit (framework cũ hơn), lập trình viên phải chỉ dẫn iOS **cách thức** xây dựng giao diện từng bước một: "tạo một nhãn, đặt nó ở đây, đổi màu khi người dùng nhấn". Đây là mô hình **mệnh lệnh** (imperative). SwiftUI hoạt động khác: lập trình viên mô tả **cái gì** cần hiển thị, và framework xử lý phần còn lại. Sự khác biệt có thể so sánh với điều hướng GPS: mô hình mệnh lệnh đưa ra chỉ dẫn rẽ từng bước, trong khi mô hình khai báo chỉ cần nêu đích đến. ### View đầu tiên Trong SwiftUI, mọi phần tử giao diện đều là một `View`. View là một struct mô tả nội dung cần hiển thị trên màn hình. Dưới đây là màn hình đầu tiên với tiêu đề, phụ đề và nút bấm: ```swift // ContentView.swift import SwiftUI // Each screen is a struct that implements the View protocol struct ContentView: View { // The "body" property describes what the view displays // "some View" means "a type of View, but Swift figures it out automatically" var body: some View { // VStack = "Vertical Stack": stacks elements vertically // spacing: 20 = 20 points of space between each element VStack(spacing: 20) { // A simple text with style modifiers Text("Welcome to SharpSkill") .font(.largeTitle) // Large title font .fontWeight(.bold) // Bold text Text("Prepare for your iOS interviews") .font(.subheadline) // Smaller font .foregroundColor(.secondary) // Gray secondary color // Button with an action (closure) and a label Button("Get Started") { print("Button tapped!") } .buttonStyle(.borderedProminent) // Filled blue button style } .padding() // Adds margins around the VStack } } ``` Cần chú ý cấu trúc: đầu tiên khai báo những gì cần hiển thị (văn bản, nút), sau đó sắp xếp các phần tử theo chiều dọc (VStack), và áp dụng kiểu dáng thông qua **modifier** (`.font()`, `.padding()`). > **Điểm mấu chốt**: Trong SwiftUI, các đối tượng giao diện không được "tạo ra" thủ công mà được **mô tả** giao diện mong muốn. SwiftUI tự quản lý việc tạo, cập nhật và hủy các phần tử thực tế. ### Modifier: Biến đổi View Modifier là các phương thức được nối chuỗi sau một view để biến đổi nó. Có thể hình dung chúng như các bộ lọc được áp dụng lần lượt. **Thứ tự rất quan trọng** vì mỗi modifier tạo ra một view mới bao bọc view trước đó. Ví dụ minh họa tại sao thứ tự lại quan trọng: ```swift // ModifiersOrder.swift // Example 1: padding THEN background Text("SwiftUI") .padding() // 1. Adds 16pt of space around the text .background(.blue) // 2. Blue background covers text + padding .foregroundColor(.white) // Result: white text on blue rectangle with margins // Example 2: background THEN padding (reversed order!) Text("SwiftUI") .background(.blue) // 1. Tight blue background around text only .padding() // 2. Transparent padding around blue background .foregroundColor(.white) // Result: white text on small blue rectangle, surrounded by empty space ``` Sự khác biệt nằm ở chỗ: trường hợp đầu tiên, padding nằm "bên trong" background. Trường hợp thứ hai, padding nằm "bên ngoài". Chi tiết tuy nhỏ nhưng cực kỳ quan trọng để làm chủ bố cục. ## Tổ chức giao diện với Stacks SwiftUI cung cấp ba container chính để tổ chức các view. Có thể hình dung chúng như những chiếc hộp sắp xếp nội dung theo các cách khác nhau. ### VStack, HStack và ZStack - **VStack** (Vertical Stack): xếp các phần tử từ trên xuống dưới - **HStack** (Horizontal Stack): xếp các phần tử từ trái sang phải - **ZStack** (Z-axis Stack): chồng các phần tử lên nhau Hãy xây dựng một thẻ hồ sơ người dùng kết hợp cả ba loại stack. Mục tiêu: hiển thị ảnh với huy hiệu xác minh, tên và vai trò của người dùng. ```swift // ProfileCard.swift struct ProfileCard: View { var body: some View { // Main HStack: photo on left, info on right HStack(spacing: 16) { // ZStack to overlay the badge on the photo ZStack(alignment: .bottomTrailing) { // Profile image (circle) Image("avatar") .resizable() .frame(width: 60, height: 60) .clipShape(Circle()) // Green "verified" badge at bottom right Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .background(Circle().fill(.white)) // White circle behind } // VStack to stack name and role vertically VStack(alignment: .leading, spacing: 4) { Text("Marie Dupont") .font(.headline) Text("iOS Developer") .font(.subheadline) .foregroundColor(.secondary) } // Spacer pushes everything to the left Spacer() // Chevron on right indicates it's tappable Image(systemName: "chevron.right") .foregroundColor(.gray) } .padding() .background(Color(.systemGray6)) .cornerRadius(12) } } ``` Thủ thuật chính là `Spacer()`: một phần tử vô hình chiếm toàn bộ không gian còn lại. Không có Spacer, các phần tử sẽ nằm ở giữa. Có Spacer, nội dung được đẩy sang trái và chevron giữ nguyên vị trí bên phải. > **Xcode Tip** > > Sử dụng `⌘ + click` trên bất kỳ view nào trong Xcode để mở visual inspector. Có thể thêm modifier mà không cần gõ mã thủ công. ## Quản lý trạng thái với @State và @Binding Quản lý trạng thái là trái tim của SwiftUI. Trạng thái là bất kỳ dữ liệu nào có thể thay đổi và cần cập nhật giao diện. Khi trạng thái thay đổi, SwiftUI tự động tính toán lại các view bị ảnh hưởng. ### @State: Trạng thái cục bộ của View `@State` là một property wrapper thông báo cho SwiftUI: "theo dõi biến này và làm mới view khi giá trị thay đổi". Phù hợp cho trạng thái cục bộ của một view. Hãy tạo một bộ đếm tương tác để hiểu cơ chế hoạt động: ```swift // CounterView.swift struct CounterView: View { // @State creates a "source of truth" for this view // private because state shouldn't be modified from outside @State private var count = 0 var body: some View { VStack(spacing: 30) { // This Text updates automatically when count changes Text("\(count)") .font(.system(size: 72, weight: .bold)) HStack(spacing: 40) { // Decrement button Button(action: { count -= 1 // Modifies state → view refreshes }) { Image(systemName: "minus.circle.fill") .font(.largeTitle) } // Increment button Button(action: { count += 1 }) { Image(systemName: "plus.circle.fill") .font(.largeTitle) } } } } } ``` Khi nhấn nút, giá trị `count` thay đổi. SwiftUI phát hiện sự thay đổi này và thực thi lại `body` để cập nhật hiển thị. Không cần cập nhật nhãn thủ công — mọi thứ diễn ra tự động. ### @Binding: Chia sẻ trạng thái giữa các View Đôi khi view con cần thay đổi trạng thái của view cha. Đó là lúc `@Binding` phát huy tác dụng: tạo kết nối hai chiều đến một `@State` hiện có. Dưới đây là ví dụ cụ thể: trường nhập tên người dùng với xác thực theo thời gian thực. ```swift // UsernameValidation.swift // Parent view: owns the state struct SignupForm: View { @State private var username = "" // Source of truth @State private var isValid = false // Validation state var body: some View { VStack(spacing: 20) { // We pass BINDINGS (with $) to the child view UsernameField(username: $username, isValid: $isValid) Button("Create Account") { // Submit the form } .disabled(!isValid) // Disabled if invalid .buttonStyle(.borderedProminent) } .padding() } } ``` View con nhận binding và có thể thay đổi chúng: ```swift // Child view: receives and modifies state via @Binding struct UsernameField: View { @Binding var username: String // Connection to parent's @State @Binding var isValid: Bool var body: some View { VStack(alignment: .leading, spacing: 8) { TextField("Username", text: $username) .textFieldStyle(.roundedBorder) .onChange(of: username) { oldValue, newValue in // Validation: at least 3 characters isValid = newValue.count >= 3 } // Visual feedback HStack { Image(systemName: isValid ? "checkmark.circle" : "xmark.circle") Text("Minimum 3 characters") } .font(.caption) .foregroundColor(isValid ? .green : .red) } } } ``` Khi người dùng nhập vào TextField, `username` được thay đổi thông qua binding. View cha nhận biết thay đổi này và có thể sử dụng. Đây là giao tiếp hai chiều rõ ràng. > **Warning** > > Chỉ nên sử dụng `@State` cho trạng thái đơn giản và cục bộ. Đối với dữ liệu được chia sẻ giữa nhiều màn hình hoặc logic phức tạp, nên dùng `@Observable` (iOS 17+) hoặc các mẫu MVVM. ## Hiển thị danh sách động Danh sách xuất hiện ở khắp nơi trong các ứng dụng di động. SwiftUI cung cấp `List` để hiển thị các tập hợp dữ liệu với kiểu dáng native iOS (đường phân cách, thao tác vuốt, v.v.). ### Tạo danh sách đơn giản Để hiển thị danh sách, cần hai thứ: dữ liệu và cách nhận dạng chúng. Giao thức `Identifiable` cho phép SwiftUI biết view nào tương ứng với dữ liệu nào. Đầu tiên, định nghĩa mô hình dữ liệu: ```swift // Models/Interview.swift // Identifiable lets SwiftUI track each element struct Interview: Identifiable { let id = UUID() // Auto-generated unique identifier let technology: String let difficulty: String let questionCount: Int } ``` Bây giờ hãy tạo danh sách. Ý tưởng là duyệt qua dữ liệu bằng `ForEach` và tạo một hàng cho mỗi phần tử: ```swift // InterviewListView.swift struct InterviewListView: View { // Data to display (in reality, this would come from an API) @State private var interviews = [ Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25), Interview(technology: "Android", difficulty: "Advanced", questionCount: 30), Interview(technology: "React", difficulty: "Beginner", questionCount: 20) ] var body: some View { // NavigationStack enables the navigation bar NavigationStack { List { // ForEach iterates over each interview // Thanks to Identifiable, no need to specify id: ForEach(interviews) { interview in InterviewRow(interview: interview) } } .navigationTitle("My Interviews") } } } ``` Và đây là view cho mỗi hàng, được tách ra thành component riêng để dễ đọc: ```swift // InterviewRow.swift struct InterviewRow: View { let interview: Interview var body: some View { HStack { // Left column: title and subtitle VStack(alignment: .leading, spacing: 4) { Text(interview.technology) .font(.headline) Text(interview.difficulty) .font(.subheadline) .foregroundColor(.secondary) } Spacer() // Badge with question count Text("\(interview.questionCount) Q") .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.blue.opacity(0.1)) .cornerRadius(8) } .padding(.vertical, 4) } } ``` Việc tách `InterviewRow` thành struct riêng giúp mã dễ đọc hơn và có thể tái sử dụng. Đây là một trong những thực hành tốt nhất của SwiftUI. ### Thêm thao tác: Xóa và sắp xếp lại Danh sách iOS hỗ trợ native thao tác xóa (vuốt trái) và sắp xếp lại (kéo thả). SwiftUI làm cho điều này trở nên đơn giản với `.onDelete` và `.onMove`: ```swift // InterviewListView.swift (enhanced version) struct InterviewListView: View { @State private var interviews = [/* ... data ... */] var body: some View { NavigationStack { List { ForEach(interviews) { interview in InterviewRow(interview: interview) } // Swipe to delete .onDelete(perform: deleteInterview) // Drag and drop to reorder .onMove(perform: moveInterview) } .navigationTitle("My Interviews") .toolbar { // "Edit" button that activates edit mode EditButton() } } } // Deletes elements at specified indices private func deleteInterview(at offsets: IndexSet) { interviews.remove(atOffsets: offsets) } // Moves elements from one position to another private func moveInterview(from source: IndexSet, to destination: Int) { interviews.move(fromOffsets: source, toOffset: destination) } } ``` Chỉ với 4 dòng mã bổ sung (`.onDelete`, `.onMove`, `EditButton` và hai hàm), danh sách trở nên hoàn toàn tương tác. Đó chính là sức mạnh của SwiftUI. ## Tạo hiệu ứng động cho giao diện SwiftUI xử lý hiệu ứng động rất xuất sắc. Khác với UIKit vốn đòi hỏi nhiều mã cho hiệu ứng, ở đây mọi thứ đều mang tính khai báo: chỉ cần mô tả trạng thái cuối cùng và SwiftUI sẽ tạo hiệu ứng chuyển đổi. ### Hiệu ứng ngầm với withAnimation Cách đơn giản nhất để tạo hiệu ứng là bọc thay đổi trạng thái trong `withAnimation`. SwiftUI phát hiện những gì thay đổi và tự động tạo hiệu ứng cho các thuộc tính trực quan bị ảnh hưởng. ```swift // AnimatedCard.swift struct AnimatedCard: View { @State private var isExpanded = false var body: some View { VStack { RoundedRectangle(cornerRadius: 20) .fill(.blue) // Dimensions change based on state .frame( width: isExpanded ? 300 : 150, height: isExpanded ? 200 : 100 ) Button(isExpanded ? "Collapse" : "Expand") { // withAnimation animates ALL visual changes // resulting from this state change withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { isExpanded.toggle() } } .padding(.top) } } } ``` Khi nhấn nút, `isExpanded` chuyển đổi. Nhờ `withAnimation`, thay đổi kích thước hình chữ nhật được tạo hiệu ứng với hiệu ứng lò xo. Không cần chỉ định cụ thể phần nào cần tạo hiệu ứng — SwiftUI tự xác định. ### Transition: Hiệu ứng xuất hiện và biến mất Transition xác định cách một view xuất hiện hoặc biến mất. Mặc định là hiệu ứng mờ dần (`opacity`), nhưng có thể tùy chỉnh: ```swift // TransitionDemo.swift struct TransitionDemo: View { @State private var showDetails = false var body: some View { VStack(spacing: 20) { Button("Show Details") { withAnimation(.easeInOut(duration: 0.3)) { showDetails.toggle() } } // This view appears/disappears with a transition if showDetails { DetailCard() // Asymmetric transition: different for entry and exit .transition( .asymmetric( insertion: .scale.combined(with: .opacity), // Entry: zoom + fade removal: .slide // Exit: slide ) ) } } } } struct DetailCard: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Interview Details") .font(.headline) Text("25 questions • 45 minutes") .foregroundColor(.secondary) } .padding() .background(Color(.systemGray6)) .cornerRadius(16) } } ``` Thẻ xuất hiện với hiệu ứng phóng to từ trung tâm (scale + opacity) và biến mất bằng cách trượt sang bên (slide). Những hiệu ứng vi mô này làm cho giao diện sống động và chuyên nghiệp. > **Performance** > > SwiftUI tự động tối ưu hóa hiệu ứng động. Nên sử dụng `.spring()` để có cảm giác tự nhiên, và tránh hiệu ứng quá dài (> 0,5 giây) vì chúng có thể gây khó chịu cho người dùng. ## Tải dữ liệu bất đồng bộ Trong các ứng dụng thực tế, dữ liệu thường đến từ API. Swift Concurrency (async/await) tích hợp hoàn hảo với SwiftUI thông qua modifier `.task`. ### Mẫu Loading / Error / Success Đây là mẫu tiêu chuẩn để hiển thị dữ liệu từ API. Ba trạng thái được xử lý: đang tải, lỗi và thành công. ```swift // AsyncDataView.swift struct AsyncDataView: View { @State private var questions: [Question] = [] @State private var isLoading = true @State private var errorMessage: String? var body: some View { Group { if isLoading { // State: loading in progress ProgressView("Loading...") } else if let error = errorMessage { // State: error VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text(error) Button("Retry") { Task { await loadQuestions() } } } } else { // State: success, display data List(questions) { question in Text(question.title) } } } // .task runs automatically when the view appears .task { await loadQuestions() } // Pull-to-refresh .refreshable { await loadQuestions() } } private func loadQuestions() async { isLoading = true errorMessage = nil do { // Async API call questions = try await QuestionService.shared.fetchQuestions() } catch { errorMessage = "Unable to load questions" } isLoading = false } } ``` Modifier `.task` đóng vai trò then chốt: khởi chạy tác vụ bất đồng bộ khi view xuất hiện và tự động hủy khi view biến mất. Rò rỉ bộ nhớ hoàn toàn được loại trừ. ## Kết luận SwiftUI đã trở thành phần không thể thiếu trong phát triển iOS hiện đại. Với sự ra mắt của iOS 18, framework này đạt mức độ trưởng thành hoàn toàn phù hợp cho các ứng dụng production. ### Những điểm chính - **Mô hình khai báo**: mô tả kết quả mong muốn thay vì cách thức xây dựng - **@State và @Binding**: quản lý trạng thái phản ứng và truyền giữa các view - **Stacks**: kết hợp VStack, HStack và ZStack cho bố cục linh hoạt - **List**: hiển thị tập hợp với mã tối thiểu và tương tác native - **Hiệu ứng động**: sử dụng `withAnimation` cho chuyển đổi mượt mà tự động - **Async/await**: tải dữ liệu với `.task` và xử lý trạng thái loading/error ### Danh sách kiểm tra - Hiểu sự khác biệt giữa mệnh lệnh (UIKit) và khai báo (SwiftUI) - Thành thạo modifier và thứ tự áp dụng - Biết khi nào dùng @State, @Binding và @Observable - Xây dựng bố cục với Stacks - Triển khai danh sách với các thao tác (xóa, di chuyển) - Tạo hiệu ứng thay đổi trạng thái với withAnimation SwiftUI mở ra cơ hội xây dựng các ứng dụng tinh tế trên toàn bộ hệ sinh thái Apple. Cách tốt nhất để học là thực hành: hãy tạo một dự án cá nhân nhỏ và thử nghiệm với từng khái niệm trong bài viết này. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/ios/swiftui-building-modern-interfaces