Best architecture for ViewModels (RxSwift) - mvvm

I'm wanting to utilise a architectural design that enables me to clearly designate input and outputs in my view model (How To Feed ViewModels) but am curious as to how I can best integrate the "working" part of the view model into this structure.
I have tended to use Actions (perhaps not very elegantly) to bind UI elements to the work they need to perform. The problem of course is that some of these Actions rely on view model properties so I can't create them in init() in the same was as Input and Outputs as the properties are not yet initialised. It's possible to work around this by defining them as private lazy vars and then exposing them via a struct that essentially presents a public interface to Action. It doesn't seem to flow very well though and I'm learning that if you're expending a lot of effort to get structure to bend to your will, it's probably a code smell. Code example below - suggestions welcome :-)
protocol PatientListViewModelType: ViewModelType { }
final class PatientListViewModel: PatientListViewModelType {
// MARK:- Protocol conformance
typealias Dependencies = HasPatientService
struct Input {
let patient: AnyObserver<Patient>
}
struct Output {
let sectionedPatients: Observable<[PatientSection]>
let patient: Observable<Patient>
}
let input: Input
let output: Output
struct Actions {
let deletePatient: Action<Patient, Void>
let togglePatient: (Patient) -> CocoaAction
let updatePatient: (Patient) -> Action<String, Void>
}
lazy var action: Actions = Actions(deletePatient: self.deletePatient,
togglePatient: self.togglePatient,
updatePatient: self.updatePatient)
// MARK: Setup
private let dependencies: Dependencies
private let patientSubject = ReplaySubject<Patient>.create(bufferSize: 1)
// MARK:- Init
init(dependencies: Dependencies) {
self.dependencies = dependencies
let sectionedPatients =
dependencies.patientService.patients()
.map { results -> [PatientSection] in
let scheduledPatients = results
.filter("checked == nil")
.sorted(byKeyPath: "created", ascending: false)
let admittedPatients = results
.filter("checked != nil")
.sorted(byKeyPath: "checked", ascending: false)
return [
PatientSection(model: "Scheduled Patients", items: scheduledPatients.toArray()),
PatientSection(model: "Admitted Patients", items: admittedPatients.toArray())
]
}
self.output = Output(sectionedPatients: sectionedPatients,
patient: patientSubject.asObservable() )
// this is immediately overriden during binding to VC - it just allows us to exit the init without errors
self.input = Input(patient: patientSubject.asObserver())
}
// MARK:- Actions
private lazy var deletePatient: Action<Patient, Void> = { (service: PatientServiceType) in
return Action { patient in
return service.delete(realmObject: patient)
}
}(self.dependencies.patientService)
lazy var togglePatient: (Patient) -> CocoaAction = { [unowned self] (patient: Patient) -> CocoaAction in
return CocoaAction {
return self.dependencies.patientService.toggle(patient: patient).map { _ in }
}
}
private lazy var updatePatient: (Patient) -> Action<String, Void> = { [unowned self] (patient: Patient) in
return Action { newName in
return self.dependencies.patientService.update(patient: patient, name: newName).map { _ in }
}
}
}

