在 SwiftUI 中有条件地更改 NavigationLink 目标

Change NavigationLink destination conditionally in SwiftUI

我有一个带有列表的视图,根据我单击的列表项目,我需要打开不同的 NavigationLink。

我有一个 行模型 用于列表的一行(我说的是列表,尽管我实际上通常使用一个名为 Collection 的定制元素,这是一个List 与 HStack 和 VStack 一起模拟 UICollectionView)

在行模型视图中,我只能为 NavigationLink 指定一个目的地

我能想到的一个解决方案是获取被单击的列表项的索引,并根据该索引打开某个视图。但是我无法让它工作。

我基本上需要 List 中的每个元素打开不同的视图。

感谢任何帮助! 谢谢! :)


GroupDetail.swift

import SwiftUI

// data displayed in the collection view
var itemsGroupData = [
  ItemsGroupModel("Projects"), // should open ProjectsView()
  ItemsGroupModel("People"), //should open PeopleView()
  ItemsGroupModel("Agenda"), //should open AgendaView()
  ItemsGroupModel("Name") //should open NameView()
]

// main view where the collection view is shown
struct GroupDetail: View {

  var body: some View {

    // FAMOUS COLLECTION ELEMENT
    Collection(itemsGroupData, columns: 2, scrollIndicators: false) { index in
      ItemsGroupRow(data: index)
    }
  }
}

// model of the data
struct ItemsGroupModel: Identifiable {
  var id: UUID
  let title: String

  init(_ title: String) {
    self.id = UUID()
    self.title = title
  }
}

// row type of the collection view
struct ItemsGroupRow: View {

  var body: some View {

    // This model is for one row
    // I need to specify what view to open depending on what item of the collection view was selected (clicked on)
    NavigationLink(destination: ProjectsView()) { // open ProjectsView only if the user clicked on the item "Projects" of the list etc..
      Text(data.title)
    }
  }
}

-----------------------------------------------------------------------

Collection.swift

// this custom struct creates the UIKit equivalent of the UICollectionView
// it uses a HStack and a VStack to make columns and rows

import SwiftUI

