I've an XCode project programmed in swift with a registration/connection process. When you're connected, even if the app restarted you don't repeat this process until you click on "log out" button.
I've noticed that if you follow the registration/connection process when you start the app, memory is about 500Mb whereas if the app started with a connected user, memory is about 140Mb.
Registration process is composed of 3 view controllers connected with custom segue. Access to logged in view is made by following code :
func switchRootViewController(rootViewController: UIViewController, animated: Bool, completion: (() -> Void)?)
{
if animated
{
UIView.transitionWithView(window!, duration: 0.5, options: .TransitionCrossDissolve, animations:
{
let oldState: Bool = UIView.areAnimationsEnabled()
UIView.setAnimationsEnabled(false)
self.window!.rootViewController = rootViewController
UIView.setAnimationsEnabled(oldState)
}, completion: { (finished: Bool) -> () in
if (completion != nil)
{
completion!()
}
})
}
else
{
window!.rootViewController = rootViewController
}
}
Where rootViewController is replace by the logged in view I want to load as you can see here :
func goToNextView()
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let homeViewController = storyboard.instantiateViewControllerWithIdentifier("conversationViewController") as!ConversationViewController
homeViewController.currentUser = self.currentUser
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.switchRootViewController(home, animated: true, completion: nil)
}
However it doesn't clean the memory.
Here a screen of memory allocation :
So my question is : is it possible to "kill" some view controller in order to clean the memory ?
Here for example it can be obvious to "kill" registration process views when you're connected.
Thank you for answers.
Related
I'm using mapbox and firebase.
I have a delegate function that updates the user's coordinates(inside of the firebase database) when the user's location changes.
To the best of my knowledge, it functions as it should when signed into the app. The mapviews delegate is the view controller (self.mapView.delegate = self)
func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool){
let latitude = mapView.userLocation?.coordinate.latitude
let longitude = mapView.userLocation?.coordinate.longitude
Database.database().reference().child(pathToCoord).updateChildValues(["latitude":latitude, "longitude":longitude], withCompletionBlock: { (err, ref) in
if err != nil { print(err!); return }
}
When I sign out of the app, I would like to stop updating the user location.
Ideally I would just like the View Controller with the map to go away completely and for everything on it to stop running.
I've written this sign out function that try several different methods of making sure that the location is no longer updated.
func signOut(){
for id in Auth.auth().currentUser?.providerData{
if id.providerID == "facebook.com"{
FBSDKLoginManager().logOut()
}
}
do {
try Auth.auth().signOut()
}catch let logoutError {
print(logoutError)
}
self.mapView.delegate = nil
if let vc = self.storyboard!.instantiateViewController(withIdentifier: "SignInViewController") as? SignInViewController{
UIApplication.shared.keyWindow?.rootViewController = vc
self.dismiss(animated: true, completion: nil)
}
}
Sometimes when I'm logged out though, I continuously get the error below in my console. The most logical solution I can think of for why this is happening is that the View Controller is still running. I don't know how to make it stop.
[Firebase/Database] updateChildValues: at `path/to/coord` failed: permission_denied
Error Domain=com.firebase Code=1 "Permission denied" UserInfo={NSLocalizedDescription=Permission denied}
Edit
So it looks like the problem was probably that I had this in my SignInViewController
if let uid = Auth.auth().currentUser?.uid{
if let vc = self.storyboard!.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController{
vc.uid = uid
UIApplication.shared.keyWindow?.rootViewController = vc
}
}
And then the delegate would run once for each uid, as if two of the view controllers were running at the same time. When I signed out, I'm guessing the other one didn't sign out and kept running for the other user id.
This is off topic to my original question but I'd like to know what the proper way to check if a user is already signed in, and then sign them in is. Because clearly my method didn't work.
Why don't you try dismiss with Completion Handler block like below.
self.dismiss(animated: true, completion: {
if let vc = self.storyboard!.instantiateViewController(withIdentifier: "SignInViewController") as? SignInViewController{
UIApplication.shared.keyWindow?.rootViewController = vc
}
})
So, I do a segue on fresh install of my app that shows a dialog that asks user to setup some variables that the app needs. Then, for admin users, I have an option to clear the app cache then restart the app so the user can redo the setup if they wanted to.
I restart my app using the code below.
func restartApplication () {
let viewController = SomeViewController()
let navCtrl = UINavigationController(rootViewController: viewController)
guard
let window = UIApplication.shared.keyWindow,
let rootViewController = window.rootViewController
else {
return
}
navCtrl.view.frame = rootViewController.view.frame
navCtrl.view.layoutIfNeeded()
UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: {
window.rootViewController = navCtrl
})
}
It works. But when the app reaches viewDidAppear() and needs to show my dialog, the app crashes saying
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'Receiver (<AppName.SomeViewController: 0x150dda00>) has no segue with identifier 'showSetup''
I think I messed up something for the restart, or I still need to do something for it to work but I don't know what is it.
I'm currently converting my android app to iOS so I want to do everything that I could convert.
This is how I do the segue.
DispatchQueue.main.async {
self.performSegue(withIdentifier: "showSetup", sender: self)
}
Since your "relaunched" view controller doesn't come from the storyboard, any references to segues (which relies on the storyboard) will fail.
You need something like:
func restartApplication () {
let navCtrl = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("NavController")
guard
let window = UIApplication.shared.keyWindow,
let rootViewController = window.rootViewController
else {
return
}
navCtrl.view.frame = rootViewController.view.frame
navCtrl.view.layoutIfNeeded()
UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: {
window.rootViewController = navCtrl
})
}
I've spent 24 hours trying to find a solution for this issue. When a user hits register on my app they will have to answer a series of survey questions (which I created using an ORKorderedtask (research kit)). Once the survey is completed I'd like the home page to be presented, however when I test the app and finish the survey it goes straight back to the register page. here's my code:
1.presenting the ordered task view controller;
let registrationTaskViewController = ORKTaskViewController(task: registrationSurvey, taskRun: nil)
registrationTaskViewController.delegate = self
self.present(registrationTaskViewController, animated: true, completion: nil)
2. Dismissing the task view controller(this doesn't work);
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
self.dismiss(animated: false) {
let home = homePageViewController()
self.present(home, animated: true, completion: nil)
}
thanks in advance.
Without knowing all the ViewControllers in your stack, I'd suggest not dismissing your register page view controller. Instead present your HomePageViewController on top of your Register screen. Just change your delegate method to this:
func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
let home = homePageViewController()
self.present(home, animated: true, completion: nil)
}
Or you could even present your HomePageViewController in the completion block after you prsent your ORKTaskViewController. The benefit of this approach would be that when the user dismisses the survey, they will see the HomePageViewController immediately:
let registrationTaskViewController = ORKTaskViewController(task: registrationSurvey, taskRun: nil)
registrationTaskViewController.delegate = self
self.present(registrationTaskViewController, animated: true, completion: {
let home = homePageViewController()
self.present(home, animated: true, completion: nil)
})
A couple more points:
• Classes should begin with capital letters (i.e. HomePageViewController). It's a convention that every single experienced developer uses, and Apple even recommends.
• Ultimately, I would suggest using a Navigation Controller to handle these transitions. With Nav Controllers you can achieve a much better 'flow' using push segues. It just feels better.
I send an async request and I present a view controller when I receive success. It works.
But I'm getting an issue when my app is in background when I receive success and that I pass it in foreground. The view controller is not always displayed.
I think it's about the main thread but I'm not sure.
How can I fix it ?
EDIT:
Here is the function that I call after the success:
func showPopup(on viewController: UIViewController) {
let viewControllerToPresent = MyPopupViewController(nibName: "Popup", bundle: nil)
let popup = PopupDialog(viewController: viewControllerToPresent, buttonAlignment: .horizontal, transitionStyle: .zoomIn, gestureDismissal: false)
let button = DefaultButton(title: "Ok") {
popup.dismiss(animated: true, completion: nil)
}
popup.addButtons([button])
viewController.present(popup, animated: true, completion: nil)
}
When your application is in the background, i.e. suspended, Apple doesn't allow you to make any changes substantive changes to the user interface. In this case your best approach is probably to save that you want to do something on return and check in your App Delegates applicationDidBecomeActive method.
If you call UI functions from the background the result is unpredictable. Just explicitly present on the main thread. Here is a simplified example:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.doBackgroundStuff()
}
func getThread() -> String {
return Thread.isMainThread ? "UI" : "BG"
}
func doBackgroundStuff() {
// force to background
DispatchQueue.global().async {
print("doBackgroundStuff: this is the \(self.getThread()) thread")
self.showPopup(on: self)
}
}
func showPopup(on viewController: UIViewController) {
print("showPopup OUTER: this is the \(self.getThread()) thread")
// force to foreground
DispatchQueue.main.sync {
print("showPopup INNER: this is the \(self.getThread()) thread")
let popup = PresentingViewController(nibName: "PresentingViewController", bundle: nil)
viewController.present(popup, animated: true, completion: nil)
}
}
}
The UI is shown and the output on the console is:
doBackgroundStuff: this is the BG thread
showPopup OUTER: this is the BG thread
showPopup INNER: this is the UI thread
I've tried a whole bunch of ways to get Game Center working in my SpriteKit game. Unfortunately the way I've done it in the past using ObjC and ViewControllers don't work because I'm using SKScene/ a GameScene.
This is the swift version of the code (I think):
// MARK: Game Center Integration
//login and make available
func authenticateLocalPlayer(){
let localPlayer = GKLocalPlayer()
print(localPlayer)
localPlayer.authenticateHandler = {(viewController, error) -> Void in
if ((viewController) != nil) {
self.presentViewController(viewController!, animated: true, completion: nil)
}else{
print((GKLocalPlayer.localPlayer().authenticated))
}
}
}
//submit a score to leaderboard
func reportScoreToLeaderboard(thisScore:Int){
if GKLocalPlayer.localPlayer().authenticated {
let scoreReporter = GKScore(leaderboardIdentifier: "LeaderboardID")
scoreReporter.value = Int64(thisScore)
let scoreArray: [GKScore] = [scoreReporter]
GKScore.reportScores(scoreArray, withCompletionHandler: { (error: NSError?) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
print("Score submitted")
}
})
}
}
//show leaderboard (call from button or touch)
func showLeaderboard() {
let vc = self.view?.window?.rootViewController
let gc = GKGameCenterViewController()
gc.gameCenterDelegate = self
vc?.presentViewController(gc, animated: true, completion: nil)
}
//hides view when finished
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController){
gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}
Unfortunetely, no matter what I try, I either get this error:
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior
... or it just crashes.
I've read that NSNotifications can be used? But how?
I'm guessing the best way is to set it all up in the GameViewController.swift and use NSNotifications to communicate with the RootViewController from the GameScene? I can't seem to find a tutorial or example though.
Use delegation when a view controller needs to change views, do not have the view itself change views, this could cause the view trying to deallocating while the new view is being presented, thus the error you are getting