Swift: Pull to refresh is too sensitive - ios

am not too sure if anyone has encountered this issue, but it seems that my refreshControl is too sensitive. In my implementation, I attempt to do a pull to refresh and the code executes even before I see the refreshControl spinning.
Is there something wrong I'm doing?
My implementation so far.
//At initiation
lazy var refreshControl: UIRefreshControl = {
let rc = UIRefreshControl()
rc.tintColor = .white
rc.addTarget(self, action: #selector(refreshControlDidRefresh), for: .valueChanged)
return rc
}()
//At viewDidLoad(), I call a function setupViews()
func setupViews() {
view.backgroundColor = .white
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.register(GossipsTableViewCell.self, forCellReuseIdentifier: "gossipCell")
tableView.tableHeaderView = emptyTableViewHeader()
tableView.refreshControl = refreshControl
}
#objc func refreshControlDidRefresh() {
DispatchQueue.main.async {
self.gossips.removeAll()
self.tableView.reloadData()
}
getData()
}
func getData() {
FirebaseClient.shared.fetchAllGossips { (results, error) in
if let error = error {
print(error.localizedDescription)
} else if let results = results {
self.gossips = results.sorted(by: { (gossip1, gossip2) -> Bool in
guard
let gossip1Timestamp = gossip1.timestamp,
let gossip2Timestamp = gossip2.timestamp else {
return false
}
return gossip1Timestamp > gossip2Timestamp
})
self.filterGossips()
DispatchQueue.main.async {
if self.refreshControl.isRefreshing {
self.refreshControl.endRefreshing()
}
if JustHUD.shared.isActive {
JustHUD.shared.hide()
}
var indexPaths = [IndexPath]()
for (index, _) in self.gossips.enumerated() {
let indexPath = IndexPath(row: index, section: 0)
indexPaths.append(indexPath)
}
self.tableView.insertRows(at: indexPaths, with: .automatic)
self.segmentedControl.isUserInteractionEnabled = true
}
}
}
}
You may see the results here. As I attempt to pull down, the refresh gets executed even before I start to see the refreshControl being drawn. Is there anything I'm missing out?

You are doing everything correctly, this is the default behaviour of pull to refresh control. If you want it to act differently, try a 3rd-part solution or remove the control and implement your own logic in scrollViewDidScroll.

Related

UIVIewController Not Getting Deinitialized When Popping

