How do I acquire the value of an observable boolean? - rx-swift

I've created an 'observable' boolean variable that is to be bound (via .bind) to a UISwitch. (The traditional imperative model would be easier; but I'm trying to learn to nuances of rxCocoa)
I'm not sure what to do here; I'm basing my logic on some sample code working with Strings.
I used 'just' because I'm only interested in the one variable's toggled value.
As you can see, the closure parameter is too vague.
What am I missing?

Try this:
var IOButton = Variable(false)
var isOn: Observable<Bool> = IOButton.asObservable()
Then, with your UISwitch:
isOn
.bind(to: switch.rx.isOn )
.disposed(by: disposeBag)
UPDATE 1:
Now you can subscribe to isOn
isOn
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

I believe I came up with the solution:
// On/Off Switch
let onOffSwitch = Variable(true)
onOffSwitch.asObservable()
.subscribe (onNext: { switchValue in
print("This is new SwitchValue: \(switchValue)")
}).disposed(by: disposeBag)
aSwitch.rx.isOn.bind(to: onOffSwitch)
.disposed(by: disposeBag)
'aSwitch' is the UISwitch.
When I toggle the switch, I get the following:
This is new SwitchValue: true
This is new SwitchValue: false
This is new SwitchValue: true
This is new SwitchValue: false
From this paradigm I can insert a self.func() within the closure instead of the print() so I can do stuff per switch; versus the familiar #IBAction.

Related

Observable.CombineLatest bind and subscribe RxSwift

I am working on an app that uses an API that have some inconsistencies, I have achieved a result with these 2 observables that perform some shared actions but the first one 'servers' is an array that binds to the UITableView.
serversViewModel.servers
.asObservable()
.observeOn(MainScheduler.instance)
.bind(to: serversTableView.rx.items(cellIdentifier: ServersTableViewCell.identifier, cellType: ServersTableViewCell.self)) { [weak self] (row, element, cell) in
guard let strongSelf = self else { return }
cell.serverProxy.accept(element)
if let currentServer = strongSelf.serversViewModel.currentServer.value,
element == currentServer,
let index = strongSelf.serversViewModel.servers.value.firstIndex(where: { $0 == currentServer }){
strongSelf.serversTableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .top)
}
}
.disposed(by: disposeBag)
serversViewModel.currentServer
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] (server) in
guard let strongSelf = self else { return }
if let server = server, let index = strongSelf.serversViewModel.servers.value.firstIndex(where: { $0 == server }){
strongSelf.serversTableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .top)
}
else{
strongSelf.serversTableView.deselectAllItems(animated: false)
}
})
.disposed(by: disposeBag)
Is it possible to create a combined observable for both and use it for binding the UITableView?
Thank you
You want to use combineLatest. Note that most of this should actually be in your view model...
In the below code, the servers constant is a tuple of both the array of Server objects that should be displayed and the index path of the current server. Whenever either emits a new value, servers will emit a value.
You might find the following article helpful in the future: Recipes for Combining Observables in RxSwift
let servers = Observable.combineLatest(serversViewModel.servers, serversViewModel.currentServer) { (servers, server) -> ([Server], IndexPath?) in
let indexPath = server.flatMap { servers.firstIndex(of: $0) }
.map { IndexPath(row: $0, section: 0) }
return (servers, indexPath)
}
servers
.map { $0.0 }
.bind(to: serversTableView.rx.items(cellIdentifier: ServersTableViewCell.identifier, cellType: ServersTableViewCell.self)) { (row, element, cell) in
cell.serverProxy.accept(element)
}
.disposed(by: disposeBag)
servers
.map { $0.1 }
.bind(onNext: { [serversTableView] indexPath in
if let indexPath = indexPath {
serversTableView.selectRow(at: indexPath, animated: true, scrollPosition: .top)
}
else {
serversTableView.deselectAllItems(animated: false)
}
})
.disposed(by: disposeBag)
I'd approach from a slightly different way. First I would consider pulling the combine observable back into the ViewModel you have already structured. No need for this composition to be in your ViewController.
Then output that composed signal to bind into your rx.items.
You can wrap your objects will a table cell view model to control whether to show them in a 'selected state'
Then also output the currentServer from your viewModel to simply scroll to it.
There are several ways to combine observables in RxSwift. For your specific case, you will have to choose one that suits your needs best. Some of the operators are:
combineLatest
zip
merge
etc.
Read this documentation to get more idea about what each one it does.

