Pull Refresh with Alamofire Swift 4 - ios

On my app, I am using Alamofire to implement pull to refresh, which is working to refresh but unfortunately doesn't bring back any data. What am I doing wrong? Something for sure I am missing.
Here is the code
my class EarthquakeListing
var earthquake: Earthquake?
var refresh = UIRefreshControl()
on the viewDidLoad()
// Refresh Control
//let refreshControl = UIRefreshControl()
refresh.tintColor = UIColor.yellow
refresh.addTarget(self, action: #selector(refreshData), for: UIControlEvents.valueChanged)
if #available(iOS 10.0, *) {
self.tableView.refreshControl = (self.refresh)
} else {
self.tableView.addSubview(self.refresh)
}
Alamofire on viewDidLoad()
Alamofire.request("http://www.seismicportal.eu/fdsnws/event/1/query?limit=50&format=json").responseJSON { response in
//print("Request: \(String(describing: response.request))") // original url
if let data = response.data{
do {
self.earthquake = try
JSONDecoder().decode(Earthquake.self, from: data)
print("features data: \(String(describing: self.earthquake?.features))")
// print("data: \(self.features.count)")
self.tableView.reloadData()
self.refresh.endRefreshing()
}
catch{}
}
}
}
and the func of objc
#objc func refreshData() {
self.tableView.reloadData()
self.refresh.endRefreshing()
}

Add your Alamofire request in a function and call it inside refreshData(). And for first time data loading you can call that function(containing Alamofire request) inside your viewDidLoad().
func myRequest() {
// Alamofire request goest here.....
/* Inside of Alamofire callback block */
// Assign data to data source
self.tableView.reloadData()
if (self?.refresh.isRefreshing)! {
self.refresh.endRefreshing()
}
/*--------*/
}
override func viewDidLoad() {
super.viewDidLoad()
myRequest()
}
#objc func refreshData() {
myRequest()
}

Update your method with the following code:
#objc func refreshData() {
// start refresh control
self.refresh.beginRefreshing()
// get data from server
Alamofire.request("http://www.seismicportal.eu/fdsnws/event/1/query?limit=50&format=json").responseJSON { response in
if let data = response.data {
do {
self.earthquake = try JSONDecoder().decode(Earthquake.self, from: data)
// End refreshing and reload table
self.refresh.endRefreshing()
self.tableView.reloadData()
}
catch{}
}
}
}

Related

RxSwift Subscriber receiving multiple events

Consider the below code.
On tapButton, we subscribe to an Observable isFetched and then call fetchPopularMovies().
fetchPopularMovies() in turn calls an API. When the response is received, we will send OnNext(true) event.
Problem is, I receive multiple events on 2nd button tap onwards. If I add onCompleted(), I don't even receive events on 2nd button tap onwards. My expectation is that one event will be triggered on each button tap. What am I missing here?
class ViewController: UIViewController {
let popularMoviesURL = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=API_KEY")
var isFetched = BehaviorSubject<Bool?>(value:nil)
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func tapButton(_ sender: Any) {
let observable = isFetched.asObservable()
observable.subscribe(onNext: { observer in
guard let result = observer else { return }
print(result)
print("onNext Recieved")
}, onError: { _ in
print("onError Recieved")
}).disposed(by: disposeBag)
fetchPopularMovies()
}
func fetchPopularMovies() {
let task = URLSession.shared.dataTask(with: popularMoviesURL!) {(data, response, error) in
guard let _ = data else { return }
self.isFetched.onNext(true)
//self.isFetched.onCompleted()
}
task.resume()
}
}
Reactive code is declarative. It's "setup" code. So it should be placed where the comment says "Do any additional setup after loading the view."
The simplest change you can do to fix the problem you are having is to move the subscription into the viewDidLoad method as Satish Patel referenced in his comment.
override func viewDidLoad() {
super.viewDidLoad()
isFetched.subscribe(onNext: { observer in
guard let result = observer else { return }
print(result)
print("onNext Recieved")
}, onError: { _ in
print("onError Recieved")
}).disposed(by: disposeBag)
}
#IBAction func tapButton(_ sender: Any) {
fetchPopularMovies()
}
(Note that Subjects should always be held with lets never vars.)
If you use RxCocoa, you can simplify this code even more:
class ViewController: UIViewController {
let button = UIButton()
let isFetched = BehaviorSubject<Bool?>(value:nil)
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let popularMoviesURL = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=API_KEY")!
let fetchedData = button.rx.tap
.flatMapLatest {
URLSession.shared.rx.data(request: URLRequest(url: popularMoviesURL))
.catch { error in
print("onError Recieved")
return Observable.empty()
}
}
fetchedData
.map { _ in true }
.bind(to: isFetched)
.disposed(by: disposeBag)
}
}
Now, all of your code is "setup code" so it all goes in viewDidLoad.

