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!
}
}
}
}
Related
I have this very basic piece of code in one of my Views:
.onOpenURL(perform: { url in
avm.handleRedirect(viewContext,url: url) })
where avm is defined as
#ObservedObject var avm = AccountsViewModel.makeAccountsViewModel()
avm has this basic property:
#Published var isLoading = true
That when set, my view listens to and shows a loading spinner. This works in all other situations, except that outlined below.
The handleRedirect function looks as follows:
func handleRedirect(_ context: NSManagedObjectContext, url: URL) {
debugPrint("Handling redirect")
let url = URLComponents(string: url.absoluteString)!
let code = url.queryItems?.first(where: { $0.name == "code" })?.value
trueLayerClient.getAccessToken(code: code!) { res in
if res == nil {
debugPrint("accessTokenResponse was nil")
}
self.api.storeTokens(
str: API.StoreTokensRequest(
userID: "***",
authToken: res!.accessToken,
refreshToken: res!.refreshToken,
apiKey: "***"
), finished: { success in
if success{
debugPrint("was successful")
DispatchQueue.main.async{
self.isLoading = false
}
} else {
debugPrint("store token failed")
}
})
}
}
I'm triggering this onOpenUrl using a universal Link I have setup.
When I hit the link, my app opens from the background and I see the following logs:
Handling Redirect
Was successful
However, the app never goes into a loading state. Furthermore, the UI becomes "blocked" and I have to hard kill the app to be able to press anything.
Once I do reopen the app, the new state (which I fetch via api and store in CoreData) is reflected in the view.
At first I thought my API was responding too quickly, but I put a 5 second sleep in it and I still see the UI get blocked but with the same logs (just further apart).
I would appreciate any help on this.
If helpful, my storeTokens API call looks like this:
func storeTokens(str: StoreTokensRequest , finished: #escaping (Bool)->Void) {
let u = URL(string:"\(self.baseURL)/\(storeTokenPath)")!
var req = URLRequest(url: u)
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpMethod = "POST"
let reqBody = str
debugPrint("req bodyed")
guard let encoded = try? JSONEncoder().encode(reqBody) else {
debugPrint("failed to encode req")
finished(false)
return
}
req.httpBody = encoded
debugPrint("encoded")
let task = URLSession.shared.dataTask(with: req) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
debugPrint("wasnt http rep")
finished(false)
return
}
debugPrint("status code is: \(httpResponse.statusCode)")
if httpResponse.statusCode < 299 {
debugPrint("About to call true")
finished(true)
return
}
}
task.resume()
}
}
I see all the logs in this function in my output too, I just left them out for succinctness.
I have tried an implementation similar to yours. My "onOpenUrl()" method is waking up, if necessary, my app, and passing the url to my app, and the app is updating its state correctly.
Basic check.
Forget for a moment "onOpenUrl". Implement inside your view this method:
.onAppear {
viewModel.onAppear()
}
where inside your view model play with the "isLoading" property. For example,
func onAppear(){
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("- loading complete")
self.isLoading = false
}
}
Does the view update (the activity loader) accordingly?
In my application, button tapping downloads data from an Internet site. The site is a list of links containing binary data. Sometimes, the first link may not contain the proper data. In this case, the application takes the next link in the array and gets data from there. The links are correct.
The problem I have is that frequently (not always though) the application freezes for seconds when I tap on the button. After 5-30 seconds, it unfreezes and downloading implements normally. I understand, something is blocking the main thread. When stopping the process in xCode, I get this (semaphore_wait_trap noted):
This is how I do it:
// Button Action
#IBAction func downloadWindNoaa(_ sender: UIButton)
{
// Starts activity indicator
startActivityIndicator()
// Starts downloading and processing data
// Either use this
DispatchQueue.global(qos: .default).async
{
DispatchQueue.main.async
{
self.downloadWindsAloftData()
}
}
// Or this - no difference.
//downloadWindsAloftData()
}
}
func downloadWindsAloftData()
{
// Creates a list of website addresses to request data: CHECKED.
self.listOfLinks = makeGribWebAddress()
// Extract and save the data
saveGribFile()
}
// This downloads the data and saves it in a required format. I suspect, this is the culprit
func saveGribFile()
{
// Check if the links have been created
if (!self.listOfLinks.isEmpty)
{
/// Instance of OperationQueue
queue = OperationQueue()
// Convert array of Strings to array of URL links
let urls = self.listOfLinks.map { URL(string: $0)! }
guard self.urlIndex != urls.count else
{
NSLog("report failure")
return
}
// Current link
let url = urls[self.urlIndex]
// Increment the url index
self.urlIndex += 1
// Add operation to the queue
queue.addOperation { () -> Void in
// Variables for Request, Queue, and Error
let request = URLRequest(url: url)
let session = URLSession.shared
// Array of bytes that will hold the data
var dataReceived = [UInt8]()
// Read data
let task = session.dataTask(with: request) {(data, response, error) -> Void in
if error != nil
{
print("Request transport error")
}
else
{
let response = response as! HTTPURLResponse
let data = data!
if response.statusCode == 200
{
//Converting data to String
dataReceived = [UInt8](data)
}
else
{
print("Request server-side error")
}
}
// Main thread
OperationQueue.main.addOperation(
{
// If downloaded data is less than 2 KB in size, repeat the operation
if dataReceived.count <= 2000
{
self.saveGribFile()
}
else
{
self.setWindsAloftDataFromGrib(gribData: dataReceived)
// Reset the URL Index back to 0
self.urlIndex = 0
}
}
)
}
task.resume()
}
}
}
// Processing data further
func setWindsAloftDataFromGrib(gribData: [UInt8])
{
// Stops spinning activity indicator
stopActivityIndicator()
// Other code to process data...
}
// Makes Web Address
let GRIB_URL = "http://xxxxxxxxxx"
func makeGribWebAddress() -> [String]
{
var finalResult = [String]()
// Main address site
let address1 = "http://xxxxxxxx"
// Address part with type of data
let address2 = "file=gfs.t";
let address4 = "z.pgrb2.1p00.anl&lev_250_mb=on&lev_450_mb=on&lev_700_mb=on&var_TMP=on&var_UGRD=on&var_VGRD=on"
let leftlon = "0"
let rightlon = "359"
let toplat = "90"
let bottomlat = "-90"
// Address part with coordinates
let address5 = "&leftlon="+leftlon+"&rightlon="+rightlon+"&toplat="+toplat+"&bottomlat="+bottomlat
// Vector that includes all Grib files available for download
let listOfFiles = readWebToString()
if (!listOfFiles.isEmpty)
{
for i in 0..<listOfFiles.count
{
// Part of the link that includes the file
let address6 = "&dir=%2F"+listOfFiles[i]
// Extract time: last 2 characters
let address3 = listOfFiles[i].substring(from:listOfFiles[i].index(listOfFiles[i].endIndex, offsetBy: -2))
// Make the link
let addressFull = (address1 + address2 + address3 + address4 + address5 + address6).trimmingCharacters(in: .whitespacesAndNewlines)
finalResult.append(addressFull)
}
}
return finalResult;
}
func readWebToString() -> [String]
{
// Final array to return
var finalResult = [String]()
guard let dataURL = NSURL(string: self.GRIB_URL)
else
{
print("IGAGribReader error: No URL identified")
return []
}
do
{
// Get contents of the page
let contents = try String(contentsOf: dataURL as URL)
// Regular expression
let expression : String = ">gfs\\.\\d+<"
let range = NSRange(location: 0, length: contents.characters.count)
do
{
// Match the URL content with regex expression
let regex = try NSRegularExpression(pattern: expression, options: NSRegularExpression.Options.caseInsensitive)
let contentsNS = contents as NSString
let matches = regex.matches(in: contents, options: [], range: range)
for match in matches
{
for i in 0..<match.numberOfRanges
{
let resultingNS = contentsNS.substring(with: (match.rangeAt(i))) as String
finalResult.append(resultingNS)
}
}
// Remove "<" and ">" from the strings
if (!finalResult.isEmpty)
{
for i in 0..<finalResult.count
{
finalResult[i].remove(at: finalResult[i].startIndex)
finalResult[i].remove(at: finalResult[i].index(before: finalResult[i].endIndex))
}
}
}
catch
{
print("IGAGribReader error: No regex match")
}
}
catch
{
print("IGAGribReader error: URL content is not read")
}
return finalResult;
}
I have been trying to fix it for the past several weeks but in vain. Any help would be much appreciated!
let contents = try String(contentsOf: dataURL as URL)
You are calling String(contentsOf: url) on the main thread (main queue). This downloads the content of the URL into a string synchronously The main thread is used to drive the UI, running synchronous network code is going to freeze the UI. This is a big no-no.
You should never call readWebToString() in the main queue. Doing DispatchQueue.main.async { self.downloadWindsAloftData() } exactly put the block in the main queue which we should avoid. (async just means "execute this later", it is still executed on Dispatch.main.)
You should just run downloadWindsAloftData in the global queue instead of main queue
DispatchQueue.global(qos: .default).async {
self.downloadWindsAloftData()
}
Only run DispatchQueue.main.async when you want to update the UI.
Your stack trace is telling you that it's stopping at String(contentsOf:), called by readWebToString, called by makeGribWebAddress.
The problem is that String(contentsOf:) performs a synchronous network request. If that request takes any time, it will block that thread. And if you call this from the main thread, your app may freeze.
Theoretically, you could just dispatch that process to a background queue, but that merely hides the deeper problem, that you are doing a network request with an API that is synchronous, non-cancellable, and offers no meaningful error reporting.
You really should doing asynchronous requests with URLSession, like you have elsewhere. Avoid using String(contentsOf:) with remote URL.
I'm using a completionHandler in this function, however it's nested within several for loops (below). The problem is the handler where it is now gets called every time the loop it's in runs, whereas I only want the handler to pass in the Set when the entire function has completed processing. If I place it outside of the loop, then it gets called too early and is empty. What should I do here?
Right now when I print to the console to test it prints:
Set item 1
Set item 1, 2
Set item 1, 2, 3 etc.
struct RekoRequest {
public func getRekos(rekoType: rekoCategory, handler: #escaping (Set<String>) -> Void) {
var urls = [NSURL]()
var IDs = Set<String>()
TwitterRequest().fetchTweets(searchType: "things") { result in
guard let tweets = result as? [TWTRTweet] else {print("Error in getRekos receiving tweet results from TwitterRequest.fetchTweets"); return}
for tweet in tweets {
let types: NSTextCheckingResult.CheckingType = .link
let detector = try? NSDataDetector(types: types.rawValue)
guard let detect = detector else { print("NSDataDetector error"); return }
let matches = detect.matches(in: text, options: .reportCompletion, range: NSMakeRange(0, (text.characters.count)))
for match in matches {
if let url = match.url {
guard let unwrappedNSURL = NSURL(string: url.absoluteString) else {print("error converting url to NSURL");return}
//Show the original URL
unwrappedNSURL.resolveWithCompletionHandler {
guard let expandedURL = URL(string: "\($0)") else {print("couldn't covert to expandedURL"); return}
guard let urlDomain = expandedURL.host else { print("no host on expandedURL"); return }
switch urlDomain {
case "www.somesite.com":
let components = expandedURL.pathComponents
for component in components {
if component == "dp" {
guard let componentIndex = components.index(of: component) else {print("component index error"); return}
let IDIndex = componentIndex + 1
let ID = components[IDIndex]
//Filter out Dups and add to Set
IDs.insert(ID)
handler(IDs)
print(ID) //this prints multiple sets of IDs, I only want one when the function is finished completely
}
}
break;
default:
break;
}
}
} else { print("error with match.url") }
} //for match in matches loop
} //for tweet in tweets loop
}
}
}
// Create an extension to NSURL that will resolve a shortened URL
extension NSURL
{
func resolveWithCompletionHandler(completion: #escaping (NSURL) -> Void)
{
let originalURL = self
let req = NSMutableURLRequest(url: originalURL as URL)
req.httpMethod = "HEAD"
URLSession.shared.dataTask(with: req as URLRequest)
{
body, response, error in completion(response?.url as NSURL? ?? originalURL)
}
.resume()
}
}
Call your completion handler after the for loop.
for component in components {
if component == "dp" {
...
}
}
handler(IDs)
Important: The handler should be called outside of the for loop, but within the TwitterRequest().fetchTweets() trailing closure.
Approaches to handling an empty set
Your IDs are being initialized to an empty set. Only after meeting certain conditions within your for loop are values being inserted into this set. If these conditions aren't met, then your IDs set will be empty.
If this is undesirable, then you will have to either make changes to your completion handler or alter your conditional logic so that you always get a non-empty set.
One approach might be to have optional set in your callback. Something like:
(Set<String>?) -> Void
If IDs are empty, then callback with a nil and have your calling code handle the possibility of a nil set.
Another approach might be to create an enum to encapsulate your result and use this in your callback. Something like:
Enum
enum Result {
case success(Set<String>)
case failure
}
Callback
handler: (Result) -> Void
Usage
handler(.success(IDs))
// or
handler(.failure)
Calling Code
getReckos(rekoType: .someType) { result in
switch result {
case .success(let IDs):
// Use IDs
case .failure:
// Handle no IDs
}
}
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?
I am writing a Swift iOS app (my first, so please bear with me) where I use Swifter HTTP server to process various requests. One such request is an HTTP POST with a JSON array specifying images to download from the web (and do some other stuff, not pertinent to the issue at hand).
I use Alamofire to download the images (this works fine), but I am looking for good (preferably simple) way to wait for all the images to finish downloading before returning a response to the POST request above (since the response has to contain JSON indicating the result, including any failed downloads).
What is a good way to accomplish this (preferably w/o blocking the main thread)?
Here are some snippets to illustrate:
public func webServer(publicDir: String?) -> HttpServer {
let server = HttpServer()
server.POST["/images/update"] = { r in
let images = ...(from JSON array in body)
let updateResult = ImageUtil.updateImages(images)
let resultJson: String = Mapper().toJSONString(updateResult, prettyPrint: true)!
if updateResult.success {
return .OK(.Text(resultJson))
}
return HttpResponse.RAW(500, "Error", nil, { $0.write([UInt8](updateResult.errorMessage.utf8)) })
}
}
static func updateImages(images: [ImageInfo]) -> UpdateResult {
let updateResult = UpdateResult()
for image in images {
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.validate()
.response{_, _, _, error in
if let error = error {
Log.error?.message("Error downloading file \(image.imageUrl) to \(image.fileName): \(error)")
} else {
updateResult.filesDownloaded++
Log.info?.message("Downloaded file \(image.imageUrl) to \(image.fileName)")
}}
}
return updateResult // It obviously returns before any images finish downloading. I need to wait until all images have downloaded before I can return an accurate result.
}
Update 1/23/2016, using dispatcher per bbum
This is an attempt to use the dispatcher mechanism, but the call to updateImages still return right away (even when using dispatch_sync).
How can I await the completion of all downloads before returning my HTTP response to the caller?
public func webServer(publicDir: String?) -> HttpServer {
let server = HttpServer()
server.POST["/images/update"] = { r in
let imageDownloader = ImageDownloader()
imageDownloader.updateimageFiles(adFilesOnServer)
let resultJson: String = Mapper().toJSONString(imageDownloader.updateResult, prettyPrint: true)!
if imageDownloader.updateResult.success {
return .OK(.Text(resultJson))
}
return HttpResponse.RAW(500, "Error", nil, { $0.write([UInt8](imageDownloader.updateResult.errorMessage.utf8)) })
}
}
class ImageDownloader {
var updateResult = AdUpdateResult()
private var imageFilesOnServer = [ImageFile]()
private let fileManager = NSFileManager.defaultManager()
private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.imageDirectory, isDirectory: true)
private let semaphore = dispatch_semaphore_create(4)
private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)
func updateimageFiles(imageFilesOnServer: [ImageFile]) {
self.imageFilesOnServer = imageFilesOnServer
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
for serverFile in imageFilesOnServer {
downloadImageFileFromServer(serverFile)
}
dispatch_sync(downloadQueue) {
dispatch_sync(dispatch_get_main_queue()) {
print("done") // It gets here before images have downloaded.
}
}
}
private func downloadImageFileFromServer(serverFile: ImageFile) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.validate()
.response { _, _, _, error in
if let error = error {
Log.error?.message("Error downloading file \(serverFile.imageUrl) to \(serverFile.fileName): \(error)")
} else {
self.updateResult.filesDownloaded++
Log.info?.message("Downloaded file \(serverFile.imageUrl) to \(serverFile.fileName)")
}
dispatch_semaphore_signal(self.semaphore)
}
}
}
First, you really don't want to be firing off a request-per-image without some kind of a throttle. Semaphores work well for that sort of thing.
Secondly, you need to basically count the number of operations outstanding and then fire a completion handler when they are all done. Or, if new operations can be started at any time, you'll probably want to group operations.
So, pseudo code:
sema = dispatch_semaphore_create(4) // 4 being # of concurrent operations allowed
serialQ = dispatch_queue_create(.., SERIAL)
dispatch_async(serialQ) {
dispatch_semaphore_wait(sema, FOREVER) // will block if there are 4 in flight already
for image in images {
downloader.downloadAsync(image, ...) { // completion
dispatch_semaphore_signal(sema) // signal that we are done with one
... handle downloaded image or error ...
... add downloaded images to downloadedImages ...
}
}
}
dispatch_async(serialQ) {
// since serialQ is serial, this will be executed after the downloads are done
dispatch_async(main_queue()) {
yo_main_queue_here_be_yer_images(... downloadedImages ...)
}
}