Apple Watch-2 page-based navigation display HTTP data on page changed - ios

I try to create an page-based navigation watch app with Swift 2.
My app is below:
For both interfaces I have unique controllers named InterfaceController and EconomyInterfaceController.
In each controller I read some JSON data with function in controller init() function
func setFeed(url: String) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let request = NSURLRequest(URL: NSURL(string: url)!)
let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if let data = data {
do {
let _data: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments)
if let items = _data!["data"] as? NSArray{
self.tableOutlet.setNumberOfRows(items.count, withRowType: "row")
for (index, item) in items.enumerate() {
if let title = self.JSONString(item["title"]) {
if let spot = self.JSONString((item["spot"])) {
let news = newsItem(title: title, spot: spot)
let row = self.tableOutlet!.rowControllerAtIndex(index) as? RowController
row!.rowLabel!.setText(news.title) }
}
}
}
} catch {
}
}
}
task.resume()
}
As I know this is not best way for HTTP request because of all request run on main thread and on app startup. This is not my main question but if you have any offer I cannot refuse :)
My main question is if I call setFeed() method in willActivate() method and set table labels with
row!.rowLabel!.setText(news.title)
my app works but this is not a good choice because on each time page changed this updates content again and again.
If I call setFeed() method in init() method and set table labels with
row!.rowLabel!.setText(news.title)
Only app's first page being displayed and other pages not being displayed. What is wrong here?

Related

How to publish changes from the background thread Swift UI GET Request

I have set up my app such that I use UserDefaults to store a users login info (isLoggedIn, account settings). If a user is logged in and exits out of the application, and then relaunches the app, I would like them to be returned to the home page tab.
This functionality works; however, for some reason, on relaunch the home page has a getRequest that should be carried out. Instead, the screen goes white. This request and the loading involved works when I navigate from the login, but not when I relaunch the app. I get this warning:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
In looking at other stack overflow posts, the common sentiment seems to be to wrap any type of change in a dispatchqueue.main.async; however, this does not seem to work for me.
import SwiftUI
struct StoresView: View {
#ObservedObject var request = Request()
#Environment(\.imageCache) var cache: ImageCache
#EnvironmentObject var carts: Carts
init() {
getStores()
}
var body: some View {
NavigationView {
List(self.request.stores) { store in
NavigationLink(destination: CategoryHome(store: store).environmentObject(self.carts)) {
VStack(alignment: .leading) {
Text(store.storeName)
.font(.system(size: 20))
}
}
}.navigationBarTitle(Text("Stores").foregroundColor(Color.black))
}
}
func getStores() {
DispatchQueue.main.async {
self.request.getStoresList() { stores, status in
if stores != nil {
self.request.stores = stores!
}
}
}
}
}
get stores call in Request class
class Request: ObservableObject {
#Published var stores = [Store]()
let rest = RestManager()
func getStoresList(completionHandler: #escaping ([Store]?, Int?)-> Void) {
guard let url = URL(string: "###################") else { return }
self.rest.makeRequest(toURL: url, withHttpMethod: .GET, useSessionCookie: false) { (results) in
guard let response = results.response else { return }
if response.httpStatusCode == 200 {
guard let data = results.data else { return}
let decoder = JSONDecoder()
guard let stores = try? decoder.decode([Store].self, from: data) else { return }
completionHandler(stores, response.httpStatusCode)
} else {
completionHandler(nil, response.httpStatusCode)
}
}
}
Make Request from RestManager, I included the make request because I've seen some others use shared dataPublishing tasks, but I may not have used it correctly when trying to use it. Any advice or help would be appreciated. Thanks!
func makeRequest(toURL url: URL,
withHttpMethod httpMethod: HttpMethod, useSessionCookie: Bool?,
completion: #escaping (_ result: Results) -> Void) {
DispatchQueue.main.async { [weak self] in
let targetURL = self?.addURLQueryParameters(toURL: url)
let httpBody = self?.getHttpBody()
// fetches cookies and puts in appropriate header and body attributes
guard let request = self?.prepareRequest(withURL: targetURL, httpBody: httpBody, httpMethod: httpMethod, useSessionCookie: useSessionCookie) else
{
completion(Results(withError: CustomError.failedToCreateRequest))
return
}
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request) { (data, response, error) in
print(response)
completion(Results(withData: data,
response: Response(fromURLResponse: response),
error: error))
}
task.resume()
}
}
You seem to be trying to call the function in the Main tread instead of setting the stores property. Calling request. getStoresList is already in the main thread once the call is made you enter the background thread from there you need to come back to the main thread once the URLSession is complete. You need to make the UI modification in the Main thread instead of the background tread as the error clearly state. Here's what you need to do to fix this issue:
func getStores() {
self.request.getStoresList() { stores, status in
DispatchQueue.main.async {
if stores != nil {
self.request.stores = stores!
}
}
}
}

