How to Remove windows from UIApplication in Swift - swift

While clicking on the button , i am moving to another view controller using the following code.
var window: UIWindow?
window = UIWindow.init(frame: UIScreen.main.bounds)
window?.autoresizesSubviews = true
window?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let trackingViewController = LoginCameraViewController.init(screen:
.main)
window?.rootViewController = trackingViewController
window?.addSubview((trackingViewController?.view)!)
window?.makeKeyAndVisible()
window?.layoutSubviews()
For every button click, a new window is added to the application.I want to remove the latest window added.
The number of windows present in the application can be known by using following code.
let windowz = UIApplication.shared.windows
print("subviews",windowz)

I think you get the wrong concept of navigation in iOS. Window is like a root object in which ViewControllers appear. So probably the solution you're looking in a first place is UINavigationController.
Apple Documentation on Navigation

For iOS 13 i was able to do it this way
I created array which contains the window using which this new viewController is being presented,
var arrWindow = [UIWindow]()
arrWindow.append(yourNewWindow)
// Note: This will be stored as strong reference so need to remove it.
Also store your original window in variable
let originalWindow = yourOriginalWindow
// Note: Same goes for this as well ,this will be stored as strong reference so need to remove it.
At the time of removing there are many ways to do it but this was the most suited way for me,
func removeAppendedWindow() {
for window in arrWindow {
if window != originalWindow {
if let index = arrWindow.index(of: window) {
window.isHidden = true
arrWindow.remove(at: index)
}
}
}
}

In the below code windowz is normal array.
let windowz = UIApplication.shared.windows
You can remove last by using
windowz.removeLast()

You should use View Controller instead of adding windows and pop it instead where you are removing the window.
Window is only one object for app and will contain the views.
Please correct your understanding and use View controllers.

Related

Swift UISplitViewController how to go from triple to double columns

