Swift: Should ViewModel be a struct or class? - swift

I am trying to use MVVM pattern in my new project. First time, I created all my view model to struct. But when I implemented async business logic such as fetchDataFromNetwork with closures, closures capture old view model value then updated to that. Not a new view model value.
Here is a test code in playground.
import Foundation
import XCPlayground
struct ViewModel {
var data: Int = 0
mutating func fetchData(completion:()->()) {
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
result in
self.data = 10
print("viewModel.data in fetchResponse : \(self.data)")
completion()
XCPlaygroundPage.currentPage.finishExecution()
}.resume()
}
}
class ViewController {
var viewModel: ViewModel = ViewModel() {
didSet {
print("viewModel.data in didSet : \(viewModel.data)")
}
}
func changeViewModelStruct() {
print("viewModel.data before fetch : \(viewModel.data)")
viewModel.fetchData {
print("viewModel.data after fetch : \(self.viewModel.data)")
}
}
}
var c = ViewController()
c.changeViewModelStruct()
Console prints
viewModel.data before fetch : 0
viewModel.data in didSet : 0
viewModel.data in fetchResponse : 10
viewModel.data after fetch : 0
The problem is View Model in ViewController does not have new Value 10.
If I changed ViewModel to class, didSet not called but View Model in ViewController has new Value 10.

You should use a class.
If you use a struct with a mutating function, the function should not perform the mutation within a closure; you should not do the following:
struct ViewModel {
var data: Int = 0
mutating func myFunc() {
funcWithClosure() {
self.data = 1
}
}
}
If I changed ViewModel to class, didSet not called
Nothing wrong here - that's the expected behavior.
If you prefer to use struct, you can do
func fetchData(completion: ViewModel ->()) {
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
result in
var newViewModel = self
newViewModel.data = 10
print("viewModel.data in fetchResponse : \(self.data)")
completion(newViewModel)
XCPlaygroundPage.currentPage.finishExecution()
}.resume()
}
viewModel.fetchData { newViewModel in
self.viewModal = newViewModel
print("viewModel.data after fetch : \(self.viewModel.data)")
}
Also note that the closure provided to dataTaskWithURL does not run on the main thread. You might want to call dispatch_async(dispatch_get_main_queue()) {...} in it.

You could get the self.data in two options: either use a return parameter in your closure for fetchResponse( using viewModel as struct) OR you can create your own set-method/closure and use it in your init method(using viewModel as class).
class ViewModel {
var data: Int = 0
func fetchData(completion:()->()) {
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
result in
self.data = 10
print("viewModel.data in fetchResponse : \(self.data)")
completion()
XCPlaygroundPage.currentPage.finishExecution()
}.resume()
}
}
class ViewController {
var viewModel: ViewModel! { didSet { print("viewModel.data in didSet : \(viewModel.data)") } }
init( viewModel: ViewModel ) {
// closure invokes didSet
({ self.viewModel = viewModel })()
}
func changeViewModelStruct() {
print("viewModel.data before fetch : \(viewModel.data)")
viewModel.fetchData {
print("viewModel.data after fetch : \(self.viewModel.data)")
}
}
}
let viewModel = ViewModel()
var c = ViewController(viewModel: viewModel)
c.changeViewModelStruct()
Console prints:
viewModel.data in didSet : 0
viewModel.data before fetch : 0
viewModel.data in fetchResponse : 10
viewModel.data after fetch : 10
Apple Document
says like this:
willSet and didSet observers are not called when a property is first initialized. They are only called when the property’s value is set outside of an initialization context.

Related

UserDefaults keypath publisher doesn't fire beyond first value

Given a obj-c keypath
#objc dynamic var someProp: String { string(forKey: "someProp") }
A regular publisher:
private let sub = UserDefaults.standard.publisher(for: \.someProp).sink { print($0) }
This publishes only works for the first value (e.g. the current value).
However observing the sub publisher from SwiftUI works fine:
.onReceive(pub) { value in
print("received", value)
}
This publishes any subsequent updates.
Any ideas why the former doesn't work?
Edit: Here is a minimal reproducible example:
public extension UserDefaults {
#objc dynamic var value1: Int {
integer(forKey: "string1")
}
}
struct ContentView: View {
#StateObject var vm = ViewModel()
private let pub = UserDefaults.standard.publisher(for: \.value1)
var body: some View {
Button("Add") {
var value = UserDefaults.standard.value(forKey: "value1") as? Int ?? 0
value += 1
debugPrint("SET", value)
UserDefaults.standard.set(value, forKey: "value1")
}
.onReceive(pub) { value in
debugPrint("UI", value)
}
}
class ViewModel: ObservableObject {
private let sub = UserDefaults.standard.publisher(for: \.value1).sink {
debugPrint("SUB", $0)
}
}
}
The error here is how you access and assign your values in the Button action. You are setting the values for the key value1. But the publisher observes the key string1 with the dynamic var named value1.
TLDR: You confused the dynamic var with your key
I would recommend you ommit the access via .value(forKey: "") and use only your dynamic var.
public extension UserDefaults {
#objc dynamic var value1: Int {
// add getter and setter
get{
integer(forKey: "string1")
}
set{
set(newValue, forKey: "string1")
}
}
}
struct ContentView: View {
#StateObject var vm = ViewModel()
private let pub = UserDefaults.standard.publisher(for: \.value1)
var body: some View {
Button("Add") {
//here
UserDefaults.standard.value1 += 1
debugPrint("SET", UserDefaults.standard.value1)
}
.onReceive(pub) { value in
debugPrint("UI", value)
}
}
class ViewModel: ObservableObject {
private let sub = UserDefaults.standard.publisher(for: \.value1).sink {
debugPrint("SUB", $0)
}
}
}
Prints:
"SUB" 0
"UI" 0
"SET" 1
"SUB" 1
"UI" 1
"SET" 2
"SUB" 2
"UI" 2
"SET" 3
"SUB" 3
"UI" 3

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.

