How do I use placeholder API on UIElements correctly - ios

I'm implementing an API called Skeleton View on my Xcode Project. Sometimes when the app loads for the first time, it takes a little bit longer to load the UI Elements, because the data come from JSON and if the user is using 4G or even a bad internet, will remain an empty field. In additional, using this API it shows like a gray view animated placeholder on each UIElement that doesn't received data.
However, I don't know how to check when the UIImage received a value to be able to remove the Skeleton effect and present the Image. I'll post some pictures below.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID") as! TableViewCell
let model = arrCerveja[indexPath.row]
cell.labelName.text = model.name
cell.labelDetail.text = "Teor alcoólico: \(model.abv)"
let resource = ImageResource(downloadURL: URL(string: "\(model.image_url)")!, cacheKey: model.image_url)
if cell.imageViewCell.image != nil {
cell.imageViewCell.kf.setImage(with: resource)
cell.imageViewCell.hideSkeleton()
}else{
cell.imageViewCell.showAnimatedGradientSkeleton()
//The placeholder still there even when the images already got downloaded
//What I wanted was, while the UIImageView has no value, the placeholder
//persists, but when receive the image the placeholder should be dismissed.
}
return cell
}
That's what I got on UIImages(it has a blur animation passing by):
The problem I'm facing is how do I dismiss this effect after each image load itself?

You will need to start the animation when starting the download (often just before your function that get the data) with:
view.startSkeletonAnimation()
The view property here is the one of your view controller, SkeletonView is recursive and will animate all views inside it.
And then after the download is completed (often in a closure before you reload your table view and I assume that all your images are downloaded and ready to be displayed) stop the animation with:
self.view.stopSkeletonAnimation()
Skeleton view also provide you a delegate for UITableView:
public protocol SkeletonTableViewDataSource: UITableViewDataSource {
func numSections(in collectionSkeletonView: UITableView) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int
func collectionSkeletonView(_ skeletonView: UITableView, cellIdenfierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier
}
As the documentation says (read the part about Collections to understand how it works):
This protocol inherits from UITableViewDataSource, so you can replace this protocol with the skeleton protocol.
You will need to make your tableView skeletonnable in you viewDidLoad() function:
tableView.isSkeletonable = true

Related

high performance several horizontal collection views in table view cells with RxSwift/RxDataSources

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.

TableView not working after leaving and coming back to the view

I am puzzled by the behavior of tableView if you leave their view and come back.
I have a screen with one tableView in it that works when I first enter the view. Adding, removing, and updating table cells work. However, when I press a button to segue into the next view and immediately come back, the tableView no longer works. The code that is supposed to execute ( tableView.reload() and all the associated methods) run as they should. However, the screen does not get updated even though internally the arrays get updated, and reload gets ran and executes the code that should update the screen( that is, tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell runs jus fine).
What am I missing? Does tableView require any special treatment if I leave the view and come back to ti?
Thanks.
Edit:
The code for the class where the tableView is something like:
class DebugViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{
#IBOutlet weak var table: UITableView!
var array = [M]()
override func viewDidLoad() {
super.viewDidLoad()
self.searchbar.delegate = self
self.table.delegate = self
self.table.dataSource = self
search_view = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Search_view") as? SomeViewController
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cellManage") as? TableCellManage else { return UITableViewCell() }
let idx = indexPath.row
let value = array[idx]
cell.lbl_time.text = value.month
cell.lbl_background.text = value.color
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 130
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.array.count
}
#IBAction func tapped_addsongs(_ sender: Any) {
self.present( search_view, animated: true, completion: nil)
}
}
Where is the tableView.reload() part? The issue might be generated because you're trying to update the table in a background thread. All UI Updates must be done in main thread:
func someFunc() {
...
DispatchQueue.main.async {
tableView.reload()
}
...
}
After looking at pretty pictures of the life cycle of apps and googling I found the issue and the solution.
The problem was that I had listeners set up to update my table view in a troublesome way. Specifically, I was using the viewDidAppear/viewDidDisappear to bring up and down the listeners, and there was some conflict in the code that managed the state because of this.
Instead, I now bring up the listeners on viewDidLoad. They stay active, regardless of how many views are pushed (within reason, but I only push one), and update my tableview so that when I come back to that view everything is already updated. I don't even see the updates happening, they happen before I get to my view. As for detaching the listeners there is a handy-dandy function I did not know about until 5 minutes ago: deinit. This is the equivalent of destructor in Swift, so I detach my listener when my class object for this view is released from memory.
That solves my issue...and increases performance and I no longer have dangling connections for not managing the listeners well. So a win-win-win.
Thank you all for trying to help! I hope this helps other folks.

