Swift: How to refresh tableview without lag (UIRefreshControl) - ios

Filling my table view with objects from a MYSQL database using PHP and JSON to Swift 3. I have a pull down to refresh function but when I'm pulling down to refresh it lags mid way for a second and then continues (like the wheel won't spin for a second).
How can I update my tableview smoother because I'm guessing as I add more content to the database in the future the bigger the lag. I currently have 12 objects in my database so imagine with 100+ objects.
In viewDidLoad
// Pull to Refresh
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
if #available(iOS 10.0, *) {
myTableView.refreshControl = refreshControl
print("iOS 10")
} else {
myTableView.addSubview(refreshControl)
print("iOS 9 or iOS 8")
}
Then pull to refresh function
// Pull to Refresh
func handleRefresh(refreshControl: UIRefreshControl) {
// Fetching Data for TableView
retrieveDataFromServer()
// Stop Refreshing
refreshControl.endRefreshing()
}
// Retrieving Data from Server
func retrieveDataFromServer() {
// Loading Data from File Manager
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Clear the arrays
self.followedArray = [Blog]()
self.mainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if followedIdentifiers.contains(blog.blogID) {
self.followedArray.append(blog)
} else {
self.mainArray.append(blog)
}
}
}
} catch {
print("Error: (Retrieving Data)")
}
myTableView.reloadData()
}

Refer to the apple sample code at the following location:
http://developer.apple.com/library/ios/#samplecode/LazyTableImages/Introduction/Intro.html
Couple of suggestion :
Don’t show the data at cellForRowAtIndexPath: method ‘cause at this time cell is not displayed yet. Try to use tableView:willDisplayCell:forRowAtIndexPath: method in the delegate of UITableView.
Re-Use single instance of cell/header/footer even if you need to show more.
Let me know if anything specific is needed.

Spinner.isHidden = false
Spinner.startAnimating()
DispatchQueue.global(qos: .background).async {
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Clear the arrays
self.followedArray = [Blog]()
self.mainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if followedIdentifiers.contains(blog.blogID) {
self.followedArray.append(blog)
} else {
self.mainArray.append(blog)
}
}
}
} catch {
print("Error: (Retrieving Data)")
}
DispatchQueue.main.async
{
myTableView.reloadData()
self.Spinner.startAnimating()
self.Spinner.isHidden = true
}
}

My guess is that your retrieveDataFromServer() is blocking the main thread, and therefore causing the lag. Try wrapping it in an async block
// Pull to Refresh
func handleRefresh(refreshControl: UIRefreshControl) {
// Fetching Data for TableView
retrieveDataFromServer { [weak refreshControl] in
// This block will run once retrieveDataFromServer() is completed
// Reload data
myTableView.reloadData()
// Stop Refreshing
refreshControl?.endRefreshing()
}
}
// Retrieving Data from Server
func retrieveDataFromServer(completion: (() -> Void)?) {
// Loading Data from File Manager
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Clear the arrays
self.followedArray = [Blog]()
self.mainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if followedIdentifiers.contains(blog.blogID) {
self.followedArray.append(blog)
} else {
self.mainArray.append(blog)
}
}
}
} catch {
print("Error: (Retrieving Data)")
}
// Calls completion block when finished
completion?()
}

I imagine the lag you're experiencing is due to the network request being executed synchronously on the main thread:
let data: Data = try Data(contentsOf: url as URL)
Network requests are slow and should almost certainly be done off the main thread. The solution here is to move the networking call to a background thread so the main (UI) thread doesn't get blocked (lag).
So how do you do that? Well that is a large question with many different answers.
I highly recommend you spend some time learning about multi-threaded programming (also known as concurrency) in Swift. Going through this Ray Wenderlich tutorial should give you a good foundation.
Then it's probably a good idea to learn about URLSession which is used for performing asynchronous network requests in iOS apps. Again Ray Wenderlich has a great starter tutorial.
Finally... here is a quick and dirty solution for you. It's "hacky" and you probably shouldn't use it, but it will probably fix your lag issue:
func retrieveDataFromServer() {
// Loading Data from File Manager
loadData()
let getDataURL = "http://example.com/receiving.php"
let url: NSURL = NSURL(string: getDataURL)!
// Move to a background queue to fetch and process data from network.
DispatchQueue.global().async {
// Don't touch anything related to the UI here.
do {
let data: Data = try Data(contentsOf: url as URL)
let jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray
// Create new temp arrays to process json
var tempFollowedArray = [Blog]()
var tempMainArray = [Blog]()
// Looping through jsonArray
for jsonObject in jsonArray {
if let blog = Blog(jsonObject:jsonObject as! [String : Any]) {
// Check if Identifiers Match
if self.followedIdentifiers.contains(blog.blogID) {
tempFollowedArray.append(blog)
} else {
tempMainArray.append(blog)
}
}
}
// Jump back to main (UI) thread to update results
DispatchQueue.main.async {
print("success")
self.followedArray = tempFollowedArray
self.mainArray = tempMainArray
self.myTableView.reloadData()
}
} catch {
DispatchQueue.main.async {
print("Error: (Retrieving Data)")
// This reload is probably not necessary, but it was
// in your original code so I included it.
self.myTableView.reloadData()
}
}
}
}

