Условно изменить пункт назначения NavigationLink в SwiftUI
У меня есть представление со списком, и в зависимости от пункта списка, на котором я щелкнул, мне нужно открыть другую NavigationLink.
У меня есть модель строки для одной строки списка (я говорю "Список", хотя обычно я использую пользовательский элемент под названием Collection, который представляет собой List с HStack и VStack вместе для эмуляции UICollectionView)
В представлении модели строки я могу указать только один пункт назначения для NavigationLink.
Решение, которое я могу придумать, - это получить индекс элемента списка, по которому щелкнули, и открыть определенное представление на основе этого. Но я не смог заставить его работать.
Мне в основном нужно, чтобы каждый элемент в Списке открывал другое представление.
Любая помощь приветствуется! Спасибо! :)
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)
}
}
3 ответа
Я предполагаю, что вы хотите посмотреть на элемент и определить, какое представление показывать, следующее достигается этим. Вы также можете использовать перечисление для этого.
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)
}
}
}
Хотя принятый ответ решает проблему, это не совсем элегантное решение. Чтобы очистить наш код, было бы предпочтительнее вызвать метод, который возвращает желаемое место назначения. Вот как я это решил:
Примечание ниже: я добавил свойство в свою модель навигационного элемента под названием 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 дляAnyView
. А вот полезная статья о том, как использовать его другими способами:
Следующий код перемещает пользователя по щелчку ячейки списка либо к
Tutor list
или чтобы
Student list
на основе
topicId
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))
}
}
}