How to keep a UIView always showing over multiple views - ios

I wish to create and present a view over each view controller in my app allowing users to interact with it or the view controllers beneath it. I want the view to always appear on top of whatever view I may present or segue to.
I have a custom UIView that appears when users tap a table view cell. Within the didSelectRowAt tableView function I have tried:
(UIApplication.shared.delegate as! AppDelegate).window?.addSubview(self.subView!)
and
self.view.addSubview(self.subView!)
Both function similarly with the view appearing over the current View controller and allowing users to interact with the table still. But when I present a new ViewController the subView disappears as it has not been added to the new View.

Subclass UIWindow, and override addSubview() to make sure your overlay view is always on top:
weak var overlay: MyOverlayView!
override func addSubview(_ view: UIView) {
if let overlay = view as? MyOverlayView {
self.overlay = overlay
super.addSubview(overlay)
} else {
self.insertSubview(view, belowSubview: overlay)
}
}
You must make sure the app delegate uses your custom class as the app's main window, and not the superclass UIWindow. For this, remove the "Main storyboard file base name" entry from your Info.plist and instead instantiate the main window manually in your AppDelegate:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: MyCustomWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
self.window = MyCustomWindow(frame: UIScreen.main.bounds)
// 1. Add your overlay view to the window
// (...)
// 2. Load your initial view controller from storyboard, or
// instantiate it programmatically
// (...)
window?.makeKeyAndVisible()
return true
}
Note: I haven't tested this code. Let me know in the comments if there's a problem.

Related

iOS - How can I keep a view at front when a viewcontroller is presented modally?

I have a navigation ViewController and I want to add a custom view named TestView on all ViewControllers when the user turns off the internet. I'm adding the custom view to the keyWindow when the internet connectivity status changes. The view is shown and stays there when I push the ViewController but when I present a ViewController modally, the modal ViewController overlays the view.
I want the view to stay at the front even when the VC is presented modally and I want to be able to add some code in my AppDelegate file for all modal ViewControllers or something like that. How can I do this in swift 5? I was thinking if there were a function that's fired every time a modal VC is presented, I could add my code there. Here's how my AppDelegate currently looks like:
class AppDelegate: UIResponder, UIApplicationDelegate, ReachabilityObserverDelegate {
var reachability: Reachability?
var window: UIWindow?
var internetView: TestView?
//This is called when the internet connects or disconnects
func reachabilityChanged(_ isReachable: Bool) {
window = getKEYWindow()
if !isReachable {
window?.addSubview(internetView!)
}
else {
internetView?.removeFromSuperview()
}
}
func getKEYWindow() -> UIWindow? {
return UIApplication.shared.connectedScenes.filter({$0.activationState == .foregroundActive}).map({$0 as? UIWindowScene}).compactMap({$0}).first?.windows.filter({$0.isKeyWindow}).first
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
try? addReachabilityObserver()
internetView = TestView(frame: CGRect(x:0, y: 50, width: UIScreen.main.bounds.size.width, height: 14))
return true
}
}
Main ViewController:
Internet Turned Off:
Push ViewController:
Present ViewController Modally:
Update: I found another way of keeping my view at front. I added the following line internetView.layer.zPosition = .greatestFiniteMagnitude in didFinishLaunchingWithOptions and it worked like a charm. Thanks for help everyone.
I could suggest one way of doing it:
To open a new UIWindow of higher level and draw it on top of your entire application.
I have no idea if this is the best way of doing it, however I just play tested it with the following code on an empty app - and it seems to work - and even can be transparent.
_myWindow = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
[_myWindow makeKeyAndVisible];
UIViewController* dummyVC = [[UIViewController alloc] init];
dummyVC.view = [[UIView alloc]initWithFrame:_myWindow.bounds];
[dummyVC.view setOpaque:NO];
[dummyVC.view setAlpha:0.1f];
[_myWindow setRootViewController:dummyVC];
_myWindow.windowLevel = UIWindowLevelAlert + 1;
[_myWindow.rootViewController.view.layer setBackgroundColor:[UIColor greenColor].CGColor];