I'm having a lot of trouble figuring out how to structure a UISplitViewController.
I want:
A sidebar in the primary view (always)
I want the 1st sidebar navigation item (animals) to show triple (sidebar, animal list, animal detail)
I want the 2nd sidebar navigation item (profile) to show double (sidebar, profile view)
I see other apps doing this (GitHub for example), but I've really got no idea how they're managing it. Resources are hard to find, and most tutorials I've seen just show one or the other column styles.
I'm mostly looking for answers on how to architecture this well, but any code would also be massively appreciated!
SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = ViewController(style: .tripleColumn)
window?.makeKeyAndVisible()
}
Root view controller
class ViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewControllers = [
SidebarViewController(),
AnimalsViewController(),
AnimalDetailViewController()
]
// Example attempt at removing the secondary view
setViewController(ProfileViewController(), for: .supplementary)
setViewController(nil, for: .secondary)
hide(.secondary)
}
}
Desired behaviour
Animals
Profile
Cheers!
There is no "official" way to do it but it is possible. As far as I can tell, one of the best ways so solve it is to have two instances of UISplitViewController in your root view controller and juggle between them when needed. Here is my approach (approximately):
Disclaimer: This code was consulted with Apple engineers during the last WWDC22 on UIKit Labs. They have confirmed that it is very unfortunate that they currently do not offer a convenient way of doing it, and that this approach is probably the best way to do it. Feedback was filed and its ID passed to the engineers so hopefully we get an official API in the iOS 17 :D
rdar://FB10140263
Step 1. Initialise the UISplitViewControllers
private lazy var doubleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(doubleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .doubleColumn))
private lazy var tripleColumnSVC: UISplitViewController = {
$0.primaryBackgroundStyle = .sidebar
// setup your SVC here
$0.setViewController(tripleColumnPrimaryNC, for: .primary)
return $0
}(UISplitViewController(style: .tripleColumn))
Step 2. Initialise your sidebar VC and two separate UINavigationControllers
I have found it to be the most reliable solution for swapping sidebar VC. With a single UINavigationController instance there was a bug that the sidebar would randomly not appear. Two instances solve this problem while still keeping a single SidebarVC with proper focus state and already laid out content.
// Sidebar is shared and swapped between two split views
private lazy var sideBar = YourSideBarViewController()
private lazy var doubleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
private lazy var tripleColumnPrimaryNC = UINavigationController(
rootViewController: UIViewController()
)
Step 3. Make a property to store currently displayed SVC
It will come in handy in the next step when toggling between the two instances.
private var current: UISplitViewController?
Step 4. Implement Toggling between two styles when needed
This function should be called every time you want to navigate to a different screen from sidebar.
private func toggleStyleIfNeeded(_ style: UISplitViewController.Style) {
switch style {
case .doubleColumn:
// skip if the desired column style is already set up
if current === doubleColumnSVC { return }
// reassign current
current = doubleColumnSVC
// here add doubleColumnSVC as child view controller
// here add doubleColumnSVC.view as subview
// swap the sidebar
doubleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove tripleColumnSVC from parent
// here remove tripleColumnSVC.view from superview
case .tripleColumn:
// skip if the desired column style is already set up
if current === tripleColumnSVC { return }
// reassign current
current = tripleColumnSVC
// here add tripleColumnSVC as child view controller
// here add tripleColumnSVC.view as subview
// swap the sidebar
tripleColumnPrimaryNC.setViewControllers([sideBar], animated: false)
// here remove doubleColumnSVC from parent
// here remove doubleColumnSVC.view from superview
default:
return
}
// If you are using UITabBarController for your compact style, assign it here
current?.setViewController(tabBar, for: .compact)
}
In lines that start with "here add" you will need to write your own code. I have simplified the code sample to make it shorter.
Step 5. Enjoy your SVC with dynamic columns!
Now you are basically ready to go! With this simple helper method on your root VC (or whichever one that is handling the navigation and managing the SVCs) you will have all the power that you need to achieve what you wanted, which is a UISplitViewController with dynamic number of columns!
func setViewController(
_ viewController: UIViewController,
for column: UISplitViewController.Column,
style: UISplitViewController.Style
) {
toggleStyleIfNeeded(style)
current?.setViewController(viewController, for: column)
}
We are using this approach in production for a few months now and it works great. The app supports iOS, iPadOS and Mac Catalyst. There are some things like customising the status bar style and getting consistent sidebar button experience a bit tricky to work perfectly but with some adjustments and help from the UISplitViewControllerDelegate everything is possible.
Good luck!
P.S. If anyone have walked this path before and is able to share suggestions, please do! I would love to learn more on how one could improve this dynamic split view experience both for users and developers.
To switch from 3 to 2 columns you must simply reinitialise the UISplitViewController with two columns (UISplitViewController(style: .doubleColumn) and reassign it to the window.rootViewController.
When reinitialising the UISplitViewController, you can either assign existing view controller objects, to maintain the current state, or initialise new ones. In case you assign existing view controller objects, it's probably handy to store these in variables after creating them for the first time.

Update to Xcode 11.3.1 - navigationBar and half of the Views disappear after storyboard refactoring

Using Xcode 11.3.1, Simulator11.3.1, iPhoneX, Swift5.1.3, iOS13.3,
I am wondering why half of my app suddenly disappears !!
Could it be the update to Xcode 11.3.1 ???
The following shows a screenshot of the Xcode Debug View Hierarchy.
The left side is what the iPhone 11 Pro Simulator shows and the right side is the Debug View Hierarchy:
Clearly there are many more objects in the view hierarchy (such as the round buttons at the bottom) that are not shown on the Simulator (and also not on a physical iPhoneX). Also the NavigationBar is missing completely !!!!
The blue highlighted object is a custom navigationBar (consisting of a stackView). This worked before but not since the Xcode update. I am really not believing this. What could go wrong here ??
If it is not the Xcode-update, then my refactoring of the storyboard could also be a cause of this view-losses.
Before my refactoring, the VC at question was a ChildViewController of another ViewController. Now, it is the entry point of the App. Could this change bring the view-losses ? I want to see a NavigationController with largeTitle. But there is no NavigationController whatsoever now!
Here is the code that sets up the navigationBar:
override func viewDidLoad() {
// set up navigationItem and navigationController look and feeel
navigationItem.largeTitleDisplayMode = .always
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationController?.set_iOS12_lookAndFeel()
navigationItem.title = "bluub"
}
And the needed NavigationController extension:
import UIKit
extension UINavigationController {
func set_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
self.keep_iOS12_lookAndFeel()
} else {
let attrLargeTitle = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
self.navigationBar.largeTitleTextAttributes = attrLargeTitle
let attrTitle = AppConstants.FontAttributes.NavBar_TitleTextAttributes
self.navigationBar.titleTextAttributes = attrTitle
}
}
private func keep_iOS12_lookAndFeel() {
if #available(iOS 13.0, *) {
let navBarAppearance = UINavigationBarAppearance()
navBarAppearance.configureWithDefaultBackground()
navBarAppearance.backgroundEffect = .init(style: .systemThickMaterialDark)
navBarAppearance.titleTextAttributes = AppConstants.FontAttributes.NavBar_TitleTextAttributes
navBarAppearance.largeTitleTextAttributes = AppConstants.FontAttributes.NavBar_LargeTitleTextAttributes
navBarAppearance.buttonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_ButtonAppearance_Normal
navBarAppearance.doneButtonAppearance.normal.titleTextAttributes = AppConstants.FontAttributes.NavBar_Done_ButtonAppearance_Normal
self.navigationBar.standardAppearance = navBarAppearance
self.navigationBar.scrollEdgeAppearance = navBarAppearance
}
}
}
.
---------------- more findings -----------------------------
After another storyboard refactoring, I could bring back the round menu buttons. However, the largeTitle-NavigationBar is still completely missing.
Frankly, the latest refactoring did not introduce any new constraints or other storyboard settings as before. The fact that I kicked out the NavigationController and replaced it by an identical new one, plus, re-assigned one or the other constraint of the menu-button-View, did bring the bottom menu back alive. As far as I can tell, no difference to the previous storyboard was introduced.
It is very annoying why a storyboard needs to be redrawn basically to render correctly. Something seems corrupt here as for the Xcode functionality with storyboard !
But lets leave this talk.
My remaining question:
How can I bring back a missing NavigationBar ?????????
.
---------------- another finding -----------------------------
If I reassign the "first-entry-ViewController" to the old ViewController that eventually adds the Menu-button-ViewController as a ChildViewController --> then everything works!
If I assign the "first-entry-ViewController" to be the Menu-button-ViewController directly, then the NavigationBar disappears !
Here is the overview:
I finally found a solution.
It indeed had to do with my login-architecture of this app.
The fact that only by setting the "first-entry-ViewController" as the old-Main-ViewController made a difference:
This old-Main-ViewController (that eventually adds the Menu-button-ViewController as its Child) did have the following line in its viewWillAppear method:
navigationController?.setNavigationBarHidden(true, animated: animated)
Its intention was actually to never show the navigationBar of its own. But instead load a ChildViewController that itself shows a navigationBar of its own.
The strange thing with storyboard: Even tough setting the Menu-button-ViewController as first-entry does somehow still consider the navigationController-hiding mechanism of the previous first-entry setting. This seems a bug to me inside storyboard. I would assume that visible navigationBar is the default behaviour. But having set it once to be hidden keeps it hidden, even tough the hiding-command is no longer executed. Anyway, very strange behaviour.
By eliminiting that line - or better - by adding it "with hidden = false" inside the Menu-Button-ViewController, makes the NavigationBar being shown again !!!
My learning is to keep an eye on all navigationController actions or mutations throughout the entire App hierarchy. The fact that a single ViewController might mutate something on its navigationController might not be enough. You have to check event parent-ViewControllers or segue-parents as well. And most annoying, applying a different first-entry to a VC does require you to overwrite default behaviours of your views to make sure your views are shown !

