Use SegmentedControl to switch between views - swiftui

I have the implementation as follow
struct DealsScreen: View {
#State var selectedSegment = "list"
var body: some View {
VStack(alignment: .leading, spacing: 0){
HStack{
Spacer()
}
Picker("", selection: $selectedSegment){
Text("List").tag("list")
Text("Map").tag("map")
}.pickerStyle(SegmentedPickerStyle()).padding(1)
Spacer()
self.selectedSegment == "list" ? ListContainer() : MapContainer()
}
}
}
My goal is to display a different container when the selected segmentedControl item changes.
How can I do it?
Thank you for your help

I tried your code, this will work after i change
self.selectedSegment == "list" ? ListContainer() : MapContainer()
to
if self.selectedSegment == "list" {
ListContainer()
}
else {
MapContainer()
}

Related

SwiftUI: How to pass an argument from one view to the next with dynamically generated buttons?

Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}

SwiftUI: animating scroll on ScrollView programmatically?

I need to programmatically animate the scroll of a scrollview. The scrollview contains either an HStack or a VStack. Code I tested with is this:
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: spacing) {
ForEach(cids, id: \.self) { cid in
....
}
}
.onAppear {
withAnimation(Animation.easeInOut(duration: 4).delay(3)) {
proxy.scrollTo(testCid)
}
}
}
.frame(maxWidth: w, maxHeight: w / 2)
}
The scrollview does land on the item with the testCid, however, it does not animate.
As soon as the view comes on screen the scrollview is already on testCid...
How can I animate the scroll?
The interactive scroll works if you start it from somewhere else (f.e. Button action) but not from the onAppear modifier. I'd guess this is intentional behavior to prevent the user seeing the scrolling when the view appears (or a bug in SwiftUI...). An ugly workaround is to defer the animation with an DispatchQueue.main.async:
import SwiftUI
struct ContentView: View {
let words = ["planet", "kidnap", "harbor", "legislation", "soap", "management", "prejudice", "an", "trunk", "divide", "critic", "area", "affair"]
#State var selectedWord: String?
var body: some View {
ScrollViewReader { proxy in
VStack(alignment: .leading) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(words, id: \.self) { word in
Text(word)
.background(self.selectedWord == word ? Color.yellow : nil)
.id(word)
}
}
}
Button("Scroll to random word") {
withAnimation(Animation.easeInOut(duration: 1)) {
let word = words.randomElement()
self.selectedWord = word
proxy.scrollTo(word)
}
}
}
.onAppear {
DispatchQueue.main.async { // <--- workaround
withAnimation(Animation.easeInOut(duration: 1).delay(1)) {
let word = self.words.last
self.selectedWord = word
proxy.scrollTo(word)
}
}
}
}
.padding(10)
}
}

NavigationLink in ContextMenu no longer working in iOS14 xcode12 beta3?

Wondering if anyone else has this issue, and if a workaround has been found. This works fine in iOS 13, but seems broken in iOS 14.
I am just trying to fire off a NavigationLink to another View, from a .contextMenu.
My code is as below.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
HStack {
Text("I am a text in a HStack ")
}
HStack {
NavigationLink(destination: TestView()) {
VStack {
Image(systemName:"gauge")
.font(.system(size: 31))
}
}
}
}
.contextMenu {
NavigationLink(destination: TestView()) {
Text("Navigate to TestView")
Image(systemName: "pencil")
}
}
}
}
}
The Destination TestView() is just a boilerplate "Hello World" view.
If I click not the Icon associated in the Stack, this triggers the navigation perfectly. But the same link in the context menu does not.
When I select it in the contextmenu, nothing happens. I.e I can select the menu item, but all it does is close the context menu and I stay on the same view.
Anyone else found this? solved it ?
Thanks
Here is a demo of possible approach. Tested with Xcode 12b3 / iOS 14 (also valid for SwiftUI 1.0)
struct ContentView: View {
#State private var showLink = false
var body: some View {
NavigationView {
VStack {
HStack {
Text("I am a text in a HStack ")
}
HStack {
NavigationLink(destination: Text("TestView")) {
VStack {
Image(systemName:"gauge")
.font(.system(size: 31))
}
}
}
}
.background(NavigationLink("", destination: Text("TestView"), isActive: $showLink))
.contextMenu {
Button(action: { self.showLink = true }) {
HStack {
Text("Navigate to TestView")
Image(systemName: "pencil")
}
}
}
}
}
}

SwiftUI, setting title to child views of TabView inside of NavigationView does not work

Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}

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)