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?
Related
I am currently facing a decision in the task I am working on. Let me give you some context. The screen Im working on is divided in 4 sections, each section has its own instance of my table view component, each with specific cells. All 4 table views are stacked in the screen. I created a Enum called SectionType and I use this information when Im instantiating the component like: CustomTableView(ofType: .type1). In this way I can switch the enum inside the table view decide which cell Im going to use, how many cells...
Now I need to pass the data to the TableView, I created 4 Models and Im thinking how is the best way to use this in my component.
Solution 1: Creating Convenience inits for each SectionType
My enum:
enum SectionType {
case news, analysis, lives, sectors
}
My Custom table:
private var newsData: [Model.Home.News]? = nil
private var analysisData: [Model.Home.Analysis]? = nil
private var livesData: [Model.Home.Lives]? = nil
private var sectorsData: [Model.Home.Sector]? = nil
// MARK: Initializers
private init(for tableViewType: SectionType) {
self.tableViewType = tableViewType
super.init(frame: .zero, style: .plain)
delegate = self
dataSource = self
registerCells()
}
convenience init(for tableViewType: SectionType, with data: [Model.Home.News]) {
self.init(for: tableViewType)
self.newsData = data
}
convenience init(for tableViewType: SectionType, with data: [Model.Home.Analysis]) {
self.init(for: tableViewType)
self.analysisData = data
}
convenience init(for tableViewType: SectionType, with data: [Model.Home.Lives]) {
self.init(for: tableViewType)
self.livesData = data
}
convenience init(for tableViewType: SectionType, with data: [Model.Home.Sector]) {
self.init(for: tableViewType)
self.sectorsData = data
}
Solution 2: Creating a Generic Let and using Enum with parameters
Changing my Enum to:
enum SectionType {
case news(data: [Model.Home.News])
case analysis(data: [Model.Home.Analysis])
case lives(data: [Model.Home.Lives])
case sectors(data: [Model.Home.Sector])
}
And in my table do something like:
private var data: Array<T>? = nil
// Call this func at initialization
func setData(_ sextionType: SectionType) {
switch sextionType {
case .news(let data), .analysis(let data), .lives(let data), .sectors(let data):
self.data = data
}
}
I don't know if this solution is possible and how to make it work
Solution 3: Accepting suggestions from you guys
Please comment
Thank you for your attention S2
If you are going to have code inside your table view to determine which cells to use etc, the I don't see the point of trying to create a single subclass that does everything. You could just use inheritance; create a MyBaseTableView and then create specific subclasses MyNewsTableView: MyBaseTableView and so on.
Solution 1 definitely doesn't look good to me; You have four different potential data properties, only one of which will have value in a particular case. You will need to use a series of if statements to determine which one applies and you don't really end up achieving anything over having four separate table view classes.
Similarly, if you use generics with an enum you are still tightly binding the possible data sources with the table view.
If you did want to use generics then you don't need a enum. You can simply make your table view generic on its data type. You can use a protocol to allow your data to provide the cell information.
e.g.
protocol CellProvider {
func provideCell(for tableView: UITableView, at indexPath: IndexPath)-> UITableViewCell
static func registerCell(in tableView: UITableView)
}
class GenericTableView<T: CellProvider>: UITableView, UITableViewDataSource {
var data: [T]
init(data: [T]) {
self.data = data
super.init(frame: .zero, style: .plain)
self.dataSource = self
T.registerCell(in: self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return self.data[indexPath.row].provideCell(for: self, at: indexPath)
}
}
struct StringModel {
var value: String
}
extension StringModel: CellProvider {
static let cellName = "StringCell"
func provideCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellName, for: indexPath)
cell.textLabel?.text = value
return cell
}
static func registerCell(in tableView: UITableView) {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellName)
}
}
struct NumberModel {
var value: Int
}
extension NumberModel: CellProvider {
static let cellName = "StringCell"
func provideCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellName, for: indexPath)
cell.textLabel?.text = "\(value)"
return cell
}
static func registerCell(in tableView: UITableView) {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellName)
}
}
Now, when you create your table view you simply provide the data and the generics and protocol do the rest:
private var strings = [StringModel(value: "A"), StringModel(value:"B")]
private var numbers = [NumberModel(value: 1),NumberModel(value:2)]
//...
self.stringTV = GenericTableView(data: strings)
self.numberTV = GenericTableView(data: numbers)
I have a data source in this form:
struct Country {
let name: String
}
The other properties won't come into play in this stage so let's keep it simple.
I have separated ViewController and TableViewDataSource in two separate files. Here is the Data source code:
class CountryDataSource: NSObject, UITableViewDataSource {
var countries = [Country]()
var filteredCountries = [Country]()
var dataChanged: (() -> Void)?
var tableView: UITableView!
let searchController = UISearchController(searchResultsController: nil)
var filterText: String? {
didSet {
filteredCountries = countries.matching(filterText)
self.dataChanged?()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredCountries.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let country: Country
country = filteredCountries[indexPath.row]
cell.textLabel?.text = country.name
return cell
}
}
As you can see there is already a filtering mechanism in place.
Here is the most relevant part of the view controller:
class ViewController: UITableViewController, URLSessionDataDelegate {
let dataSource = CountryDataSource()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.tableView = self.tableView
dataSource.dataChanged = { [weak self] in
self?.tableView.reloadData()
}
tableView.dataSource = dataSource
// Setup the Search Controller
dataSource.searchController.searchResultsUpdater = self
dataSource.searchController.obscuresBackgroundDuringPresentation = false
dataSource.searchController.searchBar.placeholder = "Search countries..."
navigationItem.searchController = dataSource.searchController
definesPresentationContext = true
performSelector(inBackground: #selector(loadCountries), with: nil)
}
The loadCountries is what fetches the JSON and load the table view inside the dataSource.countries and dataSource.filteredCountries array.
Now, how can I get the indexed collation like the Contacts app has without breaking all this?
I tried several tutorials, no one worked because they were needing a class data model or everything inside the view controller.
All solutions tried either crash (worst case) or don't load the correct data or don't recognise it...
Please I need some help here.
Thank you
I recommend you to work with CellViewModels instead of model data.
Steps:
1) Create an array per word with your cell view models sorted alphabetically. If you have data for A, C, F, L, Y and Z you are going to have 6 arrays with cell view models. I'm going to call them as "sectionArray".
2) Create another array and add the sectionArrays sorted alphabetically, the "cellModelsData". So, The cellModelsData is an array of sectionArrays.
3) On numberOfSections return the count of cellModelsData.
4) On numberOfRowsInSection get the sectionArray inside the cellModelsData according to the section number (cellModelsData[section]) and return the count of that sectionArray.
5) On cellForRowAtindexPath get the sectionArray (cellModelsData[indexPath.section]) and then get the "cellModel" (sectionArray[indexPath.row]). Dequeue the cell and set the cell model to the cell.
I think that this approach should resolve your problem.
I made a sample project in BitBucket that could help you: https://bitbucket.org/gastonmontes/reutilizablecellssampleproject
Example:
You have the following words:
Does.
Any.
Visa.
Count.
Refused.
Add.
Country.
1)
SectionArrayA: [Add, Any]
SectionArrayC: [Count, Country]
SectionArrayR: [Refused]
SectionArrayV: [Visa]
2)
cellModelsData = [ [SectionArrayA], [SectionArrayC], [SectionArrayR], [SectionArrayV] ]
3)
func numberOfSections(in tableView: UITableView) -> Int {
return self.cellModelsData.count
}
4)
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionModels = self.cellModelsData[section]
return sectionModels.count
}
5)
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let sectionModels = self.cellModelsData[indexPath.section]
let cellModel = sectionModels[indexPath.row]
let cell = self.sampleCellsTableView.dequeueReusableCell(withIdentifier: "YourCellIdentifier",
for: indexPath) as! YourCell
cell.cellSetModel(cellModel)
return cell
}
I would like to retrieve data from my simple Firestore database
I have this database:
then I have a model class where I have a method responsible for retrieving a data which looks like this:
func getDataFromDatabase() -> [String] {
var notes: [String] = []
collectionRef = Firestore.firestore().collection("Notes")
collectionRef.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
notes = documents.map { $0["text"]! } as! [String] // text is a field saved in document
print("inside notes: \(notes)")
}
print("outside notes: \(notes)")
return notes
}
and as a UI representation I have tableViewController. Let's take one of the methods, for example
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("tableview numberOfRowsInSection called")
return model.getDataFromDatabase().count
}
Then numberOfRows is 0 and the output in the console is:
and I am ending up with no cells in tableView. I added a breakpoint and it doesn't jump inside the listener.
And even though I have 3 of them, they are kinda "late"? They are loaded afterwards. And then the tableView doesn't show anything but console says (later) that there are 3 cells.
If needed, there is also my method for showing the cells names:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("Cells")
let cell = tableView.dequeueReusableCell(withIdentifier: "firstCell", for: indexPath)
cell.textLabel!.text = String(model.getDataFromDatabase()[indexPath.row].prefix(30))
return cell
}
but this method is not even loaded (no print in the console) and this method is written below the method with numberOfRowsInSection.
I have also 2 errors (I don't know why each line is written twice) and these are:
but I don't think it has something to do with the problem.
Thank you for your help!
As #Galo Torres Sevilla mentioned, addSnapshotListener method is async and you need to add completion handler to your getDataFromDatabase() function.
Make following changes in your code:
Declare Global variable for notes.
var list_notes = [String]()
Add completion handler to getDataFromDatabase() method.
func getDataFromDatabase(callback: #escaping([String]) -> Void) {
var notes: [String] = []
collectionRef = Firestore.firestore().collection("Notes")
collectionRef.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
notes = documents.map { $0["text"]! } as! [String] // text is a field saved in document
print("inside notes: \(notes)")
callback(notes)
}
}
Lastly, call function on appropriate location where you want to fetch notes and assign retrieved notes to your global variable and reload TableView like below:
self.getDataFromDatabase { (list_notes) in
self.list_notes = list_notes
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Changes in TableView:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("tableview numberOfRowsInSection called")
return self.list_notes.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("Cells")
let cell = tableView.dequeueReusableCell(withIdentifier: "firstCell", for: indexPath)
cell.textLabel!.text = String(self.list_notes[indexPath.row].prefix(30))
return cell
}
All you need to do is refresh the table cell every time you retrieve the data. Put this code after you set your data inside the array.
self.tableView.reloadData()
I create a TableView via StoryBoard. I connect all the element to a UITableViewCell class call PostTableCell. I already set identifier for the tableCell in StoryBoard. My code is as below:
class PopViewController: UIViewController ,IndicatorInfoProvider ,UITableViewDataSource, UITableViewDelegate{
#IBOutlet weak var popTableView: UITableView!
var posts = [Post]()
override func viewDidLoad() {
super.viewDidLoad()
popTableView.dataSource = self
popTableView.delegate = self
fetchInitialPost()
}
func fetchInitialPost(){
Alamofire.request(MyURL, method: .get).responseJSON{
response in
switch response.result{
case .success(let result):
let json = JSON(result)
guard let feedArr = json["feed"].array else{
return
}
for post in feedArr {
if let post = post.dictionary,let feed = Post.init(dict: post){
self.posts.append(feed)
}
}
self.popTableView.reloadData()
break
case .failure(let error):
print(error)
}
}
}
func indicatorInfo(for pagerTabScripController : PagerTabStripViewController ) -> IndicatorInfo {
return IndicatorInfo (title : "Pop")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PostTableCell", for: indexPath) as! PostTableCell
cell.posts = self.posts[indexPath.row]
return cell
}
}
I think I do all I should do in order to make it work already. But when I run the project I gives me this error, but I don't have any error in console. The app is just blank and this error below pops out.
So what cause this problem? And how to solve this?
This usually happens when there is a problem when loading the Storyboard.
I think (this has happend to me multiple times) you maybe have User Defined Runtime Attributes on popTableView in your Storyboard. If there are any that can not be set, because popTableView does not have the respective property, the debugger will show the behavior that you are seeing.
You can check if there are any User Defined Runtime Attributes by selecting the table view in Interface Builder an checking the Identity Inspector.
Try deleting any attributes that might cause problems.
Alternatively you forgot to connect the #IBOutlet in your Storyboard. You can check how to do that in the official documentation.
I'm trying to combine a CollectionViewwith a TableView, so fare everything works except one problem, which I cant fix myself.
I have to load some data in the CollectionViews which are sorted with the header of the TableViewCell where the CollectionView is inside. For some reason, every time I start the app, the first three TableViewCells are identical. If I scroll a little bit vertically, they change to the right Data.
But it can also happen that while using it sometimes displays the same Data as in on TableViewCell another TableViewCell, here again the problem is solved if I scroll a little.
I think the problem are the reusableCells but I cant find the mistake myself. I tried to insert a colletionView.reloadData() and to set the cells to nil before reusing, sadly this didn`t work.
My TableViewController
import UIKit
import RealmSwift
import Alamofire
import SwiftyJSON
let myGroupLive = DispatchGroup()
let myGroupCommunity = DispatchGroup()
var channelTitle=""
class HomeVTwoTableViewController: UITableViewController {
var headers = ["LIVE","Channel1", "Channel2", "Channel3", "Channel4", "Channel5", "Channel6"]
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.navigationBar.isTranslucent = false
DataController().fetchDataLive(mode: "get")
DataController().fetchDataCommunity(mode: "get")
}
//MARK: Custom Tableview Headers
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return headers[section]
}
//MARK: DataSource Methods
override func numberOfSections(in tableView: UITableView) -> Int {
return headers.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
//Choosing the responsible PrototypCell for the Sections
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellBig", for: indexPath) as! HomeVTwoTableViewCell
print("TableViewreloadMain")
cell.collectionView.reloadData()
return cell
}
else if indexPath.section >= 1 {
// getting header Titel for reuse in cell
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
else {
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
}
}
}
My TableViewCell with `CollectionView
import UIKit
import RealmSwift
var communities: Results<Community>?
class HomeVTwoTableViewCellSmall: UITableViewCell{
//serves as a translator from ChannelName to the ChannelId
var channelOverview: [String:String] = ["Channel1": "399", "Channel2": "401", "Channel3": "360", "Channel4": "322", "Channel5": "385", "Channel6": "4"]
//Initiaize the CellChannel Container
var cellChannel: Results<Community>!
//Initialize the translated ChannelId
var channelId: String = ""
#IBOutlet weak var collectionView: UICollectionView!
}
extension HomeVTwoTableViewCellSmall: UICollectionViewDataSource,UICollectionViewDelegate {
//MARK: Datasource Methods
func numberOfSections(in collectionView: UICollectionView) -> Int
{
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return (cellChannel.count)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCellSmall", for: indexPath) as? HomeVTwoCollectionViewCellSmall else
{
fatalError("Cell has wrong type")
}
//removes the old image and Titel
cell.imageView.image = nil
cell.titleLbl.text = nil
//inserting the channel specific data
let url : String = (cellChannel[indexPath.row].pictureId)
let name :String = (cellChannel[indexPath.row].communityName)
cell.titleLbl.text = name
cell.imageView.downloadedFrom(link :"link")
return cell
}
//MARK: Delegate Methods
override func layoutSubviews() {
myGroupCommunity.notify(queue: DispatchQueue.main, execute: {
let realm = try! Realm()
//Getting the ChannelId from Dictionary
self.channelId = self.channelOverview[channelTitle]!
//load data from Realm into variables
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.dataSource = self
self.collectionView.delegate = self
print("collectionView layout Subviews")
self.collectionView.reloadData()
})
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCommunity = (cellChannel[indexPath.row].communityId)
let home = HomeViewController()
home.showCommunityDetail()
}
}
Thanks in advance.
tl;dr make channelTitle a variable on your cell and not a global variable. Also, clear it, and your other cell variables, on prepareForReuse
I may be mistaken here, but are you setting the channelTitle on the cells once you create them? As I see it, in your viewController you create cells based on your headers, and for each cell you set TableViewController's channelTitle to be the title at the given section.
If this is the case, then the TableViewCell actually isn't receiving any information about what it should be loading before you call reloadData().
In general, I would also recommend implementing prepareForReuse in your HomeVTwoTableViewCellSmall, since it will give you a chance to clean up any stale data. Likely you would want to do something like set cellChannel and channelId to empty strings or nil in that method, so when the cell is reused that old data is sticking around.
ALSO, I just reread the cell code you have, and it looks like you're doing some critical initial cell setup in layoutSubviews. That method is going to be potentially called a lot, but you really only need it to be called once (for the majority of what it does). Try this out:
override the init with reuse identifier on the cell
in that init, add self.collectionView.dataSource = self and self.collectionView.delegate = self
add a didSet on channelTitle
set channelTitle in the viewController
So the code would look like:
var channelTitle: String = "" {
didSet {
self.channelId = self.channelOverview[channelTitle]!
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.reloadData()
}
}
This way you're only reloading your data when the cell is updated with a new channel, rather than every layout of the cell's views.
Sorry... one more addition. I wasn't aware of how your channelTitle was actually being passed. As I see it, you're using channelTitle as a global variable rather than a local one. Don't do that! remove channelTitle from where it is currently before implementing the code above. You'll see some errors, because you're setting it in the ViewController and accessing it in the cell. What you want is to set the channelTitle on the cell from the ViewController (as I outlined above). That also explains why you were seeing the same data across all three cells. Basically you had set only ONE channelTitle and all three cells were looking to that global value to fetch their data.
Hope that helps a little!
(also, you should be able to remove your else if block in the cellForRowAtIndexPath method, since the else block that follows it covers the same code. You can also delete your viewDidLoad, since it isn't doing anything, and you should, as a rule, see if you can get rid of any !'s because they're unsafe. Use ? or guard or if let instead)