০৯ মে, ২০২৫·6 মিনিট পড়তে

প্রেডিক্টেবল মাল্টি-স্টেপ ফ্লো-র জন্য SwiftUI NavigationStack প্যাটার্ন

SwiftUI NavigationStack প্যাটার্নগুলো মাল্টি-স্টেপ ফ্লো-র জন্য: স্পষ্ট রাউটিং, সেফ ব্যাক আচরণ, এবং অনবোর্ডিং ও অনুমোদন উইজার্ডের ব্যবহারিক উদাহরণ।

প্রেডিক্টেবল মাল্টি-স্টেপ ফ্লো-র জন্য SwiftUI NavigationStack প্যাটার্ন

মাল্টি-স্টেপ ফ্লোতে কী ভুল হয়\n\nমাল্টি-স্টেপ ফ্লো হলো এমন একটি সিকোয়েন্স যেখানে ধাপ 1 না হলে ধাপ 2 এর মানে হয় না। প্রচলিত উদাহরণ: অনবোর্ডিং, একটি অনুমোদন রিকওয়েস্ট (রিভিউ, কনফার্ম, সাবমিট), এবং উইজার্ড-স্টাইল ডেটা এন্ট্রি যেখানে কেউ একাধিক স্ক্রিন জুড়ে একটি ড্রাফ্ট তৈরি করে।\n\nএই ফ্লোগুলো তখনই সহজ লাগে যখন Back ব্যবহারকারীর প্রত্যাশার মতো আচরণ করে। যদি Back তাদের অপ্রত্যাশিত কোনো স্থানে নিয়ে যায়, ব্যবহারকারীরা অ্যাপে বিশ্বাস হারায়। এর প্রভাব দেখা যায় ভুল সাবমিশন, অনবোর্ডিং পরিত্যাগ, এবং সাপোর্ট টিকিট—“আমি যে স্ক্রিনে ছিলাম সেখানে ফিরতে পারছি না” ধরনের।\n\nঅগোছালো নেভিগেশন সাধারণত এগুলোর মধ্যে একটার মতো দেখা যায়:\n\n- অ্যাপটি ভুল স্ক্রিনে চলে যায়, অথবা ফ্লো খুব আগে বেরিয়ে যায়।\n- একই স্ক্রিন দুইবার দেখা যায় কারণ এটি দুইবার পুশ করা হয়েছে।\n- Back করলে কোনো ধাপ রিসেট হয়ে যায় এবং ব্যবহারকারী তার ড্রাফ্ট হারায়।\n- ব্যবহারকারী ধাপ 1 না করেই ধাপ 3-এ পৌঁছে যায়, ফলে অবৈধ স্টেট তৈরি হয়।\n- ডীপ লিংক বা অ্যাপ রিস্টার্টের পরে সঠিক স্ক্রিন দেখা গেলেও সঠিক ডেটা নেই।\n\nএকটি কাজে লাগার মতো মানসিক মডেল: একটি মাল্টি-স্টেপ ফ্লো আসলে দুইটি জিনিস একসাথে চলছে।\n\nপ্রথমটি, একটি স্ক্রিনের স্ট্যাক (ব্যবহারকারী কোন স্ক্রিনগুলোতে ফিরে যেতে পারে)। দ্বিতীয়টি, শেয়ার্ড ফ্লো স্টেট (ড্রাফ্ট ডেটা ও অগ্রগতি) যা কেবলমাত্র একটি স্ক্রিন অদৃশ্য হওয়ায় হারিয়ে যাওয়া উচিত নয়।\n\nঅনেক NavigationStack সেটআপ ভেঙে পড়ে যখন স্ক্রিন স্ট্যাক এবং ফ্লো স্টেট আলাদা দিকগুলোতে চলতে শুরু করে। উদাহরণস্বরূপ, অনবোর্ডিং ফ্লোতে "Create profile" দুইবার পুশ হয়ে যেতে পারে (ডুপ্লিকেট রুট), যখন ড্রাফ্ট প্রোফাইলটি ভিউ-তের মধ্যে থাকে এবং রি-রেন্ডারে পুনরায় তৈরি হয়। ব্যবহারকারী Back ট্যাপ করে দেখেন ভিন্ন সংস্করণের ফর্ম—এবং ধরে নেন অ্যাপ অপ্রত্যাশ্য।\n\nপ্রেডিক্টেবল আচরণ শুরু হয় ফ্লোকে নাম দেওয়া, প্রতিটি ধাপে Back কী করবে সেটি সংজ্ঞায়িত করা, এবং ফ্লো স্টেটকে একটি স্পষ্ট বাড়ি দেওয়া থেকে।\n\n## NavigationStack-র মূল যেগুলো আপনি আসলে প্রয়োজন\n\nমাল্টি-স্টেপ ফ্লোদের জন্য, পুরোনো NavigationView-এর বদলে NavigationStack ব্যবহার করুন। NavigationView বিভিন্ন iOS ভার্সনে আলাদা আচরণ করতে পারে এবং পুশ, পপ বা রিস্টোর করার সময় বোঝা কঠিন। NavigationStack হলো আধুনিক API যা নেভিগেশনকে একটি প্রকৃত স্ট্যাক হিসেবে বিবেচনা করে।\n\nNavigationStack ব্যবহারকারী যে পথটি আসছে তার হিস্টরি সংরক্ষণ করে। প্রতিটি পুশ একটি গন্তব্য স্ট্যাক এ যোগ করে। প্রতিটি ব্যাক অ্যাকশন একটি গন্তব্য সরায়। এই সহজ নিয়মটি ফ্লোকে স্থির করে: UI-কে একটি পরিষ্কার ধাপের সিকোয়েন্স প্রতিফলিত করা উচিত।\n\n### স্ট্যাক আসলে কী রাখে\n\nSwiftUI আপনার ভিউ অবজেক্টগুলো সংরক্ষণ করে না। এটি আপনি নেভিগেশনে ব্যবহৃত ডেটা (রুট ভ্যালু) সংরক্ষণ করে এবং প্রয়োজনে তা ব্যবহার করে গন্তব্য ভিউ পুনর্নির্মাণ করে। এর কিছু ব্যবহারিক ফলাফল আছে:\n\n- গুরুত্বপূর্ণ ডেটা রাখার জন্য ভিউ লাইভ থাকার উপর নির্ভর করবেন না।\n- যদি কোনো স্ক্রিনকে স্টেট লাগে, সেটি এমন একটি মডেলে রাখুন (যেমন ObservableObject) যা পুশ করা ভিউয়ের বাইরে থাকে।\n- একই গন্তব্য ভিন্ন ডেটা দিয়ে দুইবার পুশ করলে, SwiftUI সেগুলোকে ভিন্ন স্ট্যাক এন্ট্রি হিসেবে বিবেচনা করে।\n\nযখন আপনার ফ্লো কেবল এক বা দুইটি ফিক্সড পুশ নয়, তখন NavigationPath-ই আপনার হাতের কাছে থাকে। এটিকে একটি সম্পাদনযোগ্য “আমরা কোথায় যাচ্ছি” ভ্যালুগুলোর তালিকা হিসেবে ভাবুন। আপনি রুট অ্যাপেন্ড করে এগোতে পারেন, শেষ রুট সরিয়ে ব্যাক যেতে পারেন, বা পুরো পাথ রিপ্লেস করে পরে একটি ধাপে ঝাঁপ দিতে পারেন।\n\nএটি সেই পরিস্থিতিতে ভাল কাজ করে যখন আপনি উইজার্ড-স্টাইল ধাপ চান, ফ্লো সম্পন্ন হলে রিসেট করতে চান, বা সংরক্ষিত স্টেট থেকে আংশিক ফ্লো পুনরুদ্ধার করতে চান।\n\nপ্রেডিক্টেবলতা চতুরতার থেকে বেশি মূল্যবান। কম লুকানো নিয়ম (স্বয়ংক্রিয় জাম্প, অপ্রকাশিত পপস, ভিউ-চালিত সাইড ইফেক্ট) থাকলে পরে ব্যাক স্ট্যাক বাগের সম্ভাবনা কমে।\n\n## ছোট রুট enum-এ ফ্লো মডেল করুন\n\nপ্রেডিক্টেবল নেভিগেশন একটি সিদ্ধান্ত দিয়ে শুরু হয়: রাউটিং এক জায়গায় রাখুন, এবং ফ্লোর প্রতিটি স্ক্রিনকে একটি ছোট, পরিষ্কার ভ্যালু হিসেবে দেখুন।\n\nএকটি একক সোর্স অফ ট্রুথ তৈরি করুন, যেমন একটি FlowRouter (একটি ObservableObject) যা NavigationPath-এর মালিক। এতে প্রতিটি পুশ ও পপ কন্সিস্টেন্ট থাকে, ভিউগুলোতে নেভিগেশন ছড়িয়ে না থাকা ভালো।\n\n### একটি সহজ রাউটার স্ট্রাকচার\n\nস্ক্রিনগুলোকে প্রতিনিধিত্ব করতে enum ব্যবহার করুন। অ্যাসোসিয়েটেড ভ্যালুগুলো শুধুমাত্র হালকা আইডির জন্য রাখুন (যেমন IDs), পুরো মডেল নয়।\n\n```swift

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() }

}

