I am trying to create a NavigationLink destination that changes if any one of several toggle switches are set to TRUE. I have worked out the if/then logic for the links using tags and a state variable. That is, if I manually set toggleCount = the links work correctly.
What I would like to do however, is set toggleCount = Number of True Switches. You can see my failed attempt to loop through the array and increment toggleCount. It fails because it does not conform to View.
Any advice on a clean way to implement this? I don't necessarily need to count the number of true switches, I just need to know if any one of them were set to true.
import SwiftUI
struct ToggleStruct : Identifiable {
var id : Int
var name : String
var position : Bool
}
struct ContentView: View {
#State var toggleArray = [ToggleStruct(id: 1, name: "Toggle 1", position: false),
ToggleStruct(id: 2, name: "Toggle 2", position: false),
ToggleStruct(id: 3, name: "Toggle 3", position: false)
]
#State private var selection: Int? = nil
var toggleCount = 0
var body: some View {
NavigationView {
VStack {
Text("Toggle Count: \(toggleCount)")
ForEach(0..<toggleArray.count) { i in
Toggle(isOn: $toggleArray[i].position, label: {
Text(toggleArray[i].name)
})
}
NavigationLink(
destination: Text("Destination A"),
tag: 1,
selection: $selection,
label: {EmptyView()})
NavigationLink(
destination: Text("Destination B"),
tag: 2,
selection: $selection,
label: {EmptyView()})
Button(action: {
// ForEach(0..<toggleArray.count) { x in
// if toggleArray[x].position {
// toggleCount += 1
// }
// }
if toggleCount > 0 {
selection = 1
} else {
selection = 2
}
}, label: {
Text("Continue")
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Would something like this work for you? You could change the logic in destinationView to be whatever you needed.
import SwiftUI
struct ContentView: View {
#State var toggleOneIsOn = false
#State var toggleTwoIsOn = false
#State var toggleThreeIsOn = false
var body: some View {
NavigationView {
Form {
Toggle("Toggle One", isOn: $toggleOneIsOn)
Toggle("Toggle Two", isOn: $toggleTwoIsOn)
Toggle("Toggle Three", isOn: $toggleThreeIsOn)
NavigationLink("Foo", destination: destinationView)
}
}
}
var destinationView: some View {
switch (toggleOneIsOn, toggleTwoIsOn, toggleThreeIsOn) {
case _ where toggleOneIsOn || toggleTwoIsOn || toggleThreeIsOn:
return Text("At least one toggle is on")
default:
return Text("All toggles are off")
}
}
}
Related
I am trying to setup a picker, simple. I am successfully fetching an array of projects from firebase and populating the picker with the names of the projects. The problem that I am having is that I need to get the project id when I click the list but it's not doing anything after I click the option that I want. I tried to run it in a simulator and also on my iPhone and nothing happens after I make the selection. I am pretty sure I am not updating the picker and thus I am not updating the variable with the selected project id. I tried using the .onChange on the picker but nothing happens.
import SwiftUI
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: String = ""
var body: some View {
NavigationView {
VStack {
Picker("", selection: $selectedClient) {
ForEach(clients, id:\.self) {
Text($0.clientName)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
Here is the picker populated with the foo data: picker
Your selection variable $selectedClient needs to have a type that matches the tagged value of each item in the picker.
As you're not specifying an explicit .tag for your text, the ForEach creates an implicit one using what it's using for tracking its loop, which in this case looks like it's a Client.
You can either change selectedClient to be a type of Client, or tag your displayed subview with the string value to populate selectedClient with, e.g.:
ForEach(clients, id: \.self) { client in
Text(client.clientName)
.tag(client.clientID)
}
Also, if each client has a unique ID, you're better off using that as ForEach's identifier than \.self. You can either specify id: \.clientID, etc., to use a single attribute – or you can add Identifiable conformance to Client and make sure that it has an id value that is guaranteed to be unique.
import SwiftUI
import Firebase
struct NewProjectView: View {
#ObservedObject var viewModel = ProjectViewModel()
#ObservedObject var clientViewModel = ClientFeedViewModel()
#Environment (\.dismiss) var dismiss
#State var projectName: String = "s"
var clientNameIsEmpty: Bool {
if projectName.count < 3 {
return true
} else {
return false
}
}
var clients: [Client] {
return clientViewModel.clients
}
#State var selectedClient: Client = Client(id: "", clientName: "Blank", timestamp: Timestamp(), ownerId: "", ownerUsername: "")
var body: some View {
NavigationView {
VStack {
Picker("d", selection: $selectedClient) {
ForEach(clients, id:\.id) { client in
Text(client.clientName)
.tag(client)
//I need to exctract the project id so I can pass it on
}
}
.pickerStyle(.menu)
Text(selectedClient.id ?? "")
CustomTextField(text: $projectName, placeholder: Text("Client Name"), imageName: "person.text.rectangle")
.padding()
.background(Color("JUMP_COLOR")
.opacity(0.75)
)
.cornerRadius(10)
.padding(.horizontal, 40)
Text("Name must contain more than 3 characters")
.font(.system(.subheadline))
.foregroundColor(.gray.opacity(0.3))
.padding(.top, 30)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
dismiss()
}, label: {
Text("Cancel")
})
})
ToolbarItem(placement: .navigationBarTrailing , content: {
Button(action: {
viewModel.newProject(name: projectName)
dismiss()
}, label: {
Text("Save")
})
.disabled(clientNameIsEmpty)
})
}
}
}
.presentationDetents([.height(400)])
//.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
}
struct NewProjectView_Previews: PreviewProvider {
static var previews: some View {
NewProjectView()
}
}
I wanted to show a list of tags that can be selectable and want to make sure only one tag is selected in the list.
I tried to use #State, #ObservedObject, and #Published, but no luck. What is the best solution here?
import SwiftUI
struct Tag: Identifiable {
let id: Int
let name: String
var selected: Bool = false
}
struct ContentView: View {
#State var tags: [Tag]
var body: some View {
VStack {
ForEach(tags) { tag in
TagView(name: tag.name, isSelected: tag.selected) {
for (index, _) in tags.enumerated() {
// select only one tag
tags[index].selected = tags[index].id == tag.id
}
}
}
}
}
}
struct TagView: View {
let name: String
#State var isSelected: Bool
var onAction: (() -> Void) = { }
var body: some View {
Button(action: onAction) {
Text(name)
.foregroundColor(.white)
.padding()
.background(
isSelected ? Color.red : Color.blue
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(tags: [
Tag(id: 1, name: "Swift"),
Tag(id: 2, name: "Xcode"),
Tag(id: 3, name: "Apple")
])
}
}
Removing #State of isSelected variable in TagView resolved the issue
I want to better understand binding data across view, so I made this demo app
First View - if isShowing is true, navigating to SecondView (binding value)
struct ParentView: View {
#State var isShowing = false
#State var value = 5
var body: some View {
NavigationView {
if value != 5 {
ThirdView(isShowing: $isShowing)
} else {
NavigationLink(isActive: $isShowing) {
SecondView(value: $value)
} label: {
Text("Go to second view")
}
}
}
}
}
Second view - updating ParentView value
struct SecondView: View {
#Binding var value: Int
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack {
Button {
value = 5
presentationMode.wrappedValue.dismiss()
} label: {
Text("Return 5")
}
Button {
value = 1
presentationMode.wrappedValue.dismiss()
} label: {
Text("Return 1")
}
}
}
}
ThirdView - showing in FirstView in case value is not 5
struct ThirdView: View {
#Binding var isShowing: Bool
var body: some View {
ZStack {
Button {
isShowing.toggle()
} label: {
Text("Its a problem... Go to second view")
}
}
}
}
I tried to toggle isShowing in ThirdView so it can open SecondView to update value again.
But when button is clicked in ThirdView, it doesnt do anything.
The way you have things set up, it won't change. When value != 5, your `NavigationLink does not exist in the view. Instead, you want to trigger it programmatically like this:
struct ParentView: View {
#State var isShowing = false
#State var value = 5
var body: some View {
NavigationView {
VStack {
Text(value.description)
if value != 5 {
ThirdView(isShowing: $isShowing)
} else {
// Change out the NavigationLink for a button that sets isShowing.
Button {
isShowing = true
} label: {
Text("Go to second view")
}
}
}
// By placing it in the background, it is always available to be triggered.
.background(
NavigationLink(isActive: $isShowing) {
SecondView(value: $value)
} label: {
EmptyView()
}
)
}
}
}
Lastly, you don't need to toggle isShowing in ThirdView. You are better off either dismissing the view or setting the value to false. Otherwise, you can get confused what it is doing when you are in your various views.
I've created some SwiftUI code that use an EnvironmentObject to store boolean values for popping back to the root view and another EnvironmentObject to store a score variable that is intended to be used across multiple views.
In this example, I have two games, a Red Game and a Blue Game. I have booleans (.redStacked and .blueStacked) that are initially set to false. Their respective NavigationLinks set them to true. At the end of the game, a "Home" button sets it back to false, and this unwinds the navigation stack back to the root view.
The problem that I am running into is that any update to the score EnvironmentObject unexpectedly and prematurely pops the navigation stack back to the root view.
In the following example code, the only difference between games is that the Red Game button adds +1 to its score environmental variable. In this instance, the one point is added and the navigation link to the final page is executed, but then it immediately rubberbands back to the start. The Blue Game does not update the score environmental variable and transitions as expected.
I welcome any insights on why this occurs. Thanks in advance.
import SwiftUI
class Pop: ObservableObject {
#Published var redStack = false
#Published var blueStack = false
}
class Score: ObservableObject {
#Published var redTotal = 0
#Published var blueTotal = 0
}
struct GameSelection: View {
#ObservedObject var pop = Pop()
#ObservedObject var score = Score()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: RedStart(), isActive: $pop.redStack) {
Text("Play Red Game") //Tapping this link sets redStacked to true
}.foregroundColor(.red)
Divider()
NavigationLink(destination: BlueStart(), isActive: $pop.blueStack) {
Text("Play Blue Game") //Tapping this link sets blueSteacked to true
}.foregroundColor(.blue)
}
}
.environmentObject(score)
.environmentObject(pop)
}
}
struct RedStart: View {
#State var goToNextView : Bool = false
#EnvironmentObject var score : Score
var body: some View {
VStack {
NavigationLink(destination: RedEnd(), isActive: $goToNextView) {}
Button(action: {
score.redTotal += 1
goToNextView = true
}, label: {
Text("Add 1 Point. Move to Next Screen")
.foregroundColor(.red)
})
}
}
}
struct BlueStart: View {
#State var goToNextView : Bool = false
#EnvironmentObject var score : Score
var body: some View {
VStack {
NavigationLink(destination: BlueEnd(), isActive: $goToNextView) {}
Button(action: {
// score.blueTotal += 1
goToNextView = true
}, label: {
Text("Do NOT add points. Move to Next Screen")
.foregroundColor(.blue)
})
}
}
}
struct RedEnd: View {
#EnvironmentObject var pop: Pop
#EnvironmentObject var score: Score
var body: some View {
Button(action: {
pop.redStack = false
}, label: {
VStack {
Text("Your Score: \(score.redTotal)")
Text("Game Over")
Image(systemName: "house.fill")
}
.foregroundColor(.red)
})
}
}
struct BlueEnd: View {
#EnvironmentObject var pop: Pop
#EnvironmentObject var score: Score
var body: some View {
Button(action: {
pop.blueStack = false
}, label: {
VStack {
Text("Your Score: \(score.blueTotal)")
Text("Game Over")
Image(systemName: "house.fill")
}.foregroundColor(.blue)
})
}
}
struct GameSelection_Previews: PreviewProvider {
static var previews: some View {
GameSelection()
}
}
My SwiftUI app has a segmented Picker and I want to be able to disable one or more options depending on availability of options retrieved from a network call. The View code looks something like:
#State private var profileMetricSelection: Int = 0
private var profileMetrics: [RVStreamMetric] = [.speed, .heartRate, .cadence, .power, .altitude]
#State private var metricDisabled = [true, true, true, true, true]
var body: some View {
VStack(alignment: .leading, spacing: 2.0) {
...(some views)...
Picker(selection: $profileMetricSelection, label: Text("")) {
ForEach(0 ..< profileMetrics.count) { index in
Text(self.profileMetrics[index].shortName).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
...(some more views)...
}
}
What I want to be able to do is modify the metricDisabled array based on network data so the view redraws enabling the relevant segments. In UIKit this can be done by calls to setEnabled(_:forSegmentAt:) on the UISegmentedControl but I can't find a way of doing this with the SwiftUI Picker
I know I can resort to wrapping a UISegmentedControl in a UIViewRepresentable but before that I just wanted to check I'm not missing something...
you can use this simple trick
import SwiftUI
struct ContentView: View {
#State var selection = 0
let data = [1, 2, 3, 4, 5]
let disabled = [2, 3] // at index 2, 3
var body: some View {
let binding = Binding<Int>(get: {
self.selection
}) { (i) in
if self.disabled.contains(i) {} else {
self.selection = i
}
}
return VStack {
Picker(selection: binding, label: Text("label")) {
ForEach(0 ..< data.count) { (i) in
Text("\(self.data[i])")
}
}.pickerStyle(SegmentedPickerStyle())
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe something like
ForEach(0 ..< data.count) { (i) in
if !self.disabled.contains(i) {
Text("\(self.data[i])")
} else {
Spacer()
}
}
could help to visualize it better
NOTES (based on the discussion)
From user perspective, the Picker is one control, which could be in disabled / enabled state.
The option selected from Picker is not control, it is some value. If you make a list of controls presented to the user, some of them could be disabled, just to inform the user, that the action associated with it is not currently available (like menu, some buttons collection etc.)
I suggest you to show in Picker only values which could be selected. This collection of values could be updated any time.
UPDATE
Do you like something like this?
No problem at all ... (copy - paste - try - modify ...)
import SwiftUI
struct Data: Identifiable {
let id: Int
let value: Int
var disabled: Bool
}
struct ContentView: View {
#State var selection = -1
#State var data = [Data(id: 0, value: 10, disabled: true), Data(id: 1, value: 20, disabled: true), Data(id: 2, value: 3, disabled: true), Data(id: 3, value: 4, disabled: true), Data(id: 4, value: 5, disabled: true)]
var filteredData: [Data] {
data.filter({ (item) -> Bool in
item.disabled == false
})
}
var body: some View {
VStack {
VStack(alignment: .leading, spacing: 0) {
Text("Select from avaialable")
.padding(.horizontal)
.padding(.top)
HStack {
GeometryReader { proxy in
Picker(selection: self.$selection, label: Text("label")) {
ForEach(self.filteredData) { (item) in
Text("\(item.value.description)").tag(item.id)
}
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: CGFloat(self.filteredData.count) * proxy.size.width / CGFloat(self.data.count), alignment: .topLeading)
Spacer()
}.frame(height: 40)
}.padding()
}.background(Color.yellow.opacity(0.2)).cornerRadius(20)
Button(action: {
(0 ..< self.data.count).forEach { (i) in
self.data[i].disabled = false
}
}) {
Text("Enable all")
}
Button(action: {
self.data[self.selection].disabled = true
self.selection = -1
}) {
Text("Disable selected")
}.disabled(selection < 0)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}