Related

Passing variable outside of a function for use in Swift 3

I'm new to Swift, and I want to 1) run a function that extracts a value from a JSON array (this part works) and 2) pass that variable into another function which will play that URL in my audio player.
My issue: I can't access that string stored in a variable outside the first function. Luckily, there's a bunch of questions on this (example), and they say to establish a global variable outside the function and update it. I have tried this like so:
var audio = ""
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
self.audio = (foo["audio"] as? String)!
} else {
// item could not be found
}
}).resume()
print(audio) // no errors but doesn't return anything
I have confirmed the JSON extraction is working -- if I move that print(audio) inside the function, it returns the value. I just can't use it elsewhere.
I originally tried it without the self. but returned an error.
Is there a better way to store this string in a variable so I can use it in another function?
EDIT: Trying new approach based on Oleg's first answer. This makes sense to me based on how I understand didSet to work, but it's still causing a thread error with the play button elsewhere.
var audiotest = ""{
didSet{
// use audio, start player
if let audioUrl = URL(string: audiotest) {
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationUrl = documentsDirectoryURL.appendingPathComponent(audioUrl.lastPathComponent)
//let url = Bundle.main.url(forResource: destinationUrl, withExtension: "mp3")!
do {
audioPlayer = try AVAudioPlayer(contentsOf: destinationUrl)
} catch let error {
print(error.localizedDescription)
}
} // end player
}
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.example.com/example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
if let foo = data_list.first(where: {$0["episode"] as? String == "Houston Preview"}) {
// do something with foo
self.audiotest = (foo["audio"] as? String)!
} else {
// item could not be found
}
print(self.audiotest)
}).resume()
The request for the data is asynchronous so the code that is inside the completionHandler block happens some time later (depending on the server or the timeout) , that’s why if you try to print outside the completionHandler actually the print func happens before you get the data.
There are couple of solution:
1. Add property observer to your audio property and start playing when it is set:
var audio = “”{
didSet{
// use audio, start player
}
}
2. Wrapping the request with a method that one of its parameters is a completion closure:
// the request
func fetchAudio(completion:(String)->()){
// make request and call completion with the string inside the completionHandler block i.e. completion(audio)
}
// Usage
fetchAudio{ audioString in
// dispatch to main queue and use audioString
}
Try this code. No need to take global variable if it is not being used in multiple function. you can return fetched URL in completion handler.
func getAudioUrl(completionHandler:#escaping ((_ url:String?) -> Void)) {
let url = URL(string: "http://www.example.json")
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
let json: Any?
do{
json = try JSONSerialization.jsonObject(with: data, options: [])
}
catch{
return
}
guard let data_list = json as? [[String:Any]] else {
return
}
// here's the important part
if let foo = data_list.first(where: {$0["episode"] as? String == "Special Episode Name"}) {
// do something with foo
let audio = (foo["audio"] as? String)!
completionHandler(audio)
} else {
// item could not be found
completionHandler(nil)
}
}).resume()
}
func useAudioURL() {
self.getAudioUrl { (url) in
if let strUrl = url {
// perform your dependant operation
print(strUrl)
}else {
//url is nil
}
}
}

How to show all data in table view during pagination in swift 3?