\n### নেভিগেশন স্টেট থেকে ফ্লো স্টেট আলাদা রাখুন\n\nনেভিগেশনকে বিবেচনা করুন “ব্যবহারকারী কোথায় আছে”, আর ফ্লো স্টেটকে বিবেচনা করুন “তারা এখন পর্যন্ত কী এন্ট্রি করেছে”। ফ্লো ডেটাকে তার নিজস্ব স্টোরে রাখুন (উদাহরণস্বরূপ, `OnboardingState` যেখানে name, email, আপলোডকৃত ডকুমেন্টস থাকে) এবং স্ক্রিনগুলো এলে গেলে তা স্থির রাখুন।\n\nএকটি সহজ নিয়ম:\n\n- `FlowRouter.path` শুধুমাত্র `Step` মান রাখে।\n- `OnboardingState` ব্যবহারকারীর ইনপুট ও ড্রাফ্ট ডেটা রাখে।\n- Steps আইডি বহন করে ডেটা খুঁজে বের করার জন্য, ডেটা নিজেই নয়।\n\nএতে ভঙ্গুর হ্যাশিং, বিশাল পাথ, এবং SwiftUI ভিউ পুনর্নির্মাণের সময় হঠাৎ রিসেট হওয়া এড়ানো যায়।\n\n## ধাপে ধাপে: NavigationPath দিয়ে একটি উইজার্ড তৈরি করুন\n\nউইজার্ড-স্টাইল স্ক্রিনগুলোর জন্য সবচেয়ে সহজ পদ্ধতি হল স্ট্যাক নিজে নিয়ন্ত্রণ করা। “আমি ফ্লো-এ কোথায়” এর এক সোর্স অফ ট্রুথ রাখুন এবং এগোনোর বা পশ্চাতে ফিরে যাওয়ার এক উপায় রাখুন।\n\n`NavigationStack(path:)` দিয়ে শুরু করুন যা একটি `NavigationPath`-এ বাঁধা। প্রতিটি পুশ করা স্ক্রিন একটি ভ্যালু (অften enum কেস) দ্বারা প্রতিনিধিত্ব করে, এবং আপনি গন্তব্যগুলো একবার নিবন্ধন করবেন।\n\n```swift
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
    }
}

