I would like to hide my Realm implementation and instead of working on RLMNotificationBlock I would like to use RXSwift. Below how my method looks like now (RLMNotificationBlock is a block that takes String and RLMRealm):
func addNotificationBlock(block: RLMNotificationBlock) -> RLMNotificationToken? {
let rlmObject = ...
return rlmObject.addNotificationBlock(block)
}
But I would like to switch to more reactive observer-pattern way. I looked at RxSwift docs and source code of rx_clickedButtonAtIndex, but I cannot figure out how I should put all these things together. I guess my code at the end would look like:
public var rx_realmContentChanged: ControlEvent<Int> {
let controlEvent = ControlEvent()
// My code go here
return controlEvent
}
I'm new with RXSwift and know only the basics. Any help will be appreciated.
There is an Rx Realm extension available on GitHub you can use: https://github.com/RxSwiftCommunity/RxRealm
It allows you to get an Observable out of a single Realm object or a Realm Collection. Here's an example from the README:
let realm = try! Realm()
let laps = realm.objects(Lap.self))
Observable.changesetFrom(laps)
.subscribe(onNext: { results, changes in
if let changes = changes {
// it's an update
print(results)
print("deleted: \(changes.deleted) inserted: \(changes.inserted) updated: \(changes.updated)")
} else {
// it's the initial data
print(results)
}
})
There is also an additional library especially built for binding table and collection views called RxRealmDataSources
If I understood you correctly, you just want to return Observable<RLMNotificationToken>
In this case you just need to do something like this
func addNotificationBlock(block: RLMNotificationBlock) -> Observable<RLMNotificationToken> {
return create { observer -> Disposable in
let rlmObject = ...
let token = rlmObject.addNotificationBlock(block)
// Some condition
observer.onNext(token)
// Some other condition
observer.onError(NSError(domain: "My domain", code: -1, userInfo: nil))
return AnonymousDisposable {
// Dispose resources here
}
// If you have nothing to dipose return `NopDisposable.instance`
}
}
In order to use it bind it to button rx_tap or other use flatMap operator
Related
I am trying to create a UICollectionView, so that I can add and delete items from it's data source as a Driver. I have a viewModel below
import Photos
import RxCocoa
import RxSwift
class GroupedAssetsViewModel {
enum ItemAction {
case add(item: PHAssetGroup)
case remove(indexPaths: [IndexPath])
}
let assets: Driver<[GroupedAssetSectionModel]>
let actions = PublishSubject<ItemAction>()
private let deletionService: AssetDeletionService = AssetDeletionServiceImpl()
init() {
assets = actions
.debug()
.scan(into: []) { [deletionService] items, action in
switch action {
case .add(let item):
let model = GroupedAssetSectionModel()
items.append(GroupedAssetSectionModel(original: model, items: item.assets))
case .remove(let indexPaths):
var assets = [PHAsset]()
for indexPath in indexPaths {
items[indexPath.section].items.remove(at: indexPath.item)
assets.append(items[indexPath.section].items[indexPath.row])
}
deletionService.delete(assets: assets)
}
}
.asDriver(onErrorJustReturn: [])
}
func setup(with assetArray: [PHAssetGroup] = [PHAssetGroup]()) {
for group in assetArray {
actions.onNext(.add(item: group))
}
}
}
but .scan closure is never being called, even though actions.onNext is being called in setup, therefore Driver's value is always empty.
It seems like I am getting some core concepts wrong, what might be the problem here?
Just because you have
actions.onNext(.add(item: group)) doesn't mean this sequence has started. You are publishing events to a subject that hasn't started. You must have a subscriber somewhere first for assets. Then only scan will get executed. Because observables are pull driven sequences. There must be a subscriber to even make them start.
I am trying to achieve something similar in rxswift example project from RxSwift repo. But in my case there are dependent observables. I couldn't find any solution without using binding in viewmodel
Here is the structure of my viewmodel:
First the definitions of input, output and viewmodel
typealias UserListViewModelInput = (
viewAppearAction: Observable<Void>,
deleteAction: Observable<Int>
)
typealias UserListViewModelOutput = Driver<[User]>
typealias UserListViewModel = (UserListViewModelInput, #escaping UserApi) -> UserListViewModelOutput
Then there is actual implementation which doesn't compile.
let userListViewModel: UserListViewModel = { input, loadUsers in
let loadedUserList = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.elements()
.asDriver(onErrorDriveWith: .never())
let userListAfterDelete = input.deleteAction
.withLatestFrom(userList) { index, users in
users.enumerated().compactMap { $0.offset != index ? $0.element : nil }
}
.asDriver(onErrorJustReturn: [])
let userList = Driver.merge([loadedUserList, userListAfterDelete])
return userList
}
Viewmodel has two job. First load the user list. Second is delete a user at index. The final output is the user list which is downloaded with UserApi minus deleted users.
The problem in here in order the define userList I need to define userListAfterDelete. And in order to define userListAfterDelete I need to define userList.
So is there a way to break this cycle without using binding inside view model? Like a placeholder observable or operator that keeps state?
This is a job for a state machine. What you will see in the code below is that there are two actions that can affect the User array. When the view appears, a new array is downloaded, when delete comes in, a particular user is removed.
This is likely the most common pattern seen in reactive code dealing with state. So common that there are whole libraries that implement some variation of it.
let userListViewModel: UserListViewModel = { input, loadUsers in
enum Action {
case reset([User])
case delete(at: Int)
}
let resetUsers = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.compactMap { $0.element }
.map { Action.reset($0) }
let delete = input.deleteAction.map { Action.delete(at: $0) }
return Observable.merge(resetUsers, delete)
.scan(into: [User](), accumulator: { users, action in
switch action {
case let .reset(newUsers):
users = newUsers
case let .delete(index):
users.remove(at: index)
}
})
.asDriver(onErrorJustReturn: [])
}
I am fairly new with using ReactiveSwift and ReactiveCocoa and I seem to have hit a road block regarding the proper way of initializing a Property that has a dependencies.
For instance in the following code, I try to initialize a Property but i get a compiler error which is expected. My question is how/ what is the "correct" way to do this.
class SomeViewModel {
// illustration purposes, in reality the property (dependency) we will observe will change over time
let dependency = Property(value: true)
let dependency2 = Property(value: "dependency2")
let dependency3 = Property(value: 12345)
let weightLabel: Property<String>
// private(set) var weightLabel: Property<String>!
// using private(set) var weightLabel: Property<String>! works,
// however this changes the meaning behind using let, because we could
// reinitalize weightLabel again which is not similar to using a let so not a good alternative
// let weightLabel: Property<String> = Property(value: "")
// another solution that will work but will result in a wrong value
// upon initalization then, changed into the "correct value" thus, i
// am discrading this as well
init() {
weightLabel = dependency.map {
// compiler error, 'self' captured by closure before all members were initalized.
// My question is if there is a way to handle this scenario properly
if $0 && self.dependency2.value == "dependency2" && self.dependency3.value == 12345 {
return ""
}
return ""
}
}
}
So as you might have noticed above in the comments I am wondering if there is a way to handle this scenario with ReactiveSwift other then the ones i mentioned above that are not really ideal solutions.
The instrument that fits the scenario is combineLatest, which provides a combined version of all these properties (streams) whenever any of them has been updated.
weightLabel = Property.combineLatest(dependency, dependency2, dependency3)
.map { d1, d2, d3 in
return "Hello World! \(d1) \(d2) \(d3)"
}
Regarding the compiler error, the issue is that you are capturing/referring to self in a closure before every stored property has been initialised. Depending on the intention, you may use a capture list to capture directly the values and objects you are interested w/o self.
let title: String
let action: () -> Void
init() {
title = "Hello World!"
// 🚫 `action` has not been initialised when `self` is
// being captured.
action = { print(self.title) }
// ✅ Capture `title` directly. Now the compiler is happy.
action = { [title] in print(title) }
}
I have a Models class defined like this:
class BaseModel: Object {
var data: JSON = JSON.null
convenience init(_ data: JSON) {
self.init()
self.data = data
}
override static func ignoredProperties() -> [String] {
return ["data"]
}
}
class RecipeModel: BaseModel {
dynamic var title: String {
get { return data["fields"]["title"].stringValue }
set { self.title = newValue }
}
... more vars ...
var ingredients: List<IngredientsModel> {
get {
let ingredients = List<IngredientsModel>()
for item in data["fields"]["ingredients"] {
ingredients.append(IngredientsModel(item.1))
}
return ingredients
}
set { self.ingredients = newValue }
}
}
class IngredientsModel: BaseModel {
dynamic var text: String {
get { return data["text"].stringValue }
set { self.text = newValue }
}
... more vars ...
}
And I would like to use it something like this:
Api.shared.fetchAllEntries().call(onSuccess: {response in
print(response.json)
let realm = try! Realm()
try! realm.write {
realm.deleteAll()
}
for item in response.json["items"].arrayValue {
let recipe = RecipeModel(item)
try! realm.write {
realm.add(recipe)
}
}
}, onError: {
print("error")
})
So basically the idea is to just pass the whole JSON to the initial RecipeModel class, and it should parse it out and create the objects I need in the Realm database. It works quite well except for the nested list of IngredientsModel. They do not get added to the realm database.
What I see as a potential problem is that I call self.init() before I call self.data in the convenience init, but I do not see any way to work around this. Do you guys please know how I could achieve that also the IngredientsModel would have its contents set up properly and I would have a list of ingredients in the RecipeModel?
Your current implementation doesn't work, because you are not calling the getter/setter of ingredients in the init method of RecipeModel and hence the IngredientsModel instances are never persisted in Realm.
Moreover, using a computed property as a one-to-many relationship (Realm List) is a really bad idea, especially if you are parsing the results inside the getter for this property. Every time you call the getter of ingredients, you create new model objects instead of just accessing the existing ones that are already stored in Realm, but you are never deleting the old ones. If you were actually saving the IngredientsModel instances to Realm (which you don't do at the moment as mentioned above) you would see that your database is full of duplicate entries.
Your whole approach seems really suboptimal. You shouldn't store the unparsed data object in your model class and use computed properties to parse it. You should parse it when initializing your models and shouldn't store the unparsed data at all. You can use the ObjectMapper library for creating Realm objects straight away from the JSON response.
Here is an issue I have with this case of code:
let entityTypeDico = ["entityTypeOne": arrayOne, "entityTypeOneTwo": arrayTwo,
"entityTypeThree": arrayThree, "entityTypeFour": arrayFour];
for (entityName, arrayItem) in entityTypeDico {
for x in arrayItem {
/*do something with x and arrayItem
I need to use the class of entityName */
//To get it I tried to use some code like:
NSClassFromString(entityName) // But it does not work.
}
}
entityTypeOne, ..... entityTypeFour are names of CoreData entities,
arrayOne, ..... arrayFour are some kind of arrays (not so important for the question).
What exact code do I need to use to get hold of the type of each entity inside the loop?
Your second for loop doesnt make sense. Can you try?
for (entityName, arrayItem) in entityTypeDico {
NSClassFromString("YourProjectName.\(entityName)")
}
Edit 1
Currently I am working on a project where I used NSClassFromString. And that is how I used it in my code.
In my main class I use:
let carClass = NSClassFromString("MyProjectName.\(self.selectedBrand)\(self.selectedModel)") as! NSObject.Type
_ = carClass.init()
where selectedBrand and selectedModel are picked by the user.
In the car class, I have an init function
override init() {
super.init()
// My codes here like initializing webview
}
Hope it helps
Here what I ended up by finding after a number of experiments.
It works and does what I want. I hope it will be useful to some other people too.
let entityTypeDico = ["entityTypeOne": arrayOne, "entityTypeOneTwo": arrayTwo,
"entityTypeThree": arrayThree, "entityTypeFour": arrayFour];
// "entityTypeXYZ" is a class name (subclass of NSManagedObject)
// arrayXYZ is some array (not so important for the question)
for (entityName, arrayItem) in entityTypeDico {
for x in arrayItem {
/*do something with x and arrayItem
I need to use the class of entityName */
//To get it I tried to use some code like:
NSClassFromString(entityName) // But it did not work.
// Here is what finally worked for me:
((NSClassFromString("\(entityName)")) as! NSManagedObject.Type)
}
}