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

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

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.

Unable to get click event with observedObject in SwiftUI

I have tried below code. However, I am unable to get click event in ObservedObject. Did I made any mistake.
struct ContentView: View {
#StateObject var network = Network()
var body: some View {
VStack {
SecondView(network: network)
Text(self.network.networkObserver.sucess?.description ?? "Nil")
}
}
}
SecondView Code:- Here is the code when I need to have click happened then revert to main content view.
public struct AdsView: View {
#State private var banner: Model?
#State private var image: UIImage?
#State private var scale: Double = 1.0
#ObservedObject var network: Network
public var body: some View {
Group {
if let image = image {
Text("AdSDK mockup. Click on image")
Image(uiImage: image)
.resizable()
.scaledToFit()
.scaleEffect(scale)
.gesture (
TapGesture()
.onEnded { _ in
self.scale -= 0.1
network.networkObserver.sucess = network.networkObserver.sucess ?? false ? false : true
}
)
} else {
Rectangle()
.background(Color.red)
}
}
}
Note:- Network class are in my custom library.
public class Network: ObservableObject {
#Published public var adImage: UIImage?
#Published public var networkObserver = NetworkObserver()
public init() {
}
public func getImage(for imageURL: String) async throws {
}
}
And here is my ObservableObject
public class NetworkObserver {
public var sucess: Bool?
public var error: RequestError?
public init() {
}
}
If you need more information please let me know.
Thank you.
As #workingdogsupportUkraine suggest in comment I need to change my NetworkObserver class to struct.
Add this class somewhere in your code:
#MainActor class DelayedUpdater: ObservableObject {
#Published var value = 0
init() {
for i in 1...10 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
self.value += 1
}
}
}
}
To use that, we just need to add a #StatedObject property in ContentView, then show the value in our body, like this:
struct ContentView: View {
#StateObject var updater = DelayedUpdater()
var body: some View {
Text("Value is: \(updater.value)")
}
}
We can fix this by sending the change notifications manually using the objectWillChange property I mentioned earlier. This lets us send the change notification whenever we want, rather than relying on #Published to do it automatically.
Try changing the value property to this:
var value = 0 {
willSet {
objectWillChange.send()
}
}

Unable to infer complex closure return type swiftUI

I'm trying to post data to the list but keep getting the error 'Unable to infer complex closure return type; add explicit type to disambiguate'
how do I fix this?
import SwiftUI
struct ContentView: View {
#State var data: [Post] = [Post]()
#ObservedObject var networkManager = NetworkManager()
#State private var searchTerm: String = "" {
didSet {
print(searchTerm)
}
}
var body: some View {
List { // ERROR SHOWS UP HERE
SearchBar(text: $searchTerm)
ForEach(data) { post in
Text(post.fullname ?? "null")
}
}
.onAppear {
self.reload()
}
.onReceive(self.networkManager.posts, perform: { _ in
self.reload()
})
}
private func reload() {
networkManager.fetchData(playerName: "messi")
self.data = networkManager.posts
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Assuming your NetworkManage.posts is #Published property the subscriber in view have to be specified as follow
.onReceive(self.networkManager.$posts, perform: {_ in // << fixed !!
self.reload()
})
Note: btw, didSet does not work for #State, so don't spend time on that.

objectWillChange requires the types 'T.ObjectWillChangePublisher' and 'ObservableObjectPublisher' be equivalent

I am getting the error:
objectWillChange requires the types 'ViewModelType.ObjectWillChangePublisher' and 'ObservableObjectPublisher' be equivalent
Any idea how to get around this?
import SwiftUI
final class MyViewModel: ObservableObject & SomeViewModelProtocol {
#Published var name: String
init(name: String) {
self.name = name
}
}
struct MainView: View {
#EnvironmentObject var viewModel: MyViewModel
var body: some View {
VStack() {
MyView<MyViewModel>()
}
}
}
protocol SomeViewModelProtocol {}
struct MyView<ViewModelType: ObservableObject & SomeViewModelProtocol>: View {
#EnvironmentObject var viewModel: ViewModelType
var body: some View {
return VStack {
Text("")
.onReceive(self.viewModel.objectWillChange, perform: {
print("onReceive")
})
}
}
}
Perhaps you're just getting a misleading error message from the compiler. The onReceive(_:perform:) modifier requires a closure of one argument, but you're passing a closure of zero arguments. Try this instead:
.onReceive(self.viewModel.objectWillChange, perform: { _ in
// ----------------------------------------------------------- ^^^^ add this
print("onReceive")
})

Two-way binding in Swift Combine

I have a progress bar and a text field, both are updated depending on each other's input:
class ViewModel: ObservableObject {
#Published var progressBarValue: Double {
didSet {
textFieldValue = String(progressBarValue)
}
}
#Published var textFieldValue: String {
didSet {
progressBarValue = Double(progressBarValue)
}
}
}
Since updating one updates the other, I end up having an infinite recursion in my code.
Is there a way to workaround this with Combine or plain swift code?
Expanding on my comment, here is a minimal example of a slider and a textfield that both control (and be controlled by) a value via two-way bindings:
class ViewModel: ObservableObject {
#Published var progress: Double = 0
}
struct ContentView: View {
#EnvironmentObject var model: ViewModel
var body: some View {
VStack {
TextField("", value: self.$model.progress, formatter: NumberFormatter())
Slider(value: self.$model.progress, in: 0...100)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ViewModel())
}
}
Note that I also had to inject a ViewModel instance to my environment on AppDelegate in order for this to work (both on preview & actual app)
Maybe additional checks to avoid loops will work?
#Published var progressBarValue: Double {
didSet {
let newText = String(progressBarValue)
if newText != textFieldValue {
textFieldValue = newText
}
}
}
#Published var textFieldValue: String {
didSet {
if let newProgress = Double(textFieldValue),
abs(newProgress - progressBarValue) > Double.ulpOfOne {
progressBarValue = newProgress
}
}
}