Swift | UIViewController not showing as PopUp but Fullscreen - swift

So i have a viewcontroller which is basically an alert window which is supposed to be a popup and be dismissed by the tap on outside its frame.
But whenever i call that VC, it is always displayed as fullscreen and not as a pop up window.
I have tried a couple of ways to do this, namely as mentioned below.
if let exp : String = expiredVehicles[i] {
expiredVehicleNumber = expiredVehicles[i]
let popUpVC = SubscriptionExpired()
popUpVC.modalTransitionStyle = .crossDissolve
popUpVC.modalPresentationStyle = .popover // also tried other presentation styles but none work and it is still fullscreen
popUpVC.view.backgroundColor = UIColor.white.withAlphaComponent(0.8)
self.present(popUpVC, animated: true, completion: nil)
}
in case anyone need to see the definition of that VC, i will be glad to share it
i feel i should mention that the VC to be displayed as a popup is inheriting UIViewController
Any insight that might help would be great.
Thanks for the input

One potential way is to add a tap gesture recognizer to your View, which dismisses the VC.
But this will only be helpful if this popup has read-only info and doesn't require any further action from the user.
func addTapRecognizer(){
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
self.view.addGestureRecognizer(tap)
}
#objc func handleTap(){
// dismiss the VC here
}
}

You can call following method to show popup:
let popupVC = SubscriptionExpired()
popupVC.modalPresentationStyle = .overCurrentContext
self.addChild(popupVC)
popupVC.view.frame = self.view.frame
self.view.addSubview(popupVC.view)
popupVC.didMove(toParent: self)
}
Then, for removing that popup you can use:
UIView.animate(withDuration: 0.25, animations: {
self.view.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.view.alpha = 0.0
}, completion: { (finished) in
if finished {
self.view.removeFromSuperview()
}
})
In that case I have a button inside popup and whenever that button pressed above method triggers. Can you please try it? I can edit my answer according to your needs.

You need to implement this in you presenting view controller:
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
// Return no adaptive presentation style, use default presentation behaviour
return .none
}
UIPopoverPresentationController displaying popover as full screen

Related

How to handle a built in AlertController / Email Prompt from appearing behind my view

My app contains a modal UIView that can be presented from anywhere. How this works is the present method attaches the view as a subview on the key window:
func present(_ completion: ((Bool) -> ())? = { _ in }) {
guard !isPresented else {
return
}
if !isBackgroundReady {
initializeBackground()
}
UIApplication.shared.keyWindow?.addSubview(backgroundView)
UIApplication.shared.keyWindow?.addSubview(self)
UIView.animate(withDuration: 0.3, animations: {
self.backgroundView.alpha = 0.35
self.alpha = 1.0
}, completion: { _ in
self.isPresented = true
completion?(true)
})
}
private func initializeBackground() {
backgroundView.backgroundColor = UIColor.black
backgroundView.alpha = 0.0
backgroundView.frame = CGRect(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY, width: UIScreen.main.bounds.width * 1.2, height: UIScreen.main.bounds.height * 1.2)
backgroundView.center = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
}
This modal contains an email link that users can click that opens up an email prompt (or an email action sheet if they long press it). This link is added by using the an NSAttributedString and it's .link attribute on a UITextView:
let supportString = NSMutableAttributedString(
string: "general.supportEmail".localized(),
attributes: [
.link: "mailto:\("general.supportEmail".localized())",
]
)
supportTextView.attributedText = supportString
However, when the email prompt or action sheet appears, it is displayed behind the modal view:
Is it possible to get the prompt/action sheet to appear above the modal view with the current way I present the modal, or will I need to add some sort of recognizer somewhere that detects when one of these views appears and temporarily dismiss the modal until my app view comes back into focus? If it's the later, how would I accomplish that?
The quick answer as to why this is happening is that you are presenting your custom modal view on top of the Window, which will be on top of everything, and your UIAlertController will be presented on the UIViewController presenting it (which is below your custom view).
One quick solution would be to always add your custom view as a subview on the current "top" UIViewController. You can do that with a UIViewController extension - something like this:
extension UIViewController {
static func topViewController(_ viewController: UIViewController? = nil) -> UIViewController? {
let viewController = viewController ?? UIApplication.shared.keyWindow?.rootViewController
if let navigationController = viewController as? UINavigationController, !navigationController.viewControllers.isEmpty {
return self.topViewController(navigationController.viewControllers.last)
} else if let tabBarController = viewController as? UITabBarController,
let selectedController = tabBarController.selectedViewController
{
return self.topViewController(selectedController)
} else if let presentedController = viewController?.presentedViewController {
return self.topViewController(presentedController)
}
return viewController
}
}
This extension will handle any UIViewController that is "on top", whether it's in a UINavigationController, a UITabBarController, or just presented modally, etc. Should cover all cases.
After that you can adjust your present method to take this into account:
func present(_ completion: ((Bool) -> ())? = { _ in }) {
guard !isPresented else {
return
}
if !isBackgroundReady {
initializeBackground()
}
guard let topViewController = UIViewController.topViewController() else { return }
topViewController.view.addSubview(backgroundView)
topViewController.view.addSubview(self)
UIView.animate(withDuration: 0.3, animations: {
self.backgroundView.alpha = 0.35
self.alpha = 1.0
}, completion: { _ in
self.isPresented = true
completion?(true)
})
}
Instead of adding in the window, add the modal in the navicontroller -> topviewcontroller.
Link: https://developer.apple.com/documentation/uikit/uinavigationcontroller/1621849-topviewcontroller.
This might help you.

