Method with No observable object - ios14

I have a class method that apparently doesn't see the #EnvironmentObject var categories: Categories object at the top of the class. I know that this works since I use it in several other files. It also means that my coding in SceneDelegate is correct. The software is crashing with the error: Thread 1: Fatal error: No ObservableObject of type Categories found. A View.environmentObject(_:) for Categories may be missing as an ancestor of this view. The error is occurring in the method updateTotals() in the "for" loop
struct CatItem: Codable, Identifiable {
var id = UUID()
var catNum: Int
var catName: String <-- this is the class I'm trying to reference
var catTotal: Double
var catPix: String
var catShow: Bool
}
class Categories: ObservableObject {
#Published var catItem: [CatItem]
}
class BaseCurrency: ObservableObject {
#EnvironmentObject var userData: UserData
#EnvironmentObject var categories: Categories
var foundNew: Bool = false
var newRate: Double = 0.0
var baseCur: BaseModel
//-----------------------------------------------------
// new base currency so need to update the system totals
//-----------------------------------------------------
func updateTotals() -> () {
for index in 0...(categories.catItem.count - 1) { <-- error here
categories.catItem[index].catTotal *= self.newRate
}
userData.totalArray[grandTotal] *= self.newRate
userData.totalArray[transTotal] *= self.newRate
userData.totalArray[userTotal] *= self.newRate
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(self.baseCur) {
UserDefaults.standard.set(encoded, forKey: "base")
}
self.foundNew = false
}
}

I was reading somewhere recently that #EnvironmentObject is just like #State in that a change in either parameter will cause body to update the view. Therefore neither of these should be in a class. I have reorganized my software and have not seen the error since.

Related

How to convert Binding array value to Binding<[type]> instead of [Binding<type>] in Swift/SwiftUI

I have trouble with getting Binding values in correct format out of dataModel.
The XCode error I am getting is
"Cannot convert value of type '[Binding<String?>]' to expected argument type
'Binding<[String]>'"
public final class RoomSelectionDataModel: ObservableObject {
#Published var roomsList: [Room]
#Published var selectedRoom: String?
}
public struct RoomSelectionView: View {
#StateObject var dataModel: RoomSelectionDataModel
public var body: some View {
let bindingArray = $dataModel.roomsList.compactMap { $0.name } // here the format is '[Binding<String?>]' instead of 'Binding<[String]>'
SelectionList(listElements: bindingArray,
backgroundColor: Color(red: 0.94, green: 0.94, blue: 0.94), // color to be removed
selectedElement: $dataModel.selectedRoom)
}
public init(dataModel: RoomSelectionDataModel) {
self._dataModel = StateObject(wrappedValue: dataModel)
}
}
public struct SelectionList: View {
let backgroundColor: Color
#Binding var listElements: [String]
#Binding var selectedElement: String?
public init(listElements: Binding<[String]>, backgroundColor: Color, selectedElement: Binding<String?>) {
self._listElements = listElements
self.backgroundColor = backgroundColor
self._selectedElement = selectedElement
}
}
public class Room: NSObject, Codable {
public var name: String?
}
The models are simplified here, to only have the relevant information.
Lets try explicitly creating the binding array as it seems you wish to:
let bindingArray = Binding<[String]>(
get: { dataModel.roomsList.compactMap { $0.name } },
set: { newValue in
let rooms = dataModel.roomsList.filter { $0.name != nil }
rooms.enumerated().forEach { numberedPair in
numberedPair.element.name = newValue[numberedPair.offset]
}
})
The get side of the binding is pretty straightforward. When you do the put side, things get a little more complicated. What my code does is assumes that the rooms are in the same order, that all the rooms which had names last time still have names, and that you're going to assign the names into the rooms that have them in their ordinal positions based on the new array.
All in all it's not clear why you want a Binding on the array because the put operation of [String] doesn't really match the model here.

Struct initialization in SwiftUI: 'self' used before all stored properties are initialized