\nBack কে প্রত্যাশামতো রাখার জন্য কয়েকটি অভ্যাস মেনে চলুন। এগোনোর জন্য ঠিক একটিই রুট অ্যাপেন্ড করুন, “Next” লিনিয়ার রাখুন (কেবল পরবর্তী ধাপ পুশ করুন), এবং যখন আপনাকে ফিরে যেতে হবে (যেমন Review থেকে “Edit profile”), স্ট্যাকটি একটি পরিচিত ইনডেক্সে ট্রিম করুন।\n\nএটি দুর্ঘটনাক্রমে ডুপ্লিকেট স্ক্রিন এড়ায় এবং Back সেই অনুযায়ী কাজ করবে: এক ট্যাপ মানে এক ধাপ।\n\n## স্ক্রিনগুলো এলে গেলে ডেটা স্থিতিশীল রাখুন\n\nমাল্টি-স্টেপ ফ্লো তখনই অবিশ্বস্ত মনে হয় যখন প্রতিটি স্ক্রিন তার নিজস্ব স্টেট রাখে। আপনি একটি নাম টাইপ করেন, এগিয়ে যান, ফিরে আসেন, এবং ফিল্ড খালি থাকে কারণ ভিউ পুনর্নির্মিত হয়েছে।\n\nফিক্সটি সরল: ফ্লোকে একটি ড্রাফ্ট অবজেক্ট হিসেবে বিবেচনা করুন, এবং প্রতিটি ধাপ তা এডিট করুক।\n\nSwiftUI-তে সাধারণত এর মানে: ফ্লো শুরুতে একটি শেয়ার্ড ObservableObject তৈরি করুন এবং প্রতিটি ধাপে সিঁড়ি করে তা পাঠান। প্রতিটি ভিউ-এর @State-এ ড্রাফ্ট মান রাখবেন না যদি না সেটি সত্যিই কেবল সেই স্ক্রিনের জন্য প্রাসঙ্গিক।\n\n```swift final class OnboardingDraft: ObservableObject { @Published var fullName = "" @Published var email = "" @Published var wantsNotifications = false

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

}

\nএটিকে এন্ট্রি পয়েন্টে তৈরি করুন, তারপর `@StateObject` এবং `@EnvironmentObject` দিয়ে শেয়ার করুন (অথবা এক্সপ্লিসিটলি পাস করুন)। এখন স্ট্যাক পরিবর্তন হলে ও ডেটা হারিয়ে যাবে না।\n\n### ব্যাক নেভিগেশনের সময় কী বাঁচবে তা নির্ধারণ করুন\n\nসবকিছু চিরকালের জন্য রাখতে হবে না। আগেই নিয়মগুলো ঠিক করে নিন যাতে ফ্লো কনসিস্টেন্ট থাকে।\n\nব্যবহারকারীর ইনপুট (টেক্সট ফিল্ড, টগলস, সিলেকশন) রাখুন যতক্ষণ না তারা স্পষ্টভাবে রিসেট করে। স্টেপ-নির্দিষ্ট UI স্টেট (লোডিং স্পিনার, অস্থায়ী অ্যালার্ট, সংক্ষিপ্ত অ্যানিমেশন) রিসেট করুন। সংবেদনশীল ফিল্ড (এক-সময়ের কোড) সেই ধাপ থেকে বেরিয়ে গেলে ক্লিয়ার করুন। যদি কোনো পছন্দ পরে ধাপগুলোকে প্রভাবিত করে, শুধুমাত্র সেই নির্ভরশীল ফিল্ডগুলো ক্লিয়ার করুন।\n\nভ্যালিডেশন এখানে স্বাভাবিকভাবে ফিট করে। ব্যবহারকারীদের এগোতে দিলে পরের স্ক্রিনে ত্রুটি দেখানোর চেয়ে, তাদের বর্তমান ধাপে রাখা ভাল যতক্ষণ না তা বৈধ হয়। `canGoNextFromProfile`-এর মতো একটি গণনা-ভিত্তিক প্রপার্টি ব্যবহার করে বাটন নিষ্ক্রিয় করাই প্রায়ই যথেষ্ট।\n\n### চেকপয়েন্ট সংরক্ষণ সামঞ্জস্য করে করুন\n\nকিছু ড্রাফ্ট মেমরিতে থাকা ঠিক। অন্যগুলো অ্যাপ রিস্টার্ট বা ক্র্যাশ থেকে বাঁচানো উচিত। একটি ব্যবহারিক ডিফল্ট:\n\n- ব্যবহারকারী সক্রিয়ভাবে ধাপগুলো পার হওয়ার সময় ডেটা মেমরিতে রাখুন।\n- স্পষ্ট মাইলস্টোনে লোকালি পার্সিস্ট করুন (অ্যাকাউন্ট তৈরি, অনুমোদন সাবমিট, পেমেন্ট শুরু)।\n- ফ্লো দীর্ঘ হলে বা ডেটা এন্ট্রি এক মিনিটের বেশি নিলে আগেই পার্সিস্ট করুন।\n\nএভাবে স্ক্রিনগুলো স্বাধীনভাবে আসা-যাওয়া করতে পারে, এবং ব্যবহারকারীর অগ্রগতি স্থির ও সম্মানজনক লাগে।\n\n## ডীপ লিংক এবং আংশিকভাবে শেষ হওয়া ফ্লো পুনরুদ্ধার\n\nডীপ লিংক গুরুত্বপূর্ণ কারণ বাস্তব ফ্লো সাধারণত ধাপ 1-এ শুরু করে না। কেউ একটি ইমেইল, পুশ নোটিফিকেশন, বা শেয়ার্ড লিংক ট্যাপ করে ধাপ 3-এ আসা প্রত্যাশা করতে পারে।\n\nNavigationStack-এ ডীপ লিংককে একটি বৈধ পাথ তৈরি করার নির্দেশ হিসেবে দেখুন, কেবল একটি ভিউতে ঝাঁপানোর কমান্ড হিসেবে নয়। ফ্লো শুরু থেকে শুরু করে শুধুমাত্র সেই ধাপগুলো অ্যাপেন্ড করুন যেগুলো ব্যবহারকারীর ও সেশনের জন্য বৈধ।\n\n### এক্সটার্নাল লিংককে নিরাপদ রুট সিকোয়েন্সে পরিণত করা\n\nভালো একটি প্যাটার্ন: এক্সটার্নাল ID পার্স করুন, প্রয়োজনীয় ন্যূনতম ডেটা লোড করুন, তারপর এটিকে রুটের একটি সিকোয়েন্সে রূপান্তর করুন।\n\n```swift
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
}

