I'm doing some drawing on a custom UIView canvas, and rather than having a set of buttons at the bottom of the view to allow the user to select shapes, I'd like to have the user do a long press gesture, then have a popup-type menu appear with different shapes they can choose. I don't see anything like this in xCode, though I'd assume there's something like that in iOS. I don't want the alert popup that shows up when you have low battery and notifications.
I've looked into using a UIPopoverController but I'm a bit confused about some of the other Stack Overflow questions I've read about it, and also about the documentation given by Apple.
I described the steps to achieve floating menu as shown in above image:
Create segue from the barButtonItem to the MenuViewCobtroller of type 'Present as Popover'
In the MenuViewController override the preferredContentSize as:
override var preferredContentSize : CGSize
{
get
{
return CGSize(width: 88 , height: 176)
}
set
{
super.preferredContentSize = newValue
}
}
In my case I am returning CGSize with width 100 and size 200. You can set these values so as to fit your floating menu content properly.
4. In the initial/source view controller, in the prepare(for segue: sender) method set self as popoverPresentationController delegate:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowMenuSegue" {
if let tvc = segue.destination as? MenuViewController
{
tvc.delegate = self
if let ppc = tvc.popoverPresentationController
{
ppc.delegate = self
}
}
}
}
The source view controller must comply to UIPopoverPresentationControllerDelegate and implement following method:
extension ViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
}
That's it. You got the floating menu. Hopefully this will be useful.
I used Masture's method above and it worked for me (thank you!), but a couple of notes for other newbies like myself:
Make sure you put "ShowMenuSegue" (or whatever you choose) as the identifier for your segue in the Storyboard, and
I had to add
var delegate: MainViewController!
in the MenuViewController (with MainViewController being your source view controller) in order to get tvc.delegate = self to work
After you make a connection of that button with the viewController and popover as a segue you will need to prepare. Here is the following code in order to prepare for the popover segue.
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
if let identifier = segue.identifier
{
switch identifier
{
case History.SegueIdentifier:
if let tvc = segue.destinationViewController as? TextViewController
{
if let ppc = tvc.popoverPresentationController
{
ppc.delegate = self
}
tvc.text = "\(diagnosticHistory)"
}
default: break
}
}
}
Do keep in mind that if you have an iPhone the popover will take full screen, so you can fix that using this for let's say a text that takes some particular elements.
This will fix the popover to be exactly the size of the elements you have in your text.
#IBOutlet weak var textView: UITextView!
{
didSet
{
textView.text = text
}
}
var text : String = ""
{
didSet
{
textView?.text = text
}
}
override var preferredContentSize : CGSize
{
get
{
if textView != nil && presentingViewController != nil
{
return textView.sizeThatFits(presentingViewController!.view.bounds.size)
}
else
{
return super.preferredContentSize
}
}
set {super.preferredContentSize = newValue}
}
}
I have those 2 in different view controllers but I guess it will work. You will also need to implement UIPopoverPresentationControllerDelegate and
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
to your first viewController.
Related
I'm a beginner trying to learn iOS.
I'm trying to show a sheet, but I'm finding it's not really showing anything.
This is what it looks like:
This is my relevant code:
class ViewController: UIViewController {
#IBAction func openMemoryLog(_ sender: UIButton) {
let memoryLogViewController = MemoryLogViewController()
present(memoryLogViewController, animated: true)
}
}
class MemoryLogViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("Memory log loaded")
if let presentationController = presentationController as? UISheetPresentationController {
presentationController.detents = [ .medium() ]
}
}
}
I can see the print statement going through, so it's at least loading. But don't know what's going on here. Looks like some sort of modal thing is happening, but the IB interface isn't showing.
When you use Storyboard / IB to design an interface, you have to instantiate that controller from the Storyboard - you cannot simply assign the class to a variable.
#IBAction func openMemoryLog(_ sender: UIButton) {
// if you designed MemoryLogViewController in Storyboard,
// you cannot create it like this:
//let memoryLogViewController = MemoryLogViewController() <-- wrong
// you need to instantiate it from the Storyboard
// for example, if you gave your controller a Storyboard ID of "memoryLogViewController":
if let memoryLogViewController = storyboard?.instantiateViewController(withIdentifier: "memoryLogViewController") {
present(memoryLogViewController, animated: true)
}
}
I am working on the online Stanford IOS development course and am having a bug I don't know how to deal with. When I segue to the detail view in the split view controller I am able to set parameters of the detail UIView from the detail UIViewController in the didSet of outlet to the view. Later when I try to access or mutate the detail view from the controller the view variable(outlet) is nil even though the view is still on screen and responds to gestures.
This is the prepare method of the master view in the split view controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationViewController = segue.destination
if let graphViewController = destinationViewController as? GraphViewController {
if let identifier = segue.identifier{
switch identifier {
case "DisplayGraph":
graphViewController.function = funcForGraphView(_:)
default:
break
}
}
}
}
This is the detail view controller. I commented on the interesting parts.
class GraphViewController: UIViewController {
#IBOutlet weak var graphView: GraphView!{
didSet{
graphView.addGestureRecognizer(UITapGestureRecognizer(target: graphView, action: #selector(graphView.tap(recognizer:))))
graphView.addGestureRecognizer(UIPanGestureRecognizer(target: graphView, action: #selector(graphView.pan(recognizer:))))
graphView.addGestureRecognizer(UIPinchGestureRecognizer(target: graphView, action: #selector(graphView.zoom(recognizer:))))
graphView.function = cos //I can set variables in the view here
// After this didSet I am no longer able to set or read any variables from the graphView
}
}
var function: ((Double)->(Double))? {
didSet{
if let newfunction = function {
graphView.function = newfunction // I can not set or access variables in the view here since for some reason graphView is nil
}
}
}
And this is the relevant variable in the detail view
var function: ((Double) -> Double)? {
didSet{
setNeedsDisplay()
}
}
Storyboard Picture
Any help would be greatly appreciated and hopefully this isn't a stupid question. If more information would be helpful let me know.
Thanks!
It will be nil because of your view not fully loaded while your are push viewcontrolle. so you needs to pass as below :
In GraphViewController
var yourValue : ((Double)->(Double))?
override func viewDidLoad() {
super.viewDidLoad()
self.function = yourValue
}
Set value while navigation
if let identifier = segue.identifier{
switch identifier {
case "DisplayGraph":
graphViewController.yourValue = funcForGraphView(_:)
default:
break
}
}
I wanted to segue to a popover in iOS 10, this piece of code used to work fine on iPhone but not now (it shows full screen), what have I done wrong? The segue is set to "Present As Popover".
override func prepare(for segue:UIStoryboardSegue, sender:AnyObject!) {
if segue.identifier == "about" {
let aboutController = segue.destination as! AboutController
aboutController.preferredContentSize = CGSize(width:300, height:440)
let popoverController = aboutController.popoverPresentationController
if popoverController != nil {
popoverController!.delegate = self
popoverController!.backgroundColor = UIColor.black
}
}
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
Many function have been renamed in Swift 3, including adaptivePresentationStyleForPresentationController - this is now adaptivePresentationStyle(for:)
Change your code to
func adaptivePresentationStyle(for controller:UIPresentationController) -> UIModalPresentationStyle {
return .none
}
Since your function name didn't match it wasn't being called and because it is an optional function in the protocol, you didn't get a warning.
Popover is an iPad only feature.
UIKit is smart enough to figure out it should present it modally on iPhone/iPod.
In my project I have a button on the bottom right side of the screen and i added another uiviewcontroller to the storyboard, did control-drag to the uiviewcontroller I wanted as the popover, then set that viewcontroller size to (300, 300) and checked 'use preferred explicit size'. When I load the app and click the button, the entire screen gets covered by the "popover". I also tried to go into the popoverViewController's .m file and set the size but that didn't work either.
Any ideas?
Edit: Since it looks like I have to have it be full screen that is fine however I am still running into some other problems I was having earlier. My popup screen will come up and I make the background black and alpha as .5 to make it see through however it'll do the animation, then once the animation is finished the screen will go from .5 opacity to completely black and the only thing I can see is the battery icon thing.
The OP uses Objective-C. This answer presents code in swift. Converting swift to Objective-C should be easy.
In the newly added ViewController, under “Simulated Metrics” change “Size” to “Freeform” and “Status Bar” to “None.”
Under “Simulated Size” change your view’s height and width to the actual size you want your popover’s content to be.
Create a segue to the newly added VC. Use segue type as "Present As Popover" and give a name for the segue, for example "popoverSegue".
In the ViewConroller from which this segue is to be triggered, add the UIPopoverPresentationControllerDelegate protocol.
class ViewController: UIViewController, UIPopoverPresentationControllerDelegate {
}
Override the prepareForSegue function to catch your popover segue. Set the modalPresentationStyle to .Popover to explicitly state that you want a popover and then assign the delegate property of the view’s popoverPresentationController to self:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "popoverSegue" {
let popoverViewController = segue.destinationViewController as! UIViewController
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
popoverViewController.popoverPresentationController!.delegate = self
}
}
Implement the adaptivePresentationStyleForPresentationController function to tell your app that you really want that popover presentation and will accept no substitutions:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
Following these, I could get a popup on iPhone which is not full screen but the size set for the ViewController.
Source: iPad Style Popovers on the iPhone with Swift
Thanks to Bharat for the great answer, I personally use a UIStoryboardSegue that does pretty much the same thing. That way, I can change the class of the segue in the storyboard, have what I want, and not pollute my controllers:
class AlwaysPopupSegue : UIStoryboardSegue, UIPopoverPresentationControllerDelegate
{
override init(identifier: String?, source: UIViewController, destination: UIViewController)
{
super.init(identifier: identifier, source: source, destination: destination)
destination.modalPresentationStyle = UIModalPresentationStyle.popover
destination.popoverPresentationController!.delegate = self
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
}
Swift 3-5 version
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SEGUE_IDENTIFIER" {
let popoverViewController = segue.destination as! YourViewController
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.popover
popoverViewController.popoverPresentationController!.delegate = self
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
Swift 4 Version
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SegueIdentifier" {
let popoverViewController = segue.destination
popoverViewController.modalPresentationStyle = .popover
popoverViewController.presentationController?.delegate = self
}
}
Don't forget to add
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
On iPhone you can creat a custom view controller that can manage all the popovers. Since each view controller has its own navigation controller, you can add a new view controller to the app.window.rootviewcontroller as a du view and bring all to front.
If you didn't want to write your own, you can use something like this for instance: http://cocoapods.org/pods/FPPopover
This is Swift 5 code, some/most of the above mentioned solutions are all valid. This is an effort to present whole solution. This example supposes you are using a xib for popover view controller but this would work otherwise as well, say, in prepare for segue. Here's a complete code:
Presenting ViewController:
let popoverVC = PopoverVC(nibName: "popoverVC", bundle: nil)
popoverVC.completionHandler = { [unowned self] (itemIndex : Int?) in
if let itemIndex = itemIndex
{
// Do completion handling
}
}
popoverVC.preferredContentSize = CGSize(width: 200, height: 60)
popoverVC.modalPresentationStyle = .popover
if let pvc = popoverVC.popoverPresentationController {
pvc.permittedArrowDirections = [.down]
pvc.delegate = self
pvc.sourceRect = button.frame
pvc.sourceView = button // Button popover is presented from
present(popoverVC, animated: true, completion: nil)
}
This is important:
extension ViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
I am re-writing a tutorial converting the code from Objective-C to swift. The app moves from VC one where there is 3 sliders (Red, Green and Blue) that set the background colour, a label of the colour name and a button that links to the second VC. In the second VC the colour from the first VC is used as the background and the user has a chance to name the colour.
When the user enters the colour name it should return the new colour name to the orginal VC and the label that shows the colour name should show the text entered.
The following is the code that is causing issue:
func textFieldShouldReturn(nameEntry: UITextField) -> Bool
{
ViewController().colourLabel.text = nameEntry.text
nameEntry.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
return true
}
The error "fatal error: unexpectedly found nil while unwrapping an Optional value" is generated. However debugging nameEntry.text has a string in it.
I'm a little stumped. I could try and do a prepare for unwind segue but it is meant to be a tutorial app.
Cheers
ViewController() actually creates a new instance of your ViewController. This is not a reference to the already existing ViewController. What you can do is create a weak variable pointing to first ViewController inside the second ViewController and set it at prepareForSegue or when the second View controller is shown.
class SecondViewController : UIViewController {
weak var firstViewController : ViewController?
// Other code
func textFieldShouldReturn(nameEntry: UITextField) -> Bool
{
firstViewController?.colourLabel.text = nameEntry.text
nameEntry.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
return true
}
}
Inside First View Controller prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SecondViewController" {
let secondViewController = segue.destinationViewController as SecondViewController
secondViewController.firstViewController = self
}
}
It's possible that the view controller returned by ViewController() has not yet loaded its views. You could try checking this in a setter function and storing it for later use once the views have been loaded.
class VC : UIViewController {
#IBOutlet weak var colourLabel: UILabel!
var savedLabelText: String?
override func viewDidLoad() {
super.viewDidLoad()
self.colourLabel.text = self.savedLabelText
}
func setColorLabelText(label: String) {
if self.isViewLoaded() {
self.colourLabel.text = label
}
else {
self.savedLabelText = label
}
}
}