في هذا المقال سوف اشرح جميع ما تحتاج معرفته للتعامل مع الـ NavigationStack
بداية يتوجب عليا أن اوضح بأن NavigationStack هو بديل الـ NavigationView
منذ اصدار SwiftUI 4 وتحديث iOS 16 شركة Apple استبدلت NavigationView بـ NavigationStack
بما يعني بأنه فقط يعمل في iOS 16 وأحدث، لكن لاحقاً سوف اشرح كيف دعم الاصدارات السابقة
الطريقة الأساسية
باستخدام NavigationLink مع Destination
الطريقة السابقة التي كانت تستخدم مع NavigationView لازالت تعمل مع NavigationStack
مثل هذا المثال
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("Open View") {
OtherView()
}
}
}
}
struct OtherView: View {
var body: some View {
VStack {
Text("View 2")
}
}
}
في المثال السابق استخدمت نفس الطريقة القديمة ولكن بدل من استخدام NavigationView تم استخدام NavigationStack
النتيجة
الطريقة السابقة ماصرت تستخدم على نطاق واسع، لمحدوديتها ، سوف يتضح لك السبب لاحقاً
باستخدام navigationDestination(isPresented:)
في هذه الطريقة لن تحتاج الى NavigationLink فقط تحتاج الي متغير من نوع Bool و Button
struct ContentView: View {
@State private var shouldOpenView = false
var body: some View {
NavigationStack {
Button {
shouldOpenView.toggle()
} label: {
Text("Open View")
}
.navigationDestination(isPresented: $shouldOpenView) {
OtherView()
}
}
}
}
struct OtherView: View {
var body: some View {
VStack {
Text("View 2")
}
}
}
لاحظ موقع الـ navigationDestination بداخل الـ NavigationStack وليس خارجها
النتيجة سوف تكون مماثله لنسخه للفيديو السابق، لكن ما الفرق بين الطرقتين ؟
الفرق بين الطريقتين السابقة ؟
١- في الطريقة الثانية تستطيع فتح الصفحة بعد حدث معين، مثلا بعد وقت معين، بعد الانتهاء من ركويست معين الخ
٢- عند استخدام الطريقة الاولى داخل List سوف تلاحظ وجود سهم بسبب استخدامك للـ NavigationLink
يمكنك اخفائه بطرق معينه، لكن الامر لا يحتاج كل العناء، فالطريقة الثانية لن يظهر السهم، مع ملاحظة تستطيع تغيير لون الزر من الازرق للاسود او اي لون كأي زر
لاحظ الصورة التالية