RxSwift - Button Tap using Publish Subject

So I have a button inside my ViewController which is connected to ViewModel and than whenever the button is tapped, in my coordinator I navigate to another screen. The code is like this:
VC
btnShowShopsMap.rx.tap
.bind(to: viewModel.selectShowMap)
VM
let selectShowMap: AnyObserver<Void>
let showShopMap: Observable<Void>
//Inside init
let _selectShowMap = PublishSubject<Void>()
selectShowMap = _selectShowMap.asObserver()
showShopMap = _selectShowMap.asObservable()
Coordinator
viewModel.showShopMap
.subscribe(onNext: { _ in self.showShopMap()})
.disposed(by: userShopVC.disposeBag)
Is it possible to refactor above code? rather than using PublishSubject is there any other way to do what i am doing using PublishSubject
My VC, VM & Coordinator Flow
Coordinator
func showLoginScreen(logout: Bool = false) {
guard let viewController = LoginViewController.instantiate(storyboard: .main) else { return }
viewController.viewModelFactory = { inputs in
let viewModel = LoginViewModel(inputs: inputs)
viewModel.showHome
.subscribe(onNext: { isLogged in
if isLogged {
self.showHomeScreen()
}
})
.disposed(by: viewController.disposeBag)
inputs.showOnboarding
.subscribe(onNext: { _ in
self.showOnboardingScreen()
})
.disposed(by: viewController.disposeBag)
return viewModel
}
navController.pushViewController(viewController, animated: true)
VC
var viewModelFactory: (LoginViewModel.UIInputs) -> LoginViewModel = { _ in fatalError("factory not set")}
let inputs = LoginViewModel.UIInputs(userNumber: txtUserNumber.rx.text.orEmpty.asDriver(),
password: txtPassword.rx.text.orEmpty.asDriver(),
loginTapped: btnLogin.rx.tap.asSignal(),
userNumberLostFocus: txtUserNumber.rx.controlEvent(.editingDidEnd).asSignal(),
passwordLostFocus: txtPassword.rx.controlEvent(.editingDidEnd).asSignal(),
indicator: indicator,
showOnboarding: btnShowOnboarding.rx.tap.asObservable())
VM
struct UIInputs {
let userNumber: Driver<String>
let password: Driver<String>
let loginTapped: Signal<Void>
let userNumberLostFocus: Signal<Void>
let passwordLostFocus: Signal<Void>
let indicator: ActivityIndicator
let showOnboarding: Observable<Void>
}
init(inputs: UIInputs) {}
Assuming the view controller owns and instantiates the view model, you could pass the tap control event as an observable to the view model initializer, which then exposes it as an observable for the coordinator to subscribe to:
// VC:
let viewModel = ViewModel(..., showShopMap: btnShowShopMap.rx.tap.asObservable())
// VM:
let showShopMap: Observable<Void>
init(..., showShopMap: Observable<Void>) {
self.showShopMap = showShopMap
}
I try not to use subjects whenever possible and instead just expose transformed observables that were passed in.
I found very easy and simple way to solve my issue and avoid using Subject, As there was no logic related to my button in VM, I don't need pass my Button tap to my VM either by using Observable or using Subject. Instead I directly accessed my button in my Coordinator like this:
viewController.btnShowOnboarding.rx.tap
.subscribe(onNext: { _ in
self.showOnboardingScreen()
})
.disposed(by: viewController.disposeBag)

Is there a way to force reloadData on a UICollectionView using RxSwift?

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

didChangeAutomaticCapitalizationNotification not triggered

What am I doing wrong? I don't get this notification. I have this function:
#objc func onAutocorrection (_ notification: Foundation.Notification) {
Swift.print("\(notification)")
}
later in the same class I do use it as follows:
NotificationCenter.default.addObserver(
self,
selector: #selector(onAutocorrection(_:)),
name: NSSpellChecker.didChangeAutomaticCapitalizationNotification,
object: nil)
The addObserver is executed, but the function is never called even when the application is capitalising in an NSTextView.
Why? Many thanks in advance!
It looks like I misunderstood the notification. It is not meant to be triggered when automatic capitalisation happens but when the systems preference of your Mac is changing.
See the comment of ever helpful Willeke and see Notification of autocorrect
In order to get to the intended result of reacting to autocapitalisation did I implement this function in the NSTextViewDelegate:
public func textView(_ view: NSTextView, didCheckTextIn range: NSRange, types checkingTypes: NSTextCheckingTypes, options: [NSSpellChecker.OptionKey : Any] = [:], results: [NSTextCheckingResult], orthography: NSOrthography, wordCount: Int) -> [NSTextCheckingResult] {
if !range.contains(0){
return results
}
var newResult = [NSTextCheckingResult]()
for result in results {
if let textToChange = view.string[range].components(separatedBy: " ").first, let replacement = result.replacementString?.components(separatedBy: " ").first {
let firstLetterCap = textToChange.capitalizingFirstLetter()
if replacement == firstLetterCap {
continue //don't add to results
}
}
newResult.append(result)
}
return newResult
}
This function will prevent that the first character will be capitalised.
Ultimately, I check whether the capitalised version of the first word of the range that must include position "0" is equal to the first word of the replacement string. And if it is then I remove that result/suggestion from the result list.

