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.

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.
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:
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:
// 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 spaceSự 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.
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.
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:
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.
// 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:
// 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.
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.
Sẵn sàng chinh phục phỏng vấn iOS?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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:
// 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ử:
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:
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:
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.
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:
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.
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.
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
withAnimationcho chuyển đổi mượt mà tự động - Async/await: tải dữ liệu với
.taskvà 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
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
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.
Thẻ
Chia sẻ
Bài viết liên quan

Hiệu Suất SwiftUI: Tối Ưu Hóa LazyVStack và Danh Sách Phức Tạp
Kỹ thuật tối ưu hóa cho LazyVStack và danh sách SwiftUI. Giảm tiêu thụ bộ nhớ, cải thiện hiệu suất cuộn và tránh các lỗi thường gặp.

ViewModifier tùy chỉnh trong SwiftUI: các mẫu tái sử dụng cho Design System
Xây dựng ViewModifier tùy chỉnh trong SwiftUI cho một design system nhất quán. Các mẫu, thực hành tốt nhất và ví dụ thực tế để tạo kiểu cho view iOS hiệu quả.

SwiftUI @Observable vs @State: Khi Nào Dùng Cái Nào Năm 2026
Nắm vững sự khác biệt giữa @Observable và @State trong SwiftUI để chọn công cụ quản lý state phù hợp cho ứng dụng iOS.