Swift return to preview UINavigationController - ios

I have VC like a "Something went wrong". This VC i created like a separately VC(without storyboard) and i want to show it where i want. But in the "Something went wrong" View Controller i have a button "refresh". When a user click to this button he must to go back.
When i have some problem with parsing Json or something like this, i call Something went wrong" View Controller like this:
let navController = UINavigationController()
navController.pushViewController(SomethingWentWorngVC(nibName: "SomethingWentWorngView", bundle: nil), animated: false)
window?.rootViewController = navController
window?.makeKeyAndVisible()
also i have extension for getting window
extension UIViewController {
var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
var sceneDelegate: SceneDelegate? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate else { return nil }
return delegate
}
}
extension UIViewController {
var window: UIWindow? {
if #available(iOS 13, *) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate, let window = delegate.window else { return nil }
return window
}
guard let delegate = UIApplication.shared.delegate as? AppDelegate, let window = delegate.window else { return nil }
return window
}
}
in the SomethingWentWorngVC i have button for go to back
#IBAction func refreshAction(_ sender: Any) {
self.navigationController?.popToRootViewController(animated: false)
}
but it doesnt work

in this alternate way you can use this , initially u need to create the common code in appdelegate using tag, then you need to do the addsubview to window for example,
for show
func showWentWrongScreen(){
let getVC = SomethingWentWorngVC
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
}
for remove
func removeWentWrongScreen(){
if let getWindow = self.window, let getWentWrongView = getWindow.viewWithTag(501){
getWentWrongView.removeFromSuperview()
}
}
now you can use where u need

Related

Test different behaviours in AppDelegate for each or separated unit and integration tests

I want to test my application's behavior that decided on app launch. For example: In a tab bar controller, how many and which tabs will be created is been decided on app launch where the root window has been created so I want to test these behaviors for each test case.
This new feature is set via A/B service and the value retrieved only during app launching. Based on that value, the tab bar's view controllers are set.
For example:
var viewControllers: [UIViewController] = [ tabOne, tabTwo]
if Config.isNewFeatureEnabled {
viewControllers.append(self._menuCoordinator.rootViewController)
} else {
viewControllers.append(self._anotherTabBarController)
viewControllers.append(self._anotherCoordinator.rootViewController)
viewControllers.append(self._someOtherCoordinator.rootViewController)
}
_tabBarController.viewControllers = viewControllers
Let me put in code, in order to make tests easy I created a protocol (not necessarily but better approach for injection)
protocol FeatureFlag {
var isNewFeatureEnabled: Bool { get set }
}
// Implementation
class FeatureFlagService: FeatureFlag {
var isNewFeatureEnabled = false
// Bunch of other feature flags
}
In my test cases I want to switch the config with out effecting other side of the app. Something like this:
class NewFeatureVisibilityTests: XCTestCase {
func test_TabBar_has_threeTabs_when_NewFeature_isEnabled() {
// Looking for a way to inject the config
let tabBar = getKeyWindow()?.rootViewController as? UITabBarController
guard let tabBar = appDel.currentWindow?.rootViewController as? UITabBarController else {
return XCTFail("Expected root view controller to be a tab bar controller")
}
XCTAssertEqual(tabBar.viewControllers?.count, 3)
}
func test_TabBar_has_fiveTabs_when_NewFeature_isDisabled() {
// Looking for a way to inject the config
let tabBar = getKeyWindow()?.rootViewController as? UITabBarController
guard let tabBar = appDel.currentWindow?.rootViewController as? UITabBarController else {
return XCTFail("Expected root view controller to be a tab bar controller")
}
XCTAssertEqual(tabBar.viewControllers?.count, 5)
}
}
What I want is set application's behaviour through injection (a config etc) for each test case.
One test the feature will be enabled, other test will assert the feature disabled state.
Create a config property in AppDelegate using existing type of FeatureFlag along with a default value on override init.
extension UIApplication {
var currentWindow: UIWindow {
return (connectedScenes
.filter({$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
.first?.windows
.filter({$0.isKeyWindow}).first!)!
}
}
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let config: FeatureFlag!
override init() {
config = FeatureFlagService()
}
init(config: FeatureFlag!) {
self.config = config
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Create a tabBar with 3 tabs
let tabBarController = UITabBarController()
let firstViewController = UIViewController()
let secondViewController = UIViewController()
let thirdViewController = UIViewController()
let fourthViewController = UIViewController()
let fifthViewController = UIViewController()
firstViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 0)
secondViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 1)
thirdViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .more, tag: 2)
fourthViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 3)
fifthViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .contacts, tag: 4)
var viewControllers = [firstViewController, secondViewController]
if config.isNewFeatureEnabled {
viewControllers.append(thirdViewController)
} else {
viewControllers.append(fourthViewController)
viewControllers.append(fifthViewController)
}
tabBarController.viewControllers = viewControllers
// Create a window and set the root view controller
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = tabBarController
window.makeKeyAndVisible()
self.window = window
return true
}
}
And in tests, I set my config, create an instance of AppDelegate, inject the config, and launching the application through appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil) function of AppDelegate.
let appDelegate = AppDelegate(config: config)
// This is the key function
_ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
Tests:
import XCTest
#testable import ExampleApp
final class NewFeatureVisibilityTests: XCTestCase {
func test_app_can_start_with_isNewFeatureEnabled(){
let config = FeatureFlagService()
config.isNewFeatureEnabled = true
let appDelegate = AppDelegate(config: config)
// This is the key function
_ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
guard let rootVC = UIApplication.shared.currentWindow.rootViewController as? UITabBarController else {
return XCTFail("RootViewController is nil")
}
XCTAssertEqual(rootVC.viewControllers?.count, 3)
}
func test_app_can_start_with_isNewFeatureDisabled(){
let config = FeatureFlagService()
config.isNewFeatureEnabled = false
let appDelegate = AppDelegate(config: config)
// This is the key function
_ = appDelegate.application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
guard let rootVC = UIApplication.shared.currentWindow.rootViewController as? UITabBarController else {
return XCTFail("RootViewController is nil")
}
XCTAssertEqual(rootVC.viewControllers?.count, 4)
}
}