PromiseKit 3.0: chaining with loops

I'm using promisekit 3.0 to help chain alamofire callbacks in a clean way. The objective is to start with a network call, with a promise to return an array of urls.
Then, I'm looking to execute network calls on as many of those urls as needed to find the next link i'm looking for. As soon as this link is found, I can pass it to the next step.
This part is where I'm stuck.
I can pick an arbitrary index in the array that I know has what I want, but I can't figure out the looping to keep it going until the right information is returned.
I tried learning from this obj-c example, but i couldn't get it working in swift.
https://stackoverflow.com/a/30693077/1079379
He's a more tangible example of what i've done.
Network.sharedInstance.makeFirstPromise(.GET, url: NSURL(string: fullSourceLink)! )
.then { (idArray) -> Promise<AnyObject> in
let ids = idArray as! [String]
//how do i do that in swift? (from the example SO answer)
//PMKPromise *p = [PMKPromise promiseWithValue: nil]; // create empty promise
//only thing i could do was feed it the first value
var p:Promise<AnyObject> = Network.sharedInstance.makePromiseRequestHostLink(.POST, id: ids[0])
//var to hold my eventual promise value, doesn't really work unless i set it to something first
var goodValue:Promise<AnyObject>
for item in ids {
//use continue to offset the promise from before the loop started
continue
//hard part
p = p.then{ returnValue -> Promise<AnyObject> in
//need a way to check if what i get is what i wanted then we can break the loop and move on
if returnValue = "whatIwant" {
goodvalue = returnValue
break
//or else we try again with the next on the list
}else {
return Network.sharedInstance.makeLoopingPromise(.POST, id: item)
}
}
}
return goodValue
}.then { (finalLink) -> Void in
//do stuck with finalLink
}
Can someone show me how to structure this properly, please?
Is nesting promises like that anti-pattern to avoid? In that case, what is the best approach.
I have finally figured this out with a combination of your post and the link you posted. It works, but I'll be glad if anyone has input on a proper solution.
func download(arrayOfObjects: [Object]) -> Promise<AnyObject> {
// This stopped the compiler from complaining
var promise : Promise<AnyObject> = Promise<AnyObject>("emptyPromise")
for object in arrayOfObjects {
promise = promise.then { _ in
return Promise { fulfill, reject in
Service.getData(stuff: object.stuff completion: { success, data in
if success {
print("Got the data")
}
fulfill(successful)
})
}
}
}
return promise
}
The only thing I'm not doing is showing in this example is retaining the received data, but I'm assuming you can do that with the results array you have now.
The key to figuring out my particular issue was using the "when" function. It keeps going until all the calls you inputted are finished. The map makes it easier to look at (and think about in my head)
}.then { (idArray) -> Void in
when(idArray.map({Network.sharedInstance.makePromiseRequest(.POST, params: ["thing":$0])})).then{ link -> Promise<String> in
return Promise { fulfill, reject in
let stringLink:[String] = link as! [String]
for entry in stringLink {
if entry != "" {
fulfill(entry)
break
}
}
}
}.then {
}
}

Resources