Creating a visual storyboard based upon app delegate using custom class for Navigation Controller

Edit: How to I add a login View Controller in this repository:
https://github.com/gazolla/MapTable-Swift
I want to take what is in AppDelagate and create it visually with storyboard so that it
1) Isn't called immediately when I compile the code and
2) So I can make everything else with the storyboard going forward.
I added the new Storyboard and created a UIViewController with an embedded NavigationController but I can't create the custom of MyCustomClassViewController.
Here is the code AppDelagate is using below. How do I mimic this using storyboard?
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
// Override point for customization after application launch.
let v = Venue(aIdent:1, aName: "", aAddress: "", aCity: "", aCategoryName: "", aLat: "", aLng: "")
var venuesArr : Array<Venue> = []
venuesArr.append(v)
let vtv:TableMapViewController = TableMapViewController(frame: self.window?.frame as CGRect!)
vtv.setVenueCollection(venuesArr)
let nav:UINavigationController = UINavigationController(rootViewController: vtv)
self.window!.rootViewController = nav
self.window!.backgroundColor = UIColor.whiteColor()
self.window!.makeKeyAndVisible()
return true
}
If you are using storyboard and if your navigation controller is the initial point of application, you can get it directly as below.
let navigation = self.window!.rootViewController as! UINavigationController
If you want to set explicitly, you can do as below.
let nav = runTestStoryBoard.instantiateViewControllerWithIdentifier("UINavigationController") as! UINavigationController// Give navigation controller identifier as UINavigationController in storyboard.
let abc = runTestStoryBoard.instantiateViewControllerWithIdentifier("MyCustomViewController") as! MyCustomViewController// Give this view controller identifier as MyCustomViewController in storyboard.
If you want to have abc as root view controller you can do as below.
nav.viewControllers = [abc
Here is a solution to put view controller(your login view controller)
Create a view controller - LogInViewController.
Set this one as window's root view controller in appdelegate didFInishLoading method.
In LogInViewController after log in success call a method changeRootViewController(). This method will be in appdelegate class.
In AppDelegate class create the above method func changeRootViewController() where you will change the root view controller of window from log in view controller to navigation controller. Now your app will smooth flow acress scrrens.

Navigation Bar gone

How can I present a view controller from my AppDelegate and have a Navigation bar added to that view with a back button to the previous view? I need to do this programmatically from my AppDelegate. Currently I can push a controller from there, but it doesn't act like a segue. It doesn't add a nav bar with a back button. Now I know I should be able to add one myself, but when I do it gets hidden. Currently I'm using pushViewController(), but I imagine that's not the best way to do it.
I had something that I think is similar, if not the same:
HIGH LEVEL VIEW
The general composition of my App (thus far, and specific to the issue at hand - note: details about classes provided for context, not required for resolution) is as follows:
UIViewController (ViewController.swift) embedded in a UINavigationController
Buttons on UIViewController segue to a view with a custom class:
ExistingLocationViewController - subclass of:
UITableViewController
One of the buttons (Add New Location) in the UINavigationController's Toolbar segues to view with another custom class:
NewLocationViewController - subclass of:
UIViewController
CLLocationManagerDelegate
UITextFieldDelegate
There are a number of other items here, but I believe the above is sufficient as the foundation for the issue at hand
RESOLUTION
In order to preserve the navigation-bar (and tool-bar) going both forward and back - I have the following code in my custom classes (note: the following is Swift-3 code, you may have to adjust for Swift-2):
override func viewDidLoad() {
super.viewDidLoad()
//...
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) // #=# not sure if this is needed
navigationController?.isNavigationBarHidden = false
navigationController?.isToolbarHidden = false
}
You could actually omit the last two lines in viewWillDisappear, or perhaps even omit the entire override function
The net result (for me) was as depicted below:
Hope that helps.
If you want add a NavigationController in appDelegate you can do it like this,in this way,your viewcontroller is load from storyboard
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
let vc = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle()).instantiateViewControllerWithIdentifier("vc") as! ViewController
let nav = UINavigationController(rootViewController: vc)
self.window?.rootViewController = nav
self.window?.backgroundColor = UIColor.whiteColor()
self.window?.makeKeyAndVisible()
return true
}