Swift. After addSubview click listeners dont work

I have a testVC. TestVC hasnt storyboard, this viewController has XIB file. I show this VC when i have no internet. And logic for a show this VC like this:
let getVC = NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil)
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
also i have extension for UIViewController
extension UIViewController {
var appDelegate: AppDelegate {
return UIApplication.shared.delegate as! AppDelegate
}
var sceneDelegate: SceneDelegate? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate else { return nil }
return delegate
}
}
extension UIViewController {
var window: UIWindow? {
if #available(iOS 13, *) {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let delegate = windowScene.delegate as? SceneDelegate, let window = delegate.window else { return nil }
return window
}
guard let delegate = UIApplication.shared.delegate as? AppDelegate, let window = delegate.window else { return nil }
return window
}
}
all clicks in the TestVC works if i show this View Controller like this:
navController.pushViewController(NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil), animated: true)
but it doesn't suit me. I need to show NoInternetConnectionVC like i described above, that is, like this. When i show NoInternetConnectionVC like below all my listeners stop to work
let getVC = NoInternetConnectionVC(nibName: "NoInternetConnectionView", bundle: nil)
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
I tried to add line isUserInteractionEnabled to my code, like this
if let getWindow = self.window {
getVC.view.tag = 501
getVC.view.isUserInteractionEnabled = true //added line
getVC.view.frame = getWindow.bounds
getWindow.addSubview(getVC.view)
}
but it doesnt work
You have just added it as a subview. After adding subview move your controller to parent.
if let root = UIApplication.shared.windows.first?.rootViewController {
root.addChild(getVC)
getVC.didMove(toParent: root)
}

What is the best way to present lock screen in iOS?