Property is coming up empty when called from another class when using URLSession

For some reason, the products array is coming back empty when I try and access it from another class. What am I doing wrong, and how can I get the products array to populate? Is it something related to the do/catch?
The print statement shown will give me what I'm looking for, but when I try and use the property in another class after the retrieve method has been called, it comes up empty.
For information, "Product" is a struct that has name, description, etc properties attached.
private let productListUrl = URL(string: "https://api/products.json")
var products = [Product]()
func retrieveProductList() {
if let productListUrl = productListUrl {
URLSession.shared.dataTask(with: productListUrl) { (data, response, error) in
if let data = data {
do {
let jsonData = try JSONSerialization.jsonObject(with: data, options: []) as! [String:Any]
let tempArray: Array = jsonData["products"] as! [Any]
for product in tempArray {
let newProduct = Product(json: product as! [String : Any])
self.products.append(newProduct!)
}
print("In ProductService: \(self.products)")
}
catch {
print("An error occured while attempting to read data")
}
}
}.resume()
}
}
As maddy noted, this is because the URL call is asynchronous.
You basically have 3 options:
Use a semaphore approach and make your retrieveProductList method synchronous.
Change your class to have a delegate property that you can ping when the URL request finishes.
Add a completion handler to your retrieveProductList method that is called when the URL request finishes.
I personally would lean towards option 3:
func retrieveProductList(completion: #escaping ([Product])->())
{
// Right after you print the products...
completion(self.products)
}

Images loading in incorrectly even with cache

if let toID = message.chatPartnerId() {
firebaseReference.child(toID).observeSingleEvent(of: .value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
cell.nameLabel.text = dictionary["displayname"] as? String
let pic = dictionary["pictureURL"] as! String
print("THIS IS THE URL FOR EACH DISPLAYNAME")
print(dictionary["displayname"] as? String)
print(pic)
if let imageFromCache = MainPageVC.imageCache.object(forKey: pic as NSString) {
cell.pictureLabel.image = imageFromCache
} else {
let requested = URLRequest(url: URL(string: pic )!)
URLSession.shared.dataTask(with: requested) {data, response, err in
if err != nil {
print(err)
} else {
DispatchQueue.main.async {
let imageToCache = UIImage(data: data!)
MainPageVC.imageCache.setObject(imageToCache!, forKey: pic as NSString)
//cell.pictureLabel.image = nil
cell.pictureLabel.image = imageToCache
}
}
}.resume()
}
}
})
}
return cell
}
I'm running this code in my cellForRowAtIndexPath and I'm getting a ton of really bad behavior. I'm also getting similar behavior on other pages but for some reason this block of code with about a 90% consistency returns incorrect information for cells.
I get a lot of duplicate pictures being used, displaynames in the wrong places, but when I'm actually clicking into a person, my detail page shows the correct information every single time. That code is the typical didSelectRowAtIndexPath and passing the person.
What I don't understand is why on the initial load of this page all of the information is screwed up, but if I click into someone and come back the entire tableview has correct names and pictures. The names/pics also fix if I scroll a cell off the screen then come back to it.
I'm getting this behavior all over my app, meanwhile I see caching/loading done like this everywhere. Is it because I'm running the code in my cellForRowAtIndexPath? The only difference I see is that I'm running it there instead of creating a function inside of my Person class that configures cells and running it like that. What I don't understand is why that would make a difference because as far as I'm aware running a function within cellforRowAtIndexpath would be the same as copy-pasting that same code into there?
Any ideas/suggestions?
Edit: I'm getting a very similar situation when I'm running the following code:
self.PersonalSearchesList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
self.tableView.reloadData()
Where I'm sorting my array before reloading my data. The information sometimes loads in incorrectly at first, but once I scroll the cell off the screen then come back to it it always corrects itself.
if you are using swift 3 here are some handy functions that allow you to save an image to your apps directory from an URL and then access it from anywhere in the app:
func saveCurrentUserImage(toDirectory urlString:String?) {
if urlString != nil {
let imgURL: URL = URL(string: urlString!)!
let request: URLRequest = URLRequest(url: imgURL)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if (error == nil && data != nil) {
func display_image() {
let userImage = UIImage(data: data!)
if let userImageData = UIImagePNGRepresentation(userImage!) {
let filename = self.getDocumentsDirectory().appendingPathComponent("userImage")
try? userImageData.write(to: URL(fileURLWithPath: filename), options: [.atomic])
}
}
DispatchQueue.main.async(execute: display_image)
}
})
task.resume()
}
}
and then access it with any view controller using this:
extension UIViewController {
func getImage(withName name: String) -> UIImage {
let readPath = getDocumentsDirectory().appendingPathComponent(name)
let image = UIImage(contentsOfFile: readPath)
return image!
}
}
and finally calling it like this:
cell.pictureLabel.image = getImage(withName: "userImage")
If you can run the saveCurrentUserImage function prior to running cellForRowAtIndexPath then you can just check if the photo is nil in the directory before attempting to download it. You might be getting funny behavior when the page initially loads because you have multiple network calls going on at once. I wouldn't recommend making any network calls in cellForRowAtIndexPath because every time the cells are re-initialized it's going to make that network call for each cell.
Hope it helps!
EDIT: This method of image saving and retrieval is for images that you want to persist. If you want to erase them from memory you'll have to delete them from your directory.