Parse Facebook User logged in, perform segue

I am trying to skip over my login view once a user is logged in. How can I check to see if a user is logged in through Facebook while the app is starting?
I currently have the following code in a LoginViewController:
override func viewWillAppear(animated: Bool) {
var loggedIn = PFFacebookUtils.session().isOpen;
if (loggedIn) {
performSegueWithIdentifier("skipLogin", sender: self)
}
}
This does not move to my next view even after the user has clicked the "Log in with Facebook" button.
I get the following error:
Warning: Attempt to persent <_Project.HomeViewController: 0x7fa331d3af00> on
<_Project.LoginViewController: 0x7fa331f08950> whose view is not in the window hierarchy!
As discussed in chat, you have basically two options here:
Let the user "see" the animation from the login view controller to the second one. In that case you should do the push in viewDidAppear instead of viewWillAppear (where the view is not fully prepared, as the runtime warning clearly states).
If you prefer showing the final view controller immediately, without any animation, then it's better to put that logic inside your app delegate, and choose which initial view controller should be loaded from here. In that case, you're not actually performing any segue, you're just assigning one or another view controller to the main window (or your navigation controller).
Parse has the "AnyWall" sample app that implements the second logic. See here for more details: https://parse.com/tutorials/anywall#2-user-management. In particular, the chapter 2.4 is of special interest, as it explains how you can keep a user logged-in.
Simply put, here's how they did it (I've adapted their Objective-C code to Swift):
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
...
navigationController = UINavigationController()
...
// If we have a cached user, we'll get it back here
if PFFacebookUtils.session().isOpen {
// A user was cached, so skip straight to the main view
presentWallViewController(animated: false)
} else {
// No cached user, go to the welcome screen and
// have them log in or create an account.
presentLoginViewController(animated: true)
}
...
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window.rootViewController = navigationController
window.makeKeyAndVisible()
return true
}
In each of the two methods present...ViewController, they use the following skeleton:
func presentxxxViewController(#animated: Bool) {
NSLog("Presenting xxx view controller")
// Go to the welcome screen and have them log in or create an account.
let storyboard = UIStoryboard(name: "Main", bundle: nil) // Here you need to replace "Main" by the name of your storyboard as defined in interface designer
let viewController = storyboard.instantiateViewControllerWithIdentifier("xxx") as xxxViewController // Same here, replace "xxx" by the exact name of the view controller as defined in interface designer
//viewController.delegate = self
navigationController?.setViewControllers([viewController], animated: animated)
}
The navigationController and window vars should be defined like this in AppDelegate:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var navigationController: UINavigationController?
...
}
If your app also uses a navigation controller as its root view controller, you can probably use the same code.

IOS: Programmatic rootViewController renders the view differently to Storyboard 'isInitialView'

