How to check if UIViewController is already being displayed? - swift

I'm working on an app that displays a today extension with some information. When I tap on the today extension, it opens the app and navigates to a subview from the root to display the information. Normally the user would then click the back arrow to go back to the main view, but there is no way to tell if this is actually done. It is possible for the user to go back to the today extension and tap again. When this is done, the subview is opened once again with new information. If this is done a bunch of times, I end up with a bunch of instances of the subview and I have to click the back button on each of them to get back to the main view.
My question: Is it possible to check if the subview is already visible? I'd like to be able to just send updated information to it, instead of having to display an entirely new view.
I am currently handling this by keeping the instance of the UIViewController at the top of my root. If it is not nil, then I just pass the information to it and redraw. If it is nil, then I call performSegue and create a new one.
I just think that there must be a better way of handling this.
Edit: Thanks to the commenter below, I came up with this code that seems to do what I need.
if let quoteView = self.navigationController?.topViewController as? ShowQuoteVC {
quoteView.updateQuoteInformation(usingQuote: QuoteService.instance.getQuote(byQuoteNumber: quote))
}
else {
performSegue(withIdentifier: "showQuote", sender: quote)
}
This is different from the suggested post where the answer is:
if (self.navigationController.topViewController == self) {
//the view is currently displayed
}
In this case, it didn't work because I when I come in to the app from the Today Extension, it goes to the root view controller. I needed to check whether a subview is being displayed, and self.navigationController.topViewcontroller == self will never work because I am not checking to see if the top view controller is the root view controller. The suggestions in this post are more applicable to what I am trying to accomplish.

u can use this extension to check for currently displayed through the UIApplication UIViewController:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
and usage example:
if let topController = UIApplication.topViewController() {
if !topController.isKind(of: MainViewController.self) { //MainViewController- the controller u wish to equal its type
// do action...
}
}

Related

How to test if the app is presenting a certain view controller?

I'm pretty new to XCode UI tests and I'm trying to run a test where I fill two text labels and then I press a button. After the button is pressed the app should make an URL call and be redirected to another view controller. I want to check if at the end of this operation the second view controller is displayed.
To test this, I have written the following test:
let app = XCUIApplication()
app.launch()
let ownerTextField = app.textFields["ownerTextField"]
ownerTextField.tap()
ownerTextField.typeText("UserA")
let repositoryTextField = app.textFields["repositoryTextField"]
repositoryTextField.tap()
repositoryTextField.typeText("AppB")
app.buttons["SearchButton"].tap()
XCTAssertTrue(app.isDisplayingResults)
Where isDisplayingResults is
extension XCUIApplication {
var isDisplayingResults: Bool {
return otherElements["resultView"].exists
}
}
I have set up the identifier of the View controller inside its swift file class:
override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "resultView"
...
Nonetheless to say, the test fails. How can I get a success?
It's so simple.
If after clicking the URL, Viewcontroller is presenting means your previous VC button doesn't exist on screen.
So Just check for previous VC button exists or not.
If app.buttons["SearchButton"].esists()
{ //write if code
} else {
// Write else code
}

How to decide what view to display based on global variable

