This is an example of our use case:
We have a selectedIndex and a list of items.
class FoosViewModel {
let selectedIndex = Variable<Int>(0)
let items: [Foo] = ... // assume that this is initialized properly
}
In reality, we often care about which item is selected instead of the index of the selected item. So we'll have code like this:
selectedIndex.asObservable().subscribe(onNext: { [weak self] index in
guard let self = self else { return }
let selectedItem = items[index]
// Do sth with `selectedItem` here
}
Notice that the value of selectedItem is always driven by selectedIndex. Therefore, we change the code to the following:
class FoosViewModel {
let selectedIndex = Variable<Int>(0)
let selectedItem = Variable<Int>(items[0])
let items: [Foo] = ... // assume that this is initialized properly
init() {
selectedIndex.asObservable().subscribe(onNext: { [weak self] index in
guard let self = self else { return }
self.selectedItem = items[index]
}
}
}
This seems to be a common enough use case. Do we have an existing operator in Rx that can map a Variable to another? Is there sth like this:
class FoosViewModel {
let selectedIndex = Variable<Int>(0)
let selectedItem = selectedIndex.map{ items[$0] }
let items: [Foo] = ... // assume that this is initialized properly
}
What you have done is created two bits of state that are dependent on each other. It would be better to just have one source of truth and a derivative which means that one should be implemented differently than the other. Assuming that selectedIndex is the source of truth, then I would expect to see:
class FoosViewModel {
let selectedIndex = Variable<Int>(0)
let selectedItem: Observable<Foo?>
let items: [Foo]
init(items: [Foo]) {
selectedItem = selectedIndex.asObservable().map { index in
index < items.count ? items[$0] : nil
}
self.items = items
}
}
Unlike in your attempt, there is no temptation for a user of this class to try to assign a new value to selectedItem (in fact, the code won't even compile if you try.) As a side benefit, there is no need to do the "weak self dance" either since the map doesn't refer to self at all. All of this works because you made items a let rather than a var (good for you!)
If you wanted to be able to add/remove items then things get a bit more complex...
class MutableFooViewModel {
let selectedIndex = Variable<Int>(0)
let selectedItem: Observable<Foo?>
let items = Variable<[Foo]>([])
init(items: [Foo]) {
items.value = items
let _items = self.items // this is done to avoid reference to `self` in the below.
selectedItem = Observable.combineLatest(
_items.asObservable(),
selectedIndex.asObservable()
) { items, index in
index < items.count ? items[index] : nil
}
}
}
The idea here is that Subjects (Variable is a kind of subject) should not be the first thing you think of when making an Observable that depends on some other observable. In this respect, they are only good for creating the initial observable. (RxCocoa is full of them.)
Oh and by the way, Variable had been deprecated.
Related
I have the following code in RxSwift 4.0-based project:
private var _myRelay = PublishRelay<MyData>()
var myRelay: Observable<MyData> {
return _myRelay.asObservable()
}
Now I need to keep the last value (if any) in _myRelay, so I decided to convert it to BehaviorRelay:
private var _myRelay = BehaviorRelay<MyData?>(value: nil)
I want to keep public interface intact
var myRelay: Observable<MyData>
I mean I do not want to convert it to
var myRelay: Observable<MyData?>
The idea is not to "publish" initial value == nil to subscribers of myRelay, and start publishing only after some data appears. How can I do it? I am completely beginner in Rx, but I am sure there should be some elegant solution.
The simplest solution is to filter out the nils. If you were using 5.0 then you would do that with compactMap but since you said 4.0 that means you will need a filter and map:
var myRelay: Observable<MyData> {
return _myRelay.filter { $0 != nil }.map { $0! }.asObservable()
}
but you might actually be better off using a ReplaySubject instead of a Relay.
private let _myRelay = ReplaySubject<MyData>.create(bufferSize: 1)
var myRelay: Observable<MyData> {
return _myRelay.asObservable()
}
That way, you don't have to deal with nils at all. (Also note, _myRelay should be a let not a var.)
Doing the above will also allow you to emit a completed event when the observable goes out of scope (a relay doesn't allow that.)
private let _myRelay = ReplaySubject<MyData>.create(bufferSize: 1)
var myRelay: Observable<MyData> {
return _myRelay.asObservable()
}
deinit {
_myRelay.onCompleted()
}
I have a UICollectionView bound to an array of entities using BehaviorSubject and all is fine, data is loaded from the network and displayed correctly.
The problem is, based on user action, I'd like to change the CellType used by the UICollectionView and force the collection to re-create all cells, how do I do that?
My bind code looks like:
self.dataSource.bind(to: self.collectionView!.rx.items) {
view, row, data in
let indexPath = IndexPath(row: row, section: 0)
var ret: UICollectionViewCell? = nil
if (self.currentReuseIdentifier == reuseIdentifierA) {
// Dequeue cell type A and bind it to model
ret = cell
} else {
// Dequeue cell type B and bind it to model
ret = cell
}
return ret!
}.disposed(by: disposeBag)
The general way to solve problems in Rx is to think of what you want the output effect to be and what input effects can affect it.
In your case, the output effect is the display of the table view. You have identified two input effects "data is loaded from the network" and "user action". In order to make your observable chain work properly, you will have to combine your two input effects in some way to get the behavior you want. I can't say how that combination should take place without more information, but here is an article explaining most of the combining operators available: https://medium.com/#danielt1263/recipes-for-combining-observables-in-rxswift-ec4f8157265f
As a workaround, you can emit an empty list then an actual data to force the collectionView to reload like so:
dataSource.onNext([])
dataSource.onNext([1,2,3])
I think you can use different data type to create cell
import Foundation
import RxDataSources
enum SettingsSection {
case setting(title: String, items: [SettingsSectionItem])
}
enum SettingsSectionItem {
case bannerItem(viewModel: SettingSwitchCellViewModel)
case nightModeItem(viewModel: SettingSwitchCellViewModel)
case themeItem(viewModel: SettingCellViewModel)
case languageItem(viewModel: SettingCellViewModel)
case contactsItem(viewModel: SettingCellViewModel)
case removeCacheItem(viewModel: SettingCellViewModel)
case acknowledgementsItem(viewModel: SettingCellViewModel)
case whatsNewItem(viewModel: SettingCellViewModel)
case logoutItem(viewModel: SettingCellViewModel)
}
extension SettingsSection: SectionModelType {
typealias Item = SettingsSectionItem
var title: String {
switch self {
case .setting(let title, _): return title
}
}
var items: [SettingsSectionItem] {
switch self {
case .setting(_, let items): return items.map {$0}
}
}
init(original: SettingsSection, items: [Item]) {
switch original {
case .setting(let title, let items): self = .setting(title: title, items: items)
}
}
}
let dataSource = RxTableViewSectionedReloadDataSource<SettingsSection>(configureCell: { dataSource, tableView, indexPath, item in
switch item {
case .bannerItem(let viewModel),
.nightModeItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: switchReuseIdentifier, for: indexPath) as? SettingSwitchCell)!
cell.bind(to: viewModel)
return cell
case .themeItem(let viewModel),
.languageItem(let viewModel),
.contactsItem(let viewModel),
.removeCacheItem(let viewModel),
.acknowledgementsItem(let viewModel),
.whatsNewItem(let viewModel),
.logoutItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? SettingCell)!
cell.bind(to: viewModel)
return cell
}
}, titleForHeaderInSection: { dataSource, index in
let section = dataSource[index]
return section.title
})
output.items.asObservable()
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
RxDataSources
swiftHub
I've got a static filter to turn on/off for an NSArrayController based on whether or not a checkbox is checked. Right now I've bound the checkbox value to this:
private dynamic var filterPending: NSNumber! {
willSet {
willChangeValueForKey("filterPredicate")
}
didSet {
didChangeValueForKey("filterPredicate")
}
}
and then I bound the filter of the NSArrayController to this:
private dynamic var filterPredicate: NSPredicate? {
guard let filter = filterPending?.boolValue where filter == true else { return nil }
return NSPredicate(format: "pending > 0")
}
That seems to work properly, but feels like maybe I'm missing some easier way of doing this?
In your set-up the value of filterPredicate depends on the value of filterPending. As Gerd K points out, the Key-Value Observing API allows you to specify this type of relationship by registering filterPending as a dependent key of filterPredicate:
// MyFile.swift
class func keyPathsForValuesAffectingFilterPredicate() -> Set<NSObject> {
return Set<NSObject>(arrayLiteral: "filterPending")
}
private dynamic var filterPending: NSNumber!
private dynamic var filterPredicate: NSPredicate? {
guard let filter = filterPending?.boolValue where filter == true else { return nil }
return NSPredicate(format: "pending > 0")
}
I'm trying to have variables in swift that are critical app-wide user settings so they must be persisted to disk after every change. There is a small amount of these variables and I'm content with the first read happening from disk after the app starts.
I have code that looks similar to this:
var _myEnumMember:MyEnum?
var myEnumMember:MyEnum {
get {
if let value = _myEnumMember { // in memory
return value
}
var c:Cache = Cache()
var storedValue:MyEnum? = c.get("SomeStorageKey");
if let value = storedValue { // exists on disk
self.myEnumMember = value // call setter to persist
return self.myEnumMember // call getter again with value set
}
self.myEnumMember = .DefaultValue // assign via setter
return self.rankingDuration // call getter after `set`
}
set (newValue){
self._myEnumMember = newValue // assign to memory
var c:Cache = Cache()
c.put("SomeStorageKey", data: ser) // store in disk
}
I have about 5-6 properties that need to do this - I don't want to repeat myself over and over - is there any way to DRY this code up so I won't have to repeat this logic in several places?
(Note: Asking here and not CR.SE because I'd like answers to explain how to DRY getters/setters in these situations rather than receive critique on a particular piece of code)
I was working on something similar recently - this may be your best bet. I used this as a nested struct, but it doesn't strictly need to be nested.
First, define a LocalCache type that will handle the persistence of your properties:
struct LocalCache {
// set up keys as constants
// these could live in your class instead
static let EnumKey = "EnumKey"
static let IntKey = "IntKey"
static let StringKey = "StringKey"
// use a single cache variable, hopefully?
var cache = Cache()
// in-memory values go in a Dictionary
var localValues: [String: Any] = [:]
// fetch from local storage or from disk
// note that the default value also sets the return type
mutating func fetch<T>(key: String, defaultValue: T) -> T {
if localValues[key] == nil {
localValues[key] = cache.get(key) ?? defaultValue
}
return localValues[key]! as T
}
// save in both local storage and to disk
mutating func store(key: String, _ value: Any) {
localValues[key] = value
cache.put(key, data: value)
}
}
Then add a LocalCache instance to your class, and you can have much simpler getter/setters.
class Test {
// instance of the local cache
var localCache = LocalCache()
var enumPropery: MyEnum {
get { return localCache.fetch(LocalCache.EnumKey, defaultValue: MyEnum.DefaultValue) }
set { localCache.store(LocalCache.EnumKey, newValue) }
}
var intProperty: Int {
get { return localCache.fetch(LocalCache.IntKey, defaultValue: 0) }
set { localCache.store(LocalCache.IntKey, newValue) }
}
var stringProperty: String {
get { return localCache.fetch(LocalCache.StringKey, defaultValue: "---") }
set { localCache.store(LocalCache.StringKey, newValue) }
}
}
If you're using swift in an iOS or OS X context then NSUserDefaults are ABSOLUTELY the right way to do this.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/index.html
http://www.codingexplorer.com/nsuserdefaults-a-swift-introduction/
I have a class in Swift called Button:
enum ButtonBackgroundColor {
case None
case Yellow
}
class Button: UIButton {
var backgroundColorType: ButtonBackgroundColor?
}
And then I'm setting a runtime attribute:
However, I get a non-descriptive crash. The stack trace mentions something about key value coding.
Runtime attributes do work however for non enum types, like Int or Float:
// runtime attributes work for this
var backgroundColorType: Int = 0
Is there something wrong with the way I'm declaring my enum?
Given that your enum ButtonBackgroundColor includes a .None case, why not avoid declaring backgroundColorType in Button as an optional? Doing so needlessly complicates the declaration. Prefer
class Button: UIButton {
var backgroundColorType = ButtonBackgroundColor.None
}
Besides being more idiomatically correct, the above might, just might, fix your 'user defined attribute' issue now that the variable is not an optional.
You also might need to provide raw values in your enum. Like:
enum ButtonBackgroundColor : Int {
case None = 0
case Yellow = 1
}
if you expect the runtime attribute value of '0' to perhaps map to .None
#GoZoner's answer didn't work for me. Tried changing the raw type to String and that didn't work as well. My solution is to define another variable (of whichever type supported by the runtime attributes) and then override the didSet of that variable to set the enum.
So in code:
var IBButtonBackgroundColor = "None"
{
didSet
{
switch self.IBButtonBackgroundColor
{
case "None":
self.backgroundColorType = .None
break
case "Yellow":
self.backgroundColorType = .Yellow
break
default:
self.backgroundColorType = .None
break
}
}
}
var backgroundColorType:ButtonBackgroundColor = .None
I don't like having to repeat things in the switch/case, but I can't think of any better solution for now.