SwiftUI CurrentValueSubject Behavior - swift

I'm new in SwiftUI. I'm worked with UIKit and Combine framework building an architecture with ViewModel, UseCases and Repositories. All my architecture is based on the loading states. My loading states are build in this way:
/// Equivalent to #Published with `LoadingState<T, E>` property wrapper
#propertyWrapper public class Loading<T, E: Swift.Error> {
public typealias State = LoadingState<T, E>
public var wrappedValue: State {
willSet {
subject.send(newValue)
}
}
public init(wrappedValue: State) {
self.wrappedValue = wrappedValue
}
private lazy var subject = CurrentValueSubject<State, Never>(wrappedValue)
public var projectedValue: AnyPublisher<State, Never> {
return subject.eraseToAnyPublisher()
}
}
and my ViewModel works in this way:
#Loading<MyData, MyError> var myDataLoadingState = .idle
public func getMyData(ID: String) {
myDataLoadingState = .loading
myDataUseCase.execute(ID: ID)
.receive(on: DispatchQueue.main)
.sink { completion in
guard case .failure(let error) = completion else { return }
myDataLoadingState = .failure(error)
} receiveValue: { myData in
self. myDataLoadingState = .success(myData)
}
.store(in: &self.cancellables)
}
The controller works in this way:
viewModel.$myDataLoadingState
.sink { state in
switch state {
case .idle:
break
case .loading:
self.showLoader()
case .success(let myData):
print(myData)
case .failure(let error):
self.print(error)
self.hideLoader()
}
}
.store(in: &cancellables)
Can I use the loading state and my ViewModel in SwiftUI? I tried in this way but seems not works:
struct ContentView: View {
#StateObject var viewModel: MyViewModel
var body: some View {
switch viewModel.loadingState {
case .idle:
Text("Idle")
case .loading:
Text("Loading")
case .success(let myData):
Text(myData.name)
case .failure(let error):
Text(error.localizedDescription)
}
}
}
The ViewModel now is an ObservableObject
public class MyViewModel: ObservableObject {
}
Thanks in advance

Just recognised that you already has property wrapper for myDataLoadingState, so not sure if you are allowed to make it #Published instead. Anyway to inform #StateObject wrapper (who is a listener) you should fire event about changes in view model. A possible way is to use objectWillChange directly, like
myDataUseCase.execute(ID: ID)
.receive(on: DispatchQueue.main)
.sink { completion in
guard case .failure(let error) = completion else { return }
self.objectWillChange.send() // << here !!
myDataLoadingState = .failure(error)
} receiveValue: { myData in
self.objectWillChange.send() // << here !!
self.myDataLoadingState = .success(myData)
}
.store(in: &self.cancellables)

Related

Unit testing view model that depends on a publisher

