I'm trying to build a reusable navigation tab in SwiftUI and I'm facing some challenges.
I come from ReactJS and wanted to create a component that I can pass the tab images and the different views.
This is the code I have so far which works pretty well:
struct Navigation<Content: View>: View {
#State var selectedIndex = 0
var tabBarImageNames: [String]
let content: [Content]
init(tabBarImageNames: [String], _ content: Content...) {
self.content = content
self.tabBarImageNames = tabBarImageNames
}
var body: some View {
VStack{
ZStack {
content[selectedIndex]
}
Spacer()
HStack {
ForEach(0 ..< tabBarImageNames.count) { tabNum in
[ ... ]
}
}
}
}
}
I can call Navigation with these arguments an everything works as expected:
Navigation(
tabBarImageNames: ["house.fill", "chart.bar.xaxis", "target", "bubble.left"],
VStack {
Text("First view")
.navigationTitle("First Tab")
},
VStack {
Text("Second view")
.navigationTitle("Second Tab")
},
VStack {
Text("Third view")
.navigationTitle("Third Tab")
},
VStack {
Text("Fourth view")
.navigationTitle("Fourth Tab")
}
)
The real problem 🚨
The issue with the above code is that if the Views I'm passing as arguments are not exactly the same (when I say exactly I literally mean exactly the same type and number of components), Xcode complains with the following error:
I'm pretty sure it's something not very difficult to solve, but given my short experience with this language I'm struggling to find the right answer. I tried to change the init content protocol to AnyView instead of Content among other things, but no luck so far.
Looking for help 🗜
Anyone with SwiftUI experience that can help me out and explain me what I'm missing? Also open to feedback on improvements on the code.
Can't use any type of library or external package btw, everything has to be built manually.
Thanks Stack overflow!
Related
Does anyone have a fix for this? I have a Picker in my List that doesn't respond to any user input and is greyed out, but if I move it out of the List and into a VStack it functions normally.
The majority of answers I've found to this question all say that the Picker needs to be in a Form to work (which I don't understand since it works fine in a VStack), or that its a bug in which it only works on a physical device, which I've also tested on and gotten the same result.
I'll provide a screenshot as well as the code I'm using below
import SwiftUI
struct SettingsView: View {
#Binding var age : Int
var body: some View {
VStack(alignment: .leading){
List{
Picker("Your age", selection: $age) {
ForEach(1...100, id: \.self) { number in
Text("\(number)")
}
}
}
}
}
You must embed your List inside a NavigationView, so you can actually "navigate" from "Your age" to the list of Ints.
Just add one additional layer, as shown below:
var body: some View {
VStack(alignment: .leading){
NavigationView { // This is needed to navigate to the ForEach
List {
Picker("Your age", selection: $age) {
ForEach(1..<101) { number in
Text("\(number)")
}
}
}
}
}
}
I have question about returning NavigationView and List inside of SwiftUI body
There are code snippets like this:
var body: some View {
let trailingItem = HStack {
Button(action: { print("Button 1") }) {
Image(systemName: "bell")
}
Button(action: { print("Button 2") }) {
Image(systemName: "square.and.arrow.up")
}
}
return NavigationView {
Image("SwiftUI")
.navigationBarItems(trailing: trailingItem)
.navigationBarTitle("Title")
}
}
and this
var body: some View {
let numbers = [1, 2, 3, 4, 5]
return List(numbers, id: \.self) {
Text("\(String(describing: $0))")
}
}
In these two code snippets, I can find return keyword for NavigationView and List
To figure out what does that grammar do, I tried deleting return keyword but nothing happened
I want to know:
What does that return keyword do?
What is difference between using return for NavigationView and List
Thanks in advance for any help you are able to provide.
Some of the features in SwiftUI's syntax are made possible by something called a View Builder which is a type of Result Builder
When using a View Builder, functions and computed properties have an implicit return (ie the last statement is returned even without the return keyword). There are also other specific rules about what kinds of implicit code can be included in a View Builder, but let declarations like your examples contain are fine. See https://swiftwithmajid.com/2019/12/18/the-power-of-viewbuilder-in-swiftui/ and https://swiftontap.com/viewbuilder for more information.
SwiftUI Views are an interesting special case because the var body property of the View is interpreted to be a ViewBuilder even though we don't have to explicitly annotate it with #ViewBuilder. However, if you were to try the same thing in a regular function, you would need to use that #ViewBuilder to get a similar result if the function had more than one statement (one-line functions in Swift have implicit returns as well).
#ViewBuilder func myView() -> some View {
Text("test")
Text("Another item")
}
So, in your code examples (since you were in var body), there is no difference between using return and omitting it.
I'm using TabView with PageTabViewStyle, and each child view comprises a list view with a large data set.
Only on iOS 14.2, the page transitions seem to be very laggy.
However, page transitions are not delayed in list views with a small amount of data.
It's my guess that the performance of TabView comprises list would be independent of the amount of data, because of the list row display is lazy.
So, I believe it is bugs or default view style changes.
I look forward to your help to solve this problem. Thank you
#available(iOS 14.0, *)
struct ContentView: View {
#State var showHeart: Bool = false
var body: some View {
TabView{
self.contents
self.contents
}
.tabViewStyle(PageTabViewStyle())
}
var contents: some View{
List(0..<1000){_ in
Text("HELLO WORLD HELLOWORLD")
}
}
}
I have been playing with this and just a discovery - when you use TabView() it is laggy, but if you add a binding passed as TabView(selection: $selection) and just don't do anything with the selection binding it somehow doesn't lag anymore? Hacky, but a solution.
Try using lazy loading. Something like this: https://www.hackingwithswift.com/quick-start/swiftui/how-to-lazy-load-views-using-lazyvstack-and-lazyhstack
As you can see in the video: https://streamable.com/7sls0w
the List is not properly optimized. Create your own list, using LazyVStack. Much better performance, much smoother transition to it.
I don't think you understood the idea. Code to solve the issue:
#State var showHeart: Bool = false
var body: some View {
TabView {
contents
contentsSecond
}
.tabViewStyle(PageTabViewStyle())
}
var contents: some View {
List(0..<10000) { _ in
Text("HELLO WORLD HELLOWORLD")
}
}
var contentsSecond: some View {
return ScrollView {
Divider()
LazyVStack {
ForEach(1...1000, id: \.self) { value in
Text("Luke, I am your father \(value)")
.padding(.all, 5)
Divider()
}
}
}
}
I updated to iOS 14.2 yesterday and have the same issue (using Scrollview instead of List btw). I believe this is a bug.
One possible Workaround is to fallback to UIKits PageViewController by using UIViewControllerRepresentable as shown in the accepted answer here:
How can I implement PageView in SwiftUI?
This has solved the lagginess problem.
I am new to iOS and SwiftUI - why does NavigationView even exist? Why isn't there just NavigationLinks whenever we need to link somewhere?
And why does NavigationView have to be the top level view? Semantically it just doesn't make sense as the top level View should be VStack, Zstack etc
Currently:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: DetailView()) {
Text("Do Something")
}
}
}
}
}
Should be:
struct ContentView : View {
var body: some View {
VStack {
Text("Hello World")
NavigationView {
NavigationLink(destination: DetailView()) {
Text("Do Something")
}
}
}
}
}
Any idea?
SwiftUI gives you a whole bunch of conveniences practically for free.
One of those is an interface building block called NavigationView.
That interface building block has some convenient features and abilities, such as giving you automatic navigation interface (like back buttons to the original View in the navigation bar of the view you followed via a NavigationLink, etc).
Most of those conveniences simply wouldn’t make sense if it wasn’t the top level of your view.
Is there something you wanted from NavigationView at a lower level? I can’t imagine what, but I suspect there’s another SwiftUI building block that will achieve it, made specifically for the context.
After clicking the picker it navigates to the select view. The item list is rendered too far from the top, but snaps up after the animation is finished. Why is this happening?
Demo: https://gfycat.com/idioticdizzyazurevase
I already created a minimal example to rule out navigation bar titles and buttons, form sections and other details:
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
This happens in preview mode, simulator and on device (Xcode 11.2, iOS 13.2 in simulator, 13.3 beta 1 on device).
The obviously buggy behavior can be worked around when forcing the navigation view style to stacked:
NavigationView {
…
}.navigationViewStyle(StackNavigationViewStyle())
This is a solution to my problem, but I won‘t mark this as accepted answer (yet).
It seems to be a bug, even if it may be triggered by special circumstances.
My solution won‘t work if you need another navigation view style.
Additionally, it won‘t fix the horizontal repositioning mentioned by DogCoffee in the comments.
In my opinion, it has something to do with the navigation bar. In default (no mention of .navigationBarTitle extension), the navigation display mode is set to .automatic, this should be amend to .inline. I came across another post similar to this and use their solution to combine with yours, by using .navigationBarTitle("", displayMode: .inline) should help.
import SwiftUI
struct NewProjectView: View {
#State var name = ""
var body: some View {
NavigationView {
Form {
Picker("Client", selection: $name) {
Text("Client 1")
Text("Client 2")
}
}
.navigationBarTitle("", displayMode: .inline)
}
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Until this bug is resolved another way to work around this issue while retaining the DoubleColumnNavigationViewStyle for iPads would be to conditionally set that style:
let navView = NavigationView {
…
}
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(navView.navigationViewStyle(DoubleColumnNavigationViewStyle()))
} else {
return AnyView(navView.navigationViewStyle(StackNavigationViewStyle()))
}
Thanks for this thread everyone! Really helped me understand things more and get a hold of one of my problems. To share with others, I was having this problem to but I was also having this problem when I set a section to appear in a if/else statement set on a section with a toggle. When the toggle was activated it would shift the section header horizontally a few pixels.
The following is how I fixed it
Section(header: Text("Subject Identified").listRowInsets(EdgeInsets()).padding(.leading)) {
Picker(selection: $subIndex, label: Text("Test")) {
ForEach(0 ..< subIdentified.count) {
Text(self.subIdentified[$0]).tag($0)
}
}
.labelsHidden()
.pickerStyle(SegmentedPickerStyle())
I'm still having horizontal shift on my picker selection view and not sure how to fix. I created another thread to received input. Thanks again! SwiftUI Shift Picker Text Horizontal