SwiftUI List Separator within NavigationView Problem - swiftui

I'm not sure if this is well-known-issue or not but it is very odd. The problem can be reproduced with Apple's example code navigationBarItems(leading:trailing:) As you can see, list separators have extra leading space that look like they are indented for some reason.
Here is actual code:
import Foundation
import SwiftUI
import UIKit
import PlaygroundSupport
struct ContentView: View {
var body: some View {
NavigationView {
List {
Text("Chocolate")
Text("Vanilla")
Text("Strawberry")
}
.navigationBarTitle(Text("Today‘s Flavors"))
.navigationBarItems(leading:
HStack {
Button("Hours") {
print("Hours tapped!")
}
}, trailing:
HStack {
Button("Favorites") {
print("Favorites tapped!")
}
Button("Specials") {
print("Specials tapped!")
}
}
)
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
I test above code with Playground, iPhone 13/15.3.1 they are the same.
I did messing around the code and found that applying .navigationBarTitle(), .navigationBarItems() to List causes the problem. They must apply to each List item. Very odd though. This means almost all List sample code that wrapping with NavigationView are WRONG. Here is a fix I found.
import Foundation
import SwiftUI
import UIKit
import PlaygroundSupport
struct ContentView: View {
let contents = ["Chocolate", "Vanilla", "Strawberry"]
var body: some View {
NavigationView {
List {
ForEach (contents, id: \.self) { content in
Text(content)
.navigationBarTitle(Text("Today‘s Flavors"))
.navigationBarItems(leading:
HStack {
Button("Hours") {
print("Hours tapped!")
}
}, trailing:
HStack {
Button("Favorites") {
print("Favorites tapped!")
}
Button("Specials") {
print("Specials tapped!")
}
}
)
}
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
Although I'm not sure if I can call this is a bug but definitely either document or implementation is wrong. Could anyone explain this?

navigationBarItems will be deprecated in the future so you should use toolbar instead
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Hours") { }
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
Button("Favorites") { }
Button("Specials") { }
}
}
}
If you want to keep using navigationBarItems, there is a better work around by using other ListStyle
struct ContentView: View {
var body: some View {
NavigationView {
List {
Text("Chocolate")
Text("Vanilla")
}
.navigationBarTitle(Text("Today‘s Flavors"))
.navigationBarItems(
leading: Button("Hours") { }
)
.listStyle(PlainListStyle())
}
}
}
do not apply navigationBarTitle on every row for a work around, it's bad

You need to apply ANY (whatever you want) style to the List and then everything will be ok.

Related

SwiftUI .shadow blocks List scroll gesture for all but first List {}. Why?

if I place 2 or more Lists with items in VStack or HStack and give the Vstack .shadow attribute, only the first List is scrollable and receives "gesture" events. Anybody has an idea why and is this intended behaviour? Seems like a bug to me. Tried on Xcode and device with iOS 14 and 14.+
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
ListView1()
ListView2()
}
.shadow(radius: 5)
}
}
struct ListView1:View {
var values=["1","2","3"]
var body: some View {
List {
ForEach(values, id: \.self) { (value) in
Text("Value \(value)")
}
}
}
}
struct ListView2:View {
var values=["5","6","7"]
var body: some View {
List {
ForEach(values, id: \.self) { (value) in
Text("Value \(value)")
}
}
}
}
Use compositingGroup() for solving issue!
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
ListView1()
ListView2()
}
.compositingGroup() // <<: Here!
.shadow(radius: 5)
}
}

How can I use multiple fullScreenCover in IOS14