Segue transitions to the wrong page Swift Xcode

I am trying to create a series of pages that work in tandem using custom segues. These segues work fine until I have them execute in a certain series. From VC1 I go to VC2 via the default modal segue (it pops up over the original segue). Then, from VC2 I go to VC3 using a custom horizontal segue (code below). Finally, I go back from VC3 to VC1 using a custom unwind horizontal segue. The problem is that when I go back to VC1, VC2 appears instead. I checked and ViewDidLoad does not execute in VC2 when it appears, but I can still interact with it when triggered. My best guess as to what is happening is that VC2 covers VC1 so when I go back to VC1, VC2 is displayed on top of it. Even if this is the problem, I don't know how to fix it. Code below:
Horizontal Segue:
class HorizontalSegue: UIStoryboardSegue {
override func perform() {
let src = self.source as UIViewController
let dst = self.destination as UIViewController
src.view.superview?.insertSubview(dst.view, aboveSubview: src.view)
dst.view.transform = CGAffineTransform(translationX: src.view.frame.size.width, y: 0)
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveEaseOut], animations: {
dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
},
completion: { finished in
src.present(dst, animated: false, completion: nil)
})
}
}
Unwind Horizontal Segue:
class UnwindHorizontalSegue: UIStoryboardSegue {
override func perform() {
let src = self.source as UIViewController
let dst = self.destination as UIViewController
src.view.superview?.insertSubview(dst.view, belowSubview: src.view)
src.view.transform = CGAffineTransform(translationX: 0, y: 0)
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveEaseIn], animations: {
src.view.transform = CGAffineTransform(translationX: src.view.frame.size.width, y: 0)
},
completion: { finished in
src.dismiss(animated: false, completion: nil)
})
}
}
The first thing I noticed is that you said ViewDidLoad is not called for VC2. Please see if ViewDidAppear (or will appear) is being called instead.
ViewDidLoad is only called when the view loads (initially, when it wasn't there before). ViewDidAppear should fire always, when the view is brought into the user's screen.
After that, log dst in your custom UIStoryBoardSegue. If that's an instance of VC2, you are simply pushing the wrong view controller.
I also see you're using src.present and src.dismiss in your UIStoryboardSegue. This means you're not actually pushing views, but presenting them "on top" of an active view controller. Try to rethink that logic, since this is very much likely where the problem lies.
I would rather try and push the view controllers (normally, instead of 'presenting' them) and change the UIStoryboardSegue appearance to fake the animation if that's what you're after.

Transition to a new ViewController embedded in an UINavigationController causes animation problem

I use a rootViewController and I want to move to another ViewController. The transition to the newViewController works with that code.
A problem occurs when the newViewController is embedded in an UINavigationController. Then the navigation bar is animating during the animation and changes the position.
The navigation bar is animating from the top left to the correct position.
fileprivate func animateTransition(to newViewController: UIViewController) {
currentViewController.willMove(toParent: nil)
addChild(newViewController)
newViewController.view.frame = view.bounds
transition(from: currentViewController, to: newViewController, duration: 2, options: [.transitionCrossDissolve, .curveEaseOut], animations: {
self.currentViewController.removeFromParent()
newViewController.didMove(toParent: self)
self.currentViewController = newViewController
}, completion: nil)
}
How is it possible to move to another UINavigationController with a "fade" animation and how can the navigation bar be at the correct position right from the beginning of the animation?
First, you should move your view controller clean-up code call from the animations closure into the completion closure:
currentViewController.willMove(toParent: nil)
addChild(newViewController)
newViewController.view.frame = view.bounds
transition(from: currentViewController, to: newViewController, duration: 2, options: [.transitionCrossDissolve, .curveEaseOut], animations: {
// this is intentionally blank
}, completion: { _ in
self.currentViewController.removeFromParent()
newViewController.didMove(toParent: self)
self.currentViewController = newViewController
})
You don't want to indicate the transition is done until the animation is complete.
To the navigation bar issue, rather than letting transition(from:to:duration:...) handle the manipulation of the view controller hierarchy, you can add it to your view hierarchy and then animate the unhiding of it. Usually you'd use the .showHideTransitionViews option, but transition is still doing something curious with the appearance methods that is confusing the navigation controller, so it's best to just animate it yourself:
currentViewController.willMove(toParent: nil)
addChild(newViewController)
newViewController.view.frame = view.bounds
newViewController.view.alpha = 0
view.addSubview(newViewController.view)
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseOut, animations: {
newViewController.view.alpha = 1
}, completion: { _ in
self.currentViewController.view.removeFromSuperview()
self.currentViewController.removeFromParent()
newViewController.didMove(toParent: self)
self.currentViewController = newViewController
})
That will allow it to present the navigation bar correctly from the beginning and just fade it in.
try putting this underneath the class declaration of the newViewController
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.setNavigationBarHidden(false, animated: false)
}
if you already have a viewDidLoad() in your ViewController, then just use the last part.
If that doesn't work let me know.

