أنماط NavigationStack في SwiftUI لتدفقات متعددة الخطوات متوقعة
أنماط NavigationStack في SwiftUI لتدفقات متعددة الخطوات: توجيه واضح، سلوك رجوع آمن، وأمثلة عملية للترحيب ومعالجات الموافقة.

ما الذي يخطئ في التدفقات متعددة الخطوات
التدفق متعدد الخطوات هو أي تسلسل حيث يجب أن تحدث الخطوة 1 قبل أن تكون الخطوة 2 منطقية. أمثلة شائعة تشمل الترحيب (onboarding)، وطلب الموافقة (مراجعة، تأكيد، إرسال)، ودخول البيانات بنمط المعالج حيث يبني المستخدم مسودة عبر شاشات متعددة.
تبدو هذه التدفقات سهلة فقط عندما يتصرّف زر العودة كما يتوقع الناس. إذا أخذهم الزر إلى شاشة مفاجئة، سيفقد المستخدمون الثقة في التطبيق. يظهر ذلك في عمليات إرسال خاطئة، وتخلي عن الترحيب، وتذاكر دعم مثل "لا أستطيع العودة إلى الشاشة التي كنت فيها".
يبدو التنقل الفوضوي عادةً بهذا الشكل:
- التطبيق يقفز إلى الشاشة الخاطئة، أو يخرج من التدفق مبكراً.
- نفس الشاشة تظهر مرتين لأنها دُفعت مرتين.
- إحدى الخطوات تعيد ضبط نفسها عند الرجوع ويخسر المستخدم مسودته.
- المستخدم يمكنه الوصول إلى الخطوة 3 دون إكمال الخطوة 1، مما ينتج حالة غير صالحة.
- بعد رابط عميق أو إعادة تشغيل التطبيق، تعرض الشاشة الصحيحة لكن بالبيانات الخاطئة.
نموذج ذهني مفيد: التدفق متعدد الخطوات هو شيئان يتحركان معًا.
أولاً، مكدس من الشاشات (ما يمكن للمستخدم الرجوع خلاله). ثانيًا، حالة تدفق مشتركة (بيانات المسودة والتقدّم التي لا يجب أن تختفي لمجرد اختفاء الشاشة).
تفشل العديد من إعدادات NavigationStack عندما ينفصل مكدس الشاشات عن حالة التدفق. على سبيل المثال، قد يدفع تدفق الترحيب "إنشاء الملف الشخصي" مرتين (مسارات مكررة)، بينما تعيش مسودة الملف الشخصي داخل العرض وتُعاد إنشاؤها عند إعادة العرض. يضغط المستخدم Back، يرى نسخة مختلفة من النموذج، ويظن أن التطبيق غير موثوق.
يبدأ السلوك المتوقّع بتسمية التدفق، وتحديد ماذا يجب أن يفعل Back في كل خطوة، وإعطاء حالة التدفق منزلًا واضحًا واحدًا.
أساسيات NavigationStack التي تحتاجها فعلاً
للتدفقات متعددة الخطوات، استخدم NavigationStack بدلَ NavigationView القديم. NavigationView يمكن أن يتصرّف بشكل مختلف عبر نسخ iOS وهو أصعب للفهم عند إجراء push أو pop أو استعادة الشاشات. NavigationStack هو API الحديث الذي يعامل التنقل كمكدس حقيقي.
يخزن NavigationStack تاريخًا لأماكن وجود المستخدم. كل دفع (push) يضيف وجهة إلى المكدس. كل إجراء رجوع (back) يزيل وجهة واحدة. هذه القاعدة البسيطة هي ما يجعل التدفق يبدو مستقرًا: يجب أن تعكس الواجهة تسلسل خطوات واضح.
ماذا يخزن المكدس فعلاً
SwiftUI لا يخزن كائنات العرض الخاصة بك. هو يخزن البيانات التي استخدمتها للتنقل (قيمة المسار) ويستخدمها لإعادة بناء وجهة العرض عند الحاجة. لهذا نتائج عملية قليلة:
- لا تعتمد على بقاء العرض حيًا للاحتفاظ ببيانات مهمة.
- إذا كانت الشاشة تحتاج حالة، اجعلها في نموذج (مثل
ObservableObject) يعيش خارج العرض المدفوع. - إذا دفعت نفس الوجهة مرتين ببيانات مختلفة، يعامل SwiftUI كلًا منها كمدخل مختلف في المكدس.
عندما يكون التدفق أكثر من دفعتين ثابتتين، استخدم NavigationPath. فكّر فيه كقائمة قابلة للتعديل من قيم "إلى أين نتجه". يمكنك إلحاق مسارات للتقدّم، أو إزالة آخر مسار للرجوع، أو استبدال المسار بأكمله للقفز إلى خطوة لاحقة.
يناسب ذلك حالات المعالج (wizard)، وحالات إعادة تعيين التدفق بعد الاكتمال، أو عندما تريد استعادة تدفق جزئي من حالة محفوظة.
التوقعية أفضل من الابتكار الذكي. قواعد أقل مخفية (قفزات تلقائية، pops ضمنية، تأثيرات جانبية مدفوعة بالعرض) تعني أخطاء أقل لاحقًا في مكدس الرجوع.
نمذج التدفق بـ enum صغير للمسارات
يبدأ التنقل المتوقّع بقرار واحد: احتفظ بالتوجيه في مكان واحد، واجعل كل شاشة في التدفق قيمة واضحة وصغيرة.
انشئ مصدر حقائق واحد، مثل FlowRouter (وهو ObservableObject) الذي يمتلك NavigationPath. هذا يحافظ على كل عمليات الدفع والإزالة متسقة، بدلاً من تشتتها عبر العروض.
بنية موجه بسيطة
استخدم enum لتمثيل الخطوات. أضف قيم مرتبطة فقط للمعرّفات خفيفة الوزن (مثل IDs)، لا للنماذج الكاملة.
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() }
}
افصل حالة التدفق عن حالة التنقل
عامل التنقل كـ "أين يوجد المستخدم"، وحالة التدفق كـ "ما الذي أدخله حتى الآن". ضع بيانات التدفق في مخزن خاص بها (مثل OnboardingState مع الاسم، البريد الإلكتروني، المستندات المرفوعة) واحتفظ بها ثابتة بينما تأتي الشاشات وتذهب.
قاعدة بسيطة:
FlowRouter.pathيحتوي فقط على قيمStep.OnboardingStateيحتوي على مدخلات المستخدم وبيانات المسودة.- تحمل الخطوات معرفات للبحث عن البيانات، لا البيانات نفسها.
هذا يتجنّب تجزّؤ الـ hashing، ومسارات ضخمة، وإعادة ضبط مفاجئة عندما يعيد SwiftUI بناء العروض.
خطوة بخطوة: بناء معالج باستخدام NavigationPath
لشاشات بنمط المعالج، أبسط نهج هو التحكم بالمكدس بنفسك. اهدف إلى مصدر واحد للحقيقة لـ "أين أنا في التدفق؟" وطريقة واحدة للتقدّم أو الرجوع.
ابدأ بـ NavigationStack(path:) مربوط بـ NavigationPath. كل شاشة مدفوعة تمثلها قيمة (غالبًا حالة enum)، وتسجل الوجهات مرة واحدة.
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 يطابق توقع المستخدم: نقرة واحدة تساوي خطوة واحدة.
حافظ على ثبات البيانات بينما تأتي الشاشات وتذهب
يشعر التدفق متعدد الخطوات بعدم الموثوقية عندما تملك كل شاشة حالتها الخاصة. تكتب اسماً، تنتقل للأمام، تعود، والحقل فارغ لأن العرض أعيد إنشاؤه.
الحل بسيط: عامل التدفق ككائن مسودة واحد، ودع كل خطوة تعدّله.
في SwiftUI، هذا عادةً يعني ObservableObject مشترك يُنشأ مرة واحدة عند بداية التدفق ويُمرّر لكل خطوة. لا تخزن قيم المسودة في @State لكل عرض إلا إذا كانت فعلاً تخص تلك الشاشة فقط.
final class OnboardingDraft: ObservableObject {
@Published var fullName = ""
@Published var email = ""
@Published var wantsNotifications = false
var canGoNextFromProfile: Bool {
!fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& email.contains("@")
}
}
انشئها عند مدخل التدفق، ثم شاركها باستخدام @StateObject و@EnvironmentObject (أو مرّرها صراحة). الآن يمكن أن يتغير المكدس دون فقدان البيانات.
قرّر ما الذي يبقى عند الرجوع
ليس كل شيء يجب أن يبقى إلى الأبد. قرّر قواعدك مقدمًا حتى يبقى التدفق متسقًا.
احتفظ بمدخلات المستخدم (حقول النص، التبديلات، الاختيارات) ما لم يعيد المستخدم الصفر صراحةً. أعد ضبط حالة واجهة المستخدم الخاصة بالخطوة فقط (دوارات التحميل، تنبيهات مؤقتة، رسوم متحركة قصيرة). امسح الحقول الحساسة (مثل رموز الاستخدام مرة واحدة) عند مغادرة تلك الخطوة. إذا غيّر اختيار ما خطوات لاحقة، امسح فقط الحقول التابعة.
التحقق ينسجم طبيعيًا هنا. بدلًا من السماح للمستخدمين بالتقدّم ثم عرض خطأ في الشاشة التالية، أبقهم في الخطوة الحالية حتى تصبح صالحة. تعطيل الزر بناءً على خاصية محسوبة مثل canGoNextFromProfile غالبًا ما يكون كافيًا.
احفظ نقاط تفتيش دون إفراط
بعض المسودات قد تعيش في الذاكرة فقط. البعض الآخر يجب أن يصمد أمام إعادة تشغيل التطبيق أو التعطل. افتراض عملي:
- احتفظ بالبيانات في الذاكرة بينما يتحرك المستخدم خلال الخطوات بنشاط.
- خزن محليًا عند نقاط واضحة (حساب تم إنشاؤه، موافقة أرسلت، دفع بدأ).
- احفظ مبكرًا إذا كان التدفق طويلاً أو إدخال البيانات يستغرق أكثر من دقيقة.
بهذه الطريقة، يمكن للشاشات أن تأتي وتذهب بحرية، ويشعر تقدم المستخدم بالثبات والاحترام لوقته.
الروابط العميقة واستعادة تدفق نصف مكتمل
الروابط العميقة مهمة لأن التدفقات الحقيقية نادرًا ما تبدأ من الخطوة 1. شخص ما ينقر على بريد إلكتروني أو إشعار أو رابط مشترك ويتوقع الوصول إلى الشاشة الصحيحة، مثل الخطوة 3 من الترحيب أو شاشة الموافقة النهائية.
مع NavigationStack، عامل الرابط العميق كتعليمات لبناء مسار صالح، لا كأمر للقفز إلى عرض واحد. ابدأ من بداية التدفق وألحق فقط الخطوات الصحيحة لهذا المستخدم وهذه الجلسة.
حول رابط خارجي إلى تسلسل مسارات آمن
نمط جيد: حلّل المعرف الخارجي، حمّل الحد الأدنى من البيانات التي تحتاجها، ثم حوّلها إلى تسلسل من المسارات.
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
}
تلك الفحوص هي دروعك الواقية. إذا كانت المتطلبات مفقودة، لا تضع المستخدم على الخطوة 3 مع خطأ وبدون طريقة للتقدّم. أرسله إلى أول خطوة مفقودة، وتأكد أن مكدس الرجوع لا يزال يروي قصة منطقية.
استعادة تدفق نصف مكتمل
لاستعادة بعد إعادة التشغيل، احفظ شيئين: حالة آخر مسار معروف وبيانات المسودة المدخلة من المستخدم. ثم قرّر كيف تستأنف دون مفاجأة الناس.
إذا كانت المسودة طازجة (دقائق أو ساعات)، قدم خيارًا واضحًا "استئناف". إذا كانت قديمة، ابدأ من البداية لكن استخدم المسودة لملء الحقول مسبقًا. إذا تغيرت المتطلبات، أعد بناء المسار باستخدام نفس دروع المتطلبات.
الدفع مقابل النوافذ المنبثقة: اجعل الخروج من التدفق سهلاً
يشعر التدفق بالتوقعية عندما يكون هناك طريقة رئيسية واحدة للتقدّم: دفع الشاشات على مكدس واحد. استخدم sheets وfull-screen covers للمهام الجانبية، لا للمسار الرئيسي.
الدفع (NavigationStack) مناسب عندما يتوقع المستخدم أن يتعقب Back خطواته. النوافذ المنبثقة مناسبة عندما يقوم المستخدم بمهمة جانبية، اختيار سريع، أو تأكيد إجراء محفوف بالمخاطر.
مجموعة قواعد بسيطة تمنع معظم غرائب التنقل:
- ادفع للمسار الرئيسي (الخطوة 1، الخطوة 2، الخطوة 3).
- استخدم sheet للمهام الاختيارية الصغيرة (اختيار تاريخ، اختيار دولة، مسح مستند).
- استخدم fullScreenCover لعوالم منفصلة (تسجيل الدخول، التقاط الكاميرا، وثيقة قانونية طويلة).
- استخدم نافذة منبثقة للتأكيدات (إلغاء التدفق، حذف المسودة، إرسال للموافقة).
الخطأ الشائع هو وضع شاشة من المسار الرئيسي في sheet. إذا كانت الخطوة 2 نافذة منبثقة، يمكن للمستخدم إغلاقها بالسحب، يفقد السياق، وينتهي به الأمر بمكدس يقول إنه على الخطوة 1 بينما بياناته تقول إنه أكمل الخطوة 2.
التأكيدات عكس ذلك: دفع شاشة "هل أنت متأكد؟" داخل المعالج يكدّ المكدس وقد يخلق حلقات (الخطوة 3 -> تأكيد -> رجوع -> الخطوة 3 -> رجوع -> تأكيد).
كيفية إغلاق كل شيء بشكل نظيف بعد "تم"
قرّر أولاً ماذا يعني "تم": العودة إلى الشاشة الرئيسية، العودة إلى القائمة، أم عرض شاشة نجاح.
إذا كان التدفق مدفوعًا، أعد تعيين NavigationPath إلى فارغ للعودة إلى البداية. إذا كان التدفق مقدمًا كموديال، استدعِ dismiss() من البيئة. إذا كان لديك كلاهما (موديال يحتوي على NavigationStack)، فاغلق الموديال، لا كل شاشة بدورها. بعد الإرسال الناجح، امسح أيضًا أي حالة مسودة حتى يبدأ التدفق المُعاد فتحه جديدًا.
سلوك زر العودة ولحظات "هل أنت متأكد؟"
لغالبية التدفقات متعددة الخطوات، أفضل حل هو عدم عمل شيء: دع زر العودة ونقرة السحب-للخلف يعملان. هذا يطابق توقعات المستخدم وتجنّب الأخطاء حيث تقول الواجهة شيئًا لكن حالة التنقل تقول شيئًا آخر.
التدخل يستحق العناء فقط عندما يسبب الرجوع ضررًا حقيقيًا، مثل فقدان نموذج طويل غير محفوظ أو التخلي عن إجراءٍ لا يمكن التراجع عنه. إذا كان بإمكان المستخدم العودة بأمان ومواصلة العمل، فلا تضف احتكاكًا.
نهج عملي هو الحفاظ على تنقل النظام، لكن إضافة تأكيد فقط عندما تكون الشاشة "متسخة" (مصحوبة بتعديلات). هذا يعني تقديم إجراء Back خاص بك وطلب التأكيد مرة واحدة مع طريقة خروج واضحة.
@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) {}
}
}
لا تحوّل هذا إلى فخ:
- اسأل فقط عندما يمكنك شرح العاقبة في جملة قصيرة.
- قدم خيارًا آمنًا (إلغاء، متابعة التحرير) بالإضافة إلى خروج واضح (حذف، مغادرة).
- لا تخفِ أزرار الرجوع ما لم تستبدلها بزر رجوع أو إغلاق واضح.
- فضّل تأكيد الإجراء غير القابل للعكس (مثل "الموافقة") بدلًا من منع التنقل في كل مكان.
إذا وجدت نفسك تحارب إيماءة الرجوع كثيرًا، فهذه عادةً علامة أن التدفق يحتاج إلى حفظ تلقائي أو مسودة محفوظة أو خطوات أصغر.
أخطاء شائعة تخلق مكدسات رجوع غريبة
معظم أخطاء "لماذا عاد إلى هناك؟" ليست عشوائية من SwiftUI. إنها ناتجة عن أنماط تجعل حالة التنقل غير مستقرة. للتصرف بتوقعية، عامل مكدس الرجوع كبيانات تطبيق: ثابتة، قابلة للاختبار، ومملوكة من مكان واحد.
المكدسات الإضافية العرضية
فخ شائع هو وجود أكثر من NavigationStack دون أن تدرك ذلك. على سبيل المثال، كل تبويب له مكدسه الجذري، ثم عرض فرعي يضيف مكدسًا آخر داخل التدفق. النتيجة سلوك رجوع مربك، أشرطة تنقل مفقودة، أو شاشات لا تُزال كما تتوقع.
مشكلة متكررة أخرى هي إعادة تهيئة NavigationPath كثيرًا. إذا تم إنشاء المسار داخل عرض يعاد تقديمه، فقد يُعاد ضبطه عند تغيّر الحالة ويقذف المستخدم إلى الخطوة 1 بعد أن كتب في حقل.
الأخطاء وراء معظم المكدسات الغريبة واضحة:
- تداخل NavigationStack داخل مكدس آخر (غالبًا داخل تبويبات أو محتوى نافذة منبثقة)
- إعادة تهيئة
NavigationPath()أثناء تحديثات العرض بدل الاحتفاظ بها في حالة طويلة العمر - وضع قيم غير مستقرة في مسارك (مثل كائن نموذج يتغير)، مما يكسر
Hashableويتسبب في وجهات غير متطابقة - نشر قرارات التنقل عبر معالجات أزرار حتى لا أحد يمكنه شرح ما يعنيه "التالي"
- قيادة التدفق من مصادر متعددة في آنٍ واحد (مثلاً، كل من نموذج العرض view model والعرض ذاته يغيران المسار)
إذا احتجت لتمرير بيانات بين الخطوات، فضّل المعرفات المستقرة في المسار (IDs، enums خطوات) واحتفظ بالبيانات الفعلية في حالة مشتركة.
مثال ملموس: إذا كان مسارك هو .profile(User) وUser يتغير أثناء الكتابة، قد يعامل SwiftUI ذلك كمسار مختلف ويعيد توصيل المكدس. اجعل المسار .profile وخزن بيانات المسودة في حالة مشتركة.
قائمة فحص سريعة للتنقل المتوقع
عندما يبدو التدفق مضطربًا، عادة السبب أن مكدس الرجوع لا يروي نفس قصة المستخدم. قبل صقل الواجهة، قم بجولة سريعة على قواعد التنقل لديك.
اختبر على جهاز حقيقي، ليس فقط المعاينات، وحاول النقرات البطيئة والسريعة. النقرات السريعة غالبًا ما تكشف عن عمليات دفع مكررة وحالة مفقودة.
- ارجع خطوة واحدة في كل مرة من الشاشة الأخيرة إلى الأولى. أكد أن كل شاشة تعرض نفس البيانات التي أدخلها المستخدم سابقًا.
- نفّذ إلغاء من كل خطوة (بما فيها الأولى والأخيرة). أكد أنه يعود دائمًا إلى مكان منطقي، لا إلى شاشة سابقة عشوائية.
- أغلق التطبيق بالقوة منتصف التدفق وأعد التشغيل. تأكّد من أنه يمكنك الاستئناف بأمان، إما باستعادة المسار أو بإعادة البدء عند خطوة معروفة مع بيانات محفوظة.
- افتح التدفق باستخدام رابط عميق أو اختصار التطبيق. تحقّق أن خطوة الوجهة صالحة؛ إذا كانت البيانات المطلوبة مفقودة، وجّه المستخدم إلى أقدم خطوة يمكنها جمعها.
- أنهِ بـ "تم" وتأكد أن التدفق أُزيل نظيفًا. يجب ألا يتمكّن المستخدم من الضغط على Back والدخول مرة أخرى إلى معالج مكتمل.
طريقة بسيطة للاختبار: تخيّل معالج ترحيب بثلاث شاشات (الملف الشخصي، الأذونات، التأكيد). أدخل اسماً، تقدّم، عد، عدّله، ثم اقفز إلى التأكيد عبر رابط عميق. إذا عرض التأكيد الاسم القديم، أو إذا أخذك Back إلى شاشة ملف شخصي مكررة، فأن تحديثات المسار لديك غير متسقة.
إذا نجحت القائمة دون مفاجآت، سيشعر التدفق بالهدوء والتوقعية، حتى عندما يغادر المستخدمون ويعودون لاحقًا.
مثال واقعي وخطوات تالية
تخيّل تدفق موافقة مدير على طلب مصاريف. له أربع خطوات: مراجعة، تحرير، تأكيد، وإيصال. يتوقع المستخدم شيئاً واحداً: Back يذهب دائمًا إلى الخطوة السابقة، لا إلى شاشة عشوائية زارها سابقًا.
يساعد enum مسار بسيط في الحفاظ على ذلك. يجب أن يخزن NavigationPath فقط المسار وأي معرفات صغيرة لازمة لإعادة تحميل الحالة، مثل expenseID وmode (مراجعة مقابل تحرير). تجنّب دفع نماذج كبيرة قابلة للتغيير إلى المسار لأنها تجعل الاستعادة والروابط العميقة هشة.
احتفظ بالمسودة العاملة في مصدر واحد للحقيقة خارج العروض، مثل @StateObject لنموذج التدفق (أو متجر). كل خطوة تقرأ وتكتب ذلك النموذج، بحيث يمكن للشاشات الظهور والاختفاء دون فقدان المدخلات.
على الأقل، تتعقب ثلاثة أشياء:
- المسارات (مثلاً:
review(expenseID),edit(expenseID),confirm(expenseID),receipt(expenseID)) - البيانات (كائن مسودة مع بنود السطر والملاحظات، وحالة مثل
pending,approved,rejected) - الموقع (المسودة في نموذج التدفق، السجل الرسمي على الخادم، وتوكن استعادة صغير محليًا: expenseID + آخر خطوة)
الحالات الطرفية هي المكان الذي يكسب فيه التدفق الثقة أو يخسرها. إذا رفض المدير في شاشة التأكيد، قرّر هل يعود Back إلى التحرير (للتعديل) أم يخرج من التدفق. إذا عاد لاحقًا، استعد آخر خطوة من التوكن المحفوظ وأعد تحميل المسودة. إذا انتقل بين أجهزة، عامل الخادم كحقيقة: أعِد بناء المسار من حالة الخادم وأرسِل المستخدم إلى الخطوة المناسبة.
خطوات تالية: وثّق enum المسار (ماذا يعني كل حالة ومتى تُستخدم)، أضف بعض الاختبارات الأساسية لبناء المسارات وسلوك الاستعادة، والتزم بقاعدة واحدة: العروض لا تملك قرارات التنقل.
إذا كنت تبني نفس النوع من التدفقات متعددة الخطوات دون كتابة كل شيء من الصفر، فمنصات مثل AppMaster تطبق نفس الفصل: حافظ على فصل التنقل عن بيانات الأعمال حتى تتمكن الشاشات من التغيّر دون كسر تقدم المستخدم.
الأسئلة الشائعة
استخدم NavigationStack مع NavigationPath واحد تتحكم به. ادفع مساراً واحداً فقط في كل إجراء “التالي” وازل مساراً واحداً لكل إجراء رجوع. عندما تحتاج إلى قفزة (مثل "تحرير الملف الشخصي" من شاشة المراجعة)، قم بقص المسار إلى خطوة معروفة بدلاً من إضافة شاشات جديدة.
لأن SwiftUI يعيد بناء وجهات العرض استنادًا إلى قيمة المسار، وليس كائن العرض نفسه. إذا كانت بيانات النموذج مخزنة في @State الخاص بالعرض فقد تعود إلى قيمتها الافتراضية عندما يُعاد إنشاء العرض. ضع بيانات المسودة في نموذج مشترك (مثل ObservableObject) يعيش خارج الشاشات المدفوعة.
عادةً يحدث ذلك عندما تضيف نفس المسار أكثر من مرة (غالبًا بسبب نقرات سريعة أو مسارات كود متعددة تتسبب في التنقل). عطل زر التالي أثناء التنقل أو أثناء تشغيل عمليات التحقق/التحميل، واجعل تغييرات التنقل مركزة في مكان واحد حتى لا يحدث أكثر من append لكل خطوة.
احتفظ بقيم توجيه صغيرة ومستقرة مثل حالة enum ومعرفات خفيفة الوزن. احتفظ بالبيانات المتغيرة (المسودة) في كائن مشترك وابحث عنها بالمعرف عند الحاجة. دفع نماذج كبيرة متغيرة إلى المسار يمكن أن يكسر متطلبات Hashable ويسبب وجهات متباينة.
التنقل هو "أين يوجد المستخدم"، وحالة التدفق هي "ما الذي أدخله المستخدم". امتلك مسار التنقل في موجه (router) أو في حالة علوية واحدة، وامتلك المسودة في ObservableObject منفصل. كل شاشة تعدّل المسودة؛ الموجّه يغيّر الخطوات فقط.
عامل الرابط العميق كتعليمات لبناء تسلسل خطوات صالح، لا كقلب فوري إلى شاشة مفردة. ابنِ المسار عن طريق إلحاق خطوات المتطلبات الأساسية أولاً (بناءً على ما أتمّه المستخدم)، ثم أضف خطوة الهدف. هذا يحافظ على تماسك مكدس العودة ويتجنّب حالات غير صالحة.
احفظ شيئين على الأقل: المعرف ذو معنى لآخر خطوة (أو خطوة) وبيانات المسودة. عند إعادة التشغيل، أعد بناء المسار باستخدام نفس فحوص المتطلبات الأساسية التي تستخدمها للروابط العميقة، ثم حمّل المسودة. إذا كانت المسودة قديمة، فبدء التدفق من البداية مع تعبئة الحقول مسبقًا أقل إثارة للمفاجأة من إسقاط المستخدم منتصف المعالج.
ادفع للشاشات التي تشكل المسار الرئيسي حتى يعيد زر العودة تتبع الخطوات بشكل طبيعي. استخدم sheets للمهام الجانبية الاختيارية وfullScreenCover للتجارب المنفصلة مثل تسجيل الدخول أو التقاط الكاميرا. تجنّب وضع الخطوات الأساسية في نوافذ منبثقة لأن إيماءات الإغلاق قد تخلّ بتزامن واجهة المستخدم وحالة التدفق.
لا تحتجز Back بشكل افتراضي؛ اترك سلوك النظام يعمل. أضف تأكيدًا فقط عندما يؤدي الخروج إلى فقدان عمل غير محفوظ ذي قيمة، وفقط عندما تكون الشاشة فعلاً "متسخة". افضّل الحفظ التلقائي أو حفظ المسودة إذا وجدت نفسك تحتاج هذه التأكيدات كثيرًا.
أسباب شائعة هي تداخل NavigationStacks متعددة، إعادة إنشاء NavigationPath خلال تحديثات العرض، ووجود أصحاب متعددين يغيرون المسار في آنٍ واحد. احتفظ بمكدس واحد لكل تدفق، وضع NavigationPath في حالة طويلة العمر (@StateObject أو موجّه واحد)، ومركّز جميع منطق push/pop في مكان واحد.