@available(iOS 13.0, OSX 10.15, *)
public struct Collection<Data, Content>: View
where Data: RandomAccessCollection, Content: View, Data.Element: Identifiable {

  private struct CollectionIndex: Identifiable { var id: Int }

  // MARK: - STORED PROPERTIES
  private let columns: Int
  private let columnsInLandscape: Int
  private let vSpacing: CGFloat
  private let hSpacing: CGFloat
  private let vPadding: CGFloat
  private let hPadding: CGFloat
  private let scrollIndicators: Bool
  private let axisSet: Axis.Set

  private let data: [Data.Element]
  private let content: (Data.Element) -> Content

  // MARK: - COMPUTED PROPERTIES
  private var actualRows: Int {
    return data.count / self.actualColumns
  }

  private var actualColumns: Int {
    return UIDevice.current.orientation.isLandscape ? columnsInLandscape : columns
  }

  // MARK: - INIT
  public init(_ data: Data,
              columns: Int = 2,
              columnsInLandscape: Int? = nil,
              scrollIndicators: Bool = true,
              axisSet: Axis.Set = .vertical,
              vSpacing: CGFloat = 10,
              hSpacing: CGFloat = 10,
              vPadding: CGFloat = 10,
              hPadding: CGFloat = 10,
              content: @escaping (Data.Element) -> Content) {
    self.data = data.map { [=10=] }
    self.content = content
    self.columns = max(1, columns)
    self.columnsInLandscape = columnsInLandscape ?? max(1, columns)
    self.vSpacing = vSpacing
    self.hSpacing = hSpacing
    self.vPadding = vPadding
    self.hPadding = hPadding
    self.scrollIndicators = scrollIndicators
    self.axisSet = axisSet
  }

  // MARK: - BODY
  public var body : some View {

    GeometryReader { geometry in
      ScrollView(self.axisSet, showsIndicators: self.scrollIndicators) {
        if self.axisSet == .horizontal {
          HStack(alignment: .center, spacing: self.hSpacing) {
            ForEach((0 ..< self.actualRows).map { CollectionIndex(id: [=10=]) }) { row in
              self.createRow(row.id, geometry: geometry)
            }
          }
        } else {
          VStack(spacing: self.vSpacing) {
            ForEach((0 ..< self.actualRows).map { CollectionIndex(id: [=10=]) }) { row in
              self.createRow(row.id * self.actualColumns, geometry: geometry)
            }
            // LAST ROW HANDLING
            if (self.data.count % self.actualColumns > 0) {
              self.createRow(self.actualRows * self.actualColumns, geometry: geometry, isLastRow: true)
                .padding(.bottom, self.vPadding)
            }
          }
        }
      }
    }
  }

  // MARK: - HELPER FUNCTIONS
  private func createRow(_ index: Int, geometry: GeometryProxy, isLastRow: Bool = false) -> some View {
    HStack(spacing: self.hSpacing) {
      ForEach((0 ..< actualColumns).map { CollectionIndex(id: [=10=]) }) { column in
        self.contentAtIndex(index + column.id)
          .frame(width: self.contentWidthForGeometry(geometry))
          .opacity(!isLastRow || column.id < self.data.count % self.actualColumns ? 1.0 : 0.0)
      }
    }
  }

  private func contentAtIndex(_ index: Int) -> Content {
    // (Addressing the workaround with transparent content in the last row) :
    let object = index < data.count ? data[index] : data[data.count - 1]
    return content(object)
  }

  private func contentWidthForGeometry(_ geometry: GeometryProxy) -> CGFloat {
    let hSpacings = hSpacing * (CGFloat(self.actualColumns) - 1)
    let width = geometry.size.width - hSpacings - hPadding * 2
    return width / CGFloat(self.actualColumns)
  }
}

我假设您想查看元素并确定要显示的视图,下面的内容可以实现这一点。您也可以为此使用枚举。

Collection 传入元素,因此您可以使用它和条件:

struct ProjectView: View {
    var row: ItemsGroupModel
    var body: some View {
        Text("Project title = \(row.title)")
    }
}

struct NonProjectView: View {
    var row: ItemsGroupModel
    var body: some View {
        Text("Non-Project title = \(row.title)")
    }
}

// row type of the collection view
struct ItemsGroupRow: View {

    var data: ItemsGroupModel

    var body: some View {

        // This model is for one row
        // I need to specify what view to open depending on what item of the collection view was selected (clicked on)
        NavigationLink(destination: {
            VStack{
                if data.title.contains("project") {
                    ProjectView(row: data)
                } else {
                    NonProjectView(row: data)
                }
            }
        }()) { // open ProjectsView only if the user clicked on the item "Projects" of the list etc..
            Text(data.title)
        }
    }
}

注意:我认为该合集确实存在一些错误。

使用 enum 的示例无法正常工作:

import SwiftUI

struct ConditionalNavigationLinkView: View {
    var body: some View {
        GroupDetail()
    }
}

// data displayed in the collection view
var itemsGroupData = [
    ItemsGroupModel(.project), // should open ProjectsView()
    ItemsGroupModel(.people), //should open PeopleView()
    ItemsGroupModel(.agenda), //should open AgendaView()
    ItemsGroupModel(.name) //should open NameView()
]

// main view where the collection view is shown
struct GroupDetail: View {

    var body: some View {

        NavigationView{
            // FAMOUS COLLECTION ELEMENT
            Collection(itemsGroupData, columns: 2, scrollIndicators: false) { row in
                ItemsGroupRow(data: row)
            }
        }
    }
}

enum ItemsGroupModelType: String {
    case project = "Projects"
    case people = "People"
    case agenda = "Agenda"
    case name = "Name"
}

