How to use UIWindowScene.windows on iOS 15? [duplicate] - swift

This question already has answers here:
How to resolve: 'keyWindow' was deprecated in iOS 13.0
(27 answers)
Closed 1 year ago.
Currently, with iOS 14.6, I can call a function in my app that displays a share sheet using the following code:
func share(link: URL) {
let activityView = UIActivityViewController(activityItems: [link], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(activityView, animated: true, completion: nil)
}
Since iOS 15 beta, Xcode tells me "'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead". How can I update this so that my share sheet will work properly in this new version? Thanks!

(Tested with iOS 15.2 running on Xcode 13.2.1)
Improving on Rachid's answer, here is a Swiftier version:
extension UIApplication {
var keyWindow: UIWindow? {
// Get connected scenes
return UIApplication.shared.connectedScenes
// Keep only active scenes, onscreen and visible to the user
.filter { $0.activationState == .foregroundActive }
// Keep only the first `UIWindowScene`
.first(where: { $0 is UIWindowScene })
// Get its associated windows
.flatMap({ $0 as? UIWindowScene })?.windows
// Finally, keep only the key window
.first(where: \.isKeyWindow)
}
}
If you want to find the presented UIViewController in the key UIWindow , here is another extension you could find useful:
extension UIApplication {
var keyWindowPresentedController: UIViewController? {
var viewController = self.keyWindow?.rootViewController
// If root `UIViewController` is a `UITabBarController`
if let presentedController = viewController as? UITabBarController {
// Move to selected `UIViewController`
viewController = presentedController.selectedViewController
}
// Go deeper to find the last presented `UIViewController`
while let presentedController = viewController?.presentedViewController {
// If root `UIViewController` is a `UITabBarController`
if let presentedController = presentedController as? UITabBarController {
// Move to selected `UIViewController`
viewController = presentedController.selectedViewController
} else {
// Otherwise, go deeper
viewController = presentedController
}
}
return viewController
}
}
You can put this wherever you want, but I personally added it as an extension to UIViewController.
This allows me to add more useful extensions, like ones to present UIViewControllers more easily for example:
extension UIViewController {
func presentInKeyWindow(animated: Bool = true, completion: (() -> Void)? = nil) {
DispatchQueue.main.async {
UIApplication.shared.keyWindow?.rootViewController?
.present(self, animated: animated, completion: completion)
}
}
func presentInKeyWindowPresentedController(animated: Bool = true, completion: (() -> Void)? = nil) {
DispatchQueue.main.async {
UIApplication.shared.keyWindowPresentedController?
.present(self, animated: animated, completion: completion)
}
}
}

Related

<_SFAppPasswordSavingViewController:> that is already being presented by <UIKeyboardHiddenViewController_Save>

I am working on an iPad application that has both Portrait and Landscape orientation. The issue that I am facing is regarding the the popup presentation that iPad shows you for saving password while you log-in in the application.
Now, when the pop-up is presented, at that moment if I try to rotate the device, the application crashes with the message
Fatal Exception: NSInvalidArgumentException
Application tried to present modally a view controller <_SFAppPasswordSavingViewController: 0x107be7230> that is already being presented by <UIKeyboardHiddenViewController_Save: 0x107a49190>.
Now, the problem is that SFAppPasswordSavingViewController presentation is not controlled by me. Also UIKeyboardHiddenViewController_Save,is something I am not controlling.
Please help me out on how to solve this issue or let me know if I am missing anything.
This is how I am presenting Login screen in my application.
guard let loginVC = LoginVC.loadFromXIB() else {
Log.w("LoginVC could not be loaded")
return
}
let navVC = PopNavigationController(rootViewController: loginVC)
UIWindow.key?.replaceRootViewControllerWith(navVC, animated: true, completion: nil)
Here replaceRootViewControllerWith is a custom function that is an extension on UIWindow.
extension UIWindow {
func replaceRootViewControllerWith(_ replacementController: UIViewController,
animated: Bool,
completion: (() -> Void)?) {
let snapshotImageView = UIImageView(image: self.snapshot())
self.addSubview(snapshotImageView)
let dismissCompletion = { [weak self] in // dismiss all modal view controllers
self?.rootViewController = replacementController
self?.bringSubviewToFront(snapshotImageView)
if animated {
UIView.animate(withDuration: 0.4, animations: { () -> Void in
snapshotImageView.alpha = 0
}, completion: { (_) -> Void in
snapshotImageView.removeFromSuperview()
completion?()
})
} else {
snapshotImageView.removeFromSuperview()
completion?()
}
}
DispatchQueue.main.async { [weak self] in
if self?.rootViewController?.presentedViewController != nil {
self?.rootViewController?.dismiss(animated: false, completion: dismissCompletion)
} else {
dismissCompletion()
}
}
}
}

ViewController present does not always work

I have some problems displaying a viewcontroller in my IOS app.
Sometimes it works and the view is displayed, but sometimes and I guess when the context is a bit different it will not work. No errors or warnings in the debugger and it can find the ViewController from the Main storyboard (at least it is not nil)
It use to work with self.present but that seems not to work anymore.
#IBAction func showHistoryButton(_ sender: MDCButton) {
let exercisesHistoryVC = ExercisesHistoryViewController.instantiate(from: .Main)
exercisesHistoryVC.modalPresentationStyle = .fullScreen
let appDeligate = UIApplication.shared.delegate as! AppDelegate
appDeligate.window?.rootViewController!.present(exercisesHistoryVC,animated: true,completion: nil)
// parent?.present(exercisesHistoryVC, animated: true, completion: nil)
}
Use the code like below,while Present New View Controller
#IBAction func showHistoryButton(_ sender: MDCButton) {
let exercisesHistoryVC = ExercisesHistoryViewController.instantiate(from: .Main)
exercisesHistoryVC.modalPresentationStyle = .fullScreen
UIApplication.topViewController()?.present(exercisesHistoryVC, animated: false, completion: nil)
}
extension UIApplication {
static func topViewController(base: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}

Prevent ViewController from stacking in the background when created with present

I use this code to open a new ViewController:
// Get a random next post
#IBAction func buttonNextPostTapped(_ sender: UIButton) {
let postNumber = Int.random(in: 0 ..< postIds.count)
let postId = postIds[postNumber]
PostApi.shared.getPost(postId: postId) { (post) in
let storyBoard : UIStoryboard = UIStoryboard(name: "MainApplication", bundle: nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "PostsViewController") as! PostsViewController
nextViewController.post = post
nextViewController.isFromRandom = true
self.present(nextViewController, animated: true, completion: {})
}
}
This code will open the same ViewController with different data. It works, however, the "old" ViewControllers will stack in the background. So if I open 10 new ViewControllers, I have 10 VC in the background.
How can I present a new ViewController, and dismiss the "old" one?
Using setViewControllers function from UINavigationController is the best way.
func setViewControllers(_ viewControllers: [UIViewController], animated: Bool)
And you can remove whichever controller you want to from stack like
if var navigationControllersArray:Array = (self.navigationController?.viewControllers) {
navigationControllersArray.remove(at: navigationControllersArray.count-2)
self.navigationController?.viewControllers = navigationControllersArray
}