With the aim to implement a splash screen that only shows once i've modified didFinishLaunchingWithOptions in order to dynamically select the appropriate view controller. The logic seems to work fine, and the view I intended to load is the one launched
However, the UI seems to be missing elements that would otherwise be displayed should I have not altered the didFinishLaunchingWithOptions function.
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool
{
window = UIWindow(frame: UIScreen.mainScreen().bounds)
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
var entryViewController: UIViewController?
if NSUserDefaults.standardUserDefaults().boolForKey("hasSeenWelcomeScreen") == true
{
entryViewController = storyBoard.instantiateViewControllerWithIdentifier("NavigationController") as? UIViewController
}
else
{
entryViewController = storyBoard.instantiateViewControllerWithIdentifier("WelcomeViewController") as? UIViewController
NSUserDefaults.standardUserDefaults().setValue(true, forKey: "hasSeenWelcomeScreen")
NSUserDefaults.standardUserDefaults().synchronize()
}
self.window?.rootViewController = entryViewController
self.window?.makeKeyAndVisible()
return true
}
My WelcomeViewController is a simple view with 1 label, 1 button and a movie which plays in the background (resembling Spotify/Vine's welcome screen). Debugging the code I can see the initialization methods do get executed, but is just the frame that does not seem to be displayed when I dynamically override the initial view
import UIKit
import MediaPlayer
import QuartzCore
class WelcomeViewController: UIViewController {
var moviePlayerController: MPMoviePlayerController = MPMoviePlayerController()
#IBOutlet weak var loginButton: UIButton!
#IBOutlet weak var appNameLabel: UILabel!
override func viewDidLoad()
{
super.viewDidLoad()
buildMoviePreview()
buildButtonDesign()
}
override func viewWillAppear(animated: Bool)
{
self.view.addSubview(self.moviePlayerController.view)
self.view.addSubview(self.loginButton)
self.view.addSubview(self.appNameLabel)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func prefersStatusBarHidden() -> Bool {
return true
}
private func buildButtonDesign()
{
loginButton.layer.borderColor = UIColor.whiteColor().CGColor
loginButton.layer.borderWidth = 2.0
loginButton.layer.cornerRadius = 7.0
}
private func buildMoviePreview()
{
let filePath = NSBundle.mainBundle().pathForResource("intro", ofType: "mov")
self.moviePlayerController.contentURL = NSURL.fileURLWithPath(filePath)
self.moviePlayerController.movieSourceType = .File
self.moviePlayerController.repeatMode = .One
self.moviePlayerController.view.frame = self.view.bounds
self.moviePlayerController.scalingMode = .AspectFill
self.moviePlayerController.controlStyle = .None
self.moviePlayerController.allowsAirPlay = false
self.moviePlayerController.shouldAutoplay = true
self.moviePlayerController.play()
}
}
For completeness, these are the discrepancies in the layout when using the XCode UI debugger. Please note that they differ even though they implement the same viewController. The only difference is that one has been programmatically set as the initial view, while the other has been set as the initial view through storyboard.
Screenshots of the rendering issue side-by-side
Your approach is... unusual. A Storyboard has a root view controller for a reason and typically at startup you would just let the application handle loading the storyboard and installing that root view controller as the window's main view controller. (The Storyboard loaded is specified in the application target's general settings as the "Main Interface")
In this case, what I would recommend is making your root view controller the "Normal" view of the application... the one you want users to see when they launch the app on a day-to-day basis.
Define your "on first launch" view controller as a separate view controller in the storyboard and add a modal segue from the root view controller to the on first launch view controller.
Then in your applicationDidFinishLaunching, if the user has never seen the first launch controller... simply ask the Storyboard to take that segue. If the user has already seen the first launch presentation that segue will be skipped.
Another issue I see with your code is in your viewWillAppear method. You should not have to add your views as subviews in viewWillAppear... those subviews should already have been set up at the time the view was loaded from the nib file.
The one exception is the view of your movie player, but your movie player is owned by a separate view controller. That separate view controller is detached from the view controller hierarchy and does not have it's own view controller methods called at the right times. (so it never receives calls like "viewWillAppear" that might tell it to get it's movie ready to play).
What you probably want to do is implement "awakeFromNib" and make sure that the movie player's view controller is a sub-controller of this view controller. (so in awakeFromNib for the WelcomeViewController use addChildViewController to make sure the movie controller is in the hierarchy).
It will be better to user 2 storyboards :
one with your welcome screen
the other one with the rest of your app
The application launching wil look like this :
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
var storyBoard : UIStoryboard!
if NSUserDefaults.standardUserDefaults().boolForKey("hasSeenWelcomeScreen") == true {
changeStoryBoard("Main")
}
else {
changeStoryBoard("Welcome")
}
return true
}
func changeStoryBoard(name :String) {
var storyBoard = UIStoryboard(name:name, bundle: nil)
var viewController: AnyObject! = storyBoard.instantiateInitialViewController() ;
self.window!.rootViewController = viewController as UIViewController
}

Resources