// model of the data
struct ItemsGroupModel: Identifiable {
    var id: UUID
    let type: ItemsGroupModelType

    init(_ type: ItemsGroupModelType) {
        self.id = UUID()
        self.type = type
    }
}

struct ProjectView: View {
    var row: ItemsGroupModel
    var body: some View {
        Text("Projects \(row.type.rawValue)")
    }
}

struct NameView: View {
    var row: ItemsGroupModel
    var body: some View {
        Text("NameView \(row.type.rawValue)")
    }
}

struct PeopleView: View {
    var row: ItemsGroupModel
    var body: some View {
        Text("PeopleView \(row.type.rawValue)")
    }
}

struct AgendaView: View {
    var row: ItemsGroupModel
    var body: some View {
        Text("AgendaView \(row.type.rawValue)")
    }
}

// row type of the collection view
struct ItemsGroupRow: View {

    func printingEmptyView() -> EmptyView {
        print("type: \(data.type.rawValue)")
        return EmptyView()
    }

    var data: ItemsGroupModel

    var body: some View {

        // This model is for one row
        // I need to specify what view to open depending on what item of the collection view was selected (clicked on)
        NavigationLink(destination: {
            //print("Hello")

            VStack{
                self.printingEmptyView()
                if data.type == .project {
                    ProjectView(row: data)
                }
                if data.type == .people {
                    PeopleView(row: data)
                }
                if data.type == .agenda {
                    AgendaView(row: data)
                }
                if data.type == .name {
                    NameView(row: data)
                }
            }
        }()) { // open ProjectsView only if the user clicked on the item "Projects" of the list etc..
            Text(data.type.rawValue)
        }
    }
}

虽然接受的答案解决了问题,但这并不是一个完美的解决方案。要清理我们的代码,最好调用一个 return 到达所需目的地的方法。这是我解决它的方法:

下面的注释:我在名为 destination 的导航项模型中添加了一个 属性,将其类型设置为 Any,然后使用 nameOfTypeToNavigateTo.self 对其进行初始化。这样我们就不需要使用单元格标签之类的东西来确定哪个单元格被点击了。

struct NavigationCell: View {
    var navigationItem: NavigationItem
    var body: some View {
        NavigationLink(destination: getDestination(from: navigationItem)) {
            HStack {
                Text(navigationItem.name)
            }
        }
    }
    
    func getDestination(from navItem: NavigationItem) -> AnyView {
        if navItem.destination is ZoneList.Type {
            return AnyView(ZonesList())
        } else {
            return AnyView(ListStyles())
        }
    }
}

这里的技巧是确保您的 return 类型是 AnyView。直觉上你会认为 getDestination() 的 return 类型应该是 some View 因为 View 是我们在 SwiftUI 中构建的视图所遵循的协议。但是 returns some View 的方法只能 return 一种类型——恰好符合 View 的单一类型。但这不是我们需要的。我们正在尝试创建一种方法,该方法能够 returning 各种类型的任何人,具体取决于我们要导航到的视图。 AnyView 是解决方案,给我们一个 type erased view.

这是 AnyViewApple docs。 这是关于如何以其他方式使用它的 helpful article

以下代码根据 topicId

将用户点击列表单元格导航至 Tutor listStudent list
struct Topic: Identifiable
{
    var id = UUID()
    var topicId : Int
    var title   : String
    var desc    : String
}

struct TopicCell: View
{
    let topic : Topic
    
    var body: some View
    {
        NavigationLink(destination: getDestination(topic: topic))
        {
            VStack(alignment: .leading)
            {
                Text(topic.title)
                    .foregroundColor(.blue)
                    .font(.title)
                
                Text(topic.desc)
                    .foregroundColor(.white)
                    .font(.subheadline)
            }
        }
    }
    
    func getDestination(topic: Topic) -> AnyView
    {
        if topic.topicId == 0 {
            return AnyView(TutorList(topic: topic))
        } else {
            return AnyView(StudentList(topic: topic))
        }
    }
}