How to pass variable between multiple classes and views in Swift? - swift

I have one View and two classes (Cls1 and Cls2).
Object of Class1 initialised in view like:
#ObservedObject var cls1 = Cls1()
And I have no problems to get variables of clsObj declaring them inside class as:
#Published var myVarFromCls1 = false
However, I have Cls2, it was initialised inside Cls1:
var cls2 = Cls2()
and I need to get changes in variables of cls2 inside my view.
I've tried to redeclare cls2 initialization to:
#Published var cls2 = Cls2()
But I can't get its variable in view using:
#ObservedObject var cls1 = Cls1()
$cls1.cls2.myVarFromCls2
declared in Class2:
#Published var myVarFromCls2 = false
How to do it right way?

At the end of Cls1's initializer, add:
cancellable = cls2.objectWillChange.forwardedThroughObjectWillChange(of: self)
}
private var cancellable: AnyCancellable!
import Combine
public extension Publisher<Void, Never> {
/// Forward output through an `ObservableObject`'s `objectWillChange`.
func forwardedThroughObjectWillChange<Object: ObservableObject>(of object: Object) -> AnyCancellable
where Object.ObjectWillChangePublisher == ObservableObjectPublisher {
sink { [unowned object] in object.objectWillChange.send() }
}
}
You can use the same idea for e.g. "[some ObservableObject]" situations.
import Combine
import SwiftUI
#propertyWrapper
public final class ObservableObjects<Objects: Sequence>: ObservableObject
where
Objects.Element: ObservableObject,
Objects.Element.ObjectWillChangePublisher == ObservableObjectPublisher
{
public init(wrappedValue: Objects) {
self.wrappedValue = wrappedValue
assignCancellable()
}
#Published public var wrappedValue: Objects {
didSet { assignCancellable() }
}
private var cancellable: AnyCancellable!
}
// MARK: - private
private extension ObservableObjects {
func assignCancellable() {
cancellable = Publishers.MergeMany(wrappedValue.map(\.objectWillChange))
.forwardedThroughObjectWillChange(of: self)
}
}
// MARK: -
#propertyWrapper
public struct ObservedObjects<Objects: Sequence>: DynamicProperty
where
Objects.Element: ObservableObject,
Objects.Element.ObjectWillChangePublisher == ObservableObjectPublisher
{
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
#ObservedObject private var objects: ObservableObjects<Objects>
}
#propertyWrapper
public struct StateObjects<Objects: Sequence>: DynamicProperty
where
Objects.Element: ObservableObject,
Objects.Element.ObjectWillChangePublisher == ObservableObjectPublisher
{
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}
public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}
public var projectedValue: Binding<Objects> { $objects.wrappedValue }
#StateObject private var objects: ObservableObjects<Objects>
}

Related

SwiftUI toggles not changing when clicked on

I'm trying to build a simple SwiftUI view that displays a number of toggles. While I can get everything to display ok, I cannot get the toggles to flip. Here's is a simplified code example:
import SwiftUI
class StoreableParam: Identifiable {
let name: String
var id: String { name }
#State var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
init(name: String, isEnabled: Bool, toggleAction: ((Bool) -> Void)? = nil) {
self.name = name
self.isEnabled = isEnabled
self.toggleAction = toggleAction
}
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach(viewModel.storeParams, id: \.id) { storeParam in
Toggle(storeParam.name, isOn: storeParam.$isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}.navigationBarTitle("Toggle Example")
}
}
As mentioned in the comments, there are a couple of things going on:
#State is only for use in a View
Your model should be a struct
Then, you can get a Binding using the ForEach element binding syntax:
struct StoreableParam: Identifiable {
let name: String
var id: String { name }
var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach($viewModel.storeParams, id: \.id) { $storeParam in
Toggle(storeParam.name, isOn: $storeParam.isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}
}
}

How to create a generic persistent array of ObservableObjects in Swift