closure through typealias swift not working

why does typealias closure not transmit data and output nothing to the console? How to fix it?
class viewModel: NSObject {
var abc = ["123", "456", "789"]
typealias type = ([String]) -> Void
var send: type?
func createCharts(_ dataPoints: [String]) {
var dataEntry: [String] = []
for item in dataPoints {
dataEntry.append(item)
}
send?(dataEntry)
}
override init() {
super.init()
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel()
func type() {
viewModel.send = { item in
print(item)
}
}
override func viewDidLoad() {
super.viewDidLoad()
print("hello")
type()
}
}
I have a project in which a similar design works, but I can not repeat it
The pattern is fine, but the timing is off.
You’re calling createCharts during the init of the view model. But the view controller is setting the send closure after the init of the view model is done.
Bottom line, you probably don’t want to call createCharts during the init of the view model.
Possible solution is to create custom initializer:
class viewModel: NSObject {
...
init(send: type?) {
self.send = send
self.createCharts(abc)
}
}
class ViewController: UIViewController {
var viewModel: viewModel = viewModel(send: { print($0) })
...
}

How to assign sink result to variable

I would like to assign a result form a notification center publisher to the variable alert. The Error that I get is:
Cannot use instance member 'alerts' within property initializer; property initializers run before 'self' is available
Could Someone help me out here?
import Foundation
import SwiftUI
import Combine
final class PublicAlerts: ObservableObject{
init () {
fetchAlerts()
}
var alerts = [String](){
didSet {
didChange.send(self)
}
}
private func fetchPublicAssets(){
backEndService().fetchAlerts()
}
let publicAssetsPublisher = NotificationCenter.default.publisher(for: .kPublicAlertsNotification)
.map { notification in
return notification.userInfo?["alerts"] as! Array<String>
}.sink {result in
alerts = result
}
let didChange = PassthroughSubject<PublicAlerts, Never>()
}
Later I will use alerts this in SwiftUI as a List
Move the subscribtion in init
final class PublicAlerts: ObservableObject{
var anyCancelable: AnyCancellable? = nil
init () {
anyCancelable = NotificationCenter.default.publisher(for: .kPublicAlertsNotification)
.map { notification in
return notification.userInfo?["alerts"] as! Array<String>
}.sink {result in
alerts = result
}
fetchAlerts()
}
var alerts = [String](){
didSet {
didChange.send(self)
}
}
private func fetchPublicAssets(){
backEndService().fetchAlerts()
}
let didChange = PassthroughSubject<PublicAlerts, Never>()
}

How to handle navigation with observables using Rx-MVVM-C

Hello I am trying to do a project with RxSwift and I am stuck trying to do in a properly way the connection between the Coordinator and the ViewModel.
Goal
Using observables, the Coordinator receives and event (in that case, when a row has been tapped) then does whatever.
Scenario
Giving a Post (String)
typealias Post = String
I have the following Coordinator:
class Coordinator {
func start() {
let selectedPostObservable = PublishSubject<Post>()
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
selectedPostObservable.subscribe { post in
//Do whatever
}
}
}
The selectedPostObservable is what I don't know how to connect it in a "clean" way with the viewModel.
As ViewModel:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
let result = input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.asDriver(onErrorJustReturn: nil)
return Output()
}
}
The result variable is what I should connect with selectedPostObservable.
And the ViewController (although I think is not relevant for the question):
class ViewController: UIViewController {
//...
var viewModel: ViewModel!
var tableView: UITableView!
//...
func bindViewModel() {
let input = ViewModel.Input(selectedIndexPath: tableView.rx.itemSelected.asObservable())
viewModel.transform(input: input)
}
}
Thank you so much.
Working with the structure you are starting with, I would put the PublishSubject in the ViewModel class instead of the Coordinator. Then something like this:
class ViewModel {
struct Input {
let selectedIndexPath: Observable<IndexPath>
}
struct Output {
//UI Outputs
}
let selectedPost = PublishSubject<Post>()
let bag = DisposeBag()
func transform(input: Input) -> Output {
let posts: [Post] = Observable.just(["1", "2", "3"])
//Connect with selectedindex
input.selectedIndexPath
.withLatestFrom(posts) { $1[$0.row] }
.bind(to: selectedPost)
.disposed(by: bag)
return Output()
}
}
class Coordinator {
func start() {
let viewController = ViewController()
let viewModel = ViewModel()
viewController.viewModel = viewModel
viewModel.selectedPost.subscribe { post in
//Do whatever
}
.disposed(by: viewModel.bag)
}
}