09 thg 5, 2025·8 phút đọc

Mẫu NavigationStack trong SwiftUI cho luồng nhiều bước dự đoán được

Mẫu NavigationStack trong SwiftUI cho luồng nhiều bước, với điều hướng rõ ràng, hành vi Back an toàn và ví dụ thực tế cho onboarding và wizard phê duyệt.

Mẫu NavigationStack trong SwiftUI cho luồng nhiều bước dự đoán được

Điều gì hay sai trong các luồng nhiều bước

Luồng nhiều bước là bất cứ chuỗi nào mà bước 1 phải xảy ra trước thì bước 2 mới có ý nghĩa. Ví dụ phổ biến gồm onboarding, yêu cầu phê duyệt (xem xét, xác nhận, gửi), và nhập liệu theo kiểu wizard nơi người dùng dựng một dự thảo qua nhiều màn hình.

Những luồng này có vẻ đơn giản chỉ khi Back hoạt động như người dùng mong đợi. Nếu Back đưa họ đến chỗ bất ngờ, người dùng mất niềm tin vào app. Điều đó thể hiện bằng gửi sai thông tin, bỏ dở onboarding, và các vé hỗ trợ như “Tôi không quay lại được màn hình trước đó.”

Điều hướng lộn xộn thường trông như một trong các trường hợp sau:

  • App nhảy đến màn hình sai, hoặc thoát luồng quá sớm.
  • Cùng một màn hình xuất hiện hai lần vì bị push hai lần.
  • Một bước bị reset khi Back và người dùng mất bản nháp.
  • Người dùng có thể tới bước 3 mà chưa hoàn tất bước 1, tạo ra trạng thái không hợp lệ.
  • Sau deep link hoặc khởi động lại app, app hiển thị đúng màn hình nhưng với dữ liệu sai.

Một mô hình tư duy hữu ích: luồng nhiều bước là hai thứ đi cùng nhau.

Thứ nhất, một stack các màn hình (nơi người dùng có thể bấm Back). Thứ hai, trạng thái luồng chia sẻ (dữ liệu nháp và tiến độ mà không nên biến mất chỉ vì một màn hình biến mất).

Nhiều cấu hình NavigationStack sụp khi stack màn hình và trạng thái luồng lệch pha. Ví dụ, luồng onboarding có thể push “Create profile” hai lần (route trùng lặp), trong khi bản nháp profile nằm trong view và bị tái tạo khi render lại. Người dùng bấm Back, thấy phiên bản khác của form, và cho rằng app không đáng tin.

Hành vi dự đoán được bắt đầu bằng việc đặt tên cho luồng, xác định Back nên làm gì ở mỗi bước, và cho trạng thái luồng một nơi lưu trữ rõ ràng.

Những khái niệm NavigationStack bạn thực sự cần

Với luồng nhiều bước, dùng NavigationStack thay vì NavigationView cũ. NavigationView có thể hành xử khác nhau giữa các phiên bản iOS và khó suy luận khi bạn push, pop hoặc khôi phục màn hình. NavigationStack là API hiện đại xem điều hướng như một stack thực thụ.

NavigationStack lưu lịch sử nơi người dùng đã đi. Mỗi lần push thêm một destination vào stack. Mỗi hành động Back sẽ pop một destination. Quy tắc đơn giản đó làm cho luồng cảm thấy ổn định: UI nên phản chiếu chuỗi các bước rõ ràng.

Stack thực ra chứa gì

SwiftUI không lưu giữ các instance view của bạn. Nó lưu dữ liệu bạn dùng để điều hướng (giá trị route) và dùng nó để xây lại view đích khi cần. Điều này có vài hệ quả thực tế:

  • Đừng dựa vào việc view luôn còn sống để giữ dữ liệu quan trọng.
  • Nếu một màn hình cần state, đặt state đó trong model (ví dụ ObservableObject) sống bên ngoài view được push.
  • Nếu bạn push cùng một destination hai lần với dữ liệu khác nhau, SwiftUI coi đó là hai mục khác nhau trong stack.

