SwiftUI NavigationLink for row - swiftui

I have this:
ForEach(item, id: \.name) { _ in
NavigationLink(destination: DetailView()) {
ItemCell(item: $0)
}
}
But it's throwing an error saying:
Unnamed argument #2 must precede argument 'destination'
All the documentation I've seen so far seems to be following what I have.

Probably you wanted this
ForEach(item, id: \.name) { el in
NavigationLink(destination: DetailView()) {
ItemCell(item: el)
}
}

It looks like the API changed. This worked:
NavigationLink(destination: DetailView(), label: {
ItemCell(item: item)
})

Related

Multiple sheet(item: ) triggered by SwipeActions button and ToolBar buttons gets nil object in the first time in SwiftUI

I am using swipeActions in ForEach loop of a List and toolbar buttons to show different sheets on my SwiftUI view. But every first time I swipe left and click the Edit button, the object of that line is nil. If I do the same swipe and click thing again, everything goes well. Anyone else had this kind of bug before? Thank you.
Here is the related code:
struct LanguagesView: View {
#State var activeSheet: ActiveSheet?
#State var toBeEdit: MeLanguage?
var body: some View {
NavigationView {
List {
ForEach(self.meLanguages, id: \.id) { lan in
HStack {
Text("\(lan.wrappedName)")
.font(.headline)
}.swipeActions(allowsFullSwipe: false) {
Button(
action: {
self.activeSheet = .editLanguage
self.toBeEdit = lan
},
label: { Label("Edit", systemImage: "pencil") }
) .tint(.indigo)
}
}
}
.sheet(item: $activeSheet,
onDismiss: {
self.toBeEdit = nil
}
){
item in
switch item {
case .addLanguage:
AddLanguage()
case .sortLanguages:
SortLanguagesView()
case .editLanguage:
if self.toBeEdit != nil {
EditLanguageView( meLanguage: self.toBeEdit! )
}
else {
Text("self.toBeEdit is nil")
}
default:
Text("No such button on ContentView.")
}
}
.toolbar {
ToolbarItemGroup {
HStack {
Text("\(self.meLanguages.count) Languages on Card").font(.headline)
self.barButtons
}
}
}
}
}
var barButtons: some View {
HStack {
Button(
action: {
self.activeSheet = .sortLanguages
},
label: { Label("Sort Languages", systemImage: "arrow.up.arrow.down.circle")
}
).id("sortLanguages")
Button(
action: {
self.activeSheet = .addLanguage
},
label: { Label("Add Language",
systemImage: "plus")
.imageScale(.large)
}
)
}
}
}
If I only think of the sheet triggered by swipeActions Edit button, the code below works perfectly. But I still need other sheets triggered by ToolBar buttons.
.sheet(item: self.$toBeEdit, content: { toBeEdit in
EditLanguageView( meLanguage: toBeEdit)
})
After more searching I realised it's not something about SwipeActions. This is actually similar to this question:
SwiftUI presenting sheet with Binding variable doesn't work when first presented
So I added an hidden Text after the ForEach loop and the problem solved.
ForEach(self.meLanguages, id: \.id) { ... }
if self.toBeEdit != nil {
Text("\(self.toBeEdit!.wrappedName)").hidden()
}

Strange issue of "Modifying state during view update, this will cause undefined behavior"

Goal: have a SwiftUI architecture where the "add new item" and "edit existing item" are solved by the same view (EditItemView). However, for some reason, when I do this, the runtime agent complains of "Modifying state during view update, this will cause undefined behavior".
This is the code I want to use, which ensures that the EDITING of the item and ADDING a new item are handled by the same EditItemView:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.navigationBarItems(trailing:
// THIS LINE TO ADD A NEW ITEM:
NavigationLink(destination: EditItemView(item: Data.singleton.createItem(name: "New item", value: 5.0))
{
Image(systemName: "plus")
}
)
}
}
It doesn't work, leading to the issue highlighted above. I am forced to separate the functionality for Edit and Add into two distinct Views, which then works:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.sheet(isPresented: $isPresented)
{
// FORCED TO USE SEPARATE VIEW
AddItemView { name, value in
_ = Data.singleton.createItem(name: name, value: value)
self.isPresented = false
}
}
.navigationBarItems(trailing: Button(action: { self.isPresented.toggle()}) { Image(systemName: "plus")})
}
}
I don't understand why the code in the first version is considered to modify the state while updating view, because to me, it's sequential: new Item is created and THEN a view is shown for that Item.
Any ideas?
The destination of NavigationLink is not rendered lazily, meaning it'll get rendered when the NavigationLink itself is rendered -- not when clicked through.
The sheet code, depending on platform and SwiftUI version, may have the same issue, but apparently does not in the version you're using. Or, the closure you provide to AddItemView isn't run immediately -- since you didn't include the code, it's not clear.
To solve the issue in the first method, you can use the following SO answer which provides a lazy NavigationLink: https://stackoverflow.com/a/61234030/560942

List will be adjusted when add baritem in swift-ui/xcode12

