#Published for a computed property (or best workaround) - ios

I'm trying to build an app with SwiftUI, and I'm just getting started with Combine framework. My first simple problem is that I'd like a single variable that defines when the app has been properly initialized. I'd like it to be driven by some nested objects, though. For example, the app is initialized when the account object is initialized, the project object is initialized, etc. My app could then use GlobalAppState.isInitialized, instead of inspected each nested object.
class GlobalAppState: ObservableObject {
#Published var account: Account = Account()
#Published var project: Project = Project()
#Published var isInitialized: Bool {
return self.account.initialized && self.project.initialized;
}
}
I get the error Property wrapper cannot be applied to a computed property
So...clearly, this is currently disallowed. Is there a way I can work around this??? I'd like to be able to use GlobalAppState.initialized as a flag in the app. More to the point, something like GlobalAppState.project.currentProject, which would be a computed property returning the currently selected project, etc...
I can see this pattern being used in a thousand different places! Any help would be wildly appreciated...
Thanks!

In this case there's no reason to use #Published for the isInialized property since it's derived from two other Published properties.
var isInitialized: Bool {
return self.account.initialized && self.project.initialized;
}

Here is one case if both account and project are structures.
struct Account{
var initialized : Bool = false
}
struct Project{
var initialized : Bool = false
}
class GlobalAppState: ObservableObject {
#Published var account: Account = Account()
#Published var project: Project = Project()
#Published var isInitialized: Bool = false
var cancellabel: AnyCancellable?
init(){
cancellabel = Publishers.CombineLatest($account, $project).receive(on: RunLoop.main).map{
return ($0.0.initialized && $0.1.initialized)
}.eraseToAnyPublisher().assign(to: \GlobalAppState.isInitialized, on: self) as AnyCancellable
}
}
struct GlobalAppStateView: View {
#ObservedObject var globalAppState = GlobalAppState()
var body: some View {
Group{
Text(String(globalAppState.isInitialized))
Button(action: { self.globalAppState.account.initialized.toggle()}){ Text("toggle Account init")}
Button(action: { self.globalAppState.project.initialized.toggle()}){Text("toggle Project init")}
}
}
}

Related

XCTest testing asyncronous Combine #Publishers [duplicate]