I'm trying to implement an IOS swift app which starts off with a tab bar controller, and in one of the tab bar controllers is an item called "account".
When a user presses the account item, I want the app to decide (onclick event) whether the view that contains sign up/login is displayed or the profile view is displayed based on a global variable "loggedIn" (bool type).
(I've tried navigation controller but what I've understood from that is that it is a sequence of views which can't decide between views)
I want to know how can this be implemented, maybe some kind of "router" if you may that can switch between views...
If you didn't understand here's a picture of what I'm trying to implement
Basic Map of what I'm trying to explain
If you can suggest a more professional way of doing such design please don't hesitate to express your opinion.
I believe a good approach is updating the view controller when loggedIn status changes. If you don't already have, create a class inheriting from UITabBarController to manage your tabs. Here's the code:
class TabController: UITabBarController {}
In your storyboard, select your tab controller, go to the Identity Inspector and set TabController as the custom class. Now TabController will manage all the view controllers in your tab bar.
It's usually not a good approach to use global variables, so let's add loggedIn in the scope of TabController and listen for any of changes in it and update the corresponding view controller:
class TabController: UITabBarController {
var loggedIn = true {
didSet {
updateProfileTab()
}
}
}
Now, whenever you change loggedIn, that change will update the proper tab. Now let's write updateProfileTab():
class TabController: UITabBarController {
func updateProfileTab() {
let viewController: UIViewController
if loggedIn {
viewController = makeProfileViewController()
} else {
viewController = makeLoginViewController()
}
setViewController(viewController, at: 2)
}
func makeProfileViewController() -> ProfileViewController {
// create and return the profile view controller
}
func makeLoginViewController() -> LoginViewController {
// create and return the profile view controller
}
}
Naturally, you might want to write the body of both makeProfileViewController and makeLoginViewController methods. The last thing for TabController is to write setViewController(_:at:) method:
class TabController: UITabBarController {
...
func setViewController(_ viewController: UIViewController, at index: Int) {
let tabBarItem = viewControllers?[index].tabBarItem
viewController.tabBarItem = tabBarItem
viewControllers?[index] = viewController
}
...
}
Now, since TabController manages your tab bar, you can access it from within any of its children view controllers:
guard let tabController = tabBarController as? TabController else { return }
tabController.loggedIn = ...
Also, it's important to select the initial state. So, in the viewDidLoad from one of your tabbed view controllers, you should perform the above code. The first tab (the one that displays first) is probably the best place to do that. Hope this helps!
EDIT
To create your login and signup view controllers, the easiest way to go is by assigning ids in your storyboard. To do that, go to your storyboard, select the view controller, and in the Identity Inspector set a Storyboard ID, which you will use to instantiate the view controller:
func makeProfileViewController() -> ProfileViewController {
let controller = self.storyboard!.instantiateViewController(withIdentifier: "theStoryboardID")
return controller as! ProfileViewController
}
Note that I'm using force unwrap here (!). That's just for brevity. In a real case scenario you will want to use some if let or guard let statements to handle nil values.

Where in view lifecycle to update controller after modal UIViewController dismissed

I have a UIViewController with a UILabel that needs to display either "lbs" or "kg". My app has a settings screen (another UIViewController) that is presented modally over the first view controller and the user can select either of the two units and save their preference. If the units are changed and the modal settings screen is dismissed, I of course want the label on the first view controller to be updated with the new units value (but without refreshing the whole view). I thought I knew how to make it work, but evidently I don't.
On my modal settings screen, I have a UISegmentedControl to allow the user to select units. Anytime it's changed, this function updates userDefaults:
func saveUnitsSelection() {
if unitsControl.selectedSegmentIndex == 0 {
UserDefaultsManager.sharedInstance.preferredUnits = Units.pounds.rawValue
} else {
UserDefaultsManager.sharedInstance.preferredUnits = Units.kilograms.rawValue
}
}
Then they would likely dismiss the settings screen. So, I added this to viewDidLoad in my first view controller:
override func viewDidLoad() {
super.viewDidLoad()
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
units.text = preferredUnits
}
That didn't work, so I moved it to viewWillAppear() and that didn't work either. I did some research and some caveman debugging and found out that neither of those functions is called after the view has been loaded/presented the first time. It seems that viewWillAppear will be called a second time if I'm working within a hierarchy of UITableViewControllers managed by a UINavigationController, but isn't called when I dismiss my modal UIViewController to reveal the UIViewController underneath it.
Edit 1:
Here's the view hierarchy I'm working with:
I'm kinda stuck at this point and not sure what to try next.
Edit 2:
The user can tap a 'Done' button in the navigation bar and when they do, the dismissSettings() function dismisses the Settings view:
class SettingsViewController: UITableViewController {
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
// some other variables set here
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.topItem?.title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .Plain, target: self, action: #selector(self.dismissSettings(_:)))
if preferredUnits == Units.pounds.rawValue {
unitsControl.selectedSegmentIndex = 0
} else {
unitsControl.selectedSegmentIndex = 1
}
}
func dismissSettings(sender: AnyObject?) {
navigationController?.dismissViewControllerAnimated(true, completion: nil)
}
}
THE REAL PROBLEM
You misspelled viewWillAppear. You called it:
func viewWillAppear()
As far as Cocoa Touch is concerned, this is a random irrelevant function that hooks into nothing. You meant:
override func viewWillAppear(animated: Bool)
The full name of the first function is: "viewWillAppear"
The full name of the second function is: "viewWillAppear:animated"
Once you get used to this, the extreme method "overloading" that Cocoa Touch uses gets easier.
This is very different in other languages where you might at least get a warning.
The other lesson that everyone needs to learn when posting a question is: Include All Related Code!
Useful logging function I use instead of print or NSLog, to help find these things:
class Util {
static func log(message: String, sourceAbsolutePath: String = #file, line: Int = #line, function: String = #function, category: String = "General") {
let threadType = NSThread.currentThread().isMainThread ? "main" : "other"
let baseName = (NSURL(fileURLWithPath: sourceAbsolutePath).lastPathComponent! as NSString).stringByDeletingPathExtension ?? "UNKNOWN_FILE"
print("\(NSDate()) \(threadType) \(baseName) \(function)[\(line)]: \(message)")
}
}
[Remaining previous discussion removed as it was incorrect guesses]