\nএই চেকগুলোই আপনার গার্ডরেল। যদি প্রাক-শর্ত নেই, ব্যবহারকারীকে ধাপ 3-এ কোনো ত্রুটি দেখিয়ে ফেলে দেওয়ার চেয়ে, প্রথম অসম্পন্ন ধাপে পাঠান এবং নিশ্চিত করুন ব্যাক স্ট্যাকও coherent।\n\n### আংশিকভাবে শেষ হওয়া ফ্লো পুনরুদ্ধার করা\n\nরিলঞ্চের পরে পুনরুদ্ধারের জন্য দুটি জিনিস সংরক্ষণ করুন: শেষ জানা রুট স্টেট এবং ব্যবহারকারী-এন্ট্রি ড্রাফ্ট ডেটা। তারপর পুনরায় চালু করার সময় কীভাবে রিসিউম করবেন তা নির্ধারণ করুন যাতে ব্যবহারকারী অবাক না হন।\n\nযদি ড্রাফ্ট তাজা হয় (মিনিট বা ঘন্টার মধ্যে), “Resume” বিকল্প দিন। যদি পুরোনো হয়, শুরু থেকে শুরু করুন কিন্তু ফিল্ডগুলো পূর্বরূপে ভর্তি রাখুন। যদি প্রয়োজনীয়তা বদলে যায়, একই গার্ডরেল ব্যবহার করে পাথ পুনর্নির্মাণ করুন।\n\n## Push বনাম modal: ফ্লো সহজে প্রস্থানযোগ্য রাখুন\n\nফ্লো তখনই প্রত্যাশিত লাগে যখন অগ্রগতির একটি মূল পথ থাকে: স্ক্রিনগুলো এক স্ট্যাক-এ পুশ করা। সাইড টাস্কের জন্য sheet এবং full-screen cover ব্যবহার করুন, মূল পথে না।\n\nPush (NavigationStack) সেই সময় ব্যবহার করুন যখন ব্যবহারকারী প্রত্যাশা করে Back তাদের ধাপগুলো অনুসরণ করবে। Modals (sheet বা fullScreenCover) ব্যবহার করুন সাইড টাস্ক, দ্রুত পছন্দ, বা ঝুঁকিপূর্ণ কনফার্মেশনের জন্য।\n\nকিছু নিয়ম যেগুলো বেশিরভাগ নেভিগেশন অদ্ভুততা প্রতিরোধ করে:\n\n- মূল পথের জন্য Push ব্যবহার করুন (Step 1, Step 2, Step 3)।\n- ছোট অপশনাল টাস্কের জন্য sheet ব্যবহার করুন (তারিখ পছন্দ, দেশ নির্বাচন, ডকুমেন্ট স্ক্যান)।\n- আলাদা অভিজ্ঞতার জন্য fullScreenCover ব্যবহার করুন (লগইন, ক্যামেরা ক্যাপচার, বড় আইনি ডকুমেন্ট)।\n- কনফার্মেশনের জন্য modal ব্যবহার করুন (ফ্লো বাতিল, ড্রাফ্ট মুছুন, অনুমোদনের জন্য সাবমিট)।\n\nসাধারণ ভুল: মূল ফ্লো-স্ক্রিনগুলোকে sheet-এ রাখা। যদি Step 2 একটি sheet হয়, ব্যবহারকারী সেটি সুইপ করে ডিসমিস করতে পারে, প্রসঙ্গ হারায়, এবং এমন একটি স্ট্যাক পেতে পারে যা বলে তারা Step 1-এ আছে যখন ডেটা বলে তারা Step 2 শেষ করেছে।\n\nকনফার্মেশনগুলি উল্টো দিকে: “Are you sure?” ধরণের স্ক্রিনকে উইজার্ডে পুশ করলে স্ট্যাক ভরাট হয় এবং লুপ তৈরি করতে পারে (Step 3 -> Confirm -> Back -> Step 3 -> Back -> Confirm)।\n\n### “Done”-এর পর সবকিছু পরিষ্কারভাবে বন্ধ করার উপায়\n\nপ্রথমে নির্ধারণ করুন “Done” মানে কী: হোমে ফিরবেন, একটি লিস্টে ফিরবেন, না কি একটি সফলতা স্ক্রিন দেখাবেন।\n\nযদি ফ্লো পুশ করা থাকে, NavigationPath-কে খালি করে দিন যাতে সব পুশ কপাল হয়ে প্রথমে ফিরে যায়। যদি ফ্লো মডালি উপস্থাপিত হয়, পরিবেশ থেকে dismiss() কল করুন। যদি আপনার কাছে উভয়ই থাকে (একটি মডাল যার ভিতরে NavigationStack), মডালটি ডিসমিস করুন, প্রতিটি পুশ করা স্ক্রিন নয়। সফল সাবমিশনের পরে ড্রাফ্ট স্টেটও ক্লিয়ার করুন যাতে পুনরায় খোলার সময় ফ্লো নতুন থাকে।\n\n## Back বোতামের আচরণ এবং “Are you sure?” মুহূর্তগুলো\n\nঅধিকাংশ মাল্টি-স্টেপ ফ্লোর জন্য, সেরা পদ্ধতি হল কিছু করা না: সিস্টেম ব্যাক বাটন (এবং সুইপ-ব্যাক জেস্চার) যাতে কাজ করে দিন। এটি ব্যবহারকারীর প্রত্যাশার সাথে মেলে এবং UI-স্টেট-সংশ্লিষ্ট বাগগুলো এড়ায় যেখানে UI এক কথা বলে কিন্তু নেভিগেশন স্টেট আরেকটা।\n\nইন্টারসেপ্ট করা কেবল তখনই মূল্যবান যখন পিছনে গেলে বাস্তবে ক্ষতি হবে—যেমন দীর্ঘ অসেভড ফর্ম হারানো বা অপরিবর্তনীয় অ্যাকশন ত্যাগ করা। যদি ব্যবহারকারী নিরাপদে ফিরে এসে চালিয়ে যেতে পারে, তবে বাধা তৈরি করবেন না।\n\nপ্র্যাকটিক্যাল পন্থা: সিস্টেম নেভিগেশন রাখুন, কিন্তু কনফার্মেশন যোগ করুন কেবল তখনই যখন স্ক্রীন সত্যিই "dirty" (সম্পাদিত)। এর মানে হচ্ছে নিজের ব্যাক অ্যাকশন প্রদান করে একবার জিজ্ঞাসা করা, একটি স্পষ্ট উপায় প্রদান করে।\n\n```swift @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) {} } }