How to keep NSWindow.FrameAutosaveName out of scope of tabbed windows?

I have a NSWindowControllerclass in an app that can be used to show a standalone window or to show a tabbed window attached to a main window, which has a different window controller. If the user opens the window as standalone, then I want to use the same window position and size as the last standalone window session. It's comparable to opening a code view in Xcode in a new window.
So I have a self.windowFrameAutosaveName = NSWindow.FrameAutosaveName("Detail App Window") line in windowDidLoad. This works, but the problem is that it also works when the window is shown as a tab in the main window. Next time the user opens the same window as standalone, the size of the main window is being used.
Question: how can I keep self.windowFrameAutosaveName outside a tabbed window scenario?
I create the window controller like this:
let storyboard = NSStoryboard(name: "Detail", bundle: nil)
if let controller = storyboard.instantiateInitialController() as? DetailController {
document.addWindowController(controller)
if tabbed,
let mainWindow = document.windowControllers.first?.window,
let detailWindow = controller.window {
mainWindow.addTabbedWindow(detailWindow, ordered: .above)
}
else {
controller.showWindow(nil)
}
}
The code in windowDidLoad of the DetailController above is:
override func windowDidLoad() {
super.windowDidLoad()
self.shouldCascadeWindows = true
self.windowFrameAutosaveName = NSWindow.FrameAutosaveName("Detail App Window")
}
As you see, I use self.shouldCascadeWindows = true, and this must also keep working. I have experimented with detecting being in a tab scenario, but without any complete success. Also, moving the windowFrameAutosaveName before the controller.showWindow(nil) works in itself, but the windows are no longer loaded with shouldCascadeWindows = true.
Interesting, even Xcode doesn't do that, so maybe it's a limitation of AppKit.

