How to detect if there is no more post to fetch in CollectionView? - swift

I am making new app with Xcode using Swift and i am fetching posts from my WordPress website , all works fine but there is one problem, when i scroll down to the very last post of category then the indicator is just running and nothing happens, i want when there is no more post then Progress bar should stop running and i want to toast a message that there is no more post , how is that possible ? this is my code to fetch posts
func fetchPostData(completionHandler: #escaping ([Postimage]) -> Void ) {
let url = URL(string: "https://www.sikhnama.com/wp-json/wp/v2/posts/?categories=4&page=\(page)\(sortBy)")!
print(url)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {return}
do {
let postsData = try JSONDecoder().decode([Postimage].self, from: data)
completionHandler(postsData)
DispatchQueue.main.async {
if(self.newsData.isEmpty == false){
print("collection view empty")
self.collectionView.reloadData()
SVProgressHUD.dismiss()
}
else{
if(self.collectionView == nil){
print("collection view nill")
self.fetchPostData { (posts) in
self.newsData = posts }
}
}
}
}
catch {
let error = error
print(String(describing: error))
}
}
task.resume()
}
can you please help ?

Related

Displaying networking error message to user in Swift

The question is how can I make this code reusable especially the error checking in the network method and the condition in the completionhandler, so I don't have duplicate code?
I created a method which makes a network request with URLSession and calls a completion handler with the statuscode as argument. In the completion handling, I created a condition which shows an error message or perfom a segue based on the statuscode. All of this code works but I want to make it reusable so I don't have duplicate code.
Networking method:
func saveMessage(data: String, day: String, completion: #escaping (Int)->()) {
let url = URL(string: "\(Constants.baseURL)/daily_mindset/today_message")
guard let requestUrl = url else { fatalError() }
var request = URLRequest(url: requestUrl)
request.httpMethod = "POST"
// Set HTTP Request Header
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let jsonData = encodeJSON(with: data, day: day)
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
completion(700)
return
}
guard let response = response as? HTTPURLResponse else {
completion(701)
return
}
guard (200...299).contains(response.statusCode) else {
completion(response.statusCode)
return
}
guard let mime = response.mimeType, mime == "application/json" else {
completion(702)
return
}
guard let data = data else {
completion(703)
return
}
do {
let todoItemModel = try JSONDecoder().decode(MessageData.self, from: data)
Constants.currentMindsetId = todoItemModel._id!
print("Response data:\n \(todoItemModel)")
} catch let jsonErr{
print(jsonErr)
}
completion(response.statusCode)
}
task.resume()
}
Calling networking method with completionhandler:
messageManager.saveMessage(data: textView.text, day: day, completion: {(statusCode: Int) -> Void in
if (200...299).contains(statusCode) {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ToDailyMindsetScreen", sender: sender)
}
} else if (400...499).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Please make sure you filled in the all the required fields."
}
} else if (500...599).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, couldn't reach our server."
}
} else if (700...).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, something went wrong. Try again later."
}
}
})
Code in the networking method I want to reuse:
if error != nil {
completion(700)
return
}
guard let response = response as? HTTPURLResponse else {
completion(701)
return
}
guard (200...299).contains(response.statusCode) else {
completion(response.statusCode)
return
}
guard let mime = response.mimeType, mime == "application/json" else {
completion(702)
return
}
guard let data = data else {
completion(703)
return
}
Code in the completionhandler I want to reuse:
if (200...299).contains(statusCode) {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ToDailyMindsetScreen", sender: sender)
}
} else if (400...499).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Please make sure you filled in the all the required fields."
}
} else if (500...599).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, couldn't reach our server."
}
} else if (700...).contains(statusCode) {
DispatchQueue.main.async {
self.errorLabel.text = "Sorry, something went wrong. Try again later."
}
}
If the error messages are ViewController specific you can start with creating a function that returns the message based on the status code like this:
private func getErrorMessageFor(statusCode: Int) -> String? {
if (200...299).contains(statusCode) {
//If no error message is returned assume that the request was a success
return nil
} else if (400...499).contains(statusCode) {
return "Please make sure you filled in the all the required fields."
} else if (500...599).contains(statusCode) {
return "Sorry, couldn't reach our server."
} else if (700...).contains(statusCode) {
return "Sorry, something went wrong. Try again later."
} else {
return "Message for other errors?"
}
}
You can always move this code to a ViewController subclass to provide more generic error messages and override it later to provide more detailed errors for a specific View Controller.
class BaseViewController: UIViewController {
func getErrorMessageFor(statusCode: Int) -> String? {
//base implementation here
}
}
class OtherViewController: BaseViewController {
override func getErrorMessageFor(statusCode: Int) -> String? {
//create a new error message only for statusCode 404
if statusCode == 404 {
return "The requested resource was not found on the server. Please contact the support team"
} else {
return super.getErrorMessageFor(statusCode: statusCode)
}
}
}
Keep in mind that as your app grows you might want to create an APIClient that would handle networking and error handling for you. Take a look at https://bustoutsolutions.github.io/siesta/, it is very user friendly

