How to trigger automatic SwiftUI Updates with #ObservedObject using MVVM - swift

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.

Related

How to observer a property in swift ui

How to observe property value in SwiftUI.
I know some basic publisher and observer patterns. But here is a scenario i am not able to implement.
class ScanedDevice: NSObject, Identifiable {
//some variables
var currentStatusText: String = "Pending"
}
here CurrentStatusText is changed by some other callback method that update the status.
Here there is Model class i am using
class SampleModel: ObservableObject{
#Published var devicesToUpdated : [ScanedDevice] = []
}
swiftui component:
struct ReviewView: View {
#ObservedObject var model: SampleModel
var body: some View {
ForEach(model.devicesToUpdated){ device in
Text(device.currentStatusText)
}
}
}
Here in UI I want to see the real-time status
I tried using publisher inside ScanDevice class but sure can to use it in 2 layer
You can observe your class ScanedDevice, however you need to manually use a objectWillChange.send(),
to action the observable change, as shown in this example code.
class ScanedDevice: NSObject, Identifiable {
var name: String = "some name"
var currentStatusText: String = "Pending"
init(name: String) {
self.name = name
}
}
class SampleViewModel: ObservableObject{
#Published var devicesToUpdated: [ScanedDevice] = []
}
struct ReviewView: View {
#ObservedObject var viewmodel: SampleViewModel
var body: some View {
VStack (spacing: 33) {
ForEach(viewmodel.devicesToUpdated){ device in
HStack {
Text(device.name)
Text(device.currentStatusText).foregroundColor(.red)
}
Button("Change \(device.name)") {
viewmodel.objectWillChange.send() // <--- here
device.currentStatusText = UUID().uuidString
}.buttonStyle(.bordered)
}
}
}
}
struct ContentView: View {
#StateObject var viewmodel = SampleViewModel()
var body: some View {
ReviewView(viewmodel: viewmodel)
.onAppear {
viewmodel.devicesToUpdated = [ScanedDevice(name: "device-1"), ScanedDevice(name: "device-2")]
}
}
}

SwiftUI - changes in nested View Model classes not detected using onChange method

I have a nested View Model class WatchDayProgramViewModel as an ObservableObject. Within WatchDayProgramViewModel, there is a WorkoutModel that is a child class. I want to detect any updates in the currentHeartRate to trigger data transfer to iPhone.
Hence, I tried from ContentView using WatchDayProgramViewModel as an EnvironmentObject and detecting changes in WorkoutModel via onChange() method. But it seems that SwiftUI views does not detect any property changes in WorkoutModel.
I understand that this issue could be due to ObservableObject not detecting changes in child/nested level of classes, and SO answer (SwiftUI change on multilevel children Published object change) suggests using struct instead of class. But changing WorkoutModel to struct result in various #Published properties and functions to show error.
Is there any possible way to detect changes in child View Model from the ContentView itself?
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
WatchControlView().id(0)
NowPlayingView().id(1)
}
.environmentObject(watchDayProgramVM)
.onChange(of: self.watchDayProgramVM.workoutModel.currentHeartRate) { newValue in
print("WatchConnectivity heart rate from contentView \(newValue)")
}
}
}
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
init() {
}
}
WorkoutModel
import Foundation
import HealthKit
class WorkoutModel: NSObject, ObservableObject {
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
#Published var currentHeartRate: Double = 0
#Published var workout: HKWorkout?
//Other functions to start/run workout hidden
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else {
return
}
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
self.currentHeartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
default:
return
}
}//end of dispatchqueue
}// end of function
}
extension WorkoutModel: HKLiveWorkoutBuilderDelegate {
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return
}
let statistics = workoutBuilder.statistics(for: quantityType)
updateForStatistics(statistics)
}
}
}
Try to change
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
with
#ObservedObject var watchDayProgramVM = WatchDayProgramViewModel()
Figure it out. Just had to create another AnyCancellable variable to call objectWillChange publisher.
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
var cancellable: AnyCancellable?
init() {
cancellable = workoutModel.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
}
}
While I have provided my answer, that worksaround with viewmodels, I would love to see/get advice on other alternatives.

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

SwiftUI: Preview with data in ViewModel

