Issue
I have a number of cases on iOS 11 (that do not occur on iOS 10 and below) where .UIKeyboardWillChangeFrame notification is not being fired — specifically when transitioning between view controllers where both view controllers have a UITextfield which is set as the fristResponder.
Since I have UI that needs to animate above the keyboard in response to the keyboard showing, receiving this notification is essential.
On iOS 10 and below, I get the notification on both view controllers (on showing VC A and also when pushing VC B). However, on iOS 11, the notification does not fire when pushing VC B. It's as if the keyboard remained in place.
Does anyone know what the cause of this is?
Details
Both view controllers inherit from a base view controller with the following implementation:
class BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: .UIKeyboardWillChangeFrame, object: nil)
}
// Update layout when the keyboard is shown or hidden
#objc func keyboardWillChangeFrame(notification : Notification) {
// Check if got info
if (notification.userInfo == nil) {
return;
}
// Get resize properties
let dict = notification.userInfo!
let rect = self.view.convert((((dict[UIKeyboardFrameEndUserInfoKey as NSObject] as Any) as AnyObject).cgRectValue)!, from: nil)
let size = self.view.bounds.size.height - rect.origin.y
let duration = ((dict[UIKeyboardAnimationDurationUserInfoKey] as Any) as AnyObject).doubleValue
let curve = UIViewAnimationCurve.init(rawValue: (((dict[UIKeyboardAnimationCurveUserInfoKey] as Any) as AnyObject).intValue)!)
self.keyboardOffset = max(0, size)
// Set animation options
var options : UIViewAnimationOptions
switch (curve!) {
case .easeInOut:
options = UIViewAnimationOptions()
case .easeIn:
options = UIViewAnimationOptions.curveEaseIn
case .easeOut:
options = UIViewAnimationOptions.curveEaseOut
case .linear:
options = UIViewAnimationOptions.curveLinear
}
// Animate the change
UIView.animate(withDuration: duration!, delay: 0, options: options, animations: { () -> Void in
// Relayout
self.relayout()
}, completion: nil)
}
}
Example of a subclass:
class ViewControllerA: BaseViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
passwordField.becomeFirstResponder()
}
func relayout() {
// ... do animations to show stuff above keyboard.
}
}
Note that .keyboardWillShow and .keybaordWillHide are also not fired on the transition to VC B.
You need to add self.view.endEditing(force:Bool) in you first view controller's viewWillDisappear method.
Generally, you can always call endEditing method when you changing the screen.
I've worked around this by saving the keyboard height in a class variable in VC A and then passing that along to VC B.
It's not ideal, as I would prefer my VC's to be independent for those details.
Related
I currently have a view controller that has a container view which functions similar to the bottom drawer in the iOS Maps application. When an image is scanned, the drawer animates onto the screen (it begins hidden off the screen at (0,700)) and displays information that comes from Firebase. Within this container, I have another container that allows me to have a PageViewController setup. This setup allows the user to swipe between 2 different sets of information (all of which needs to be loaded from Firebase).
I don't know how to reset the data within the second PageViewController so that whenever the drawer is dismissed through a button, the PageViewController system resets and I can use my function inside the secondPageViewController to reload the tableView it has inside.
TL;DR / More Information
Scanning an image is what loads the information into the ViewControllers.
I have one independent VC (ViewController) with a container view inside of it (BottomDrawerViewController). The BottomDrawerViewController also has a container inside of it which is a PageViewController with 2 pages (Swipe1ViewController & Swipe2ViewController)
The first page of the PageViewController setup works fine (Most likely because that information is loaded using an NSNotification)
I haven't been able to use an NSNotification to send and refresh this data the same way because this is not the first view controller that shows up after I post my notification.
I can't just load all my information independently in the second PageViewController because I get the pathway to the Firebase information from my original ViewController
Question: How can I reset both this data and the PageViewController so that whenever the user dismisses the bottom drawer the information on the second page is able to be refreshed on the next scan and the PageViewController starts on the first page? I thought maybe by forcing the view to disappear so that ViewDidLoad is called when it appears on screen but I am realizing this isn't possible.
I've attached some of the important code from all of the view controllers and my Firebase code in image and JSON form just in case that helps.
ViewController: -- This is the part that gets information to send to containers and what animates in the main container
ref.child("Directory").child(imageName).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot)
let content = ImageInformation(snapshot: snapshot)
// TODO: Deliver Image Metadata here
let imageInfo = ["eventTitle": imageName, "source": content.Source, "date": content.Date] as [String : Any]
NotificationCenter.default.post(name: Notification.Name.drawerVC, object: nil, userInfo: imageInfo)
NotificationCenter.default.post(name: Notification.Name.newsVC, object: nil, userInfo: imageInfo)
})
UIView.animate(withDuration: 0.50) {
self.bottomDrawer.frame = CGRect(x: self.bottomPosition.x, y: self.bottomPosition.y, width: self.bottomDrawer.frame.size.width, height: self.bottomDrawer.frame.size.height)
}
BottomDrawerViewController: -- This gets a notification from ViewController and changes the drawers title. It also sends a notification back to ViewController when the view is pressed so that it can be dismissed.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(appeared), name: Notification.Name.drawerVC, object: nil)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
view.addGestureRecognizer(tap)
}
#objc func appeared(_ notification: NSNotification) {
if let eventTitle = notification.userInfo?["eventTitle"] as? String {
self.eventTitleLabel.text = eventTitle
}
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
NotificationCenter.default.post(name: Notification.Name.mainVC, object: nil, userInfo: nil)
}
PageViewController: --This controls my PageViewController setup. Since everything is the same as a normal setup I did not include. This line allows me to send a variable from the first page to the second.
var eventTitle: String = ""
Swipe1ViewController: -- Receives a notification from ViewController and uses it to populate the Labels on screen. It also sends the event title ("Test Input" in the database) to the PageViewController to be used by the Swipe2ViewController (No code shown)
Swipe2ViewController: -- As of right now it takes the eventTitle variable from the PageViewController and uses it to get data from Firebase but this doesn't work multiple times because viewDidLoad() only runs once when the BottomDrawerViewController is swiped up. I've also tried putting the fetchHeadlines() function in viewDidAppear() but if the user dismisses and scans a completely different picture it won't update.
override func viewDidLoad() {
super.viewDidLoad()
self.newsTableView.delegate = self
self.newsTableView.dataSource = self
// fetchHeadlines()
}
override func viewDidAppear(_ animated: Bool) {
//
fetchHeadlines()
}
override func viewDidDisappear(_ animated: Bool) {
}
func fetchHeadlines() {
let pageVC = self.parent as! DrawerPageViewController
let eventTitle = pageVC.eventTitle
print("Title: " + eventTitle)
ref.child("Directory").child(eventTitle).child("News").observeSingleEvent(of: .value) { (newsSnapshot) in
for news in newsSnapshot.children.allObjects as! [DataSnapshot] {
print("Key: \(news.key)", "Value: \(news.value)")
//self.headlines.append(news)
DispatchQueue.global().async {
DispatchQueue.main.async {
// self.tableView.reloadData()
}
}
}
}
}
You shouldn't be using notifications to propagate data from one view to another. As you see, it doesn't work that well, it can be error prone, and it can be challenging to debug. Instead, you should embrace the relationship between parent and child view controllers. That is, The ViewController should pass the data to the BottomViewController, who passes the data to the PageViewController, and it's the PageViewController's job to set up the data in the sub-pages. This lets you put the data where it belongs when each view controller is put on screen.
Note that a parent view controller can access a child using the childViewControllers array. So ViewController can access the BottomViewController directly using:
if let bottomViewController = childViewControllers.first as? BottomViewController {
// set up the bottomViewController here
}
I'm working on a simple chat as a part of my App.
I have a list of Chats (i.e. friends) in a Table View Controller and Chat View Controller displaying messages for each chat. Both of them are embedded in a NavigationController, which is embedded in a TabBarController.
I want to place UITextView message text input field in a ChatViewController below my UITableVeiw that shows messages of that chat. I would also like the UITextView to look like it's embedded in a tab bar.
I've run through dozens of manuals tutorials and guides and that's where I got so far.
App simulator screenshots here: nice at the top, buggy at the bottom
Main.storyboard screenshot here
I use UITextView instead of UITextField because I want it to be able to change its size depending on content size
I use UIViewController instead of UITableViewController to be able to add another UIView along with UITableView
I don't use actual UITabBar to embed my UITextView because it behaves really weird when UITextView needs to change its height. And as far as I understood Apple doesn't support embedding UI element to UITabBar
What I do is I hide TabBar in ChatViewController viewWillAppear:
override func viewWillAppear(_ animated: Bool) {
tabBarController?.tabBar.isHidden = true
}
And I show it again in parent ChatsTableViewController:
override func viewWillAppear(_ animated: Bool) {
tabBarController?.tabBar.isHidden = false
}
Once TabBar is hidden, my custom stackView embedding textView and sendButton takes its place and operates quite normally as I want it to.
To handle keyboard behaviour properly I have an outlet for my inputStackView bottom constraint (selected on screenshot), which I update on keyboardWillShow/Hide Notifications. Code for that looks like this:
#IBOutlet weak var inputBottomConstraint: NSLayoutConstraint!
var inputBottomConstraintInitialValue: CGFloat!
//MARK: - Keyboard handling
private func enableKeyboardHideOnTap(){
NotificationCenter.default.addObserver(self, selector: #selector(ChatTableViewController.keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ChatTableViewController.keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ChatTableViewController.hideKeyboard))
self.view.addGestureRecognizer(tap)
}
#objc func hideKeyboard() {
textView.resignFirstResponder()
}
#objc func keyboardWillShow(notification: NSNotification) {
let info = notification.userInfo!
let keyboardFrame: CGRect = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let duration = info[UIKeyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) { [weak self] () -> Void in
self?.inputBottomConstraint.constant = keyboardFrame.size.height + 8
self?.view.layoutIfNeeded()
}
}
#objc func keyboardWillHide(notification: NSNotification) {
let duration = notification.userInfo![UIKeyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: duration) { [weak self] () -> Void in
guard let `self` = self else { return }
self.inputBottomConstraint.constant = self.inputBottomConstraintInitialValue
self.view.layoutIfNeeded()
}
}
Everything seems to work fine at this point:
TabBar is hidden in a ChatViewController and is shown in ChatsTableViewController
textView and sendButton embedded in a stackView slide along will keyboard up and down, as shown in simulator screenshots above.
The BUG appears when I swipe between ChatViewController and ChatsTableViewController instead of using NavigationControllers
When I start swiping back to ChatsTableViewController, it perform its viewWillAppear method, so tabBarController's tabBar become visible. And if I don't finish that swipe to ChatsTableViewController and go back ChatViewController, ChatViewController's viewWillAppear sets it back to invisible, but the input stackView doesn't get back to it's initial position. It just hangs there above the invisible tabBar.
I've tried moving
tabBarController?.tabBar.isHidden = false
from viewWillAppear to viewDidAppear in ChatsTableViewController.
It fixes the "floating input" issue but it adds a lag of about a second, when ChatsTableViewController is showing without tabBar. Looks not too good either.
Any ideas how that could be fixed properly?
I've been struggling with that issue for about a week))
Because your ChatsTableViewController is embedded in in UINavigationController and your ChatViewController is pushed you can override this hidesBottomBarWhenPushed in ChatViewController which will determine to show your view controller or not:
public override var hidesBottomBarWhenPushed: Bool {
get { return true }
set { super.hidesBottomBarWhenPushed = newValue }
}
This is a better way to hide the tab bar to my mind, because the frame of the view controllers will be adjusted.
Why do you change constraint on keyboard show/Hide. Try to Google inputAccesoryView, many chats use it. It’s natural behaviour is to go with the keyboard. Try FaceBook Messenger for example and see there how swiping hides the keyboard and Textfield together, you can’t accomplish this kind od behaviour with regular view.
Issue:
Modally presented view controller does not move back up after in-call status bar disappears, leaving 20px empty/transparent space at the top.
Normal : No Issues
In-Call : No Issues
After In-Call Disappears:
Leaves a 20px high empty/transparent space at top revealing orange view below. However the status bar is still present over the transparent area. Navigation Bar also leaves space for status bar, its' just 20px too low in placement.
iOS 10 based
Modally presented view controller
Custom Modal Presentation
Main View Controller behind is orange
Not using Autolayout
When rotated to Landscape, 20px In-Call Bar leaves and still leaves 20px gap.
I opt-out showing status bar in landscape orientations. (ie most stock apps)
I tried listening to App Delegates:
willChangeStatusBarFrame
didChangeStatusBarFrame
Also View Controller Based Notifications:
UIApplicationWillChangeStatusBarFrame
UIApplicationDidChangeStatusBarFrame
When I log the frame of presented view for all four above methods, the frame is always at (y: 0) origin.
Update
View Controller Custom Modal Presentation
let storyboard = UIStoryboard(name: "StoryBoard1", bundle: nil)
self.modalVC = storyboard.instantiateViewController(withIdentifier: "My Modal View Controller") as? MyModalViewController
self.modalVC!.transitioningDelegate = self
self.modalVC.modalPresentationStyle = .custom
self.modalVC.modalPresentationCapturesStatusBarAppearance = true;
self.present(self.modalVC!, animated: true, completion: nil)
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
toViewController!.view.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [.curveEaseOut], animations: { () -> Void in
toViewController!.view.transform = CGAffineTransform.identity
}, completion: { (completed) -> Void in
transitionContext.completeTransition(completed)
})
}
I've been looking for a solution for 3 days. I don't like this solution but didn't found better way how to fix it.
I'he got situation when rootViewController view has bigger height for 20 points than window, when I've got notification about status bar height updates I manually setup correct value.
Add method to the AppDelegate.swift
func application(_ application: UIApplication, didChangeStatusBarFrame oldStatusBarFrame: CGRect) {
if let window = application.keyWindow {
window.rootViewController?.view.frame = window.frame
}
}
After that it works as expected (even after orientation changes).
Hope it will help someone, because I spent too much time on this.
P.S. It blinks a little bit, but works.
I faced this problem too but after I put this method, problem is gone.
iOS has its default method willChangeStatusBarFrame for handling status bar. Please put this method and check it .
func application(_ application: UIApplication, willChangeStatusBarFrame newStatusBarFrame: CGRect) {
UIView.animate(withDuration: 0.35, animations: {() -> Void in
let windowFrame: CGRect? = ((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame
if newStatusBarFrame.size.height > 20 {
windowFrame?.origin?.y = newStatusBarFrame.size.height - 20
// old status bar frame is 20
}
else {
windowFrame?.origin?.y = 0.0
}
((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame = windowFrame
})
}
Hope this thing will help you.
Thank you
I had the same issue with the personnal hospot modifying the status bar.
The solution is to register to the system notification for the change of status bar frame, this will allow you to update your layout and should fix any layout issue you might have.
My solution which should work exactly the same for you is this :
In your view controller, in viewWillAppear suscribe to the UIApplicationDidChangeStatusBarFrameNotification
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(myControllerName.handleFrameResize(_:)), name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
Create your selector method
func handleFrameResize(notification: NSNotification) {
self.view.layoutIfNeeded() }
Remove your controller from notification center in viewWillDisappear
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
You also need your modal to be in charge of the status bar so you should set
destVC.modalPresentationCapturesStatusBarAppearance = true
before presenting the view.
You can either implement this on every controller susceptible to have a change on the status bar, or you could make another class which will do it for every controller, like passing self to a method, keep the reference to change the layout and have a method to remove self. You know, in order to reuse code.
I think this is a bug in UIKit. The containerView that contains a presented controller's view which was presented using a custom transition does not seem to move back completely when the status bar returns to normal size. (You can check the view hierarchy after closing the in call status bar)
To solve it you can provide a custom presentation controller when presenting. And then if you don't need the presenting controller's view to remain in the view hierarchy, you can just return true for shouldRemovePresentersView property of the presentation controller, and that's it.
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool {
return true
}
}
or if you need the presenting controller's view to remain, you can observe status bar frame change and manually adjust containerView to be the same size as its superview
class PresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
NotificationCenter.default.addObserver(self,
selector: #selector(self.onStatusBarChanged),
name: .UIApplicationWillChangeStatusBarFrame,
object: nil)
}
#objc func onStatusBarChanged(note: NSNotification) {
//I can't find a way to ask the system for the values of these constants, maybe you can
if UIApplication.shared.statusBarFrame.height <= 20,
let superView = containerView?.superview {
UIView.animate(withDuration: 0.4, animations: {
self.containerView?.frame = superView.bounds
})
}
}
}
I've been looking for a solution to this problem. In fact, I posted a new question similar to this one. Here: How To Avoid iOS Blue Location NavigationBar Messing Up My StatusBar?
Believe me, I've been solving this for a couple of days now and it's really annoying having your screen messed up because of the iOS's status bar changes by in-call, hotspot, and location.
I've tried implementing Modi's answer, I put that piece of code in my AppDelegate and modified it a bit, but no luck. and I believe iOS is doing that automatically so you do not have to implement that by yourself.
Before I discovered the culprit of the problem, I did try every solution in this particular question. No need to implement AppDelegate's method willChangeStatusBar... or add a notification to observe statusBar changes.
I also did redoing some of the flows of my project, by doing some screens programmatically (I'm using storyboards). And I experimented a bit, then inspected my previous and other current projects why they are doing the adjustment properly :)
Bottom line is: I am presenting my main screen with UITabBarController in such a wrong way.
Please always take note of the modalPresentationStyle. I got the idea to check out my code because of Noah's comment.
Sample:
func presentDashboard() {
if let tabBarController = R.storyboard.root.baseTabBarController() {
tabBarController.selectedIndex = 1
tabBarController.modalPresentationStyle = .fullScreen
tabBarController.modalTransitionStyle = .crossDissolve
self.baseTabBarController = tabBarController
self.navigationController?.present(tabBarController, animated: true, completion: nil)
}
}
I solve this issue by using one line of code
In Objective C
tabBar.autoresizingMask = (UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleTopMargin);
In Swift
self.tabBarController?.tabBar.autoresizingMask =
UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleTopMargin.rawValue)))`
You just need to make autoresizingMask of tabBar flexible from top.
In my case, I'm using custom presentation style for my ViewController.
The problem is that the Y position is not calculated well.
Let's say the original screen height is 736p.
Try printing the view.frame.origin.y and view.frame.height, you'll find that the height is 716p and the y is 20.
But the display height is 736 - 20(in-call status bar extra height) - 20(y position).
That is why our view is cut from the bottom of the ViewController and why there's a 20p margin to the top.
But if you go back to see the navigation controller's frame value.
You'll find that no matter the in-call status bar is showing or not, the y position is always 0.
So, all we have to do is to set the y position to zero.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let f = self.view.frame
if f.origin.y != 0 {
self.view.frame = CGRect(x: f.origin.x, y: 0, width: f.width, height: f.height)
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
}
}
Be sure to set the frame of the view controller's view you are presenting to the bounds of the container view, after it has been added to the container view. This solved the issue for me.
containerView.addSubview(toViewController.view)
toViewController.view.frame = containerView.bounds
I am dismissing a popover view controller programmatically. How can i detect that in my first view controller? Is there a way to send values from the popover to the first one?
Note: popoverPresentationControllerDidDismissPopover does not work when dismissed programmatically.
Any proposition?
this is my code in the main view controller:
let addFriendsPopoverViewController = storyboard?.instantiateViewControllerWithIdentifier("HomeEmotionPopOver") as! EmotionPopOverViewController
addFriendsPopoverViewController.modalInPopover = true
addFriendsPopoverViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
addFriendsPopoverViewController.preferredContentSize = CGSizeMake(100, 100)
let popoverMenuViewController = addFriendsPopoverViewController.popoverPresentationController
popoverMenuViewController!.permittedArrowDirections = .Any
popoverMenuViewController!.delegate = self
popoverMenuViewController!.sourceView = self.view
let height = (self.tableView.rowHeight - HeartAttributes.heartSize / 2.0 - 10) + (self.tableView.rowHeight * CGFloat((sender.view?.tag)!)) - 50
popoverMenuViewController!.sourceRect = CGRect(
x: 30,
y: height,
width: 1,
height: 1)
presentViewController(
addFriendsPopoverViewController,
animated: true,
completion: nil)
and in the popover view controller, i'm dismissing it from a button IBAction:
#IBAction func dismissPop(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
The way you have worded your question is that you are looking for a function on the main view controller that is called when a popover is dismissed.
This technically happens with viewDidAppear(animated:). However, it isn't a full proof solution. If your popover doesn't cover the full screen context, this function wont fire, so it is an unreliable solution.
Really what you want is to invoke a function from the popover alerting the main view controller that it has finished/dismissed. This is easily done with a delegate protocol
protocol PopoverDelegate {
func popoverDismissed()
}
class PopoverViewController {
weak var delegate: PopoverDelegate?
//Your Popover View Controller Code
}
Add the protocol conformance to your main view controller
class MainViewController: UIViewController, PopoverDelegate {
//Main View Controller code
}
Then you need to set the delegate to for the popover to be the main view controller.
let addFriendsPopoverViewController = storyboard?.instantiateViewControllerWithIdentifier("HomeEmotionPopOver") as! EmotionPopOverViewController
addFriendsPopoverViewController.delegate = self
//The rest of your code
Finally, call this delegate function from your popover view controller when you dismiss.
#IBAction func dismissPop(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
delegate?.popoverDismissed()
}
And in your main view controller, implement the delegate method
func popoverDismissed() {
//Any code to run when popover is dismissed
}
The trick is to dismiss the segue yourself but make it seem that the user initiated it so it can be detected by the delegate method popoverPresentationControllerDidDismissPopover().
I did it by adding a completion closure to the presentingViewController dismiss() function and directly invoked the routine.
if let pvc = self.presentingViewController {
var didDismiss : ((UIPopoverPresentationController) -> Void)? = nil
if let delegate = popoverPresentationController?.delegate {
// check it is okay to dismiss the popover
let okayToDismiss = delegate.popoverPresentationControllerShouldDismissPopover?(popoverPresentationController!) ?? true
if okayToDismiss {
// create completion closure
didDismiss = delegate.popoverPresentationControllerDidDismissPopover
}
}
// use local var to avoid memory leaks
let ppc = popoverPresentationController
// dismiss popover with completion closure
pvc.dismiss(animated: true) {
didDismiss?(ppc!)
}
}
It is working fine for me.
I'm trying to add a simple popoverController to my iphone app, and I'm currently struggling with the classic "blank screen" which covers everything when I tap the button.
My code looks like this:
#IBAction func sendTapped(sender: UIBarButtonItem) {
var popView = PopViewController(nibName: "PopView", bundle: nil)
var popController = UIPopoverController(contentViewController: popView)
popController.popoverContentSize = CGSize(width: 3, height: 3)
popController.presentPopoverFromBarButtonItem(sendTappedOutl, permittedArrowDirections: UIPopoverArrowDirection.Up, animated: true)
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController!) -> UIModalPresentationStyle {
// Return no adaptive presentation style, use default presentation behaviour
return .None
}
}
The adaptivePresentationStyleForPresentationController-function was just something I added because I read somewhere that this is what you need to implement to get this function on the iphone. But still: there is still a blank image covering the whole screen, and I do not know how to fix it.
Any suggestions would be appreciated.
The solution I implemented for this is based on an example presented in the 2014 WWDC session View Controller Advancements in iOS 8 (see the slide notes). Note that you do have to implement the adaptivePresentationStyleForPresentationController function as a part of the UIPopoverPresentationControllerDelegate, but that function should be outside of your sendTapped function in your main view controller, and you must specify UIPopoverPresentationControllerDelegate in your class declaration line in that file to make sure that your code modifies that behaviour. I also took the liberty to separate out the logic to present a view controller in a popover into its own function and added a check to make sure the function does not present the request view controller if it is already presented in the current context.
So, your solution could look something like this:
// ViewController must implement UIPopoverPresentationControllerDelegate
class TheViewController: UIViewController, UIPopoverPresentationControllerDelegate {
// ...
// The contents of TheViewController class
// ...
#IBAction func sendTapped(sender: UIBarButtonItem) {
let popView = PopViewController(nibName: "PopView", bundle: nil)
self.presentViewControllerAsPopover(popView, barButtonItem: sender)
}
func presentViewControllerAsPopover(viewController: UIViewController, barButtonItem: UIBarButtonItem) {
if let presentedVC = self.presentedViewController {
if presentedVC.nibName == viewController.nibName {
// The view is already being presented
return
}
}
// Specify presentation style first (makes the popoverPresentationController property available)
viewController.modalPresentationStyle = .Popover
let viewPresentationController = viewController.popoverPresentationController?
if let presentationController = viewPresentationController {
presentationController.delegate = self
presentationController.barButtonItem = barButtonItem
presentationController.permittedArrowDirections = .Up
}
viewController.preferredContentSize = CGSize(width: 30, height: 30)
self.presentViewController(viewController, animated: true, completion: nil)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
}
Real world implementation
I implemented this approach for input validation on a sign up form in an in-progress app that I host on Github. I implemented it as extensions to UIVIewController in UIViewController+Extensions.swift. You can see it in use in the validation functions in AuthViewController.swift. The presentAlertPopover method takes a string and uses it to set the value of a UILabel in a GenericAlertViewController that I have set up (makes it easy to have dynamic text popovers). But the actual popover magic all happens in the presentViewControllerAsPopover method, which takes two parameters: the UIViewController instance to be presented, and a UIView object to use as the anchor from which to present the popover. The arrow direction is hardcoded as UIPopoverArrowDirection.Up, but that wouldn’t be hard to change.