I wondering the best way to present lock screen in iOS(swift).
ex) If the user presses the home button or receives a call, I want to display the lock screen when user re-enter the app.
So, I tried this way.
func applicationWillResignActive(_ application: UIApplication) {
guard let passcodeManageView = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "passcodeManageView") as? PasscodeManageViewController else { return }
if let window = self.window, let rootViewController = window.rootViewController {
var currentController = rootViewController
while let presentController = currentController.presentedViewController {
currentController = presentController
}
currentController.present(passcodeManageView, animated: true, completion: nil)
}
}
Actually it works pretty well.
However, if the alert window is displayed, it does not work normally.
How can I fixed it? (Sorry for my eng.)
Alert views are always an issue in these cases. A quick solution might be to check if alert view is presented and dismiss it. I played with the following:
func showOverlayController(currentController: UIViewController) {
currentController.present(OverlayViewController(), animated: true, completion: nil)
}
if let window = UIApplication.shared.keyWindow, let rootViewController = window.rootViewController {
var currentController = rootViewController
while let presentController = currentController.presentedViewController {
guard presentController as? UIAlertController == nil else {
presentController.dismiss(animated: false) {
showOverlayController(currentController: currentController)
}
return
}
currentController = presentController
}
showOverlayController(currentController: currentController)
}
Putting aside animations and all this still seems very bad because I suspect if if a view controller inside navigation controller or tab bar controller (or any other type of content view controller) would present an alert view this issue would again show itself. You could use the same logic of finding a top controller to always present alert view on top controller to overcome this.
So I moved to another way which is I would rather change the root view controller instead of presenting an overlay. So I tried the following:
static var currentOverlay: (overlay: OverlayViewController, stashedController: UIViewController)?
static func showOverlay() {
guard currentOverlay == nil else { return }
guard let currentController = UIApplication.shared.keyWindow?.rootViewController else { return }
let overlay: (overlay: OverlayViewController, stashedController: UIViewController) = (overlay: OverlayViewController(), stashedController: currentController)
self.currentOverlay = overlay
UIApplication.shared.keyWindow?.rootViewController = overlay.overlay
}
static func hideOverlay() {
guard let currentOverlay = currentOverlay else { return }
self.currentOverlay = nil
UIApplication.shared.keyWindow?.rootViewController = currentOverlay.stashedController
}
It works great... Until alert view is shown again. So after a bit of an inspection I found out that in case of alert views your application has multiple windows. It makes sense an alert would create a new window over the current one but I am unsure how did anyone think it would be intuitive or that it would in any possible way make sense that you are presenting alert view. I would then expect something like UIApplication.shared.showAlert(alert) but let's put this stupidity aside.
The only real solution I see here is to add a new window for your dialog. To do that you could look around the web. What seems to work for me is the following:
static var currentOverlayWindow: (overlay: OverlayViewController, window: UIWindow, previousWindow: UIWindow)?
static func showOverlay() {
guard currentOverlay == nil else { return }
guard let currentWindow = UIApplication.shared.keyWindow else { return }
let overlay: (overlay: OverlayViewController, window: UIWindow, previousWindow: UIWindow) = (overlay: OverlayViewController(), window: UIWindow(frame: currentWindow.bounds), previousWindow: currentWindow)
self.currentOverlayWindow = overlay
overlay.window.backgroundColor = UIColor.black
overlay.window.rootViewController = overlay.overlay
overlay.window.windowLevel = UIWindowLevelAlert + 1
overlay.window.makeKeyAndVisible()
}
static func hideOverlay() {
guard let currentOverlayWindow = currentOverlayWindow else { return }
self.currentOverlay = nil
currentOverlayWindow.window.isHidden = true
currentOverlayWindow.previousWindow.makeKeyAndVisible()
}
Just to fill in the gaps. What I used as an overlay view controller:
class OverlayViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 12.0, y: 100.0, width: 150.0, height: 55.0))
button.setTitle("Dismiss", for: .normal)
view.addSubview(button)
button.addTarget(self, action: #selector(onButton), for: .touchUpInside)
}
#objc private func onButton() {
AppDelegate.hideOverlay()
// self.dismiss(animated: true, completion: nil)
}
}

