onReceive not getting called in SwiftUI View when ObservedObject changes - ios

I don't manage to trigger the onReceive method in a SwiftUI View whenever a variable from ObservedObject changes.
I tried two methods: using #Publish and using PassthroughSubject<>
Here is the ViewModel
class MenuViewModel: ObservableObject {
#Published var selectedItems = Set<UUID>()
#Published var currentFocusItem: UUID?
// Output
let newItemOnFocus = PassthroughSubject<(UUID?), Never>()
// This function gets called good :)
func tapOnMenuItem(_ item: MenuItem) {
if selectedItems.contains(item.id) {
//These changes should trigger the onReceive?
currentFocusItem = item.id
newItemOnFocus.send(item.id)
} else {
selectedItems.insert(item.id)
currentFocusItem = nil
newItemOnFocus.send(nil)
}
}
}
Here is the View when trying to catch the changes in #Published var currentFocusItem
struct MenuView: View {
#ObservedObject private var viewModel: MenuViewModel
#State var showPicker = false
#State private var menu: Menu = Menu.mockMenu()
init(viewModel: MenuViewModel = MenuViewModel()) {
self.viewModel = viewModel
}
var body: some View {
VStack {
List(menu.items, selection: $viewModel.selectedItems) { item in
MenuItemView(item: item)
}
Divider()
getBottomView(showPicker: showPicker)
}
.navigationBarTitle("Title")
.navigationBarItems(trailing: Button(action: closeModal) {
Image(systemName: "xmark")
})
.onReceive(viewModel.$currentFocusItem, perform: { itemUUID in
self.showPicker = itemUUID != nil // <-- This only gets called at launch time
})
}
}
The View in the same way but trying to catch the PassthroughSubject<>
.onReceive(viewModel.newItemOnFocus, perform: { itemUUID in
self.showPicker = itemUUID != nil // <-- This never gets called
})
----------EDIT----------
Adding MenuItemView, although viewModel.tapOnMenuItem gets always called, so I am not sure if it's very relevant
MenuItemView is here:
struct MenuItemView: View {
var item: MenuItem
#ObservedObject private var viewModel: MenuViewModel = MenuViewModel()
#State private var isSelected = false
var body: some View {
HStack(spacing: 24) {
Text(isSelected ? " 1 " : item.icon)
.font(.largeTitle)
.foregroundColor(.blue)
.bold()
VStack(alignment: .leading, spacing: 12) {
Text(item.name)
.bold()
Text(item.description)
.font(.callout)
}
Spacer()
Text("\(item.points)\npoints")
.multilineTextAlignment(.center)
}
.padding()
.onTapGesture {
self.isSelected = true
self.viewModel.tapOnMenuItem(self.item). // <-- Here tapOnMenuItem gets called
}
}
func quantityText(isItemSelected: Bool) -> String {
return isItemSelected ? "1" : item.icon
}
}
What am I doing wrong?

Well, here it is - your MenuView and MenuItemView use different instances of view model
1)
struct MenuView: View {
#ObservedObject private var viewModel: MenuViewModel
#State var showPicker = false
#State private var menu: Menu = Menu.mockMenu()
init(viewModel: MenuViewModel = MenuViewModel()) { // 1st one created
2)
struct MenuItemView: View {
var item: MenuItem
#ObservedObject private var viewModel: MenuViewModel = MenuViewModel() // 2nd one
thus, you modify one instance, but subscribe to changes in another one. That's it.
Solution: pass view model via .environmentObject or via argument from MenuView to MenuItemView.

Related

How to observe change in #StateObject in SwiftUI?

I am having property with #StateObject, I am trying to observe change in viewmodel, I am able to print correct result but can not able to show on screen as view is not refreshing.
Tried using binding but not worked because of #StateObject
import SwiftUI
struct AbcView: View {
#StateObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self._abcViewModel = StateObject(wrappedValue: abcViewModel)
}
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .top) {
ScrollView {
Text("some txt")
}
.overlay(
VStack {
TopView(content: classViews(data: $abcViewModel.somedata, abcViewModel: abcViewModel))
Spacer()
}
)
}
}
}
}
func classViews(data: Binding<[SomeData]>, abcViewModel: AbcViewModel) -> [AnyView] {
var views: [AnyView] = []
for element in data {
views.append(
VStack(spacing: 0) {
HStack {
print("\(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? "")) )") // printing correct value
Text(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? ""))) // want to observe change here
}
}
.convertToAnyView())
}
return views
}
If you are injecting your AbcViewModel into AbcView you should use #ObserverdObject instead of #StateObject , full explanation here Also you should conform tour AbcViewModel to ObservableObject and make your desired property #Published if you want to trigger the change in View . Here is simplified code example:
Making AbcViewModel observable:
class AbcViewModel: ObservableObject {
#Published var dataID: String = "" //by changing the #Published proprty you trigger change in View using it
}
store AbcViewModel as #ObserverdObject:
struct AbcView: View {
#ObservedObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self.abcViewModel = abcViewModel
}
var body: some View {
//...
}
}
If you now use your AbcViewModel dataID property anywhere in the project, and you change its value, the property will publish the change and your View (struct) will be rebuilded. Use the same pattern for creating TopView and assigning AbcViewModel to it the same way.