\nএটিকে একটি ফাঁদে পরিণত হতে দেবেন না:\n\n- কেবল তখনই জিজ্ঞাসা করুন যখন আপনি এক বাক্যে ফলে ব্যাখ্যা করতে পারেন।\n- একটি নিরাপদ অপশন (Cancel, Keep Editing) এবং একটি পরিষ্কার এক্সিট (Discard, Leave) দিন।\n- ব্যাক বাটন লুকাবেন না যদি না আপনি স্পষ্টভাবে একটি Back বা Close দিয়ে প্রতিস্থাপন করেন।\n- সর্বত্র নেভিগেশন ব্লক করার চেয়ে অবিচলযোগ্য অ্যাকশন কনফার্ম করা ভালো।\n\nআপনি যদি প্রায়ই ব্যাক জেস্চার-র বিরুদ্ধে লড়াই করেন, সাধারণত এটা সংকেত দেয় যে ফ্লোতে autosave, সেভড ড্রাফ্ট, বা ছোট ধাপ থাকা দরকার।\n\n## অদ্ভুত ব্যাক স্ট্যাক তৈরি করা সাধারণ ভুলগুলো\n\nঅধিকাংশ “কেন এটা সেখানে ফিরে গেল?” বাগগুলো SwiftUI র‍্যান্ডম নয়। এগুলো সাধারণত এমন প্যাটার্ন থেকে আসে যা নেভিগেশন স্টেটকে অস্থিতিশীল করে। প্রত্যাশাযোগ্য আচরণের জন্য, ব্যাক স্ট্যাককে অ্যাপ ডেটার মতো বিবেচনা করুন: স্থির, টেস্টযোগ্য, এবং এক জায়গায় মালিকানাধীন।\n\n### ঘটিত অতিরিক্ত স্ট্যাকগুলো\n\nএকটি সাধারণ ফাঁদ হলো অজান্তেই একের বেশি NavigationStack হওয়া। উদাহরণস্বরূপ, প্রতিটি ট্যাবের নিজস্ব রুট স্ট্যাক আছে, এবং তারপর একটি চাইল্ড ভিউ ফ্লো-এ আরেকটি স্ট্যাক যোগ করে। ফলে কনফিউজিং ব্যাক আচরণ, মিসিং নেভিগেশন বার, বা এমন স্ক্রিনের ফলাফল আসে যেগুলো প্রত্যাশানুযায়ী পপ করে না।\n\nআরেকটি ঘন সমস্যা হলো `NavigationPath` বারবার পুনরায় তৈরি করা। যদি পাথ এমন একটি ভিউ-তে তৈরি হয় যা রি-রেন্ডার করে, এটি স্টেট চেঞ্জে রিসেট হয়ে ব্যবহারকারীকে টাইপ করার পরে ধাপ 1-এ ফিরিয়ে দিতে পারে।\n\nবেশিরভাগ অদ্ভুত স্ট্যাকের পেছনের ভুলগুলো সরল:\n\n- NavigationStack-কে অন্য একটি স্ট্যাকের ভিতরে নেস্ট করা (অধিকাংশ ক্ষেত্রে ট্যাব বা শীট কনটেন্ট-এ)\n- ভিউ আপডেটের সময় `NavigationPath()` পুনরায় ইনিশিয়ালাইজ করা, বরং সেটি দীর্ঘজীবী স্টেটে রাখা উচিত\n- রুটে অস্থিতিশীল মান রাখা (যেমন একটি মডেল অবজেক্ট যা পরিবর্তিত হয়), যা `Hashable` ভাঙায় এবং মিসম্যাচড গন্তব্য সৃষ্টি করে\n- নেভিগেশন সিদ্ধান্তগুলো বাটন হ্যান্ডলার জুড়ে ছড়িয়ে দেওয়া যতক্ষণ পর্যন্ত কেউ “next” মানে কি বোঝেন না\n- একাধিক সূত্র থেকেই ফ্লো চালানো (উদাহরণ: একটি ভিউ মডেল এবং একটি ভিউ একসাথে path মিউটেট করছে)\n\nযদি ধাপে ডেটা পাঠাতে হয়, রুটে স্থিতিশীল আইডি ব্যবহার করুন (IDs, step enums) এবং প্রকৃত ফর্ম ডেটা শেয়ার্ড স্টেটে রাখুন।\n\nএকটি কনক্রিট উদাহরণ: যদি আপনার রুট `.profile(User)` হয় এবং `User` টাইপ ব্যবহারকারী টাইপ করার সঙ্গে সঙ্গে পরিবর্তিত হয়, SwiftUI এটিকে একটি ভিন্ন রুট হিসেবে বিবেচনা করে স্ট্যাক পুনর্গঠন করতে পারে। রুটকে `.profile` রাখুন এবং ড্রাফ্ট প্রোফাইল ডেটা শেয়ার্ড স্টেটে রাখুন।\n\n## প্রত্যাশাযোগ্য নেভিগেশনের জন্য দ্রুত চেকলিস্ট\n\nযখন একটি ফ্লো বিকৃত মনে হয়, সাধারণত কারণ ব্যাক স্ট্যাক ব্যবহারকারীর কাহিনী বলছে না। UI পালিশ করার আগে আপনার নেভিগেশন নিয়মগুলোর উপর দ্রুত এক পাস করুন।\n\nরিয়েল ডিভাইসে টেস্ট করুন, কেবল প্রিভিউতে নয়, এবং ধীর ও দ্রুত উভয় ট্যাপ ট্রাই করুন। দ্রুত ট্যাপ প্রায়ই ডুপ্লিকেট পুশ ও মিসিং স্টেট প্রকাশ করে।\n\n- শেষ স্ক্রিন থেকে প্রথম স্ক্রিন পর্যন্ত এক ধাপ করে ব্যাক যান। নিশ্চিত করুন প্রতিটি স্ক্রিনে ব্যবহারকারী আগে যে ডেটা দিয়েছিল তা দেখা যায়।\n- প্রতিটি ধাপ থেকে Cancel ট্রিগার করুন (প্রথম ও শেষসহ)। নিশ্চিত করুন এটি সর্বদা একটি যৌক্তিক জায়গায় ফিরে যায়, কোনো র‍্যান্ডম পূর্বের স্ক্রিন নয়।\n- ফ্লোর মাঝপথে ফোর্স কুইট করে পুনরায় চালু করুন। নিরাপদে পুনরায় শুরু করা যায় কিনা পরীক্ষা করুন—পাথ পুনর্নির্মাণ করে বা সংরক্ষিত ডেটা দিয়ে একটি পরিচিত ধাপ থেকে শুরু করে।\n- ডীপ লিংক বা অ্যাপ শর্টকাট দিয়ে ফ্লো খুলুন। নিশ্চিত করুন গন্তব্য ধাপ বৈধ; যদি প্রয়োজনীয় ডেটা নেই, প্রথম অসম্পন্ন ধাপে রিডাইরেক্ট করুন।\n- Done দিয়ে শেষ করে নিশ্চিত করুন ফ্লো ক্লিয়ান করে সরানো হয়েছে। ব্যবহারকারীকে Back করে একটি সম্পন্ন উইজার্ড আবার প্রবেশ করা উচিত নয়।\n\nপরীক্ষার একটি সহজ উপায়: একটি তিন-স্ক্রিন অনবোর্ডিং উইজার্ড কল্পনা করুন (Profile, Permissions, Confirm)। একটি নাম দিন, এগিয়ে যান, ফিরে যান, এডিট করুন, তারপর ডীপ লিংকের মাধ্যমে Confirm এ ঝাঁপ দিন। যদি Confirm-এ পুরোনো নাম দেখায়, বা Back আপনাকে একটি ডুপ্লিকেট Profile স্ক্রিনে নিয়ে যায়, তাহলে আপনার পাথ আপডেট কনসিস্টেন্ট নয়।\n\nযদি আপনি চেকলিস্ট পার হয়ে যান কোন সারপ্রাইজ ছাড়া, আপনার ফ্লো শান্ত ও প্রত্যাশাযোগ্য লাগবে, এমনকি ব্যবহারকারী পরে ফিরে এলে।\n\n## বাস্তবসম্মত উদাহরণ এবং পরবর্তী ধাপ\n\nএকটি ম্যানেজার অনুমোদন ফ্লো কল্পনা করুন একটি expense request-এর জন্য। এতে চারটি ধাপ আছে: Review, Edit, Confirm, এবং Receipt। ব্যবহারকারী একটি জিনিস প্রত্যাশা করে: Back সবসময় পূর্ববর্তী ধাপে যায়, তাদের আগে দেখা কোনো র‍্যান্ডম স্ক্রিনে নয়।\n\nএকটি সহজ রুট enum এটি প্রত্যাশাযোগ্য রাখে। আপনার `NavigationPath`-এ কেবল রুট ও পুনরায় লোড করার জন্য প্রয়োজনীয় ছোট আইডি রাখুন, যেমন `expenseID` এবং একটি `mode` (review vs edit)। বড়, পরিবর্তনশীল মডেল পুশ করা এড়িয়ে চলুন কারণ তা রিস্টোর ও ডীপ লিংক ভঙ্গুর করে দেয়।\n\nওয়ার্কিং ড্রাফ্ট একটি একক সোর্স অফ ট্রুথে রাখুন (উদাহরণ: একটি `@StateObject` ফ্লো মডেল বা একটি স্টোর)। প্রতিটি ধাপ সেই মডেল পড়ে ও লেখে, তাই স্ক্রিনগুলো আপনা আপনি এসে গেলে ডেটা হারাবে না।\n\nঅন্ততপক্ষে, আপনি তিনটি জিনিস ট্র্যাক করছেন:\n\n- Routes (উদাহরণ: `review(expenseID)`, `edit(expenseID)`, `confirm(expenseID)`, `receipt(expenseID)`)\n- Data (লাইনের আইটেম ও নোটসহ ড্রাফ্ট অবজেক্ট, এবং একটি স্ট্যাটাস যেমন `pending`, `approved`, `rejected`)\n- Location (ড্রাফ্ট আপনার ফ্লো মডেলে, ক্যাননিক্যাল রেকর্ড সার্ভারে, এবং একটি ছোট রিস্টোর টোকেন লোকালি: expenseID + last step)\n\nএজ কেসগুলোই সেই জায়গা যেখানে ফ্লো ব্যবহারকারীর বিশ্বাস জিতবে বা হারাবে। যদি ম্যানেজার Confirm-এ রিজেক্ট করে, নিশ্চিত করুন Back কীভাবে আচরণ করবে—Edit-এ ফিরবে কি ফ্লো ছেড়ে যাবে? যদি তারা পরে ফিরে আসে, শেষ স্টেপটি সংরক্ষিত টোকেন থেকে পুনরুদ্ধার করুন এবং ড্রাফ্ট রিলোড করুন। ডিভাইস বদলালে সার্ভারকে সত্যি হিসাবে বিবেচনা করুন: সার্ভার স্ট্যাটাস থেকে পাথ পুনর্নির্মাণ করুন এবং সঠিক ধাপে পাঠান।\n\nপরবর্তী ধাপ: আপনার রুট enum ডকুমেন্ট করুন (প্রতিটি কেস কী অর্থ এবং কখন ব্যবহার করা হয়), পাথ বিল্ডিং ও রিস্টোর আচরণের জন্য কয়েকটি বেসিক টেস্ট যোগ করুন, এবং এক নিয়ম মেনে চলুন: ভিউগুলো নেভিগেশন সিদ্ধান্তের মালিক নয়।\n\nযদি আপনি একই ধরনের মাল্টি-স্টেপ ফ্লো বারবার তৈরি করছেন কিন্তু স্ক্র্যাচ থেকে সবকিছু লিখতে চান না, AppMaster (appmaster.io)-এর মতো প্ল্যাটফর্মগুলো একই আলাদা করার নীতি প্রয়োগ করে: স্টেপ নেভিগেশন ও বিজনেস ডেটা আলাদা রাখুন যাতে স্ক্রিন বদলালেও ব্যবহারকারীর অগ্রগতি ভাঙে না।