This question already has answers here:
How To UnitTest Combine Cancellables?
(2 answers)
Closed 1 year ago.
I'm working on an iOS app (utilizing Swift, XCTest, and Combine) trying to test a function within my view model, which is calling and setting a sink on a publisher. I'd like to test the view model, not the publisher itself. I really don't want to use DispatchQueue.asyncAfter( because theoretically I don't know how long the publisher will take to respond. For instance, how would I test XCTAssertFalse(viewModel.isLoading)
class ViewModel: ObservableObject {
#Published var isLoading: Bool = false
#Published var didError: Bool = false
var dataService: DataServiceProtocol
init(dataService: DataServiceProtocol) {
self.dataService = dataService
}
func getSomeData() { // <=== This is what I'm hoping to test
isLoading = true
dataService.getSomeData() //<=== This is the Publisher
.sink { (completion) in
switch completion {
case .failure(_):
DispatchQueue.main.async {
self.didError = true
}
case .finished:
print("finished")
}
DispatchQueue.main.async {
self.isLoading = false
}
} receiveValue: { (data) in
print("Ok here is the data", data)
}
}
}
I'd like to write a test that might look like:
func testGetSomeDataDidLoad() {
// this will test whether or not getSomeData
// loaded properly
let mockDataService: DataServiceProtocol = MockDataService
let viewModel = ViewModel(dataService: mockDataService)
viewModel.getSomeData()
// ===== THIS IS THE PROBLEM...how do we know to wait for getSomeData? ======
// It isn't a publisher...so we can't listen to it per se... is there a better way to solve this?
XCTAssertFalse(viewModel.isLoading)
XCTAssertFalse(viewModel.didError)
}
Really hoping to refactor our current tests so we don't utilize a DispatchQueue.asyncAfter(
Yeah, everybody's saying, MVVM increases testability. Which is hugely true, and thus a recommended pattern. But, how you test View Models is shown only very rarely in tutorials. So, how can we test this thing?
The basic idea testing a view model is using a mock which can do the following:
The mock must record changes in its output (which is the published properties)
Record a change of the output
Apply an assertion function to the output
Possibly record more changes
In order to work better with the following tests, refactor your ViewModel slightly, so it gets a single value representing your view state, using a struct:
final class MyViewModel {
struct ViewState {
var isLoading: Bool = false
var didError: Bool = false
}
#Published private(set) var viewState: ViewState = .init()
...
}
Then, define a Mock for your view. You might try something like this, which is a pretty naive implementation:
The mock view also gets a list of assertion functions which test your view state in order.
class MockView {
var viewModel: MyViewModel
var cancellable = Set<AnyCancellable>()
typealias AssertFunc = (MyViewModel.ViewState) -> Void
let asserts: ArraySlice<AssertFunc>
private var next: AssertFunc? = nil
init(viewModel: MyViewModel, asserts: [AssertFunc]) {
self.viewModel = viewModel
self.asserts = ArraySlice(asserts)
self.next = asserts.first
viewModel.$viewState
.sink { newViewState in
self.next?(newViewState)
self.next = self.asserts.dropFirst().first
}
}
}
You may setup the mock like this:
let mockView = MockView(
viewModel: viewModel,
asserts: [
{ state in
XCTAssertEqual(state.isLoading, false)
XCTAssertEqual(state.didError, false)
},
{ state in
XCTAssertEqual(state.isLoading, true)
...
},
...
])
You can also use XCT expectation in the assert functions.
Then, in your test you create the view model, your mock data service and the configured mockView.
let mockDataService: DataServiceProtocol = MockDataService
let viewModel = ViewModel(dataService: mockDataService)
let mockView = MockView(
viewModel: viewModel,
asserts: [
{ state in
XCTAssertEqual(state.isLoading, false)
XCTAssertEqual(state.didError, false)
},
...
{ state in
XCTAssertEqual(state.isLoading, false)
XCTAssertEqual(state.didError, false)
expectFinished.fulfill()
},
...
])
viewModel.getSomeData()
// wait for the expectations
Caution: I didn't compile or run the code.
You may also take a look at Entwine

Published object not publishing, am I doing it wrong?

My code looks like this:
final class MyModelController: ObservableObject {
#Published var model = MyModel()
}
enum ButtonSelection: Int {
case left, right
}
final class MyModel {
var buttonSelection: ButtonSelection?
}
I have injected an instance of MyModelController as an #EnvironmentObject into my SwiftUI views.
When I set myModelController.model.buttonSelection, I thought it would update myModelController.model and send out an update because it's marked as #Published. However, it doesn't. How can I fix this?
#Published only detects changes for value types. MyModel is a class, which is a reference type.
If possible, changing MyModel to a struct will fix this. However, if this is not possible, see the rest of this answer.
You can fix it with Combine. The below code will update MyModelController when model (now an ObservableObject) changes.
final class MyModelController: ObservableObject {
#Published var model = MyModel()
init() {
_ = model.objectWillChange.sink { [weak self] in
self?.objectWillChange.send()
}
}
}
/* ... */
final class MyModel: ObservableObject {
#Published var buttonSelection: ButtonSelection?
}

Passing variable object from View to Observable Object in Swift / SwiftUI

I'm trying to pass a variable object from a SwiftUI View to an observable Object but I'm running into the error: "Cannot use instance member 'loadedGroup' within property initializer; property initializers run before 'self' is available".
Here is how my SwiftUI View class is currently structured
struct LoadedGroupView: View {
#Binding var loadedGroup: group
#StateObject var userData = UserViewModel()
#StateObject var postData = PostViewModel(passedLoadedGroup: loadedGroup) //error here
var body: some View {
...
}
}
Here is my Observable Object class for PostViewModel()
class PostViewModel: ObservableObject {
var loadedGroup: group
let ref = Firestore.firestore()
init(passedLoadedGroup: group) {
group = passedLoadedGroup
}
}
How would I go about fixing this error because I really need to get that value passed into this observable object class from the View somehow. Thanks for the help!

