Why does sorted(by:) work but sort(by:) does not in ForEach loop - swift

I am trying to show a list of custom objects in an ordered manner.
While this works...
import SwiftUI
struct MyModel: Identifiable {
let id = UUID()
let timestamp: Date
}
struct ViewModel {
var models = [
MyModel(timestamp: Date().addingTimeInterval(300)),
MyModel(timestamp: Date().addingTimeInterval(100)),
MyModel(timestamp: Date().addingTimeInterval(500))
]
}
struct MyView: View {
#State var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.models.sorted(by: { (lhs, rhs) -> Bool in // will not work with `sort(by:)`
lhs.timestamp > rhs.timestamp
})) { model in
Text("\(model.timestamp)")
}
.onDelete(perform: { indexSet in
//delete from viewModel.models
})
}
}
}
...it falls short when deleting, because the array I am working on is not sorted (only it's visualisation, so to say).
sort(by:) should be what I need, as it "Sorts the collection in place, using the given predicate as the comparison between elements." according to Apple's documentation.
However, the same ForEach statement will not work for sort(by:), throwing the error Cannot convert value of type '()' to expected argument type 'Range<Int>'.
Is there a way to sort in place and loop with ForEach in this case or will I have to handle sorting in my ViewModel?

The reason why you can't use sort(by:) inside the ForEach is because sort(by:) is a mutating function that returns Void. sorted(by:) on the other hand returns a new array that you can iterate through with the ForEach.
You can solve your problem by creating a separate computed property on ViewModel, which return models sorted and calling ForEach with this sorted properties. You just need to make sure you also perform deletion on the sorted array.
struct ViewModel {
var models = [
MyModel(timestamp: Date().addingTimeInterval(300)),
MyModel(timestamp: Date().addingTimeInterval(100)),
MyModel(timestamp: Date().addingTimeInterval(500))
]
var sortedModels: [MyModel] {
get {
models.sorted(by: { $0.timestamp > $1.timestamp })
}
set {
models = newValue
}
}
}
struct MyView: View {
#State var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.sortedModels) { model in
Text("\(model.timestamp)")
}
.onDelete(perform: { indexSet in
for index in indexSet {
viewModel.sortedModels.remove(at: index)
}
})
}
}
}

No need to create a computed property. You can simply sort the models when your list appears. No need to sort it every time you delete a model if they are already sorted. You should also conform your model to comparable protocol:
import SwiftUI
struct ContentView: View {
#State var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.models) {
Text("\($0.timestamp)")
}
.onDelete {
$0.reversed().forEach { viewModel.models.remove(at: $0) }
}
}.onAppear {
viewModel.models.sort(by: >)
}
}
}
struct MyModel: Identifiable {
let id = UUID()
let timestamp: Date
}
extension MyModel: Comparable {
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.timestamp < rhs.timestamp
}
}
struct ViewModel {
var models = [
MyModel(timestamp: Date().addingTimeInterval(300)),
MyModel(timestamp: Date().addingTimeInterval(100)),
MyModel(timestamp: Date().addingTimeInterval(500))
]
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

In addition to the other two answers, I'd like to add some more information regards how you're meant to use ViewModels.
Essentially you want to have a ViewModel that is a ObservableObject with a #Published property for the values you expect to change. That way you can encapsulate any important logic inside the ViewModel.
Having a struct ViewModel doesn't make any sense, as structs aren't mutable.
Furthermore you can implement Comparable on your model type, allowing you to have sorting tied to the type, rather than at random places in your code. Now how it's also not necessary to re-sort the content when you delete individual models if your model is already sorted.
And if you had inserts, you would want to sort them when inserting.
And finally, you should only use #State variables for UI state, not (View)Model state.
import SwiftUI
struct MyModel: Identifiable, Equatable, Comparable {
let id = UUID()
let timestamp: Date
static func < (lhs: MyModel, rhs: MyModel) -> Bool {
return lhs.timestamp > rhs.timestamp
}
}
class ViewModel: ObservableObject {
#Published
private(set) var models: [MyModel]
init() {
let now = Date()
models = [
MyModel(timestamp: now.addingTimeInterval(300)),
MyModel(timestamp: now.addingTimeInterval(100)),
MyModel(timestamp: now.addingTimeInterval(500))
].sorted()
}
func remove(at indicies: IndexSet) {
indicies.forEach { index in
models.remove(at: index)
}
}
}
struct ContentView: View {
#ObservedObject
var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.models) { model in
Text("\(model.timestamp)")
}
.onDelete(perform: { indexSet in
viewModel.remove(at: indexSet)
})
}
}
}