Back button disappears after creating AppDelegate in Swift 3.0

I created the structure as below in xcode 8 swift 3.0.
Before I add AppDelegate code. Back button still appear fine on Apply, Apply Behalf and Profile controller.
I use segue to open page.
But after I add AppDelegate code into Homepage and Login controllers , back button disappears on Apply, Apply behalf and profile controller page.
Can someone help or explain why is this happening ? How to enable back the back button on apply, apply behalf and profile page ?
Thanks.
Home.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet var staffNumberLbl: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
staffNumberLbl.text = UserDefaults.standard.object(forKey: "login") as? String
}
override func viewDidAppear(_ animated: Bool) {
let isUserLoggedIn = UserDefaults.standard.bool(forKey: "loggedIn")
if(!isUserLoggedIn){
self.performSegue(withIdentifier: "loginview", sender: self)
}
}
#IBAction func logoutData(_ sender: Any) {
UserDefaults.standard.set(false, forKey: "loggedIn")
UserDefaults.standard.synchronize();
let loginViewController = self.storyboard!.instantiateViewController(withIdentifier: "loginview") as! LoginViewController
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = loginViewController
appDelegate.window?.makeKeyAndVisible()
}
}
Login.swift
import UIKit
class LoginViewController: UIViewController {
#IBOutlet var loginlbl: UITextField!
#IBOutlet var passlbl: UITextField!
#IBOutlet var login_button: UIButton!
var login: String!
var pw: String!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func loginData(_ sender: Any) {
login = loginLbl.text
pw = passLbl.text
if(login == "" || pw == ""){
return
}
else{
let url = URL(string: "http://localhost/login.php")
let session = URLSession.shared
let request = NSMutableURLRequest(url: url! as URL)
request.httpMethod = "POST"
let LoginDataToPost = "login=\(login!)&pw=\(pw!)"
request.httpBody = LoginDataToPost.data(using: String.Encoding.utf8)
let task = session.dataTask(with: request as URLRequest, completionHandler: {
(data, response, error) in
if error != nil {
return
}
else {
do {
if let json = try JSONSerialization.jsonObject(with: data!) as? [String: String]
{
DispatchQueue.main.async
{
let message = Int(json["message"]!)
let login = json["login"]
if(message == 1) {
UserDefaults.standard.set(true, forKey: "isUserLoggedIn")
UserDefaults.standard.set(login, forKey: "login")
UserDefaults.standard.synchronize();
self.dismiss(animated: true, completion: nil)
let myViewController:ViewController = self.storyboard!.instantiateViewController(withIdentifier: "ViewController") as! ViewController
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = myViewController
appDelegate.window?.makeKeyAndVisible()
print("Value for login is : \(login!)")
return
}
else {}
}
}
else {}
}
catch let jsonParse {}
}
})
task.resume()
}
}
}
AppDelegate.swift
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let mainStoryBoard: UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let isUserLoggedIn:Bool = UserDefaults.standard.bool(forKey: "isUserLoggedIn")
if(!isUserLoggedIn) {
let loginViewController = mainStoryBoard.instantiateViewController(withIdentifier: "loginview") as! LoginViewController
window!.rootViewController = loginViewController
window!.makeKeyAndVisible()
}
else {
let homePage = mainStoryBoard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
window!.rootViewController = homePage
window!.makeKeyAndVisible()
}
return true
}
}
You are setting rootviewcontroller without embedding navigation controller to it in logoutData & loginData function.
Use this code :
let navigationController = UINavigationController.init(rootViewController: myViewController)
appDelegate.window?.rootViewController = navigationController
Use this code in AppDelegate:
if(!isUserLoggedIn) {
let loginViewController = mainStoryBoard.instantiateViewController(withIdentifier: "loginview") as! LoginViewController
let navigationController = UINavigationController.init(rootViewController: loginViewController)
appDelegate.window?.rootViewController = navigationController
window!.makeKeyAndVisible()
}
else {
let homePage = mainStoryBoard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
let navigationController = UINavigationController.init(rootViewController: homePage)
appDelegate.window?.rootViewController = navigationController
window!.makeKeyAndVisible()
}
Remove this from Home.swift,
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = loginViewController
appDelegate.window?.makeKeyAndVisible()
because its not inheriting the properties of Navigation controller
and add it in Appdelegate.swift file
For the other 3 viewcontrollers, you need to add the Navigation controller between eachSegway in order to inherit it or code the button by instantiating the viewcontrollers respectively
After successful login,try to make NavigationController as rootViewController instead of your ViewController
Your back button will start appearing.
In AppDelegate, in else block, instead of this line
let homePage = mainStoryBoard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
write this
let homePage = mainStoryBoard.instantiateViewController(withIdentifier: "NavigationController") as! UINavigationController
Inside LoginViewController, in the block if(message == 1)
replace
let myViewController:ViewController = self.storyboard!.instantiateViewController(withIdentifier: "ViewController") as! ViewController
with
let navController:UINavigationController = self.storyboard!.instantiateViewController(withIdentifier: "NavigationController") as! UINavigationController
Also set storyboard identifier for UINavigationController in storyboard to NavigationController
Depending on your configuration:
self.navigationItem.hidesBackButton = YES;
OR:
self.navigationController.navigationItem.hidesBackButton = YES;
Or, if you just want to disable the button without hiding it, you can use this.
self.navigationController.navigationItem.backBarButtonItem.enabled = NO;

