Swift tableView bottom loading indicator - swift

I am currently struggling with implementing a bottom loading indicator for my app, exactly like Instagram and Facebook has. Simply I want to show a loading indicator at the bottom (on reverse drag) just like a normal table view loading.
Here is the code that I have for the regular table view update:
var refreshControl: UIRefreshControl!
//In viewDidLoad
refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: "Refresh:", forControlEvents: UIControlEvents.ValueChanged)
tableView.addSubview(refreshControl)
In my func Refresh() I simply just fetch the data, and controls the activity indicator from there. However, how would I approach this, if I wanted to enable this in the bottom of my tableView?
Help is much appreciated.

Add it to the table footer view
tableView.tableFooterView = footerView
Adding a refreshControl would be difficult.
Add a UIIndicatorView to the footerview.
Implement scrollViewDidScroll:
func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height {
tableView.tableFooterView!.hidden = true
// call method to add data to tableView
}
}

Refresh Controller is for pulltorefresh which usually use to reload data.
Spinner at the bottom of the screen is used for pagination.
What you need to do in numberOfRows method return arraysize+1.
in cellforRowAtIndexPath method check if indexpath.row > arraySize than return a cell having uiactivitycenter in the center.
Hope this will help you.

If you are getting the data from API call with pagination, which has to show in table view Then Kuntal's answer is right, in addition you can make 0 hight for cell containing indicator view when there is no more data, and after completion of reload. It is easy manage than check and return arraysize+1.

Related

Swift 5 call function after scrollup UITable

I m implement view with UITableViewDataSource, now I need to call a method to refresh this UITable if the user scroll UP all the table as in Android system.
There is a way to call a method using swift 5 when the UITable when the user swipes his finger down and is at the beginning of the table?
regard
You can use UIRefreshControl provided by Apple.
// initializing the refreshControl
tableView.refreshControl = UIRefreshControl()
// add target to UIRefreshControl
tableView.refreshControl?.addTarget(self, action: #selector(callPullToRefresh), for: .valueChanged)
Add your code in callPullToRefresh() and it will be executed when the user will pull
func callPullToRefresh() {
// Do what you need for example fetchDataFromAPI()
// Don't forget to dismiss the refresh control.
DispatchQueue.main.async {
self.myScrollingView.refreshControl?.endRefreshing()
}
For more information https://developer.apple.com/documentation/uikit/uirefreshcontrol

Prefer Large Titles and RefreshControl not working well

I am using this tutorial to implement a pull-to-refresh behavior with the RefreshControl. I am using a Navigation Bar. When using normal titles everything works good. But, when using "Prefer big titles" it doesn't work correctly as you can see in the following videos. Anyone knows why? The only change between videos is the storyboard check on "Prefer Large Titles".
I'm having the same problem, and none of the other answers worked for me.
I realised that changing the table view top constraint from the safe area to the superview fixed that strange spinning bug.
Also, make sure the constant value for this constraint is 0 🤯.
At the end what worked for me was:
In order to fix the RefreshControl progress bar disappearing bug with large titles:
self.extendedLayoutIncludesOpaqueBars = true
In order to fix the list offset after refreshcontrol.endRefreshing():
let top = self.tableView.adjustedContentInset.top
let y = self.refreshControl!.frame.maxY + top
self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated:true)
If you were using tableView.tableHeaderView = refreshControl or tableView.addSubView(refreshControl) you should try using tableView.refreshControl = refreshControl
It seems there are a lot of different causes that could make this happen, for me I had a TableView embedded within a ViewController. I set the top layout guide of the tableview to the superview with 0. After all of that still nothing until I wrapped my RefreshControl end editing in a delayed block:
DispatchQueue.main.async {
if self.refreshControl.isRefreshing {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
self.refreshControl.endRefreshing()
})
}
}
The only working solution for me is combining Bruno's suggestion with this line of code:
tableView.contentInsetAdjustmentBehavior = .always
I've faced the same problem. Call refreshControl endRefreshing before calling further API.
refreshControl.addTarget(controller, action: #selector(refreshData(_:)), for: .valueChanged)
#objc func refreshData(_ refreshControl: UIRefreshControl) {
refreshControl.endRefreshing()
self.model.loadAPICall {
self.tableView.reloadData()
}
}
The only solution that worked for me using XIBs was Bruno's one:
https://stackoverflow.com/a/54629641/2178888
However I did not want to use a XIB. I struggled a lot trying to make this work by code using AutoLayout.
I finally found a solution that works:
override func loadView() {
super.loadView()
let tableView = UITableView()
//configure tableView
self.view = tableView
}
I had this issue too, and i fixed it by embedded my scrollView (or tableView \ collectionView) inside stackView, and it's important that this stackView's top constraint will not be attached to the safeArea view (all the other constraints can). the top constraint should be connect to it's superview or to other view.
I was facing the same issue for very long, the only working solution for me was adding refresh control to the background view of tableview.
tableView.backgroundView = refreshControl
Short Answer
I fixed this by delaying calling to API until my collection view ends decelerating
Long Answer
I notice that the issue happens when refresh control ends refreshing while the collection view is still moving up to its original position. Therefore, I delay making API call until my collection view stops moving a.k.a ends decelerating. Here's a step by step:
Follow Bruno's suggestion
If you set your navigation bar's translucent value to false (navigationBar.isTranslucent = false), then you will have to set extendedLayoutIncludesOpaqueBars = true on your view controller. Otherwise, skip this.
Delay api call. Since I'm using RxSwift, here's how I do it.
collectionView.rx.didEndDecelerating
.map { [unowned self] _ in self.refreshControl.isRefreshing }
.filter { $0 == true }
.subscribe(onNext: { _ in
// make api call
})
.disposed(by: disposeBag)
After API completes, call to
refreshControl.endRefreshing()
Caveat
Do note that since we delay API call, it means that this whole pull-to-refresh process is not as quick as it could have been done without the delay.
Unfortunately, no advice helped. But I found a solution that helped me. Setting the transparency of the navigation bar helped.enter image description here
Problem can be solved if add tableview or scroll view as root view in UIViewController hierarchy (like in UITableViewController)
override func loadView() {
view = customView
}
where customView is UITableView or UICollectionView