I implemented a service class with a function that returns a publisher when some data is loaded:
class Service {
let fileURL: URL // Set somewhere else in the program
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
URLSession.shared.dataTaskPublisher(for: fileURL)
.map( { $0.data } )
.decode(type: [MyModelClass].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
This function is used in my view model class like this:
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let service: Service
init(service: Service) {
self.service = service
loadCityData()
}
func loadModels() {
service.loadModels()
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
I find the view model difficult to unit-test because I don't have the publisher returned from the service available directly in my unit test class, but I have the #Published property instead. So I tried to implement a test like this one:
let expectation = expectation(description: "loadModels")
viewModel.$models
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
finishLoading.fulfill()
}, receiveValue: { _ in
})
.store(in: &cancellables) // class-scoped property
viewModel.loadModels()
wait(for: [expectation], timeout: 10)
The problem is that the receiveComplection callback is never called. If I had the publisher available (the one returned from the Service object), the same code applied to the publisher would run successfully and fulfill the expectation. Instead, the complection is not being called but the receiveValue is being called multiple times. Why?
First, instead of passing the entire service to the view model, just pass in the Publisher itself. In the test, you can pass in a synchronous publisher which makes testing much easier.
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
let cancellable = viewModel.$models
.dropFirst(1)
.sink(receiveValue: { output in
XCTAssertEqual(input, output)
})
viewModel.loadModels()
}
}
class ViewModel: ObservableObject {
#Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let modelLoader: AnyPublisher<[MyModelClass], Error>
init(modelLoader: AnyPublisher<[MyModelClass], Error>) {
self.modelLoader = modelLoader
}
func loadModels() {
modelLoader
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
Notice that there is no need to setup an expectation and wait for it. This makes for a much faster test.
Even simpler would be to just examine the models property directly:
final class ExampleTests: XCTestCase {
func test() {
let input = [MyModelClass()]
let modelLoader = Just(input).setFailureType(to: Error.self).eraseToAnyPublisher()
let viewModel = ViewModel(modelLoader: modelLoader)
viewModel.loadModels()
XCTAssertEqual(viewModel.models, input)
}
}
For all of these though, what exactly do you think you are testing here? There are no transformations and no logic in this code. You aren't testing to ensure the ViewModel calls into the Service, because in order to do this test at all, you have to mock out the Service. So in reality, the only thing you are doing is testing to see if the test itself mocked out the Service correctly. But what's the point in that? Who cares if the test was set up correctly if it doesn't test production code?
You could use a combination of interface and dependency injection to allow testing.
First you define an interface for service:
protocol ServiceInterface {
func loadModels() -> AnyPublisher<[MyModelClass], Error>
}
Next you make Service conform to this new protocol:
class Service: ServiceInterface {
// ...
}
Now you can inject Service into your ViewModel using the interface defined above:
class ViewModel: ObservableObject {
//...
let service: ServiceInterface
init(service: ServiceInterface = Service()) {
self.service = service
loadModels()
}
//...
}
This means you are able to inject any entity conforming to ServiceInterface into the ViewModel, so let's define one in the test target:
struct MockService: ServiceInterface {
let loadModelsResult: Result<[MyModelClass], Error>
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
loadModelsResult.publisher.eraseToAnyPublisher()
}
}
Lastly let's inject MockService into ViewModel for testing purposes:
func testExample() {
let expectedModels = [MyModelClass()]
let subject = ViewModel(service: MockService(loadModelsResult: .success(expectedModels)))
let expectation = expectation(description: "expect models to get loaded")
subject
.$models
.sink(
receiveCompletion: { _ in },
receiveValue: { actualModels in
// or any other test that is meaningful in your context
if actualModels == expectedModels {
expectation.fulfill()
}
}
)
.store(in: &cancellables)
subject.loadModels()
waitForExpectations(timeout: 0.5)
}

how to return Error from Service layer to ViewModel using PassthroughSubject Error handling

I am new to Combine Framework, and facing issue while returning error from Service Layer to ViewModel layer.
Attaching sample code here for reference:
ViewModel
final class ViewModel: ObservableObject {
#Published var user: User?
func sendUserRequest() {
#Injected var userService: UserServiceContract
let publisher = userService.get()
publisher.sink(receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
self?.error = error
}
}, receiveValue: { user in
self.user = user
})
.store(in: cancelBag)
}
}
Service
protocol UserServiceContract {
func get() -> AnyPublisher<User, Error>
}
class UserService: UserServiceContract {
private var cancelBag = CancelBag()
private let subject = PassthroughSubject<User, Error>()
private var requestHoldBackTimeInterval: TimeInterval {
return 0.5
}
func get() -> AnyPublisher<User, Error> {
let repo:AnyPublisher<User, Error> = UserWebRepository().get()
repo.ensureTimeSpan(requestHoldBackTimeInterval)
.mapError({ (error) -> Error in
return error
})
.sink(
receiveCompletion: { _ in },
receiveValue: {
print($0)
self.subject.send($0)
}
)
.store(in: cancelBag)
return subject.eraseToAnyPublisher()
}
}
In Service layer:
a. I am able to pass response to ViewModel from Service when result is successful - using passthroughSubject.
b. I am stuck when result is Error: how to handle it in Service layer and later pass that error to ViewModel?
I am making network call using Combine framework and need help in service layer.
View <> ViewModel <> **Service** <> Repository
You can use .assign(to: &$user) instead of .sink and .store.
The pipeline is usually created in the ObservableObject's init.

Can't get SwiftUI View objects to update from #Published ObservableObject or #EnvironmentObject variables