Loading the server response into the UICollectionView

I have a UIViewController class where I'm trying to populate the data into my UICollectionView
// HomeController.swift
import UIKit
import SwiftyJSON
class HomeController: UICollectionViewController,
UICollectionViewDelegateFlowLayout, TaskServiceManagerDelegate {
func didUpdate(sender: TaskServiceManager) {
self.collectionView?.reloadData()
}
var taskList:JSON? = nil
override func viewDidLoad() {
super.viewDidLoad()
// getting the task list to populate the UICollectionView
let taskServiceManager:TaskServiceManager = TaskServiceManager()
taskList = taskServiceManager.getAllTasks()
collectionView?.dataSource = self;
collectionView?.delegate = self;
navigationItem.title = "Test"
collectionView?.backgroundColor = UIColor.lightGray
collectionView?.register(QuestionCell.self, forCellWithReuseIdentifier: "cellId")
}
override func viewDidAppear(_ animated: Bool) {
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// getting the task list to populate the UICollectionView
let taskServiceManager:TaskServiceManager = TaskServiceManager()
taskList = taskServiceManager.getAllTasks()
collectionView?.dataSource = self;
collectionView?.delegate = self;
}
override func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return (taskList?.count)!
}
}
I have another class TaskServiceManager where I'm making a REST api call and getting the data.
// TaskServiceManager.swift
import UIKit
import Alamofire
import SwiftyJSON
class TaskServiceManager : NSObject{
var delegate: TaskServiceManagerDelegate?
func getAllTasks() -> JSON {
var swiftyJsonVar: JSON = []
Alamofire.request(url, headers: getHeaders()).responseJSON { (responseData) -> Void in
print("Status Code: \(responseData.response?.statusCode)")
if((responseData.result.value) != nil) {
swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar)
print("Task List Count: \(swiftyJsonVar.array?.count)")
}
}
return swiftyJsonVar
}
}
protocol TaskServiceManagerDelegate {
func didUpdate(sender: TaskServiceManager)
}
The taskList?.count in the numberOfItemsInSection returns 0 even though I get a success from the api response with count as 6. Why is it so?
How do I refer the HomeController from the TaskServiceManager class?
Also, why does my TaskServiceManagerDelegate doesn't reflect the UICollectionView?
No need of delegate to fetch data from API, completion handlers are better choice for this kind of task.
Make this change in your function :
func getAllTasks(completion:#escaping(JSON)->()) {
var swiftyJsonVar: JSON = []
Alamofire.request(url, headers: getHeaders()).responseJSON { (responseData) -> Void in
print("Status Code: \(responseData.response?.statusCode)")
if((responseData.result.value) != nil) {
swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar)
print("Task List Count: \(swiftyJsonVar.array?.count)")
completion(swiftyJsonVar)
}
}
}
To call it from your another class:
TaskServiceManager().getAllTasks { (tasks) in
print("tasks : \(tasks)")
//here reload your collection view
}
You are doing everything right except return statement. API calls are asynchronous and so your return statement is getting called before your network request is over. You need a completion callback to return the data when you get response.
And your delegation is incomplete. If you want to the delegation method then you need add these lines.
In viewDidLoad() after :
let taskServiceManager:TaskServiceManager = TaskServiceManager()
you need to assign your HomeController's instance to delegate variable of TaskServiceManager class
taskServiceManager.delegate = self
And in TaskServiceManager class when you get data from server you need to pass it using your delegate method, I don't know why you are trying to pass instance of TaskServiceManager in delegate method ?
protocol TaskServiceManagerDelegate {
func didUpdate(sender: TaskServiceManager) // Why ?
// func didUpdate(serverData: JSON) // Maybe this is what you need
}
To call delegate method one last step without any return statement :
func getAllTasks() {
var swiftyJsonVar: JSON = []
Alamofire.request(url, headers: getHeaders()).responseJSON { (responseData) -> Void in
print("Status Code: \(responseData.response?.statusCode)")
if((responseData.result.value) != nil) {
swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar)
print("Task List Count: \(swiftyJsonVar.array?.count)")
delegate.didUpdate(serverData: swiftyJsonVar) // this is to pass data
}
}
}
Now this function in your HomeController will be called (I changed the data to serverData instead of TaskServiceManager) :
func didUpdate(serverData: dataFormServer) {
print("dataFormServer : \(dataFormServer)")
self.collectionView?.reloadData()
}
you forgot to call the didUpdate function after successful API call. You need to reload the collectionView after the successful API call. The reason is API calls are asynchronous in nature

