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

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:
Let me know if you need any other information. Any help is appreciated.

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.
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.
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.

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
}

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

Related

swift: loading array with 300 images

I have ViewController with button. When I press on button I move to TableViewController with images in cells. I have array with 300 images.
But when I press on button my app paused for 12-14 seconds to load an array of images. After 14 second I move to TableViewController. How to fix it?
class TableViewController: UITableViewController {
var imageArray: [UIImage] =
[UIImage(named: "1.jpg")!,
...
UIImage(named: "300.jpg")!]
var textArray: [String] = ["text1", ... "text300"]
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return textArray
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
cell.cardImageView.image = imageArray[indexPath.row]
cell.label.text = "\(textArray[indexPath.row])"
}
}
Rather than creating an array of images straight out why don't you try this:
For numberOfRowsInSection set:
return 299
Add this method to your Viewcontroller:
func loadImageAsync(imageName: String, completion: #escaping (UIImage) -> ()) {
DispatchQueue.global(qos: .userInteractive).async {
guard let image = UIImage(named: imageName) else {return}
DispatchQueue.main.async {
completion(image)
}
}
}
in your tableView cellForRowAt all you will have to call is:
loadImageAsync(imageName: "\(indexPath.row + 1).jpg") { (image) in
cell.cardImageView.image = image
}
cell.label.text = "text\\(indexPath.row + 1)"
It means the app will only have to load the images on demand instead of making a huge array of images that it may never use if the user doesn't scroll all the way down. A 300 image array would be very CPU and memory intensive to create.
Don't store the object of UIImage in the array, as your code suggested, your images are locally stored as asset, just load the image names.
instead of this
var imageArray: [UIImage] =
[UIImage(named: "1.jpg")!,
...
UIImage(named: "300.jpg")!]
Do this
var imageNameArray: [String] =
["1.jpg",
...
"300.jpg"]
In the cellForRowAtIndexPath do following
cell.cardImageView.image = UIImage(named: imageArray[indexPath.row])
You could load the image data in background thread, once available then set it. But this approach has several shortcoming. If you do this you have to manually manage the image caching mechanism, because you dont want to load same image twice while shown twice in the cell.
There is a good news for you that, Uiimage(named:) method is thread safe for iOS9+. So if you are targeting iOS9+ you can load the image using UIImage(named:) in background thread. It will automatically manage the caching mechanism
DispatchQueue.global(qos: .background).async {
// load image here
let image = UIImage(named......
DispatchQueue.main.async {
// set image here
cell.card........
}
}
one possible bug may arise in this approach, such that while scrolling fast you may see old images for a while. You could solve it easily by managing the indexpath for which this image was loaded and comparing with current one.
I believe you should perform some kind of lazy initialization here so you get the benefit of cell reusing.
your code must be like this:
var imageTitles: [String] =
"1.jpg",
...
"300.jpg"]
var textArray: [String] = ["text1", ... "text300"]
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return textArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell
cell.cardImageView.image = UIImage(named: imageTitles[indexPath.row])
cell.label.text = textArray[indexPath.row]
}

How should I manage correctly images provided by JSON

I'm trying to parse all the Image provided by downloaded JSON on my App. So, I've heard many ways to do that correctly. Which API should I used to manage the Images on my App? How should I do that, can I have an example?
I also wanted to take care correctly of delays between:
Run the app --> Load data --> Populate UI elements
What should I do to minimize this delay, I think a professional app shouldn't take that long to load all components.
That's the part where I'll populate a UITableView with Images.
var arrCerveja = [Cerveja]()
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
//TableView DataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrCerveja.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellID") as! TableViewCell
let model = arrCerveja[indexPath.row]
cell.labelName.text = model.name
cell.labelDetail.text = "\(model.abv)"
cell. imageViewCell.image = ???? //How should I do that?
return cell
}
//TableView Delegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
override func viewDidLoad() {
super.viewDidLoad()
getApiData { (cerveja) in
arrCerveja = cerveja
self.tableView.reloadData()
}
}
Model Folder:
import Foundation
struct Cerveja:Decodable{
let name:String
let abv:Double
let image_url:String
}
Networking Folder:
import Alamofire
func getApiData(completion: #escaping ([Cerveja]) -> ()){
guard let urlString = URL(string: "https://api.punkapi.com/v2/beers") else {
print("URL Error")
return
}
Alamofire.request(urlString).responseJSON { response in
if response.data == response.data{
do{
let decoder = try JSONDecoder().decode([Cerveja].self, from: response.data!)
completion(decoder)
}catch{
print(error)
}
}else{print("API Response is Empty")}
}
}
What you can do is to cache the downloaded images, many libraries exist to help you do that, here is a list of some of them:
Kingfisher
HanekeSwift
Cache
Kingfisher is a good one that also allow you to download the images and explain you how to use the library with table views.
Caching the images will also reduce the loading time the next time the app is open.
You can also use this library to display a user friendly loading to the user during loading.

