I am working on an app with a popover that displays a UIDatePicker. I have it set up to always be a popup view, even on phones. It works until I rotate the device to landscape. When I do this, the popover displays the way a popover would normally display on an iPhone that is in landscape mode.
This is what it looks like in portrait mode. It's exactly what I wanted it to look like.
And here it is in landscape mode:
It doesn't matter if I turn the device when the popup is already visible or if I pop it up after the device is turned. This is the code that I am using to display the popup.
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
#IBAction func showPopUpDate(_ sender: UIButton) {
let popoverVC = storyboard?.instantiateViewController(withIdentifier: "DateTimeSelectorView") as! DateVC
popoverVC.modalPresentationStyle = .popover
popoverVC.preferredContentSize = CGSize(width: 350, height: 350)
popoverVC.labelTag = sender.tag
if sender.tag == 0 {
popoverVC.showType = .date
popoverVC.passedDate = date
}
else {
popoverVC.showType = .time
popoverVC.passedDate = time
}
if let popoverController = popoverVC.popoverPresentationController {
popoverController.permittedArrowDirections = .any
popoverController.sourceRect = CGRect(x: 0, y: 0, width: 20, height: 20)
popoverController.delegate = self
popoverController.sourceView = sender
popoverVC.delegate = self
present(popoverVC, animated: true, completion: nil)
}
}
How can I set this up so that the popover continues to look like it does in portrait mode?
Please add vertically center and horizontally center constrain to the view.
Related
I'm using a custom UIPresentationController to present a view modally. After presenting the view, the first textfield in the presented view becomes the first responder and the keyboard shows up. To ensure that the view is still visible, I move it up. However, when I do this the frameOfPresentedViewInContainerView is not matching the actual frame of the view anymore. Because of this, when I tap on the view it's being dismissed, because there's a tapGestureRecogziner on the backgroundView which is on top of the presentingView. How to notify the presentingController that the frame/position of the presentedView has changed?
In the UIPresentationController:
override var frameOfPresentedViewInContainerView: CGRect {
var frame = CGRect.zero
let safeAreaBottom = self.presentingViewController.view.safeAreaInsets.bottom
guard let height = presentedView?.frame.height else { return frame }
if let containerBounds = containerView?.bounds {
frame = CGRect(x: 0,
y: containerBounds.height - height - safeAreaBottom,
width: containerBounds.width,
height: height + safeAreaBottom)
}
return frame
}
override func presentationTransitionWillBegin() {
if let containerView = self.containerView, let coordinator = presentingViewController.transitionCoordinator {
containerView.addSubview(self.dimmedBackgroundView)
self.dimmedBackgroundView.backgroundColor = .black
self.dimmedBackgroundView.frame = containerView.bounds
self.dimmedBackgroundView.alpha = 0
coordinator.animate(alongsideTransition: { _ in
self.dimmedBackgroundView.alpha = 0.5
}, completion: nil)
}
}
Presenting the view modally:
let overlayVC = CreateEventViewController()
overlayVC.transitioningDelegate = self.transitioningDelegate
overlayVC.modalPresentationStyle = .custom
self.present(overlayVC, animated: true, completion: nil)
Animation when keyboard appears (in the presented view):
#objc func animateWithKeyboard(notification: NSNotification) {
let userInfo = notification.userInfo!
guard let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height,
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double,
let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else {
return
}
// bottomContraint is the constraint that pins content to the bottom of the superview.
let moveUp = (notification.name == UIResponder.keyboardWillShowNotification)
bottomConstraint.constant = moveUp ? (keyboardHeight) : originalBottomValue
let options = UIView.AnimationOptions(rawValue: curve << 16)
UIView.animate(withDuration: duration, delay: 0,
options: options,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
From the Apple documentation:
UIKit calls this method multiple times during the course of a
presentation, so your implementation should return the same frame
rectangle each time. Do not use this method to make changes to your
view hierarchy or perform other one-time tasks.
AFAIK, if you specify frame through this variable, it's advised not to change it throughout the course of presentation. If you plan to play around with the frames, don't specify this variable and handle all the changes manually in your animator
I have a screen in my iOS app that has side menu, when I swipe this side menu I want it to cover the status bar ( but I don't want status bar to be hidden completely ), I just what the part that overlaps with side menu, to get under side menu, not front of it, can anyone help me? (I'm using swift 4.2 in my app)
(this side menu is just another ViewController that I animate in and out of my MainViewController)
A possible way to show the side menu over the status bar is to use a UIWindow with wndowLevel = .statusBar that will present the status menu UIViewController. Here is a quick implementation I made:
func presentSideMenu() {
let vc = UIViewController() // side menu controller
vc.view.backgroundColor = .red
window = UIWindow()
window?.frame = CGRect(x: -view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height)
window?.rootViewController = vc
window?.windowLevel = .statusBar
window?.makeKeyAndVisible()
window?.isHidden = false
window?.addSubview(vc.view)
}
Then you can add a pan recognizer to your view and change the frame of the UIWindow accordingly. Again a simple snippet:
func hideSideMenu() {
window?.isHidden = true
window = nil
}
#objc func pan(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .began {
presentSideMenu()
} else if recognizer.state == .changed {
let location = recognizer.location(in: view)
window?.frame = CGRect(x: -view.frame.width + location.x, y: 0, width: view.frame.width, height: view.frame.height)
} else if recognizer.state == .ended {
hideSideMenu()
}
}
Note that you should hold a strong reference to the UIWindow otherwise it will be released immediately. Maybe you should consider if presenting over the status bar is a good idea though. Hope this helps.
I would like to present a UIViewController on top of the current view controller and set it's height to ~80% of the screen size. I've got the first part:
let additionalVC = ChartsViewController(currentSelection)
additionalVC = .overCurrentContext
present(additionalVC, animated: true)
I tried setting the self.view.frame inside my ChartsVC in viewDidLoad and couple of different things but it is always presented in the full screen mode.
That's what I want to achieve:
blueVC - currentVC
redVC - ChartsVC - VC on top of the current VC with ~80% of the original height
btw I'm doing everything programmatically, no xib and UIStoryboard.
There's a number of ways to achieve this.
You could use a 3rd party framework (http://transitiontreasury.com/) or the way I would do this.
Present the newVC where a transition = model over current context
ensure the newVC.views background color is clear
add another view where origin.y is the distance between the top and the desired gap. This is the view where all your objects will sit on.
If you need a coding example let me know, but its a pretty simple solution and looking at your code your 80% there.
Thomas
Implement a custom UIPresentationController. To use a custom view size, you only need to override a single property.
This code will simply inset the presented view controller by 50x100 pts:
class MyPresentationController: UIPresentationController {
// Inset by 50 x 100
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
}
To darken the presenting view controller, override presentationTransitionWillBegin() and dismissalTransitionWillBegin() to insert a shading view and animate it into view:
class MyPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
return self.presentingViewController.view.bounds.insetBy(dx: 50, dy: 100)
}
let shadeView = UIView()
override func presentationTransitionWillBegin() {
self.shadeView.backgroundColor = UIColor.black
self.shadeView.alpha = 0
// Insert the shade view above the presenting view controller
self.shadeView.frame = self.presentingViewController.view.frame
self.containerView?.insertSubview(shadeView,
aboveSubview: self.presentingViewController.view)
// Animate it into view
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.3
}, completion: nil)
}
override func dismissalTransitionWillBegin() {
self.presentingViewController.transitionCoordinator?.animate(alongsideTransition: { (context) in
self.shadeView.alpha = 0.0
}, completion: nil)
}
}
To use your custom presentation controller, set the modalPresentationStyle and transitioningDelegate:
class MyViewController : UIViewController, UIViewControllerTransitioningDelegate {
//
// Your code
//
func presentCharts() {
let additionalVC = ChartsViewController(currentSelection)
additionalVC.modalPresentationStyle = .custom
additionalVC.transitioningDelegate = self
self.present(additionalVC, animated: true)
}
//
// UIViewControllerTransitioningDelegate protocol
//
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
return MyPresentationController(presentedViewController: presented,
presenting: presenting)
}
}
In IOS 13 and Xcode 11, you can present ViewController with modalPresentationStyle = .automatic
Take Two ViewController.First view controller have a button and the button action name is clicked.The target is to clicking the button we want to add secondVC as a child of first view controller and show secondVC 80% of the first view controller.again click button we remove secondVC from first view controller. below is the code for click button action.
#IBAction func clicked(_ sender: UIButton) {
if !isshown{
isshown = true
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
let height = view.frame.height
let width = view.frame.width
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)
}, completion: { (result) in
// do what you want to do
})
}else{
isshown = false
UIView.animate(withDuration: 0.3,
delay: 0,
options: UIViewAnimationOptions.curveEaseIn,
animations: { () -> Void in
var frame = self.vc.view.frame
frame.origin.y = UIScreen.main.bounds.maxY
self.vc.view.frame = frame
}, completion: { (finished) -> Void in
self.vc.view.removeFromSuperview()
self.vc.removeFromParentViewController()
})
}
}
here vc is a reference of secondVC.
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Second") as! secondVC
change below piece of code to get whatever percentage you want.
self.vc.view.frame = CGRect(x: 0, y: 100 , width: width, height: height - 100)
In my app, I want the user to be able to be able to take a picture, be presented with the picture, and by tapping the photo a textfield can be added so that they can write on top of the image. This is exactly the same as the functionality of adding text to pictures in Snapchat.
As far as I can understand, the only way to be presented the image after having taken it and be able to edit it, is to set:
imagePicker.showsCameraControls = false
Make a custom overlay:
#IBAction func takePhoto(sender: UIButton) {
imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .Camera
imagePicker.showsCameraControls = false
imagePicker.allowsEditing = true
let overlayView = UIView(frame: CGRectMake(0, self.view.frame.width, self.view.frame.width, self.view.frame.height-self.view.frame.width))
overlayView.backgroundColor = UIColor.blackColor()
overlayView.alpha = 0.5
println(overlayView)
let snapButton = UIView(frame: CGRectMake(150, 160, 80, 80))
snapButton.layer.cornerRadius = 40
snapButton.userInteractionEnabled = true
snapButton.backgroundColor = UIColor.purpleColor()
overlayView.addSubview(snapButton)
overlayView.bringSubviewToFront(snapButton)
let recognizer = UITapGestureRecognizer(target: self, action:Selector("handleSnapTap:"))
recognizer.delegate = self
snapButton.addGestureRecognizer(recognizer)
let cancelButton = UIView(frame: CGRectMake(40, 40, 44, 44))
cancelButton.layer.cornerRadius = 22
cancelButton.userInteractionEnabled = true
cancelButton.backgroundColor = UIColor.redColor()
overlayView.addSubview(cancelButton)
overlayView.bringSubviewToFront(cancelButton)
let cancelRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleCancelTap:"))
cancelRecognizer.delegate = self
cancelButton.addGestureRecognizer(cancelRecognizer)
let changeCameraButton = UIView(frame: CGRectMake(165, 40, 44, 44))
changeCameraButton.layer.cornerRadius = 22
changeCameraButton.userInteractionEnabled = true
changeCameraButton.backgroundColor = UIColor.blueColor()
overlayView.addSubview(changeCameraButton)
overlayView.bringSubviewToFront(changeCameraButton)
let changeCameraRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleChangeCameraTap:"))
changeCameraRecognizer.delegate = self
changeCameraButton.addGestureRecognizer(changeCameraRecognizer)
let flashButton = UIView(frame: CGRectMake(300, 40, 44, 44))
flashButton.layer.cornerRadius = 22
flashButton.userInteractionEnabled = true
flashButton.backgroundColor = UIColor.yellowColor()
overlayView.addSubview(flashButton)
overlayView.bringSubviewToFront(flashButton)
let flashRecognizer = UITapGestureRecognizer(target: self, action:Selector("handleFlashTap:"))
flashRecognizer.delegate = self
flashButton.addGestureRecognizer(flashRecognizer)
imagePicker.cameraOverlayView = overlayView
presentViewController(imagePicker, animated: true, completion: nil)
}
func handleSnapTap(recognizer: UITapGestureRecognizer) {
println("Take picture")
imagePicker.takePicture()
self.performSegueWithIdentifier("cameraToImageViewSegue", sender: self)
}
func handleCancelTap(recognizer: UITapGestureRecognizer) {
println("Cancel")
self.imagePicker.dismissViewControllerAnimated(true, completion: nil)
}
func handleChangeCameraTap(recognizer: UITapGestureRecognizer) {
if (hasChangedCamera == nil){
imagePicker.cameraDevice = UIImagePickerControllerCameraDevice.Front
hasChangedCamera = true
return
}
if (hasChangedCamera == true){
imagePicker.cameraDevice = UIImagePickerControllerCameraDevice.Rear
hasChangedCamera = false
return
}
if (hasChangedCamera! == false){
imagePicker.cameraDevice = UIImagePickerControllerCameraDevice.Front
hasChangedCamera = true
return
}
}
func handleFlashTap(recognizer: UITapGestureRecognizer) {
if (hasTurnedOnFlash == nil){
imagePicker.cameraFlashMode = UIImagePickerControllerCameraFlashMode.On
hasTurnedOnFlash = true
return
}
if (hasTurnedOnFlash == true){
imagePicker.cameraFlashMode = UIImagePickerControllerCameraFlashMode.Off
hasTurnedOnFlash = false
return
}
if (hasTurnedOnFlash == false){
imagePicker.cameraFlashMode = UIImagePickerControllerCameraFlashMode.On
hasTurnedOnFlash = true
return
}
}
And finally present a new view controller in which the picked image is placed in a UIView, and edit it from there. My issue is how to segue directly from the UIImagePickerController to a new view controller. I have tried the following:
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
self.imagePicker.dismissViewControllerAnimated(false, completion: nil)
let vc = ModifyCameraImageViewController() //change this to your class name
self.presentViewController(vc, animated: false, completion: nil)
}
First off, this just leads to a black screen, but I'm sure there's a simple enough way around that. My main issue is the fact that the view controller from which the UIImagePickerController was presented briefly appears on the screen before the next view controller appears. This obviously does not look good. I also tried removing the dismissViewController function, as well as placing the presentViewController function above the dismissView controller function. Both of these attempts gave me the error message:
Warning: Attempt to present <xxx.ModifyCameraImageViewController: 0x145e3eb70> on <xxx.ViewController: 0x145d20a60> whose view is not in the window hierarchy!
Attempting to use performSegueWithIdentifier with a segue linking the underlying view and the next view controller gives the same error warning.
I have found the following similar question, but I am completely inept at Objective C, so I'm struggling to make any sense of it: Push a viewController from the UIImagePickerController camera view
So, can anyone help in regards to how to present a view controller directly from the UIImagePickerController?
Also, keep in mind that I'm doing this in order to be able to create a text overlay on the newly picked image (like in Snapchat), so if anyone has a more elegant solution to that, feel free to post it!
Thanks!
Ok, found a simple solution to my issue. Instead of presenting the imagePickerController from the underlying view controller when the takePicture button is pressed, and segueing to another view controller directly from there, use the takePicture button to segue to another view controller and present the imagePickerController from the viewDidLoad of the second view controller. The second view controller will then be presented when the imagePickerController is dismissed. This however requires the underlying view controller to look similar to the camera controls, and some playing around with animations for this to look natural.
let pickerController: Void = picker.dismissViewControllerAnimated(true) { _ in
UIImageView.image = info[UIImagePickerControllerOriginalImage] as? UIImage
self.performSegueWithIdentifier("segueIdentifier", sender: nil)
//Do what you want when picker is dismissed
}
Is there a way to simply change the UIPopoverView background color (including its arrow) on iOS8?
(I did read a couple of articles on customizing "UIPopoverControllers". Does this apply here too, meaning the answer is "no"?)
Isn't this something I should be able to address in the prepareForSegue method triggering the popover? How can I reach the according view to change its appearance?
I found the solution. Subclassing is not necessary anymore with iOS8! The background can be accessed and changed like this from within the tableview -> navigation -> popoverPresentationController
self.navigationController?.popoverPresentationController?.backgroundColor = UIColor.redColor()
More information about this in WWDC session 2014.
You can simply modify popover like this:
let popoverViewController = self.storyboard?.instantiateViewControllerWithIdentifier("popoverSegue")
popoverViewController!.popoverPresentationController?.delegate = self
popoverViewController!.modalPresentationStyle = .Popover
let popoverSize = CGSize(width: 150, height: 60)
popoverViewController!.preferredContentSize = popoverSize
let popover = popoverViewController!.popoverPresentationController
popover?.delegate = self
popover?.permittedArrowDirections = .Up
popover?.sourceView = self.view
//change background color with arrow too!
popover?.backgroundColor = UIColor.whiteColor()
popover?.sourceRect = CGRect(x: self.view.frame.width, y: -10, width: 0, height: 0)
presentViewController(popoverViewController!, animated: true, completion: nil)
Seems like that popoverPresentationController.backgroundColor no longer works in iOS13.
Popover arrows now appear to take on the color of the popover viewController's view.backgroundColor.
Here's the whole code for the demo below:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let sourceButton = sender as? UIButton, let popover = segue.destination.popoverPresentationController {
popover.sourceView = sourceButton.superview
popover.sourceRect = sourceButton.frame
popover.permittedArrowDirections = [.left]
popover.delegate = self
segue.destination.preferredContentSize = CGSize(width: 100, height: 100)
//popover.backgroundColor = sourceButton.tintColor //old way
segue.destination.view.backgroundColor = sourceButton.tintColor //new way
}
}
#IBAction func btnTap(_ sender: Any) {
performSegue(withIdentifier: "popoverSegue", sender: sender)
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
SwiftUI : Xcode 11.5
Add the .background modifier with the color and add .edgesIgnoringSafeArea modifier.
.popover(isPresented: self.$vm.presentMenu, content: {
self.menuView
.background(Color.bgGray.edgesIgnoringSafeArea(.all))
})
Just adding that if you are using SwiftUI inside of a UIPopover or if you are using SwiftUI's popover modifier you can set the background color of the popover by just using a Color in the background, like as in a ZStack.
If you want the arrow colored you can add the .edgesIgnoringSafeArea(.all) modifier to the color in the background so it will extend into the arrow.
SwiftUI example:
import SwiftUI
struct PopoverTest: View {
#State var showing: Bool = true
var body: some View {
Button("Show") {
self.showing.toggle()
}
.popover(isPresented: $showing) {
ZStack {
Color.green.edgesIgnoringSafeArea(.all) // will color background and arrow
Text("Popover!")
}
}
}
}
struct PopoverTest_Previews: PreviewProvider {
static var previews: some View {
PopoverTest()
}
}