IGListKit with sections and multiple items - ios

I'm trying to implement a collection view with IGListKit. It can either have one or three sections. The datasource is populated in realtime and so needs to refresh the content when new data are available.
To do so, I've created a datasource object conform to IGListDiffable that represent each section as such:
final class DataSource: NSObject {
var title: String?
var items: [SJResult] = []
}
extension DataSource: IGListDiffable {
public func diffIdentifier() -> NSObjectProtocol {
return self
}
public func isEqual(toDiffableObject object: IGListDiffable?) -> Bool {
if object === self {
return true
}
guard let obj = object as? DataSource else {
return false
}
return obj.items.count == items.count
}
}
Inside each section, I would like to display a list of items. I've managed to create the section by reusing the same section controller, but the only way I've found to insert new result was to do call reloadData() on the adapter. This is pretty bad as it will reload the complete list of items (which can potentially be very long, >50 items). It also give no possibility to animate the insertion of new item. I've also tried to do a adapter.performUpdates(animated: true) with no luck as it only refresh my datasource but not the items inside the datasource.
I then tried to create a section controller which will display each section with one cell. The cell would contain a IGListCollectionView and will take care of display the items, but with this I had no luck as well as despite the second section controller receives the objects it never displays the cells.
I'm a bit struggling now and not sure how I could display three different section that use the same type of cells with IGListKit, by having the sections static and the items dynamic. I'm thinking of creating three IGListCollectionView and setup bottom / top constraints between them, but I'm having some doubt on the possibility of doing that.
I'm wondering if someone already came across a similar issue and/or if the guys from IGListKit could give me some hints of what's the best implementation to resolve that?

You should be returning unique instances of your section controllers for each object. Don't reuse them!
Another thing to note is that you're using self as the diff identifier which means that the instance of the object identifies its uniqueness. That means two DataSource objects wont ever be compared (obj.items.count == items.count will never happen). Not a deal-breaker, but just be aware that's how it'll behave.
You also might want to take a look at IGListBindingSectionController which takes your original model and breaks it down into view models that drive each cell in the section.
More details and an example in the pull request. Note that this requires using master if you're using CocoaPods.
https://github.com/Instagram/IGListKit/pull/494

Related

Best way to store and refrence lots data in Swift for use in a UITableView

This is a bit confusing so I apologise. But:
I am trying to make an app where the user has the ability to add items to a list of individual items that will be displayed back to them(Something along the lines of a todo list app) as a table view.
However, I have hit a roadblock I need to store several different bits of data for each item(Some Strings, Some ints and a date) in the list.
I think that a class(or struct) would be the best way to do this where an instance of the class holds the information need for each item and then the name of that instance is stored in a list so it can be accessed via the indexPath in the table view.
However, I don't know how I am going to make a new instance of the class for every item because the app could have hundreds of individual items.
I'm sorry, this is so confusing and any help would be appreciated! Feel free to ask for more info
Edit: what I am looking for and I'm sure there's a stupidly easy way of doing it but I'm try to work out how to create an instance of a class when the name of the class is stored in a variable. Ecencialy I want the instance of the class to store the item. To be created when the user inputs the item to be added to the table.
Eg. They enter an item. item1 and the other data that goes along with then I want to be able to store that in instance of the item class but I don't know how to make the name of that instance because the name I want which is item 1 is stored in a variable.
Sorry that's so confusing that's the reason I need help
So first: You can't store the Name of a Class in a Variable and use this variable to get a new instance of a class.
What you need is an Array containing all the different Items. This array is unlimited so you can store as many items in it as you like and you don't need to set a name for each of these Instances.
First create an empty array containing all Items as a property:
var items : [Item] = []
Second call the function populateItemArray() in your viewDidLoad():
private func populateItemArray() {
// Populate the items Array (Are you using CoreData, Realm, ...?)
}
Third, use the Item Array to populate the TableView.
REMEMBER: If your only using one section in your tableView, indexPath.row is always equal to the corresponding item in the array.
E.G. items[indexPath.row]
Hope this helps you!
UPDATE:
Look at this example struct Item. As you can see you can also store a date in it:
struct Item {
// First create all the properties for your struct.
var data1: String
var data2: Int
var data3: String
// You can create properties of any type you want, also of type date.
var dateOfCreation : Date
// Then implement all the methods of your custom Struct Item.
func setDate(with date : Date) {
self.dateOfCreation = date
}
func returnDate() -> Date {
return self.dateOfCreation
}
}