الطريقة الاحترافية
كما هو واضح من شرح الطرق الاساسية فهيا محدوده جداً، وهي سبب في اعادة بناء الـ Navigation وتغييره من NavigationView الى NavigationStack
من الشرح السابق سوف تلاحظ محدودية الطريقة الاساسية
لانه لا تستطيع عمل التالي باستخدام الطريقة الاساسية :
١- لا تستطيع الرجوع الى الـ Root
والمقصود هنا بأنه لا تستطيع الرجوع لأول صفحة، افترض عندك 5 صفحات وتبغى عند الوصول لصفحة 5 انك ترجع لاول صفحة بالطرق الاساسية لا تستطيع عمل هذا الامر
٢- لا تستطيع الرجوع لصفحة معينه
٣- لا تستطيع الانتقال لصفحة معينه عن طريق الكود (برمجياً)
٣- فتح التطبيق على صفحة معينه عند الضغط على الـ Push Notification
باستخدام navigationDestination(for:)
الطريقة هذه مشابه تقريبا لسابقتها ولكن الفرق هنا اننا نستخدم
NavigationLink(value:)
كما هو واضح في الكود التالي:
struct ContentView: View {
@State private var shouldOpenView = false
var body: some View {
NavigationStack {
List {
NavigationLink(value: "Open View") {
Text("Open View")
}
}
.navigationDestination(for: String.self) { value in
OtherView(title: value)
}
}
}
}
struct OtherView: View {
var title: String
var body: some View {
VStack {
Text(title)
}
}
}
لاحظ قيمة value ممكن تكون اي شي لكن في هذا المثال مجرد نص عادي
ملاحظة: القيمة التي تمرر الى value هيا التي سوف تصل الى navigationDestination، بمعنى اذا مررت id مثلا تستطيع الوصول له من هنا ومن تم تمريره الى الـ View الاخر
النتيجة
الان سوف نضيف انوع اخرى مثل Int و Bool
struct ContentView: View
var body: some View {
NavigationStack {
List {
NavigationLink(value: "Open View") {
Text("Open View")
}
NavigationLink(value:1) {
Text("Open Int")
}
NavigationLink(value: true) {
Text("Open Bool")
}
}
.navigationDestination(for: String.self) { value in
OtherView(title: value)
}
.navigationDestination(for: Int.self) { value in
OtherView(title: "Int value is \(value)")
}
.navigationDestination(for: Bool.self) { value in
OtherView(title: "Bool value is \(value)")
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
}
}
}لا
لاحظ بانه استطيع التفرق بين كل NavigationLink اعتماداً على نوع الـ value
النتيجة
الامر اصبح واضح من المثال السابق بأنه اعتماداً على النوع تقدر تنتقل للصفحة التي تريدها
لكن في المشاريع ماراح تكون بهذه البساطة ، راح تعتمد على نوع البيانات الي وصلك من الـ API بصيغة اخرى اعتماداً على Model مو فقط اعتماداً على Data Type
اضيف هذا الـ JSON لمشروعك كملف json
هذه مجرد بيانات وهميه تم أخدها من هذا الموقع
{
"posts": [
{
"id": 1,
"title": "His mother had always taught him",
"body": "His mother had always taught him not to ever think of himself as better than others. He'd tried to live by this motto. He never looked down on those who were less fortunate or who had less money than him. But the stupidity of the group of people he was talking to made him change his mind.",
"userId": 9,
"tags": [
"history",
"american",
"crime"
],
"reactions": 2
},
{
"id": 2,
"title": "He was an expert but not in a discipline",
"body": "He was an expert but not in a discipline that anyone could fully appreciate. He knew how to hold the cone just right so that the soft server ice-cream fell into it at the precise angle to form a perfect cone each and every time. It had taken years to perfect and he could now do it without even putting any thought behind it.",
"userId": 13,
"tags": [
"french",
"fiction",
"english"
],
"reactions": 2
},
{
"id": 3,
"title": "Dave watched as the forest burned up on the hill.",
"body": "Dave watched as the forest burned up on the hill, only a few miles from her house. The car had been hastily packed and Marta was inside trying to round up the last of the pets. Dave went through his mental list of the most important papers and documents that they couldn't leave behind. He scolded himself for not having prepared these better in advance and hoped that he had remembered everything that was needed. He continued to wait for Marta to appear with the pets, but she still was nowhere to be seen.",
"userId": 32,
"tags": [
"magical",
"history",
"french"
],
"reactions": 5
},
{
"id": 4,
"title": "All he wanted was a candy bar.",
"body": "All he wanted was a candy bar. It didn't seem like a difficult request to comprehend, but the clerk remained frozen and didn't seem to want to honor the request. It might have had something to do with the gun pointed at his face.",
"userId": 12,
"tags": [
"mystery",
"english",
"american"
],
"reactions": 1
},
{
"id": 5,
"title": "Hopes and dreams were dashed that day.",
"body": "Hopes and dreams were dashed that day. It should have been expected, but it still came as a shock. The warning signs had been ignored in favor of the possibility, however remote, that it could actually happen. That possibility had grown from hope to an undeniable belief it must be destiny. That was until it wasn't and the hopes and dreams came crashing down.",
"userId": 41,
"tags": [
"crime",
"mystery",
"love"
],
"reactions": 2
},
{
"id": 6,
"title": "Dave wasn't exactly sure how he had ended up",
"body": "Dave wasn't exactly sure how he had ended up in this predicament. He ran through all the events that had lead to this current situation and it still didn't make sense. He wanted to spend some time to try and make sense of it all, but he had higher priorities at the moment. The first was how to get out of his current situation of being naked in a tree with snow falling all around and no way for him to get down.",
"userId": 47,
"tags": [
"english",
"classic",
"american"
],
"reactions": 3
},
{
"id": 7,
"title": "This is important to remember.",
"body": "This is important to remember. Love isn't like pie. You don't need to divide it among all your friends and loved ones. No matter how much love you give, you can always give more. It doesn't run out, so don't try to hold back giving it as if it may one day run out. Give it freely and as much as you want.",
"userId": 12,
"tags": [
"magical",
"crime"
],
"reactions": 0
},
{
"id": 8,
"title": "One can cook on and with an open fire.",
"body": "One can cook on and with an open fire. These are some of the ways to cook with fire outside. Cooking meat using a spit is a great way to evenly cook meat. In order to keep meat from burning, it's best to slowly rotate it.",
"userId": 31,
"tags": [
"american",
"english"
],
"reactions": 9
},
{
"id": 9,
"title": "There are different types of secrets.",
"body": "There are different types of secrets. She had held onto plenty of them during her life, but this one was different. She found herself holding onto the worst type. It was the type of secret that could gnaw away at your insides if you didn't tell someone about it, but it could end up getting you killed if you did.",
"userId": 42,
"tags": [
"american",
"history",
"magical"
],
"reactions": 2
},
{
"id": 10,
"title": "They rushed out the door.",
"body": "They rushed out the door, grabbing anything and everything they could think of they might need. There was no time to double-check to make sure they weren't leaving something important behind. Everything was thrown into the car and they sped off. Thirty minutes later they were safe and that was when it dawned on them that they had forgotten the most important thing of all.",
"userId": 1,
"tags": [
"fiction",
"magical",
"history"
],
"reactions": 4
}
],
"total": 150,
"skip": 0,
"limit": 10
}
اضيف هذا الـ Model
struct Posts: Decodable {
let posts: [Post]
let total: Int
let skip: Int
let limit: Int
}
struct Post: Decodable, Identifiable, Hashable {
let id: Int
let title: String
let body: String
let userId: Int
let tags: [String]
let reactions: Int
}
اضيف هذا الكود لتسهيل قراءة الـ json المرفق في المشروع
الـ Identifiable لاجل استخدامه داخل الـ List بدون الحاجة لاضافة id
أما الـ Hashable فضرورية لاستخدامها في الـ navigationDestination
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from file: String, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
do {
return try decoder.decode(T.self, from: data)
} catch DecodingError.keyNotFound(let key, let context) {
fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' not found – \(context.debugDescription)")
} catch DecodingError.typeMismatch(_, let context) {
fatalError("Failed to decode \(file) from bundle due to type mismatch – \(context.debugDescription)")
} catch DecodingError.valueNotFound(let type, let context) {
fatalError("Failed to decode \(file) from bundle due to missing \(type) value – \(context.debugDescription)")
} catch DecodingError.dataCorrupted(_) {
fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON")
} catch {
fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)")
}
}
}
الكود النهائي
struct ContentView: View {
var body: some View {
NavigationStack {
List {
ForEach(posts()) { post in
NavigationLink(value: post) {
Text(post.title)
}
}
}
List {
NavigationLink(value: "Open View") {
Text("Open View")
}
NavigationLink(value:1) {
Text("Open Int")
}
NavigationLink(value: true) {
Text("Open Bool")
}
}
.navigationDestination(for: String.self) { value in
OtherView(title: value)
}
.navigationDestination(for: Int.self) { value in
OtherView(title: "Int value is \(value)")
}
.navigationDestination(for: Bool.self) { value in
OtherView(title: "Bool value is \(value)")
}
.navigationDestination(for: Post.self, destination: { post in
PostView(post: post)
})
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
}
}
func posts() -> [Post] {
let result = Bundle.main.decode(Posts.self, from: "posts.json")
return result.posts
}
}
struct PostView: View {
var post: Post
var body: some View {
VStack(alignment: .leading) {
Text("Id: \(post.id)")
.padding()
Text("Title: " + post.title)
.padding()
Text("Body: " + post.body)
.padding()
}
}
}
struct OtherView: View {
var title: String
var body: some View {
VStack {
Text(title)
}
}
}
struct Posts: Decodable {
let posts: [Post]
let total: Int
let skip: Int
let limit: Int
}
struct Post: Decodable, Identifiable, Hashable {
let id: Int
let title: String
let body: String
let userId: Int
let tags: [String]
let reactions: Int
}
لاحظ في الكود السابق اضفت PostView لتوضيح الفرق بين الانواع السابقة
ايضا في الـ navigationDestination استخدم نوع Post بدل من Data Type
النتيجة
باستخدام Path مع navigationDestination(for:)
في الفقرة السابقة شرحنا اساسيات الـ NavigationStack مع navigationDestination ولكن كما تلاحظ لا يوجد فرق كبير عن الطريقة الاساسية، لازلت لا تستطيع الرجوع الى الـ Root او الرجوع لصفحة معينه او حتى الانتقال لصفحة معينه عن طريق الكود
في هذه الفقرة سيتم شرح الأجزاء الباقية
struct ContentView: View {
@State private var path: [Post] = []
var body: some View {
NavigationStack(path: $path) {
List {
ForEach(posts()) { post in
NavigationLink(value: post) {
Text(post.title)
}
}
}
.navigationDestination(for: Post.self) { post in
PostView(path: $path, post: post)
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
}
}
}
func posts() -> [Post] {
let result = Bundle.main.decode(Posts.self, from: "posts.json")
return result.posts
}
}
في الكود السابق كما تلاحظ ازلت بقيت الصفحات وابقيت فقط List وحده حالياً
لاحظ
– الـ path تم اضافته من نوع [Post] وايضا تم اضافته ضمن الـ NavigationStack
– ايضا لاحظ بأنه مررت الـ path لصفحة PostView
في الفقره السابقة لم نضيف الـ path لانه تلقائيا هو موجود فمافي حاجة له في حال عدم الحاجة لتنقل بشكل برمجي او الرجوع لصفحة معينه او الرجوع لـ Root لكننا نحتاجه الان
بشكل افتراضي كل ما انتقل لصفحة يتم اضافة الصفحة ضمن اريه الـ path
الرجوع للـ Root
لاجل الرجوع للـ Root سوف نستفيد من الـ path وكل الي علينا نعمله هو حذف محتوى الاريه بالكامل
بمعنى جعله []
مثل الكود التالي
struct PostView: View {
@Binding var path: [Post]
var post: Post
var body: some View {
VStack(alignment: .leading) {
Text("Id: \(post.id)")
.padding()
Text("Title: " + post.title)
.padding()
Text("Body: " + post.body)
.padding()
Button(action: {
path = []
}, label: {
Text("Return to Root")
})
.padding()
}
}
}
لاحظ الزر Return to Root
ملاحظة هناك طرق اخرى للعودة الى الـ Root
// 1.
path = []
// 2.
path = .init()
// 3.
path.removeLast(path.count)
// 4.
path.removeAll()
جميع الطرق التي ذكرتها تقوم بنفس المهمه
النتيجة
الرجوع لصفحة معين
تستطيع الرجوع لصفحة معينه بحذف الصفحة من الاريه
مثال لحذف اخر صفحة
path.removeLast()
مثال اخر افترض عندك 5 صفحات وتريد الرجوع لصفحة رقم 3
path.removeLast(2)
اي رقم تضيفه داخل الاقواس سيتم حذفه
انتبيه من استخدام الكودين السابقة
لانه في حال لا يوجد شي في الاريه سينهار التطبيق!
لذلك يتوجب التحقق من محتوى الاريه بإستخدام
if path.count > 2 {
path.removeLast(2)
}
في الكود السابق تحققت بأنه الاريه يحتوي على عنصرين على الاقل قبل حذفهم من الاريه الـ path
باستخدام NavigationPath مع navigationDestination(for:)
كما لاحظت في الفقره السابقة الـ path كان من نوع الـ Model نفسه
في حال كان الـ View لا ينقلك الا لصفحات من نفس النوع ، فالفقره السابقة سوف تعمل بدون مشاكل
لكن الواقع وفي غالبية التطبيقات، المستخدم سوف ينتقل لصفحات مختلفه
مثال في موقع تويتر او X
راح تشوف تغريده قدامك
تقدر تضغط عليها وراح يفتحلك صفحة تفاصيل التغريده مع الردود
تقدر تضغط على حساب وتدخله صفحته الخاصه
فانت كذا انتقلت لكذا صفحة تحتوي انواع مختلفه ماهو نوع واحد
Apple حلت هذه المشكلة باضافة نوع NavigationPath ، هذا النوع يسمحلك تضيع اي نوع فيه
في المثال التالي سوف تلاحظ هذا الامر
struct ContentView: View {
@State private var path: NavigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
ForEach(posts()) { post in
NavigationLink(value: post) {
Text(post.title)
}
}
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: Post.self) { post in
PostView(path: $path, post: post)
}
.navigationDestination(for: Int.self) { value in
OtherView(path: $path, title: "Int value is \(value)")
}
}
}
func posts() -> [Post] {
let result = Bundle.main.decode(Posts.self, from: "posts.json")
return result.posts
}
}
struct PostView: View {
@Binding var path: NavigationPath
var post: Post
var body: some View {
VStack(alignment: .leading) {
Text("Id: \(post.id)")
.padding()
Text("Title: " + post.title)
.padding()
Text("Body: " + post.body)
.padding()
Button(action: {
path.append(10)
}, label: {
Text("Move to last View")
})
.padding()
}
}
}
struct OtherView: View {
@Binding var path: NavigationPath
var title: String
var body: some View {
VStack {
Text("Last Page")
Text(title)
Button(action: {
path.removeLast(path.count)
}, label: {
Text("Return to Root")
})
}
}
}
ملاحظات مهمة :
في هذه الفقره في عدة ملاحظات مهمه يتوجب عليك الانتباه لها
١- تم تغيير نوع الـ path الى NavigationPath
٢- الـ navigationDestination يتواجد فقط في اول صفحة مع الـ NavigationStack
لا يتوجب عليه اضافتهم في اي صفحة اخرى، اي صفحة اساسية سوف تضيف لها NavigationStack خاص بها
لكن اي صفحة فرعيه او بصيغة اخرى اي صفحة child ينبغي عليك الا تضيفها !!
٣- في صفحة PostView لاحظ اني استخدمت append للاضافة الصفحة في الاريه الخاص بالـ path
هذه هيا الطريقة البرمجية لاضافتها
النتيجة
من المثال السابق لاحظ بانك تقدر تضيف للـ NavigationStack باستخدام path بما يعني تستطيع تغير هذا الكود ايضا
ForEach(posts()) { post in
NavigationLink(value: post) {
Text(post.title)
}
}
الى
ForEach(posts()) { post in
Button(action: {
path.append(post)
}, label: {
Text(post.title)
})
.foregroundColor(.black)
}
لن تلاحظ اي فرق عن الكود السابق غير اختفاء السهم، وتم اضافة لون اسود لتغير لون النص للون الاسود
تحسين الـ navigationDestination
في التطبيقات بشكل عام، ماراح تكون ببساطة المثال السابق
بمعنى راح تحتاج عدة navigationDestination في صفحة وحده، لذلك لا يفضل عمل كذا navigationDestination ، ولكن يفضل الاكتفاء بواحد فقط
الامر مشابه لاستخدام item مع sheet او fullScreenCover بدلاً من استخدام isPresented
الفكره هيا ببساطه الاعتماد على اسنخدام enum
عمل enum للـ Destination
في المثال السابق كما لاحظت عندي نوعين من الصفحات نوع للـ post واخر لـ String لذلك سوف نعمل enum لهذه النوعين
enum Routing: Hashable {
case otherView(value: String)
case postView(post: Post)
}
لاحظ اني استخدمت Hashable
طريقة الاستخدام هيا بهذه الطريقة
طريقة الاستخدام
// 1.
path.append(Routing.postView(post: post))
// 2.
path.append(Routing.otherView(value: "\(10)"))
- عند اضافة الـ post استخدم السطر الاول
- عند اضافة الـ value استخدم السطر الثاني
طريقة استخدام الـ enum مع navigationDestination
.navigationDestination(for: Routing.self) { route in
switch route {
case .postView(let post) :
PostView(path: $path, post: post)
case .otherView(let value):
OtherView(path: $path, title: "Int value is \(value)")
}
}
الكود كامل
enum Routing: Decodable, Hashable {
case otherView(value: String)
case postView(post: Post)
}
struct ContentView: View {
@State private var path: NavigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
ForEach(posts()) { post in
Button(action: {
path.append(Routing.postView(post: post))
}, label: {
Text(post.title)
})
.foregroundColor(.black)
}
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: Routing.self) { route in
switch route {
case .postView(let post) :
PostView(path: $path, post: post)
case .otherView(let value):
OtherView(path: $path, title: "Int value is \(value)")
}
}
}
}
func posts() -> [Post] {
let result = Bundle.main.decode(Posts.self, from: "posts.json")
return result.posts
}
}
سبب عدم اضافة الـ enum داخل الـ struct هو امكانيه استخدامه في صفحات اخرى اذا احتجت لذلك
الاعتماد على Router و ObservableObject
في الامثله السابقة اعتمدت على الـ NavigationPath داخل الـ View نفسه، هذه تعتبر فكره ممتازه في حال تطبيقك صغير او يحتوي على صفحات قليلة، لكن عادة التطبيقات تحتوي على عدة صفحات، ايضا عادة التطبيقات تعتمد على الاشعارات وغيرها لذلك تحتاج الى الية لإدارة الـ Navigation ووحده من اشهر الطرق هيا استخدام Router كـ ObservableObject بالتالي يمكن استخدامه كـ StateObject تمريره كـ EnvironmentObject
في الكود التالي بنيت حل شامل وليس لاستخدامه لهذا المثال فقط، افترضت بأن سيتم استخدامه في مشروع يحتوي على عدة صفحات مختلفة ، مثلا كـ TabView و تعتمد على NavigationStack
final class Router: ObservableObject {
public enum Destination: Hashable {
case otherView(value: String)
case postView(post: Post)
}
public enum Path: String {
case mainView
}
@Published private var navPaths = [Path: NavigationPath]()
func pathBinding(forPathKey key: Path) -> Binding<NavigationPath> {
Binding(
get: { self.navPaths[key, default: NavigationPath()] },
set: { self.navPaths[key] = $0 }
)
}
func navigate(to destination: Destination, in pathKey: Path) {
if navPaths[pathKey] == nil {
navPaths[pathKey] = NavigationPath()
}
navPaths[pathKey]?.append(destination)
}
func navigateBack(in pathKey: Path) {
navPaths[pathKey]?.removeLast()
}
func navigateToRoot(in pathKey: Path) {
navPaths[pathKey]?.removeLast(navPaths[pathKey]?.count ?? 0)
}
}
لاحظ غيرت تسميت Routing الى Destination واضفت الـ enum بداخل الـ Router لتسهيل الوصول
لاحظ بأني اضفت enum بإسم Path ، يمكنك اضافة اي عدد تريده حسب تطبيقك
تذكر بأنه كل صفحة اساسية في تطبيقك تحتاج الى NavigationStack وايضا الى NavigationPath
فاذا مثلا عندك TabView مكون من 3 صفحات فكل صفحة راح تحتاج تضيف NavigationStack وايضا كل NavigationStack راح تحتاج NavigationPath خاص به ، على الاقل في الصفحات الاساسيه فقط
لاحظ بأني استخدمت navPaths كـ Dictionary لكي استطيع استخدامه مع اي NavigationPath
اما بخصوص pathBinding فانا احتاج الـ NaivgationPath يكون Binding عند استخدامه مع NavigationStack لذلك هذا الـ Function سوف يجلب الـ path كـ Binding ستتضح الفائده لاحقاً
navigate فائدته لجعل المستخدم ينتقل للصفحة التي يريدها ، لاحظ بأنه يتوجب تكرر الـ path له مع الـ destination
navigateBack هذه تستخدم للرجوع للصفحة السابقة
navigateToRoot تستخدم للرجوع لأول صفحة
طريقة الاستخدام
struct ContentView: View {
@StateObject var router = Router()
var body: some View {
NavigationStack(path: router.pathBinding(forPathKey: .mainView)) {
List {
ForEach(posts()) { post in
Button(action: {
router.navigate(to: .postView(post: post), in: .mainView)
}, label: {
Text(post.title)
})
.foregroundColor(.black)
}
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: Router.Destination.self) { route in
switch route {
case .postView(let post) :
PostView(post: post)
.environmentObject(router)
case .otherView(let value):
OtherView(title: "Int value is \(value)")
.environmentObject(router)
}
}
}
}
func posts() -> [Post] {
let result = Bundle.main.decode(Posts.self, from: "posts.json")
return result.posts
}
}
في هذه الصفحة اضفت الـ router كـ StateObject يمكنك اضافته في صفحة Main وبعدها تمرره كـ EnvironmentObject لكن في هذا المثال لم اقم بهذه الخطوه
لاحظ الـ path في NavigationStack استخدمت الـ pathBinding ومررت له mainView
لانه في مثالي هذا يحتوي فقط على صفحة وحده اساسيه الى هيا صفحة الـ ContentView
اما بقية الصفحة فهيا صفحات تابعه لها او بصيغة اخرى صفحة تفاصيل لهذا السبب احتاج NavigationPath وحده فقط
عند الانتقال للصفحة اخرى بعد الضغط على زر استخدمت router.navigate(to:)
وفي navigationDestination استخدمت Router.Destination
لاحظ ايضا باني مررت الـ route كـ environmentObject لبقيت الصفحات
struct PostView: View {
@EnvironmentObject private var router: Router
var post: Post
var body: some View {
VStack(alignment: .leading) {
Text("Id: \(post.id)")
.padding()
Text("Title: " + post.title)
.padding()
Text("Body: " + post.body)
.padding()
Button(action: {
router.navigate(to: .otherView(value: "\(10)"), in: .mainView)
}, label: {
Text("Move to last View")
})
.padding()
}
}
}
struct OtherView: View {
@EnvironmentObject private var router: Router
var title: String
var body: some View {
VStack {
Text("Last Page")
Text(title)
Button(action: {
router.navigateToRoot(in: .mainView)
}, label: {
Text("Return to Root")
})
}
}
}
بقيت الصفحات ، في الصفحة الاولى فقط استخدمت router.navigate
وفي الصفحة الثانية استخدمت router.navigateToRoot للرجوع للـ Root
النتيجة
الكود يعمل مثل السابق ، بدون مشاكل
لكن الان صار عندنا ميزة بأنه بإستطاعتنا فتح التطبيق على صفحة معينه، او مثلا لما المستخدم يضغط على اشعار تستطيع الانتقال لصفحة التفاصيل مباشره !
مثال يحاكي فكره فتح التطبيق من الاشعارات
لانه هذا المقال عن NavigationStack فلا اريد أن اشرح فيه خطوات عمل الاشعارات، لكن اريد توضيح للألية كيف تفتح التطبيق على صفحة معينه
import SwiftUI
@main
struct NavigationStackArticleApp: App {
@StateObject var router = Router()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(router)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
router.navigate(to: .otherView(value: "\(13)"), in: .mainView)
}
}
}
اذا شغلت التطبيق سوف تلاحظ بان انتقلت لصفحة التفاصيل مباشره، بخصوص التأخير لمدة ثانيه لاحظت اذا ما اضفت أي تأخير الصفحة تكون فاضيه في هذا المثال
دعم iOS 14 و iOS 15
كما ذكرت في بداية الموضوع بأن هذه الطريقة تم اضافتها في iOS 16 ولن تعمل في الاصدارات السابقة
ولكن هناك طريقة بدعمها حتى في الاصدارات السابقة وهيا الاعتماد على هذه المكتبة
NavigationBackport تعمل كحل وسيط بين استخدام الطريقة الجديدة ودعم جميع الاصدارات السابقة
بالاعتماد على اضافة NB قبل الاسم مثلا
بدل NavigationStack تصبح NBNavigationStack
NavigationLink تصبح NBNavigationLink
NavigationPath تصبح NBNavigationPath
navigationDestination تصبح nbNavigationDestination
المكتبة تعمل بدون مشاكل في اغلب السينايوهات
لكن في بعض المشاكل مع DeepLink فقط وجب التنويه
الخاتمة
هنا نصل لنهاية المقال
في هذا المقال شرحت كل اساسيات الـ NavigationStack
لكن هناك دائما امكانية لتحسين والتطوير، في حال وجدت او تعلمت طرق افضل للـ Navigation سوف احدث المقال او اكتب مقال منفصل لشرحها