(UIApplication.shared.keyWindow?.rootViewController as? BaseSlidingController)?.openMenu() return nil Swift 4.2

I have a problem with the above stated. I can not find the exact information on the forums. Most of them are outdated and I have written the code programmatically. I have a controller that contains a view to edit the profile. I can not access that after changing the function listed below. I have a rootviewcontroller set to something else, but I tried the UiApplication calls anyway and it return nil and I can not open the profile controller. This is the function listed below.
#objc func handleOpen2() {
(UIApplication.shared.keyWindow?.rootViewController as? BaseSlidingController)?.openMenu()
}
Xcode does not give me an error but I can not get my menu to open. My rootviewcontroller is set to something else in app delegate. I have a controller that is used to control the sliding menu when I press the edit profile button.
func openMenu() {
isMenuOpened = true
redViewLeadingConstraint.constant = menuWidth
redViewTrailingConstraint.constant = menuWidth
performAnimations()
setNeedsStatusBarAppearanceUpdate()
}
This code is used to open my side bar menu with the information I need and also to perform animations as well. I was wondering if someone had any idea what I can do different instead in my handleOpen2 function. If you need more code, please let me know. Thanks
On swift 5 version:
(UIApplication.shared.windows.first?.rootViewController as? BaseSlidingController)?.openMenu()

Opening window + view from an other view controller

I've got a ViewControllerOne. ViewControllerOne is connected via Ctrl-Drag (in storyboard) to a menu-button mBtn (which means I don't know how it is implemented programmatically).
Clicking on this mBtn, a ViewOne appears (present modal). This ViewOne is bound to ViewControllerOne. ViewOne has a button btnOne.
Clicking on btnOne I want ViewOne to be dismissed and ViewTwo to be shown. ViewTwo belongs to ViewControllerTwo and to WindowControllerTwo.
The WindowControllerTwo-ViewControllerTwo-binding is the standard case as created on a new project.
I have the following code in the IBAction for button btnOne in ViewControllerOne:
#IBAction func onbtnOnePressed(sender: AnyObject){
let m_WindowControllerTwo = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("WindowControllerTwo")) as! NSWindowController // I have no custom class for the window controller as I don't really know what I can use it for ...
let m_ViewTwo = WindowControllerTwo.contentViewController as! ViewControllerTwo // my custom class for ViewTwo
m_ViewTwo.attributeIWantToPassToThisView = self.x // an attribute I want to pass from view a to view b
m_WindowControllerTwo.contentViewController = m_ViewTwo // passing the attribute from a to b
m_WindowControllerTwo.showWindow(self) // this does not work
self.dismiss(nil) // see NOTE
}
This code actually does not work. On debugging it step by step, I'm seeing the window/view flickering but not appearing...
NOTE: I could connect the button btnOne with a ctrl-drag to ViewControllerTwo. This works. But then the current ViewOne does not get dismissed!
Question: What am I doing wrong here? In iOS swift this also works. I don't quite get the WindowController stuff, so I'll need your advice on this.
Instead of this: m_WindowControllerTwo.showWindow(self)
use:
let application = NSApplication.shared()
application.runModal(for: wordCountWindow) //this will present WindowControllerTwo modally.
then to close your present controller add this line: PresentWindowControllerName.close()