The answer is actually simple enough once I got a chance to sit down and have a play. I've put Actions into the Output struct (as the seemed to be the most logical place) rather than creating a dedicated interface as before. The next question of course is whether Actions are the best fit for the problem but I'll deal with that later...
final class PatientListViewModel: PatientListViewModelType {
// MARK:- Protocol conformance
typealias Dependencies = HasPatientService
struct Input {
let patient: AnyObserver<Patient>
}
let input: Input
struct Output {
let sectionedPatients: Observable<[PatientSection]>
let patient: Observable<Patient>
let deletePatient: Action<Patient, Void>
let togglePatient: (Patient) -> CocoaAction
let updatePatient: (Patient) -> Action<String, Void>
}
let output: Output
// MARK: Setup
private let dependencies: Dependencies
private let patientSubject = ReplaySubject<Patient>.create(bufferSize: 1)
// MARK:- Init
init(dependencies: Dependencies) {
self.dependencies = dependencies
let sectionedPatients =
dependencies.patientService.patients()
.map { results -> [PatientSection] in
let scheduledPatients = results
.filter("checked == nil")
.sorted(byKeyPath: "created", ascending: false)
let admittedPatients = results
.filter("checked != nil")
.sorted(byKeyPath: "checked", ascending: false)
return [
PatientSection(model: "Scheduled Patients", items: scheduledPatients.toArray()),
PatientSection(model: "Admitted Patients", items: admittedPatients.toArray())
]
}
let deletePatient: Action<Patient, Void> = { patientService in
return Action { patient in
return patientService.delete(realmObject: patient)
}
}(dependencies.patientService)
let togglePatient: (Patient) -> CocoaAction = { patient in
return CocoaAction {
return dependencies.patientService.toggle(patient: patient)
.map { _ in }
}
}
let updatePatient: (Patient) -> Action<String, Void> = { patient in
return Action { newName in
return dependencies.patientService.update(patient: patient, name: newName)
.map { _ in }
}
}
// this is immediately overriden during binding to VC - it just allows us to exit the init without errors
self.input = Input(patient: patientSubject.asObserver())
self.output = Output(sectionedPatients: sectionedPatients,
patient: patientSubject.asObservable(),
deletePatient: deletePatient,
togglePatient: togglePatient,
updatePatient: updatePatient)
}

Related

Efficiently refactoring piece of Swift code to be less redundant

In the code below, A key is remapped to B key, and vice versa. The remapping is activated via a SwiftUI toggle switch.
In example presented here the same block of code is used in three different functions.
Additionally, the loop that iterates through the function call is also used in all three of these functions.
I've been struggling to simplify this code and make it less redundant for more than a day. Any help would be greatly appreciated.
let aKey: UInt64 = 0x700000004
let bKey: UInt64 = 0x700000005
func isKeyboardServiceClientForUsagePage(_ serviceClient: IOHIDServiceClient, _ usagePage: UInt32, _ usage: UInt32) -> Bool {
return IOHIDServiceClientConformsTo(serviceClient, usagePage, usage) == 1
}
func updateKeyboardKeyMapping(_ keyMap: [[String: UInt64]]) {
let eventSystemClient = IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault)
guard let serviceClients = IOHIDEventSystemClientCopyServices(eventSystemClient) as? [IOHIDServiceClient] else {
return
}
for serviceClient in serviceClients {
let usagePage = UInt32(kHIDPage_GenericDesktop)
let usage = UInt32(kHIDUsage_GD_Keyboard)
if isKeyboardServiceClientForUsagePage(serviceClient, usagePage, usage) {
IOHIDServiceClientSetProperty(serviceClient, kIOHIDUserKeyUsageMapKey as CFString, keyMap as CFArray)
}
}
}
func areKeysMappedOnAnyServiceClient() -> Bool {
let eventSystemClient = IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault)
guard let serviceClients = IOHIDEventSystemClientCopyServices(eventSystemClient) as? [IOHIDServiceClient] else {
return false
}
for serviceClient in serviceClients {
let usagePage = UInt32(kHIDPage_GenericDesktop)
let usage = UInt32(kHIDUsage_GD_Keyboard)
if isKeyboardServiceClientForUsagePage(serviceClient, usagePage, usage) {
guard let keyMapping = IOHIDServiceClientCopyProperty(serviceClient, kIOHIDUserKeyUsageMapKey as CFString) as? [[String: UInt64]] else {
return false
}
if keyMapping.contains(where: { $0[kIOHIDKeyboardModifierMappingSrcKey] == aKey && $0[kIOHIDKeyboardModifierMappingDstKey] == bKey }) &&
keyMapping.contains(where: { $0[kIOHIDKeyboardModifierMappingSrcKey] == bKey && $0[kIOHIDKeyboardModifierMappingDstKey] == aKey })
{
return true
}
}
}
return false
}
func remapABBA() {
let keyMap: [[String: UInt64]] = [
[
kIOHIDKeyboardModifierMappingSrcKey: aKey,
kIOHIDKeyboardModifierMappingDstKey: bKey,
],
[
kIOHIDKeyboardModifierMappingSrcKey: bKey,
kIOHIDKeyboardModifierMappingDstKey: aKey,
],
]
updateKeyboardKeyMapping(keyMap)
}
func resetKeyMapping() {
updateKeyboardKeyMapping([])
}
And here’s the SwiftUI part if you would like to try the app:
import SwiftUI
struct ContentView: View {
#State private var remapKeys = areKeysMappedOnAnyServiceClient()
var body: some View {
HStack {
Spacer()
Toggle(isOn: $remapKeys, label: { Text("Remap A → B and B → A.") })
.toggleStyle(SwitchToggleStyle())
.onChange(of: remapKeys, perform: toggleKeyboardRemapping)
Spacer()
}
}
}
private func toggleKeyboardRemapping(_ remapKeys: Bool) {
if remapKeys {
remapABBA()
} else {
resetKeyMapping()
}
}
OK... this is going to take some time to answer.
It seems like you're lacking in a place to store things. That's why you have to use the same block of code over and over. We can solve that with a view model...
In here I'm going to hide away the logic of what is happening from the view and only expose what the view needs access to in order to display itself.
// we make it observable so the view can subscribe to it.
class KeyMappingViewModel: ObservableObject {
private let aKey: UInt64 = 0x700000004
private let bKey: UInt64 = 0x700000005
private let srcKey = kIOHIDKeyboardModifierMappingSrcKey
private let dstKey = kIOHIDKeyboardModifierMappingDstKey
private var keyMap: [[String: UInt64]] {
[
[
srcKey: aKey,
dstKey: bKey,
],
[
srcKey: bKey,
dstKey: aKey,
],
]
}
// A more concise way to get hold of the client ref
private var client: IOHIDEventSystemClientRef {
IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault)
}
// Making this published means the view can use it as state in the Toggle
#Published var toggleState: Bool {
didSet {
if toggleState {
client.updateKeyMapping(keyMap)
} else {
client.updateKeyMapping([])
}
}
}
init() {
// set the initial value by asking the client if it has any keys mapped
toggleState = client.areKeysMappedOnAnyServiceClient(aKey: aKey, bKey: bKey)
}
}
I'm going to make extensions of IOHIDServiceClient and IOHIDEventSystemClientRef to encapsulate your logic...
extension IOHIDEventSystemClientRef {
private var srcKey: String { kIOHIDKeyboardModifierMappingSrcKey }
private var dstKey: String { kIOHIDKeyboardModifierMappingDstKey }
// Make this an optional var on the client ref itself.
private var serviceClients: [IOHIDServiceClient]? {
IOHIDEventSystemClientCopyServices(self) as? [IOHIDServiceClient]
}
func areKeysMappedOnAnyServiceClient(aKey: UInt64, bKey: UInt64) -> Bool {
// Nice Swift 5.7 syntax with the optional var
guard let serviceClients else {
return false
}
// I made this more concise with a filter and map.
// Also, using the extension we can make use of keyPaths to get the values.
return serviceClients.filter(\.isForGDKeyboard)
.compactMap(\.keyMapping)
.map { keyMapping in
keyMapping.contains(where: { $0[srcKey] == aKey && $0[dstKey] == bKey }) &&
keyMapping.contains(where: { $0[srcKey] == bKey && $0[dstKey] == aKey })
}
.contains(true)
}
func updateKeyMapping(_ keyMap: [[String: UInt64]]) {
// serviceClients is optional so we can just ? it.
// if it's nil, nothing after the ? happens.
serviceClients?.filter(\.isForGDKeyboard)
.forEach {
IOHIDServiceClientSetProperty($0, kIOHIDUserKeyUsageMapKey as CFString, keyMap as CFArray)
}
}
}
extension IOHIDServiceClient {
var isForGDKeyboard: Bool {
let usagePage = UInt32(kHIDPage_GenericDesktop)
let usage = UInt32(kHIDUsage_GD_Keyboard)
return IOHIDServiceClientConformsTo(self, usagePage, usage) == 1
}
var keyMapping: [[String: UInt64]]? {
IOHIDServiceClientCopyProperty(self, kIOHIDUserKeyUsageMapKey as CFString) as? [[String: UInt64]]
}
}
Doing all of this means that your view can look something like this...
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: KeyMappingViewModel = .init()
var body: some View {
HStack {
Spacer()
Toggle(isOn: $viewModel.toggleState) {
Text("Remap A → B and B → A.")
}
.toggleStyle(SwitchToggleStyle())
Spacer()
}
}
}
This contains all your same logic and TBH wasn't too bad already.
My main changes were to take the free functions and vars and add them to their respective types.
So, the update and areKeysMapped... functions now belong to the IOHIDEventSystemClientRef type.
The isForGDKeyboard and keyMapping vars now belong to the IOHIDServiceClient type.
Doing this removed a lot of the repeated code you had as you no longer had to continuously call free functions. It also meant we unlocked some very Swifty keyPath usage which helped make some of the logic more concise.
Then we made a view model. This allowed us to keep all the moving parts of the view in one place. It had a place to easily get hold of the client. It also meant we could hide a lot of the stuff inside the view model by making it private.
This meant that the view only had one thing it could do. Which is to use the binding to the toggleState. Everything else was behind closed doors to the view.