Download JSON Data THEN Reload Collection View

Quick one here. I have a class called NetworkService. It has a method that sets up an NSURLSession and completion block. My Object class, NewsItem, has a method downloadNewsItems that calls the first method and goes to a url, downloads JSON data, appends it to an array and returns it. up until that point everything works as i'd want. I create the object and append it then return it (the method is named downloadImage but it can work with any sort of data).
func downloadImage(completion: (NSData -> Void)) {
let request = NSURLRequest(URL: self.url)
let dataTask = session.dataTaskWithRequest(request) { (data, response, error) in
if error == nil {
if let httpResponse = response as? NSHTTPURLResponse {
switch (httpResponse.statusCode) {
case 200:
if let data = data {
completion(data)
}
default:
print(httpResponse.statusCode)
}
}
} else {
print("Error: \(error?.localizedDescription)")
}
}
dataTask.resume()
}
here's the implementation of the same method on my object class.
static func downloadNewsItems() -> [NewsItem] {
var newsItems = [NewsItem]()
let url = NSURL(string: "http://my-url.com.json")
let networkService = NetworkService(url: url!)
networkService.downloadImage { (data) -> Void in
let jsonData = JSON(data:data)
for item in jsonData["stories"].arrayValue {
// print(item["title"])
let newsArticle = NewsItem()
newsArticle.category = item["category"].string
newsArticle.titleText = item["title"].string
newsArticle.paragraph1 = item["paragraph1"].string
newsArticle.paragraph2 = item["paragraph2"].string
newsArticle.featureImage = NSURL(string: "\(item["headerImage"].string)")
newsArticle.date = item["date"].string
newsArticle.majorReference = item["majorReference"].string
newsArticle.fact = item["fact"].string
newsItems.append(newsArticle)
}
print(newsItems.count)
}
return newsItems
}
That print(newsItems.count) shows I have downloaded and updated my objects properly into a dictionary. Now here comes the problem. I have a CollectionViewController. I want to populate it with the data I get from the method call. I create an array and call the method that returns the objects on it inside of ViewDidLoad: but NO! when I print, I get 0 and my collectionView doesn't display any cells.
var newsItems = [NewsItem]()
then in viewDidLoad:
newsItems = NewsItem.downloadNewsItems()
print(newsItems.count)
collectionView.reloadData()
The objects are downloaded, get set up by my init() method and are added to the array in the method whilst inside of the NetworkService / NewsItem classes but when I call the method from the Collection View Controller, Nothing. Initially I tried the default JSONSerialisation route but i had the same problem. I thought maybe I'm not doing it right. Switched to a 3rd party JSON Library (SwiftyJSON)... Exact SAME PROBLEM. Please help. I have had 3 weeks of this. I.. I can't. Not anymore..
user2361090,
You are making a webservice call to fetch all the newsItems inside downloadNewsItems. Which is an asynchronous call. So it takes a little bit of time to fetch process and then populate the newsItems array. But you are not waiting for it to get populated even before it gets populated you have returned it Hence you will return empty array in newsItems = NewsItem.downloadNewsItems(). Use blocks to handover the data. Change your method as
static func downloadNewsItems(completionBlock block : ([NewsItem]) -> ()){
var newsItems = [NewsItem]()
let url = NSURL(string: "http://my-url.com.json")
let networkService = NetworkService(url: url!)
networkService.downloadImage { (data) -> Void in
let jsonData = JSON(data:data)
for item in jsonData["stories"].arrayValue {
// print(item["title"])
let newsArticle = NewsItem()
newsArticle.category = item["category"].string
newsArticle.titleText = item["title"].string
newsArticle.paragraph1 = item["paragraph1"].string
newsArticle.paragraph2 = item["paragraph2"].string
newsArticle.featureImage = NSURL(string: "\(item["headerImage"].string)")
newsArticle.date = item["date"].string
newsArticle.majorReference = item["majorReference"].string
newsArticle.fact = item["fact"].string
newsItems.append(newsArticle)
}
print(newsItems.count)
//if the control comes here in background thread because you know you have to reload the collection view which is UI operation change it main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
block(newsItems)
})
}
}
Finally you can call it as,
NewsItem.downloadNewsItems{ (passedArray) -> () in
newsItems = passedArray
collectionView.reloadData()
}