I've done a ton of searching and read a bunch of articles but I cannot get SwiftUI to dynamically update the view based on changing variables in the model, at least the kind of thing I'm doing. Basically I want to update the view based on the app's UNNotificationSettings.UNAuthorizationStatus. I have the app check the status on launch and display the status. If the status is not determined, then tapping on the text will trigger the request notifications dialog. However, the view doesn't update after the user either permits or denies the notifications. I'm sure I'm missing something fundamental because I've tried it a dozen ways, including with #Published ObservableObject, #ObservedObject, #EnvironmentObject, etc.
struct ContentView: View {
#EnvironmentObject var theViewModel : TestViewModel
var body: some View {
VStack {
Text(verbatim: "Notifications are: \(theViewModel.notificationSettings.authorizationStatus)")
.padding()
}
.onTapGesture {
if theViewModel.notificationSettings.authorizationStatus == .notDetermined {
theViewModel.requestNotificationPermissions()
}
}
}
}
class TestViewModel : ObservableObject {
#Published var notificationSettings : UNNotificationSettings
init() {
notificationSettings = type(of:self).getNotificationSettings()!
}
func requestNotificationPermissions() {
let permissionsToRequest : UNAuthorizationOptions = [.alert, .sound, .carPlay, .announcement, .badge]
UNUserNotificationCenter.current().requestAuthorization(options: permissionsToRequest) { granted, error in
if granted {
print("notification request GRANTED")
}
else {
print("notification request DENIED")
}
if let error = error {
print("Error requesting notifications:\n\(error)")
}
else {
DispatchQueue.main.sync {
self.notificationSettings = type(of:self).getNotificationSettings()!
}
}
}
}
static func getNotificationSettings() -> UNNotificationSettings? {
var settings : UNNotificationSettings?
let start = Date()
let semaphore = DispatchSemaphore(value: 0)
UNUserNotificationCenter.current().getNotificationSettings { notificationSettings in
settings = notificationSettings
semaphore.signal()
}
semaphore.wait()
while settings == nil {
let elapsed = start.distance(to: Date())
Thread.sleep(forTimeInterval: TimeInterval(0.001))
if elapsed > TimeInterval(1) {
print("ERROR: did not get notification settings in less than a second, giving up!")
break
}
}
if settings != nil {
print("\(Date()) Notifications are: \(settings!.authorizationStatus)")
}
return settings
}
}
func getUNAuthorizationStatusString(_ authStatus : UNAuthorizationStatus) -> String {
switch authStatus {
case .notDetermined: return "not determined"
case .denied: return "denied"
case .authorized: return "authorized"
case .provisional: return "provisional"
case .ephemeral: return "ephemeral"
#unknown default: return "unknown case with rawValue \(authStatus.rawValue)"
}
}
extension UNAuthorizationStatus : CustomStringConvertible {
public var description: String {
return getUNAuthorizationStatusString(self)
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ authStatus: UNAuthorizationStatus) {
appendLiteral(getUNAuthorizationStatusString(authStatus))
}
}
EDIT: I tried adding objectWillChange but the view still isn't updating.
class TestViewModel : ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var notificationSettings : UNNotificationSettings {
willSet {
objectWillChange.send()
}
}
init() {
notificationSettings = type(of:self).getNotificationSettings()!
}
Per the apple docs the properties wrappers like #Published should hold values. UNNotificationSettings is a reference type. Since the class gets mutated and the pointer never changes, #Publushed has no idea that you changed anything. Either publish a value (it make a struct and init it from he class) or manually send the objectwillChange message manually.
While I was not able to get it to work with manually using objectWillChange, I did create a basic working system as follows. Some functions are not repeated from the question above.
struct TestModel {
var notificationAuthorizationStatus : UNAuthorizationStatus
init() {
notificationAuthorizationStatus = getNotificationSettings()!.authorizationStatus
}
}
class TestViewModel : ObservableObject {
#Published var theModel = TestModel()
func requestAndUpdateNotificationStatus() {
requestNotificationPermissions()
theModel.notificationAuthorizationStatus = getNotificationSettings()!.authorizationStatus
}
}
struct ContentView: View {
#ObservedObject var theViewModel : TestViewModel
var body: some View {
VStack {
Button("Tap to update") {
theViewModel.requestAndUpdateNotificationStatus()
}
.padding()
switch theViewModel.theModel.notificationAuthorizationStatus {
case .notDetermined: Text("Notifications have not been requested yet.")
case .denied: Text("Notifications are denied.")
case .authorized: Text("Notifications are authorized.")
case .provisional: Text("Notifications are provisional.")
case .ephemeral: Text("Notifications are ephemeral.")
#unknown default: Text("Notifications status is an unexpected state.")
}
}
}
}

Memory leak when using CombineLatest in Swift Combine