Khi luồng của bạn không chỉ là một hoặc hai lần push cố định, NavigationPath là công cụ bạn nên dùng. Hãy xem nó như một danh sách có thể chỉnh sửa của các giá trị “chúng ta sẽ đi đâu”. Bạn có thể append route để tiến, removeLast để quay lại, hoặc thay thế toàn bộ path để nhảy tới bước sau.

Nó phù hợp khi bạn cần các bước kiểu wizard, cần reset luồng sau khi hoàn tất, hoặc muốn khôi phục một luồng còn dở từ trạng thái đã lưu.

Dự đoán tốt hơn là khôn ngoan. Ít luật ẩn (nhảy tự động, pop ngầm, side effect do view) nghĩa là ít bug back stack lạ sau này.

Mô hình hoá luồng với một enum route nhỏ

Điều hướng dự đoán được bắt đầu bằng một quyết định: giữ routing ở một chỗ, và biến mỗi màn hình trong luồng thành một giá trị nhỏ, rõ ràng.

Tạo một nguồn chân lý duy nhất, ví dụ FlowRouter (một ObservableObject) sở hữu NavigationPath. Điều này giữ mọi push và pop nhất quán, thay vì phân tán điều hướng qua nhiều view.

Cấu trúc router đơn giản

Sử dụng một enum để đại diện các bước. Chỉ thêm associated values cho các định danh nhẹ (như ID), không phải cả model lớn.

enum Step: Hashable {
    case welcome
    case profile
    case verifyCode(phoneID: UUID)
    case review(applicationID: UUID)
    case done
}

final class FlowRouter: ObservableObject {
    @Published var path = NavigationPath()

    func go(_ step: Step) { path.append(step) }
    func back() { if !path.isEmpty { path.removeLast() } }
    func reset() { path = NavigationPath() }
}

Giữ trạng thái luồng tách khỏi trạng thái điều hướng

Xem điều hướng là “người dùng đang ở đâu”, và trạng thái luồng là “họ đã nhập gì đến giờ”. Đặt dữ liệu luồng vào kho riêng (ví dụ OnboardingState với tên, email, tài liệu upload) và giữ nó ổn định khi các màn hình xuất hiện và biến mất.

Quy tắc đơn giản:

  • FlowRouter.path chỉ chứa các giá trị Step.
  • OnboardingState chứa các input và dữ liệu nháp của người dùng.
  • Các bước mang ID để lookup dữ liệu, không phải dữ liệu thực tế.

Điều này tránh hashing mong manh, path cồng kềnh và reset bất ngờ khi SwiftUI rebuild view.

Từng bước: xây wizard với NavigationPath

Với các màn hình kiểu wizard, cách đơn giản nhất là kiểm soát stack bằng tay. Mục tiêu là một nguồn chân lý cho “tôi đang ở đâu trong luồng?” và một cách duy nhất để tiến hoặc lùi.

Bắt đầu với NavigationStack(path:) liên kết với một NavigationPath. Mỗi màn hình được push là một giá trị (thường là case enum), và bạn đăng ký destinations một lần.

import SwiftUI

enum WizardRoute: Hashable {
    case profile
    case verifyEmail
    case permissions
    case review
}

struct OnboardingWizard: View {
    @State private var path = NavigationPath()
    @State private var currentIndex = 0

    private let steps: [WizardRoute] = [.profile, .verifyEmail, .permissions, .review]

    var body: some View {
        NavigationStack(path: $path) {
            StartScreen {
                goToStep(0) // push first step
            }
            .navigationDestination(for: WizardRoute.self) { route in
                switch route {
                case .profile:
                    ProfileStep(onNext: { goToStep(1) })
                case .verifyEmail:
                    VerifyEmailStep(onNext: { goToStep(2) })
                case .permissions:
                    PermissionsStep(onNext: { goToStep(3) })
                case .review:
                    ReviewStep(onEditProfile: { popToStep(0) })
                }
            }
        }
    }

    private func goToStep(_ index: Int) {
        currentIndex = index
        path.append(steps[index])
    }

    private func popToStep(_ index: Int) {
        let toRemove = max(0, currentIndex - index)
        if toRemove > 0 { path.removeLast(toRemove) }
        currentIndex = index
    }
}