RxSwift, RxCocoa - no called when writing TextField after validation

I am new to RxSwift and RxCocoa
I need to any advice for learning
After result of Checking Id Validation, expect no word in label
But it is updating label and no entering in break point at bind function
What’s problem my code…?
var disposeBag: DisposeBag = DisposeBag()
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input: Signal<String> = userIDTextField.rx.text.orEmpty
.asSignal(onErrorSignalWith: .empty())
let output: Driver<String> = viewModel.bind(input)
disposeBag.insert(
output.drive(userIDLabel.rx.text)
)
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Signal<Bool> {
return .just(false).asSignal()
}
func bind(_ input: Signal<String>) -> Driver<String> {
let validState = input
.map { _ in self.checkUserIDFromDB(id:)}
.withLatestFrom(input)
return validState.asDriver(onErrorDriveWith: .empty())
}
}
This line: .map { _ in self.checkUserIDFromDB(id:)} produces a Signal<(String) -> Signal<Bool>> which is likely not what you wanted.
I'm going to assume that the goal here is to pass the entered string to the network request and wait for it to emit. If it emits true then emit the string to the label, otherwise do nothing...
Further, let's simplify things by using the Observable type instead of Signals and Drivers:
final class ViewController: UIViewController {
let userIDTextField = UITextField()
let userIDLabel = UILabel()
let disposeBag = DisposeBag() // this should be a `let` not a `var`
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input = userIDTextField.rx.text.orEmpty
let output = viewModel.bind(input.asObservable())
disposeBag.insert(
output.bind(to: userIDLabel.rx.text)
)
}
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Observable<Bool> { .just(false) }
func bind(_ input: Observable<String>) -> Observable<String> {
input.flatMapLatest { id in // Note this should be `flatMapLatest` not `map`
Observable.zip( // zip up the text with its response
Observable.just(id),
self.checkUserIDFromDB(id: id) // you weren't actually making the network call. This makes it.
.catchAndReturn(false) // if the call fails, emit `false`.
)
}
.compactMap { $0.1 ? $0.0 : nil } // if the response is true, emit the text, else nothing
}
}
The biggest concern I have with this code is what happens if the user continues to type. This will fire after every character the user enters which could be a lot of network requests, the flatMapLatest will cancel ongoing requests that are no longer needed, but still... Consider putting a debounce in the stream to reduce the number of requests.
Learn more about the various versions of flatMap from this article.
Edit
In response to your comment. In my opinion, a ViewModel should not be dependent on RxCocoa, only RxSwift. However, if you feel you must use Driver, then something like this would be appropriate:
func bind(_ input: ControlProperty<String>) -> Driver<String> {
input.asDriver()
.flatMapLatest { id in
Driver.zip(
Driver.just(id),
self.checkUserIDFromDB(id: id)
.asDriver(onErrorJustReturn: false)
)
}
.compactMap { $0.1 ? $0.0 : nil }
}
Using Signal doesn't make much sense in this context.

