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

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

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.

Chain propagation of a RxSwift Variable to another Variable

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.

NSOutlineView - Get NSTableCellView for a Given Item

My view-based outline view displays a custom context menu (right-click menu) for its rows.
One of the menu items on the menu is "Rename...", and the menu item's representedObject property is set to the object represented by the outline view row:
let menu = NSMenu()
// ...other menu items...
let renameItem = NSMenuItem(
title: "Rename...",
action: #selector(OutlineViewController.rename(_:)),
keyEquivalent: "")
renameItem.representedObject = object
menu.addItem(renameItem)
On the action side, I want to make the text field in the table cell editable, programmatically. The problem is, I am not sure of how to get a reference to the table cell from the represented object alone.
This is my action method:
#IBAction func rename(_ sender: Any) {
guard let menuItem = sender as? NSMenuItem else { return }
guard let item = menuItem.representedObject else { return }
I can get the row for the represented object (Int):
let row = outlineView.row(forItem: item)
...and the row view (NSTableRowView):
let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false)
I can get the column index (Int) and column (NSTableColumn):
let columnIndex = outlineView.column(withIdentifier: "TitleColumn")
let column = outlineView.tableColumns[columnIndex]
...and attempt to get the cell view (NSTableCellView):
guard let cell = outlineView(outlineView, viewFor: column, item: item) as? NSTableCellView else {
return
}
Finally, I try to make the text field editable:
guard let textField = cell.textField else {
return
}
textField.becomeFirstResponder()
All these test pass (I set breakpoints), but nothing happens: The text field does not become editable (unlike when double clicking).
What am I doing wrong?
Edit I have realized that when I call:
outlineView(outlineView, viewFor: column, item: item)
This is basically the NSOutlineViewDelegate method that my view controller is implementing, so instead of giving me the cell already on screen, it is creating a new copy on demand. Kind of like calling
UITableViewController.tableView(_:,cellForRowAt:)
// Data source method, creates or dequeues a new cell to pass
// back to the table for display
...instead of calling:
UITableView.cellForRowAt(_:)
// Method of table view proper; returns existing cell if
// already on screen, or defers to the data source (method
// above) otherwise
So instead of calling a delegate method that creates the cell view anew, I should query the outline view itself. However, the only meaningful method I can see is:
func rowView(atRow row: Int, makeIfNecessary: Bool) -> NSTableRowView?
...which returns an NSTableRowView...
Solved it. Just had to dig a little deeper into the API:
// Get row index
let row = outlineView.row(forItem: item)
// Get view for the whole row:
guard let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false) else {
return
}
// THIS is the missing piece: Get row subview for the given column
// (= cell)
guard let cell = rowView?.view(atColumn: 0) as? NSTableCellView else {
return
}
Now I have a reference to the actual text field already on screen, and it becomes editable:
self.view.window?.makeFirstResponder(cell.textField)

NSTextFields using bindings not updating after initialization

