Vadim
Vadim

Reputation: 335

Swift, loading more data into table view causes scroll lags

I have a UITableView with data, that parsed from url.

UITableView will load more data, when scrolled right to the bottom (or got some more space to scroll, but close to the end - did both, same results)

When more data is loaded - i simple append it to array of my class, that contains data for TableView and then list scrolls back more than half of list (e.g. got 40 items, loading 10 more -> scrolling back to 20-25).

Calling TableView.reloadData() after append is complete.

Is there some mistakes in plan of doing it? I can share code, but it's pretty common.

class TabAllTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, XMLParserDelegate {

@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var BlogAllTableView: UITableView!

var loadMoreStatus = false
var maxPage = 1.0
var currentPageLoad = 1.0

var blogList: [DetailAndRenderBlogObject] = []
var eName: String = String()
var category = String()
var full_text = String()
var short_text = String()
var blog_title = String()
var avatar = String()
var full_name = String()
var url = String()
var created_at = String()

private let CATEGORY = ConfigData.CATEGORY_ALL

let cellIdentifier = "BlogTableViewCell"

override func viewDidLoad() {
    super.viewDidLoad()
    setupNavMenuButtons()

    self.BlogAllTableView.insertSubview(refreshControl, at: 0)

    self.BlogAllTableView.tableFooterView?.isHidden = true

    downloadBlogData(1, true, true)

    BlogAllTableView.delegate = self
    BlogAllTableView.dataSource = self
}

var refreshControl: UIRefreshControl = {
    let refreshControl = UIRefreshControl()
    refreshControl.addTarget(self, action:
        #selector(TabAllTableViewController.handleRefresh(_:)),
                             for: UIControlEvents.valueChanged)
    refreshControl.tintColor = Colors.ColorLoadingIndicator

    return refreshControl
}()

@objc func handleRefresh(_ refreshControl: UIRefreshControl) {
    downloadBlogData(1, true, false)
}

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let currentOffset = scrollView.contentOffset.y
    let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
    let deltaOffset = maximumOffset - currentOffset

    if deltaOffset <= 0 && currentPageLoad < maxPage {
        loadMore()
    }
}

func loadMore() {
    if ( !loadMoreStatus ) {
        self.loadMoreStatus = true
        self.activityIndicator.startAnimating()
        self.BlogAllTableView.tableFooterView?.isHidden = false
        downloadBlogData(currentPageLoad, false, false)
    }
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.sideMenuController?.isLeftViewEnabled = true
    AppDelegate.tabBarReference.isHidden = false
}

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    self.navigationController?.navigationBar.topItem?.title = "all".localized()
}

private func setupNavMenuButtons() {
    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(handleMenuRefresh))

    let image = UIImage(named:"menu_ham.png")
    let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
    imageView.contentMode = .scaleAspectFit
    imageView.isUserInteractionEnabled = true
    imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    imageView.image = image

    let imageHeight = navigationController?.navigationBar.frame.size.height
    let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: imageHeight!, height: imageHeight!))
    wrapperView.addSubview(imageView)

    imageView.center = CGPoint(x: imageView.frame.size.width / 2, y: wrapperView.frame.size.height / 2)

    let tap = UITapGestureRecognizer(target: self, action: #selector(TabAllTableViewController.menuButtonClick))
    wrapperView.addGestureRecognizer(tap)

    let btnHamburgerMenu: UIBarButtonItem = UIBarButtonItem(customView: wrapperView)
    navigationItem.setLeftBarButton(btnHamburgerMenu, animated: false)
}

@objc private func menuButtonClick()
{
    self.sideMenuController?.showLeftViewAnimated()
}

@objc private func handleMenuRefresh() {
    downloadBlogData(1, true, true)
}

func numberOfSections(in tableView: UITableView) -> Int {
    return blogList.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 1
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    if (section != 0) {
        return 12
    } else {
        return CGFloat.leastNonzeroMagnitude
    }
}