প্রশ্নোত্তর

কিভাবে আমি একটি মাল্টি-স্টেপ SwiftUI ফ্লো-এ Back বোতামকে প্রত্যাশামতো রাখব?

NavigationStack এবং আপনার নিয়ন্ত্রণাধীন একটি একক NavigationPath ব্যবহার করুন। প্রতিটি “Next” অ্যাকশনে একটিই রুট যোগ করুন এবং প্রতিটি Back-এ একটিই রুট সরান। যখন আপনাকে জাম্প দরকার (যেমন Review থেকে “Edit profile”), স্ট্যাক কেটে একটি পরিচিত ধাপে ফেরান যেটি নির্ভরযোগ্য; অতিরিক্ত স্ক্রিন পুশ করবেন না।

কেন আমি নেভিগেট করে ফিরে এলে আমার ফর্ম ডেটা লুপে হারিয়ে যায়?

SwiftUI আপনার ভিউ অবজেক্ট নয়, বরং নেভিগেশনে ব্যবহৃত ডেটা (রুট মান) সংরক্ষণ করে এবং প্রয়োজন হলে গন্তব্য ভিউ পুনর্নির্মাণ করে। যদি আপনার ফর্ম ডেটা ভিউয়ের @State-এ থাকে, ভিউ পুনর্নির্মাণ হলে তা হারিয়ে যেতে পারে। ড্রাফ্ট ডেটা একটি শেয়ার্ড মডেলে (উদাহরণ: ObservableObject) রাখুন যাতে পুশ-কৃত ভিউগুলো থেকে লক্ষ্যভ্রষ্ট না হয়।