On iOS, for the DisposeBag in MVVM, can it be placed in ViewModel?

I am new to RxSwift and here I would like to ask a question about where should the DisposeBag be.
My case is, I have retrieve a list of items being displayed in a tableview, and each of them will have its own flag to indicate if it is selected.
So I am not just binding the result list to the tableview. I need to have some logic which editing the list on local while user navigate via the tableview.
I have created an instance Variable([Item]) in the ViewModel but if I place the logic in the ViewModel a DisposeBag is needed.
After having some googling, most of the examples of MVVM+RxSwift which have instances of Variable place the DisposeBag in ViewModel but some say it should only be placed in ViewController. Is that true? How can I listen the Observable in ViewModel so that my business logic can be placed in ViewModel?
A DisposeBag more often than not should not be placed in a ViewModel unless there's a good reason.
In general a DisposeBag is meant to tie subscriptions to their owner. It is usually not the case that the ViewModel creates any subscriptions, but merely provides Observables so a consumer could subscribe to them (for example a ViewController).
That means the ViewController is the one to usually hold the DisposeBag, as its usually the one that uses the subscriptions (and not the ViewModel itself).
Yes Disposable bag can be placed in viewModel.Wherever there is observable you create , there is a need to dispose of observable.So it can be taken in viewModels as well.As you are new to Rxswift , I recommend to go through this blog for further clarity of RxSwift:
https://medium.com/#arnavgupta180/shift-from-swift-to-rxswift-8dece8af9f4
You can place Disposable or DisposeBag in ViewModel, it's all up to when you want subscriptions to die. It's a good practice to keep them all in one place, as for ex. ViewController. Where will be much easier to handle subscriptions, as sometimes you don't really need a subscription for business logic, ex. when scroll view scrolls you disable a button. (But still, there are architectures, like RxMVP, where it's the other way around)
In your case, you can combine Observables instead of having a variable in ViewModel. It all depends on your needs, but you can have something like:
class ViewModel {
var activeItems: Observable<[Item]> {
return Observable.combineLatest(retrieveData(), itemEdited().startWith(nil)) { (allItems, editedItem) in
// TODO: check if edited item should be in list
}
}
private func retrieveData() -> Observable<[Item]> {
return .empty()
}
private func itemEdited() -> Observable<Item?> {
return .empty()
}
}
If you have this items coming from Realm or CoreData, you can use Rx implementations for your database, so it will emit an event every time your entity is modified.

Equivalent of Androids DiffUtil for Swift / iOS or other effective implementation