Calling a function of the current UIViewController when using SWRevealViewController

I have an app written in swift that displays location information on different views. I am receiving location updates in AppDelegate (I don't want to use a singleton class since I want the location updates even when the app is in the background).
Now, I am using SWRevealViewController to implement a sidebar menu to toggle between the different views. When a new location update is received, how do I call the function of the viewController that is currently active to update the UI?
I searched a lot and all the solutions that talk about how to find the current UIViewController actually return SWRevealViewController as the current UIViewController, which doesn't help.
I gave it another shot and was able to find my viewController in my app from another view controller. However, I couldn't get my child view controllers app delegate.
Looking forward to other answers...
here's what I did:
let app = UIApplication.sharedApplication().delegate! as! AppDelegate
if let nav = app.window?.rootViewController?.childViewControllers {
nav.forEach { vc in
if let viewControllers = vc as? UINavigationController {
viewControllers.childViewControllers.forEach { vc in
if let x = vc as? ViewController {
print("Got it \(x)")
}
}
}
}
}

Warning: Attempt to present ViewController on ViewController which is already presenting ViewController

I have a view controller with a toolbar with 3 UIButtons that open a new view controller as a popover. I created the segues in Storyboard and selected "Present as Popover". The popovers work but when the user taps on another button while a popover is currently open, I get this error:
Warning: Attempt to present <Fingerpainter.OpacityViewController: 0x79095110> on <Fingerpainter.DrawingViewController: 0x7b278000> which is already presenting <Fingerpainter.BrushSizeViewController: 0x79573770>
Is there a way to like make sure all popovers are closed before opening a new one? Here's my prepareForSegue method in the main ViewController (containing the toolbar):
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let identifier = segue.identifier ?? ""
let popoverPresentationController = segue.destinationViewController.popoverPresentationController
popoverPresentationController!.delegate = self
switch identifier {
case Storyboard.BrushSizeSegueIdentifier:
if let brushSizeViewController = popoverPresentationController?.presentedViewController as? BrushSizeViewController {
// set properties in brushSizeViewController
}
case Storyboard.OpacitySegueIdentifier:
if let opacityViewController = popoverPresentationController?.presentedViewController as? OpacityViewController {
//set properties in opacityViewController
}
case Storyboard.ColorSegueIdentity:
if let colorViewController = popoverPresentationController?.presentedViewController as? ColorViewController {
//set properties in colorViewController
}
default:
break
}
}
Is there a way to like make sure all popovers are closed before opening a new one
It's the other way around. It's your job to make sure that while the popover is present, a button that summons another popover is not tappable. You can do this by disabling the button, but more commonly, in order to coordinate the disabling of the button with the presence of the popover, it's done by adjusting the popover presentation controller's passthroughViews.
Unfortunately there's a massive and long-standing bug where even setting the passthroughViews to nil doesn't prevent toolbar buttons from being tappable. The workaround is to do it with a delay. A lot of my popover code adds this sort of thing:
if let pop = popoverPresentationController {
delay(0.1) {
pop.passthroughViews = nil
}
}
(where delay is described here: https://stackoverflow.com/a/24318861/341994).