Control "Program Flow" in Table View

again me with a short question since Swift is confusing me ATM - but I hope I will get used to it soon ;)
Ok so what I was wondering is: When I call a TableView and generate different Cells is there a way to Interrupt after a few and wait for User Input and react to it?
For Example: 2nd Cell is something like "Go to North or West" after that I want a User Input - with Buttons - in whatever direction he likes to go and react to it with following Cells (different Story Parts -> out of other Arrays?).
What is confusing me is that I just load the whole Application in viewDidLoad and i don't know how I can control the "Flow" within this.
I would really appreciate if someone could show me how I can achieve this maybe even with a small description about how I can control the Program Flow within the Method. I really think this knowledge and understanding would lift my understanding for Swift a few Levels higher.
Thanks in advance!
Here is my current Code which is not including any functionality for the named Question since I don't know how to manage this :)
import UIKit
class ViewController: UIViewController {
var storyLines = ["Test Cell 1 and so on first cell of many","second cell Go to North or West","third Cell - Buttons?"]
var actualTables = 1
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.dataSource = self
}
#IBOutlet weak var tableView: UITableView!
}
extension ViewController : UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return storyLines.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TxtLine", for: indexPath)
cell.textLabel?.text = storyLines[indexPath.row]
cell.textLabel?.numberOfLines = 0;
cell.textLabel?.lineBreakMode = .byWordWrapping
return cell
}
}
Cocoa is event driven. You are always just waiting for user input. It's a matter of implementing the methods that Cocoa has configured to tell you about it.
So here, for example, if you want to hear about when the user presses a button, you configure the button with an action-and-target to call a method in your view controller when the user presses it. That way, your method can see which button the user pressed and remake the table view's data model (your storyLines array) and reload the table with the new data.

Improve TableView Scroll by Moving code to willDisplayHeaderView