Cell in UITableView doesn't update until selected in iOS (swift)

I'm developing an app with Parse that has a tableview with cells containing a label that has mutual friends from Facebook, my problem is that everything thing in the table works fine but the mutual friends label, it takes long to show unless I select the row (when I select the row the number appears immediately).. Here's my code for getting the mutual friends and setting the label in the cell:
let facebookContext = driverobj?.objectForKey("facebookContext") as! String
let user: PFUser = PFUser.currentUser()!
let access_token = user.valueForKey("authData")?.valueForKey("facebook")?.valueForKey("access_token") as! String
let usercontexturl: NSURL = NSURL(string: "https://graph.facebook.com/\(facebookContext)/mutual_friends?access_token=\(access_token)")!
let myrequest: NSURLRequest = NSURLRequest(URL: usercontexturl)
let mysession = NSURLSession.sharedSession()
let task = mysession.dataTaskWithRequest(myrequest) { data, response, error in
print("Task completed")
do {
let jsonresult = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
cell.mutualFriendsLabel?.text = (jsonresult.valueForKey("summary")?.valueForKey("total_count"))!.stringValue + " Mutual Friends"
} catch {
print(error)
}
}
task.resume()
This code is inside cellForRowAtIndexPath method, and didSelectRowAtIndexPath method in empty.
The block passed to dataTaskWithRequest is probably not executing on the main thread which can cause these types of symptoms to appear. Try executing the UI updates to the main thread like:
dispatch_async(dispatch_get_main_queue()) {
cell.mutualFriendsLabel?.text = (jsonresult.valueForKey("summary")?.valueForKey("total_count"))!.stringValue + " Mutual Friends"
}
I think you need to update UI on main queue like this:
dispatch_async_(your_queue) {
task.response {
//handle data
dispatch_async(dispatch_get_main_queue()) {
//update UI, etc. change label text
}
}
}

Resources