Related

How to trigger automatic SwiftUI Updates with #ObservedObject using MVVM

I have a question regarding the combination of SwiftUI and MVVM.
Before we start, I have read some posts discussing whether the combination of SwiftUI and MVVM is necessary. But I don't want to discuss this here, as it has been covered elsewhere. I just want to know if it is possible and, if yes, how. :)
So here comes the code. I tried to add the ViewModel Layer in between the updated Object class that contains a number that should be updated when a button is pressed. The problem is that as soon as I put the ViewModel Layer in between, the UI does not automatically update when the button is pressed.
View:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#ObservedObject var numberStorage = NumberStorage()
var body: some View {
VStack {
// Text("\(viewModel.getNumberObject().number)")
// .padding()
// Button("IncreaseNumber") {
// viewModel.increaseNumber()
// }
Text("\(numberStorage.getNumberObject().number)")
.padding()
Button("IncreaseNumber") {
numberStorage.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published var number: NumberStorage
init() {
self.number = NumberStorage()
}
func increaseNumber() {
self.number.increaseNumber()
}
func getNumberObject() -> NumberObject {
self.number.getNumberObject()
}
}
Model:
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func getNumberObject() -> NumberObject {
return self.numberObject
}
public func increaseNumber() {
self.numberObject.number+=1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
} ```
Looking forward to your feedback!
I think your code is breaking MVVM, as you're exposing to the view a storage model. In MVVM, your ViewModel should hold only two things:
Values that your view should display. These values should be automatically updated using a binding system (in your case, Combine)
Events that the view may produce (in your case, a button tap)
Having that in mind, your ViewModel should wrap, adapt and encapsulate your model. We don't want model changes to affect the view. This is a clean approach that does that:
View:
struct ContentView: View {
#StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
private var viewModel = ViewModel()
var body: some View {
VStack {
Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published
private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
private let numberStorage = NumberStorage()
init() {
numberStorage.currentNumber
.map { $0.number }
.assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
}
func increaseNumber() {
self.numberStorage.increaseNumber()
}
}
Model:
class NumberStorage {
private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())
var currentNumber: AnyPublisher<NumberObject, Never> {
currentNumberSubject.eraseToAnyPublisher()
}
func increaseNumber() {
let currentNumber = currentNumberSubject.value.number
currentNumberSubject.send(.init(number: currentNumber + 1))
}
}
struct NumberObject: Identifiable { // I'd not use this, just send and int directly
let id = UUID()
var number = 0
}
It's a known problem. Nested observable objects are not supported yet in SwiftUI. I don't think you need ViewModel+Model here since ViewModel seems to be enough.
To make this work you have to trigger objectWillChange of your viewModel manually when objectWillChange of your model is triggered:
class ViewModel: ObservableObject {
init() {
number.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: &cancellables)
}
}
You better listen to only the object you care not the whole observable class if it is not needed.
Plus:
Since instead of injecting, you initialize your viewModel in your view, you better use StateObject instead of ObservedObject. See the reference from Apple docs: Managing model data in your app
One way you could handle this is to observe the publishers in your Storage class and send the objectWillChange publisher when it changes. I have done this in personal projects by adding a class that all my view models inherit from which provides a nice interface and handles the Combine stuff like this:
Parent ViewModel
import Combine
class ViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
func publish<T>(on publisher: Published<T>.Publisher) {
publisher.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
}
}
Specific ViewModel
class ContentViewModel: ViewModel {
private let numberStorage = NumberStorage()
var number: Int { numberStorage.numberObject.number }
override init() {
super.init()
publish(on: numberStorage.$numberObject)
}
func increaseNumber() {
numberStorage.increaseNumber()
}
}
View
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(viewModel.number)")
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
Model/Storage
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func increaseNumber() {
self.numberObject.number += 1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
}
This results in the view re-rendering any time Storage.numberObject changes.

SwiftUI - MVVM - nested components (array) and bindings understanding problem

I just stuck trying to properly implement MVVM pattern in SwiftUI
I got such application
ContainerView is the most common view. It contains single business View Model object - ItemsViewModel and one surrogate - Bool to toggle it's value and force re-render of whole View.
ItemsView contains of array of business objects - ItemView
What I want is to figure out:
how to implement bindings which actually works❓
call back event and pass value from child view to parent❓
I came from React-Redux world, it done easy there. But for the several days I cant figure out what should I do... I chose MVVM though as I also got some WPF experience and thought it'll give some boost, there its done with ObservableCollection for bindable arrays
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
#Published var items: ItemsViewModel;
// variable used to refresh most common view
#Published var refresh: Bool = false;
init() {
self.items = ItemsViewModel();
}
func buttonRefresh_onClick() -> Void {
self.refresh.toggle();
}
func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemViewModel())
}
}
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
#StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: $viewModel.items).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
Button(action: viewModel.buttonRefresh_onClick) {
Text("Refresh")
}.padding()
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
#Published var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemsView.swift⤵️
struct ItemsView: View {
#Binding var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.count)")
ScrollView(.vertical) {
ForEach($viewModel.items) { item in
ItemView(viewModel: item).padding()
}
}
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item form ItemsView")
}
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
#Published public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
#Published public var intProp: Int;
init() {
self.intProp = 0;
}
func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
//todo ❗ I want to delete item in parent component
}
}
ItemView.swift⤵️
struct ItemView: View {
#Binding var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.intProp)")
Button(action: viewModel.buttonIncrementIntProp_onClick) {
Image(systemName: "plus")
}
Button(action: viewModel.buttonDelete_onClick) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
I read official docs and countless SO topics and articles, but nowhere got solution for exact my case (or me doing something wrong). It only works if implement all UI part in single view
UPD 1:
Is it even possible to with class but not a struct in View Model?
Updates works perfectly if I use struct instead of class:
ItemsViewModel.swift⤵️
struct ItemsViewModel {
var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
mutating func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemViewModel.swift⤵️
struct ItemViewModel: Identifiable, Equatable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
mutating func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
}
}
But is it ok to use mutating functions? I also tried to play with Combine and objectWillChange, but unable to make it work
UPD 2
Thanks #Yrb for response. With your suggestion and this article I came added Model structures and ended up with such results:
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
#StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: ItemsViewModel(viewModel.items)).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
}
}
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
#Published var items: ItemsModel;
init() {
self.items = ItemsModel();
}
#MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel())
}
}
ContainerModel.swift⤵️
struct ContainerModel {
public var items: ItemsModel;
}
ItemsView.swift⤵️
struct ItemsView: View {
#ObservedObject var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.items.count)")
ScrollView(.vertical) {
ForEach(viewModel.items.items) { item in
ItemView(viewModel: ItemViewModel(item)).padding()
}
}
Button(action: {
viewModel.buttonAddItem_onClick()
}) {
Text("Add item form ItemsView")
}
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
#Published var items: ItemsModel;
init(_ items: ItemsModel) {
self.items = items;
}
#MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel());
}
}
ItemsModel.swift⤵️
struct ItemsModel {
public var items: [ItemModel] = [ItemModel]();
}
ItemView.swift⤵️
struct ItemView: View {
#StateObject var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.item.intProp)")
Button(action: {
viewModel.buttonIncrementIntProp_onClick()
}) {
Image(systemName: "plus")
}
Button(action: {
viewModel.buttonDelete_onClick()
}) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
#Published private(set) var item: ItemModel;
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
init(_ item: ItemModel) {
self.item = item;
self.item.intProp = 0;
}
#MainActor func buttonIncrementIntProp_onClick() -> Void {
self.item.intProp = self.item.intProp + 1;
}
#MainActor func buttonDelete_onClick() -> Void {
}
}
ItemModel.swift⤵️
struct ItemModel: Identifiable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
}
This code runs and works perfectly, at least I see no problems. But I'm not shure if I properly initializes and "bind" ViewModels and Models - code looks quite messy. Also I'n not shure I correctly set ObservedObject in ItemsView and StateObject in ItemView. So please check me
We don't need MVVM in SwiftUI, see: "MVVM has no place in SwiftUI."
In SwiftUI, the View struct is already the view model. We use property wrappers like #State and #Binding to make it behave like an object. If you were to actually use objects instead, then you'll face all the bugs and inconsistencies of objects that SwiftUI and its use of value types was designed to eliminate.
I recommend Data Essentials in SwiftUI WWDC 2020 for learning SwiftUI. The first half is about view data and the second half is about model data. It takes a few watches to understand it. Pay attention to the part about model your data with value type and manage its life cycle with a reference type.
It's best to also use structs for your model types. Start with one single ObservableObject to hold the model types (usually arrays of structs) as #Published properties. Use environmentObject to pass the object into the View hierarchy. Use #Binding to pass write access to the structs in the object. Apple's sample ScrumDinger is a good starting point.

How we can notify ObservableObject about changes of its initializers?

I have a ObservableObject-Class which inside this class, I got a published var with name of persones! I do initialize it with some data called: allData.
Then I try to update my allData with action of a Button, and this action apply the wanted update to my allData, but my published var has no idea, that this data got updated!
How we can make published see the new updated allData?
struct PersonData: Identifiable {
let id = UUID()
var name: String
}
var allData = [PersonData(name: "Bob"), PersonData(name: "Nik"), PersonData(name: "Tak"), PersonData(name: "Sed"), PersonData(name: "Ted")]
class PersonDataModel: ObservableObject {
#Published var persones: [PersonData] = allData
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack
{
Button("update allData") { allData = [PersonData(name: "Bob")] }
HStack
{
ForEach(personDataModel.persones) { person in Text(person.name) }
}
}
.font(Font.title)
}
}
PS: I don´t want use .onChange or other things for this, I would like this happens internally in my class.
Also I know I can use down code for this work, but that is not the answer
personDataModel.persones = [PersonData(name: "Bob")]
Having a top-level property (outside of any class or struct) is probably not a good idea. I don't see the whole picture, but it looks like your app needs a global state (e.g., a #StateObject initialised on the App level). Consider this answer:
Add EnvironmentObject in SwiftUI 2.0
If you really need to observe your array, you need to make it observable.
One option is to use CurrentValueSubject from the Combine framework:
var persons = ["Bob", "Nik", "Tak", "Sed", "Ted"].map(PersonData.init)
var allData = CurrentValueSubject<[PersonData], Never>(persons)
class PersonDataModel: ObservableObject {
#Published var persones: [PersonData] = allData.value
private var cancellables = Set<AnyCancellable>()
init() {
allData
.sink { [weak self] in
self?.persones = $0
}
.store(in: &cancellables)
}
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack {
Button("update allData") {
allData.send([PersonData(name: "Bob")])
}
HStack {
ForEach(personDataModel.persones) { person in
Text(person.name)
}
}
}
.font(Font.title)
}
}
The allData is copied into persones at initialization time, so changing it afterwards does nothing to personDataModel. After StateObject created you have to work with it, like
Button("update allData") {
self.personDataModel.persones = [PersonData(name: "Bob")]
}
I think you're doing something wrong.
if you want to update all your views, you have to pass the same object with #EnviromentObject.
I don't know your storage method (JSON, CORE DATA, iCloud) but the correct approach is to update directly the model
class PersonDataModel: ObservableObject
{
#Published var persones: [PersonData] = loadFromJSON //one func that is loading your object stored as JSON file
func updateAllData() {
storeToJSON(persones) //one func that is storing your object as JSON file
}
}
struct ContentView: View {
#StateObject var personDataModel = PersonDataModel()
var body: some View {
VStack
{
Button("update allData") {
self.personDataModel.persones = [PersonData(name: "Bob")]
}
HStack
{
ForEach(personDataModel.persones) { person in Text(person.name) }
}
}
.font(Font.title)
.onChange($personDataModel.persones) {
persones.updateAllData()
}
}
}

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 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())
}
}