Để Back dự đoán được, giữ vài thói quen: mỗi lần tiến append đúng một route, giữ "Next" tuyến tính (chỉ push bước tiếp theo), và khi cần quay về (như “Edit profile” từ Review), cắt stack về một index đã biết.

Điều này tránh màn hình trùng lặp vô tình và khiến Back khớp kỳ vọng của người dùng: một lần chạm = một bước.

Giữ dữ liệu ổn định khi các màn hình xuất hiện và biến mất

Thêm những thành phần cần thiết cho luồng của bạn
Sử dụng các module có sẵn như auth và payments để hoàn thiện luồng đầu-cuối nhanh hơn.
Khám phá nền tảng

Luồng nhiều bước trở nên không đáng tin khi mỗi màn hình tự ôm state riêng. Bạn gõ tên, đi tiếp, quay lại, và trường trống vì view bị tạo lại.

Cách sửa đơn giản: xem luồng như một đối tượng nháp duy nhất, và mỗi bước chỉnh sửa nó.

Trong SwiftUI, điều này thường là một ObservableObject chia sẻ được tạo một lần khi vào luồng và truyền cho mọi bước. Đừng lưu giá trị nháp trong @State mỗi view trừ khi chúng thực sự chỉ thuộc về màn hình đó.

final class OnboardingDraft: ObservableObject {
    @Published var fullName = ""
    @Published var email = ""
    @Published var wantsNotifications = false

    var canGoNextFromProfile: Bool {
        !fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        && email.contains("@")
    }
}

Tạo nó ở điểm vào, rồi chia sẻ bằng @StateObject@EnvironmentObject (hoặc truyền rõ ràng). Khi đó stack có thể thay đổi mà không mất dữ liệu.

Quyết định dữ liệu nào tồn tại khi back

Không phải thứ gì cũng nên tồn tại mãi. Quyết định quy tắc trước để luồng nhất quán.

Giữ input của người dùng (text field, toggle, lựa chọn) trừ khi họ rõ ràng reset. Reset state UI cụ thể cho bước (spinner load, alert tạm thời, animation ngắn). Xoá các trường nhạy cảm (mã xác thực dùng một lần) khi rời bước đó. Nếu một lựa chọn thay đổi các bước sau, chỉ xoá các trường phụ thuộc.

Validation phù hợp ở đây. Thay vì cho người dùng tiến và rồi hiện lỗi ở màn hình tiếp theo, giữ họ ở bước hiện tại cho tới khi hợp lệ. Vô hiệu hoá nút dựa trên property tính toán như canGoNextFromProfile thường là đủ.

Lưu checkpoint mà không quá tay

Một số nháp chỉ cần sống trong bộ nhớ. Một số khác cần tồn tại qua khởi động lại hoặc crash. Mặc định thực tế:

  • Giữ dữ liệu trong bộ nhớ khi người dùng đang đi qua các bước.
  • Persist cục bộ ở các mốc rõ ràng (tạo tài khoản, gửi phê duyệt, bắt đầu thanh toán).
  • Persist sớm hơn nếu luồng dài hoặc nhập liệu mất hơn một phút.

Như vậy, màn hình có thể xuất hiện và biến mất tự do, và tiến độ người dùng vẫn ổn định và tôn trọng thời gian của họ.

Deep link quan trọng vì luồng thực tế hiếm khi bắt đầu ở bước 1. Ai đó nhấn email, thông báo push, hoặc link chia sẻ và mong tới đúng màn hình, như bước 3 của onboarding hoặc màn final approval.

Với NavigationStack, xử lý deep link như hướng dẫn để xây một path hợp lệ, không phải lệnh nhảy thẳng đến một view. Bắt đầu từ đầu luồng và append chỉ những bước phù hợp với người dùng và session này.

Một mẫu tốt là: parse ID ngoài, load dữ liệu tối thiểu cần thiết, rồi chuyển nó thành 1 chuỗi route.

enum Route: Hashable {
    case start
    case profile
    case verifyEmail
    case approve(requestID: String)
}

func pathForDeepLink(requestID: String, hasProfile: Bool, emailVerified: Bool) -> [Route] {
    var routes: [Route] = [.start]
    if !hasProfile { routes.append(.profile) }
    if !emailVerified { routes.append(.verifyEmail) }
    routes.append(.approve(requestID: requestID))
    return routes
}