কেন আমি স্ট্যাক-এ একই স্ক্রিন দুবার দেখছি?

এটি সাধারণত ঘটে যখন একই রুট একাধিকবার অ্যাপেন্ড করা হয় (প্রায়ই দ্রুত ট্যাপ বা একাধিক কোড পথ নেভিগেশন ট্রিগার করার কারণে)। Next বোতামকে নেভিগেট করার সময় বা লোডিং/ভ্যালিডেশনের সময় অক্ষম করে দিন, এবং নেভিগেশন মিউটেশনগুলো এক জায়গায় কেন্দ্রীভূত রাখুন যাতে প্রতিটি ধাপে একবার মাত্র অ্যাপেন্ড ঘটে।

আমার রুট enum-এ কী রাখা উচিত এবং কী রাখা উচিত নয়?

রুটে ছোট ও স্থিতিশীল মান রাখুন—একটি enum কেস এবং হালকা আইডি। পরিবর্তনশীল ডেটা (ড্রাফ্ট) আলাদা শেয়ার্ড অবজেক্টে রাখুন এবং প্রয়োজনে ID দিয়ে লুক আপ করুন। বড় বা পরিবর্তনশীল মডেলকে পাথ-এ পুশ করা Hashable ধারণাকে ভাঙতে পারে ও গন্তব্যের অনমিল তৈরি করে।