I just try this code but I got unexpected edge for List.
If I remove navigationBarItems everything is ok.
import SwiftUI
let numbers: [String] = ["One", "Two", "Three"]
struct NavBarView: View {
var body: some View {
NavigationView {
List(numbers, id: \.self) { number in
SimpleRow(title: number)
}
.navigationBarTitle(Text("Numbers"), displayMode: .inline)
.navigationBarItems(trailing: Button(action: { // add this will affect the List position and size
}, label: {Image(systemName: "plus").imageScale(.medium)}
))
}
}
}
struct SimpleRow: View {
var title: String
var body: some View {
HStack {
Text(title)
Spacer()
}
}
}
Interesting behavior, as it is only when modifier applied directly to List... even can't say if it is a bug... Maybe it is because navigationBarItems has deprecated.
Anyway here is possible solution - attach navigation bar items to something else, like background.
Tested with Xcode 12 / iOS 14
struct NavBarView: View {
var body: some View {
NavigationView {
List(numbers, id: \.self) { number in
SimpleRow(title: number)
}
.background(Color.clear
.navigationBarItems(trailing: Button(action: {
}, label: {Image(systemName: "plus").imageScale(.medium)}
)))
.navigationBarTitle(Text("Numbers"), displayMode: .inline)
}
}
}

Swift Ui - Different View(Layout) for first item of a List

