Add border to only 1 button - swiftui

I have a simple setup where I have 2 buttons and when a user clicks on one of them I want a border to show around it so that they know which one they clicked
I only ever want 1 button to have a border at once
I came up with this
import SwiftUI
struct TestView: View {
#State var isBorder:Bool = false
var body: some View {
VStack{
Button {
isBorder.toggle()
} label: {
Label("Sports", systemImage: "sportscourt")
}
.foregroundColor(.black)
.padding()
.background(Color(hex: "00bbf9"))
.cornerRadius(8)
.overlay(isBorder ? RoundedRectangle(cornerRadius: 8).stroke(Color.black, lineWidth:2) : nil)
Button {
isBorder.toggle()
} label: {
Label("Firends", systemImage: "person")
}
.foregroundColor(.black)
.padding()
.background(Color(hex: "fee440"))
.cornerRadius(8)
.overlay(isBorder ? RoundedRectangle(cornerRadius: 8).stroke(Color.black, lineWidth:2) : nil)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
But a border shows around both buttons because I am using 1 variable "isBorder"
How could I adapt my solution so that I can accommodate for more buttons

As the comments allready stated this should be encapsulated in its own view. But here you also have the additional problem with the shared state of what button is clicked.
First create an enum that holds all properties a button has that distinguishes it from others:
enum ButtonEnum: Int, CaseIterable{
case sports, friends
var systemIcon: String{
switch self{
case .sports:
return "sportscourt"
case .friends:
return "person"
}
}
var label: String{
switch self{
case .sports:
return "Sports"
case .friends:
return "Friends"
}
}
var backgroundColor: Color{
switch self{
case .sports:
return Color("00bbf9")
case .friends:
return Color("fee440")
}
}
}
Then create a View that represents that button:
struct BorderedButton: View{
#Binding var selected: ButtonEnum?
var buttonType: ButtonEnum
var action: () -> Void
var body: some View{
Button {
selected = buttonType
action()
} label: {
Label(buttonType.label, systemImage: buttonType.systemIcon)
}
.foregroundColor(.black)
.padding()
.background(buttonType.backgroundColor)
.cornerRadius(8)
.overlay(selected == buttonType ? RoundedRectangle(cornerRadius: 8).stroke(Color.black, lineWidth:2) : nil)
}
}
and usage example:
struct TestView: View {
#State private var selected: ButtonEnum? = nil
var body: some View {
VStack{
BorderedButton(selected: $selected, buttonType: .friends) {
print("friends pressed")
}
BorderedButton(selected: $selected, buttonType: .sports) {
print("sports selected")
}
}
}
}
This solution can be easily expanded by adding new cases to the enum.

Related

View Change in SwiftUi Using Enum

I have multiple views in a swift project I am trying to change Views My idea is to make an enum, and change state. I do not want to use navigationLinks.
Here is my code:
struct NightOutApp: App {
var body: some Scene {
WindowGroup {
ViewNavigator()
}
}
}
enum ViewState{
case LoginView
case UserProfileView
}
struct ViewNavigator: View {
var body: some View {
#State var ViewState = ViewState.LoginView
return Group{
switch ViewState{
case .LoginView:
LoginView()
case .UserProfileView:
UserProfileView()
}
}
}
}
I have a variable
#Binding var ViewState: ViewState at the top of the LoginView
some logic on the LoginView that would change ViewState from LoginView to UserProfileView:
self.ViewState = .UserProfileView
I tried using binding variables. this gave me a warning: Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
Edit-
Here is what happens when I run it. I press a button to login, It takes me to this breakpoint. The code seems to process, but the view does not change.
Code
Ok I've edited my answer in an attempt to understand what you're looking for. It seems you are needing a login flow but I don't think I can understand your problem fully without seeing more of your code. Here is an example that you should be able to copy and paste and play around with while you figure out exactly what you're needing.
import SwiftUI
struct ViewNavigator: View {
#State var viewState = 0
#State var withEmail: String = ""
#State var withPassword: String = ""
#State var showAlert: Bool = false
#State var alertTitle: String = ""
#State var alertMessage: String = ""
var body: some View {
ZStack {
switch viewState {
case 0:
loginView
case 1:
profileView
default:
RoundedRectangle(cornerRadius: 25)
.foregroundColor(.red)
}
VStack {
Spacer()
bottomButton
}
.padding()
}
.alert(alertTitle, isPresented: $showAlert) { } message: {
Text(alertMessage)
}
}
func handleBottomButtonPressed() {
switch viewState {
case 0:
guard !withEmail.isEmpty else {
showAlert(title: "Wait!", message: "Your email is required.")
return
}
guard withPassword == "password" else {
showAlert(title: "Hold Up!", message: "Your password is incorrect.")
return
}
default:
break
}
if viewState == 0 {
viewState += 1
withPassword = ""
} else {
viewState -= 1
}
}
func showAlert(title: String, message: String) {
alertTitle = title
alertMessage = message
showAlert.toggle()
}
}
struct ViewNavigator_Previews: PreviewProvider {
static var previews: some View {
ViewNavigator()
}
}
extension ViewNavigator {
private var bottomButton: some View {
Button {
handleBottomButtonPressed()
} label: {
Text(viewState == 0 ? "LOGIN" : "LOGOUT")
.font(.headline)
.foregroundColor(.white)
.frame(height: 55)
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(15)
.shadow(radius: 10)
}
}
private var loginView: some View {
ZStack {
Color.green.ignoresSafeArea()
VStack {
Image(systemName: "1.square")
.font(.largeTitle)
Text("LOGIN")
.font(.largeTitle)
VStack {
TextField("email...", text: $withEmail)
.padding()
.background(Color(UIColor.systemGray5))
.cornerRadius(10)
.textInputAutocapitalization(.never)
SecureField("password...", text: $withPassword)
.padding()
.background(Color(UIColor.systemGray5))
.cornerRadius(10)
.textInputAutocapitalization(.never)
}
.padding()
}
}
}
private var profileView: some View {
ZStack {
Color.orange.ignoresSafeArea()
VStack {
Image(systemName: "2.square")
.font(.largeTitle)
Text("Profile View")
.font(.largeTitle)
}
}
}
}

SwiftUI .searchable implementation in the wrong way?

I am trying to use a tab bar in order to use different views. On some of those views I have a list of items and I wish that list to be .searchable. If I go to each of the views and search it works like a charm, but when I embed that in the tabbed view the list becomes non-responsive to click but it responds to scroll gesture.
I will expand the idea with code that I have and screenshots, but I am pretty sure that the problem resides in how I'm implementing the combination of the tab bar view and the views that have the searchable modifier:
This code works well
import SwiftUI
struct ClientListView: View {
#ObservedObject var viewModel = ClientFeedViewModel()
#State var searchText: String
#State private var showingSheet = false
#State private var showList = false
var clients: [Client] {
if searchText.count > 2 {
return searchText.isEmpty ? viewModel.clients : viewModel.search(withText: searchText)
}
return viewModel.clients
}
init(){
searchText = ""
}
var body: some View {
NavigationView {
List(clients) { client in
NavigationLink(destination: {
}, label: {
VStack {
Text(client.clientName)
}
})
.listRowSeparator(.hidden)
}
.searchable(text: $searchText)
.listStyle(.plain)
}
}
}
struct ClientListView_Previews: PreviewProvider {
static var previews: some View {
ClientListView()
}
}
The problem starts when I do this and implement the ClientListView in a tab bar view like this:
Tab bar with different views not working searchable modifier
This is the code of the Tab Bar View:
import SwiftUI
struct MainTabView: View {
#EnvironmentObject var viewModel: AuthViewModel
#Binding var selectedIndex: Int
var body: some View {
NavigationView {
VStack {
TabView(selection: $selectedIndex) {
ClientListView()
.onTapGesture {
selectedIndex = 0
}
.tabItem {
Label("Clients", systemImage: "list.bullet")
}.tag(0)
ProjectListView()
.onTapGesture {
selectedIndex = 1
}
.tabItem {
Image(systemName: "person")
Label("Projects", systemImage: "list.dash")
}.tag(1)
TaskListView()
.tabItem {
Image(systemName: "person")
Label("Tasks", systemImage: "list.dash")
}.tag(2)
.onTapGesture {
selectedIndex = 2
}
ClientListView()
.tabItem {
Label("Settings", systemImage: "gear")
}.tag(3)
.onTapGesture {
selectedIndex = 3
}
}
.navigationTitle(tabTitle)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Image("logo_silueta")
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
viewModel.signOut()
}, label: {
Text("logout")
})
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
var tabTitle: String {
switch selectedIndex {
case 0: return "Clients"
case 1: return "Projects"
case 2: return "Tasks"
case 3: return "Settings"
default: return ""
}
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView(selectedIndex: .constant(0))
}
}
Navigation on the tabbed view works and displays the different names on the tab bar title, but when I click cancel or x button of the search bar, it doesn't work and also the list becomes unclickable
So far I haven't been able to find where the problem is but I am assuming its because the tab bar view is messing up with the searchable property
The culprit would seem to be your .onTapGesture modifiers, which will take precedence over any tap handling in your child views.
I'm not sure what value those modifiers bring, since using appropriate .tag values is enough for the tab view to keep track of its selected index. I'd start by removing them.
#ObservedObject var viewModel = ClientFeedViewModel() is a memory leak, try changing it to something like:
struct ClientListViewData {
var searchText: String = ""
var showingSheet = false
var showList = false
mutating func showSheet() {
showingSheet = true
}
}
struct ClientListView: View {
#Binding var data: ClientListViewData

Overlay views one by one in SwiftUI

I have the following code with a struct and two views. On tap of the firstScreenOverlay button i want to show the secondScreenOverlay and hide the previous one and so on. Any help appreciated!
import SwiftUI
struct ContentView: View {
var body: some View {
Text("hello there")
.overlay(firstScreenOverlay, alignment: .center)
}
}
private var firstScreenOverlay: some View {
ZStack {
Color.blue
.opacity(0.5)
Button {} label: {
Text("Next")
.fullWidth()
}
}
}
private var secondScreenOverlay: some View {
ZStack {
Color.red
.opacity(0.5)
}
}
You just need a way to keep track of what is showing. You can use a variable and an enum
import SwiftUI
struct DynamicOverlay: View {
//Keeps track of what is showing
#State var selectedOverlay: OverlayViews = .none
var body: some View {
VStack{
Text("hello there")
//Button changes what is being displayed
Button("first", action: {
selectedOverlay = .first
})
}
//Displays the selected view
.overlay(selectedOverlay.view($selectedOverlay), alignment: .center)
}
}
enum OverlayViews{
case first
case second
case none
//Holds all the options for the views
#ViewBuilder func view(_ selectedView: Binding<OverlayViews>) -> some View{
switch self{
case .first:
ZStack {
Color.blue
.opacity(0.5)
Button {
selectedView.wrappedValue = .second
} label: {
Text("Next")
}
}
case .second:
ZStack {
Color.red
.opacity(0.5)
Button("home") {
selectedView.wrappedValue = .none
}
}
case .none:
EmptyView()
}
}
}
struct DynamicOverlay_Previews: PreviewProvider {
static var previews: some View {
DynamicOverlay()
}
}

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)
}
}

Any ideas how to help this Form + ForEach SwiftUI view update it’s NavigationLink Destination?

I’m seeing this issue where a Form / NavigationLink set up seems to stop passing through the data on changes.
Expected behavior: Checkmarks should update when you pick a different food.
Observed behavior: You can see the favorite food changing outside the NavigationLink Destination, but not inside.
This setup mirrors a dynamic application where a ForEach is used to display various NavigationLinks in the Form based on parent data. Weirdly enough, this works if you replace Form with VStack, so I’m curious why this isn’t updating.
I have attached two minimum-setup example projects that replicate this issue where the destination of a NavigationLink is not receiving an update when data is changing. One with Binding, one with simpler passed properties.
Sample Project #1 with Binding - Dropbox
Sample Project #2 without Binding - Dropbox
Code #1:
//
// ContentView.swift
// Form Updating Example
//
// Created by Sahand Nayebaziz on 12/10/20.
//
import SwiftUI
struct ContentView: View {
#State var isPresentingMainView = false
#State var favoriteFood: FoodType = .bagel
var body: some View {
VStack {
Button(action: { isPresentingMainView = true }, label: {
Text("Present Main View")
})
}
.fullScreenCover(isPresented: $isPresentingMainView) {
MainView(favoriteFood: $favoriteFood)
}
}
}
struct MainView: View {
#Binding var favoriteFood: FoodType
var body: some View {
NavigationView {
HStack {
Spacer()
Text(favoriteFood.emoji)
.font(.title)
.foregroundColor(.secondary)
Spacer()
NavigationView {
Form {
List {
ForEach(["SomethingRepresentingShowingFood"], id: \.self) { _ in
NavigationLink(
destination: makeDetail(),
label: {
Text("Food Randomizer")
})
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.frame(maxWidth: 350)
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func makeDetail() -> some View {
Form {
ForEach(FoodType.allCases) { foodType in
Button(action: { favoriteFood = foodType }, label: {
HStack {
Text(foodType.emoji)
Spacer()
if favoriteFood == foodType {
Image(systemName: "checkmark")
}
}
})
}
}
}
}
enum FoodType: String, Identifiable, CaseIterable {
case bagel, pizza, broccoli
var id: String { rawValue }
var emoji: String {
switch self {
case .bagel: return "🥯"
case .pizza: return "🍕"
case .broccoli: return "🥦"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
MainView(favoriteFood: .constant(.bagel))
MainView(favoriteFood: .constant(.bagel))
.makeDetail()
}
}
}
Code #2:
//
// ContentView.swift
// Form Updating Example
//
// Created by Sahand Nayebaziz on 12/10/20.
//
import SwiftUI
struct ContentView: View {
#State var isPresentingMainView = false
#State var favoriteFood: FoodType = .bagel
var body: some View {
VStack {
Button(action: { isPresentingMainView = true }, label: {
Text("Present Main View")
})
}
.fullScreenCover(isPresented: $isPresentingMainView) {
MainView(currentFavoriteFood: favoriteFood, onUpdateFavoriteFood: { favoriteFood = $0 })
}
}
}
struct MainView: View {
let currentFavoriteFood: FoodType
let onUpdateFavoriteFood: (FoodType) -> Void
var body: some View {
NavigationView {
HStack {
Spacer()
Text(currentFavoriteFood.emoji)
.font(.title)
.foregroundColor(.secondary)
Spacer()
NavigationView {
Form {
List {
ForEach(["SomethingRepresentingShowingFood"], id: \.self) { _ in
NavigationLink(
destination: makeDetail(),
label: {
Text("Food Randomizer")
})
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.frame(maxWidth: 350)
}
.navigationTitle("Main")
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func makeDetail() -> some View {
Form {
ForEach(FoodType.allCases) { foodType in
Button(action: { onUpdateFavoriteFood(foodType) }, label: {
HStack {
Text(foodType.emoji)
Spacer()
if currentFavoriteFood == foodType {
Image(systemName: "checkmark")
}
}
})
}
}
}
}
enum FoodType: String, Identifiable, CaseIterable {
case bagel, pizza, broccoli
var id: String { rawValue }
var emoji: String {
switch self {
case .bagel: return "🥯"
case .pizza: return "🍕"
case .broccoli: return "🥦"
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
MainView(currentFavoriteFood: .bagel, onUpdateFavoriteFood: { _ in })
MainView(currentFavoriteFood: .bagel, onUpdateFavoriteFood: { _ in })
.makeDetail()
}
}
}