Here i had implemented pagination for the table view and items are loaded by using model class but here the loaded items are replacing with the new items and whenever it calls api it returns the new data and old data is overriding on it and displaying only 10 items at a time i am implementing it for first time can anyone help me how to resolve the issue ?
func listCategoryDownloadJsonWithURL(listUrl: String) {
let url = URL(string: listUrl)!
print(listUrl)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil { print(error!); return }
do {
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel = ModelClass(dict: jsonObj as [String : AnyObject])
DispatchQueue.main.async {
guard let obj = self.listClassModel else { return }
let itemsCount = obj.items.count
print(itemsCount)
for i in 0..<itemsCount {
let customAttribute = obj.items[i].customAttribute
for j in 0..<customAttribute.count {
if customAttribute[j].attributeCode == "image" {
let baseUrl = "http://192.168.1.11/magento2/pub/media/catalog/product"
self.listCategoryImageArray.append(baseUrl + customAttribute[j].value)
print(self.listCategoryImageArray)
}
}
}
self.activityIndicator.stopAnimating()
self.activityIndicator.hidesWhenStopped = true
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.reloadData()
self.collectionView.isHidden = false
self.tableView.reloadData()
}
}
} catch {
print(error)
}
}
task.resume()
}
You are assigning your result data to model array, each time you call your API. This is the reason that your old data is getting replaced with new one. Rather than assigning, you should append the new data to your datasource array.
if let jsonObj = try JSONSerialization.jsonObject(with: data!) as? [String:Any] {
self.listClassModel.append(contentsOf: ModelClass(dict: jsonObj as [String : AnyObject]))
Also make sure you initialize your array as an empty array first. (maybe in declaration or viewDidLoad) before calling API.

iOS - Why reloadData tableView data on first application load?

I am working on a simple Flickr app that gets some data from their API and displays it on a tableview instance. Here's a piece of the code for the TableViewController subclass.
var photos = [FlickrPhotoModel]()
override func viewDidLoad() {
super.viewDidLoad()
getFlickrPhotos()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
private func getFlickrPhotos() {
DataProvider.fetchFlickrPhotos { (error: NSError?, data: [FlickrPhotoModel]?) in
//data is received
dispatch_async(dispatch_get_main_queue(), {
if error == nil {
self.photos = data!
self.tableView.reloadData()
}
})
}
}
The application does not seem to load the data if the { tableView.reloadData() } line is removed. Does anyone know why this would happen since I call getFlickrPhotos() within viewDidLoad(). I believe I am also dispatching from the background thread in the appropriate place. Please let me know what I am doing incorrectly.
EDIT -- Data Provider code
class func fetchFlickrPhotos(onCompletion: FlickrResponse) {
let url: NSURL = NSURL(string: "https://api.flickr.com/services/rest/?method=flickr.photos.getRecent&api_key=\(Keys.apikey)&per_page=25&format=json&nojsoncallback=1")!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
if error != nil {
print("Error occured trying to fetch photos")
onCompletion(error, nil)
return
}
do {
let jsonResults = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
let photosContainer = jsonResults!["photos"] as? NSDictionary
let photoArray = photosContainer!["photo"] as? [NSDictionary]
let flickrPhoto: [FlickrPhotoModel] = photoArray!.map{
photo in
let id = photo["id"] as? String ?? ""
let farm = photo["farm"] as? Int ?? 0
let secret = photo["secret"] as? String ?? ""
let server = photo["server"] as? String ?? ""
var title = photo["title"] as? String ?? "No title available"
if title == "" {
title = "No title available"
}
let model = FlickrPhotoModel(id: id, farm: farm, server: server, secret: secret, title: title)
return model
}
//the request was successful and flickrPhoto contains the data
onCompletion(nil, flickrPhoto)
} catch let conversionError as NSError {
print("Error parsing json results")
onCompletion(conversionError, nil)
}
}
task.resume()
}
I'm not familiar with that API, but it looks like the fetchFlickrPhotos method is called asynchronously on a background thread. That means that the rest of the application will not wait for it to finish before moving on. viewDidLoad will call the method, but then move on without waiting for it to finish.
The completion handler that you provide is called after the photos are done downloading which, depending on the number and size of the photos, could be seconds later. So reloadData is necessary to refresh the table view after the photos are actually done downloading.

Swift 2 api call takes longer than other functions run

Let me preface this by saying I'm VERY new to Swift 2 and am building my first app which calls an api (php) for data (JSON). The problem I'm running into is when I make the call to the api the other functions ran before the api can send back the data.
I've researched some type of a onComplete to call a functions after the api response is done. I'm sure for most of you this is easy, but I cant seem to figure it our.
Thanks in advance!
class ViewController: UIViewController {
var Selects = [Selectors]()
var list = [AnyObject]()
var options = [String]()
var index = 0
#IBOutlet var Buttons: [UIButton]!
override func viewDidLoad() {
super.viewDidLoad()
self.API()
self.Render()
}
func API() {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
print("request failed \(error)")
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
}
}
task.resume()
}
func BuildOptions() {
// BuildOptions stuff happens here
}
func Render() {
// I do stuff here with the data
}
}
So I assume your Render() method is called before data gets back from the api? Keeping your api-calling code in the view controllers is bad design, but as you're new i won't expand on that. In your case it's as simple as not calling your Render() method in viewDidLoad() - call it after you're done with parsing the data from JSON (after the self.Selects = [Selectors... line). NSURLSession.sharedSession().dataTaskWithRequest(request) method is called asynchronously , and the callback block with data, response, error parameters is executed after this method is done with fetching your data, so it can happen after the viewDidLoad is long done and intially had no data to work on as the asynchronous method was still waiting for response from the API.
Edit - speaking of handling api calls, it's a wise thing to keep them separated from specific view controllers to maintain a clean reusable code base. You should call the API and wait for a callback from it, so i'd just do that to your API function, it would look like this:
static func callAPI(callback: [AnyObject]? -> Void ) {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
completion(nil)
}
do {
var list = [AnyObject]()
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
completion(list)
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
completion(nil)
}
}
task.resume()
}
Generally speaking methods should do one specific thing - in your case call the api and return data or error. Initialize your selectors in the view controllers on callback. Your view controller's viewDidLoad would look like this using the code above:
override func viewDidLoad() {
super.viewDidLoad()
YourApiCallingClass.callApi() {
result in
if let list = result {
self.list = list
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
self.Render()
} else {
//Handle situation where no data will be returned, you can add second parameter to the closue in callApi method that will hold your custom errors just as the dataTaskWithRequest does :D
}
}
}
Now you have a nice separation of concerns, API method is reusable and view controller just handles what happens when it gets the data. It'd be nice if you slapped an UIActivityIndicator in the middle of the screen while waiting, it'd look all neat and professional then :P