App crashing when trying to change profile photo

I am working in the edit profile portion of my application. When I try to change and update a users profile photo. The app crashes and I get this error
reason: 'URL scheme must be one of gs://, http://, or https://
When I create a new profile and add a profile photo or if I upload a photo it works fine but when I try to change the profile photo I get this. It will first remove the profile photo and update ( leaving the image view gray when a user doesn't have a photo) then when I try to rechange the photo again it will crash.
Here is the code I have.
func updateProfileImage() {
guard imageChanged == true else { return }
guard let currentUid = Auth.auth().currentUser?.uid else { return }
guard let user = self.user else { return }
Storage.storage().reference(forURL: user.profileImageUrl).delete(completion: nil)
let filename = NSUUID().uuidString
guard let updatedProfileImage = profileImageView.image else { return }
guard let imageData = updatedProfileImage.jpegData(compressionQuality: 0.3) else { return }
STORAGE_PROFILE_IMAGES_REF.child(filename).putData(imageData, metadata: nil) { (metadata, error) in
if let error = error {
print("Failed to upload image to storage with error: ", error.localizedDescription)
}
STORAGE_PROFILE_IMAGES_REF.downloadURL(completion: { (url, error) in
USER_REF.child(currentUid).child("profileImageUrl").setValue(url?.absoluteString, withCompletionBlock: { (err, ref) in
guard let userProfileController = self.userProfileController else { return }
userProfileController.fetchCurrentUserData()
self.dismiss(animated: true, completion: nil)
})
})
}
}
}
The first thing you check URL is valid or not using a guard.
guard let urlis = yourUrl else{
// url is nill.
return
}
if let url = NSURL(string: urlis) {
// your image code
}
else{
// url is invalid.
return
}
Add Exception Breakpoint: This quick tip will save you a lot of debugging time!. So Xcode will stop where the exception is caught.
In your project, go to the Breakpoint Navigator, click on the ’+’ button and ’Add Exception Breakpoint…’

How to create proper completion handler for server login in swift?

I have an api manager class in my swift application and it has a server login with username and password.
I want to know how to create a completion handler for it that when the server responses with 200 status code, the function handles that response and for example performs a segue in the viewcontroller.
I did not find any tutorials for this. Thanks for your help!
EDIT 1:
What i need is: The completion handler is immediately run when the function is called. I want the completion handler run after server responds.
And this is my login function:
public class func Login(username: String, password: String, complitionHandler: #escaping (Int) -> Void) {
let urlS = "http://server.com/" + "login.php"
let url = URL(string: urlS)
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let body = "username=\(username.lowercased())&password=\(password)"
request.httpBody = body.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
print(error!)
print("error")
logedIn = 2
return
}
do{
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSDictionary
if let parseJson = json {
let code = parseJson["status"] as! String
if code == "200" {
print("loged inn")
logedIn = 1
}else if code == "400" {
print("uuuser/pass error")
logedIn = 0
}
}
}catch{
print("json error")
logedIn = 2
}
}
task.resume()
DispatchQueue.main.async {
complitionHandler(logedIn)
}
}
And how i call the function in my ViewController:
Manager.Login(username: "1", password: "1") { (i) in
switch i {
case 0:
print("user/pass error")
case 1:
print("loged in")
self.performSegue(withIdentifier: "toMain", sender: self)
case 2:
print("json error")
default:
()
}
}
You have all of the pieces in place. You just need to move your call to the completion handler to the correct place:
}catch{
print("json error")
logedIn = 2
}
DispatchQueue.main.async {
complitionHandler(logedIn)
}
}
task.resume()
Also note that method names should start with lowercase letters so your Login function should be named login.
Now you can use this login method like:
login(username: someUsername, password: somePassword) { (result) in
if result == 1 {
// success - do your segue
} else if result == 0 {
// bad username/password
} else {
// some error
}
}

Swift 4: How to asynchronously use URLSessionDataTask but have the requests be in a timed queue?