I have a class that implements an array of ObservableObjects that will save itself to persistent storage whenever one of the objects changes. Here is a very simplified version of the working code:
class MyObject : ObservableObject {
#Published var name: String = ""
// Lots of other #Published variables
}
class MyArray {
#Published var objects: [MyObject] = []
#Published private var objectWillChange: Void = ()
private var objectChanged: AnyCancellable?
init() {
loadFromStorage()
for object in objects {
object.objectWillChange.assign(to: &$objectWillChange)
}
objectChanged = $objectWillChange.dropFirst().sink() {
self.saveToStorage()
}
}
func loadFromStorage() {
// Loads objects from persistent storage
}
func saveToStorage() {
// Saves objects to persistent storage
}
}
This all works fine. Now I would like to create a generic version of this code, but I can't get it to compile.
Here is what I have so far
class MyPersistentArray: PersistentArray<MyObject> {
}
class PersistentArray<Object: ObservableObject> {
#Published var objects: [Object] = []
#Published private var objectWillChange: Void = ()
private var objectChanged: AnyCancellable?
init() {
loadFromStorage()
for object in objects {
object.objectWillChange.assign(to: &$objectWillChange) // DOES NOT COMPILE
}
objectChanged = $objectWillChange.dropFirst().sink() {
self.saveToStorage()
}
}
func loadFromStorage() {
// Loads objects from persistent storage
}
func saveToStorage() {
// Saves objects to persistent storage
}
}
In Xcode 14.2, the call to object.objectWillChange gives the following compiler error:
Cannot convert parent type 'Published<Void>' to expected type 'Published<Object.ObjectWillChangePublisher.Output>'
How can I fix this compiler error?
I will answer my own question. I figured out that I had to constrain the ObservableObject using a where clause to limit the type of ObjetWillChangePublisher.Output to Void, like this:
protocol PersistentObject: ObservableObject where ObjectWillChangePublisher.Output == Void {
}
class MyObject : PersistentObject {
#Published var name: String = ""
// Lots of other #Published variables
}
class MyPersistentArray: PersistentArray<MyObject> {
}
class PersistentArray<Object: PersistentObject> {
#Published var objects: [Object] = []
#Published private var objectWillChange: Void = ()
private var objectChanged: AnyCancellable?
init() {
loadFromStorage()
for object in objects {
object.objectWillChange.assign(to: &$objectWillChange)
}
objectChanged = $objectWillChange.dropFirst().sink() { _ in
self.saveToStorage()
}
}
func loadFromStorage() {
// Loads objects from persistent storage
}
func saveToStorage() {
// Saves objects to persistent storage
}
}

How to pass a Publisher to a class

I have a model class which holders a few publishers as the source of truth. And I also have a few classes that processes data. I need to process data depending on a publisher from the model class.
class Model: ObservableObject {
#Published var records = [Record]()
let recordProcessor: RecordProcessor
init() {
...
}
}
class RecordProcessor: ObservableObject {
#Published var results = [Result]()
}
struct RootView: View {
var body: some View {
MyView()
.environmentObject(Model())
}
}
struct MyView: View {
#EnvironmentObject var model: Model
var body: some View {
ForEach(model.recordProcessor.results) { ... }
}
}
RecordProcessor does a lot of work on records so the work is encapsulated into a class, but the input is the records stored on the Model. What is a proper way of passing in the records to the RecordProcessor?
Assuming Record is a value type, here is possible way (as far as I understood your goal)
class Model: ObservableObject {
#Published var records = [Record]() {
didSet {
recordProcessor.process(records) // << something like this
}
}
let recordProcessor: RecordProcessor
init(processor: RecordProcessor) {
self.recordProcessor = processor
}
}
class RecordProcessor: ObservableObject {
#Published var results = [Result]()
func process(_ records: [Record]) {
}
}
The best I'm come up with is to have the RecordProcessor process the data and return a publisher with the results:
struct Record {}
struct Result {}
class Model: ObservableObject {
#Published var records = [Record]()
#Published var results = [Result]()
let recordProcessor = RecordProcessor()
var cancellables = Set<AnyCancellable>()
init() {
$records
.map(recordProcessor.process)
.switchToLatest()
.sink {
self.results = $0
}
.store(in: &cancellables)
}
}
class RecordProcessor: ObservableObject {
func process(items: [Record]) -> AnyPublisher<[Result], Never> {
return Just([]).eraseToAnyPublisher()
}
}
struct RootView: View {
var body: some View {
MyView()
.environmentObject(Model())
}
}
struct MyView: View {
#EnvironmentObject var model: Model
var body: some View {
ForEach(model.results) { }
}
}

How to connect published properties of model and viewmodel in Swift?

Let's assume a model, which implements the protocol ObservableObject and has got a #Published property name.
// MARK: Model
class ContentSinglePropertyModel: ObservableObject {
#Published public var name: String
}
Now, I would like to display that name in a view and update the view, whenever name in the model changes. Additionally, I would like to use the Model-View-ViewModel (MVVM) pattern to achieve this goal.
// MARK: ViewModel
final class ContentSinglePropertyViewModel: ObservableObject {
private let model: ContentSinglePropertyModel
#Published var name: String = ""
init() {
self.model = ContentSinglePropertyModel()
}
}
// MARK: View
struct ContentSinglePropertyView: View {
#ObservedObject var viewModel: ContentSinglePropertyViewModel
var body: some View {
Text(self.viewModel.name)
}
}
Since I don't like the idea to make the model or it's properties public within the viewmodel, one option is to wrap the model's property name in the viewmodel. My question is: How to connect the name of the model and the viewmodel in the most idiomatic way?
I've came up with the solution to update the viewmodel's property through the use of Combine's assign method:
self.model.$name.assign(to: \.name, on: self).store(in: &self.cancellables)
Is there a better solution?
My working example:
import SwiftUI
import Combine
// MARK: Model
class ContentSinglePropertyModel: ObservableObject {
#Published public var name: String
init() {
self.name = "Initial value"
}
func doSomething() {
self.name = "Changed value"
}
}
// MARK: ViewModel
final class ContentSinglePropertyViewModel: ObservableObject {
private let model: ContentSinglePropertyModel
private var cancellables: Set<AnyCancellable> = []
#Published var name: String = ""
init() {
self.model = ContentSinglePropertyModel()
// glue Model and ViewModel
self.model.$name.assign(to: \.name, on: self).store(in: &self.cancellables)
}
func doSomething() {
self.model.doSomething()
}
}
// MARK: View
struct ContentSinglePropertyView: View {
#ObservedObject var viewModel: ContentSinglePropertyViewModel
var body: some View {
VStack {
Text(self.viewModel.name)
Button("Do something!", action: {
self.viewModel.doSomething()
})
}
}
}
struct ContentSinglePropertyView_Previews: PreviewProvider {
static var previews: some View {
ContentSinglePropertyView(viewModel: .init())
}
}

How to implement a custom property wrapper which would publish the changes for SwiftUI to re-render it's view

Trying to implement a custom property wrapper which would also publish its changes the same way #Publish does.
E.g. allow my SwiftUI to receive changes on my property using my custom wrapper.
The working code I have:
import SwiftUI
#propertyWrapper
struct MyWrapper<Value> {
var value: Value
init(wrappedValue: Value) { value = wrappedValue }
var wrappedValue: Value {
get { value }
set { value = newValue }
}
}
class MySettings: ObservableObject {
#MyWrapper
public var interval: Double = 50 {
willSet { objectWillChange.send() }
}
}
struct MyView: View {
#EnvironmentObject var settings: MySettings
var body: some View {
VStack() {
Text("\(settings.interval, specifier: "%.0f")").font(.title)
Slider(value: $settings.interval, in: 0...100, step: 10)
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView().environmentObject(MySettings())
}
}
However, I do not like the need to call objectWillChange.send() for every property in MySettings class.
The #Published wrapper works well, so I tried to implement it as part of #MyWrapper, but I was not successful.
A nice inspiration I found was https://github.com/broadwaylamb/OpenCombine, but I failed even when trying to use the code from there.
When struggling with the implementation,
I realised that in order to get #MyWrapper working I need to precisely understand how #EnvironmentObject and #ObservedObject subscribe to changes of #Published.
Any help would be appreciated.
Until the https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#referencing-the-enclosing-self-in-a-wrapper-type gets implemented, I came up with the solution below.
Generally, I pass the objectWillChange reference of the MySettings to all properties annotated with #MyWrapper using reflection.
import Cocoa
import Combine
import SwiftUI
protocol PublishedWrapper: class {
var objectWillChange: ObservableObjectPublisher? { get set }
}
#propertyWrapper
class MyWrapper<Value>: PublishedWrapper {
var value: Value
weak var objectWillChange: ObservableObjectPublisher?
init(wrappedValue: Value) { value = wrappedValue }
var wrappedValue: Value {
get { value }
set {
value = newValue
objectWillChange?.send()
}
}
}
class MySettings: ObservableObject {
#MyWrapper
public var interval1: Double = 10
#MyWrapper
public var interval2: Double = 20
/// Pass our `ObservableObjectPublisher` to the property wrappers so that they can announce changes
init() {
let mirror = Mirror(reflecting: self)
mirror.children.forEach { child in
if let observedProperty = child.value as? PublishedWrapper {
observedProperty.objectWillChange = self.objectWillChange
}
}
}
}
struct MyView: View {
#EnvironmentObject
private var settings: MySettings
var body: some View {
VStack() {
Text("\(settings.interval1, specifier: "%.0f")").font(.title)
Slider(value: $settings.interval1, in: 0...100, step: 10)
Text("\(settings.interval2, specifier: "%.0f")").font(.title)
Slider(value: $settings.interval2, in: 0...100, step: 10)
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView().environmentObject(MySettings())
}
}