কিভাবে আমি নেভিগেশন স্টেটকে ফ্লো স্টেট থেকে পরিষ্কারভাবে আলাদা রাখব?

নেভিগেশন হচ্ছে “ব্যবহারকারী কোথায় আছে”, আর ফ্লো স্টেট হচ্ছে “তারা কী দিয়েছে/ভর্তি করেছে”。নেভিগেশন পাথ একটি রাউটার (অথবা টপ-লেভেল স্টেট) দখল করুক এবং ড্রাফ্ট আলাদা ObservableObject-এ থাকুক। প্রতিটি স্ক্রিন ড্রাফ্ট সম্পাদনা করবে; রাউটার শুধু ধাপ বদলাবে।

কিভাবে আমি উইজার্ডের ধাপ ৩-এ ডীপ লিংক নিরাপদভাবে হ্যান্ডেল করব?

ডীপ লিংককে একটি নিরাপদ রুট সিকোয়েন্স বানিয়ে নিন, সরাসরি একটি ভিউতে টেলিপোর্ট না করে। প্রথমে প্রয়োজনীয় প্রাক-শর্ত যাচাই করে সেই ধাপগুলো অ্যাপেন্ড করুন, তারপর টার্গেট ধাপ যোগ করুন। এটি ব্যাক স্ট্যাককে সমন্বিত রাখে এবং অবৈধ স্টেটে ফেলে দেয় না।

কিভাবে আমি অ্যাপ রিস্টার্টের পর আংশিকভাবে শেষ হওয়া ফ্লো পুনরুদ্ধার করব?

দুইটা জিনিস সেভ করুন: শেষ অর্থবহ রুট (বা স্টেপ আইডি) এবং ড্রাফ্ট ডেটা। রিলঞ্চে, একই প্রাকশর্ত যাচাই করে পাথ পুনর্নির্মাণ করুন এবং ড্রাফ্ট লোড করুন। যদি ড্রাফ্ট পুরোনো হয়ে থাকে, ক্ষেত্রগুলো পূর্বরূপে ভর্তি করে শুরু করা অধিক স্পষ্ট হতে পারে বদলে মাঝপথে ফেলে দেওয়ার চেয়ে।

কখন আমি ফ্লো-তে push করব এবং কখন modal উপস্থাপন করব?

মূল পথের জন্য push ব্যবহার করুন যাতে Back স্বাভাবিকভাবে ধাপগুলো ট্রেস করে। ছোট অনলাইন টাস্কের জন্য sheet ব্যবহার করুন (তারিখ পছন্দ, দেশ নির্বাচন), আর আলাদা অভিজ্ঞতার জন্য fullScreenCover ব্যবহার করুন (লগইন, ক্যামেরা ক্যাপচার)। মডাল-এ মূল ধাপ রাখা এড়িয়ে চলুন কারণ ডিসমিস জেস্চার স্টেট ও UI-কে অসামঞ্জস্য করতে পারে।

কখন আমি ব্যাক জেস্চার/বাটন ওভাররাইড করে “Are you sure?” ডায়ালগ দেখাবো?

পেজ সিস্টেমের ডিফল্ট ব্যাক আচরণকে বাধা না দিলে ব্যবহারকারীর প্রত্যাশা মিলে যায়। শুধুমাত্র সেই ক্ষেত্রে ব্যাক ইন্টারসেপ্ট করুন যেখানে ফিরে যাওয়া বাস্তবে ক্ষতি করতে পারে (দীর্ঘ অসেভড ফর্ম হারানো ইত্যাদি)। স্ক্রীন যদি "dirty" হয়, একবার স্পষ্ট কনফার্মেশন জিজ্ঞাসা করুন—সহজ ভাষায় ফলাফল ব্যাখ্যা করে।

কোন ভুলগুলোই সাধারণত ব্যাক স্ট্যাক-বাগ তৈরি করে?

সাধারণ কারণগুলো: একাধিক NavigationStack অনিচ্ছাকৃতভাবে নেস্ট হওয়া; NavigationPath বারবার পুনরায় ইনিশিয়ালাইজ করা; একাধিক উৎস থেকে পাথ মিউটেট করা। প্রতিটি ফ্লো-এ একটিই স্ট্যাক রাখুন, পাথ দীর্ঘস্থায়ী স্টেটে (@StateObject বা একটি রাউটার) রাখুন এবং পুশ/পপ লজিক এক জায়গায় কেন্দ্রীভূত করুন।

শুরু করা সহজ
কিছু আশ্চর্যজনকতৈরি করুন

বিনামূল্যের পরিকল্পনা সহ অ্যাপমাস্টারের সাথে পরীক্ষা করুন।
আপনি যখন প্রস্তুত হবেন তখন আপনি সঠিক সদস্যতা বেছে নিতে পারেন৷

এবার শুরু করা যাক