Swift unit testing view model interface

As I understand, it is best to only test public methods of a class.
Let's have a look at this example. I have a view model for the view controller.
protocol MyViewModelProtocol {
var items: [SomeItem] { get }
var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }
func viewLoaded()
}
class MyViewModel: MyViewModelProtocol {
func viewLoaded() {
let items = createDetailsCellModels()
updateCellModels(with: items)
requestDetails()
}
}
I want to test class viewLoaded(). This class calls two other methods - updateItems() and requestDetails()
One of the methods sets up the items and the other one call API to retrieve data and update those items. Items array us updated two times and onInsertItemsAtIndexPaths are called two times - when setting up those items and when updating with new data.
I can test whether after calling viewLoaded() expected items are set up and that onInsertItemsAtIndexPaths is called.
However, the test method will become rather complex.
What is your view, should I test those two methods separately or just write this one huge test?
By testing only viewLoaded(), my idea is that the implementation can change and I only care that results are what I expect.
I think the same thing, only public functions should be tested, since public ones use private ones, and your view on MVVM is correct. You can improve it by adding a DataSource and a Mapper that allows you to improve testing.
However, yes, the test seems huge to me, the tests should test simple units and ensure that small parts of the code work well, with the example you show is difficult, you need to divide by layers (clean code).
In the example you load the data into the viewModel and make it difficult to mockup the data. But if you have a Domain layer you can pass the UseCase mock to the viewModel and control the result. If you run a test on your example, the result will also depend on what the endpoint returns. (404, 200, empty array, data with error ...). So it is important, for testing purposes, to have a good separation by layers. (Presentation, Domain and Data) to be able to test each one separately.
I give you an example of how I would test a view mode, sure there are better and cooler examples, but it's an approach.
Here you can see a viewModel
protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}
protocol BeersListViewModelInput {
func viewDidLoad()
func updateView()
func image(url: String?, index: Int) -> Cancellable?
}
protocol BeersListViewModelOutput {
var items: Box<BeersListModel?> { get }
var loadingStatus: Box<LoadingStatus?> { get }
var error: Box<Error?> { get }
}
final class DefaultBeersListViewModel {
private let beersListUseCase: BeersListUseCase
private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
var items: Box<BeersListModel?> = Box(nil)
var loadingStatus: Box<LoadingStatus?> = Box(.stop)
var error: Box<Error?> = Box(nil)
#discardableResult
init(beersListUseCase: BeersListUseCase) {
self.beersListUseCase = beersListUseCase
}
func viewDidLoad() {
updateView()
}
}
// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
func updateView() {
self.loadingStatus.value = .start
beersLoadTask = beersListUseCase.execute(completion: { (result) in
switch result {
case .success(let beers):
let beers = beers.map { DefaultBeerModel(beer: $0) }
self.items.value = DefaultBeersListModel(beers: beers)
case .failure(let error):
self.error.value = error
}
self.loadingStatus.value = .stop
})
}
}
// MARK: - Images
extension DefaultBeersListViewModel {
func image(url: String?, index: Int) -> Cancellable? {
guard let url = url else { return nil }
return beersListUseCase.image(with: url, completion: { (result) in
switch result {
case .success(let imageData):
self.items.value?.items?[index].image.value = imageData
case .failure(let error ):
print("image error: \(error)")
}
})
}
}
Here you can see the viewModel test using mocks for the data and view.
class BeerListViewModelTest: XCTestCase {
private enum ErrorMock: Error {
case error
}
class BeersListUseCaseMock: BeersListUseCase {
var error: Error?
var expt: XCTestExpectation?
func execute(completion: #escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
let beersMock = BeersMock.makeBeerListEntityMock()
if let error = error {
completion(.failure(error))
} else {
completion(.success(beersMock))
}
expt?.fulfill()
return nil
}
func image(with imageUrl: String, completion: #escaping (Result<Data, Error>) -> Void) -> Cancellable? {
return nil
}
}
func testWhenAPIReturnAllData() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "All OK")
beersListUseCaseMock.error = nil
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.items.bind { (_) in}
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNotNil(viewModel.items.value)
XCTAssertNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
func testWhenDataReturnsError() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "Error")
beersListUseCaseMock.error = ErrorMock.error
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNil(viewModel.items.value)
XCTAssertNotNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
}
in this way you can test the view, the business logic and the data separately, in addition to being a code that is very reusable.
Hope this helps you, I have it posted on github in case you need it.
https://github.com/cardona/MVVM

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.