func tableView(_ tableView: UITableView,  viewForHeaderInSection section: Int) -> UIView? {
    let view = UIView()
    view.backgroundColor = UIColor.black.withAlphaComponent(0.0)
    return view
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return CGFloat(ConfigData.CELL_HEIGHT)
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    // Configure the cell...

    guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? BlogTableViewCell  else {
        fatalError("The dequeued cell is not an instance of BlogTableViewCell.")
    }

    cell.layer.cornerRadius = 10

    let blogItem = blogList[indexPath.section]

    cell.avatarBackground.contentMode = .scaleAspectFill
    cell.avatarBackground.layer.cornerRadius = cell.avatarBackground.frame.size.width / 2;
    cell.avatarBackground.clipsToBounds = true;


    if let authorImgUrl = URL(string: blogItem.authorImg) {
        cell.authorRoundImage.contentMode = .scaleAspectFill
        cell.authorRoundImage.layer.cornerRadius = cell.authorRoundImage.frame.size.width / 2;
        cell.authorRoundImage.clipsToBounds = true;
        //Helper.downloadImage(url: authorImgUrl, imageview: cell.authorRoundImage)
        cell.authorRoundImage.sd_setImage(with: authorImgUrl)
    }

    if let headerImageUrl = URL(string: blogItem.image) {
        cell.bigImage.contentMode = .scaleToFill
        //Helper.downloadImage(url: headerImageUrl, imageview: cell.bigImage)
        cell.bigImage.sd_setImage(with: headerImageUrl)
    }

    cell.authorLabel.text = blogItem.author
    cell.dateLabel.text = blogItem.date
    cell.title.text = blogItem.title
    cell.titleDescription.text = blogItem.shortDescription

    return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let secondViewController = self.storyboard!.instantiateViewController(withIdentifier: "ShowArticleViewController") as! ShowArticleViewController

    secondViewController.blogItem = blogList[indexPath.section]

    self.navigationController!.pushViewController(secondViewController, animated: true)
}

private func downloadBlogData(_ page : Double, _ refresh : Bool, _ showOverlay : Bool) {
   // print(InAppProperties.sharedInstance.getDefaulLang())

    if showOverlay {
        LoadingOverlay.shared.showOverlay(view: self.view)
    }

    let req = NSMutableURLRequest(url: NSURL(string: Helper.getBlogUrl(CATEGORY, Int(page)))! as URL)
    req.httpMethod = "GET"
    req.httpBody = "key=\"value\"".data(using: String.Encoding.utf8) //This isn't for GET requests, but for POST requests so you would

    URLSession.shared.dataTask(with: req as URLRequest) { data, response, error in
        if error != nil {
            //Your HTTP request failed.
            print(error?.localizedDescription)
        } else {
            //Your HTTP request succeeded

            if refresh {
                self.currentPageLoad = 1
            } else {
                self.currentPageLoad += 1
            }

            let stringData = String(data: data!, encoding: String.Encoding.utf8)!

            //print(stringData)

            let xml = SWXMLHash.parse(stringData)

            if page == 1 {
                self.blogList = [DetailAndRenderBlogObject]()
            }

            for elem in xml["response"]["provider"].all {
                let itemList = elem["item"]

                for article in itemList.all {
                    let blogItem = DetailAndRenderBlogObject()

                    blogItem.articleUrl = article["url"].element!.text
                    blogItem.author = article["full_name"].element!.text
                    blogItem.authorImg = article["avatar"].element!.text
                    blogItem.text = article["full_text"].element!.text
                    blogItem.shortDescription = article["short_text"].element!.text
                    blogItem.title = article["title"].element!.text
                    blogItem.categoryUrl = article["category"].element!.text
                    blogItem.image = self.repairLink(article["thumbnail"].element!.text)
                    blogItem.date = self.formatDate(article["created_at"].element!.text)

                    if (blogItem.categoryUrl.lowercased().range(of:"video") == nil &&
                        blogItem.title.lowercased().range(of: "видео") == nil) {

                        self.blogList.append(blogItem)
                    }
                }

                if let totalItemsCount = xml["response"]["pagination"]["totalCount"].element?.text {
                    self.maxPage = (Double(totalItemsCount)! / 10).rounded(.up)
                }
            }

            DispatchQueue.main.async {
                self.BlogAllTableView.reloadData()
                self.refreshControl.endRefreshing()

                self.loadMoreStatus = false
                self.activityIndicator.stopAnimating()
                self.BlogAllTableView.tableFooterView?.isHidden = true

                if showOverlay {
                    LoadingOverlay.shared.hideOverlayView()
                }

                if (page == 1) {
                    let indexPath = NSIndexPath(row: 0, section: 0)
                    self.BlogAllTableView.scrollToRow(at: indexPath as IndexPath, at: .top, animated: true)
                }

            }
        }
        }.resume()
}