In my app I have to load some data from a web service. Then I have to sort the data by properties of models of a collection property of the model and then update my collectio view with the new data.
Unfortunately I have no clue how I can know which cell moved to which new position. On Android, I used DiffUtil in the past but I'm not aware of a Swift equivalent to animate the changes in my collection view.
Heres the data model I use. For the this example, I omit all the other fields and use protocols to make it clearer (at least I hope this makes it clearer):
protocol Alert {
var date: Date
var statusType: StatusType
}
protocol Status {
var statusType: StatusType
var level: Level
}
protocol Device {
var states: [Status] //Contains never more than 1 Status of each StatusType so its basically a set
var alertsByStatusType: [StatusType: [Alert]]
}
enum StatusType {
case someStatus, someOtherStatus
}
enum Level {
case low, medium, high
}
The web service returns me a device which contains a collection of Status and a collection of Alert.
//Web service model kind of sucks
protocl WebServiceDevice {
var states: [Status]
var alerts: [Alert]
}
In the device constructor, I do some manually sorting and mapping to build the alertsByStatusType dictionary. I also sort the Status collection by the Status.level so that first, all "high" level states come, then the "medium" level and finally the "low" level states.
Inside the same level, I then have to sort by the Alerts.date so when two Status have the same Level, the one with the more recent Alert comes first.
I know, this data model I get from the web service is terrible to begin with and I begged for a proper model where the alerts are inside their corresponding states and the states are properly sorted but hey, thats life.
Now when I display all states in a tableview, let's say a Status with level == low gets and alert, then the level will change and after updating my data from the web service and sorting everything, it will be further to the beginning in the Device.states collection. How can I know from which indexPath I have to move the cell to which new indexPath?
I hope you understand what I mean and what the problem is. Basically I have to find out which Status has a different Level and from where to where it has moved in the data source of my table view so I can animate the changes instead just call reload() on the table view.
I'm not sure why but the answer I selected as correct for this question has vanished so I thought I'd answer it myself. I was pointed to a library which can do exactly that and by the time of writing, I found more libraries.
They probably all have their ups and downs and the one I use now is the Dwifft Library (https://github.com/jflinter/Dwifft)
Other interesting diff libraries can be found here: https://awesome-repos.ecp.plus/ios.html under the Data Structures / Algorithms section
Instagram's IGListKit does really good job when it comes to deal with arrays in a list view(i.e. UICollectionView) thanks to ListDiff function.

reloadData() doesn't update parent TableView

I have a custom TableViewController called LibraryTableViewController.
class LibraryTableViewController: UITableViewController {
contains this data for rendering.
var items: [item] = []
Then, I have another class for Firebase that inherits the LibraryTableViewController as a delegate. (I am not sure if this is the right description.)
class FirebaseDB : LibraryTableViewController {
Then, I have a global instance of FirebaseDB as
let database = FirebaseDB()
I have an observer listening to the changes in the database in the Firebase class, and I was updating
self.items
Then, trying to refresh the LibraryTableViewController using
self.tableView.reloadData()
I might be wrong in understanding my current problem. What I think is that the items array that I am updating isn't the same instance as the tableView that I see on the app. It is updating the FirebaseDB instance database's items which isn't being rendered on the app. How do I make sure that the right instance is being updated?
a lot of code missing
but you can check:
thread must be main, maybe you call from background
check is the items is changed
the numbers for you
check is the tableview not nil
check your delegate maybe its also nil
but better give us more code)

What is the best design pattern for reusing a UITableView with many data sources?

I have a UITableViewController that displays data retrieved over the network. I am working to figure out what the best design pattern would be so that I can reuse that same table view, but display data from many potential data sources. In my case I could potentially have upwards of 50 completely different network requests that will retrieve data to put into this table view. I don't want to subclass and have 50 different table views all with just a different network request method. What would be the best way about reusing a single class, but enabling the ability to have the table views data source retrieve data from many places?
Whether you want to actually subclass UITableViewController only depends on the kind of data that you want to display and if you need customized UITableViewCells for it.
If you don't need any customization and you just want to display the data in a simple way, there is no need to subclass UITableViewController at all.
Also, the design pattern you are looking for isn't really required, since UITableViewController itself is designed in such a flexible way that you can easily show data from different data sources (that's what the UITableViewDataSource protocol ist for after all).
In case all your 50 data sources always return an array of objects, you can just implement one version of UITableViewController and let it flexibly pull the data from any of these data sources. Then you can use a property, e.g.
#property (nonatomic, strong) NSArray* data;
within the UITableViewController.
Then you use your web service interface to perform the different kinds of requests when needed and pull the appropriate data. And then, once the new data returns from the web service, you can assign it to your property data and call [self.tableView reloadData] which will cause the UITableView to repopulate with the new contents of data.
Not sure if this exactly solves your issue, please let me know if I didn't get your problem quite right. Maybe you need to elaborate a bit more precisely on your issue as well so that I can provide better help :)
You can definitely use one UITableView in this scenario. The root of the issue is translating data from the server of 50 different versions to a data type (i.e. model) that will be displayed in each UITableViewCell.
You need to do the following:
Figure out the data that will be displayed in the cell (i.e. title, description, author, etc.)
Create a custom class/struct that holds this data (i.e. NewsItem)
Create NewsItem objects based on server data.
Populate your data source.
Match up your model properties with your UITableViewCell.
Invoke tableView.reloadData() when you get new data.
So let's say I find that I have a title, description and author, I could create this struct:
struct NewsItem {
var title: String
var detail: String
var author: String
}
As part of my code that interacts with the server, I create objects:
var newsItem1 = NewsItem(title: "Swift's Amazing", detail: "Here we go into why Swift is....", author: "Dustin Hill")
news.append(newsItem1)
Then when as part of configuring my UITableViewCells, I do the following:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let newsItem = news[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseId")
cell.textLabel.text = "\(newsItem.title) by \(newsItem.author)"
cell.detailTextLabel.text = newsItem.detail
return cell
}
This way, you only have one type of object to display in the cell. The key is to translate what you are getting from the server and store them in a generic data type (which from reading your comments is what you are looking for)

Resources