Những kiểm tra đó là hàng rào bảo vệ. Nếu các tiền đề còn thiếu, đừng đặt người dùng lên bước 3 với lỗi và không có đường tiến. Chuyển họ tới bước thiếu đầu tiên, và đảm bảo back stack vẫn kể một câu chuyện mạch lạc.

Khôi phục luồng dở dang

Để khôi phục sau khi relaunch, lưu hai thứ: trạng thái route gần nhất và dữ liệu nháp người dùng. Rồi quyết định cách resume mà không gây ngạc nhiên.

Nếu nháp còn mới (phút hoặc vài giờ), hiển thị lựa chọn “Resume” rõ ràng. Nếu nó cũ, bắt đầu lại từ đầu nhưng dùng nháp để điền trước các trường. Nếu yêu cầu thay đổi, rebuild path dùng cùng các guardrails.

Push vs modal: giữ luồng dễ thoát

Sở hữu codebase ứng dụng
Giữ quyền kiểm soát bằng cách xuất mã nguồn thật khi bạn cần tự host hoặc tùy chỉnh sâu.
Xuất mã

Luồng cảm thấy dự đoán được khi có một cách chính để tiến: push màn hình trên một stack duy nhất. Dùng sheet và full-screen cover cho các tác vụ phụ, không cho con đường chính.

Push (NavigationStack) phù hợp khi người dùng mong Back sẽ lần theo bước trước. Modal (sheet hoặc fullScreenCover) phù hợp khi người dùng làm tác vụ phụ, chọn nhanh, hoặc xác nhận hành động rủi ro.

Một bộ quy tắc đơn giản tránh phần lớn sự lộn xộn:

  • Push cho con đường chính (Bước 1, Bước 2, Bước 3).
  • Dùng sheet cho tác vụ nhỏ tùy chọn (chọn ngày, chọn quốc gia, scan tài liệu).
  • Dùng fullScreenCover cho “thế giới riêng” (đăng nhập, chụp ảnh, đọc tài liệu pháp lý dài).
  • Dùng modal cho xác nhận (hủy luồng, xoá nháp, gửi phê duyệt).

Sai lầm phổ biến là đặt màn chính vào sheet. Nếu Bước 2 là sheet, người dùng có thể vuốt dismiss, mất ngữ cảnh, và có stack nói họ đang ở Bước 1 trong khi dữ liệu lại báo đã hoàn tất Bước 2.

Xác nhận là ngược lại: push một màn “Bạn có chắc?” vào wizard sẽ làm rối stack và có thể tạo vòng lặp (Step 3 -> Confirm -> Back -> Step 3 -> Back -> Confirm).

Cách đóng mọi thứ gọn sau “Done”

Quyết định “Done” nghĩa gì trước: trở về home, trở về list, hay hiển thị màn success.

Nếu luồng được push, reset NavigationPath về rỗng để pop về gốc. Nếu luồng được present modal, gọi dismiss() từ environment. Nếu có cả hai (modal chứa NavigationStack), dismiss modal chứ không dismiss từng màn. Sau khi submit thành công, cũng nên xoá dữ liệu nháp để luồng mở lại bắt đầu sạch.

Hành vi nút Back và các khoảnh khắc “Bạn có chắc?”

Xây dựng full-stack không cần glue code
Sinh backend, web và mobile sẵn sàng sản xuất từ cùng một thiết kế luồng.
Tạo ứng dụng

Với hầu hết luồng nhiều bước, việc tốt nhất là không làm gì: để nút Back hệ thống (và gesture swipe-back) hoạt động. Nó khớp với kỳ vọng người dùng và tránh bug nơi UI nói một đằng mà trạng thái điều hướng nói một nẻo.

Can thiệp chỉ đáng khi quay lại sẽ gây hại thực sự, như mất một form dài chưa lưu hoặc bỏ một hành động không thể đảo ngược. Nếu người dùng có thể quay lại an toàn và tiếp tục, đừng thêm friction.

