SwiftUI - Convert a scrollable TabBar into a still TabBar - swift

currently having an issue with a tutorial I followed which creates an underlined tab bar with SwiftUI. At the current stage, the tab bar scrolls with a scrollview when a view is changed, however, instead of this happening I want the buttons to remain idle in their place as a normal (still functioning) underlined tab bar. This is currently what it looks like when a view is swiped across:
Notice how when a tab changes, instead of remaining still within the tab bar, the labels scroll over. I have tried removing the scroll view of the tab bar but just can't seem to work out how to get it to remain fixed within the screen.
The code from the tutorial looks like this:
import SwiftUI
struct ContentView: View {
#State var currentTab: Int = 0
var body: some View {
ZStack(alignment: .top) {
TabView(selection: self.$currentTab) {
View1().tag(0)
View2().tag(1)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.all)
TabBarView(currentTab: self.$currentTab)
}
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["View 1", "View 2"]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.padding(.horizontal)
}
.background(Color.white)
.frame(height: 80)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
Color.black
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.buttonStyle(.plain)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Help resolving this would be greatly appreciated! Thanks!!

Try this code. This should sole your problem. The main issue was that you used a ZStack instead of a VStack. After that, just fiddle around with the spacing and positioning of the elements you build in your VStack.
Let me know if it creates the result you want.
import SwiftUI
struct ContentView: View {
#State var currentTab: Int = 0
var body: some View {
VStack(alignment: .center, spacing: 0) {
TabBarView(currentTab: self.$currentTab)
TabView(selection: self.$currentTab) {
Text("This is view 1").tag(0)
Text("This is view 2").tag(1)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.background(Color.secondary) // <<<< Remove
}
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["View 1", "View 2"]
var body: some View {
HStack(spacing: 20) {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.padding(.horizontal)
.background(Color.orange) // <<<< Remove
.frame(height: 80)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
Color.black
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.buttonStyle(.plain)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Related

Binding a button leads to "Missing argument for parameter in call" error

I'm trying to create a Binding in two views so I can change something on one side and have it reflected on the other.
I basically have:
a circle on both views
a button to change the other view's circle color
and one to go to the other view
It all works fine if I only have a Binding in the "ColorChange2"
view, but when I add a Binding in "ColorChange1" I get into trouble.
It tells me: Missing argument for parameter 'isOn2'.
But when I add isOn2 into ColorChange1() it wants a binding, but if I do ColorChange1(isOn2: $isOn2) it says it can't find '$isOn2' in scope.
I found one solution suggesting to add .constant(true)) into the preview but since it's a constant, it wont change the view like I wanted since it's a constant.
What can I do to make it work?
Code:
struct ColorChange1: View {
#State private var isOn = false
#Binding var isOn2 : Bool
var body: some View {
NavigationView {
VStack {
Circle()
.fill(isOn ? .green : .red)
.frame(width: 100)
Button(action: {
isOn2.toggle()
}, label: {
Text("Change button view 2")
.padding()
})
NavigationLink(destination: {
ColorChange2(isOn: $isOn)
}, label: {
Text("Go to view 2")
})
}
}
}
}
struct ColorChange2: View {
#Binding var isOn : Bool
#State private var isOn2 = false
#Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Circle()
.fill(isOn2 ? .green : .red)
.frame(width: 100)
Button(action: {
isOn.toggle()
}, label: {
Text("Change button view 1")
.padding()
})
Button(action: {
dismiss.callAsFunction()
}, label: {
Text("Go to view 1")
})
}
.navigationBarBackButtonHidden(true)
}
}
struct ColorChange_Previews: PreviewProvider {
static var previews: some View {
// ColorChange(isOn2: .constant(true))
ColorChange1()
}
} ```
You don't need both #Binding value in both screen to connect between screen like that.
#Binding means that get the value in #State of the first view and make a connection in the second view. In this scenero, when you go back from second view, it was dismissed.
For your problem, make an ObservableObject to store value. 1 for present in first view and 1 for second view. Then add it to second view when ever you need to display.
Code will be like this
class ColorModel : ObservableObject {
#Published var isOnFirstView = false
#Published var isOnSecondView = false
func didTapChangeColor(atFirstView: Bool) {
if atFirstView {
isOnSecondView = !isOnSecondView
} else {
isOnFirstView = !isOnFirstView
}
}
}
struct ColorChange2: View {
// binding model
#ObservedObject var colorModel : ColorModel
#Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Circle()
.fill(colorModel.isOnSecondView ? .green : .red)
.frame(width: 100)
Button(action: {
colorModel.didTapChangeColor(atFirstView: false)
}, label: {
Text("Change button view 1")
.padding()
})
Button(action: {
dismiss.callAsFunction()
}, label: {
Text("Go to view 1")
})
}
.navigationBarBackButtonHidden(true)
}
}
struct ColorChange1: View {
#StateObject private var colorModel = ColorModel()
var body: some View {
NavigationView {
VStack {
Circle()
.fill(colorModel.isOnFirstView ? .green : .red)
.frame(width: 100)
Button(action: {
colorModel.didTapChangeColor(atFirstView: true)
}, label: {
Text("Change button view 2")
.padding()
})
NavigationLink(destination: {
ColorChange2(colorModel: colorModel)
}, label: {
Text("Go to view 2")
})
}
}
}
}
struct ColorChange_Previews: PreviewProvider {
static var previews: some View {
ColorChange1()
}
}

Swift ui custom tabView breaking with navigationView

Having difficulties with my tabView, (which has been custom made to accommodate my gridView which allows picture buttons taking me to a new display). Displaying a blank View with "back" button on launch. Then after selecting tab 2 and coming back to tab 1, displays the screen in full detail. After some testing the navigationlink is the cause but i'm confused on the resolution.
Within my main build the navigationTitle sits mid centre the screen and when scrolled greys out a large sections. I believe this is due to there being multiple navigationlinks/views creating a rectangle block where the navigationlView/Links are sitting on top of each other.
TabView
struct headerView: View {
#State var currentTab: Int = 0
var body: some View {
VStack {
TabBarView(currentTab: self.$currentTab)
.padding(10)
TabView(selection: self.$currentTab) {
grid().tag(0)
tabView2().tag(1)
tabView3().tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.all)
}
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["Test 1", "Test 2", "Test 3"]
var body: some View {
HStack {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.frame(width: (UIScreen.main.bounds.width / 1.25) - 1)
.background(Color.white)
.frame(height: 50)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
Color.black
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.buttonStyle(.plain)
}
}
TabView2
struct tabView2: View {
var body: some View {
VStack {
Text("Hello")
}
}
}
imageGrid
enum ImageEnum: String, CaseIterable { //Please find a better name ;)
case image1, image2
var imageName: String { // get the assetname of the image
switch self {
case .image1:
return "image1"
case .image2:
return "image2"
}
}
#ViewBuilder
var detailView: some View { // create the view here, if you need to add
switch self { // parameters use a function or associated
case .image1: // values for your enum cases
TestView1()
case .image2:
TestView2()
}
}
}
struct grid: View {
var columnGrid: [GridItem] = [GridItem(.flexible(), spacing: 25), GridItem(.flexible(), spacing: 25)]
var body: some View {
NavigationView{ // Add the NavigationView
LazyVGrid(columns: columnGrid, spacing: 50) {
ForEach(ImageEnum.allCases, id:\.self) { imageEnum in // Iterate over all enum cases
NavigationLink(destination: imageEnum.detailView){ // get detailview here
Image(imageEnum.imageName) // get image assset name here
.resizable()
.scaledToFill()
.clipped()
.cornerRadius(25)
}
}
}
}
}
}
TestView
struct TestView1: View{
var body: some View{
Text("test1")
}
}
struct TestView2: View{
var body: some View{
Text("test2")
}
}

SwiftUI - OnExitCommand inside TabView

I have lately been trying to make a tvOS app, but have run into the following rather annoying problem. I can't use navigation inside a TabView and still have the menu button on the remove take me back to the previous state.
struct TestView: View {
#State var selection : Int = 0
var body: some View {
TabView(selection: self.$selection) {
ExpView()
.tabItem {
HStack {
Image(systemName: "magnifyingglass")
Text("Explore")
}
}
.tag(0)
}
}
}
struct ExpView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "Hey")) {
Text("Detail")
}
}
}
}
struct DetailView: View {
var title : String
var body: some View {
VStack {
Text(title)
}
}
}
My question is: Is there any way to enable the menu button to go back to the previous view in the hierachy without dismissing the app completely?
You don't need to call dismiss on Menu it is called automatically for NavigationLink (so calling one more dismiss quits to main menu)
Here are fixed views. Tested with Xcode 11.4
struct ExploreView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(title: "Hey")) {
Text("Detail")
}
}
}
}
struct DetailView: View {
var title : String
var body: some View {
VStack {
Text(title)
}
}
}
So I found a workaround for the issue.
If you place the navigationView outside the TabView and then use the following code it works:
struct TestView: View {
#State var selection : Int = 0
#State var hideNavigationBar : Bool
var body: some View {
NavigationView {
TabView(selection: self.$selection) {
ExpView(hideNavigationBar: self.$hideNavigationBar)
.tabItem {
HStack {
Image(systemName: "magnifyingglass")
Text("Explore")
}
}
.tag(0)
}
}
}
}
struct ExpView: View {
#Binding var hideNavigationBar : Bool
var body: some View {
NavigationLink(destination: DetailView(title: "Hey")) {
Text("Detail")
}.navigationBarTitle("")
.navigationBarHidden(self.hideNavigationBar)
.onAppear {
self.hideNavigationBar = true
}
}
}
struct DetailView: View {
var title : String
var body: some View {
VStack {
Text(title)
}
}
}

Swiftui navigationLink macOS default/selected state

I build a macOS app in swiftui
i try to create a listview where the first item is preselected. i tried it with the 'selected' state of the navigationLink but it didn't work.
Im pretty much clueless and hope you guys can help me.
The code for creating this list view looks like this.
//personList
struct PersonList: View {
var body: some View {
NavigationView
{
List(personData) { person in
NavigationLink(destination: PersonDetail(person: person))
{
PersonRow(person: person)
}
}.frame(minWidth: 300, maxWidth: 300)
}
}
}
(Other views at the bottom)
This is the normal View when i open the app.
When i click on an item its open like this. Thats the state i want as default opening state when i render this view.
The Code for this view looks like this:
//PersonRow
struct PersonRow: View {
//variables definied
var person: Person
var body: some View {
HStack
{
person.image.resizable().frame(width:50, height:50)
.cornerRadius(25)
.padding(5)
VStack (alignment: .leading)
{
Text(person.firstName + " " + person.lastName)
.fontWeight(.bold)
.padding(5)
Text(person.nickname)
.padding(5)
}
Spacer()
}
}
}
//personDetail
struct PersonDetail: View {
var person : Person
var body: some View {
VStack
{
HStack
{
VStack
{
CircleImage(image: person.image)
Text(person.firstName + " " + person.lastName)
.font(.title)
Text("Turtle Rock")
.font(.subheadline)
}
Spacer()
Text("Subtitle")
.font(.subheadline)
}
Spacer()
}
.padding()
}
}
Thanks in advance!
working example. See how selection is initialized
import SwiftUI
struct Detail: View {
let i: Int
var body: some View {
Text("\(self.i)").font(.system(size: 150)).frame(maxWidth: .infinity)
}
}
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack(alignment: .top) {
NavigationView {
List {
ForEach(0 ..< 10) { (i) in
NavigationLink(destination: Detail(i: i), tag: i, selection: self.$selection) {
VStack {
Text("Row \(i)")
Divider()
}
}
}.onAppear {
if self.selection != nil {
self.selection = 0
}
}
}.frame(width: 100)
}
}.background(Color.init(NSColor.controlBackgroundColor))
}
}
screenshot
You can define a binding to the selected row and used a List reading this selection. You then initialise the selection to the first person in your person array.
Note that on macOS you do not use NavigationLink, instead you conditionally show the detail view with an if statement inside your NavigationView.
If person is not Identifiable you should add an id: \.self in the loop. This ressembles to:
struct PersonList: View {
#Binding var selectedPerson: Person?
var body: some View {
List(persons, id: \.self, selection: $selectedPerson) { person in // persons is an array of persons
PersonRow(person: person).tag(person)
}
}
}
Then in your main window:
struct ContentView: View {
// First cell will be highlighted and selected
#State private var selectedPerson: Person? = person[0]
var body: some View {
NavigationView {
PersonList(selectedPerson: $selectedPerson)
if selectedPerson != nil {
PersonDetail(person: person!)
}
}
}
}
Your struct person should be Hashable in order to be tagged in the list. If your type is simple enough, adding Hashable conformance should be sufficient:
struct Person: Hashable {
var name: String
// ...
}
There is a nice tutorial using the same principle here if you want a more complete example.
Thanks to this discussion, as a MacOS Beginner, I managed a very basic NavigationView with a list containing two NavigationLinks to choose between two views. I made it very basic to better understand. It might help other beginners.
At start up it will be the first view that will be displayed.
Just modify in ContentView.swift, self.selection = 0 by self.selection = 1 to start with the second view.
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
Text("(1) Hello, I am the first view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
import SwiftUI
struct SecondView: View {
var body: some View {
Text("(2) Hello, I am the second View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack() {
NavigationView {
List () {
NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
Text("Click Me To Display The First View")
} // End Navigation Link
NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
Text("Click Me To Display The Second View")
} // End Navigation Link
} // End list
.frame(minWidth: 350, maxWidth: 350)
.onAppear {
self.selection = 0
}
} // End NavigationView
.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
} // End HStack
} // End some View
} // End ContentView
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Result:
import SwiftUI
struct User: Identifiable {
let id: Int
let name: String
}
struct ContentView: View {
#State private var users: [User] = (1...10).map { User(id: $0, name: "user \($0)")}
#State private var selection: User.ID?
var body: some View {
NavigationView {
List(users) { user in
NavigationLink(tag: user.id, selection: $selection) {
Text("\(user.name)'s DetailView")
} label: {
Text(user.name)
}
}
Text("Select one")
}
.onAppear {
if let selection = users.first?.ID {
self.selection = selection
}
}
}
}
You can use make the default selection using onAppear (see above).

SwiftUI: Initial list element's position is wrong

Im new to Swift and I have a question regarding the list view.
The code below creates a List of an image as a button followed by a text.
The problem is that the List elements are initially slightly left, but they move to the right into place when they're first updated.
import SwiftUI
struct TodoItem {
var name: String
var completed: Bool
init(_ name: String) {
self.name = name
self.completed = false
}
}
struct ContentView: View {
#State var todos: [TodoItem] = [
TodoItem("This"),
TodoItem("Is"),
TodoItem("Some"),
TodoItem("Todo"),
TodoItem("Task")
]
var body: some View {
NavigationView {
List(todos.indices) { index in
HStack {
Image(systemName: self.todos[index].completed ? "checkmark.circle" : "circle")
.imageScale(.large)
.onTapGesture {
self.todos[index].completed.toggle()
}
Text(self.todos[index].name)
Spacer()
}
}
.navigationBarTitle("Todos")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is fixed body (tested & works with Xcode 11.3.1)
var body: some View {
NavigationView {
List {
ForEach (todos.indices) { index in
HStack {
Image(systemName: self.todos[index].completed ? "checkmark.circle" : "circle")
.imageScale(.large)
.onTapGesture {
self.todos[index].completed.toggle()
}
Text(self.todos[index].name)
Spacer()
}
}.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
}
.navigationBarTitle("Todos")
}
}