Add custom recognizer delay

I've disabled delaysContentTouches in my tableview subclass using:
delaysContentTouches = false
subviews.forEach { ($0 as? UIScrollView)?.delaysContentTouches = false }
But in one of my sections, I still want to keep the delay. Is there a way to cancel the delay for certain sections or perhaps I can add a custom recognizer delay to a section?
Sections are not actual objects within a tableView, so my answer to your first question is no. The .delaysContentTouches applies to the entire tableView.
For your second inquiry, I believe that one way it could be possible is through setting a delay for desired cells' scrollView subview. In your tableView(cellForRowAt: indexPath) func, you could have something like this:
if indexPath.section == 3 { //or whatever your desired section is
for view in cell.subviews {
if view is UIScrollView {
let currentView = view as! UIScrollView
currentView.delaysContentTouches = true
}
}
}
This will find the UIScrollView in your cell's subviews in your desired section. It will then set the .delaysContentTouches property accordingly.
I have not personally executed this code, just researched it, so let me know if it works.
Edit
Apparently the UIScrollView in UITableViewCell has been deprecated, so the above method will not work anymore.
My next best suggestion to you is to use a UILongPressGuestureRecognizer. This will not be quite the same thing as delaying the touch, but could have a similar effect in real execution.
You could use it in the same tableView(cellForRowAt: indexPath) func as so:
let press = UILongPressGestureRecognizer(target: self, action: #selector(handlePress))
press.minimumPressDuration = 2.0 //however long you want
cell.addGestureRecognizer(press)
Whatever you are trying to achieve by selecting certain rows of your tableView could be placed in the handlePress func above which would be trigged upon the long press.

How to dismiss keyboard in UISearchController when changing focus in tvos?

I'm completely new to tvos and I'm trying to implement a UISearchController view where, in my SearchResultsViewController, I have two UICollectionViews displayed one above the other:
The problem is that when the user swipes down to select one of the items in the UICollectionView, the keyboard doesn't dismiss. Even swiping back up to select the keyboard doesn't fully scroll up and it's impossible to see what you're typing. The resulting view is this:
Ideally, I'd like to dismiss the keyboard when the user swipes down to focus on anything else in the interface. I looked at Apple's tvos UIKit Catalog and their example shows a UISearchController which dismisses the keyboard when changing focus, but I don't see that they're doing anything differently.
Here is the code I'm using to setup my UISearchController when the user clicks on a button:
#IBAction func onSearchButton(sender: AnyObject) {
guard let resultsController = storyboard?.instantiateViewControllerWithIdentifier(SearchResultsViewController.storyboardID) as? SearchResultsViewController else { fatalError("Unable to instantiate a SearchResultsViewController.") }
// Create and configure a `UISearchController`.
let searchController = UISearchController(searchResultsController: resultsController)
searchController.searchResultsUpdater = resultsController
searchController.hidesNavigationBarDuringPresentation = false
let searchPlaceholderText = NSLocalizedString("Search for a Show or Movie", comment: "")
searchController.searchBar.placeholder = searchPlaceholderText
// Present the search controller from the root view controller.
guard let rootViewController = view.window?.rootViewController else { fatalError("Unable to get root view controller.") }
rootViewController.presentViewController(searchController, animated: true, completion: nil)
}
After quite a bit of trial and error, I was able to figure out the solution.
The keyboard will automatically dismiss itself as long as:
1) The item the user focuses on is inside of a scrollview
2) The scrollview content size is larger than the screen height by at least 1px (1081px).
After quite a lot of trial and error, finally I figured out.
The reason is that you have nested ScrollViews in searchResultsController.
"ScrollViews" of-course includes UICollectionView, UITableView, and UIScrollView.
According to my investigation, UISearchController behaves as follows.
If the first view which gets focused in searchResultsController is subview of the inner scrollView (which is the horizontal UICollectionView, in your case), then you won't get keyboard hidden as expected.
Interestingly, if the first view which gets focused in searchResultsController is subview of outer scrollView, then you will get keyboard hidden completely, animated, just as expected (!).
I think this is sort of UIKit's bug.
I had exactly same layout and wasn't able to achieve this so far. I believe you return false in tableView(tableView: UITableView, canFocusRowAtIndexPath indexPath: NSIndexPath) -> Bool so that each cells in collection view can scroll horizontally with proper focus behavior. I think it's actually causing the issues. If you make the first cell in the tableview focusable the problem goes away but of course focus behavior is not desired. I just found that out today and will try more tomorrow to find out what I can do about this. I sense that I will need a new design that allows me to use a single collectionview or tableview that has its cells focusable in resultsController. Hope this is easily achievable in tvOS 10.
Work Around Solution: Add one dummy cell at indexPath.row == 0 with height as 1 pixel and enable the focus on it.

