I have a UITableView, that on the delegate of the UIViewController that adds a new item, inserts the row. The row gets correctly inserted.
However, the UIRefreshControl on the UITableView will not go away when dragging the UITableView after that.
extension FeedViewController: AddPostDelegate {
func didAddPost(_ userPost: UserPost) {
self.postsWrapper?.posts.insert(PostWrapper(type: PostType.user(userPost)), at: 0)
self.tableView.beginUpdates()
self.tableView.insertRows(at: [
IndexPath(row: 1, section: 0)
], with: .automatic)
self.tableView.endUpdates()
self.refresher.endRefreshing()//Does nothing
}
}
In viewDidLoad:
refresher = UIRefreshControl()
tableView.addSubview(refresher)
refresher.addTarget(self, action: #selector(didDragScrollView), for: .valueChanged)
refresher.layer.zPosition = -1//otherwise in front of the header cell when refreshing (iOS 9.2)
If I do not go to another view first than come back before attempting to pull for refresh, it always hangs spinning forever.
EDIT:
It looks like the UIRefreshControl is no longer calling the target function AFTER I add a post.
Any ideas on why this may be occurring? How to fix that?
Make sure that beginRefreshing() was not called before the user pulled to refresh. If refreshControl.isRefreshing is true then the selector does not get called upon "pull to refresh".
A simple test:
let refreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
refreshControl.addTarget(self, action: #selector(ViewController.refreshData), for: .valueChanged)
tableView.refreshControl = refreshControl
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// sets refreshControl.isRefreshing to true
// before the user initiates it
refreshControl.beginRefreshing()
}
Then the user pulls to refresh. Because refreshControl.isRefreshing is true the selector will not get called. If you remove refreshControl.beginRefreshing()in viewDidAppear, the selector will get called:
#objc func refreshData() {
print("refresh initiated")
refreshControl.endRefreshing()
}
Try assigning your UIRefreshControl into the refreshControl property of UITableView like this:
if #available(iOS 10.0, *) {
tableView.refreshControl = refresher
} else {
tableView.addSubview(refresher)
}
Note that this requires iOS 10.0 or higher, but we're soon getting iOS 12.0 so I don't think you should support iOS lower than 10.0
Related
I'm trying to add pull to refresh on my wkwebview app. I'm using UIViewRepresentable for my webview, so I don't have the onViewLoad function and view controllers. Here's my code:
var wbWebViewEl: WKWebView = WKWebView()
struct SwiftUiWebView: UIViewRepresentable {
....
class refreshWebViewClass {
#objc func refreshWebView1(sender: UIRefreshControl) {
print("test debug :D")
wbWebViewEl.reload()
DispatchQueue.main.async {
sender.endRefreshing()
}
}
func makeUIView(context: Context) -> WKWebView {
......
wbWebViewEl.scrollView.bounces = true
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshWebViewClass.refreshWebView1), for: UIControl.Event.valueChanged)
wbWebViewEl.scrollView.addSubview(refreshControl)
wbWebViewEl.scrollView.refreshControl = refreshControl
.....
}
.....
}
At the moment, when you swipe down, it shows the refresh loading spinner, but it doesn't stop spinning. Also the test print I put in the refreshWebView1 function, doesn't show in the logcat. Does anyone know what I'm getting wrong here? If you need to see anymore code let me know :)
Looks like your refreshWebViewClass should be a Coordinator. Then, in your addTarget, the target would be context.coordinator, not self, which doesn't make sense here, since self doesn't actually implement this method.
I have used UITableView.refreshControl with large titles. I am trying to mimic the way the native Mail app works with pull to refresh. The refresh control, for me, moves and doesn't stay stuck at the top of the screen like the Mail app does. Here is a playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
public init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var refresh: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.backgroundColor = .clear
refreshControl.tintColor = .black
refreshControl.addTarget(self, action: #selector(refreshIt), for: .valueChanged)
return refreshControl
}()
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.refreshControl = refresh
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
private var data: [Int] = [1,2,3,4,5]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// Add tableview
addTableView()
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "View Controller"
}
private func addTableView() {
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
#objc func refreshIt(_ sender: Any) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.refresh.endRefreshing()
self.data = [6,7,8,9,10]
self.tableView.reloadData()
}
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Row \(data[indexPath.row])"
return cell
}
}
let vc = ViewController()
let nav = UINavigationController(rootViewController: vc)
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = nav
How can I get the spinner to stick to the top, and have it behave like the Mail app does? Also the navigation title does this weird thing where it moves back slower than the table data and causes some overlap to happen... Any help is appreciated!
EDIT JUNE 7, 2020
I switched to using a diffable datasource which fixes the overlap animation, however, there is still this weird glitch right when the haptic feedback occurs and causes the spinner to shoot up to the top and back down, so my original question still remains - how do we get the spinner to stay pinned at the top like the mail app. Thanks for taking a look at this unique issue :)!!!
The reason is in used reloadData which synchronously drops & rebuilds everything thus breaking end refreshing animation.
The possible solution is to give time to end animation and only then perform reload data.
Tested with Xcode 11.4 / Playground
Modified code:
#objc func refreshIt(_ sender: Any) {
// simulate loading
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.refresh.endRefreshing() // << animating
// simulate data update
self.data = [6,7,8,9,10]
// forcefully synchronous, so delay a bit
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.tableView.reloadData()
}
}
}
Alternate approach, if you known modified data structure, would be to use beginUpdate/endUpdate pair
#objc func refreshIt(_ sender: Any) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.refresh.endRefreshing()
self.data = [6,7,8,9,10]
// more fluent, but requires knowledge of what is modified,
// in this demo it is simple - first section
self.tableView.beginUpdates()
self.tableView.reloadSections(IndexSet([0]), with: .automatic)
self.tableView.endUpdates()
}
}
I don't really know how to answer the first question, but since you asked 2 questions I'll go ahead and try to help you with the second in the meanwhile.
The problem with the rows getting to the top too fast is due to the use of tableView.reloadData().
That is always done without animation, so what is happening is to be expected.
What you could do is to use other methods on the table view, like insert or delete rows like this:
#objc func refreshIt(_ sender: Any) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.refresh.endRefreshing()
let newData = [6,7,8,9,10]
let newIndexPaths = newData.enumerated().map{IndexPath(row: self.data.count+$0.offset, section: 0)}
self.data.append(contentsOf: newData)
self.tableView.insertRows(at: newIndexPaths, with: .automatic)
}
}
In this way the rows will be added with an animation and the scroll down animation won't be interrupted by any reloadData().
If you don't have only rows to add (or delete) and maybe have to do some sort of combination of both, then you can use the performBatchUpdates method to do both add and delete in the same animation block. (iOS 11+)
That's, of course, only viable if you are able to calculate the indexes of the items to add and delete. Keep in mind that the amount of rows you add and delete should always be reflected by the actual change in your data structure, or you'll have a nasty exception thrown by the table view.
Anyway that should always be feasible, but if you want it out of the box you could use iOS 13 diffable data source or, if you want it even before iOS 13, you could use instagram's IGListKit.
Both pretty much use the same concept of diffing your array of input and automatically detect deletions and inserts. So changing the array will automatically reflect in the animated change in your tableView rows. Of course both of them are very different ways of implementing a tableView, so you really can't just change a little piece of your code to make it work.
Hope this can help.
I think the problems is tableView.reloadData(). You can change it to tableView.beginUpdates() and tableView.endUpdates()
In this demo, I will create an extension for easier for tableView to reload with beginUpdate and enUpdates.
extension UITableView {
func reloadWithNewData<T:Equatable>(_ newDatas : [T], _ oldDatas : [T],_ animation : UITableView.RowAnimation) {
var deleteOnes : [IndexPath] = []
var appendOnes : [IndexPath] = []
for i in 0 ..< oldDatas.count {
for j in 0 ..< newDatas.count {
if newDatas[j] == oldDatas[i] {
break
}
if j == newDatas.count - 1 {
deleteOnes.append(IndexPath(row: i, section: 0))
}
}
}
for i in 0 ..< newDatas.count {
for j in 0 ..< oldDatas.count {
if newDatas[i] == oldDatas[j] {
break
}
if j == oldDatas.count - 1 {
appendOnes.append(IndexPath(row: i, section: 0))
}
}
}
self.beginUpdates()
self.deleteRows(at: deleteOnes, with: animation)
self.insertRows(at: appendOnes, with: animation)
self.endUpdates()
}
}
Then you just need to change for function refreshIt()
#objc func refreshIt(_ sender: Any) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.refresh.endRefreshing()
let oldData = self.data
self.data = [6,7,8,9,10]
self.tableView.reloadWithNewData(self.data, oldData, .fade)
}
}
Happy coding
When i want to the update data source of the tableview, firstly i want to scroll to top (to header view) then show the UIRefreshControl view, after data source updated then i want to hide the UIRefreshControll.
To do that i tried following line:
self.kisilerTable.scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: false)
self.kisilerTable.setContentOffset(CGPoint(x: 0, y: self.kisilerTable.contentOffset.y - (self.refreshControl.frame.size.height)), animated: true)
self.kisilerTable.refreshControl?.beginRefreshing()
Above code is scrolling to top then it shows the UIRefreshControl. And after my data updated i tried to hide it with following code:
self.kisilerTable.reloadData()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.kisilerTable.refreshControl?.endRefreshing()
}
But the problem is, above code is hiding the indicator animation but it leaves a space there. As you can see in the screenshot:
If i try to hide with following code:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.kisilerTable.reloadData()
self.kisilerTable.refreshControl?.endRefreshing()
}
It is hiding but, i am losing the hide animation. I don't want to lose hide animation.
How can i resolve this problem?
I think it happens, because in the beginning you use setContentOffset to leave a space, but that space is not removed.
I will suggest another implementation, which works very well:
First you declare the refreshControll before the method viewDidLoad():
let refreshControl = UIRefreshControl()
Now do you need create a method for stop the animation when needed:
func stopAnimatingRefreshControl() {
if let refreshControl =
tableView.subviews.first(where: { $0 is UIRefreshControl})as? UIRefreshControl,
refreshControl.isRefreshing {
refreshControl.endRefreshing()
}
}
Now you create an #objc function that be called when scrolls the table to update, normally in this moment you call methods that make your server request to update data.
When this data is updated, you call the function stopAnimatingRefreshControl():
#objc func refreshMyData() {
// call methods for get data from server
getDataFromServer()
}
func getDataFromServer() {
// request completed and data is updated, now i need stop the loading.
DispatchQueue.main.async {
self.tableView.reloadData()
}
stopAnimatingRefreshControl()
}
Finally, after creating the necessary methods, you need to link our #objc function to refreshControll and our tableView, do it in method viewDidLoad() :
refreshControl.addTarget(self, action: #selector(refreshMyData), for: .valueChanged)
tableView.addSubview(refreshControl)
Now you're done, I hope I helped.
I have a table in view "A" that is inside a tab bar controller. When I scroll down to reload the table refresh control start animating.When press tab bar controller going in view "B" and later go back to view "A", refresh control is visible but isn't animating.
Can someone help me? thank
func refresh(sender:AnyObject)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
Call API for data
dispatch_async(dispatch_get_main_queue(), { () -> Void in
UPDATE UI
refreshControl.endRefreshing()
})
});
}
override func viewDidAppear(animated: Bool)
{
if(requestTerminated == false)
{
//Continue animate refresh control somehow
}
}
Suppose you have this situation in your controller A , for example in viewDidLoad:
//global var
var refreshControl:UIRefreshControl!
self.refreshControl = UIRefreshControl()
self.refreshControl.attributedTitle = NSAttributedString(string: "pull to refresh")
self.refreshControl.addTarget(self, action: #selector(MyTableViewController.pullToRefresh(_:)), forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refreshControl)
As can you see you have your refreshControl add as a subview to your A table controller. When you go to another controller probably you present another controller (B) and when you come back to A your refresh is freezed or stucked.
You could start UIRefreshControl animation on viewWillAppear and end it on viewDidDisappear. During transition save the state of refresh process to know when to show UIRefreshControl.
You can use a boolean like this:
// Refresh state
var isRefreshing = false
// Call on viewWillApper
func superviewWillApper(){
if isRefreshing && !refreshControl.refreshing{
startRefreshAnimation()
}
}
// Call on viewDidDisapper
func superviewDidDisappear(){
endRefreshAnimation(false, dataFetched: !isRefreshing)
}
func startRefreshAnimation(){
refreshControl.beginRefreshing()
contentOffset = CGPointMake(0, -refreshControl.bounds.size.height)
isRefreshing = true
}
//Saves state of refresh
func endRefreshAnimation(wasEmpty: Bool, dataFetched: Bool){
refreshControl.endRefreshing()
isRefreshing = !dataFetched
if !wasEmpty{
setContentOffset(CGPointZero, animated: true)
}else{
setContentOffset(CGPointZero, animated: false)
}
}
I am trying to show a refresh control right when the view loads to show that I am getting getting data from Parse. The refresh control works as it should when the app is running, but I cannot get it to fire programmatically from anywhere in my app.
This is the code that does not appear to run:
override func viewDidAppear(animated: Bool) {
self.refresher.beginRefreshing()
}
Not only does this not run, but having the code in the app changes the attributes of the refresh control. When I have this code in the app, and show the refresher from user interaction, the refresh control does not have its attributed title as it usually does nor does it run the code that it should.
I needed to add a delay between setContentOffset and the self.refreshControl?.sendActions(for: .valueChanged) call. Without the delay, the refreshControl's attributedTitle would not render.
This works in iOS 10 (Swift 3):
self.tableView.setContentOffset(CGPoint(x:0, y:self.tableView.contentOffset.y - (self.refreshControl!.frame.size.height)), animated: true)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: {
self.refreshControl?.sendActions(for: .valueChanged)
})
Here's an extension working on 10.3:
extension UIRefreshControl {
func refreshManually() {
if let scrollView = superview as? UIScrollView {
scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height), animated: false)
}
beginRefreshing()
sendActions(for: .valueChanged)
}
}