My tableview is not scrolling smoothly. I have seen this comment from apple.
But very important thing is still there: tableView:cellForRowAtIndexPath: method, which should be implemented in the dataSource of UITableView, called for each cell and should work fast. So you must return reused cell instance as quickly as possible.
Don’t perform data binding at this point, because there’s no cell on
screen yet. For this you can use
tableView:willDisplayCell:forRowAtIndexPath: method which can be
implemented in the delegate of UITableView. The method called exactly
before showing cell in UITableView’s bounds.
From Perfect smooth scrolling
So I am trying to implement all my code in viewForHeaderInSection to willDisplayHeaderView (Note, I am using sections rather than rows for this specific example because I have custom sections). However, I am getting a "fatal error: unexpectedly found nil while unwrapping an Optional value" error at
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
Below are my original and attempted code that crashed
Original (Note, this code works fine with some minor scrolling lagging problem that I am trying to improve)
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableSectionHeader") as? TableSectionHeader {
// Cancel request of current cell if there is a request going on to prevent requesnt info from background from previous use of same cell
cell.AlamoFireRequest?.cancel()
var image: UIImage?
if let url = post.imageUrl {
image = DiscoverVC.imageCache.objectForKey(url) as? UIImage
}
cell.configureCell(post) // This is data binding part
cell.delegate = self
return cell
} else {
return TableSectionHeader()
}
}
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
}
Attempt
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let cell = tableView.dequeueReusableHeaderFooterViewWithIdentifier("TableSectionHeader") as? TableSectionHeader {
return cell
} else {
return TableSectionHeader()
}
}
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
// Cancel request of current cell if there is a request going on to prevent requesnt info from background from previous use of same cell
cell.AlamoFireRequest?.cancel()
var image: UIImage?
if let url = post.imageUrl {
image = DiscoverVC.imageCache.objectForKey(url) as? UIImage
}
cell.configureCell(post) // This is data binding part
cell.delegate = self
}
-----Update to address Michael's answer------
As there are word limits on replies, here is a response to the Answer from Michael
You are correct, I have updated where I got the snippet from in my questions. My mistake
I agree that problem could very well be lying else where, however this is something that I am going through at the moment. My tableview scrolls OK but sometimes when there is image it slows down a little. So I am going through some steps to ellimate any potential cause.
The reason that I specifically didnt use if let here is that becuase I was expecting the cell to be displayed will be TableSectionHeader. I tried to add it in just then and I ALWAYS gets a failed cast.
The reason that I subclass UITableViewHeaderFooterView is because my headerview is a Xib file where I have func configureCell() so I could call cell.configureCell. (And many other functions)
My header includes a few items like
labels to display title, date, time downloaded from firebase
image that can be optional
image description
like btn, commentbtn, more, btn
All of theses function are addressed in my TableSectionHeader.swift which inherits from UITableViewHeaderFooterView
Could you please explain what you mean by "It's looking suspiciously like you're trying to store state in the header - you should store state outside the tableView."?
Reason that I am cancelling Alamofire request here is because the cell gets dequeued. So if the user scrolls really fast, the cell would get many alamofire request. So I cancelled it first and re-open a download request (inside cell.configureCell) if I dont have anything in my cache
I am not sure how printing sections would help identify. I am thinking it is something foundamentally wrong that I am doing here putting everything in willDisplayHeaderView code (As most place you would put it in viewForHeaderInSection instead). Or maybe it is just the syntax
You're given the header view in the method, you just need to cast it. Try this:
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
guard let cell = view as? TableSectionHeader else { return }
cell.AlamoFireRequest?.cancel()
...
}
I don't think that comment comes from Apple, and I think your problem probably lies elsewhere (eg. blocking the main queue with a task that should run in the background). If you post the contents of TableSectionHeader, this may become clearer.
Pressing on regardless, you are getting a nil value exception in the following line:
let cell = tableView.headerViewForSection(section) as! TableSectionHeader
This is because you're forcing the cast as TableSectionHeader, and it undoubtedly isn't one. If you change it to an if let or guard let, you will see a different code path.
Since you obviously expect it to always be of that type (after all, that is what you're creating in viewForHeaderInSection), something is happening that you don't realise (why are you subclassing UITableViewHeaderFooterView anyway?). I'd be inclined to print the section number and view in both sections so you know what is really happening. It's looking suspiciously like you're trying to store state in the header - you should store state outside the tableView.
Also, there should be no need to cancel the Alamofire request, as this is not the cause of a performance problem - retrieving an image should just fire off another request. Otherwise while a user scrolls around, no images will be displayed because the requests will keep getting cancelled. They would have to leave the screen alone and wait for those images to load before scrolling elsewhere.

Having an issue grabbing the index of selected viewtable in swift

I have been learning swift through the last few days and I have come across an error that I have been stuck on for quite a while now.
I am attempting to get the selected indexPath so that I can then push data according to which item he selected. I have searched through and tried many different solutions I have found on stack overflow as well as different websites but I am not able to get this figured out still.
The code is below:
#IBOutlet var selectGroceryTable: UITableView!
/* Get size of table */
func tableView(tableView: UITableView, numberOfRowsInSection: Int) ->Int
{
return grocery.count;
}
/* Fill the rows with data */
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let myCell:UITableViewCell = selectGroceryTable.dequeueReusableCellWithIdentifier("groceryListRow", forIndexPath:indexPath) as! UITableViewCell
myCell.textLabel?.text = grocery[indexPath.row];
myCell.imageView?.image = UIImage(named: groceryImage[indexPath.row]);
return myCell;
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
print("Row Selected");
NSLog("Row Selected");
}
Nothing ever prints acting like the function is not being called. However, I do not understand why this would not be called?
Any help would be greatly appreciated!
UPDATE:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
selectGroceryTable.data = self;
selectGroceryTable.delegate = self; //gives error states you can not do this
}
There are a couple of things to check in cases like this:
First, what kind of method is didSelectRowAtIndexPath?
Answer: It's a UITableViewDelegate method. Did you set your view controller up as the delegate of the table view? If not, this method won't get called.
Second, have you made absolutely certain that the method signature is a perfect match for the method from the protocol? A single letter out of place, the wrong upper/lower case, a wrong parameter, and it is a different method, and won't be called. it pays to copy the method signature right out of the protocol header file and then fill in the body to avoid minor typos with delegate methods.
It looks to me like your method signature is correct, so my money is on forgetting to set your view controller up as the table view's delegate.

Resources