Run Modal after async call made - swift

I am new to Swift Mac App development, I am having troubling going from a login window to showing the main window after a login URLRequest, and making another URLRequest in the new main window. If I just go from one window without making the login URLRequest, it works fine.
func loadMainView() {
self.view.window?.close()
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let mainWindowController = storyboard.instantiateController(withIdentifier: "MainViewController") as! NSWindowController
if let mainWindow = mainWindowController.window {
let application1 = NSApplication.shared()
application1.runModal(for: mainWindow)
}
}
func tryLogin(_ username: String, password: String ) {
Staff.login(username: self.username.stringValue, password: self.password.stringValue) { (completed, result, staff) in
// DispatchQueue.main.async {
if completed {
if result == .Success && staff != nil {
DispatchQueue.main.async(execute: {
self.loadMainView()
})
} else {
self.dialogOKCancel(message: "Error", text: "Your credentials are incorrect")
}
} else {
print("error")
}
}
}
HTTPSConnection.httpGetRequestURL(token: token, url: digialOceanURL, mainKey: mainKeyVal) { ( complete, results) in
DispatchQueue.main.async {
if complete {
}
}
}
I have tried calling the self.loadMainView() without the execute, but still not luck.
Any help appreciated. Thanks

Don't run your main view in modal, modal should be used for dialogs. So you can run login view in modal (and finish it by calling stopModal on the application). In that case you could use smth like loadLoginView which will have similar implementation to your current loadMainView (but without this self.view.window?.close() call. And main view would be loaded from nib on application launch. But you have to post some more code (how your app initalization looks like?) to get help on that.

Related

Swift: Ignored request to start a new background task because RunningBoard has already started the expiration timer

I have a series of tasks that is triggered by a silent push notification. Upon receiving the push notification, it wakes the iOS up in the background and performs the following tasks:
Opens up a WebViewController that contains a WKWebview
Goes to a webpage, and clicks some buttons automated by javascript injection
Once completed, dismisses the WebViewController
I have added selected BackgroundTasks handlers to manage it by following this tutorial but the console is flooded with the following warning.
[ProcessSuspension] 0x280486080 - WKProcessAssertionBackgroundTaskManager: Ignored request to start a new background task because RunningBoard has already started the expiration timer
Note that the tasks that needs to be done are still performed correctly.
class WebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
lazy var webView: WKWebView = {
let v = WKWebView()
v.translatesAutoresizingMaskIntoConstraints = false
v.navigationDelegate = self
return v
}()
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
//Remove BG task when not needed
deinit {
NotificationCenter.default.removeObserver(self)
endBackgroundTask()
}
override func viewDidLoad() {
super.viewDidLoad()
//Register notification for background task
NotificationCenter.default.addObserver(self,
selector: #selector(reinstateBackgroundTask),
name: UIApplication.didBecomeActiveNotification,
object: nil)
registerBackgroundTask()
//Load webview with URL
if let url = url {
let request = URLRequest(url: url)
webView.load(request)
}
}
//MARK:- Handle BG Tasks
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
}
func endBackgroundTask() {
Log("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
#objc func reinstateBackgroundTask() {
if backgroundTask == .invalid {
registerBackgroundTask()
}
}
func endBackgroundTaskIfNotInvalid() {
if backgroundTask != .invalid {
self.endBackgroundTask()
}
}
//This is the final task that needs to be done
fileprivate func updateScheduler(visitedPlace: VisitedPlace) {
if navigator == .scheduler {
if let jobId = jobId {
let data = [
"status": "scheduled",
"completedOn": Date()
] as [String : Any]
///Do some work here...
//Dismiss controller after completing
self.dismiss(animated: true) {
self.endBackgroundTaskIfNotInvalid()
}
}
} else {
self.endBackgroundTaskIfNotInvalid()
}
}
}
What is triggering all these warnings and how do I silence it?
I'm having the same console flood. For me it turned out to be adMob that was the cause.
This happens to me when I run unit tests that wait for test expectations to be filled. I was hoping that it was just a simulator issue, since I don't see it in production, but it sounds like that's not the case.
I fixed the flood by removing the AdMob banner from the view hierarchy on app suspension:
self.bannerView?.removeFromSuperview()
AdMob would still try infrequently which would generate a single log message.

