The documentation is sparse on FirebaseUI. How do I use different data sources for a tableview using FirebseUI?
For a tableview that uses a single datasource, you can use the FUIFirestoreTableViewDataSource or FirebaseTableViewDataSource. You can bind either of them to a tableview using a query (such as an FIRQuery) thus:
let query: Query = self.db.collection("users").whereField("name", isEqualTo: "Taiwo")
self.dataSource = self.tableView.bind(toFirestoreQuery: query) { tableView, indexPath, snapshot in
// get your data type out of the snapshot, create and return your cell.
}
This works very well for a tableview with a 'single source of truth' and makes your tableview react to changes etc in the database without much work from you.
However, in a case where you need to change the data on the fly (such as where you need to display different data based on user selection), using a datasource won't work. In this case, you need to use a backing collection from Firebase such as a FUIBatchedArray or FUIArray.
This isn't much different from using a Swift array alongside your tableview's datasource. The only significant difference is that you need to initialize the array using typically your viewcontroller as its delegate:
var datasourceArray = FUIBatchedArray(query: query, delegate: self)
Then
extension MyViewController: FUIBatchedArrayDelegate {
func batchedArray(_ array: FUIBatchedArray, didUpdateWith diff: FUISnapshotArrayDiff<DocumentSnapshot>) {
// you'll receive changes to your array in `diff` and `array` is a whole array with
// all new changes together with old data
datasourceArray = array
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
func batchedArray(_ array: FUIBatchedArray, willUpdateWith diff: FUISnapshotArrayDiff<DocumentSnapshot>) {
}
func batchedArray(_ array: FUIBatchedArray, queryDidFailWithError error: Error) {
}
}
Then you can use datasourceArray as you would a Swift array in your UITableViewDataSource methods:
extension MyViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datasourceArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let snapshot = datasourceArray.object(at: indexPath.row)
do {
let task = try snapshot.data(as: Task.self)
// create, customise and return your cell
}
}
catch {
print("coudln't get data out of snapshot", error.localizedDescription)
}
return UITableViewCell()
}
}
Related
A famous layout you can find in most apps is having several horizontal lists in a table view cell where each list gets its data from the server. can be found in Airbnb. example below:
Each list has a loading view and an empty state to show when something is wrong.
Each list triggers the network request only when its first time created, so when displayed again by scrolling the table view, it should NOT make another network request to get data.
I tried several approaches and but not yet satisfied. some of which run into memory leaks and performance issues when having several collectionview. currently, I do the network requests in the View controller that holds the table view and passes the data to each cell.
Can anyone share their approach on how to do this? Appreciated!
Example:
This is a huge question with a lot of different possible answers. I recently solved it by using a custom table view data source that reports the first time (and only the first time) an item is displayed by a cell. I used that to trigger when the individual inner network requests should happen. It uses the .distinct() operator which is implemented in RxSwiftExt...
final class CellReportingDataSource<Element>: NSObject, UITableViewDataSource, RxTableViewDataSourceType where Element: Hashable {
let cellCreated: Observable<Element>
init(createCell: #escaping (UITableView, IndexPath, Element) -> UITableViewCell) {
self.createCell = createCell
cellCreated = _cellCreated.distinct()
super.init()
}
deinit {
_cellCreated.onCompleted()
}
func tableView(_ tableView: UITableView, observedEvent: Event<[Element]>) {
if case let .next(sections) = observedEvent {
self.items = sections
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[indexPath.row]
let cell = createCell(tableView, indexPath, item)
_cellCreated.onNext(item)
return cell
}
private var items: [Element] = []
private let createCell: (UITableView, IndexPath, Element) -> UITableViewCell
private let _cellCreated = PublishSubject<Element>()
}
Each table view cell needs its own Observable that emits the results of the network call every time something subscribes to it. I do that by using .scan(into:accumulator:). An example might be something like this:
dataSource.cellCreated
.map { ($0, /* logic to convert a table view item into network call parameters */) }
.flatMap {
Observable.zip(
Observable.just($0.0),
networkCall($0.1)
.map { Result.success($0) }
.catchError { Result.failure($0) }
)
}
.scan(into: [TableViewItem: Result<NetworkResponse, Error>]()) { current, next in
current[next.0] = next.1
}
.share(replay: 1)
Each cell can subscribe to the above and use compactMap to extract it's particular piece of state.
Evening, in my application I do not want to use RxCocoa and I'm trying to conforming to tableview data source and delegate but I'm having some issues.
I can't find any guide without using RxCocoa or RxDataSource.
In my ViewModel in have a lazy computed var myData: Observable<[MyData]> and I don't know how to get the number of rows.
I was thinking to convert the observable to a Bheaviour Subject and then get the value but I really don't know which is the best prating to do this
You need to create a class that conforms to UITableViewDataSource and also conforms to Observer. A quick and dirty version would look something like this:
class DataSource: NSObject, UITableViewDataSource, ObserverType {
init(tableView: UITableView) {
self.tableView = tableView
super.init()
tableView.dataSource = self
}
func on(_ event: Event<[MyData]>) {
switch event {
case .next(let newData):
data = newData
tableView.reloadData()
case .error(let error):
print("there was an error: \(error)")
case .completed:
data = []
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = data[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
// configure cell with item
return cell
}
let tableView: UITableView
var data: [MyData] = []
}
Make an instance of this class as a property of your view controller.
Bind your myData to it like:
self.myDataSource = DataSource(tableView: self.tableView)
self.myData
.bind(to: self.myDataSource)
.disposed(by: self.bag)
(I put all the selfs in the above to make things explicit.)
You could refine this to the point that you effectively re-implement RxCoca's data source, but what's the point in that?
so here's the problem I had today. In one method of mine on a project I've been working on I fetch entities using core data, and it seems to work. Here's my method:
func getVehicles() {
let moc = DataController().managedObjectContext
let vehicleFetch = NSFetchRequest(entityName: "VehicleEntity")
do {
allVehicles = try moc.executeFetchRequest(vehicleFetch) as! [VehicleEntity]
} catch {
fatalError("Failed to fetch vehicles: \(error)")
}
}
I call that in my viewDidLoad method right after calling super.viewDidLoad.
I'm loading a property of those entities into a tableview and I do so like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("vehicleCell")
cell!.textLabel!.text = allVehicles[indexPath.item].vehicleType!
return cell!
}
That works great, and it returns the .vehicleType property from all the entities and and puts it in the table. Here's where my problem occurs, in the method that gets called when a row is selected:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedVehicle = allVehicles[indexPath.item]
if let del = delegate {
del.updateCurrentVehicle(selectedVehicle)
}
self.navigationController?.popViewControllerAnimated(true)
}
Selected vehicle comes back with a bunch of weird data. Interacting with it in the console at a breakpoint has the .vehicleType property coming back as nil and it gives me an exception when I try to access other properties, all of which are of type NSNumber. If I go and fetch the data again like this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedVehicle: VehicleEntity
let moc = DataController().managedObjectContext
let vehicleFetch = NSFetchRequest(entityName: "VehicleEntity")
do {
selectedVehicle = try moc.executeFetchRequest(vehicleFetch)[indexPath.item] as! VehicleEntity
// allVehicles = fetchedVehicles
} catch {
fatalError("Failed to fetch vehicles: \(error)")
}
if let del = delegate {
del.updateCurrentVehicle(selectedVehicle)
}
self.navigationController?.popViewControllerAnimated(true)
}
Why am I not able to access the properties of entities in that array in my first way of selecting the row, but I am by reselecting it? Is this some weird bug, or some nuance of Swift I was unaware of?
If anyone could help explain this, I'd really appreciate it. I'm glad I made it work, but I'd really like to understand why one way works and the other way doesnt.
Edit in response to the comments:
VehicleEntity is in fact a subclass of NSManagedObject. Here is the code for updateCurrentVehicle:
protocol SavedVehicleDelegate {
func updateCurrentVehicle(newVehicle: VehicleEntity)
}
and
func updateCurrentVehicle(newVehicle: VehicleEntity) {
setVehicleData(newVehicle)
}
Although the selectedVehicle has bad data before it gets passed into that method. I set a breakpoint in both the cellForRowAtIndexPath and didSelectRowAtIndexPath methods, and in the former the data is fine and correct, but in the second the properties of the vehicle object are as I've described above.
The updateCurrentVehicle method is what I use to pass the selected VehicleEntity object back to the parent view (this is in a navigation controller)
I use the indexPath.item only because the tableview isn't divided up into sections, so the item just returns the overall index in the tableview as a whole, or that's my understanding of it anyways.
I'd like to get started using swift to make a small list based application. I was planning on using two table view controllers to display the two lists, and was wondering if it were possible to have them share a common data source.
Essentially the data would just be an item name, and two integers representing the amount of the item owned vs needed. When one number increases, the other decreases, and vice versa.
I figured this might be easiest to do using a single data source utilized by both table view controllers.
I did some googling on shared data sources and didn't find anything too useful to help me implement this. If there are any good references for me to look at please point me in their direction!
You can create one data source class and use it in both view controllers:
class Item {
}
class ItemsDataSource: NSObject, UITableViewDataSource {
var items: [Item] = []
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
//setup cell
// ...
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
}
class FirstViewController : UITableViewController {
var dataSource = ItemsDataSource()
override func viewDidLoad() {
self.tableView.dataSource = dataSource
self.tableView.reloadData()
}
}
class SecondViewController : UITableViewController {
var dataSource = ItemsDataSource()
override func viewDidLoad() {
self.tableView.dataSource = dataSource
self.tableView.reloadData()
}
}
use singleton design pattern, it means both tables will get data source from the same instance
class sharedDataSource : NSObject,UITableViewDataSource{
static var sharedInstance = sharedDataSource();
override init(){
super.init()
}
//handle here data source
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
}
}
var tableOne = UITableView();
var tableTwo = UITableView();
tableOne.dataSource = sharedDataSource.sharedInstance;
tableTwo.dataSource = sharedDataSource.sharedInstance;
The first argument to the delegate method is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
}
At that point, your one Datasource delegate can decide which table view is wanting a cell, for example, and return results accordingly.
I have a UITableViewController which works fine with TableViewCell:
class NewTableViewController: UITableViewController {
However I want to implement a method to update and reload the data in the TableView.
The update part works well, it deletes CoreData objects, queries HealthKit, saves to CoreData and then call a function (func setupArrays) in TableViewController which fetch data from CoreData and appends it to arrays used in cellForRowAtIndexPath. From the Console log I can see that it works well(e.g. the updated arrays reflects changes in Healthkit). The problem arises when I try to reload the tableView:
self.tableView.reloadData()
nothing happens !
I did some research and also tried this method:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
Still nothing.
I call the reloadData function at the end of the function setupArrays. (This is also the place where I print to the log the arrays which correctly reflect changes):
func setupArrays (){
if NSUserDefaults.standardUserDefaults().boolForKey("stepsSwitch") == true {
titleArray.append(stepsCell.title())
imageNameArray.append(stepsCell.imageName)
xAxisDatesArray.append(cdFetchSteps.queryCoreDataDate())
yAxisValuesArray.append(cdFetchSteps.queryCoreDataData())
}
if NSUserDefaults.standardUserDefaults().boolForKey("hrSwitch") == true {
titleArray.append(heartRateCell.title())
imageNameArray.append(heartRateCell.imageName)
xAxisDatesArray.append(cdFetchHeartRate.queryCoreDataDate())
yAxisValuesArray.append(cdFetchHeartRate.queryCoreDataData())
}
if NSUserDefaults.standardUserDefaults().boolForKey("weightSwitch") == true {
titleArray.append(weightCell.title())
imageNameArray.append(weightCell.imageName)
xAxisDatesArray.append(cdFetchWeight.queryCoreDataDate())
yAxisValuesArray.append(cdFetchWeight.queryCoreDataData())
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
println(yAxisValuesArray)
}
Both delegate and dataSource is set correctly in the IB. I tried to add them "explicitly" in the class e.g.:
class NewTableViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate{
But that did nothing.
UPDATE 1
Here is my numberOfRowsInSection and cellForRowAtIndexPath:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return titleArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var myCell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("myCell") as TableViewCell
myCell.title.text = titleArray[indexPath.row]
myCell.imageName = imageNameArray[indexPath.row]
myCell.xAxisDates = xAxisDatesArray[indexPath.row]
myCell.yAxisValues = yAxisValuesArray[indexPath.row]
return myCell }
UPDATE 2
when I put a breakpoint cellForRowAtIndexPath it shows the values for title and imageName, but appears empty for xAxisDates and xAxisValues both at the first load and after reload (see attached picture). This seems strange to me as the values are available in the TableViewCell and displays fine. Are arrays not visible in the debug area/variables view ?
Question: How do I update my TableViewController?
Any help would be very much welcomed - thank you.