Swift 3 Popover Dim Background

I have read multiple places with suggestions on how to accomplish this. I went with adding a UI view in the background and setting it to disable and then after showing the popover, setting the view to enable.
As you can see it looks to work nicely:
But I do have two problems. The first one is once the popover is presented, you can tap anywhere on the background to dismiss the popover. Is there anywhere to block this from happening? I assumed my background UIView would block any inputs.
Also, after the popover is dismissed, the screen is still dim. I tried the following but neither of them load after dismissing the popover so the View never gets set back to disable:
override func viewDidAppear(_ animated: Bool) {
dimView.isHidden = true
}
override func viewWillAppear(_ animated: Bool) {
dimView.isHidden = true
}
EDIT:
This is the code that I use to present the popover:
let popover = storyboard?.instantiateViewController(withIdentifier: "PopoverVC")
popover?.modalPresentationStyle = .popover
popover?.popoverPresentationController?.delegate = self as? UIPopoverPresentationControllerDelegate
popover?.popoverPresentationController?.sourceView = self.view
popover?.popoverPresentationController?.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
popover?.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
dimView.isHidden = false
self.present(popover!, animated: false)
I realize that, dimView is not in PopoverVC, add it into PopoverVC and handle dismiss when tap on it.After the popover is dismissed viewDidAppear and viewWillAppear will not be called. So your screen is still blurry.If you add dimView into Popover, hope you can solve these issuses
I think you could solve your two problems with the UIPopoverPresentationControllerDelegate and a protocol/ delegate to tell the presenting viewcontroller when your are dismissing and hide your dimView.
The first issue can be implemented like this:
extension YourViewController: UIPopoverPresentationControllerDelegate {
func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool {
return false
}
For the second issue you can pass a function through delegation. Hopefully this link will help with that.
https://matteomanferdini.com/how-ios-view-controllers-communicate-with-each-other/
Cheers

Searchbar cancel button not visible only on iPad

First I want to say I didn't find any good answer about it in Swift. I created search bar. On button click it shows and on cancel it hides the searchbar. It is working properly, cancel button is visible on all iPhones but for some reason not on iPad. What should cause this?
That is how I create the searchbar:
//Create searchbar
func createSearchBar(){
searchBar.showsCancelButton = true
searchBar.tintColor = UIColor(red:0.184, green:0.996, blue:0.855, alpha:1.00)
searchBar.placeholder = "Search brands"
searchBar.delegate = self
searchBar.hidden = false
searchBar.alpha = 0
navigationItem.titleView = searchBar
navigationItem.setLeftBarButtonItem(menuButton, animated: true)
navigationItem.setRightBarButtonItem(searchButtton, animated: true)
UIView.animateWithDuration(0.5, animations: {
self.searchBar.alpha = 1
}, completion: { finished in
self.searchBar.becomeFirstResponder()
})
}
Faced the same one of my project. I've posted on apple forum and lot of developers commented as a xcode bug. So I added the cancel button manually for ipad views
func configureCancelBarButton() {
let cancelButton = UIBarButtonItem()
cancelButton.image = UIImage(named: "cancelButton")
cancelButton.action = #selector(viewController.ipadCancelButton(_:))
cancelButton.target = self
self.navigationItem.setRightBarButtonItem(cancelButton, animated: true)
}
And I previously posted an answer about this question. Check that too. :)
https://developer.apple.com/documentation/uikit/uisearchbar/1624283-showscancelbutton
The value of this property is ignored, and no cancel button is displayed, for apps running on iPad.
Apple add that in documents!
The UISearchbar's cancel button will work in iPads if you are NOT using the UISearchbar as NavigationBarItem.