Tableview not loading data after popping View Controller

When I pop the view controller stack, I need a table view in the first view controller to reload. I am using viewWillAppear (I already tried viewDidAppear and it didn't work).
override func viewWillAppear(_ animated: Bool) {
print("will appear")
loadData()
}
Once the view controller has re-appeared, I need to query the API again which I am doing in another service class with a completion handler of course and then reloading the table view:
#objc func loadData() {
guard let userEmail = userEmail else { return }
apiRequest(userId: userEmail) { (queriedArticles, error) in
if let error = error {
print("error in API query: \(error)")
} else {
guard let articles = queriedArticles else { return }
self.articlesArray.removeAll()
self.articleTableView.reloadData()
self.articlesArray.append(contentsOf: articles)
DispatchQueue.main.async {
self.articleTableView.reloadData()
}
}
}
}
What happens is that I am able to pop the stack and see the first view controller BUT it has the same data as it did before. I expect there to be one more cell with new data and it doesn't appear. I have to manually refresh (using refresh control) to be able to query and load the new data.
Any idea what I am doing wrong?

Executing code after nested completion handlers

I am writing a Safari app extension and want to fetch the URL for the active page in my view controller.
This means nested completion handlers to fetch the window, to fetch the tab, to fetch the page, to access its properties. Annoying but simple enough. It looks like this:
func doStuffWithURL() {
var url: URL?
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage { (page) in
page?.getPropertiesWithCompletionHandler { (properties) in
url = properties?.url
}
}
}
}
// NOW DO STUFF WITH THE URL
NSLog("The URL is \(String(describing: url))")
}
The obvious problem is it does not work. Being completion handlers they will not be executed until the end of the function. The variable url will be nil, and the stuff will be done before any attempt is made to get the URL.
One way around this is to use a DispatchQueue. It works, but the code is truly ugly:
func doStuffWithURL() {
var url: URL?
let group = DispatchGroup()
group.enter()
SFSafariApplication.getActiveWindow { (window) in
if let window = window {
group.enter()
window.getActiveTab { (tab) in
if let tab = tab {
group.enter()
tab.getActivePage { (page) in
if let page = page {
group.enter()
page.getPropertiesWithCompletionHandler { (properties) in
url = properties?.url
group.leave()
}
}
group.leave()
}
}
group.leave()
}
}
group.leave()
}
// NOW DO STUFF WITH THE URL
group.notify(queue: .main) {
NSLog("The URL is \(String(describing: url))")
}
}
The if blocks are needed to know we are not dealing with a nil value. We need to be certain a completion handler will return, and therefore a .leave() call before we can call a .enter() to end up back at zero.
I cannot even bury all that ugliness away in some kind of getURLForPage() function or extension (adding some kind of SFSafariApplication.getPageProperties would be my preference) as obviously you cannot return from a function from within a .notify block.
Although I tried creating a function using queue.wait and a different DispatchQueue as described in the following answer to be able to use return…
https://stackoverflow.com/a/42484670/2081620
…not unsurprisingly to me it causes deadlock, as the .wait is still executing on the main queue.
Is there a better way of achieving this? The "stuff to do," incidentally, is to update the UI at a user request so needs to be on the main queue.
Edit: For the avoidance of doubt, this is not an iOS question. Whilst similar principles apply, Safari app extensions are a feature of Safari for macOS only.
Thanks to Larme's suggestions in the comments, I have come up with a solution that hides the ugliness, is reusable, and keep the code clean and standard.
The nested completion handlers can be replaced by an extension to the SFSafariApplication class so that only one is required in the main body of the code.
extension SFSafariApplication {
static func getActivePageProperties(_ completionHandler: #escaping (SFSafariPageProperties?) -> Void) {
self.getActiveWindow { (window) in
guard let window = window else { return completionHandler(nil) }
window.getActiveTab { (tab) in
guard let tab = tab else { return completionHandler(nil) }
tab.getActivePage { (page) in
guard let page = page else { return completionHandler(nil) }
page.getPropertiesWithCompletionHandler { (properties) in
return completionHandler(properties)
}
}
}
}
}
}
Then in the code it can be used as:
func doStuffWithURL() {
SFSafariApplication.getActivePageProperties { (properties) in
if let url = properties?.url {
// NOW DO STUFF WITH THE URL
NSLog("URL is \(url))")
} else {
// NOW DO STUFF WHERE THERE IS NO URL
NSLog("URL ERROR")
}
}
}