Cách thực tế là giữ điều hướng hệ thống, nhưng chỉ thêm xác nhận khi màn hình “dirty” (đã chỉnh sửa). Điều đó nghĩa là cung cấp back action của riêng bạn và hỏi một lần, với lựa chọn rõ ràng.

@Environment(\.dismiss) private var dismiss
@State private var showLeaveConfirm = false
let hasUnsavedChanges: Bool

var body: some View {
  Form { /* fields */ }
    .navigationBarBackButtonHidden(hasUnsavedChanges)
    .toolbar {
      if hasUnsavedChanges {
        ToolbarItem(placement: .navigationBarLeading) {
          Button("Back") { showLeaveConfirm = true }
        }
      }
    }
    .confirmationDialog("Discard changes?", isPresented: $showLeaveConfirm) {
      Button("Discard", role: .destructive) { dismiss() }
      Button("Keep Editing", role: .cancel) {}
    }
}

Đừng để điều này biến thành bẫy:

  • Hỏi chỉ khi bạn có thể giải thích hậu quả trong một câu ngắn.
  • Cung cấp lựa chọn an toàn (Cancel, Keep Editing) và một lối thoát rõ ràng (Discard, Leave).
  • Đừng ẩn nút Back trừ khi bạn thay bằng Back hoặc Close dễ thấy.
  • Thích xác nhận hành động không thể đảo ngược (như “Approve”) hơn là chặn điều hướng khắp nơi.

Nếu bạn thường xuyên phải chống lại cử chỉ back, đó thường là dấu hiệu luồng cần autosave, lưu nháp, hoặc các bước nhỏ hơn.

Những lỗi phổ biến tạo back stack lạ

Hầu hết bug “tại sao nó quay về chỗ đó?” không phải SwiftUI ngẫu nhiên. Chúng thường đến từ các pattern làm trạng thái điều hướng không ổn định. Để hành vi dự đoán được, đối xử với back stack như dữ liệu app: ổn định, có thể kiểm thử, và được sở hữu ở một nơi.

Nhiều stack vô tình

Cạm bẫy phổ biến là có nhiều hơn một NavigationStack mà bạn không nhận ra. Ví dụ, mỗi tab có root stack riêng, rồi một view con thêm một stack khác trong luồng. Kết quả là back behavior rối, navigation bar biến mất, hoặc màn hình không pop như mong đợi.

Vấn đề khác là tái tạo NavigationPath quá thường xuyên. Nếu path được tạo trong một view re-render, nó có thể reset khi state thay đổi và nhảy người dùng về bước 1 sau khi họ gõ vào trường.

Những sai lầm đằng sau hầu hết các stack lạ rất đơn giản:

  • Lồng NavigationStack trong một stack khác (thường trong tab hoặc nội dung sheet)
  • Tái khởi tạo NavigationPath() trong quá trình cập nhật view thay vì giữ nó trong state sống lâu
  • Đặt các giá trị không ổn định trong route (như model thay đổi), phá vỡ Hashable và gây mismatched destinations
  • Phân tán quyết định điều hướng qua nhiều handler nút đến mức không ai giải thích được “next” nghĩa là gì
  • Điều khiển luồng từ nhiều nguồn cùng lúc (ví dụ, view model và view cùng mutate path)

Nếu cần truyền dữ liệu giữa các bước, ưu tiên ID ổn định trong route (IDs, enum bước) và giữ dữ liệu form thực tế trong state chia sẻ.

Ví dụ cụ thể: nếu route của bạn là .profile(User)User thay đổi khi người ta gõ, SwiftUI có thể coi đó là route khác và nối lại stack. Hãy để route là .profile và giữ dữ liệu nháp trong shared state.

Checklist nhanh cho điều hướng dự đoán được

Phát hành cấu trúc wizard sạch sẽ
Tạo wizard cho onboarding và phê duyệt với một nguồn chân lý duy nhất cho các bước và tiến độ.
Xây luồng

Khi một luồng cảm thấy sai, thường là vì back stack không kể cùng một câu chuyện như người dùng. Trước khi mài giũa UI, rà soát các quy tắc điều hướng.