Swift - Accessing AppDelegate window from viewController

I make walkthrough (onboarding flow) in my app and I'd like to have a skip button.
The button is located on viewController, so I figured out that the best way to move to another viewController would be access app delegate window.
However, it keeps getting me an error that AppDelegate.Type does not have a member called "window".
#IBAction func skipWalkthrough(sender: AnyObject) {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
AppDelegate.window!.rootViewController = RootViewController
}
Is there anything wrong with such approach?
Thanks in advance!
You have a typo it is supposed to be appDelegate not AppDelegate. So like this:
#IBAction func skipWalkthrough(sender: AnyObject) {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window!.rootViewController = RootViewController
}
Swift 3.2
#IBAction func skipWalkthrough(_ sender: AnyObject) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window!.rootViewController = controller
}
This is for with or without Storyboard and it is working for Swift 3+
let appDelegate = UIApplication.shared.delegate as? AppDelegate
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let homeController = mainStoryboard.instantiateViewController(withIdentifier: "HomeViewController") as! HomeViewController
appDelegate?.window?.rootViewController = homeController
Swift 3
This is a better way:
if let window = NSApplication.shared().windows.first {
window.acceptsMouseMovedEvents = true;
}
appDelegate.window!.rootViewController is not working in Swift 5
Here is working code
Add below extension
extension UIWindow {
static var key: UIWindow! {
if #available(iOS 13, *) {
return UIApplication.shared.windows.first { $0.isKeyWindow }
} else {
return UIApplication.shared.keyWindow
}
}
}
use
let mainSB = UIStoryboard(name: "Main", bundle: nil)
if let RootVc = mainSB.instantiateViewController(withIdentifier: "NavigationController") as? UINavigationController{
UIWindow.key.rootViewController = RootVc
}
UIWindow.key // to access only window
You can also use conditional binding to reach the window.
if let window = UIApplication.shared.windows.first {
// use window here.
}
You are using the protocol name (i.e. AppDelegate) instead of the instance:
Should be:
appDelegate.window!.rootViewController = RootViewController
You can access tab bar anywhere from the app. Use below:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
if let tabBarController = appDelegate.window!.rootViewController as? UITabBarController {
if let tabItems = tabBarController.tabBar.items {
let tabItem = tabItems[2]
tabItem.badgeValue = "5" //enter any value
}
}
This solution work for :
After Login / Register Programmatically add UITabbarController
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window!.rootViewController = tabs
appDelegate.window!.makeKeyAndVisible()

Resources