Why doesn't my completion block work?

Implementing solution given here How to make a synchronous request using Alamofire?
I don't get any errors, it just doesn't work as expected. In tableViewController
override func viewDidLoad() {
super.viewDidLoad()
loadData() { (didCompleteRequest) in
if (didCompleteRequest) {
self.TodosTableView.delegate = self
self.TodosTableView.dataSource = self
print("loading successfull")
} else {
print("loading failed")
}
}
print("leaving viewDidLoad")
}
func loadData(completion: #escaping (Bool) -> Void) {
Alamofire.request(TodosViewController.serverAdress + "projects/index.json").responseJSON { response in
do {
// async stuff
} catch {
completion(false)
}
print("leaving loadData")
completion(true)
}
}
output I get
leaving viewDidLoad
leaving loadData
loading successfull
apparently, the first element should be the last one
First viewDidLoad is running in the main thread. So when you put this loadData() in viewDidLoad controls dispatched to background thread where alamofire works on, and the main thread continues and prints leaving viewDidLoad
Try this
override func viewDidLoad() {
super.viewDidLoad()
self.TodosTableView.delegate = self
self.TodosTableView.dataSource = self
loadData() { (didCompleteRequest) in
if (didCompleteRequest) {
self.TodosTableView.reloadData()
print("loading successfull")
} else {
print("loading failed")
}
}
print("leaving viewDidLoad")
}
You are call block code after get response from server. so first call "print("leaving viewDidLoad")".
response code get with delay so call block code with delay

Binding to a UIRefreshControl after network call

I am new to RxSwift and I was wondering how I would be able to "reactively" use a UIRefreshControl with a UITableView instead of the normal way of creating a target, and manually calling beginRefreshing() and endRefreshing().
For instance, say I am loading some strings from an API:
class TableViewController: UITableViewController {
var data : [String] = []
let db = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
refreshControl = UIRefreshControl()
//I don't want to use
//refreshControl?.addTarget(self, action: #selector(getData), forControlEvents: .ValueChanged)
//Do something to refreshControl.rx_refreshing?
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let str = data[indexPath.row]
cell.textLabel?.text = str
return cell
}
//MARK: - Requests
private func getData() {
let myData = MyAPI.getData() //Returns Observable<[String]>
myData
.subscribe({ [weak self] (event) in
switch event {
case .Next(let strings):
self?.data = strings
self?.tableView.reloadData()
break
case .Error(let err):
print(err)
break
case .Completed:
break
}
// self?.refreshControl?.endRefreshing()
})
.addDisposableTo(db)
}
}
MyAPI sends a request for some string values, how can I bind the refreshControl to call getData() and also stop refreshing when it's finished (or error'd) the network request? Do I need to bind to refreshControl.rx_refreshing?
RxSwift's example app provides an interesting class to handle this kind of logic: ActivityIndicator.
Once you have ActivityIndicator in, code for binding rx_refreshing to the request becomes really easy.
let activityIndicator = ActivityIndicator()
override func viewDidLoad() {
super.viewDidLoad()
refreshControl = UIRefreshControl()
// When refresh control emits .ValueChanged, start fetching data
refreshControl.rx_controlEvent(.ValueChanged)
.flatMapLatest { [unowned self] _ in
return self.getData()
.trackActivity(activityIndicator)
}
.subscribeNext { [unowned self] strings in
self.data = strings
self.tableView.reloadData()
}
.addDisposableTo(db)
// Bind activity indicator true/false to rx_refreshing
activityIndicator.asObservable()
.bindTo(refreshControl.rx_refreshing)
.addDisposableTo(db)
}
// getData only needs to return an observable, subscription is handled in viewDidLoad
private func getData() -> Observable<[String]> {
return myData = MyAPI.getData() //Returns Observable<[String]>
}