Test trên thiết bị thật, không chỉ preview, và thử cả nhấn chậm và nhấn nhanh. Nhấn nhanh thường lộ các push trùng lặp và trạng thái mất.

  • Quay lại từng bước từ màn cuối về màn đầu. Xác nhận từng màn hiển thị cùng dữ liệu người dùng đã nhập trước đó.
  • Gọi Cancel từ mọi bước (bao gồm đầu và cuối). Xác nhận luôn về chỗ hợp lý, không phải màn hình trước đó ngẫu nhiên.
  • Force quit giữa chừng rồi relaunch. Đảm bảo bạn có thể resume an toàn, hoặc khôi phục path hoặc bắt đầu lại từ bước đã biết với dữ liệu đã lưu.
  • Mở luồng bằng deep link hoặc shortcut. Xác minh màn đích hợp lệ; nếu dữ liệu cần thiết thiếu, chuyển hướng tới bước sớm nhất có thể thu thập nó.
  • Kết thúc bằng Done và xác nhận luồng bị loại bỏ gọn. Người dùng không nên bấm Back và đi vào wizard đã hoàn thành.

Cách test đơn giản: tưởng tượng một onboarding wizard với ba màn (Profile, Permissions, Confirm). Nhập tên, đi tiếp, quay lại, sửa, rồi nhảy tới Confirm bằng deep link. Nếu Confirm hiện tên cũ, hoặc Back đưa bạn tới màn Profile trùng lặp, nghĩa là cập nhật path của bạn không nhất quán.

Nếu bạn vượt qua checklist mà không có bất ngờ, luồng sẽ cảm thấy bình tĩnh và dự đoán được, ngay cả khi người dùng rời rồi quay lại sau.

Ví dụ thực tế và bước tiếp theo

Hình dung một luồng phê duyệt của quản lý cho yêu cầu chi phí. Nó có bốn bước: Review, Edit, Confirm, và Receipt. Người dùng mong một điều: Back luôn về bước trước đó, không phải một màn ngẫu nhiên họ đã ghé trước đó.

Một enum route đơn giản giữ điều này dự đoán được. NavigationPath chỉ nên chứa route và các ID nhỏ cần để load lại trạng thái, ví dụ expenseIDmode (review vs edit). Tránh push các model lớn và mutable vào path vì điều đó làm restore và deep link mong manh.

Giữ bản nháp làm nguồn chân lý duy nhất bên ngoài view, như @StateObject flow model (hoặc một store). Mỗi bước đọc và ghi model đó, để màn hình có thể xuất hiện và biến mất mà không mất input.

Tối thiểu, bạn theo dõi ba thứ:

  • Routes (ví dụ: review(expenseID), edit(expenseID), confirm(expenseID), receipt(expenseID))
  • Data (một object nháp với các dòng mục và ghi chú, cùng trạng thái như pending, approved, rejected)
  • Location (nháp trong flow model, bản canonical trên server, và một token restore nhỏ cục bộ: expenseID + bước cuối)

Các edge case là nơi luồng xây dựng hoặc phá hoại niềm tin. Nếu quản lý từ chối ở Confirm, quyết định Back có về Edit để sửa hay thoát luồng. Nếu họ trở lại sau, khôi phục bước cuối từ token đã lưu và load lại nháp. Nếu chuyển thiết bị, xem server là nguồn chân lý: tái cấu trúc path từ trạng thái server và đưa họ đến bước phù hợp.

Bước tiếp theo: document enum route của bạn (mỗi case nghĩa là gì và khi nào dùng), thêm vài test cơ bản cho việc xây path và khôi phục, và tuân theo một quy tắc: view không sở hữu quyết định điều hướng.

Nếu bạn đang xây nhiều luồng giống nhau mà không muốn viết mọi thứ từ đầu, các nền tảng như AppMaster (appmaster.io) áp dụng cùng nguyên tắc: tách biệt điều hướng bước và dữ liệu nghiệp vụ để màn hình có thể thay đổi mà không phá tiến độ người dùng.

Câu hỏi thường gặp

Làm sao để nút Back có hành vi dự đoán được trong luồng nhiều bước SwiftUI?

Sử dụng NavigationStack với một NavigationPath duy nhất do bạn kiểm soát. Mỗi hành động “Next” chỉ append đúng một route và mỗi hành động Back chỉ remove đúng một route. Khi cần nhảy (ví dụ “Edit profile” từ màn Review), hãy cắt path về một bước đã biết thay vì tiếp tục append thêm màn hình.

