SwiftUI - Binding in child view is not updating - swift

I have two views currently. A parent view with a List of `Pledges' (basically just a struct with a string and an image), and a child view which displays a confirmation of the pledge you selected from the list.
I have a state variable in the parent and in the child, a binding to this variable. However, the List item selected isn't always the one which is passed to the child confirmation view. It's as if the child binding variable isn't updating when a list item is selected.
does anyone have any ideas? thanks
Parent view:
let worstArea: String
#State var selection: String? = nil
var body: some View {
let pledgeList = getListOfPledges(worstArea: worstArea)
List(pledgeList) {
pledge in
Spacer()
NavigationLink(destination: PleadeConfirmation(pledgePicked: $selection)) {
pledgeRow(pledge: pledge, worstArea: worstArea).onTapGesture {
selection = pledge.description
}
}
}
}
Child view (PleadeConfirmation):
struct PleadeConfirmation: View {
#Binding var pledgePicked:String?
var body: some View {
Text("Commit to pledge: \(pledgePicked ?? "none selected")").multilineTextAlignment(.center)
}
}
The pledgePicked variable in child is the one in question which doesn't always reflect the list value being selected

Related

Bindings from the sheet modifier

I am using the SwiftUI sheet modifier on a View that has a collection published from its view model. When a user clicks on a list item, I am setting an optional State value to the corresponding item from the list. The sheet modifier uses the $selectedSubscription value to present and hide the sheet when needed, and passes the value to the closure in order for me to use it in a details view. I have set up the details view to receive a #Binding to a list item in order to edit it at a later stage. The problem is that SwiftUI will not let me pass the item in the closure as a binding.
struct SubscriptionsListView: View {
#StateObject var vm = SubscriptionsListVM()
#State private var selectedSubscription: PVSubscription? = nil
var body: some View {
NavigationView {
ZStack {
...
}
.sheet(item: $selectedSubscription) { subscription in
SubscriptionDetailsView(subscription: $subscription) <--- error here
.presentationDetents([.medium, .large])
}
}
}
}
struct SubscriptionDetailsView: View {
#Binding var subscription: PVSubscription
...
}
I already tried to get the index of the item from the array and to pass that as a binding, but I was unsuccessful.
The problem arises from the use of the sheet modifier. You are trying to use the "selectedSubscription" variable both for showing a sheet and showing the details of your model. You have to create another variable for showing the sheet.
Since your sheet only shows one view, you don't need the sheet modifier with the item argument. Item argument is used for showing different views in the same sheet modifier (settings, profile etc.). It doesn't give you a binding in the trailing closure to put into the subview.
struct SubscriptionsListView: View {
#StateObject var vm = SubscriptionsListVM()
#State private var selectedSubscription: PVSubscription? = nil
#State private var showSheet: Bool = false
var body: some View {
NavigationView {
ZStack {
... // Toggle show sheet here
}
.sheet(isPresented: $showSheet) {
SubscriptionDetailsView(subscription: $selectedSubscription)
.presentationDetents([.medium, .large])
}
}
}
}

SwiftUI: How do I share an ObservedObject between child views without affecting the parent view

TL;DR: If I have a view containing a NavigationSplitView(sidebar:detail:), with a property (such as a State or StateObject) tracking user selection, how should I make it so that the sidebar and detail views observe the user selection, but the parent view does not?
Using SwiftUI's new NavigationSplitView (or the deprecated NavigationView), a common paradigm is to have a list of selectable items in the sidebar view, with details of the selected item in the detail view. The selection, of course, needs to be observed, usually from within an ObservedObject.
struct ExampleView: View {
#StateObject private var viewModel = ExampleViewModel()
var body: some View {
NavigationSplitView {
SidebarView(selection: $viewModel.selection)
} detail: {
DetailView(item: viewModel.selection)
}
}
}
struct SidebarView: View {
let selectableItems: [Item] = []
#Binding var selection: Item?
var body: some View {
List(selectableItems, selection: $viewModel.selected) { item in
NavigationLink(value: item) { Text(item.name) }
}
}
}
struct DetailView: View {
let item: Item?
var body: some View {
// Details of the selected item
}
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
}
This, however, poses a problem: the ExampleView owns the ExampleViewModel used for tracking the selection, which means that it gets recalculated whenever the selection changes. This, in turn, causes its children SidebarView and DetailView to be redrawn.
Since we want those children to be recalculated, one might be forgiven for thinking that everything is as intended. However, the ExampleView itself should not be recalculated in my opinion, because doing so will not only update the child views (intended), but also everything in the parent view (not intended). This is especially true if its body is composed of other views, modifiers, or setup work. Case in point: in this example, the NavigationSplitView itself will also be recalculated, which I don't think is what we want.
Almost all tutorials, guides and examples I see online use a version of the above example - sometimes the viewModel is passed as an ObservedObject, or as an EnvironmentObject, but they all share the same trait in that the parent view containing the NavigationSplitView is observing the property that should only be observed by the children of NavigationSplitView.
My current solution is to initiate the viewmodel in the parent view, but not observe it:
struct ExampleView: View {
let viewModel = ExampleViewModel()
...
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
nonisolated init() { }
}
This way, the parent view will remain intact (at least in regards to user selection); however, this will cause the ExampleViewModel to be recreated if anything else would cause the ExampleView to be redrawn - effectively resetting our user selection. Additionally, we are unable to pass any of the viewModel's properties as bindings. So while it works for my current use-case, I don't consider this an effective solution.

How to pass data from a modal view list to parent view in SwiftUI?

I have (probably) an easy question related to SwiftUI state management.
A have a modal view with a simple list of buttons:
struct ExerciseList: View {
var body: some View {
List {
ForEach(1..<30) { _ in
Button("yoga") {
}
}
}
}
}
The parent view is this one:
struct SelectExerciseView: View {
#State private var showingSheet = false
#State private var exercise = "select exercise"
var body: some View {
Button(exercise) {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet){
ExerciseList()
}
}
}
How can I do to pass the selected button text from the list to the parent view ?
I'm thinking that I need a Binding variable inside the modal and use that, but not really sure how in this example.
At its most basic, you need the selected exercise in your parent view (SelectExerciseView) as a state variable. You then pass that in to the child view (the modal) via a binding. Assuming exercise as a string holds the variable you want to change:
.sheet(isPresented: $showingSheet) {
ExerciseList(exercise: $exercise)
}
Your modal then needs to have a #Binding reference.
struct ExerciseList: View {
#Binding var exercise: Exercise
var body: some View {
List {
ForEach(1..<30) { _ in
Button("yoga") {
exercise = "yoga"
}
}
}
}
}
Im not sure what you're asking...
Are you trying to show a "Detail View" from the modal.
Meaning theres the parent view -> Modal View -> Detail View
In your case it would be the SelectExerciseView -> ExerciseListView -> DetailView which shows the text of the button that was pressed on the previous view (can be any view you want)
If thats what you're trying to do I would use a NavigationLink instead of a button on the modal. The destination of the NavigationLink would be the detail view

Refresh view when navigating to it

In SwiftUI 2, when I navigate from a parent view to a child view and then back to the parent view, the parent view does not refresh its contents. How can I force it to refresh its contents?
In order to test if the contents get refreshed, in my parent view I displayed a random number using the following code:
Text("Random number is \(Int.random(in: 1..<100))")
When I navigate to a child view, and then I tap the Back button in the navigation bar to return to this parent view, the random number displayed remains the same. This indicates that the view is not refreshing.
How can I force the view to refresh itself whenever the user navigates back to it?
You could force SwiftUI to update the list by adding an .id(viewID) view modifier to the source view with an #State variable, in this case called "viewID". Then update this viewID in .onDisappear() of the destination view:
struct ContentView: View {
#State private var viewID: Int = 0
var body: some View {
NavigationView {
VStack {
Text("Hello, random world number \(Int.random(in: 1...100))")
.padding()
.id(viewID)
NavigationLink(
destination: destinationView,
label: { labelView })
}
}
}
private var labelView: some View {
Text("Go to Destination View")
}
private var destinationView: some View {
return Text("I am the Destination.")
.onDisappear{
viewID += 1
}
}
}
SwiftUI is declarative - as in you define the states the view could be in and Swift UI will take care of when it should update. In your example, there is no change in state, and thus, the view doesn't update.
If you want the view to refresh, you need to update state on appear events. You can do so like this:
struct ContentView: View {
#State var intValue: Int = 0
var body: some View {
Text("Random number is \(self.intValue)")
.onAppear {
intValue = Int.random(in: 1..<100)
}
}
}
You'll find that if you push a different view using a NavigationView/NavigationLink, and then navigate back, the label should update with a new random value.

Pass an observed object's projected value's property to #Binding

I have a main screen with an #FetchRequest that returns a FetchResult<Item>. In that main screen I have a list of all of the items with navigation links that, when selected, pass an Item to an ItemDetail view. In this ItemDetail view, item is marked with #ObservedObject. A subview of ItemDetail is ItemPropertiesView which lists all of the item's properties. I pass the item properties directly to #Binding properties of ItemPropertiesView using $item.{insert property here}. In ItemPropertiesView, there's several LineItem where I pass the property using $ once again to an #Binding property called "value" which is passed into a text field, that can ultimately be changed.
My goal is to be able to edit this text field and once you're done editing, to be able to save these changes to my core data store.
Since this has been a little hard to read, here is a code recreation:
struct MainScreen: View {
#FetchRequest(entity: Item.entity(), sortDescriptors: [NSSortDescriptor(key: "itemName", ascending: true)]) var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { (item: Item) in
NavigationLink(destination: ItemDetail(item: item)) {
Text(item.itemName ?? "Unknown Item Name")
}
} // ForEach
}
}
} // body
} // MainScreen
struct ItemDetail: View {
#ObservedObject var item: Item
var body: some View {
ItemPropertiesView(itemCost: $item.itemCost)
}
}
struct ItemPropertiesView: View {
#Binding var itemCost: String?
var body: some View {
LineItem(identifier: "Item Cost", value: $itemCost)
}
}
struct LineItem: View {
let identifier: String
#Binding var value: String
var body: some View {
HStack {
Text(identifier).bold() + Text(": ")
TextField("Enter value",text: $value)
}
}
}
I am getting an error in ItemDetail: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
This is on the only error I'm getting.
I'm new to SwiftUI, so all feedback is appreciated.
Just by code reading I assume the problem is in optional property in ItemPropertiesView, just remove it
struct ItemPropertiesView: View {
#Binding var itemCost: String // << here !!
// .. other code
and update parent to have bridge to CoreData model optional property
struct ItemDetail: View {
#ObservedObject var item: Item
var body: some View {
let binding = Binding(
get: { self.item.itemCost ?? "" },
set: { self.item.itemCost = $0 }
)
return ItemPropertiesView(itemCost: binding)
}
}