I want to present the two destinations view in full screen mode from a single view.
Below is a sample of my code. Seem that the function only works for single presentation, if I have a second fullScreenCover defined, the first fullScreenCover didn't work properly.Is that any workaround at this moment?
import SwiftUI
struct TesFullScreen: View {
init(game : Int){
print(game)
}
var body: some View {
Text("Full Screen")
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
.fullScreenCover(isPresented: self.$showFullScreen1){
TesFullScreen(game: 1)
}
.fullScreenCover(isPresented: self.$showFullScreen2){
TesFullScreen(game: 2)
}
}
}
Not always the accepted answer works (for example if you have a ScrollView with subviews (cells in former days) which holds the buttons, that set the navigational flags).
But I found out, that you also can add the fullScreen-modifier onto an EmptyView. This code worked for me:
// IMPORTANT: Has to be within a container (e.g. VStack, HStack, ZStack, ...)
if myNavigation.flag1 || myNavigation.flag2 {
EmptyView().fullScreenCover(isPresented: $myNavigation.flag1)
{ MailComposer() }
EmptyView().fullScreenCover(isPresented: $myNavigation.flag2)
{ RatingStore() }
}
Usually some same modifier added one after another is ignored. So the simplest fix is to attach them to different views, like
struct FullSContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
.fullScreenCover(isPresented: self.$showFullScreen1){
Text("TesFullScreen(game: 1)")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
.fullScreenCover(isPresented: self.$showFullScreen2){
Text("TesFullScreen(game: 2)")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
}
}
Alternate is to have one .fullScreenCover(item:... modifier and show inside different views depending on input item.
The only thing that worked for me was the answer in this link:
https://forums.swift.org/t/multiple-sheet-view-modifiers-on-the-same-view/35267
Using the EmptyView method or other solutions always broke a transition animation on one of the two presentations. Either transitioning to or from that view and depending on what order I chose them.
Using the approach by Lantua in the link which is using the item argument instead of isPresented worked in all cases:
enum SheetChoice: Hashable, Identifiable {
case a, b
var id: SheetChoice { self }
}
struct ContentView: View {
#State var sheetState: SheetChoice?
var body: some View {
VStack {
...
}
.sheet(item: $sheetState) { item in
if item == .a {
Text("A")
} else {
Text("B")
}
}
}
}
The sheetState needs to be optional for it to work.

Corrupted Navigation Views

I'm pretty sure this is a bug in SwiftUI, but I wondered if anyone has encountered it and figured out a workaround. My normal use case is to have a search field appear, but I've simplified it to the point where a simple text string exhibits the bug.
Create a single-view app, copy this into ContentView, and run it. Tap the search icon twice, then scroll the view; you'll see the text scrolling UNDER the title.
import SwiftUI
struct ContentView: View {
private var items = (0 ... 50).map {String($0)}
#State private var condition = false
var searchButton: some View {
Button(action: {self.condition.toggle()}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}
}
var body: some View {
NavigationView {
VStack {
if condition {
Text("Peekaboo")
}
List {
ForEach(items, id: \.self) {item in
HStack {
Text(item)
}
}
}
}
.navigationBarTitle("List of Items")
.navigationBarItems(leading: searchButton)
}
}
}
Maybe it is a bug, submit feedback to Apple, but currently this is how NavigationView behaves - it collapses navigation bar only if its top content is List/ScrollView/Form. So to solve the issue move your VStack either into a List or out of NavigationView
1)
var body: some View {
NavigationView {
List {
if condition {
Text("Peekaboo")
}
ForEach(items, id: \.self) {item in
2)
var body: some View {
VStack {
if condition {
Text("Peekaboo")
}
NavigationView {
List {
It seems that a View cannot cope with variable number of views.
A workaround this strange behavior is this:
import SwiftUI
struct ContentView: View {
private var items = (0 ... 50).map {String($0)}
#State private var condition = false
var searchButton: some View {
Button(action: {self.condition.toggle()}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}
}
var body: some View {
NavigationView {
VStack {
if condition {
Text("Peekaboo")
} else {
Text("")
}
// or use this Text(condition ? "Peekaboo" : "")
List {
ForEach(items, id: \.self) {item in
HStack {
Text(item)
}
}
}
}
.navigationBarTitle("List of Items")
.navigationBarItems(leading: searchButton)
}
}
}
Let me know if it works, if not let us know what device/system you are using. Tested with Xcode 11.6 beta, Mac 10.15.5, target ios 13.5 and mac catalyst.

SwiftUI NavigationButton without the disclosure indicator?

When making a List with a row that pushes to a new view, SwiftUI adds a disclosure indicator ">" automatically? How do I remove it if I don't want it?
NavigationView {
List {
NavigationButton(destination: DetailView()) {
ListItem()
}
}
.navigationBarTitle(Text("Some title"))
}
On a UITableViewCell you set Accessory to None but how do I do that in SwiftUI?
Setting the NavigationLink width and hiding it did the trick for me
List {
ForEach(pages) { page in
HStack(spacing: 0) {
Text("Something")
NavigationLink(destination: Text("Somewhere")) {
EmptyView()
}
.frame(width: 0)
.opacity(0)
}
}
}
Swift 5, Xcode 11. ZStack works perfect.
var body: some View {
NavigationView {
List {
ForEach(viewModel.currenciesViewModel) { cellViewModel in
ZStack {
cellViewModel.makeView()
NavigationLink(destination: ChooseCurrencyListView()) {
EmptyView()
}
.buttonStyle(PlainButtonStyle())
}
}
}
.navigationBarHidden(true)
.navigationBarTitle("", displayMode: .inline)
}
}
The easiest one. The content for each item in the list.
ZStack {
NavigationLink(destination: DetailView()) {
EmptyView()
}.hidden()
RowView()
}
As workaround I can suggest to add .padding modifier like this:
NavigationView {
List {
NavigationButton(destination: DetailView()) {
ListItem()
}
}
.navigationBarTitle(Text("Some title"))
}
.padding(.trailing, -32.0)
So you will get rows without visible disclosure:
You can also put it in the .background modifier:
List {
Text("Go to...")
.background(NavigationLink("", destination: Text("Detail View")))
}
If you already have the background modifier on the Text, you can wrap the Text in a HStack and apply background to the HStack.
What you can do, if you are using list, is setting the navigationlink to hidden and its frame width to zero.
HStack{
Button(action: {self.statusShow = 1}, label: {
Image(systemName: "info.circle")
})
NavigationLink(destination: StimulatorSettingView(),
tag: 1,
selection: self.$statusShow){
EmptyView()
}.hidden().frame(width: 0)
}
This worked for me.
As of beta 6, this works well:
struct SwiftUIView: View {
var body: some View {
NavigationView {
List {
HStack {
Text("My Cell Content")
NavigationLink(destination: Text("destination"), label: {
EmptyView()
})
}
}
}
}
}
You don't have to use NavigationLink to wrap your Label directly. It will work as long as the link is anywhere in your view hierarchy.
Here I've wrapped it in a button, which allows you to trigger an action prior to pushing the view. Since the NavigationLink has an EmptyView for the label the disclosure indicator is not visible. You can also style this with ButtonStyle.
struct NavigationButton<Destination: View, Label: View>: View {
var action: () -> Void = { }
var destination: () -> Destination
var label: () -> Label
#State private var isActive: Bool = false
var body: some View {
Button(action: {
self.action()
self.isActive.toggle()
}) {
self.label()
.background(NavigationLink(destination: self.destination(), isActive: self.$isActive) {
EmptyView()
})
}
}
}
And to use it:
NavigationButton(
action: { print("tapped!") },
destination: { Text("Pushed View") },
label: { Text("Tap me") }
)
NavigationLink is what we should define in a scope enclosed inside a NavigationView.
But when we use NavigationLink it is attached to the enclosing view, so to reuse the same NavigationLink with other views, we use tag which differentiates between different Destinations.
struct SwiftUIView: View {
#State private var viewState: Int? = 0
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("View 1"), tag: 1, selection: $viewState) {
EmptyView()
}
NavigationLink(destination: Text("View 2"), tag: 2, selection: $viewState) {
EmptyView()
}
Text("First View")
.onTapGesture {
self.viewState = 1
}
Text("Second View")
.onTapGesture {
self.viewState = 2
}
}
}
}
}
Here we bind a Hashable property with all the NavigationLinks present in our VStack so that when a particular View is tapped we can notify which Destination should be opened by setting the value of Bindable property.
If we don't notify the correct Destination by setting the value of tag, always the View defined inside the Closure of NavigationLink will be clickable and nothing else.
Using this approach you don't need to wrap all your clickable views inside NavigationView, any action on any view can use any NavigationLink just by setting the tag.
Thanks, hope this helps.
Works well for me!
import SwiftUI
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarkData) { landmark in
LandmarkRow(landmark: landmark)
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
EmptyView()
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone 11 Pro Max"], id: \.self) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
}
}
Use .frame(width: 0).opacity(0.0):
NavigationView {
List {
ForEach(options) {
option in
ZStack {
YourView(option: option)
NavigationLink(destination: ProductListView(),
label: {
EmptyView()
}).frame(width: 0).opacity(0.0)
}.listRowInsets(EdgeInsets())
}
}.navigationBarHidden(true)
}
My version of this solution is to make a view modifier. I think it's the cleanest way, as it doesn't use AnyView.
Note that this solution runs the init() for the destination when it draws the element the .navigationLink() is attached to.
Usage
Text("Link")
.navigationLink({
// put your destination here
})
How To
import SwiftUI
extension View {
func navigationLink<Destination: View>(_ destination: #escaping () -> Destination) -> some View {
modifier(NavigationLinkModifier(destination: destination))
}
}
fileprivate struct NavigationLinkModifier<Destination: View>: ViewModifier {
#ViewBuilder var destination: () -> Destination
func body(content: Content) -> some View {
content
.background(
NavigationLink(destination: self.destination) { EmptyView() }.opacity(0)
)
}
}
This helps to push and pass the model to the next navigation view controller.
struct ContentView : View {
#State var model = PostListViewModel()
var body: some View {
NavigationView {
List(model.post) { post in
ListCell(listData: post)
}.navigationBarTitle(Text("My Post"))
}
}
}
struct ListCell: View {
var listData: Post
var body: some View {
return NavigationButton(destination: DetailContentView(post: listData)) {
HStack {
ImageRow(model: listData) // Get image
VStack(alignment: .leading) {
Text(listData.login).font(.headline).lineLimit(nil)
Text(listData.url).font(.subheadline).lineLimit(nil)
}.padding(.leading, 10)
}.padding(.init(top: 5, leading: 0, bottom: 5, trailing: 0))
}
}
}
Here's a reusable "plain" navigation link view (i.e. without the chevron disclosure indicator) that can be a drop-in replacement for NavigationLink:
struct PlainNavigationLink<Label, Destination>: View where Label: View, Destination: View {
#ViewBuilder var destination: () -> Destination
#ViewBuilder var label: () -> Label
var body: some View {
label()
.background(
NavigationLink(destination: destination, label: {})
.opacity(0)
)
}
}
To use it, simply replace NavigationLink with PlainNavigationLink:
NavigationView { // or NavigationStack in iOS 16
List {
ForEach(1...30, id: \.self) { _ in
PlainNavigationLink {
Text("Hello, world!")
} label: {
Text("Hello, world!")
}
}
}
}
We can also extend it with convenience initializers for LocalizedStringKey and String, just like NavigationLink does.
just came here looking for the answer to this question, but none of the proposed solutions worked for me (can't have an empty view, because i want to put something in the list row; i'm already messing with the padding (and increasing trailing padding didn't seem to work) ... i was about to give up, and then something occurred to me: what if you crank up the z-index of the list row itself? seemed somewhat unlikely, but i gave it a try and, i'll be damned, it worked! i was so pleasantly surprised, i felt like sharing ...
e.g.:
// in body of your list row view
HStack(alignment: .top, spacing: 0.0) {
// stuff ...
}
.zIndex(9999999999)
If you need children behaviour for List and NavigationLink, without additional discloser in the same time, I want to promote this tricky solution, main point at HStack
var body: some View {
NavigationView {
List(items, children: \.items) { item in
ZStack {
NavigationLink(destination: DetailsView()) {
EmptyView()
}.hidden()
HStack {
RowView(item: item)
Spacer()
}
}
}
}
}
Once you put your button in a scrollview, the disclosure button will be hidden. Just make sure to disable your scroll indicator.
there is no documentation yet, so you can use ScrollView for now
NavigationView {
ScrollView {
ForEach(0...100){ x in
NavigationButton(destination: Text("ss")) {
HStack {
Text(String(x))
Spacer()
}
.padding()
.background(Color.white)
.shadow(radius:1,y:1)
}
}
.frame(width: UIScreen.main.bounds.width - 32)
.padding()
}
}
Removing List and just using ForEach works fine with navigation link. You just have to create your own list row. This works for me
NavigationView {
ForEach(pages) {
page in
NavigationLink(destination: DetailView()) {
ListItem()
}
}
}

SwiftUI animations differ if called from within a VStack or NavigationItem

I've got a very simple VStack, based directly off of one of Paul Hudson's excellent SwiftUI samples. There are two lines of Text, one hidden. There's a method to toggle an #State var which controls the hidden Text.
If I call that function from within the VStack, it animates properly. If I call it from a navigationBarItems, it loses the animation. Am I missing something about how views are composed?
struct ContentView: View {
#State var showDetails = false
func toggleDetails() { withAnimation { self.showDetails.toggle() } }
var body: some View {
NavigationView() {
VStack {
Button(action: { self.toggleDetails() }) { Text("Tap to show details") }
if showDetails { Text("Details go here.") }
}
.navigationBarTitle(Text("Nav Bar"))
.navigationBarItems(trailing:
Button(action: { self.toggleDetails() }) {
Text("Details")
})
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Beta 5 Update
It seems beta 5 fixed this problem. Workaround no longer needed.
Workaround for beta 4 and previous versions
I think the reason it does not work, is because you are calling withAnimation from a different branch of the view tree. The "Details" button and the views that need to be animated are on different branches of the hierarchy. I am just guessing, but it seems to be supported by the workaround I posted here.
If instead of using explicit animations (i.e., withAnimation), you use implicit animations on both the VStack and the Text, it works:
struct ContentView: View {
#State var showDetails = false
func toggleDetails() { self.showDetails.toggle() }
var body: some View {
NavigationView() {
VStack {
Button(action: { self.toggleDetails() }) { Text("Tap to show details") }
if showDetails {
Text("Details go here.").animation(.basic())
}
}
.animation(.basic())
.navigationBarTitle(Text("Nav Bar"))
.navigationBarItems(trailing:
Button(action: {
self.toggleDetails()
}) { Text("Details") })
}
}
}