Basically I have some JSON data that I wish to retrieve from a bunch of URL's (all from the same host), however I can only request this data roughly every 2 seconds at minimum and only one at a time or I'll be "time banned" from the server. As you'll see below; while URLSession is very quick it also gets me time banned almost instantly when I have around 700 urls to get through.
How would I go about creating a queue in URLSession (if its functionality supports it) and while having it work asynchronously to my main thread; have it work serially on its own thread and only attempt each item in the queue after 2 seconds have past since it finished the previous request?
for url in urls {
get(url: url)
}
func get(url: URL) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
print(error.localizedDescription)
}
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
DispatchQueue.main.async {
print("Server Error")
}
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
DispatchQueue.main.async {
self.itemsCount.append(count)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
Recursion solves this best
import Foundation
import PlaygroundSupport
// Let asynchronous code run
PlaygroundPage.current.needsIndefiniteExecution = true
func fetch(urls: [URL]) {
guard urls.count > 0 else {
print("Queue finished")
return
}
var pendingURLs = urls
let currentUrl = pendingURLs.removeFirst()
print("\(pendingURLs.count)")
let session = URLSession.shared
let task = session.dataTask(with: currentUrl, completionHandler: { (data, response, error) in
print("task completed")
if let _ = error {
print("error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("server error received")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
return
}
if response.mimeType == "application/json" {
print("json data parsed")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}else {
print("unknown data")
DispatchQueue.main.async {
fetch(urls: pendingURLs)
}
}
})
//start execution after two seconds
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (timer) in
print("resume called")
task.resume()
}
}
var urls = [URL]()
for _ in 0..<100 {
if let url = URL(string: "https://google.com") {
urls.append(url)
}
}
fetch(urls:urls)
The easiest way is to perform recursive call:
Imagine you have array with your urls.
In place where you initially perform for loop with, replace it with single call get(url:).
self.get(urls[0])
Then add this line at the and of response closure right after self.itemsCount.append(count):
self.urls.removeFirst()
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) in
self.get(url: urls[0])
}
Make DispatchQueue to run your code on threads. You don't need to do this work on Main Thread. So,
// make serial queue
let queue = DispatchQueue(label: "getData")
// for delay
func wait(seconds: Double, completion: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + seconds) { completion() }
}
// usage
for url in urls {
wait(seconds: 2.0) {
self.get(url: url) { (itemCount) in
// update UI related to itemCount
}
}
}
By the way, Your get(url: url) function is not that great.
func get(url: URL, completionHandler: #escaping ([Int]) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
print(error.localizedDescription)
/* Don't need to use main thread
DispatchQueue.main.async {
print(error.localizedDescription)
}
*/
return
}
let data = data!
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("Server Error")
/* Don't need to use main thread
DispatchQueue.main.async {
print("Server Error")
}
*/
return
}
if response.mimeType == "application/json" {
do {
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
if json["success"] as! Bool == true {
if let count = json["total_count"] as? Int {
self.itemsCount.append(count)
// append all data that you need and pass it to completion closure
DispatchQueue.main.async {
completionHandler(self.itemsCount)
}
}
}
} catch {
print(error.localizedDescription)
}
}
})
task.resume()
}
I would recommend you to learn concept of GCD(for thread) and escaping closure(for completion handler).
GCD: https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1
Escaping Closure: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID546

Slow JSONSerialization Parsing Swift 3.0

I'm building an application able to return books info (Title,Description,Author ...) using Google Books Api.
The problem is that after detecting ISBN (International Standard Book Number)from the previous view it takes time to show details on my current view.
After 1-3 seconds I get parsing result:
I'm using this method to get data using google books api:
func getBookInfo(isbn: String) {
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=isbn:\(isbn)") else {
print("***********************************")
print("the url is not valid")
return
}
URLSession.shared.dataTask(with: url, completionHandler: {data, response, error -> Void in
guard error == nil else {
print(response)
print(error!.localizedDescription)
return
}
guard let data = data else {
print("no error but no data")
print("***********************************")
print(response)
return
}
guard let jsonResult = try? JSONSerialization.jsonObject(with: data, options: []) else {
print("the JSON is not valid")
return
}
if let arrayOfLa = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.language") as? [String] {
DispatchQueue.global(qos: .userInitiated).async {
// Bounce back to the main thread to update the UI
DispatchQueue.main.async {
print("opopopop")
print (arrayOfLa)
}
}
}
///finish language
if let arrayOfTitles = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.title") as? [String] {
DispatchQueue.global(qos: .userInitiated).async {
// Bounce back to the main thread to update the UI
DispatchQueue.main.async {
self.title = "\(arrayOfTitles[0])"
}
}
}
if let arrayOfDesc = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.description") as? [String] {
DispatchQueue.global(qos: .userInitiated).async {
// Bounce back to the main thread to update the UI
DispatchQueue.main.async {
self.bookDescription.text = "\(arrayOfDesc[0])"
}
}
}
//start here
if let arrayOfPictures = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.imageLinks.thumbnail") as? [String] {
print ("\(arrayOfPictures[0])")
self.bookPicture = "\(arrayOfPictures[0])"
if let checkedUrl = URL(string: "\(arrayOfPictures[0])") {
self.imageBook.contentMode = .scaleAspectFit
self.downloadImage(url: checkedUrl)
}
}
//end here
//start author here
if let arrayOfAuthors = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.authors") as? [[String]] {
DispatchQueue.global(qos: .userInitiated).async {
// Bounce back to the main thread to update the UI
DispatchQueue.main.async {
self.authorText.text = "\((arrayOfAuthors[0])[0])"
}
}
}
// finish author here
//start categories here
if let arrayOfCategories = (jsonResult as AnyObject).value(forKeyPath: "items.volumeInfo.categories") as? [[String]] {
DispatchQueue.global(qos: .userInitiated).async {
// Bounce back to the main thread to update the UI
DispatchQueue.main.async {
self.categorieText.text = "\((arrayOfCategories[0])[0])" }
}
}
// finish caetegories here
}).resume()
}
and I'm calling it on the ViewWillAppaer
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
getBookInfo(isbn: isbnCode)
}
Any suggestions ?