i have a behavior i cant resolve.
I have a controller (controller 1) where i check some defaults value. if not present (first time use of app: check login and pwd) i present (modal) a settings vc:
IN CONTROLLER 1
override func viewDidAppear(_ animated: Bool) {
if(!isKeyPresentInUserDefaults(key: "username")) {
NSLog("username not present")
let vc = self.storyboard?.instantiateViewController(withIdentifier: "settings") as! SettingsViewController
self.present(vc, animated: true, completion: nil)
}
}
in this vc (controller 2) (also in the main storyboard) i have a button done. When pressed, it is associated to the:
IN CONTROLLER 2: SettingsVc -> ID : settings
#IBAction func doneSettings(sender: AnyObject) {
if(isInternetAvailable()) {
NSLog("internet available")
login { (result) in
switch result
{
case .Success(let result):
//print(result)
self.dismissSelf()
break
case .Failure(let error):
print(error)
break
}
}
}
else {
NSLog("internet not available")
}
}
the dismissSelf func is defined in the Settingsvc as:
func dismissSelf() {
NSLog("dismissSettingsVC")
self.dismiss(animated: false, completion: nil)
}
the login func is defined in another class, dealing with networking stuff and is as is:
func login(completion: #escaping (AsyncResult<[CustomUserObject]>)->())
{
let myUrl = URL(string: "http://www.xxxx.com/api/api.php");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"// Compose a query string
let postString = "u=login&s=password&cmd=login";
request.httpBody = postString.data(using: String.Encoding.utf8);
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest){
(data, response, error) -> Void in
if let error = error
{
completion(AsyncResult.Failure(error as NSError?))
} else {
let result: [CustomUserObject] = []//deserialization json data into array of [CustomUserObject]
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("*********response data = \(responseString)")
completion(AsyncResult.Success(result))
}
}
task.resume()
}
So, I launch the app for the first time (no login, no pwd in defaults), then the Settingvc is presented. I press the done button with param hardcoded. The login is correctly called, I receive the answer correctly, and on completion i should dismiss the Settingvc.
thing is, i see the NSLOG for dismiss, but the dismiss appears seconds after the network func completion (from 10 secs to up to a minute), which I can't understand. Why such a delay? any idea?
2017-11-27 21:54:33.262892 app[998:211517] username not present
2017-11-27 21:54:36.119754 app[998:211517] internet available
*********response data =
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-27 21:54:38.472306 app[998:211542] dismissSettingsVC
2017-11-27 21:54:48.048095 app[998:211517] username not present
in this case, it took 10 sec to dismiss the Settingsvc after receiving the login results.
another one:
2017-11-27 22:04:20.364097 app[998:211517] internet available
*********response data =
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-27 22:04:22.495642 app[998:212974] dismissSettingsVC
2017-11-27 22:05:00.049177 app[998:211517] username not present
in this other case, it took 38 sec to dismiss the Settingsvc after receiving the login results.
EDITED
I tried not using a vc presented. Instead in controller 1, i added a view that i first set as visible if username in defaults does not exist and then that I will hide after the login completion. in this view, i added a button to call the loginAction.
#IBAction func loginAction(sender: AnyObject) {
if(isInternetAvailable()) {
NSLog("internet available")
login { (result) in
switch result
{
case .Success(let users):
print(users)
self.loginView.isHidden = true
NSLog("login ok: hiding view")
break
case .Failure(let error):
print(error ?? "ERROR")
break
}
}
}
else {
NSLog("internet not available")
}
}
Same result:
I see the completion and the received data:
2017-11-28 18:17:34.314706 cellar[1270:311710] username not present
2017-11-28 18:17:35.066333 cellar[1270:311710] internet available
2017-11-28 18:17:35.076930 cellar[1270:311710] done login
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-28 18:17:37.655829 cellar[1270:311763] login ok: hiding view
the view should be hidden before the NSLOG "login ok: hiding view". Instead, the UI is updated seconds after (about a min, but variable)
What would avoid the UI to be updated for so long as I wait the completion of the network stuff to perform the UI update?
UPDATE:
weird situation:
as soon as I get the network completion result, by changing the orientation, the dismiss appears right away:
Optional({"cmd":"login","success":"true","message":"login succeded"})
2017-11-28 22:28:30.620408 cellar[1461:360470] dismiss
2017-11-28 22:28:31.537588 cellar[1461:360413] username not present
2017-11-28 22:28:32.126759 cellar[1461:360413] [App] if we're in the
real pre-commit handler we can't actually add any new fences due to CA
restriction
your help is much that appreciated. Thanks
Not sure if this is the reason for your problem but it looks like you aren't running the dismiss() call on the main thread. You should call all UI code on the main thread. Wrap it as follows
case .Success(let result):
//print(result)
DispatchQueue.main.async {
self.dismissSelf()
}
break
Related
I've made a widget which fetches Codable data and it's working just fine in the simulator ONLY. The widget updates within 30 seconds or less after the data has changed. I've set a 5 minute update limit (I understand it's called far less frequently). It's working actually really great in the simulator without any kind of background data fetches and updates in less time than I set in getTimeline. Then I ran into an issue on a a real test device.
The data won't update anywhere between 2-10+ mins while testing a real device, in the snapshot it's updated and can see the new data changes but not in the widget on springboard. I don't understand why the simulator works just fine but not a real device. The Widget is definitely being updated when the data changes but only in the Simulator so am I suppose to fetch data in the background?
I've come across this Keeping a Widget Up To Date | Apple Developer Documentation. I'm still very new to Swift and SwiftUI so this is a little bit harder for me to grasp. I'm trying to understand the section Update After Background Network Requests Complete to update my Codeable data. My guess is the simulator is different from a real device and I need to fetch data in the background for the must up to date data?
The end goal is to have the widget update as frequently as possible with the most current data. I'm not sure I even need the background data fetch?
My data model for my widget as an example (which is working fine)
class DataModel {
var data: DataClass = DataClass(results: []))
func sessions(_ completion: #escaping (DataClass -> Void) {
guard let url = URL(string: "URL HERE") else { return }
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(DataClass.self, from: data) {
self.data = response
completion(self.data)
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
}
}
}
.resume()
}
}
My getTimeline calling the data model
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let model = DataModel()
var entries: [SimpleEntry] = []
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
let entry = SimpleEntry(date: entryDate, model: model)
entries.append(entry)
model.sessions {_ in
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
I have this for my background network request
import Foundation
import WidgetKit
class BackgroundManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
var completionHandler: (() -> Void)? = nil
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "widget-bundleID")
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func update() {
let task = urlSession.downloadTask(with: URL(string: "SAME URL FROM DATA MODEL HERE")!)
task.resume()
}
func urlSession(_ session: URLSession ,downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print (location)
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
self.completionHandler!()
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
print("Background update")
}
}
Then in my Widget I set .onBackgroundURLSessionEvents(). I never see any background updates or errors in the console. This seems very wrong, the Codable data will never be updated? How do I properly update my data in the background?
struct Some_Widget: Widget {
let kind: String = "Widget"
let backgroundData = BackgroundManager()
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
SomeWidget_WidgetEntryView(entry: entry)
}
.configurationDisplayName("Widget")
.description("Example widget.")
.onBackgroundURLSessionEvents { (sessionIdentifier, completion) in
if sessionIdentifier == self.kind {
self.backgroundData.update()
self.backgroundData.completionHandler = completion
print("background update")
}
}
}
}
I'm trying to dismiss a .sheet in SwiftUI, after calling an async process to confirm the user's MFA code. (I'm using the AWS Amplify Framework).
I have a binding variable set on the main view, and reference it in the view the sheet presents with #Binding var displayMFAView: Bool. I have an authentication helper that tracks the user state: #EnvironmentObject var userAuthHelper: UserAuthHelper.
The following code dismisses the sheet as expected:
func confirmMFACode(verificationCode: String) {
// Code to confifm MFA...
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
However, if I call the auth process via Amplify's confirmSignIn method,
func confirmVerificationMFA(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) { (signInResult, error) in
if let error = error as? AWSMobileClientError {
// ... error handling ...
} else if let signInResult = signInResult {
switch (signInResult.signInState) {
case .signedIn:
print("User confirmed MFA")
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
default:
print("\(signInResult.signInState.rawValue)")
}
}
}
}
the sheet does not get dismissed. I have tried wrapping the variable assignment in DispatchQueue.main.async {..., but that hasn't solved the issue either.
...
DispatchQueue.main.async {
self.userAuthHelper.isSignedIn = true
self.displayMFAView = false
}
...
In fact, this throws the following into my logs:
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.
Wrapping the switch (... in a DispatchQueue per https://stackoverflow.com/a/58288437/217101 gave me the same warning in my log.
Admittedly I don't have a firm grasp on SwiftUI or AWS Amplify. What am I not understanding?
From what I can tell the async call does something unexpected with the state variables, but not with an EnvironmentObject. So, nstead of #Binding var displayMFAView: Bool, I stored displayMFAView in an EnvironmentObject,
#EnvironmentObject var settings: UserSettings
#State var mfaCode: String = ""
and then can show or hide the .sheet(... by updating a boolean in that object:
Button(action: {
self.signIn() // Async call happens here
self.settings.displayMFAView.toggle()
}) {
Text("Sign In")
}.sheet(isPresented: self.$settings.displayMFAView) {
// Example code to capture text
TextField("Enter your MFA code", text: self.$mfaCode)
}
Button(action: {
self.verifyMFACode(verificationCode: self.mfaCode) // async call
}) {
Text("Confirm")
}
In func verifyMFACode(), I can make an async call to validate my user, then toggle the sheet to disappear on success:
func verifyMFACode(verificationCode: String) {
AWSMobileClient.default().confirmSignIn(challengeResponse: verificationCode) {
...
case .signedIn:
self.settings.displayMFAView.toggle()
...
I am working on a login view and trying to change the border color of a UITextField in Xcode/swift3 when validation of the textfield fails. The UITextField should get a red border color.
The problem is that if enter an email, then a password and then press the submit button, i have to focus email text field again before it gets a red border.
This is my LoginViewController.swift so far:
import Foundation
import UIKit
class LoginViewController : UIViewController, UITextFieldDelegate {
#IBOutlet weak var userEmailTextField: UITextField!
#IBOutlet weak var userPasswordTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
// login button action
#IBAction func loginButtonTabbed(_ sender: Any) {
// getting values from text fields
let userEmail = userEmailTextField.text;
let userPassword = userPasswordTextField.text;
// set enpoind data
let requestURL = NSURL(string: Constants.apiUrl)
//creating a task to send the post request
var request = URLRequest(url: requestURL as! URL)
request.httpMethod = "POST"
let postString = "cmd=addUser&email="+userEmail!+"&password="+userPassword!
request.httpBody = postString.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
do {
let json = try? JSONSerialization.jsonObject(with: data, options: [])
// store json response to dictionary
if let dictionary = json as? [String: Any] {
// check if we got validation errors
if let nestedDictionary = dictionary["validation"] as? [String: Any] {
// display validation messages on device
if let emailMsg = nestedDictionary["Email"] as? String { // change color of textfield
self.userEmailTextField.errorField()
}
}
}
} catch let error as NSError {
print(error)
}
}
//executing the task
task.resume()
}
}
and the UITextField extension UITextField.swift:
import Foundation
import UIKit
extension UITextField {
func errorField(){
self.layer.borderColor = UIColor(red: 255/255.0, green: 59/255.0, blue: 48/255.0, alpha: 1.0).cgColor
self.layer.borderWidth = 1.0;
}
}
When you're doing a network call, it always happens in the background...so in order to do any kind of UI updates you need to be on the main queue. Just put the self.userEmailTextField.errorField() inside DispatchQueue.main.async {...} so it would be done immediately.
Also haven't really tested your code very well. Why?
Even in your current code the border would still turn red, but it turns red after almost like 6-7 seconds (it could take less or more for you)...because it's being ran from background thread.
What I don't understand is why clicking on the textField again brings the red border right away!? Here's what I'm guessing happens:
From the background thread you update the model ie change the textField color which queues the UI/view to be updated...but since we're on a background queue, that UI updated could take a few seconds to happen
But then you tapped on the textField right away and forced a super quick read of the textField and all its properties which includes the border—from main thread (actual user touches are always handled through main thread)...which even though are not yet red on the screen, but since it's red on the model it will read from it and change color to red immediately.
I have an endpoint which takes in a phone number and sends a code to the number, but also returns that same message to the data section of the session that called it.
All of that works, but the problem I'm having is that, after the session makes the call, I'm segueing to the next screen and i'm passing that code into the next controller. But i think the api is responding too slow, so by time the segue (and prep for segue) has happened the code has not been returned yet. How can i fix this?
let scriptURL = "https://---------------/api/verify/sms?"
let urlWithParams = scriptURL + "number=\(phone.text!)"
let myUrl = NSURL(string: urlWithParams)
let request = NSMutableURLRequest(url: myUrl! as URL)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
//print(error?.localizedDescription)
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as AnyObject
self.currentCode = json["code"]!! as! String //-> This is the code the is returned from the api call
}catch{
print("error with serializing JSON: \(error)")
}
}
task.resume()
self.performSegue(withIdentifier: "toVerifyCode", sender: (Any?).self)
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if segue.identifier == "toVerifyCode"{
let newController = segue.destination as! verifyCodeController
newController.code = self.currentCode
}
}
The problem is that you placed self.performSegue(withIdentifier: "toVerifyCode", sender: (Any?).self) not in the closure.
So, you have to place it like this:
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
//print(error?.localizedDescription)
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as AnyObject
//on main thread
DispatchQueue.main.async {
self.currentCode = json["code"]!! as! String //-> This is the code the is returned from the api call
self.performSegue(withIdentifier: "toVerifyCode", sender: (Any?).self)
}
}catch{
print("error with serializing JSON: \(error)")
}
}
task.resume()
Also, please note that your closure is executed asynchronously, so I wrapped the call to be executed on main thread by using GCD.
Thanks to Apple my iOS 9 Project 'Swift 2.3' is completely unusable with iOS 10's 'Swift 3'...
I fixed almost everything except that I am having issue with using NSURLSession, Xcode is telling me that it has been renamed to URLSession, if I rename it Xcode will tell me:
use of undeclared type URLSession
Foundation is imported.
What is the issue?!
For example I am using it this way...
lazy var defaultSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "reCoded.BGDownload")
configuration.sessionSendsLaunchEvents = true
configuration.isDiscretionary = true
let session = URLSession(configuration: configuration, delegate: self, delegateQueue, queue: nil)
return session
}()
and even with the delegate methods the same issue.
Try using Foundation.URLSession where ever you use URLSession.
/Got it to work/ In some cases try to copy your code somewhere else then remove everything in your class that uses URLSession then type the session methods again and put back your copied code you should be fine.
Update your URLSessin functions with;
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.data.append(data as Data)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON()
}
}
I can explain how but by playing around with the code I got this to work in SWIFT 3 after two days of frustration. I guess SWIFT 3 removed a lot of unnecessary words.
let task = Foundation.URLSession.shared.dataTask(with: <#T##URL#>, completionHandler: <#T##(Data?, URLResponse?, Error?) -> Void#>)
Here's where I am right now. It's not perfect but works maybe half of the time.
First, in the class where my URLsession is defined:
import Foundation
class Central: NSObject, URLSessionDataDelegate, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
I don't think all of that is necessary, but there it is. Then here is the function that is called by my background fetch:
func getWebData() {
var defaults: UserDefaults = UserDefaults.standard
let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: "myBGconfig")
let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil)
urlString = "https://www.powersmartpricing.org/psp/servlet?type=dayslider"
if let url = URL(string: urlString) {
let rateTask = backgroundSession.downloadTask(with: URL(string: urlString)!)
rateTask.taskDescription = "rate"
rateTask.resume()
}
When the task comes back:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) {
if downloadTask.taskDescription == "rate" { // I run 2 web tasks during the session
if let data = NSData(contentsOf: location) {
var return1 = String(data: data as! Data, encoding: String.Encoding.utf8)!
DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + 0.2){
var defaults: UserDefaults = UserDefaults.standard
defaults.set(myNumber, forKey: "electricRate") // myNumber is an extract of the text in returned web data
defaults.set(Date(), forKey: "rateUpdate")
defaults.synchronize()
self.calcSetting() //Calls another function defined in the same class. That function sends the user a notification.
let notificationName = Notification.Name("GotWebData")
NotificationCenter.default.post(name: notificationName, object: nil)
} // Closes the Dispatch
}
if session.configuration.identifier == "myBGconfig" {
print("about to invalidate the session")
session.invalidateAndCancel()
}
}
I haven't figured out yet how to kill the session when BOTH tasks have completed, so right now I kill it when either one is complete, with invalidateAndCancel as above.
And finally, to catch errors:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didCompleteWithError: Error?) {
if downloadTask.taskDescription == "rate" {
print("rate download failed with error \(didCompleteWithError)")
}
if downloadTask.taskDescription == "other" {
print("other download failed with error \(didCompleteWithError)")
}
downloadTask.resume() // I'm hoping this retries if a task fails?
}
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
if let error = error as? NSError {
print("invalidate, error %# / %d", error.domain, error.code)
} else {
print("invalidate, no error")
}
}