I load my data from a viewModel which is loading data from web. Problem: I want to set some preview sample data to have content in preview window. Currently my preview contains an empty list as I do not provide data.
How can I achieve this?
struct MovieListView: View {
#ObservedObject var viewModel = MovieViewModel()
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView()
}
}
class MovieViewModel: ObservableObject{
private let provider = NetworkManager()
#Published var movies = [Movie]()
init() {
loadNewMovies()
}
func loadNewMovies(){
provider.getNewMovies(page: 1) {[weak self] movies in
print("\(movies.count) new movies loaded")
self?.movies.removeAll()
self?.movies.append(contentsOf: movies)}
}
}
Here is possible approach (based on dependency-injection of view model members instead of tight-coupling)
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// create Movie to be previewed inline, say from bundled data
MovieListView(viewModel: MovieViewModel(provider: nil, movies: [Movie(...)]))
}
}
class MovieViewModel: ObservableObject {
private var provider: NetworkManager?
#Published var movies: [Movie]
// same as before by default, but allows to modify if/when needed explicitly
init(provider: NetworkManager? = NetworkManager(), movies: [Movie] = []) {
self.provider = provider
self.movies = movies
loadNewMovies()
}
func loadNewMovies(){
provider?.getNewMovies(page: 1) {[weak self] movies in
print("\(movies.count) new movies loaded")
self?.movies.removeAll()
self?.movies.append(contentsOf: movies)
}
}
}
This question was written before #StateObject was introduced at WWDC 2020. I believe these days you'd want to use #StateObject instead of #ObservedObject because otherwise your view model can be re-initialized numerous times (which would result in multiple network calls in this case).
I wanted to do the exact same thing as OP, but with #StateObject. Here's my solution that doesn't rely on any build configurations.
struct MovieListView: View {
#StateObject var viewModel = MovieViewModel()
var body: some View {
MovieListViewInternal(viewModel: viewModel)
}
}
private struct MovieListViewInternal<ViewModel: MovieViewModelable>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
List {
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
}
}
.onAppear {
viewModel.fetchMovieRatings()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListViewInternal(viewModel: PreviewMovieViewModel())
}
}
The View model protocols and implementations:
protocol MovieViewModelable: ObservableObject {
var movies: [Movie] { get }
func fetchMovieRatings()
// Define vars or funcs for anything else your view accesses in your view model
}
class MovieViewModel: MovieViewModelable {
#Published var movies = [Movie]()
init() {
loadNewMovies()
}
private func loadNewMovies() {
// do the network call
}
func fetchMovieRatings() {
// do the network call
}
}
class PreviewMovieViewModel: MovieViewModelable {
#Published var movies = [fakeMovie1, fakeMovie2]
func fetchMovieRankings() {} // do nothing while in a Preview
}
This way your external interface to MovieListView is exactly the same, but for your previews you can use the internal view definition and override the view model type.
Further to the answer above, and if you want to keep your shipping codebase clean, I've found that extending the class captured in PreProcessor flags to add a convenience init works.
#if DEBUG
extension MovieViewModel{
convenience init(forPreview: Bool = true) {
self.init()
//Hard code your mock data for the preview here
self.movies = [Movie(...)]
}
}
#endif
Then modify your SwiftUI structs using preprocessor flags as well:
struct MovieListView: View {
#if DEBUG
let viewModel: MovieViewModel
init(viewModel: MovieViewModel = MovieViewModel()){
self.viewModel = viewModel
}
#else
#StateObject var viewModel = MovieViewModel()
#endif
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView(viewModel: MovieViewModel(forPreview: true)
}
}
So while #Kramer's solution works, I hit a challenge with it in the sense that when I would debug the app on my device it would load the preview data and not other "development" data that I would be wanting to be using.
So I extended the solution a little by creating a new build configuration called "Preview" and then wrapped all the 'preview' related data into that build configuration.
That gives me the option then to preview dummy data in the Xcode preview, while still allowing me then to build and debug a development build with development data on my devices/simulators.
So my solution now looks like this..
class MovieViewModel: ObservableObject {
init() {
#if PREVIEW
//Hard code your mock data for the preview here
self.movies = [Movie(...)]
#else
// normal init stuff here
#endif
}
}
struct MovieListView: View {
#if PREVIEW
let viewModel: MovieViewModel
init(viewModel: MovieViewModel = MovieViewModel()){
self.viewModel = viewModel
}
#else
#StateObject var viewModel = MovieViewModel()
#endif
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView(viewModel: MovieViewModel())
}
}
Might not be the best crack at this, but gave me the flexibility to manage my Preview dummy data separate to my development/Debug data and has so far proven to work well for my use cases so far. :)
I have been struggling with this as well and came up with the following simple solution.
//View
struct MyView: View {
#StateObject private var viewModel = ViewModel()
init(forPreview: Bool = false) {
guard forPreview else { return }
let viewModel = ViewModel()
viewModel.title = "Preview" // Call internal func to load sample data
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
Text(viewModel.title)
}
}
//View Model
extension MyView {
#MainActor class ViewModel: ObservableObject {
#Published var title: String = "Standard"
}
}
//Previews
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(forPreview: true)
}
}
This initialization of #StateObject is Apple approved.

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