I am importing data from JSON to be used as the models for some items in a CollectionView, and it seems that they are being initialized, and with the correct number of elements. But for some reason the representedObject (aliased as morpheme below) is returning nil initially. Hence the placeholder if nil values being the ones showing up.
If you click the items, I have it set up to show up in the log the name of the item clicked, and it works fine, and doesn't return the debugging defaults. So I'm guessing there is a concurrency issue going on.
For more details, this are items being manually prototyped because XCode 7 still hasn't fixed the segue bug with collection item prototypes.
Here is a screenshot I hopefully managed to get it all the important info in:
Here is the cell's controller/delagating class code in detail:
///Acts as view controller for the items of the morpheme collection
public class MorphemeCell: NSCollectionViewItem, NSTextViewDelegate{
var backgroundColor = NSColor.clearColor()
var morphemeLabel: String{
get{
return morpheme?.morphemeDisplayName ?? "Morpheme"
}
}
var allomorphsLabel: String{
get{
return (morpheme?.allomorphsAsString ?? "Allomorphs")
}
}
///The morpheme data contained in the cell
public var morpheme : Morpheme?{
get{
return representedObject as? Morpheme
}
set{
representedObject = newValue
}
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
}
///Detects clicks on each item
public override func mouseUp(theEvent: NSEvent) {
Swift.print("Clicked on " + morphemeLabel)
backgroundColor = NSColor.blueColor()
}
}
Not sure if this is needed, but just in case here is the main window's ViewController doing some setup/loading functionality.
The loading code itself:
///Loads morpheme data into memory and then into the collection view
func loadMorphemeData(){
//Open morphemes.json and begin parsing
let morphemeDataPath = "morphemes"
if let file = NSBundle.mainBundle().URLForResource(morphemeDataPath, withExtension: "json")
{
let data = NSData(contentsOfURL: file)
let json = JSON(data:data!)
//Create Morpheme objects passing in JSON elements
for morphemeElement in json{
let toAdd = Morpheme(JSONElement: morphemeElement)
fullMorphemesList.append(toAdd)
}
///TODO Use full range or filters in final product
let morphemesToLoad = fullMorphemesList[0...100]
collectionView.content.appendContentsOf(Array(morphemesToLoad) as [AnyObject])
}
else
{
print("Resource Failure")
}
So, recap: It seems that I either need to delay the collectionView's setup, or find out how to update the Labels once the data is in.
Thanks very much for any help! I'm very new to the Cocoa framework so it's been a doozy.
Take different route: bind your label to self.morpheme.morphemeDisplayName. Then set "Null Placeholder" text to be "Morpheme" (in that right panel see the list of text edits below binding settings). Finally make property morphemeDisplayName dynamic:
dynamic var morphemeDisplayName: String?
Obviously, you dont need morphemeLabel property inside cell anymore.
morpheme property of cell must be dynamic as well, or if there is setter-based property, you can call:
set {
willChangeValueForKey("morpheme")
<whatever variable> = newValue
didChangeValueForKey("morpheme")
}
Edit by original poster:
Also, in order to avoid binding synchrony issues, it turns out using viewWillAppear() instead of viewDidLoad() was causing issues with data loading and "freezing the labels".

Type casting in for-in loop

I have this for-in loop:
for button in view.subviews {
}
Now I want button to be cast into a custom class so I can use its properties.
I tried this: for button in view.subviews as AClass
But it doesnt work and gives me an error:'AClass' does not conform to protocol 'SequenceType'
And I tried this: for button:AClass in view.subviews
But neither does that work.
For Swift 2 and later:
Swift 2 adds case patterns to for loops, which makes it even easier and safer to type cast in a for loop:
for case let button as AClass in view.subviews {
// do something with button
}
Why is this better than what you could do in Swift 1.2 and earlier? Because case patterns allow you to pick your specific type out of the collection. It only matches the type you are looking for, so if your array contains a mixture, you can operate on only a specific type.
For example:
let array: [Any] = [1, 1.2, "Hello", true, [1, 2, 3], "World!"]
for case let str as String in array {
print(str)
}
Output:
Hello
World!
For Swift 1.2:
In this case, you are casting view.subviews and not button, so you need to downcast it to the array of the type you want:
for button in view.subviews as! [AClass] {
// do something with button
}
Note: If the underlying array type is not [AClass], this will crash. That is what the ! on as! is telling you. If you're not sure about the type you can use a conditional cast as? along with optional binding if let:
if let subviews = view.subviews as? [AClass] {
// If we get here, then subviews is of type [AClass]
for button in subviews {
// do something with button
}
}
For Swift 1.1 and earlier:
for button in view.subviews as [AClass] {
// do something with button
}
Note: This also will crash if the subviews aren't all of type AClass. The safe method listed above also works with earlier versions of Swift.
This option is more secure:
for case let button as AClass in view.subviews {
}
or swifty way:
view.subviews
.compactMap { $0 as AClass }
.forEach { .... }
The answers provided are correct, I just wanted to add this as an addition.
When using a for loop with force casting, the code will crash (as already mentioned by others).
for button in view.subviews as! [AClass] {
// do something with button
}
But instead of using an if-clause,
if let subviews = view.subviews as? [AClass] {
// If we get here, then subviews is of type [AClass]
...
}
another way is to use a while-loop:
/* If you need the index: */
var iterator = view.subviews.enumerated().makeIterator()
while let (index, subview) = iterator.next() as? (Int, AClass) {
// Use the subview
// ...
}
/* If you don't need the index: */
var iterator = view.subviews.enumerated().makeIterator()
while let subview = iterator.next().element as? AClass {
// Use the subview
// ...
}
Which seems to be more convenient if some elements (but not all) of the array might be of type AClass.
Although for now (as of Swift 5), I'd go for the for-case loop:
for case let (index, subview as AClass) in view.subviews.enumerated() {
// ...
}
for case let subview as AClass in view.subviews {
// ...
}
You can also use a where clause
for button in view.subviews where button is UIButton {
...
}
The answer provided by vacawama was correct in Swift 1.0. And no longer works with Swift 2.0.
If you try, you will get an error similar to:
'[AnyObject]' is not convertible to '[AClass]';
In Swift 2.0 you need to write like:
for button in view.subviews as! [AClass]
{
}
You can perform the casting and be safe at the same time with this:
for button in view.subviews.compactMap({ $0 as? AClass }) {
}
If you want to have an index in your loop:
for case let (index, button as AClass) in view.subviews.enumerated() {
}

Resources