Using WebViews With Firebase Database

Recently in my app I have been using Firebase to store information for my app and it has worked well. Now I am using it to stream videos with a web view being used in the tableview to display Youtube videos. When trying to link the WebView to the database, I get an error that says:
Type 'video' has no subscript members
What would be causing this?
Here is the code:
import UIKit
import Firebase
class videoController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var ref = DatabaseReference()
var video = [UIWebView]()
var databaseHandle:DatabaseHandle = 0
#IBOutlet var videoController: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
databaseHandle = ref.child("Videos").observe(.childAdded) { (snapshot) in
let post = snapshot.value as? UIWebView
if let actualPost = post {
self.video.append(actualPost)
self.videoController.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return video.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
video.videos.allowsInlineMediaPlayback = video[indexPath.row]
return(video)
}
}
This line:
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
is your problem. This creates a new, local variable named video and it hides your video array property. Change it to:
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
Here's the whole method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
videoCell.videos.allowsInlineMediaPlayback = video[indexPath.row]
return videoCell
}
But on top of all of that, why do you have an array of web views? You certainly are not getting web views from Firebase.
And please fix your naming conventions. Class, struct, and enum names should start with uppercase letters. Variable, function, and case names start with lowercase letters. And use descriptive names. Naming everything simply video is confusing.
And change your video array to videos.

ios - Images in table cell view not having proper size at runtime and self changing size and clipping bounds when I scroll

When I run my app on the simulator the images that I load via a http request don't have proper sizing and bounds clip. I want them to a size 60x60 with a round shape, but instead they scale to fit the UITableViewCell kinda randomly but after I scroll up and down they remain fixed but still to big, I don't know what causes this neither do I know how to fix it, I'm new to iOS.I will post a screenshot with my UIImageView in Table Cell and with the effect that it has when I first run the app and my View Controller class.
I have tried to mess with the constraints, set fixed width and height constraint on the UIImageView but with no result.
I also tried to disable subview auto resize from the cell view but with no result.
This is the effect,this happens before I start scrolling:
This happens after I scroll up and down,the clipping on bounds returns to normal but the size is still to big:
This is my storyboard with the cell image view:
And this is my ViewController.swift class :
//
// ViewController.swift
// TopDevelopers
//
// Created by Eduard Valentin on 12/04/2018.
// Copyright © 2018 Eduard Valentin. All rights reserved.
//
import UIKit
import Alamofire
import Foundation
struct UserInfo {
var name:String
var imageURL:String
var imageView:UIImageView
init(newName: String, newImageURL:String, newImageView: UIImageView) {
self.name = newName
self.imageURL = newImageURL
self.imageView = newImageView
}
}
class ViewController: UIViewController,UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var tableView: UITableView!
var users:[UserInfo] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
// GET the data from the stackexchange api
let param: Parameters = [
"order": "desc",
"max" : 10,
"sort" : "reputation",
"site" : "stackoverflow"
]
Alamofire.request("https://api.stackexchange.com/2.2/users", method: .get, parameters: param).responseJSON { (response) -> (Void) in
if let json = response.result.value {
// we got a result
/* I know this is a bit ugly */
let json1 = json as! [String:AnyObject]
let usersInfoFromJSON = json1["items"] as! NSArray // remember to cast it as NSDictionary
for userInfo in usersInfoFromJSON {
let userDict = userInfo as! NSDictionary
Alamofire.request(userDict["profile_image"] as! String).responseData { (response) in
if response.error == nil {
print(response.result)
// Show the downloaded image:
if let data = response.data {
let imageView = UIImageView()
imageView.image = UIImage(data: data)
self.users.append(UserInfo(newName: userDict["display_name"] as! String,
newImageURL: userDict["profile_image"] as! String,newImageView: imageView))
self.tableView.reloadData()
}
}
}
}
}
}
}
#available(iOS 2.0, *)
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.users.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)
#available(iOS 2.0, *)
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomTableViewCell
cell.cellImageView.image = self.users[indexPath.row].imageView.image
cell.cellImageView.layer.cornerRadius = (cell.cellImageView.layer.frame.height / 2)
cell.cellLabel.text = self.users[indexPath.row].name
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
EDIT 1:
I also tried to set content mode to "scale to fit", "aspect fit" still the same results.
EDIT 2: Ok, I solved it by just deleting almost everything and doing it all over again but this time I did set the option for the "Suggested constraints", also I used xib's for the cells and everything is normal now, I still don't know what caused it.
Set clipsToBounds property to true, and set frames according to cellImageView's frame rather cellImageView.layer's frame:
cell.cellImageView.clipsToBounds = true
cell.cellImageView.layer.masksToBounds = true
cell.cellImageView.contentMode = .scaleAspectFit
cell.cellImageView.layer.cornerRadius = cell.cellImageView.frame.height / 2
And try to add UIImage in your struct rather than UIImageView. And use in cellForRowAtIndexPath as:
cell.cellImageView.image = self.users[indexPath.row].image
In your case do not use CustomCell, because basic tableviewcell provide default UIImageView and UILable
Update inside cellForRowAt function with below code
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourCellIndentifier")
cell.imageView?.contentMode = .scaleAspectFit
cell.imageView?.image = self.users[indexPath.row].imageView.image
cell.imageView?.layer.cornerRadius = cell.imageView?.frame.width / 2
cell.imageView?.layer.masksToBounds = true
return cell
}