Firebase Login Persistence Swift

I'm using Firebase to handle my user register and login for my app. But if I log in, and then close my app entirely - the user is forced to re-log in. I'd like to keep the user logged in unless they click "Log out"
My login code is this:
Auth.auth().signIn(withEmail: email, password: password, completion: {(user, error) in
if let firebaseError = error {
print(firebaseError.localizedDescription)
return
}
self.presentTabBar()
})
}
}
How do I keep this user logged in unless specifically told to logout?
Here's a handy full example for 2020:
Anywhere in your iOS+Firebase app, you can simply say:
guard let uid = Auth.auth().currentUser?.uid else {
return print("no current user!")
}
Thus, on the launch screen of your app, simply:
import UIKit
import Firebase
import FirebaseUI
class ViewController: UIViewController, FUIAuthDelegate {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let uid = Auth.auth().currentUser?.uid else {
return print("no current user, hence AuthUI flow...")
basicAuthUIFlow()
}
print("User .. \(Auth.auth().currentUser?.displayName)")
continueWhenUserPresent()
}
It's that easy. So then as usual ...
private func basicAuthUIFlow() {
let authUI = FUIAuth.defaultAuthUI()
authUI?.delegate = self
let pp: [FUIAuthProvider] = [ FUIGoogleAuth() ]
authUI?.providers = pp
if let authVC = authUI?.authViewController() {
present(authVC, animated: true)
}
}
func authUI(_ authUI: FUIAuth,
didSignInWith authDataResult: AuthDataResult?,url: URL?, error: Error?) {
let u = authDataResult?.user
print("Successful login via authUI \(String(describing: u?.displayName))")
continueWhenUserPresent()
}
private func continueWhenUserPresent() {
.. pushViewController .. your first screen
}
Check if the user is logged in or not:
if Auth.auth().currentUser != nil {
// User is signed in.
// ...
} else {
// No user is signed in.
// ...
}
if the user is logged in, then go the Home ViewController. This way when he opens the app again he will go to the Home ViewController unless he sign outsFIRAuth.auth().signOut()
For more info check this: https://firebase.google.com/docs/auth/ios/manage-users
For keep user login you need to check currentUser Auth session, If it's not nil then you can redirect user to Home screen.
Call "setRootViewController" method from didFinishLaunchingWithOptions, just after FirebaseApp.configure() code
Swift 4
func setRootViewController() {
if Auth.auth().currentUser != nil {
// Set Your home view controller Here as root View Controller
self.presentTabBar()
} else {
// Set you login view controller here as root view controller
}
}

Update Image When Parse Query Done

I'm using Parse for my app written in Swift. It is a golf app that allows the user to have a profile. The user can edit the profile on an edit profile view controller, save it and then they're taken back to the main profile view controller. The problem I'm having is if the user changes their profile image and saves it, the image isn't updated on the main profile view controller but the rest of the new profile info is. My belief is the timing is off with the Parse query and the image isn't coming back in time. Here is my query for my main profile page. I tried using the "dispatch_async" method but this doesn't seem to be working. Thanks in advance.
func getProfileFromBackground() {
profileData.removeAll()
if let userQuery = PFUser.query() {
userQuery.whereKey("username", equalTo: (PFUser.currentUser()?.username)!)
userQuery.findObjectsInBackgroundWithBlock({ (currentUserProfile:[PFObject]?, error: NSError?) -> Void in
if error == nil {
for object:PFObject in currentUserProfile! {
self.profileData.append(object)
for data in self.profileData {
dispatch_async(dispatch_get_main_queue()) {
self.golferNameLabel.text = data.objectForKey("name") as? String
self.usernameLabel.text = "Username: \(data.objectForKey("username")!)" as String
self.golferProfileImage.file = data.objectForKey("profileImage") as? PFFile
self.golferProfileImage.loadInBackground()
}
}
}
} else {
print(error)
}
})
}
}