I'm trying to pass a Binding to my VM which is supposed to be a filter so the VM fetches objects according to the filtering passed by params.
Unfortunately, I'm not able to initialize the VM, as I'm getting the error 'self' used before all stored properties are initialized in the line where I'm initializing my VM self.jobsViewModel = JobsViewModel(jobFilter: $jobFilter)
struct JobsTab: View {
#ObservedObject var jobsViewModel: JobsViewModel
#ObservedObject var categoriesViewModel: CategoriesViewModel
#StateObject var searchText: SearchText = SearchText()
#State private var isEditing: Bool
#State private var showFilter: Bool
#State private var jobFilter: JobFilter
init() {
self.categoriesViewModel = CategoriesViewModel()
self.jobFilter = JobFilter(category: nil)
self.showFilter = false
self.isEditing = false
self.jobsViewModel = JobsViewModel(jobFilter: $jobFilter)
}
I think I'm initializing all the vars, and self.searchText isn't in the init block because the compiler complains that it is a get-only property.
Is there any other way to do this?
Thanks!
EDIT: Here's my VM:
class JobsViewModel: ObservableObject {
#Published var isLoading: Bool = false
#Published var jobs: [Jobs] = []
#Binding var jobFilter: JobFilter
init(jobFilter: Binding<JobFilter>) {
_jobFilter = jobFilter
}
...
}
struct JobFilter {
var category: Category?
}
My idea was to have the job filter as a state in the JobsTab, and every time that state changes, the VM would try to fetch the jobs that match the JobFilter
You shouldn't create #ObservedObject values in your initializer. Doing so leads to bugs, because you'll create new instances every time the view is recreated. Either jobsViewModel and categoriesViewModel should be passed as arguments to init, or you should be using #StateObject for those properties.
But anyway, you actually asked: why can't we use $jobFilter before initializing jobsViewModel?
Let's start by simplifying the example:
struct JobsTab: View {
#State var jobFilter: String
var jobFilterBinding: Binding<String>
init() {
jobFilter = ""
jobFilterBinding = $jobFilter
// ^ 🛑 'self' used before all stored properties are initialized
}
var body: some View { Text("hello") }
}
So, what's going on here? It'll help if we “de-sugar” the use of #State. The compiler transforms the declaration of jobFilter into three properties:
struct JobsTab: View {
private var _jobFilter: State<String>
var jobFilter: String {
get { _jobFilter.wrappedValue }
nonmutating set { _jobFilter.wrappedValue = newValue }
}
var $jobFilter: Binding<String> {
get { _jobFilter.projectedValue }
}
var jobFilterBinding: Binding<String>
init() {
_jobFilter = State<String>(wrappedValue: "")
jobFilterBinding = $jobFilter
// ^ 🛑 'self' used before all stored properties are initialized
}
var body: some View { Text("hello") }
}
Notice now that $jobFilter is not a stored property. It is a computed property. So accessing $jobFilter means calling its “getter”, which is a method on self. But we cannot call a method on self until self is fully initialized. That's why we get an error if we try to use $jobFilter before initializing all stored properties.
The fix is to avoid using $jobFilter. Instead, we can use _jobFilter.projectedValue directly:
struct JobsTab: View {
#State var jobFilter: String
var jobFilterBinding: Binding<String>
init() {
jobFilter = ""
jobFilterBinding = _jobFilter.projectedValue
}
var body: some View { Text("hello") }
}

Cannot convert value of type 'Published<Bool>.Publisher' to expected argument type 'Binding<Bool>'

When trying to compile the following code:
class LoginViewModel: ObservableObject, Identifiable {
#Published var mailAdress: String = ""
#Published var password: String = ""
#Published var showRegister = false
#Published var showPasswordReset = false
private let applicationStore: ApplicationStore
init(applicationStore: ApplicationStore) {
self.applicationStore = applicationStore
}
var passwordResetView: some View {
PasswordResetView(isPresented: $showPasswordReset) // This is where the error happens
}
}
Where PasswordResetView looks like this:
struct PasswordResetView: View {
#Binding var isPresented: Bool
#State var mailAddress: String = ""
var body: some View {
EmptyView()
}
}
}
I get the error compile error
Cannot convert value of type 'Published<Bool>.Publisher' to expected argument type 'Binding<Bool>'
If I use the published variable from outside the LoginViewModel class it just works fine:
struct LoginView: View {
#ObservedObject var viewModel: LoginViewModel
init(viewModel: LoginViewModel) {
self.viewModel = viewModel
}
var body: some View {
PasswordResetView(isPresented: self.$viewModel.showPasswordReset)
}
}
Any suggestions what I am doing wrong here? Any chance I can pass a published variable as a binding from inside the owning class?
Thanks!
Not sure why the proposed solutions here are so complex, when there is a very direct fix for this.
Found this answer on a similar Reddit question:
The problem is that you are accessing the projected value of an #Published (which is a Publisher) when you should instead be accessing the projected value of an #ObservedObject (which is a Binding), that is: you have globalSettings.$tutorialView where you should have $globalSettings.tutorialView.
** Still new to Combine & SwiftUI so not sure if there is better way to approach **
You can initalize Binding from publisher.
https://developer.apple.com/documentation/swiftui/binding/init(get:set:)-6g3d5
let binding = Binding(
get: { [weak self] in
(self?.showPasswordReset ?? false)
},
set: { [weak self] in
self?.showPasswordReset = $0
}
)
PasswordResetView(isPresented: binding)
I think the important thing to understand here is what "$" does in the Combine context.
What "$" does is to publish the changes of the variable "showPasswordReset" where it is being observed.
when it precedes a type, it doesn't represent the type you declared for the variable (Boolean in this case), it represents a publisher, if you want the value of the type, just remove the "$".
"$" is used in the context where a variable was marked as an #ObservedObject,
(the ObservableObject here is LoginViewModel and you subscribe to it to listen for changes in its variables market as publishers)
struct ContentView: View {
#ObservedObject var loginViewModel: LoginViewModel...
in that context (the ContentView for example) the changes of "showPasswordReset" are going to be 'Published' when its value is updated so the view is updated with the new value.
Here is possible approach - the idea to make possible observation in generated view and avoid tight coupling between factory & presenter.
Tested with Xcode 12 / iOS 14 (for older systems some tuning might be needed)
protocol ResetViewModel {
var showPasswordReset: Bool { get set }
}
struct PasswordResetView<Model: ResetViewModel & ObservableObject>: View {
#ObservedObject var resetModel: Model
var body: some View {
if resetModel.showPasswordReset {
Text("Show password reset")
} else {
Text("Show something else")
}
}
}
class LoginViewModel: ObservableObject, Identifiable, ResetViewModel {
#Published var mailAdress: String = ""
#Published var password: String = ""
#Published var showRegister = false
#Published var showPasswordReset = false
private let applicationStore: ApplicationStore
init(applicationStore: ApplicationStore) {
self.applicationStore = applicationStore
}
var passwordResetView: some View {
PasswordResetView(resetModel: self)
}
}
For error that states: "Cannot convert value of type 'Binding' to expected argument type 'Bool'" solution is to use wrappedValue as in example below.
If you have MyObject object with property isEnabled and you need to use that as vanilla Bool type instead of 'Binding' then do this
myView.disabled($myObject.isEnabled.wrappedValue)

Swift How can I convert String from class into #State String variable?

How can I convert this variable from my class into the #State variable in my View? I am getting an exception right now:
Cannot assign value of type 'Binding' to type 'String'
Inside my View:
struct ContentView: View {
//Error private members
#State var alert = false
#State var error = ""
#EnvironmentObject var manager: HttpAuth
func verify(){
//Cannot assign value of type Binding<String> to type String
self.error = $manager.error
}
}
Inside my HttpAuth class
class HttpAuth: ObservableObject{
//Error private members
var alert = false
var error: String = ""
..
}
You don't need binding here, #State is a property wrapper, so assign like property
func verify(){
self.error = manager.error // no $
}
Note: actually you can use manager.error directly, w/o remapping it to provide state, just make it #Published
class HttpAuth: ObservableObject{
//Error private members
var alert = false
#Published var error: String = ""
the only caution(!) you must modify this published error only on main queue, and then all usage inside View will be updated automatically.

#Published property wrapper not working on subclass of ObservableObject

I have a class conforming to the #ObservableObject protocol and created a subclass from it with it's own variable with the #Published property wrapper to manage state.
It seems that the #published property wrapper is ignored when using a subclass. Does anyone know if this is expected behaviour and if there is a workaround?
I'm running iOS 13 Beta 8 and xCode Beta 6.
Here is an example of what I'm seeing. When updating the TextField on MyTestObject the Text view is properly updated with the aString value. If I update the MyInheritedObjectTextField the anotherString value isn't updated in the Text view.
import SwiftUI
class MyTestObject: ObservableObject {
#Published var aString: String = ""
}
class MyInheritedObject: MyTestObject {
#Published var anotherString: String = ""
}
struct TestObserverWithSheet: View {
#ObservedObject var myTestObject = MyInheritedObject()
#ObservedObject var myInheritedObject = MyInheritedObject()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")
TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}
Finally figured out a solution/workaround to this issue. If you remove the property wrapper from the subclass, and call the baseclass objectWillChange.send() on the variable the state is updated properly.
NOTE: Do not redeclare let objectWillChange = PassthroughSubject<Void, Never>() on the subclass as that will again cause the state not to update properly.
I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.
Here is a fully working example:
import SwiftUI
class MyTestObject: ObservableObject {
#Published var aString: String = ""
}
class MyInheritedObject: MyTestObject {
// Using #Published doesn't work on a subclass
// #Published var anotherString: String = ""
// If you add the following to the subclass updating the state also doesn't work properly
// let objectWillChange = PassthroughSubject<Void, Never>()
// But if you update the value you want to maintain state
// of using the objectWillChange.send() method provided by the
// baseclass the state gets updated properly... Jaayy!
var anotherString: String = "" {
willSet { self.objectWillChange.send() }
}
}
struct MyTestView: View {
#ObservedObject var myTestObject = MyTestObject()
#ObservedObject var myInheritedObject = MyInheritedObject()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")
TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}
iOS 14.5 resolves this issue.
Combine
Resolved Issues
Using Published in a subclass of a type conforming to ObservableObject now correctly publishes changes. (71816443)
This is because ObservableObject is a protocol, so your subclass must conform to the protocol, not your parent class
Example:
class MyTestObject {
#Published var aString: String = ""
}
final class MyInheritedObject: MyTestObject, ObservableObject {
#Published var anotherString: String = ""
}
Now, #Published properties for both class and subclass will trigger view events
UPDATE
This has been fixed in iOS 14.5 and macOS 11.3, subclasses of ObservableObject will correctly publish changes on these versions. But note that the same app will exhibit the original issues when run by a user on any older minor OS version. You still need the workaround below for any class that is used on these versions.
The best solution to this problem that I've found is as follows:
Declare a BaseObservableObject with an objectWillChange publisher:
open class BaseObservableObject: ObservableObject {
public let objectWillChange = ObservableObjectPublisher()
}
Then, to trigger objectWillChange in your subclass, you must handle changes to both observable classes and value types:
class MyState: BaseObservableObject {
var classVar = SomeObservableClass()
var typeVar: Bool = false {
willSet { objectWillChange.send() }
}
var someOtherTypeVar: String = "no observation for this"
var cancellables = Set<AnyCancellable>()
init() {
classVar.objectWillChange // manual observation necessary
.sink(receiveValue: { [weak self] _ in
self?.objectWillChange.send()
})
.store(in: &cancellables)
}
}
And then you can keep on subclassing and add observation where needed:
class SubState: MyState {
var subVar: Bool = false {
willSet { objectWillChange.send() }
}
}
You can skip inheriting BaseObservableObject in the root parent class if that class already contains #Published variables, as the publisher is then synthesized. But be careful, if you remove the final #Published value from the root parent class, all the objectWillChange.send() in the subclasses will silently stop working.
It is very unfortunate to have to go through these steps, because it is very easy to forget to add observation once you add a variable in the future. Hopefully we will get a better official fix.
This happens also when your class is not directly subclass ObservableObject:
class YourModel: NSObject, ObservableObject {
#Published var value = false {
willSet {
self.objectWillChange.send()
}
}
}
From my experience, just chain the subclass objectWillChange with the base class's objectWillChange like this:
class GenericViewModel: ObservableObject {
}
class ViewModel: GenericViewModel {
#Published var ...
private var cancellableSet = Set<AnyCancellable>()
override init() {
super.init()
objectWillChange
.sink { super.objectWillChange.send() }
.store(in: &cancellableSet)
}
}

Resources