Table View data appears on tableview with row selection code

I guess this could be one of my rookie mistakes I couldn't figure out.
I have an app which has a table view. It has text label and detail text label.
When I select a row, I takes me to another story board using segue...all of this works fine except the table view display on my simulator.
detail text label shows up on the simulator shown in this picture circled.
Here is the code I am using to detect cell/row selected. When I comment it out this issue goes away...
What you see in the red circle is gradeselected which is also in the detail text label in the tableview.
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
let gradeselected = String(describing: sgrade)
return [gradeselected]
}
Screenshot of simulator with the issue
Please help in resolving this issue. Let me know if you need any more info.
Xcode 9.1
Swift 4
#Caleb here is my code.
import UIKit
class StudentsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var cellButton: UIButton!
#IBOutlet weak var studentDetailTable: UITableView!
var sname:[String]?
var sgrade:[Int]?
var gradetext = "Grade:"
var sstudentname = ""
override func viewDidLoad() {
super.viewDidLoad()
studentDetailTable.delegate = self
studentDetailTable.dataSource = self
// Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sname!.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = studentDetailTable.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = sname[indexPath.row] + gradetext + String(sgrade[indexPath.row])
sstudentname = sname![indexPath.row]
cell?.detailTextLabel?.text = String(sgrade![indexPath.row])
cell?.layer.cornerRadius = (cell?.frame.height)!/2
cell?.backgroundColor = UIColor.blue
cell?.textLabel?.textColor = UIColor.white
cell?.layer.borderWidth = 6.0
cell?.layer.cornerRadius = 15
cell?.layer.borderColor = UIColor.white.cgColor
cell?.textLabel?.textColor = UIColor.white
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedIndex = tableView.dataSource?.sectionIndexTitles!(for: studentDetailTable)
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRow(at: indexPath!)!
let scell = currentCell.detailTextLabel!.text!
sstudentname = (currentCell.textLabel?.text)!
}
// - If I comment this section of the code issue goes away.
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
let gradeselected = String(describing: sgrade)
return [gradeselected]
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let myKLVC = segue.destination as! KindergartenLevelViewController
myKLVC.klvstudentname = sstudentname
}
The text in the red circle says [1, 2], which looks like the array that probably holds all the grades, not just the one for a specific cell that we see in the string gradeselected. If you have such an array in your code, look for places where you might be converting it to a string and drawing it. Maybe you did that in an earlier iteration of your code to make sure that the array contained what you thought, or something?
Arrays don't just mysteriously draw themselves on the screen — somewhere, there's some code that causes that to happen. We can't really help you find it because you haven't shown very much of your code, but just knowing what to look for may help you find it yourself.
You can query the selected row via table view's property indexPathForSelectedRow.
The method you have implemented does exactly what you see in the simulator.
Just have a look at the documentation:
property indexPathForSelectedRow: https://developer.apple.com/documentation/uikit/uitableview/1615000-indexpathforselectedrow
func sectionIndexTitles: https://developer.apple.com/documentation/uikit/uitableviewdatasource/1614857-sectionindextitles

Resources