func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
    eName = elementName

    if elementName == "item" {
        category = String()
        full_text = String()
        short_text = String()
        blog_title = String()
        avatar = String()
        full_name = String()
        url = String()
        created_at = String()
    }
}

func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {

    if elementName == "item" {
        let blogListItem = DetailAndRenderBlogObject()

        blogListItem.categoryUrl = category
        blogListItem.text = full_text
        blogListItem.shortDescription = short_text
        blogListItem.title = blog_title
        blogListItem.authorImg = avatar
        blogListItem.author = full_name
        blogListItem.articleUrl = url
        blogListItem.date = created_at

        print("WRITING DATA")

        blogList.append(blogListItem)
    }
}

func parser(_ parser: XMLParser, foundCharacters string: String) {

    //print("ENAME: " + eName)
    switch eName {
    case "category":
        print("writing category: " + string)
        category = string;
    case "full_text":
        full_text = string;
    case "short_text":
        short_text = string;
    case "title":
        title = string;
    case "thumbnail":
        avatar = string;
    case "avatar":
        avatar = string;
    case "full_name":
        full_name = string;
    case "url":
        url = string;
    case "created_at":
        created_at = string;
    default:
        ()
    }
}

func parserDidEndDocument(_ parser: XMLParser) {
}

func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
    print(parseError)
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

func repairLink(_ link : String) -> String { //sometimes links looks like https://asdasd.comhttps://asdasd.com/...

    let newLink = link.suffix(from: link.index(link.startIndex, offsetBy: 1))

    if let range = newLink.range(of: "http") {
        let substring = newLink[range.lowerBound...]
        return String(substring)
    }
    return link
}

func formatDate(_ date : String) -> String {  //date    String    "2018-01-22 08:59:43"
    if let dividerIndex = date.index(of: " ") {
        return String(date[..<dividerIndex])
    }

    return date
}


}

Upvotes: 3

Views: 1679

Answers (2)

gcharita
gcharita

Reputation: 8327

I had the same issue and found another solution, after long time search.

It seems that whenever you call the endRefreshing() function of an UIRefreshControl that is attached to a UITableView scrollview, it stops the decelerating process of the scroll view.

So, if your code, that fetches the next page, is calling endRefreshing() function (with no reason at all) and the user started the scrolling and lifted his/her finger from the device's screen (scrollview is decelerating) this will have as a result to stop the scrolling.

The solution is simple, replace this line in your code:

self.refreshControl.endRefreshing()

with these lines:

if self.refreshControl.isRefreshing {
    self.refreshControl.endRefreshing()
}

I even created a handy extension that does this check for me, to call it whenever I need it:

extension UIRefreshControl {
    func endIfRefreshing() {
        if isRefreshing { endRefreshing() }
    }
}

Upvotes: 0

Chris Edgington
Chris Edgington

Reputation: 3236

Reloading the entire table is quite an intensive operation. The better way might be to use the tableView.insertRows(at:with:) method. An example would be something like -

func didFinishLoadingData(newBlogs: [DetailAndRenderBlogObject]) {
    tableView.beginUpdates()

    var indexPaths: [IndexPath] = []

    for row in (blogList.count..<(blogList.count + newBlogs.count)) {
        indexPaths.append(IndexPath(row: row, section: 0))
    }

    blogList.append(contentsOf: newBlogs)

    tableView.insertRows(at: indexPaths, with: .fade)
    tableView.endUpdates()
}

Upvotes: 4

Related Questions