I am building a settings screen in an app where I have a list of cells. If a user taps on a cell it pushes another controller onto the stack. However, I have this flow in several places in my app.
Therefore, I decided to reuse a generic controller and initialize it with sections (depending on which cell was tapped)
However, when popping a UIViewController it isn't getting deinitialized
VIEW CONTROLLER CODE
// Class
class ProfileController: UIViewController {
private let authService: AuthSerivce
private let sections: [FormSectionComponent]
init(authService: AuthSerivce,
sections: [FormSectionComponent]) {
self.authService = authService
self.sections = sections
super.init(nibName: nil, bundle: nil)
}
}
// Cell Delegate
extension ProfileController: NavigateCellDelegate {
func navigate(cell: NavigateCell) {
guard let sections = cell.item?.components else { return }
let controller = ProfileController(authService: authService, sections: sections)
self.navigationController?.pushViewController(controller, animated: true)
}
}
CELL CODE
protocol NavigateCellDelegate {
func navigate(cell: NavigateCell)
}
class NavigateCell: UICollectionViewCell {
var item: NavigateComponent?
var delegate: NavigateCellDelegate?
lazy var titleLabel: UILabel = {
let view = UILabel()
view.numberOfLines = 0
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func bind(_ item: FormItemComponent) {
guard let item = item as? NavigateComponent else { return }
self.item = item
setUpView(item: item)
addTapGestureRecogniser()
}
func addTapGestureRecogniser() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapGesture))
self.addGestureRecognizer(tapGesture)
self.isUserInteractionEnabled = true
}
#objc func tapGesture() {
delegate?.navigate(cell: self)
}
override func prepareForReuse() {
super.prepareForReuse()
titleLabel.text = ""
}
}
extension NavigateCell {
func setUpView(item: NavigateComponent) {
titleLabel.text = item.title
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
} // END
UPDATED WEAK DELEGATE IN CELL
protocol NavigateCellDelegate: AnyObject {
func navigate(cell: NavigateCell)
}
class NavigateCell: UICollectionViewCell {
weak var item: NavigateComponent?
weak var delegate: NavigateCellDelegate?
Figured it out - The problem was with my DiffableDataSource and not declaring [weak self]
return UICollectionViewDiffableDataSource(collectionView: profileView.collectionView) { [weak self] collectionView, indexPath, item in

Spinner don't want to remove from superview

I have a UIActivityIndicatorView so when I try to search in search bar, It will loading first before the data come in. I make the spinner in UIViewController extension for animating and remove the animating from the view. but somehow I can't figure it out why the view I want to remove is not removing, when I try to use break point. the function dismissLoading I create is not being called, why it happen? can someone help me. this is my code
fileprivate var containerView: UIView!
extension UIViewController {
func showLoadingView() {
containerView = UIView(frame: view.bounds)
view.addSubview(containerView)
containerView.backgroundColor = .systemBackground
containerView.alpha = 0.0
UIView.animate(withDuration: 0.25) { containerView.alpha = 0.8 }
let activityIndicator = UIActivityIndicatorView(style: .large)
containerView.addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
activityIndicator.startAnimating()
}
func dismissLoadingView() {
DispatchQueue.main.async {
containerView.removeFromSuperview()
containerView = nil
}
}
}
// This is my PhotosViewController
extension PhotosViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text, !searchText.isEmpty else {
dataSource.data.value.removeAll()
viewModel.isSearching.value = false
return
}
photoName = searchText
viewModel.isSearching.value = true
showLoadingView()
if let photoName = photoName {
viewModel.getPhoto(query: photoName) { [weak self] error in
self?.dismissLoadingView()
print(error)
}
}
}
}
You're not seeing it disappear because you're adding a new instance of the container view and activity indicator every time the search query changes. If the query updates a second time before the first query has returned results, the reference to your first containerView will be reassigned to a new view instance.
If you attempt to fix this by checking if containerView == nil before presenting, you will also run into issues if you have multiple search results requests out, one returns, and then the loading view gets dismissed despite there being other requests out.

Pull to refresh and activity indicator on a view

I've a VC containing Table View. I've implemented pull to refresh on table view and activity indicator on view. On pull to refresh table view flickers as it looks like it has finished refreshing and reloads while I pull down and keep holding the table view. I'm not sure where it's going wrong. Following is my code. How to I implement it correctly?
class MyViewController: CustomVC {
override func viewDidLoad() {
super.viewDidLoad()
self.addRefreshControl(to: tableView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.view.activityStartAnimating()
self.tableView.isHidden = true
loadContent(isRefresh: false)
}
private func loadContent(isRefresh: Bool) {
fetchContent() { (result, error) in
// ....
if isRefresh {
self.refreshControl.endRefreshing()
} else {
self.view.activityStopAnimating()
}
// ...
self.tableView.reloadData()
self.tableView.isHidden = false
}
}
// Pull to refresh
override func fetchDataForRefresh() {
loadContent(isRefresh: true)
}
}
Custom VC class
class CustomVC: UIViewController {
lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refresh(_:)), for: UIControl.Event.valueChanged)
refreshControl.transform = CGAffineTransform(scaleX: 0.87, y: 0.87)
return refreshControl
}()
override func viewDidLoad() {
super.viewDidLoad()
}
func addRefreshControl(to tableView: UITableView) {
if #available(iOS 10.0, *) {
tableView.refreshControl = refreshControl
} else {
tableView.addSubview(refreshControl)
}
tableView.indicatorStyle = .white
}
#objc func refresh(_ refreshControl: UIRefreshControl) {
fetchDataForRefresh()
}
func fetchDataForRefresh() {
}
}
Fetch Content
func fetchContent() {
// .....
DispatchQueue.main.async {
completed(resultData,nil)
return
}
//....
}
Try this technique
func setupRefreshControl() {
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshData), for: .valueChanged)
tableView?.refreshControl = refreshControl
}
When you finished fetching your data:
DispatchQueue.main.async {
self.refreshControl?.perform(#selector(UIRefreshControl.endRefreshing), with: nil, afterDelay: 0)
self.tableView.reloadData()
}
Please note I have access to refreshControl because I'm inside a UITableViewController but you can easily add it in a UIViewController as well.
Remove the if refresh else ... from your code and put your reload your UITableView and endRefreshing on the main thread as I mentioned above

Search bar issue ios 11, refresh control

Im adding refresh control to tableView.
then start animate it from code dosent show on top. I must scroll tableView to down.
And after this i add Search controll to navbaritem then its look like:
here is code
lazy var customRefreshControl: UIRefreshControl = {
let control = UIRefreshControl()
control.attributedTitle = NSAttributedString(string: "Downloading Locations")
control.addTarget(self, action: #selector(updateLocation), for: .valueChanged)
return control
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.largeTitleDisplayMode = .automatic
searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.tintColor = .white
searchController.searchBar.delegate = self
tableView.addSubview(customRefreshControl)
title = "location_groups".localized()
tableView.tableFooterView = UIView()
tableView.backgroundColor = .groupTableViewBackground
navigationItem.searchController = searchController
searchController.dimsBackgroundDuringPresentation = false
}
func loadLocationGroups() {
DispatchQueue.global(qos: .userInteractive).async { [unowned self] in
self.locationGroups = DataBaseManager.Instance.GetLocations()
DispatchQueue.main.async {
self.tableView.reloadData()
if self.locationGroups.isEmpty {
self.customRefreshControl.attributedTitle = NSAttributedString(string: "Downloading Locations")
} else {
self.customRefreshControl.attributedTitle = NSAttributedString(string: "Synchronize")
}
self.customRefreshControl.beginRefreshing()
}
self.request(requestType: .getLocationGroups)
}
}

UIRefreshControl spins too fast

I want to trigger refreshControl to show automatically when a tableView is initializing its data from server, and I have tried several ways in this question
UIRefreshControl - beginRefreshing not working when UITableViewController is inside UINavigationController
The refreshControl did show when the data is being initialized at the begining, but it spins too fast(maybe 2x the normal speed).
Does anyone know the reason and how to solve this problem?
Here is the code
override func viewDidLoad() {
super.viewDidLoad()
networkManager = BookNetworkManager(userAccount: userAccount)
HUD = JGProgressHUD.standard
refreshControl!.addTarget(self, action: #selector(refreshData), for: .valueChanged)
tableView.tableFooterView = UIView()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
delegate?.setNavgationItem(status: .none)
if !loaded {
tableView.setContentOffset(CGPoint(x: 0, y: -self.refreshControl!.height - self.topLayoutGuide.length), animated: true)
refreshControl!.beginRefreshing()
refreshData()
loaded = true
}
}
func refreshData() {
delegate?.hidePlaceHolder(type: .history)
refreshControl!.attributedTitle = NSAttributedString(string: "刷新数据中")
networkManager.getHistoryBook()
{ (response, errorInfo, books) in
if response == .success {
self.historyBookList = books
self.historyBookList.sort{ $0.returnDateTimeInterval > $1.returnDateTimeInterval }
self.tableView.reloadData()
if books.isEmpty {
self.delegate?.showPlaceHolder(message: "还没有借阅过书籍噢", for: .history)
}else{
self.delegate?.hidePlaceHolder(type: .history)
}
}else {
print(errorInfo?.errorDescription ?? response.rawValue)
self.HUD.show(message: errorInfo?.errorDescription ?? response.rawValue, time: 2)
self.delegate?.showPlaceHolder(message: "出错啦,下拉刷新一下吧?", for: .history)
}
self.refreshControl!.endRefreshing()
self.refreshControl!.attributedTitle = NSAttributedString(string: "下拉刷新数据")
}
}

Resources