Swift 2.0 iOS9 UITableView update does not render

When i load a JSON file inside my UITableViewController it loads and updates my datasource and view, but only renders the update when i touch my screen.
The loading and parsing code i'm using looks like this:
func fetchData() {
let jsonUrl = 'http://myrestserver.com/apicall'
let session = NSURLSession.sharedSession()
let urlObject = NSURL(string: jsonUrl)
let task = session.dataTaskWithURL(urlObject!) {
(data, response, error) -> Void in
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers ) as! NSDictionary
var items :[Article] = []
let jsonItems = jsonData["channel"] as! NSArray
for (_, item) in jsonItems.enumerate() {
let article = Article()
article.id = item["id"] as? String
article.title = item["title"] as? String
article.guid = item["guid"] as? String
items.append(article)
}
self.articles.insertContentsOf(items, at: 0)
} catch {
print("error fetchData")
}
}
task.resume()
self.tableView.reloadData()
}
Is there a method i'm not aware of to handle this re-rendering?
I've tried render methods for UITableViewCell like described here:
setNeedsLayout and setNeedsDisplay
But there is no luck, can someone explain what is the best practice for rendering new records?
Best regards,
Jos
#nwales is correct, though I would recommend getting familiar with property observers for reloading your data. Once your data is reloaded simply update your property and it will automatically fire your update.
var data: [String] = [""] {
didSet {
// you could call a function or just reload right here
self.tableView.reloadData()
}
}
using #nwales method:
var data: [String] = [""] {
didSet {
dispatch_async(dispatch_get_main_queue(),{
myTableView.reloadData()
})
}
}
After you've parsed the JSON try adding the following
dispatch_async(dispatch_get_main_queue(),{
myTableView.reloadData() //myTableView = your table view instance
})

Resources