Reset scroll on UICollectionView

I have a horizontal UICollectionView which works fine and scrolls. When I tap an item I update my data and call reloadData. This works and the new data is displayed in the UICollectionView.
The problem is the scroll position doesn't change and it is still viewing the last place. I want to reset the scrolling to the top (Or left in my case). How can I do this?
You want setContentOffset:. It takes a CGPoint as and argument that you can set to what ever you want using CGPointMake, but if you wish to return to the very beginning of the collection, you can simply use CGPointZero.
[collectionView setContentOffset:CGPointZero animated:YES];
You can use this method to scroll to any item you want:
- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath
atScrollPosition:(UICollectionViewScrollPosition)scrollPosition
animated:(BOOL)animated
I use this quite often in different parts of my app so I just extended UIScrollView so it can be used on any scroll view and scroll view subclass:
extension UIScrollView {
/// Sets content offset to the top.
func resetScrollPositionToTop() {
self.contentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
}
}
So whenever I need to reset the position:
self.collectionView.resetScrollPositionToTop()
In Swift:
collectionView.setContentOffset(CGPointZero, animated: true)
If you leave the view that houses the collection view, make a change in the 2nd view controller, and need the collection view to update upon returning:
#IBAction func unwindTo_CollectionViewVC(segue: UIStoryboardSegue) {
//Automatic table reload upon unwind
viewDidLoad()
collectionView.reloadData()
collectionView.setContentOffset(CGPointZero, animated: true)
}
In my situation the unwind is great because the viewDidLoad call will update the CoreData context, the reloadData call will make sure the collectionView updates to reflect the new CoreData context, and then the contentOffset will make sure the table sets back to the top.
If you view controller contains safe area, code:
collectionView.setContentOffset(CGPointZero, animated: true)
doesn't work, so you can use universal extension:
extension UIScrollView {
func scrollToTop(_ animated: Bool) {
var topContentOffset: CGPoint
if #available(iOS 11.0, *) {
topContentOffset = CGPoint(x: -safeAreaInsets.left, y: -safeAreaInsets.top)
} else {
topContentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
}
setContentOffset(topContentOffset, animated: animated)
}
}
CGPointZero in my case shifted the content of the collection view because you are not taking in count the content inset. This is what it worked for me:
CGPoint topOffest = CGPointMake(0,-self.collectionView.contentInset.top);
[self.collectionView setContentOffset:topOffest animated:YES];
Sorry I couldn't comment at the time of this writing, but I was using a UINavigationController and CGPointZero itself didn't get me to the very top so I had to use the following instead.
CGFloat compensateHeight = -(self.navigationController.navigationBar.bounds.size.height+[UIApplication sharedApplication].statusBarFrame.size.height);
[self.collectionView setContentOffset:CGPointMake(0, compensateHeight) animated:YES];
Hope this helps somebody in future. Cheers!
To deal with an UINavigationController and a transparent navigation bar, I had to calculate the extra top offset to perfectly match the position of the first element.
Swift 1.1 code:
let topOffest = CGPointMake(0, -(self.collectionView?.contentInset.top ?? O))
self.collectionView?.setContentOffset(topOffest, animated: true)
Cheers!
SWIFT 4.2 SOLUTION-
Say I have a tableView with each tableViewCell containing a HorizontalCollectionView
Due to reuse of cells, even though first tableViewCell is scrolled to say page 4, downwards, other cell is also set to page 4.
This works in such cases-
In the cellForRowAt func in tableView,
let cell = //declaring a cell using deque
cell.myCollectionView.contentOffset = CGPoint(x:0,y:0)