I have a view with a List and depending on the item of the list I have clicked on, I need to open a different NavigationLink.
I have a row model for one row of the List (I'm saying List even though I'm actually usually using a custom made element called Collection, which is a List with HStack and VStack together to emulate the UICollectionView)
In the row model view I can only specify one destination for the NavigationLink.
A solution I can think of is getting the index of the item of the list that was clicked and opening a certain view based on that. But I was not able to get it to work.
I basically need that each element in the List opens a different view.
Any help is appreciated! Thank you ! :)
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)
}
}
While the accepted answer solves the problem it's not exactly an elegant solution. To clean up our code it would be preferable to call a method that returns the desired destination. Here is how I solved it:
Note on below: I added a property to my nav item model called destination
, set its type to Any
and then init it with nameOfTypeToNavigateTo.self
. This way we avoid needing to using things like cell labels to determine which cell was tapped.
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())
}
}
}
The trick here is to ensure your return type is AnyView
. Intuitively you would think the return type of getDestination()
ought to be some View
since View
is the protocol to which the views we build in SwiftUI conform. But a method that returns some View
can only return ONE type - a single type that happens to conform to View
. But that is not what we need. We are trying to create a method that is capable of returning anyone of a variety of types depending on which view we want to navigate to. AnyView
is the solution and gives us a type erased view
.
Here are the Apple docs for AnyView
.
And here is a helpful article on how to use it in other ways:
I assume you want to look at the element and to determine what view to show, the following achieves that. You can also use an enum for this.
The Collection
passes in the element, so you can use that and a conditional:
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)
}
}
}
Note: I think the collection does have some bugs.
Example using enum
which does not work correctly:
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)
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With