Removing Data on Maps with different view controllers

I am really struggling on an issue that I think is rather interesting and quite difficult. My application lets the user create annotation locations within a Mapview. They also have the option to edit and delete these locations in another modal view controller.
The issue I am facing is that when the user presses delete, which removes the location from firebase, the annotation is still displayed upon the map. I cannot reload my annotation data within the view did appear as this does not suit my application. I cant have my annotations being reloaded every time I bring up the Mapview.
I need to figure out a way to implement an annotation reload when the delete button is pressed. However, as this happens within my delete view controller (which does not contain the mapView) I cannot use the reload function. Is there a way to connect view controllers so that I can apply the reload function when delete is pressed?
Updated Code **
This is my map view controller:
class ViewController: UIViewController, SideBarDelegate, MGLMapViewDelegate, DeleteVCDelegate {
let EditSaveSpotController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "EditVC") as! EditSaveSpotViewController
override func viewDidLoad() {
super.viewDidLoad()
EditSaveSpotController.delegate = self
}
func wholeRefresh() {
let uid = FIRAuth.auth()!.currentUser!.uid
let userLocationsRef = FIRDatabase.database().reference(withPath: "users/\(uid)/personalLocations")
userLocationsRef.observe(.value, with: { snapshot in
for item in snapshot.children {
guard let snapshot = item as? FIRDataSnapshot else { continue }
let newSkatepark = Skatepark(snapshot: snapshot)
self.skateparks.append(newSkatepark)
self.addAnnotation(park: newSkatepark)
}
})
if let annotations = mapView.annotations {
mapView.removeAnnotations(annotations)
}
for item in skateparks {
self.addAnnotation(park: item)
}
}
This is my delete view controller:
import UIKit
import Firebase
protocol DeleteVCDelegate {
func wholeRefresh()
}
class EditSaveSpotViewController: UIViewController {
var delegate: DeleteVCDelegate?
#IBAction func deleteSkateSpot(_ sender: Any) {
ref = FIRDatabase.database().reference(withPath: "users").child(Api.User.CURRENT_USER!.uid).child("personalLocations/\(parkId!)")
ref.observe(.value, with: { (snapshot) in
self.ref.setValue(nil)
self.dismiss(animated: true, completion: nil)
self.delegate?.wholeRefresh()
// self.delegate?.mainRefresh()
print("CheckWorking")
})
}
}
This is very high level and I did not have a chance to verify but it should be enough to get you going:
Modal Delete View
protocol DeleteVCDelegate {
func mainRefresh()
}
class DeleteVC: UIViewController {
var delegate: DeleteVCDelegate?
//your delete code
#IBAction func deleteSkateSpot(_ sender: Any) {
ref = FIRDatabase.database().reference(withPath: "users").child(Api.User.CURRENT_USER!.uid).child("personalLocations/\(parkId!)")
ref.observe(.value, with: { (snapshot) in
self.ref.setValue(nil)
//call to delegate
self.delegate?.mainRefresh()
})
}
}
MapView Class (implement DeleteVCDelegate)
class mapVC: MKMapViewDelegate, DeleteVCDelegate{
//when you present your DeleteVC set its delegate to the map view
let vc=(self.storyboard?.instantiateViewController(withIdentifier: "deleteVC"))! as! DeleteVC
//set the delegate
vc.delegate=self
//present deleteVC
self.present(vc, animated: true, completion:nil)
//implement delegate method of DeleteVC
func mainRefresh(){
//dismiss modal
self.dismiss(animated: true) {
//update view
self.loadLocations()
self.annotationRefresh()
}
}
}