Tại sao dữ liệu form của tôi biến mất khi điều hướng quay lại?

Vì SwiftUI dựng lại view đích từ giá trị route chứ không giữ nguyên thể hiện view. Nếu dữ liệu form nằm trong @State của view, nó có thể bị đặt lại khi view được tạo lại. Đặt dữ liệu nháp vào một model chia sẻ (ví dụ ObservableObject) tồn tại bên ngoài các view được push.

Tại sao tôi thấy cùng một màn hình xuất hiện hai lần trong stack?

Thường là do bạn append cùng một route nhiều lần (thường vì nhấn nhanh hoặc nhiều luồng code cùng gọi navigation). Vô hiệu hóa nút Next khi đang điều hướng hoặc khi đang validate/loading, và tập trung mọi thao tác điều hướng ở một nơi để chỉ có một append xảy ra cho mỗi bước.

Tôi nên lưu gì trong enum route và nên tránh gì?

Giữ các giá trị routing nhỏ và ổn định, ví dụ một case enum kèm ID nhẹ. Dữ liệu có thể thay đổi (nháp) nên để ở một object chia sẻ riêng và tìm lại theo ID khi cần. Đẩy các model lớn và mutable vào path có thể phá vỡ Hashable và gây ra mismatched destinations.

Làm sao tách biệt trạng thái điều hướng và trạng thái luồng một cách rõ ràng?

Điều hướng là “người dùng đang ở đâu”, trạng thái luồng là “họ đã nhập gì”. Quản lý NavigationPath trong một router (hoặc state cấp cao duy nhất) và quản lý nháp trong một ObservableObject riêng. Mỗi màn hình sửa nháp; router chỉ thay đổi các bước.

Cách an toàn nhất để xử lý deep link vào bước 3 của wizard là gì?

Xử lý deep link như một hướng dẫn để xây dựng một chuỗi route hợp lệ, không phải dịch chuyển thẳng đến một view. Xây path bằng cách append các bước tiền đề cần thiết trước (dựa trên trạng thái người dùng), rồi mới append bước đích. Điều này giữ cho stack Back có nghĩa và tránh trạng thái không hợp lệ.

Làm thế nào để khôi phục luồng đang dở dang sau khi app khởi động lại?

Lưu hai thứ: route có ý nghĩa cuối cùng (hoặc ID bước) và dữ liệu nháp. Khi khởi động lại, rebuild path bằng các kiểm tra tiền đề giống như khi xử lý deep link, rồi load dữ liệu nháp. Nếu nháp cũ, bắt đầu lại luồng nhưng prefill trường thường ít gây ngạc nhiên hơn việc đặt người dùng giữa chừng.

Khi nào tôi nên push so với present modal trong luồng nhiều bước?

Push cho con đường chính của luồng để Back lần theo bước một cách tự nhiên. Dùng sheet cho các tác vụ phụ tùy chọn và fullScreenCover cho trải nghiệm tách biệt như login hoặc chụp ảnh. Tránh đặt các bước cốt lõi trong modal vì cử chỉ dismiss có thể làm mất đồng bộ giữa UI và trạng thái luồng.

Tôi có nên chặn cử chỉ back để hiện hộp thoại “Bạn có chắc?” không?

Đừng can thiệp Back mặc định; để hệ thống xử lý. Chỉ thêm xác nhận khi quay lại sẽ làm mất dữ liệu đáng kể chưa lưu, và chỉ khi màn hình thực sự “dirty”. Ưu tiên autosave hoặc lưu nháp nếu bạn thấy mình thường xuyên cần các hộp thoại xác nhận.

Những lỗi hay gặp nhất khiến back stack hoạt động kì lạ là gì?

Nguyên nhân phổ biến là có nhiều NavigationStack lồng nhau, tái khởi tạo NavigationPath trong các lần render, hoặc có nhiều chủ thể cùng mutate path. Giữ một stack cho mỗi luồng, để path trong state sống lâu (@StateObject hoặc router), và tập trung logic push/pop vào một chỗ.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu