在 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
.
这是 AnyView
的 Apple docs。
这是关于如何以其他方式使用它的 helpful article:
以下代码根据 topicId
将用户点击列表单元格导航至 Tutor list
或 Student 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))
}
}
}
我有一个带有列表的视图,根据我单击的列表项目,我需要打开不同的 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
.
这是 AnyView
的 Apple docs。
这是关于如何以其他方式使用它的 helpful article:
以下代码根据 topicId
Tutor list
或 Student 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))
}
}
}