Pull to Refresh in Swift not Reloading UITableView

I've got JSON filling my UITableView successfully, but the JSON is often updated so I need the ability to refresh. I followed THIS TUTORIAL to implement a pull to refresh control. Visually, it seems like it all works correctly, but when I call tableView.reloadData() the table doesn't reload. However, if I leave the ViewController and return, the table is updated. Why would tableView.reloadData() work in viewDidAppear and viewWillAppear but not in my custom refresh() function?
MainVC.swift file
class MainVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var dataArray: NSArray = NSArray()
#IBOutlet var Controller: UISegmentedControl!
var refreshControl:UIRefreshControl!
func refresh(sender:AnyObject)
{
refreshBegin("Refresh",
refreshEnd: {(x:Int) -> () in
self.tableView .reloadData()
println("Table Reloaded")
self.refreshControl.endRefreshing()
})
}
func refreshBegin(newtext:String, refreshEnd:(Int) -> ()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
println("refreshing")
sleep(2)
dispatch_async(dispatch_get_main_queue()) {
refreshEnd(0)
}
}
}
override func viewWillAppear(animated: Bool) {
self.tableView .reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.titleView = UIImageView(image: UIImage(named: "logojpg.jpg"))
startConnectionAt("http://www.domain.com/json.php")
refreshControl = UIRefreshControl()
refreshControl.backgroundColor = UIColor.orangeColor()
refreshControl.tintColor = UIColor.whiteColor()
refreshControl.attributedTitle = NSAttributedString(string: "Pull to Refresh")
refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
tableView.addSubview(refreshControl)
}
//MARK: JSON Loading
var data: NSMutableData = NSMutableData()
func startConnectionAt(urlPath: String){
var url: NSURL = NSURL(string: urlPath)
var request: NSURLRequest = NSURLRequest(URL: url)
var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)
connection.start()
}
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
println("Connection failed.\(error.localizedDescription)")
}
func connection(connection: NSURLConnection, didRecieveResponse response: NSURLResponse) {
println("Recieved response")
}
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
self.data = NSMutableData()
}
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
self.data.appendData(data)
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
var dataAsString: NSString = NSString(data: self.data, encoding: NSUTF8StringEncoding)
var err: NSError
var json: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
var results: NSArray = json["needs"] as NSArray
self.dataArray = results
tableView.reloadData()
println("success")
}
//End loading of JSON
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
return self.dataArray.count;
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
var cell:CustomCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as CustomCell
var rowData: NSDictionary = dataArray[indexPath.row] as NSDictionary
var firstName=rowData["needFirstname"] as String
var descrip=rowData["needDescription"] as String
var poster=rowData["needPoster"] as String
var city=rowData["needCity"] as String
var state=rowData["needState"] as String
var country=rowData["needCountry"] as String
cell.needFirstName.text = firstName
cell.needDescription.text = descrip
cell.needDescription.numberOfLines = 0
cell.needPoster.text = poster
cell.needCity.text = city
cell.needState.text = state
cell.needCountry.text = country
return cell
}
#IBAction func Change(sender: AnyObject) {
if Controller.selectedSegmentIndex == 0 {
startConnectionAt("http://www.domain.com/localJSON.php")
}
else if Controller.selectedSegmentIndex == 1 {
startConnectionAt("http://www.domain.com/intlJSON.php")
}
self.tableView .reloadData()
}
}
Your last comment is right-on in my view.
During your pull to refresh function, you call tableView.reloadData(), however, reloadData() does not inherently do any repopulating the elements in the data source (in your case, dataArray). It simply reloads all the data that's currently in the table view's data source at the time it is called.
So my recommendation would be to construct your refresh function such that the following happens:
Initiate a request to your web service.
When the response comes back (ie, connectionDidFinishLoading is executed), parse the JSON results and assign that result to the dataArray instance. You seem to be doing this already in connectionDidFinishLoading, so it's just a matter of sending the request to your web service, I'd think.
Call tableView.reloadData() to display any new elements that have been added since the last time the tableView's data was displayed. Again, you're doing this already in connectionDidFinishLoading, so #1 is the primary thing that I think needs to happen.
Referring to https://stackoverflow.com/a/25957339
Not sure but maybe the connection is run on a different thread, if so you need to run the table update on the main UI thread
// using Swift's trailing closure syntax:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}

Resources