I have a state declared in my view
#State var selected: Bool = false
#State var selectedPerson: Person
When a "Person" is selected from a view there is a callback that sets the "selected" state to true, and sets the person that is selected:
MapView(viewModel: MapViewModel(people: viewModel.people)) { value in
selectedPerson = viewModel.getPersonWith(name: value.title)
selected = true
}
When all of this is set, it triggers a NavigationLink like this:
NavigationLink(destination: PersonDetailsView(viewModel: PersonViewModel(person: selectedPerson)), isActive: self.$selected) {
EmptyView()
}.hidden()
Everything works fine, but I have to set a default value to the "selectedPerson" state before the struct is initialized, even tho there is no person selected. Is there a way to bypass this, without giving the "selectedPerson" state a default "dummy" value.
Thanks :)
You need to make initial state optional, like
#State var selectedPerson: Person?
but as well you have to handle this optional in all places that use it, like
if nil != selectedPerson {
NavigationLink(destination:
PersonDetailsView(viewModel: PersonViewModel(person: selectedPerson!)), // << !!
isActive: self.$selected) {
EmptyView()
}.hidden()
}
Related
I'm finishing up online auditing of Stanford CS193P class (great class BTW) and I have an oddity on my last assignment. I have created a theme data store and I use it to select a theme (color, number of pairs of cards, emoji) and then kick off and play a matching game. That works fine. Using an edit button, the user can edit a theme and change any of the theme elements.
I run into a problem the first time I use the edit button and select a theme to edit. My code acts as if the #State myEditTheme is nil. If I force unwrap it it crashes. I have put it in a nil-coalescing option as shown, the edit window comes up with the first theme in the array. Any subsequent edit attempts work normally.
In the tap gesture function, I set the value of the #State var myEditTheme, then I set the themeEditing to true. My debug print statement indicates that the myEditTheme has been properly set. When the sheet(isPresented: $themeEditing) presents the ThemeEditor in a "sheet" view, the initial value of myEditTheme is nil.
Is there a timing issue between when I set it in the tap function and when Swift senses that themeEditing is true? The code below is obviously not functional as is, I have edited it for conciseness, only showing relevant portions.
struct ThemeManager: View {
#EnvironmentObject var store: ThemeStore // injected
#State private var editMode: EditMode = .inactive
// inject a binding to List and Edit button
#State private var myEditTheme: Theme?
#State private var themeEditing = false
// used to control .sheet presentation for theme editing
var body: some View {
NavigationView {
List {
ForEach(store.themes) { theme in
NavigationLink(destination: ContentView(viewModel: EmojiMemoryGame(theme: theme))) {
VStack(alignment: .leading) {
Text(theme.name).font(.title2)
Text(theme.emojis).lineLimit(1)
} // end VStack
.sheet(isPresented: $themeEditing) {
ThemeEditor(theme: $store.themes[myEditTheme ?? theme])
.environmentObject(store)
}
.gesture(editMode == .active ? tap(theme) : nil)
} // end NavigationLink
} // end ForEach
} // end List
.navigationTitle("Themes")
.navigationBarTitleDisplayMode(.inline) // removes large title, leaves small inline one
.toolbar {
ToolbarItem { EditButton() }
ToolbarItem(placement: .navigationBarLeading) {
newThemeButton
}
}
.environment(\.editMode, $editMode)
} // NavigationView
} // body
private func tap(_ theme:Theme) -> some Gesture {
TapGesture().onEnded {
myEditTheme = theme
print("edit theme: \(myEditTheme)")
themeEditing = true
}
}
This may seem like an odd question but I have looked around and can't seem to find an answer for it.
I would like to create toggles in a view that are not binded to any variable. Imagine a list of toggle switches that are toggle-able but don't actually do anything.
I have tried using .constant butt as you would expect, that doesn't allow me to toggle the switch. Obviously leaving It blank throws an error.
//Can't be changed
Toggle(isOn: .constant(true)) {
Text("Checkbox")
}
//Throws an error
Toggle() {
Text("Checkbox")
}
Is there anything that can be passed in the isOn: parameter to allow for that?
Edit:
In theory I could just have a #State variable in my view and binding to the toggle and simple not used that variable anywhere else in my view. Only thing is, I do not know ahead of time how many toggles will be displayed in my view so I can't just declare a bunch of #State variables. And if I were too only create one #State variable and blind it to all of my toggles, they would all be in sync, which is not what I am looking for, I would like them to all be independent.
Below is a simplified example of the layout of my view
private var array: [String]
var body: some View {
ForEach((0..<self.array.count), id: \.self) {
Toggle("Show welcome message", isOn: *binding here*)
}
}
Thank you
You can simply create a single #State variable of type [Bool], an array containing all the toggle booleans.
Here is some example code:
struct ContentView: View {
var body: some View {
ToggleStack(count: 5)
}
}
struct ToggleStack: View {
#State private var toggles: [Bool]
private let count: Int
init(count: Int) {
self.count = count
toggles = Array(repeating: true, count: count)
}
var body: some View {
VStack {
ForEach(0 ..< count) { index in
Toggle(isOn: $toggles[index]) {
Text("Checkbox")
}
}
}
}
}
Result:
I don't understand why SwiftUI NavigationLink's isActive behaves as if it has it's own state. Even though I pass a constant to it, the back button overrides the value of the binding once pressed.
Code:
import Foundation
import SwiftUI
struct NavigationLinkPlayground: View {
#State
var active = true
var body: some View {
NavigationView {
VStack {
Text("Navigation Link playground")
Button(action: { active.toggle() }) {
Text("Toggle")
}
Spacer()
.frame(height: 40)
FixedNavigator(active: active)
}
}
}
}
fileprivate struct FixedNavigator: View {
var active: Bool = true
var body: some View {
return VStack {
Text("Fixed navigator is active: \(active)" as String)
NavigationLink(
destination: SecondScreen(),
// this is technically a constant!
isActive: Binding(
get: { active },
set: { newActive in print("User is setting to \(newActive), but we don't let them!") }
),
label: { Text("Go to second screen") }
)
}
}
}
fileprivate struct SecondScreen: View {
var body: some View {
Text("Nothing to see here")
}
}
This is a minimum reproducible example, my actual intention is to handle the back button press manually. So when the set inside the Binding is called, I want to be able to decide when to actually proceed. (So like based on some validation or something.)
And I don't understand what is going in and why the back button is able to override a constant binding.
Your use of isActive is wrong. isActive takes a binding boolean and whenever you set that binding boolean to true, the navigation link gets activated and you are navigated to the destination.
isActive does not control whether the navigation link is clickable/disbaled or not.
Here's an example of correct use of isActive. You can manually trigger the navigation to your second view by setting activateNavigationLink to true.
EDIT 1:
In this new sample code, you can disable and enable the back button at will as well:
struct ContentView: View {
#State var activateNavigationLink = false
var body: some View {
NavigationView {
VStack {
// This isn't visible and should take 0 space from the screen!
// Because its `label` is an `EmptyView`
// It'll get programmatically triggered when you set `activateNavigationLink` to `true`.
NavigationLink(
destination: SecondScreen(),
isActive: $activateNavigationLink,
label: EmptyView.init
)
Text("Fixed navigator is active: \(activateNavigationLink)" as String)
Button("Go to second screen") {
activateNavigationLink = true
}
}
}
}
}
fileprivate struct SecondScreen: View {
#State var backButtonActivated = false
var body: some View {
VStack {
Text("Nothing to see here")
Button("Back button is visible: \(backButtonActivated)" as String) {
backButtonActivated.toggle()
}
}
.navigationBarBackButtonHidden(!backButtonActivated)
}
}
I'm working on a validation routine for a form, but when the validation results come in, the onChange is not being triggered.
So I have a form that has some fields, and some nested items that have some more fields (the number of items may vary). Think of a form for creating teams where you get to add people.
When the form is submitted, it sends a message to each item to validate itself, and the results of the validation of each item are stored in an array of booleans. Once all the booleans of the array are true, the form is submitted.
Every time a change occurs in the array of results, it should change a flag that would check if all items are true, and if they are, submits the form. But whenever I change the flag, the onChange I have for it never gets called:
final class AddEditProjectViewModel: ObservableObject {
#Published var array = ["1", "2", "3", "hello"]
// In reality this array would be a collection of objects with many properties
}
struct AddEditItemView: View {
#State var text : String
#Binding var doValidation: Bool // flag to perform the item validation
#Binding var isValid : Bool // result of validating all fields in this item
init(text: String, isValid: Binding<Bool>, doValidation: Binding<Bool>) {
self._text = State(initialValue: text)
self._isValid = isValid
self._doValidation = doValidation
}
func validateAll() {
// here would be some validation logic for all form fields,
//but I'm simulating the result to all items passed validation
// Validation needs to happen here because there are error message
//fields within the item view that get turned on or off
isValid = true
}
var body: some View {
Text(text)
.onChange(of: doValidation, perform: { value in
validateAll() // when the flag changes, perform the validation
})
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var performValidateItems : Bool = false // flag to perform the validation of all items
#State var submitFormFlag = false // flag to detect when validation results come in
#State var itemsValidationResult = [Bool]() // store the validation results of each item
{
didSet {
print(submitFormFlag) // i.e. false
submitFormFlag.toggle() // Even though this gets changed, on changed on it won't get called
print(submitFormFlag) // i.e. true
}
}
init(viewModel : AddEditProjectViewModel) {
self.viewModel = viewModel
var initialValues = [Bool]()
for _ in (0..<viewModel.array.count) { // populate the initial validation results all to false
initialValues.append(false)
}
_itemsValidationResult = State(initialValue: initialValues)
}
//https://stackoverflow.com/questions/56978746/how-do-i-bind-a-swiftui-element-to-a-value-in-a-dictionary
func binding(for index: Int) -> Binding<Bool> {
return Binding(get: {
return self.itemsValidationResult[index]
}, set: {
self.itemsValidationResult[index] = $0
})
}
var body: some View {
HStack {
ForEach(viewModel.array.indices, id: \.self) { i in
AddEditItemView(
text: viewModel.array[i],
isValid: binding(for: i),
doValidation: $performValidateItems
)
}
Text(itemsValidationResult.description)
Button(action: {
performValidateItems.toggle() // triggers the validation of all items
}) {
Text("Validate")
}
.onChange(of: submitFormFlag, perform: { value in // this never gets called
print(value, "forced")
// if all validation results in the array are true, it will submit the form
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: AddEditProjectViewModel())
}
}
You shouldn't use didSet on the #State - it's a wrapper and it doesn't behave like standard properties.
See SwiftUI — #State:
Declaring the #State isFilled variable gives access to three
different types:
isFilled — Bool
$isFilled — Binding
_isFilled — State
The State type is the wrapper — doing all the extra work for us — that stores an underlying wrappedValue,
directly accessible using isFilled property and a projectedValue,
directly accessible using $isFilled property.
Try onChange for itemsValidationResult instead:
var body: some View {
HStack {
// ...
}
.onChange(of: itemsValidationResult) { _ in
submitFormFlag.toggle()
}
.onChange(of: submitFormFlag) { value in
print(value, "forced")
}
}
You may also consider putting the code you had in .onChange(of: submitFormFlag) inside the .onChange(of: itemsValidationResult).
I am attempting to have a list that when a cell it tapped it changes the hasBeenSeen Bool value within the State object itself.
struct State: Identifiable {
var id = UUID()
let name: String
var hasBeenSeen: Bool = false
}
struct ContentView: View {
let states: [State] = [
State(name: "Oregon", hasBeenSeen: true),
State(name: "California", hasBeenSeen: true),
State(name: "Massachussets", hasBeenSeen: false),
State(name: "Washington", hasBeenSeen: true),
State(name: "Georgia", hasBeenSeen: false)
]
var body: some View {
NavigationView {
List {
ForEach(states, id: \.id) { state in
StateCell(state: state)
}
}.navigationBarTitle(Text("States"))
}
}
}
struct StateCell: View {
var state: State
var body: some View {
HStack {
Text(state.name)
Spacer()
if state.hasBeenSeen {
Image(systemName: "eye.fill")
}
}.onTapGesture {
// state.hasBeenSeen.toggle()
}
}
}
My original thought is that I need to make hasBeenSeen to a #State var but that doesn't seem to work. How can I make this Bool val editable from a list?
Views in SwiftUI are immutable - they are just structures - so you can't change their properties. That's why SwiftUI has a concept of a #State property wrapper. When you change "state" property, SwiftUI actually updates the state value, not the view's property value (which is, again, immutable).
So, you need to set #State on the states property within your view. (You'd also need to change the name, since identifier State is already taken by the State property wrapper - so I just changed it to StateEntity)
#State var states: [StateEntity] = [
StateEntity(name: "Oregon", hasBeenSeen: true),
// ... etc
]
That's not enough, though, since when you pass an element of the states array (a StateEntity value) to a child view, you're just passing a copy.
For that, you'd need a binding. A binding allows child views to modify state properties of parent views, without owning the data. So, the child view's property should use the #Binding property wrapper:
struct StateCell: View {
#Binding var state: StateEntity
// ...
}
SwiftUI made it easy to get the binding of state property by using the projected value, which in this case is $states.
However, you need to pass a binding not to the entire array, but to a specific element of that array. That, unfortunately (and rather annoyingly), is a bit trickier. You need to get the index of the element, and given the index, access the binding like so: $state[index].
One way is to do a ForEach over indices of states:
var body: some View {
NavigationView {
List {
ForEach(states.indices) { index in
StateCell(state: self.$states[index])
}
}.navigationBarTitle(Text("States"))
}
}