Blank screen first time launching app xcode

I am having a little issue with logging into an app the first time with a blank screen and I am getting the warning "Attempt to present on whose view is not in the window hierarchy!" After I close and relaunch, the views appear fine. I believe it has something to do with the rootViewController but not sure... Thanks in advance for any help or direction!
App delegate
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var ref:FIRDatabaseReference?
var databaseHandle:FIRDatabaseHandle?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
window?.rootViewController = MainNavigationController()
FIRApp.configure()
ref = FIRDatabase.database().reference()
return true
}
Navigation controller as rootViewController
class MainNavigationController: UINavigationController {
var segmentedController: UISegmentedControl!
override func viewDidLoad() {
super.viewDidLoad()
let vc1 = TravelersFeedVC()
let vc2 = ProfileVC()
if isLoggedIn() {
// assume user is logged in
let homeController = HomeController()
viewControllers = [homeController]
homeController.firstViewController = vc1
homeController.secondViewController = vc2
} else {
perform(#selector(showLoginController), with: nil, afterDelay: 0.01)
}
}
fileprivate func isLoggedIn() -> Bool {
return UserDefaults.standard.isLoggedIn()
}
func showLoginController() {
let loginController = LoginController()
present(loginController, animated: true, completion: {
// perhaps do something here later
})
}
}
// log in function called
func finishLoggingIn() {
let rootViewController = UIApplication.shared.keyWindow?.rootViewController
guard let mainNavigationController = rootViewController as? MainNavigationController else { return }
let vc1 = TravelersFeedVC()
let vc2 = ProfileVC()
if isLoggedIn() {
// assume user is logged in
let homeController = HomeController()
mainNavigationController.viewControllers = [HomeController()]
homeController.firstViewController = vc1
homeController.secondViewController = vc2
} else {
perform(#selector(showLoginController), with: nil, afterDelay: 0.01)
}
UserDefaults.standard.setIsLoggedIn(value: true)
dismiss(animated: true, completion: nil)
}
Ok, so my last answer was wrong (i deleted it), the things is that in app there is a keywindow, which is your navcontroller, and you cannot present anything on this one untill it will load its subviews (even if it hasn't any), and that would happend on viewdidappear, so you should put your code from viewdidload there.