I am using the Redux pattern for building a messaging application. Everything works fine so far but then I notice a memory leak in some parts of the app that I'm unable to solve. My view controller that binds to messages publisher. Deinit won't get called when the view controller is dismissed.
let messages = {
store.$state
.map { $0.chatState.messagesByChannel[self.channelId] }
.removeDuplicates()
.eraseToAnyPublisher()
}()
messages.combineLatest(Just("Hello world"))
.sink { [weak self] (messages, state) in
}
.store(in: &cancellableSet)
When I changed from referencing a dictionary object to another object in the chat state deinit gets called
let chatRoomDetailResponse = {
store.$state
.map { $0.chatState.getChatRoomDetailResponse }
.removeDuplicates()
.eraseToAnyPublisher()
}()
chatRoomDetailResponse.combineLatest(Just("Hello world"))
.sink { [weak self] (messages, state) in
}
.store(in: &cancellableSet)
This is a small snapshot of my store:
final public class Store<State: FluxState>: ObservableObject {
#Published public var state: State
private var dispatchFunction: DispatchFunction!
private let reducer: Reducer<State>
and my ChatState:
public struct ChatState: FluxState {
public typealias ChannelID = String
public var messagesByChannel: [ChannelID: [Message]] = [:]
public var getChatRoomDetailResponse: NetworkResponse<ChatChannel>? = nil
}
$0.chatState.messagesByChannel[self.channelId] is capturing self strongly, for the sake of being able to access its most-up-to-date channelId value.
Either catpure self weakly:
.map { [weak self] in
guard let strongSelf = self else { return ??? }
$0.chatState.messagesByChannel[strongSelf.channelId]
}
Or if channelId doesn't change, you can use a capture list to capture it by value:
.map { [channelId] in $0.chatState.messagesByChannel[channelId] }

Testing view models that rely on an asynchronous data source

I'm building a SwiftUI app that is using the MVVM pattern. The data source for the view models is provided by a custom Publisher for a Realm database. As I'm trying to be good and do a bit of test-driven development, I wrote a test to ensure that the view model responds appropriately to inputs from the SwiftUI front end (specifically in this instance, only querying the Realm once the UI was displayed). The code functions as expected but the test doesn't...
This is almost certainly because I'm not accounting for background processing / thread issues. My normal approach would to set up an expectation but this doesn't help as I need to use the property I'm interested in to create a Publisher but this completes immediately after emitting the initial state and I don't know how to keep it "alive" until the expectation expires. Can anyone point me in the right direction?
View model:
final class PatientListViewModel: ObservableObject, UnidirectionalDataFlowType {
typealias InputType = Input
enum Input {
case onAppear
}
private var cancellables = Set<AnyCancellable>()
private let onAppearSubject = PassthroughSubject<Void, Never>()
// MARK: Output
#Published private(set) var patients: [Patient] = []
// MARK: Private properties
private let realmSubject = PassthroughSubject<Array<Patient>, Never>()
private let realmService: RealmServiceType
// MARK: Initialiser
init(realmService: RealmServiceType) {
self.realmService = realmService
bindInputs()
bindOutputs()
}
// MARK: ViewModel protocol conformance (functional)
func apply(_ input: Input) {
switch input {
case .onAppear:
onAppearSubject.send()
}
}
// MARK: Private methods
private func bindInputs() {
let _ = onAppearSubject
.flatMap { [realmService] _ in realmService.all(Patient.self) }
.share()
.eraseToAnyPublisher()
.receive(on: RunLoop.main)
.subscribe(realmSubject)
.store(in: &cancellables)
}
private func bindOutputs() {
let _ = realmSubject
.assign(to: \.patients, on: self)
.store(in: &cancellables)
}
}
Test class: (very bulky due to my debugging code!)
import XCTest
import RealmSwift
import Combine
#testable import AthenaVS
class AthenaVSTests: XCTestCase {
private var cancellables = Set<AnyCancellable>()
private var service: RealmServiceType?
override func setUp() {
service = TestRealmService()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
service = nil
cancellables.removeAll()
}
func testPatientListViewModel() {
let viewModel = PatientListViewModel(realmService: service!)
let expectation = self.expectation(description: #function)
var outcome = Array<Patient>()
let _ = viewModel.patients.publisher.collect()
.handleEvents(receiveSubscription: { (subscription) in
print("Receive subscription")
}, receiveOutput: { output in
print("Received output: \(output)")
outcome = output
}, receiveCompletion: { _ in
print("Receive completion")
expectation.fulfill()
}, receiveCancel: {
print("Receive cancel")
expectation.fulfill()
}, receiveRequest: { demand in
print("Receive request: \(demand)")})
.sink { _ in }
.store(in: &cancellables)
viewModel.apply(.onAppear)
waitForExpectations(timeout: 2, handler: nil)
XCTAssertEqual(outcome.count, 4, "ViewModel state should change once triggered")
}
}
EDITED:
My apologies for the lack of clarity. The rest of the code base is as follows:
SwiftUI View
struct ContentView: View {
#ObservedObject var viewModel: PatientListViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.patients) { patient in
Text(patient.name)
}
.onDelete(perform: delete )
}
.navigationBarTitle("Patients")
.navigationBarItems(trailing:
Button(action: { self.viewModel.apply(.onAdd) })
{ Image(systemName: "plus.circle")
.font(.title)
}
)
}
.onAppear(perform: { self.viewModel.apply(.onAppear) })
}
func delete(at offset: IndexSet) {
viewModel.apply(.onDelete(offset))
}
}
Realm Service
protocol RealmServiceType {
func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object
#discardableResult
func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never>
func deletePatient(_ patient: Patient, from realm: Realm)
}
extension RealmServiceType {
func all<Element>(_ type: Element.Type) -> AnyPublisher<Array<Element>, Never> where Element: Object {
all(type, within: try! Realm())
}
func deletePatient(_ patient: Patient) {
deletePatient(patient, from: try! Realm())
}
}
final class TestRealmService: RealmServiceType {
private let patients = [
Patient(name: "Tiddles"), Patient(name: "Fang"), Patient(name: "Phoebe"), Patient(name: "Snowy")
]
init() {
let realm = try! Realm()
guard realm.isEmpty else { return }
try! realm.write {
for p in patients {
realm.add(p)
}
}
}
func all<Element>(_ type: Element.Type, within realm: Realm) -> AnyPublisher<Array<Element>, Never> where Element: Object {
return Publishers.realm(collection: realm.objects(type).sorted(byKeyPath: "name")).eraseToAnyPublisher()
}
func addPatient(_ name: String, to realm: Realm) throws -> AnyPublisher<Patient, Never> {
let patient = Patient(name: name)
try! realm.write {
realm.add(patient)
}
return Just(patient).eraseToAnyPublisher()
}
func deletePatient(_ patient: Patient, from realm: Realm) {
try! realm.write {
realm.delete(patient)
}
}
}
Custom Publisher (using Realm as a backend)
/ MARK: Custom publisher - produces a stream of Object arrays in response to change notifcations on a given Realm collection
extension Publishers {
struct Realm<Collection: RealmCollection>: Publisher {
typealias Output = Array<Collection.Element>
typealias Failure = Never // TODO: Not true but deal with this later
let collection: Collection
init(collection: Collection) {
self.collection = collection
}
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = RealmSubscription(subscriber: subscriber, collection: collection)
subscriber.receive(subscription: subscription)
}
}
}
// MARK: Convenience accessor function to the custom publisher
extension Publishers {
static func realm<Collection: RealmCollection>(collection: Collection) -> Publishers.Realm<Collection> {
return Publishers.Realm(collection: collection)
}
}
// MARK: Custom subscription
private final class RealmSubscription<S: Subscriber, Collection: RealmCollection>: Subscription where S.Input == Array<Collection.Element> {
private var subscriber: S?
private let collection: Collection
private var notificationToken: NotificationToken?
init(subscriber: S, collection: Collection) {
self.subscriber = subscriber
self.collection = collection
self.notificationToken = collection.observe { (changes: RealmCollectionChange) in
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
let _ = subscriber.receive(Array(collection.elements))
// case .update(_, let deletions, let insertions, let modifications):
case .update(_, _, _, _):
let _ = subscriber.receive(Array(collection.elements))
case .error(let error):
fatalError("\(error)")
#warning("Impl error handling - do we want to fail or log and recover?")
}
}
}
func request(_ demand: Subscribers.Demand) {
// no impl as RealmSubscriber is effectively just a sink
}
func cancel() {
subscriber = nil
notificationToken = nil
}
}
The issue I'm experiencing is a failure of the test case. I am anticipating the the view model will map an input (.onAppear) from the SwiftUI front end into an array of 'Patients' and assign this array to its patients property. The code works as expected but XCTAssertEqual fails, reporting that the 'patients' property is an empty array after calling 'viewmodel.assign(.onAppear)'. If I put a property observer on 'patients' it does update as expected but the test is not "seeing" the this.