I have a view with a list and inside the list there is a toggle that is binded with a boolean in the viewmodel, if I turn on the toggle the boolean is true and viceversa, the issue here is that if I turn on the toggle, and then enter background when I reopen the app, the toggle appears turned off even when the boolean is true, then I go back to the previous screen and when I return to the screen with the toggle it appears turned on, is there a way to avoid this issue? here is the List code below:
List {
Section(header: Text("Flags")) {
ForEach(viewModel.flags.indices, id: \.self) { index in
Toggle(isOn: $viewModel.flags[index].isActive) {
Text(viewModel.flags[index].name.rawValue.camelCaseToCapitalized())
}
}
}
You can try to use the onAppear modifier to update the state of the toggle when the view appears.
List {
Section(header: Text("Flags")) {
ForEach(viewModel.flags.indices, id: \.self) { index in
Toggle(isOn: $viewModel.flags[index].isActive) {
Text(viewModel.flags[index].name.rawValue.camelCaseToCapitalized())
}
.onAppear {
viewModel.flags[index].isActive = true
}
}
}
Related
I'm developing an app, one view of which has the primary goal of allowing users to reorder a List of NavigationLinks, but which I would also like to allow navigation & a few other things. I want users to be able to:
Reorder by dragging on the reorder control.
Navigate to the link by tapping elsewhere on the row.
Swipe from the leading edge to activate a swipe action.
At the moment, enabling EditMode (to allow reordering) disables navigation & swipe actions; I haven't been able to find a workaround that allows all 3 functions simultaneously. Is there a good way to do this?
Here's an example:
struct ReorderableListView: View {
var items: [X] // List of custom objects
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: CustomView(item)) { // winds up disabled by edit mode
Text(item.name)
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
.environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}
Update:
Based on the accepted answer, it looks like this behavior got updated in iOS 16: Now, if onMove is implemented, you can reorder with a long press even when not in edit mode, which allows these three actions to coexist. I've added custom drag-handles to make this behavior obvious to the user, but otherwise I'm going with exactly the solution given in the accepted answer, below.
I am not sure that the .environment is necessary in this case (I could be wrong). You can remove that piece.
Additionally, you should add an ID to each item in your foreach. This should ideally come from your model when you create new items (for example, your model can contain an ID variable = UUID()), but for the time being we can add it inline in your foreach.
I had to write some code on my end to get this up and running, so my solution is based on the code I spun up (very similar to yours, but missing your custom items object):
struct ReorderableListView: View {
var items: [X] // List of custom objects
var body: some View {
NavigationView {
List {
// added ID here
ForEach(items, id: \.self) { item in
NavigationLink(destination: CustomView(item)) { // winds up disabled by edit mode
Text(item.name)
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
// REMOVED .environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}
For reference, here is the code I wrote locally to fill in the gaps. This is what worked & what I implemented within your code:
struct ReorderableListView: View {
#State var items = [1, 2, 3] // List of custom objects
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ContentView2()) { // winds up disabled by edit mode
Text("\(item)")
}
.swipeActions(edge: .leading) { Button("Swipe") {print("swipe")} } // winds up disabled by edit mode
}
.onMove { from, to in
print("move \(from) to \(to)")
}
}
// .environment(\.editMode, .constant(.active)) // always in edit mode for reordering
}
}
}
Goal: have a SwiftUI architecture where the "add new item" and "edit existing item" are solved by the same view (EditItemView). However, for some reason, when I do this, the runtime agent complains of "Modifying state during view update, this will cause undefined behavior".
This is the code I want to use, which ensures that the EDITING of the item and ADDING a new item are handled by the same EditItemView:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.navigationBarItems(trailing:
// THIS LINE TO ADD A NEW ITEM:
NavigationLink(destination: EditItemView(item: Data.singleton.createItem(name: "New item", value: 5.0))
{
Image(systemName: "plus")
}
)
}
}
It doesn't work, leading to the issue highlighted above. I am forced to separate the functionality for Edit and Add into two distinct Views, which then works:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.sheet(isPresented: $isPresented)
{
// FORCED TO USE SEPARATE VIEW
AddItemView { name, value in
_ = Data.singleton.createItem(name: name, value: value)
self.isPresented = false
}
}
.navigationBarItems(trailing: Button(action: { self.isPresented.toggle()}) { Image(systemName: "plus")})
}
}
I don't understand why the code in the first version is considered to modify the state while updating view, because to me, it's sequential: new Item is created and THEN a view is shown for that Item.
Any ideas?
The destination of NavigationLink is not rendered lazily, meaning it'll get rendered when the NavigationLink itself is rendered -- not when clicked through.
The sheet code, depending on platform and SwiftUI version, may have the same issue, but apparently does not in the version you're using. Or, the closure you provide to AddItemView isn't run immediately -- since you didn't include the code, it's not clear.
To solve the issue in the first method, you can use the following SO answer which provides a lazy NavigationLink: https://stackoverflow.com/a/61234030/560942
My need is simple but I can't seem to figure a way (and many posts are rather complicated or are old/for iOS).
I have a NavigationView with NavigationLink and it all works fine. For certain reasons, I'd like to know the item the user clicked on because it's used for other actions on the view.
I tried putting a button inside the NavigationView but then only the button action fires, but the link doesn't work.
I tried OnTapGesture - then the function fires but isn't propagated so the detail view never shows up.
What am I missing? Is there some way I can do this without overcomplicating?
ForEach(anArray, id: \.self) { entry in
NavigationLink(destination: SettingsView(s: entry)
{
Text("something")
}.onTapGesture {print("Hello")}
}
Here is the solution, actually use a button that does some action and activates a link
Tested with Xcode 11.5b2
#State private var activatedLink: Int? = nil
// ... other code
ForEach(Array(anArray.enumerated()), id: \.element) { (i, entry) in
Button("something") {
print(">> some action")
self.activatedLink = i // << order might be important !!
}.background(
NavigationLink(destination: SettingsView(s: entry), tag: i, selection: $activatedLink) { EmptyView() }
.buttonStyle(PlainButtonStyle()) // << required !!
)
}
I have a SwiftUI Form that contains a Picker, a TextField, and a Text:
struct ContentView: View {
var body: some View {
Form {
Section {
Picker(selection: $selection, label: label) {
// Code to populate picker
}.pickerStyle(SegmentedPickerStyle())
HStack {
TextField(title, text: $text)
Text(text)
}
}
}
}
}
The code above results in the following UI:
I am able to easily select the second item in the picker, as shown below:
Below, you can see that I am able to initiate text entry by tapping on the TextField:
In order to dismiss the keyboard when the Picker value is updated, a Binding was added, which can be seen in the following code block:
Picker(selection: Binding(get: {
// Code to get selected segment
}, set: { (index) in
// Code to set selected segment
self.endEditing()
}), label: label) {
// Code to populate picker
}
The call to self.endEditing() is provided in the following method:
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
The following screenshot displays that selecting a different segment of the Picker dismisses the keyboard:
Up to this point, everything works as expected. However, I would like to dismiss the keyboard when tapping anywhere outside of the TextField since I am unable to figure out how to dismiss the keyboard when dragging the Form's containing scroll view.
I attempted to add the following implementation to dismiss the keyboard when tapping on the Form:
Form {
Section {
// Picker
HStack {
// TextField
// Text
}
}
}.onTapGesture {
self.endEditing()
}
Below, the following two screenshot displays that the TextField is able to become the first responder and display the keyboard. The keyboard is then successfully dismissed when tapping outside of the TextField:
However, the keyboard will not dismiss when attempting to select a different segment of the `Picker. In fact, I cannot select a different segment, even after the keyboard has been dismissed. I presume that a different segment cannot be selected because the tap gesture attached to the form is preventing the selection.
The following screenshot shows the result of attempting to select the second value in the Picker while the keyboard is shown and the tap gesture is implemented:
What can I do to allow selections of the Picker's segments while allowing the keyboard to be dismissed when tapping outside of the TextField?
import SwiftUI
struct ContentView: View {
#State private var tipPercentage = 2
let tipPercentages = [10, 15, 20, 25, 0]
#State var text = ""
#State var isEdited = false
var body: some View {
Form {
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
.pickerStyle(SegmentedPickerStyle())
HStack {
TextField("Amount", text: $text, onEditingChanged: { isEdited in
self.isEdited = isEdited
}).keyboardType(.numberPad)
}
}
}.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: isEdited ? .all : .none)
}
}
Form's tap gesture (to finish editing by tap anywhere) is enabled only if text field isEdited == true
Once isEdited == false, your picker works as before.
You could place all of your code in an VStack{ code }, add a Spacer() to it and add the onTap to this VStack. This will allow you to dismiss the keyboard by clicking anywhere on the screen.
See code below:
import SwiftUI
struct ContentView: View {
#State private var text: String = "Test"
var body: some View {
VStack {
HStack {
TextField("Hello World", text: $text)
Spacer()
}
Spacer()
}
.background(Color.red)
.onTapGesture {
self.endEditing()
}
}
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Changing the background color of an HStack or VStack to red simplifies figuring out where the user may click to dismiss.
Copy and paste code for a ready to run example.
Using SwiftUI, I would like to have the ability to change the button on my NavigationView based upon some Bool value indicating if it should be On or Off.
This would behave similar to how with UIKit you can replace a bar button item on either side of the screen to show a different button & associated action upon clicking.
I am able to get it working with the following code, but I am not certain if this is the best way to accomplish it, so am open to improvement.
import SwiftUI
struct HomeList: View {
#State var isOn = true
var body: some View {
NavigationView {
List(1 ..< 4) { index in
Text("Row \(index)")
}
.navigationBarTitle(Text(verbatim: "Title"), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.isOn = !self.isOn
}, label: {
Text(self.isOn ? "On" : "Off")
})
)
}
}
}
The key pieces being:
Using the #State modifier on my isOn variable, which tells my interface to invalidate & re-render upon changes
Having my Button action modify isOn &it can also support other actions if I need
The ternary operator in my Button label that updates the Text (or an Image if I want) to reflect the correct appearance
Finally, how it appears in action: