Passing variable object from View to Observable Object in Swift / SwiftUI - ios

I'm trying to pass a variable object from a SwiftUI View to an observable Object but I'm running into the error: "Cannot use instance member 'loadedGroup' within property initializer; property initializers run before 'self' is available".
Here is how my SwiftUI View class is currently structured
struct LoadedGroupView: View {
#Binding var loadedGroup: group
#StateObject var userData = UserViewModel()
#StateObject var postData = PostViewModel(passedLoadedGroup: loadedGroup) //error here
var body: some View {
...
}
}
Here is my Observable Object class for PostViewModel()
class PostViewModel: ObservableObject {
var loadedGroup: group
let ref = Firestore.firestore()
init(passedLoadedGroup: group) {
group = passedLoadedGroup
}
}
How would I go about fixing this error because I really need to get that value passed into this observable object class from the View somehow. Thanks for the help!

Related

Binding To Array Values SwiftUI iOS

I'm struggling to find how to bind an array value to a Toggle view in SwiftUI.
Lets says I have an observabled class with a Boolean array:
class TestClass: ObservabledObject {
#Published var onStates: [Bool] = [true, false, true]
static let shared = TestClass()
}
and in a View I have
...
Toggle(isOn: TestClass.shared.$onStates[0]) { // Throws error 'Referenceing subscript 'subscript(_:)' requires wrapped value of type '[Bool]'
Text("Example Toggle")
}
Why is it seemingly impossible to bind a particular array value to the toggle button?
Thanks.
We need observer in view for observable object, so fixed variant is
#StateObject var vm = TestClass.shared // << observer
var body: some View {
Toggle(isOn: $vm.onStates[0]) { // << binding via observer
Text("Example Toggle")
}
}

Published object not publishing, am I doing it wrong?

My code looks like this:
final class MyModelController: ObservableObject {
#Published var model = MyModel()
}
enum ButtonSelection: Int {
case left, right
}
final class MyModel {
var buttonSelection: ButtonSelection?
}
I have injected an instance of MyModelController as an #EnvironmentObject into my SwiftUI views.
When I set myModelController.model.buttonSelection, I thought it would update myModelController.model and send out an update because it's marked as #Published. However, it doesn't. How can I fix this?
#Published only detects changes for value types. MyModel is a class, which is a reference type.
If possible, changing MyModel to a struct will fix this. However, if this is not possible, see the rest of this answer.
You can fix it with Combine. The below code will update MyModelController when model (now an ObservableObject) changes.
final class MyModelController: ObservableObject {
#Published var model = MyModel()
init() {
_ = model.objectWillChange.sink { [weak self] in
self?.objectWillChange.send()
}
}
}
/* ... */
final class MyModel: ObservableObject {
#Published var buttonSelection: ButtonSelection?
}

How to add an item to a global list var initted/wrapped in ObservableObject in parent view and have it reflect in ForEach in SwiftUI in a child view?

When I click a button in ContentView.swift, I expect the action triggering an 'append' to a sportList would show the newly added item.. but it doesn't. No compile errors. Clicking on the button does nothing (even though I can see the SportItem being packaged up correctly) in a print statement.
I created a model for an item "SportItem" that has simple properties (e.g. name: String) and a ObservableObject class.
struct SportItem: Identifiable {
let name: String
}
I then create a global sportData variable and an ObservableObject class outside of everything:
var sportData = [
SportItem(name: "Tennis"),
SportItem(name: "Basketball")
]
class SportList: ObservableObject {
#Published var sportList: [SportItem]
init() {
self.sportList = sportData
}
}
In SportListView.swift, I have inside of the body:
#ObservedObject var sportList: SportList = SportList();
...
ForEach(sportList.sportList) {
sport in
VStack(alignment: .leading) {
Text(sport.name)
}
}
SportListView is referenced in ContentView.swift, which has:
var sportList:[SportItem] = sportItems
that SportList() using:
SportListView(sportList: SportList())
I also have a button in the parent ContentView.swift file where I have a button where the action of it performs a:
SportList().sportData.append(SportItem(name: "Soccer"))
When I click on that button, I notice the SportListView in the simulator does not add the new item. How do I get the list to be updated to show "Soccer" added onto the list?
You should keep sportData inside SportList (if needed via shared instance), like
class SportList: ObservableObject {
static let shared = SportList()
#Published var sportData: [SportItem] = [ SportItem(name: "Tennis"), SportItem(name: "Basketball") ]
}
and then
#ObservedObject var sportList: SportList = SportList.shared
...
ForEach(sportList.sportData) {
sport in
VStack(alignment: .leading) {
Text(sport.name)
}
}
and add like (if it is somewhere externally of view)
SportList.shared.sportData.append(SportItem(name: "Soccer"))

How do I get get set to work with protocols?

I'm confused when using get set in protocols. Using only get works fine, but the set part doesnt'.
protocol MainViewModelProtocol {
var localDoor: LocalDoorCoreDataObject { get set }
}
extension MainViewModelProtocol {
var localDoor: LocalDoorCoreDataObject {
get { return MainViewModel.instance.localDoor }
set { localDoor = newValue }
}
}
final class MainViewModel: MainViewModelProtocol {
var localDoor: LocalDoorCoreDataObject = LocalDoorCoreDataObject()
...
Then when I use it in the viewController
self.mainViewModel.localDoor = $0
But this gives me the error
Cannot assign to property: 'mainViewModel' is a get-only property
How do I set it up properly?
EDIT
Initiation of the viewModel is done with factory based dependency injection
protocol MainViewModelInjected {
var mainViewModel: MainViewModelProtocol { get }
}
extension MainViewModelInjected {
var mainViewModel: MainViewModelProtocol { return MainViewModel.instance }
}
It is totally depends on how you create object for mainViewModel.
Let's create some cases with your code:
import UIKit
typealias LocalDoorCoreDataObject = String
protocol MainViewModelProtocol {
var localDoor: LocalDoorCoreDataObject { get set }
}
extension MainViewModelProtocol {
var localDoor: LocalDoorCoreDataObject {
get { return MainViewModel.instance.localDoor }
set { localDoor = newValue }
}
}
final class MainViewModel: MainViewModelProtocol {
static let instance = MainViewModel()
var localDoor: LocalDoorCoreDataObject = LocalDoorCoreDataObject()
}
protocol MainViewModelInjected {
var mainViewModel: MainViewModelProtocol { get }
}
extension MainViewModelInjected {
var mainViewModel: MainViewModelProtocol { return MainViewModel.instance }
}
Case 1
Here we are creating an object and assigning object through getter as a closure.
So, here mainViewModel has only getter not setter i.e. it'a get-only property.
var mainViewModel: MainViewModelProtocol { MainViewModel.instance }
mainViewModel.localDoor = "assign some thing" // Error: Cannot assign to property: 'mainViewModel' is a get-only property
Case 2
Here we are directly assigning object to mainViewModelOther. So, this will be a normal property and you can make changes in properties of model.
var mainViewModelOther: MainViewModelProtocol = MainViewModel.instance
mainViewModelOther.localDoor = "assign some thing"
Case 3
You can also create a class that will hold your model object, and created another object of your class. You can make changes in properties of model.
class MyClass {
var mainViewModel: MainViewModelProtocol = MainViewModel.instance
}
let myClassObj = MyClass()
myClassObj.mainViewModel.localDoor = "assign some thing"
TL;DR
Mark your MainViewModelProtocol as being class-only (i.e. protocol MainViewModelProtocol: class { ... }) to solve the issue.
The long answer
To understand why marking your MainViewModelProtocol as class-only fixes the problem, we need to take couple steps back and look at how structs and classes are stored internally.
Case 1: MainViewModelProtocol is a reference-type (i.e. class)
First, let's consider the case where MainViewModel is a class: Classes are reference-types, which means that after you retrieve the your view model through the mainViewModel property, you have a pointer to the same view model that is stored inside your view controller. Modifying the referenced type will also modify the view model of the view itself (since they both point to the same object). As an example
/* ... */
class MainViewModel: MainViewModelProtocol { /* ... */ }
var viewModel = myViewController.mainViewModel
viewModel.localDoor = /* something */
modifies the view model that's shared between the local variable viewModel and the view controller. This is exactly what you want.
Case 2: MainViewModelProtocol is a value type (i.e. struct)
Now let's consider if the MainViewModel was a struct: structs are value-types, so retrieving the view model through the mainViewModel computed property essentially clones the view model. Now you might modify the retrieved view model as much as you like locally, but there is no way assign it back to your view controller
/* ... */
struct MainViewModel: MainViewModelProtocol { /* ... */ }
var viewModel = myViewController.mainViewModel
viewModel.localDoor = /* something */
just modifies the local copy of the view model stored in the viewModel variable. There is no way to assign the local variable back to myViewController.
Conclusion
I hope this illustrates why your pattern only works with reference-types and not value types.
Now the Swift compiler needs to be conservative and consider both cases since it doesn't know if all types conforming to MainViewModelProtocol will be classes or structs (consider public protocols vended as a library to which library-users can conform). If you add the class-constraint to the protocol, you tell the compiler that using the pattern from Case 1 is totally fine – just grab a shared instance and modify it – and that there is no need for a setter to modify the view model.
No need to mark MainViewModelProtocol as class only, when the compiler says :
Cannot assign to property: 'mainViewModel' is a get-only property
it's actually complaining about your view controller implementation. I assume mainViewModel is a computed property so you can't assign it.
I managed to reproduce your error with the following playground :
typealias LocalDoorCoreDataObject = String
protocol MainViewModelProtocol {
var localDoor: LocalDoorCoreDataObject { get set }
}
extension MainViewModelProtocol {
var localDoor: LocalDoorCoreDataObject {
get { return MainViewModel.instance.localDoor }
set { localDoor = newValue }
}
}
final class MainViewModel: MainViewModelProtocol {
static let instance = MainViewModel()
var localDoor: LocalDoorCoreDataObject = LocalDoorCoreDataObject()
}
final class FakeVC {
var mainViewModel: MainViewModelProtocol {
MainViewModel.instance
}
}
var viewController = FakeVC()
viewController.mainViewModel.localDoor = "foo" // Cannot assign to property: 'mainViewModel' is a get-only property
I got rid of the error by changing FakeVC implementation to :
final class FakeVC {
var mainViewModel: MainViewModelProtocol = MainViewModel()
}

#Published for a computed property (or best workaround)

I'm trying to build an app with SwiftUI, and I'm just getting started with Combine framework. My first simple problem is that I'd like a single variable that defines when the app has been properly initialized. I'd like it to be driven by some nested objects, though. For example, the app is initialized when the account object is initialized, the project object is initialized, etc. My app could then use GlobalAppState.isInitialized, instead of inspected each nested object.
class GlobalAppState: ObservableObject {
#Published var account: Account = Account()
#Published var project: Project = Project()
#Published var isInitialized: Bool {
return self.account.initialized && self.project.initialized;
}
}
I get the error Property wrapper cannot be applied to a computed property
So...clearly, this is currently disallowed. Is there a way I can work around this??? I'd like to be able to use GlobalAppState.initialized as a flag in the app. More to the point, something like GlobalAppState.project.currentProject, which would be a computed property returning the currently selected project, etc...
I can see this pattern being used in a thousand different places! Any help would be wildly appreciated...
Thanks!
In this case there's no reason to use #Published for the isInialized property since it's derived from two other Published properties.
var isInitialized: Bool {
return self.account.initialized && self.project.initialized;
}
Here is one case if both account and project are structures.
struct Account{
var initialized : Bool = false
}
struct Project{
var initialized : Bool = false
}
class GlobalAppState: ObservableObject {
#Published var account: Account = Account()
#Published var project: Project = Project()
#Published var isInitialized: Bool = false
var cancellabel: AnyCancellable?
init(){
cancellabel = Publishers.CombineLatest($account, $project).receive(on: RunLoop.main).map{
return ($0.0.initialized && $0.1.initialized)
}.eraseToAnyPublisher().assign(to: \GlobalAppState.isInitialized, on: self) as AnyCancellable
}
}
struct GlobalAppStateView: View {
#ObservedObject var globalAppState = GlobalAppState()
var body: some View {
Group{
Text(String(globalAppState.isInitialized))
Button(action: { self.globalAppState.account.initialized.toggle()}){ Text("toggle Account init")}
Button(action: { self.globalAppState.project.initialized.toggle()}){Text("toggle Project init")}
}
}
}

Resources