【问题标题】:Change NavigationLink destination conditionally in SwiftUI在 SwiftUI 中有条件地更改 NavigationLink 目标
【发布时间】:2019-08-22 09:34:59
【问题描述】:

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

我有一个 row model 用于 List 的一行(我说的是 List,尽管我实际上通常使用一个名为 Collection 的自定义元素,它是一个带有 HStack 和 VStack 的 List一起模拟 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 { $0 }
    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: $0) }) { row in
              self.createRow(row.id, geometry: geometry)
            }
          }
        } else {
          VStack(spacing: self.vSpacing) {
            ForEach((0 ..< self.actualRows).map { CollectionIndex(id: $0) }) { 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: $0) }) { 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 元素告诉我选择了哪一行,然后采取相应的行动!
  • 我现在无法访问我的 Macbook,所以我无法发布代码,但我可以在几个小时内完成。你遇到了什么错误?

标签: ios swiftui swiftui-list


【解决方案1】:

虽然公认的答案解决了问题,但它并不是一个优雅的解决方案。为了清理我们的代码,最好调用一个返回所需目的地的方法。以下是我的解决方法:

以下注意事项:我在导航项模型中添加了一个名为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())
        }
    }
}

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

这里是Apple docs 对应AnyView。 这里有一个helpful article,关于如何以其他方式使用它:

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 非常感谢!我也想过使用视图的标题,但我想避免它,因为当我将这些文本翻译成其他语言时,它就不再起作用了。我正在寻找一种让 Collection 告诉我选择了哪一行的方法。
    • 像什么错误?
    • 我添加了一个枚举示例,在该示例中,由于某种原因,单击无法正常工作。我不确定错误是什么。这可能是 SwiftUI 无法区分的问题,结果是未定义的行为,但谁知道呢。
    • mmmh .. 谢谢你的例子,我会调查一下,看看我是否能找到问题,或者只是 SwiftUI 仍然有问题:(
    • @Arthur 你发现按钮出了什么问题,还是在 iOS 13.1 中修复了?
    【解决方案3】:

    以下代码根据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))
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-10-24
      • 1970-01-01
      • 2021-01-26
      • 2020-08-26
      • 1970-01-01
      • 1970-01-01
      • 2022-09-29
      相关资源
      最近更新 更多