Hunter
Hunter

Reputation: 421

swift: uitableview populated from a local json file stutters heavily while scrolling

When the button is tapped to segue to the tableview, it takes about 5 seconds for it to segue. After it finally segues, when the tableview scrolls, it stutters and sometimes crashes. The tableview is populated from a local json file and references local images. The images are optimized to low sizes. What is causing this and how can I optimize/fix my code to stop this from happening?

import UIKit

class PDList: UIViewController, UITableViewDelegate, UITableViewDataSource {

@IBOutlet weak var pdTableView: UITableView!

var pdArt = [Decode]()

override func viewDidLoad() {
    super.viewDidLoad()

    json()

    pdTableView.delegate = self
    pdTableView.dataSource = self

}

func json() {

    let path = Bundle.main.path(forResource: "palmdesert", ofType: "json")
    let url = URL(fileURLWithPath: path!)

    do {
        let data = try Data(contentsOf: url)
        self.pdArt = try JSONDecoder().decode([Decode].self, from: data)
    }
    catch {}

}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return pdArt.count

}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "pdCell")
    let image = UIImage(named: pdArt[indexPath.row].pic)
    cell.textLabel?.text = pdArt[indexPath.row].art
    cell.detailTextLabel?.text = pdArt[indexPath.row].artist.capitalized
    cell.imageView?.image = image 

    return cell
}

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

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    performSegue(withIdentifier: "pdDetail", sender: self)
    self.pdTableView.deselectRow(at: indexPath, animated: true)
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let destination = segue.destination as? PDDetail {
        destination.pdArt = pdArt[(pdTableView.indexPathForSelectedRow?.row)!]
    }
}

@IBAction func done(sender: AnyObject) {
    dismiss(animated: true, completion: nil)
}

}

JSON Example:

{
  "art": "Agave",
  "desc": "Agave by Michael Watling serves as the City's entry monument and is inspired by the imagery of the agave, a succulent native to the desert.  The stone forms are representative of Palm Desert's many well-managed resources for survival and growth.",
  "artist": "Michael Watling",
  "lat": 33.7215,
  "long": -116.362,
  "pic": "test.png"
  }

Time Profile: Time Profile Time Profile2

Let me know if you need any other information. Any help is appreciated.

Upvotes: 2

Views: 453

Answers (3)

Warren Burton
Warren Burton

Reputation: 17382

I built a sample UITableView project with an asset library of 20 large jpeg images of 4032 × 3024 to feed in via UIImage(named:) to my cells.

This was vastly overspec for an image view of 120 x 70 so a max requirement of 360 x 210.

The time profiler showed this for scrolling in an entirely new batch of 12 cells.

enter image description here

Roughly 54ms per cell (650/12). However even large JPGs take advantage of hardware decoding so not too bad.

A modern iWidget at 60Hz refresh gives you a maximum of 16ms per frame to refresh the cell. So I got stuttering.

Replacing the images with appropriately scaled images (360 x 210 ) gives this.

enter image description here

Roughly 14ms per cell. The table scrolls mostly smooth.

So the problem here is likely one of these things.

  • Your images are too large. Ensure your assets are a pixel size of 3x the size of the image view.
  • Your images are not PNG or JPEG and cannot be handed off to the hardware decoder or treated with pngcrush. Make sure your images are PNG or JPEG. e.g GIF would be bad.
  • You are using PNG when JPG might be more appropriate. Real world images would be best as JPG. Line art/solid colors would be best as PNG.

Upvotes: 1

AD Progress
AD Progress

Reputation: 5086

My idea is to make a class or Struct something like this.

struct ObjectFromJSON {
    var art = ""
    var desc = ""
    var artist = ""
    var lat = 0.0
    var long = 0.0
    var pic = ""
}

where you decode the data create an array:

var decodedData = [ObjectFromJSON]()

then add all the decoded data to this array. If that will not help then try to add a CocoaPod called SwiftyJSON it makes JSON decoding a breeze and might help.

after decoding just set the info into the ObjectFromJSON object and append it to the decodedData array.

let decodedArt = json["art"].stringValue
let decodedDesc = json["desc"].stringValue
let decodedArtist = json["artist"].stringValue
let decodedLat = json["lat"].doubleValue
let decodedLong = json["long"].doubleValue
let decodedPic = json["pic"].stringValue

let newObject = ObjectFromJSON(art: decodedArt, desc: decodedDesc, artist: decodedArtist, lat: decodedLat, long: decodedLong, pic: decodedPic) 

decodedData.append(newObject)

this is based on your JSON however if you have more than 1 object in that json than it makes sense to put them into an array and then decoding would look a little different for example if you number the objects than would be something like:

let decodedArt = json[0]["art"].stringValue

if you have multiple JSON files than the first solution should be fine

Upvotes: 0

Adeel Miraj
Adeel Miraj

Reputation: 2496

Dequeue cells instead of creating new one every time. Change cellForRow to the following:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("pdCell", forIndexPath: indexPath)
  let image = UIImage(named: pdArt[indexPath.row].pic)
  cell.textLabel?.text = pdArt[indexPath.row].art
  cell.detailTextLabel?.text = pdArt[indexPath.row].artist.capitalized
  cell.imageView?.image = image 

  return cell
}

Upvotes: 0

Related Questions