Swift: too many static functions?

I have a class that represents Calendar items (model) retrieved from the event store. I haven't implemented any delegation yet for the AppDelegate or ViewControllers.
All my methods in this class are static functions - the main reason is so that I can "see" them from the AppDelegate or the VC. I have a suspicion that:
1) I need to make this a singleton - whose only function is to retrieve calendar items from the eventStore and post to the UI
2) learn how to code better - perhaps creating an instance of the class in the AppDelegate and the VC
This is still very fuzzy to me - not sure if posting code would help, but the class has a bunch of "static func .... doSomething() { ...}" and is called by the AppDelegate and VC as "ClassName.doSomething()..."
I'm prepared to refactor the Class code, thinking that a singleton would work - or perhaps things are just fine as they are...
EDITED: Adding code:
import Foundation
import EventKit
class Calendars: NSObject {
enum calendarAuthState {
case restricted
case authorized
case denied
case notDetermined
}
struct Calendar {
var id: String
var color: NSColor
var title: String
var isUserActive: Bool
var events: [EventItem]
}
struct EventItem {
var originalStartDate: Date
var date: String
var title: String
var isAllDayEvent: Bool
}
static var calendarState: calendarAuthState = .notDetermined
static var eventStore = EKEventStore()
static var currentCalendars = [Calendar]()
//MARK: Check Calendar Authorization Status
static func calendarAuthorizationStatus() {
let status = EKEventStore.authorizationStatus(for: .event)
switch (status) {
case EKAuthorizationStatus.notDetermined:
// This happens on first-run
calendarState = .notDetermined
case EKAuthorizationStatus.authorized:
calendarState = .authorized
case EKAuthorizationStatus.restricted:
self.requestAccessToCalendar()
calendarState = .restricted
case EKAuthorizationStatus.denied:
self.requestAccessToCalendar()
calendarState = .denied
}
}
static func requestAccessToCalendar() {
self.eventStore.requestAccess(to: EKEntityType.event, completion: {
(accessGranted: Bool, error: Error?) in
if accessGranted == true {
DispatchQueue.main.async(execute: {
self.calendarState = .authorized
})
} else {
DispatchQueue.main.async(execute: {
self.calendarState = .denied
})
}
})
}
//MARK: Do the two below
static func createMenuFromCalendars() {
guard calendarState == .authorized else {
return
}
let calendars = self.returnCalendars()
guard calendars.count >= 0 else {
return
}
self.addCalendarsToMenuItems(from: calendars)
}
//MARK: First, return the calendar titles from the Store
static func returnCalendars() -> [Calendar] {
guard self.calendarState == .authorized else {
return[]
}
let calendars = self.eventStore.calendars(for: .event)
for calendar in calendars {
self.currentCalendars.append(Calendar(id: calendar.calendarIdentifier, color: calendar.color, title: calendar.title, isUserActive: false, events: []))
}
return self.currentCalendars
}
//MARK: Next, send those to the Menu for MenuItem creation
static func addCalendarsToMenuItems(from calendars:[Calendar]) {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
let appMainMenu = NSApp.mainMenu
if let calendarMenu = appMainMenu?.item(withTitle: "Calendars") {
let calendarSubMenu = calendarMenu.submenu
for calendar in calendars {
let menuItem = calendarSubMenu?.addItem(withTitle: calendar.title, action: #selector(appDelegate.actionFromSelectedCalendar) , keyEquivalent: "")
menuItem?.isEnabled = true
menuItem?.state = .off
menuItem?.target = appDelegate.self
menuItem?.toolTip = calendar.id
}
}
}
class func retrieveCalendarEvents() {
guard self.calendarState == .authorized || !(self.currentCalendars.isEmpty) else {
return
}
let startDate = Date()
let endDate = Date(timeIntervalSinceNow: 4*24*3600)
var activeCalendars = findUserActiveCalendars(in: currentCalendars)
//need to flush the events at this stage or they'll pile
guard !((activeCalendars?.isEmpty)!) else {
return
}
var eventCalendar = [EKCalendar]()
for dayBookCalendar in activeCalendars! {
// much of the risk here is unwrapping optionals unsafely!!!!! - refactor this and other please
eventCalendar.append(self.eventStore.calendar(withIdentifier: dayBookCalendar.id)!)
let eventPredicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: eventCalendar)
let returnedEvents = eventStore.events(matching: eventPredicate)
let calendarIndex = findCalendarIndex(by: dayBookCalendar.id, in: currentCalendars)
for event in returnedEvents {
let eventItems = eventItem(from: event)
currentCalendars[calendarIndex!].events.append(eventItems)
}
}
}
//MARK: Helper methods and stuff
static func changeUserCalendarState(with id:String, state:Bool) {
guard !(currentCalendars.isEmpty) else {
return
}
let calendarIndex = findCalendarIndex(by: id, in:self.currentCalendars)
if let calendarIndex = calendarIndex {
currentCalendars[calendarIndex].isUserActive = !state
retrieveCalendarEvents()
}
}
static func findCalendarIndex(by id:String, in calendarArray: [Calendar]) -> Int? {
return calendarArray.index(where: {$0.id == id})
}
static func findUserActiveCalendars(in calendarArray: [Calendar]) -> [Calendar]? {
return calendarArray.filter({$0.isUserActive == true})
}
// static func flushEventsFromCalendar(in calendarArray: inout [Calendar]) {
// calendarArray.map({$0.events.removeAll()})
// }
static func eventItem(from events:EKEvent) -> EventItem {
return EventItem(originalStartDate: events.startDate, date:eventTime(from: events.startDate), title: events.title!, isAllDayEvent: events.isAllDay)
}
static func parseCalendarEvents(from events:[EKEvent]) -> [EventItem] { //can this be variadic?
var calendarEvents = [EventItem]()
for event in events {
calendarEvents.append(eventItem(from: event))
}
return calendarEvents
}
static func eventTime(from date:Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale.current
let stringTime = dateFormatter.string(from: date)
return stringTime
}
}
''
I think you're making an elementary mistake about object-oriented programming. In your Calendars class you seem to have encapsulated all the code for accessing the user's calendar. Then you seem to have reasoned: "Well, this code needs to be callable from anywhere. Therefore all my class's members need to be global (static / class)."
That's a mistake. There is nothing wrong with doing such encapsulation; indeed it's a good thing. But then the way to use your encapsulation is with a helper instance. For example, let's say you're in a view controller (which is most likely after all). Then it can have a property:
let calendarHelper = Calendars()
Now all (or nearly all) your members can (and should) become instance members. Remember, instances of the same type each get to maintain state separately from one another; that is part of their encapsulation. You're going to want that ability.
If your underlying reason for thinking you need static/class members is that you only want one EKEventStore instance for the life of the app, then push the globalness / staticness down to that one object (e.g. by a "shared" EKEventStore and methods for accessing it) and let everything else be a normal instance member.
From what you've said, suspicion 1) is correct - you need to use a singleton:
class CalendarService {
private var eventStore = EKEventStore()
//Static shared instance, this is your singleton
static var sharedInstance = CalendarService()
//Your public methods for adding events can go here
public func doSomething() {
//...
}
//As can your private methods for producing, deleting and editing calendar events + checking permissions
}
Usage:
CalendarService.sharedInstance.doSomething()
I can't really say much more without specific examples of your existing code.