I am relatively new to Swift. I want to accomplish that the first item of the weatherList is displayed with a different View.
Unfortunately I get this error in those 2 Lines (VStack and at ForEach-Line). If i put away the if i == 0 then and just use for normal layout for all listitems there is no error.
ERROR: Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
Here is my code:
var body: some View {
NavigationView {
VStack { // here first error
if self.model.error != nil {
Text(self.model.error!)
}
ForEach(0..<self.model.weatherList.count, id: \.self) { i in // here second error
if i == 0 {
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}
else{
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}
}
}
.navigationBarTitle(Text("Weather in " + UserSettings.instance.locationSetting))
.navigationBarItems(trailing:
Button(action: {
self.model.reload()
}) {
Image(systemName: "arrow.clockwise")
}
.disabled(model.reloading)
)
}
}
Would appreciate each advice. Thanks.
I haven't tries this, but what if you wrap the NavigationLink like this:
if i == 0 {
AnyView(
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
})
} else {
AnyView(
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
})
}
It's a little tricky to know for sure without having the rest of your code to make this compile, but I mocked it up locally and it looks like all you need to change is:
ForEach(0..<self.model.weatherList.count, id: \.self) { i in
to
ForEach(0..<self.model.weatherList.count) { i in // remove the id: argument
Explanation:
If you're using a range to iterate through your array, you don't use the id: argument. The range is automatically identifiable. The ForEach(_:id:content:) initialiser is used for cases where you might say
ForEach(self.model.weatherlist, id: \.self) { model in
SomeView(model: model)
}
Does that compile for you?
Edit: The example you've given looks like it will do the same thing whether the index is 0 or not. Is there a reason why it doesn't work if you try:
ForEach(0..<self.model.weatherList.count) { i in
NavigationLink(destination: DetailView(model: self.model.weatherList[i])) {
RowView(model: self.model.weatherList[i])
}
}

SwiftUI NavigationLink Hide Arrow

Is there a way to hide the arrow to the right of the navigation link view that is automatically added?
I want to show an image grid using NavigationView -> List -> HStack -> NavigationLink_1 - NavigationLink_2
The NavigationLinks have arrows and it looks weird
The way it worked for me:
List {
ForEach(elements) { element in
ZStack {
CustomView(element: element)
NavigationLink(destination: DestinationView()) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
}
}
The easiest way I've found is to place the navigation in the .background modifier with the opacity of zero:
List {
Text("The cell")
.background( NavigationLink("", destination: Text("The detail view")).opacity(0) )
}
And with this solution you don't loose the dynamic height functionality of the cells.
I got it done with this
NavigationLink(destination: DestinationView()) {
EmptyView()
}
.frame(width: 0, height: 0)
.hidden()
#State var selection: Int? = nil
var body: some View {
let navigation = NavigationLink(destination: Text("View"), tag: 1, selection: $selection) { EmptyView() }
return
VStack {
navigation
Text("Tap").onTapGesture { self.selection = 1 }
}
}
The only thing that helped me is to add .opacity(0) to NavigationLink like so:
List {
ForEach(elements) { element in
ZStack {
CustomView(element: element)
NavigationLink(destination: DestinationView()),
label: {}).opacity(0)
}
}
}
List {
ForEach(elements) { element in
ZStack {
CustomView(element: element)
NavigationLink(destination: DestinationView()) {
EmptyView()
}.opacity(0.0)
}
}
}
Setting .opacity(0) on the NavigationLink seems to be the most reliable solution for me because I noticed that it might show the indicators again when messing with the .listStyle property. You will also not lose the highlighted effect.
var body: some View {
NavigationView {
List {
ForEach(items) { item in
ZStack(alignment: .leading) {
NavigationLink(destination: EmptyView()) {
EmptyView()
}
.opacity(0)
Text(item.value)
}
}
}
}
}
This is what worked for me, just adding an empty NavigationLink in a ZStack
List(viewModel.items, id: \.id) { item in
ZStack {
NavigationLink(destination: Destination()) {}
CustomView(item: item)
}
}
Only this worked for me, when I tried to implement button tap inside row in List:
ZStack {
NavigationLink(destination: FlightBoardInformation(flight: flight), tag: FlightBoardNavigation.toFlightDetailed, selection: $navigation) {
EmptyView()
}
.frame(width: 0, height: 0)
.hidden()
.disabled(true)
Button(action: {
self.navigation = .toFlightDetailed
}) {
Text("\(self.flight.airline) \(self.flight.number)")
}.buttonStyle(PlainButtonStyle())
}
Although .background(...).opacity(0) works, in a more complex view it expands itself through all the view and conflicts with other elements like buttons.
If you need it inside a List, what worked for me is also marking the NavigationLink as .disabled(true):
Text(...)
.background( NavigationLink(...).opacity(0).disabled(true) )
Use .background modifier.
ForEach(elements) { e in
AnyViewYouWantToShow(e)
.background(
NavigationLink("", destination: DestinationView()))
.opacity(0)
)
}
Finally found out a way how to avoid the the chevron without doing some tricky ZStacks and other solutions. The only downside is that this is only tested on iOS 16 with the new NavigationPath + NavigationStack.
Instead of using a regular NavigationLink where you apply the hashable object, you'll just use a regular Button and append the object to the NavigationPath.
Example:
#State private var path = NavigationPath()
var body: some View {
List {
ForEach(viewModel.customers) { customer in
Button {
path.append(customer)
} label: {
CustomerCell(customer: customer)
}
}
}
.navigationDestination(for: Customer.self) { customer in
CustomerView(customer: customer)
}
}
For projects using the NavigationBackport (for preparing the new navigation), it might work as well. As you can use NBNavigationPath and append the object to the path with a Button just like the example above.
The best workaround for me is using background:
NavigationLink(...) {}
.opacity(0)
.background(
HStack {
Text("Your custom view without arrow")
}
)
Or if you need dynamic height as #turingtested posted use NavigationLink as background
Text("Your custom view without arrow")
.background(NavigationLink( ... ) {}.opacity(0))
though there is lots of solution. I'm posting my one.
var body: some View {
VStack{
List{
ForEach (items){item in
switch item.onClick {
//For SettingsOverviewView
case .Settings:
ZStack{
NavigationLink (destination: SettingsMenuView(item: item)){
EmptyView()
}
.opacity(0.0)
.buttonStyle(PlainButtonStyle())
//some views that you will show as your listItem
HStack {
Text(item.name)
.font(.body)
Spacer()
}
}
}
}
.listStyle(GroupedListStyle())
}
}
}
A lot of examples playing around with ZStack and .opacity but for my opinion SwiftUI can offer more elegant solution using NavigationLink with isActive parameter that works perfect with .listRowSeparator or .listStyle modificators:
struct HidingNavArrowInList: View {
let planets = ["Mars", "Sun", "Mercury", "Venus", "Jupiter", "Uranus", "Saturn", "Earth"]
#State var selectedPlanet: String?
#State var showDetailView = false
var body: some View {
NavigationView {
List {
ForEach(planets, id: \.self) { planet in
Text(planet)
.onTapGesture {
segue(planet: planet)
}
}
}
.background(
NavigationLink(isActive: $showDetailView, destination: {
if let unwrappedPlanet = selectedPlanet {
VStack {
Text("This is detail view of \(unwrappedPlanet)")
}
}
}, label: {
EmptyView()
})
)
}
}
private func segue(planet: String) {
selectedPlanet = planet
showDetailView.toggle()
}
}
I've also struggled with this recently and I think I've found a solution by using a custom view for the navigation link (it works for me):
struct CustomNavigationLink<D: View, L: View>: View {
#ViewBuilder var destination: () -> D
#ViewBuilder var label: () -> L
#State private var isActive = false
var body: some View {
Button {
withAnimation {
isActive = true
}
} label: {
label()
}
.onAppear {
isActive = false
}
.overlay {
NavigationLink(isActive: $isActive) {
destination()
} label: {
EmptyView()
}
.opacity(0)
}
}
}
And you use like this:
CustomNavigationLink {
SomeViewHere()
} label: {
Text("hello world")
}
2023 Update
This simple solution works for me:
ZStack {
CustomCell()
NavigationLink(destination: DetailView()) {
EmptyView()
}
.opacity(0)
}
You can also do like:
This worked for me,
#State var boolValue: Bool = false
HStack {
Text("Your text")
Toggle(isOn: $boolValue){
Text("")
}
if boolValue {
NavigationLink(destination: DestinationView()) {
EmptyView()
}.frame(width: 0)
}
}
It also works with any View (not only Text)
ZStack {
Text("Some text")
NavigationLink(destination: Text("Hello")) {
EmptyView()
}.frame(width: 0)
}
I set the opacity of the navigationLink to zero and it work like a charm
NavigationLink(
destination: Text("Destination"),
label: {}
).opacity(0)