How to add an item to a global list var initted/wrapped in ObservableObject in parent view and have it reflect in ForEach in SwiftUI in a child view?

When I click a button in ContentView.swift, I expect the action triggering an 'append' to a sportList would show the newly added item.. but it doesn't. No compile errors. Clicking on the button does nothing (even though I can see the SportItem being packaged up correctly) in a print statement.
I created a model for an item "SportItem" that has simple properties (e.g. name: String) and a ObservableObject class.
struct SportItem: Identifiable {
let name: String
}
I then create a global sportData variable and an ObservableObject class outside of everything:
var sportData = [
SportItem(name: "Tennis"),
SportItem(name: "Basketball")
]
class SportList: ObservableObject {
#Published var sportList: [SportItem]
init() {
self.sportList = sportData
}
}
In SportListView.swift, I have inside of the body:
#ObservedObject var sportList: SportList = SportList();
...
ForEach(sportList.sportList) {
sport in
VStack(alignment: .leading) {
Text(sport.name)
}
}
SportListView is referenced in ContentView.swift, which has:
var sportList:[SportItem] = sportItems
that SportList() using:
SportListView(sportList: SportList())
I also have a button in the parent ContentView.swift file where I have a button where the action of it performs a:
SportList().sportData.append(SportItem(name: "Soccer"))
When I click on that button, I notice the SportListView in the simulator does not add the new item. How do I get the list to be updated to show "Soccer" added onto the list?
You should keep sportData inside SportList (if needed via shared instance), like
class SportList: ObservableObject {
static let shared = SportList()
#Published var sportData: [SportItem] = [ SportItem(name: "Tennis"), SportItem(name: "Basketball") ]
}
and then
#ObservedObject var sportList: SportList = SportList.shared
...
ForEach(sportList.sportData) {
sport in
VStack(alignment: .leading) {
Text(sport.name)
}
}
and add like (if it is somewhere externally of view)
SportList.shared.sportData.append(SportItem(name: "Soccer"))

LinkingObjects one to many in Realm

I have stop which have many directions. I've managed to do it using function 'linkingObjects' but since last update it's deprecated and I should use object 'LinkingObjects'.
Stop
class Stop: Object {
dynamic var name:String = ""
var directions = List<Direction>()
override static func primaryKey() -> String? {
return "name"
}
}
Old Direction
class Direction: Object {
dynamic var tag:String = ""
var stop:Stop? {
return linkingObjects(Stop.self, forProperty: "directions").first
}
}
When I apply my previous approach to new object then I always get nil
New Direction with nil returned by LinkingObjects
class Direction: Object {
dynamic var tag:String = ""
let stop = LinkingObjects(fromType: Stop.self, property: "directions").first //always return nil
}
But here I'm getting array with one element. So it works as it should.
New Direction with nil returned by LinkingObjects
class Direction: Object {
dynamic var tag:String = ""
let stops = LinkingObjects(fromType: Stop.self, property: "directions")
}
Question
Is there other way to use 'LinkingObjects' rather than this last example because using in every time 'direction.stop.first?.name' instead of 'direction.stop?.stop'?
Of course, I could use in 'direction' function that will take always pick first element in 'stops' but maybe I don't have to.
UPDATE
In meanwhile I’m using this. This isn’t ideal solution, but works.
private let stops:LinkingObjects<Stop> = LinkingObjects(fromType: Stop.self, property: "directions")
var stop:Stop? {
return self.stops.first
}
The solution you came up with yourself for now is the best what you could do.
We discussed whether we should expose any other API for singular backlinks, but as there is no way to enforce their multiplicity on the data storage layer, it didn't made sense so far. In addition, you would still need a wrapper object, so that we can propagate updates. For that reason a unified way to retrieve the value via LinkedObjects seemed to be the way so far.
What about specifiying the relation the other way around?
Specify the connection on the 'one'-side
Do a query in the getter on the 'many'-side:
So it should read like this:
class Child: Object {
dynamic var name:String = ""
dynamic var parent:Parent? = nil
}
class Parent: Object {
dynamic var name:String = ""
var children:Results<Child>? {
return realm?.objects(Child.self).filter(NSPredicate(format: "parent == %#", self))
}
}

Resources