How to pass data between ViewModels in SwiftUI

I have this use case where I have a parent view and a child view. Both of the views have their own corresponding ViewModels.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
ParentViewModel:
class ParentViewModel: ObservableObject {
#Published var newListName: String = ""
func saveList() {
// some logic to save to CoreData, method would be called via a button
// how do I reference "someString" from ChildViewModel in this ViewModel?
}
}
ChildView:
struct ChildView: View {
#StateObject var childViewModel = ChildViewModel()
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
childViewModel.alterData()
}
}
}
}
}
ChildViewModel:
class ChildViewModel: ObservableObject {
#Published var someString: String = ""
func alterData() {
someString = "Toast"
}
}
My question now is, how do I pass the new value of "someString" from ChildViewModel into the ParentViewModel, in order to do some further stuff with it?
I've tried to create a #StateObject var childViewModel = ChildViewModel() reference in the ParentViewModel, but that does obviously not work, as this will create a new instance of the ChildViewModel and therefore not know of the changes made to "someString"
Solution:
As proposed by Josh, I went with the approach to use a single ViewModel instead of two. To achieve this, the ParentView needs a .environmentObject(T) modifier.
ParentView:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView()) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}.environmentObject(parentViewModel)
}
The ChildView then references that environment Object via #EnvironmentObject without an initializer:
struct ChildView: View {
#EnvironmentObject var parentViewModel: ParentViewModel
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
parentViewModel.alterData()
}
}
}
}
}
Most likely you would use a binding for this situation:
struct ChildView: View {
#Binding var name: String
var body: some View {
NavigationView {
List{
Text("Some element")
.onTapGesture {
name = "Altered!"
}
}
}
}
}
And in the parent:
struct ParentView: View {
#StateObject var parentViewModel = ParentViewModel()
var body: some View {
NavigationView {
List {
TextField("Add Name", text: $parentViewModel.newListName)
NavigationLink(destination: ChildView(name: $parentViewModel.newListName)) {
Label("Select Products", systemImage: K.ListIcons.productsNr)
}
}
}
}
Also, I think you can remove the NavigationView view from ChildView. Having it ParentView is enough.

Why is withAnimation changing how these references are managed?

In the following code, including one less line in a withAnimation block triggers an unowned reference fault. Dismissing the view shown by button A and B should trigger the addition of a new element in both case. However, B causes the reference to the items list to be invalid:
struct ListItem: Identifiable, Equatable {
var id = UUID()
var name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct DetailView: View {
#Binding var item: ListItem
var body: some View {
VStack(alignment: .leading) {
TextField("Name", text: self.$item.name)
}
}
}
struct MyListView: View {
#StateObject var myList = MyListClass()
#State private var itemDraft = ListItem(name: "Foo")
#State private var showingA = false
#State private var showingB = false
var body: some View {
NavigationView {
List(myList.items) { item in
// Pass binding to item into DetailsView
Text(item.name)
}
.navigationBarItems(
leading: Button(action: {
showingA.toggle()
}) {
Text("A")
},
trailing: Button(action: {
showingB.toggle()
}) {
Text("B")
}
)
.sheet(isPresented: $showingA) {
DetailView(item: $itemDraft)
.onDisappear { // Closure A
let newItem = itemDraft
withAnimation {
itemDraft = ListItem(name: "Bar")
myList.items.append(newItem) // <- here
}
}
}
.sheet(isPresented: $showingB) {
DetailView(item: $itemDraft)
.onDisappear { // Closure B
let newItem = itemDraft
itemDraft = ListItem(name: "Bar")
withAnimation {
myList.items.append(newItem) // <- and here
}
}
}
}
}
}
The error is
Attempted to read an unowned reference but the object was already deallocated
What is the difference in references management between closures A (annotation above) and B? (Bonus points: what tool of xcode is the most useful to find the source of that bug?)

How to use #State if #Binding not provided in the initializer

Imagine a view with some #Binding variables:
init(isEditing: Binding<Bool>, text: Binding<Bool>)
How can we have the selection working with an internal #State if it is not provided in the initializer?
init(text: Binding<Bool>)
This is how to make TextField become first responder in SwiftUI
Note that I know we can pass a constant like:
init(isEditing: Binding<Bool> = .constant(false), text: Binding<Bool>)
But!
This will kill the dynamicity of the variable and it won't work as desire. Imagine re-inventing the isFirstResponder of the UITextField.
It can't be .constant(false). The keyboard will be gone on each view update.
It can't be .constant(true). The view will take the keyboard on each view update.
Maybe! Apple is doing it somehow with TabView.
One solution is to pass an optional binding and use a local state variable if the binding is left nil. This code uses a toggle as an example (simpler to explain) and results in two interactive toggles: one being given a binding and the other using local state.
import SwiftUI
struct ContentView: View {
#State private var isOn: Bool = true
var body: some View {
VStack {
Text("Special toggle:")
SpecialToggle(isOn: $isOn)
.padding()
SpecialToggle()
.padding()
}
}
}
struct SpecialToggle: View {
/// The binding being passed from the parent
var isOn: Binding<Bool>?
/// The fallback state if the binding is left `nil`.
#State private var defaultIsOn: Bool = true
/// A quick wrapper for accessing the current toggle state.
var toggleIsOn: Bool {
return isOn?.wrappedValue ?? defaultIsOn
}
init(isOn: Binding<Bool>? = nil) {
if let isOn = isOn {
self.isOn = isOn
}
}
var body: some View {
Toggle(isOn: isOn ?? $defaultIsOn) {
Text("Dynamic label: \(toggleIsOn.description)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You may create separate #State and #Binding properties and sync them using onChange or onReceive:
struct TestView: View {
#State private var selectionInternal: Bool
#Binding private var selectionExternal: Bool
init() {
_selectionInternal = .init(initialValue: false)
_selectionExternal = .constant(false)
}
init(selection: Binding<Bool>) {
_selectionInternal = .init(initialValue: selection.wrappedValue)
_selectionExternal = selection
}
var body: some View {
if #available(iOS 14.0, *) {
Toggle("Selection", isOn: $selectionInternal)
.onChange(of: selectionInternal) {
selectionExternal = $0
}
} else {
Toggle("Selection", isOn: $selectionInternal)
.onReceive(Just(selectionInternal)) {
selectionExternal = $0
}
}
}
}
struct ContentView: View {
#State var selection = false
var body: some View {
VStack {
Text("Selection: \(String(selection))")
TestView(selection: $selection)
TestView()
}
}
}

Why the first item of the list is displayed all the on the opened sheet

I am passing binding variable into other view:
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var isSheetIsVisible = false
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.isSheetIsVisible.toggle()
}
.sheet(isPresented: self.$isSheetIsVisible){
PocketDetailsView(pocketItem: self.$pocket.pockets[index])
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
}
}
}
the other view is:
struct PocketDetailsView: View {
#Binding var pocketItem: PocketItem
var body: some View {
Text("\(pocketItem.name)")
}
}
Why I see the first item when i open sheet for second or third row?
When I use NavigationLink instead of the .sheet it works perfect
You activate all sheets at once, try the following approach (I cannot test your code, but the idea should be clear)
struct PocketlistView: View {
#ObservedObject var pocket = Pocket()
#State var selectedItem: PocketItem? = nil
var body: some View {
NavigationView{
List{
ForEach(Array(pocket.pockets.enumerated()), id: \.element.id) { (index, pocketItem) in
VStack(alignment: .leading){
Text(pocketItem.name).font(.headline)
Text(pocketItem.type).font(.footnote)
}
.onTapGesture {
self.selectedItem = pocketItem
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Pockets")
.sheet(item: self.$selectedPocket) { item in
PocketDetailsView(pocketItem:
self.$pocket.pockets[self.pocket.pockets.firstIndex(of: item)!])
}
}
}
}

Resources