My View Contollers are like MainView -> NavigationController A -> ViewController A -> ViewController B -> ViewController C, I want to dismiss ViewControllers (A,B and C) and present MainView again... I used this line
self.view.window?.rootViewController?.dismiss(animated: false, completion: nil)
and it works just fine.. the problem is while dismissing viewControllers (A,B and C) it calls every viewDidAppear and this runs code that I don't want to be run.. what's the best practice to dismiss and go back to MainView without calling each viewController
You must have some other code that is causing the issue.
Here is a complete example...
PresDisViewController is the starting view controller
its button creates a nav controller with ViewControllerA as its root
ViewControllerA has a button that pushes ViewControllerB
ViewControllerB has a button that pushes ViewControllerC
ViewControllerC has a button that executes your self.view.window?.rootViewController?.dismiss(animated: false, completion: nil)
Each VC implements viewDidAppear() and prints itself to the debug console.
Output when I run it:
viewDidAppear scratchy.PresDisViewController
viewDidAppear scratchy.ViewControllerA
viewDidAppear scratchy.ViewControllerB
viewDidAppear scratchy.ViewControllerC
viewDidAppear scratchy.PresDisViewController
As you see, viewDidAppear is NOT called in the VCs when the nav controller is dismissed.
import UIKit
class ViewControllerA: UIViewController {
let btn: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Do Push", for: .normal)
v.backgroundColor = .red
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(btn)
NSLayoutConstraint.activate([
btn.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -40.0),
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any?) -> Void {
let vc = ViewControllerB()
self.navigationController?.pushViewController(vc, animated: true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear", NSStringFromClass(self.classForCoder))
}
}
class ViewControllerB: UIViewController {
let btn: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Do Push", for: .normal)
v.backgroundColor = .systemYellow
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(btn)
NSLayoutConstraint.activate([
btn.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0),
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any?) -> Void {
let vc = ViewControllerC()
self.navigationController?.pushViewController(vc, animated: true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear", NSStringFromClass(self.classForCoder))
}
}
class ViewControllerC: UIViewController {
let btn: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Do Dismiss", for: .normal)
v.backgroundColor = .blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(btn)
NSLayoutConstraint.activate([
btn.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 40.0),
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any?) -> Void {
self.view.window?.rootViewController?.dismiss(animated: false, completion: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear", NSStringFromClass(self.classForCoder))
}
}
class PresDisViewController: UIViewController {
let btn: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Do Present", for: .normal)
v.backgroundColor = .orange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(btn)
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: view.topAnchor, constant: 60.0),
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any?) -> Void {
let vcA = ViewControllerA()
let navVC = UINavigationController(rootViewController: vcA)
navVC.modalPresentationStyle = .fullScreen
present(navVC, animated: true, completion: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear", NSStringFromClass(self.classForCoder))
}
}
Here is how it is done via Storyboard. I don't know how to do it via code. I hope it helps
In your MainViewController add this:
#IBAction func unwindToMainView(segue:UIStoryboardSegue) { }
In all otherViewController (viewController A,ViewController B,ViewController C),
Click and drag like this
Then chose the segue you added in the mainVC
Now select that segue and give it an identifier
From here you can perform segue with that identifier wherever you want. I use button to perform that segue
Related
I have a parent view controller and a child view controller that takes up a portion of the parent's main view:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let childVC = ChildVC()
addChild(childVC)
childVC.view.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 200, height: 200))
view.addSubview(childVC.view)
childVC.didMove(toParent: self)
}
}
class ChildVC: UIViewController {
override func loadView() {
let v = UIView()
v.backgroundColor = .cyan
view = v
}
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .system)
button.setTitle("Present", for: .normal)
button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
button.sizeToFit()
view.addSubview(button)
}
#objc func pressed() {
self.definesPresentationContext = true
self.providesPresentationContextTransitionStyle = true
self.modalTransitionStyle = .crossDissolve
let pvc = PresentedVC()
pvc.modalPresentationStyle = .currentContext
self.present(pvc, animated: true, completion: nil)
}
}
class PresentedVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
let button = UIButton(type: .system)
button.setTitle("Dismiss", for: .normal)
button.addTarget(self, action: #selector(pressed), for: .touchUpInside)
button.sizeToFit()
view.addSubview(button)
}
#objc func pressed() {
self.dismiss(animated: true, completion: nil)
}
}
When I present a view controller using the currentContext style, it presents the new view controller as it should, covering only the child view controller's view:
However, when I dismiss it, the size of the ChildVC's main view takes up the entire screen:
When I log the superview of the ChildVC's main view, it's still a subview of the parent view controller's main view so I'm not sure why this is happening.
Just change your presentationStyle to .overCurrentContext like this:
#objc func pressed() {
self.definesPresentationContext = true
self.providesPresentationContextTransitionStyle = true
self.modalTransitionStyle = .crossDissolve
let pvc = PresentedVC()
pvc.modalPresentationStyle = .overCurrentContext // <-- here
pvc.modalTransitionStyle = .crossDissolve // <-- and here
self.present(pvc, animated: true, completion: nil)
}
It will prevent the unwanted upscaling.
So I'm trying to show an UIView in a stack view when I click continue on a controller and pop back to the previous controller. However, I'm having trouble showing the view when I pop. It will show if I present the controller, but I need it to show when I pop. How should I go about this? Thank you.
// ServiceDetailController
// MARK: - Properties
lazy var dateContainer: ShadowCardView = {
let view = ShadowCardView()
view.backgroundColor = .white
view.addShadow()
view.setHeight(height: 40)
let stack = UIStackView(arrangedSubviews: [calendarIcon, dateLabel, timeOfDayLabel])
stack.axis = .horizontal
stack.distribution = .fillProportionally
stack.spacing = 8
view.addSubview(stack)
stack.centerY(inView: view)
stack.anchor(left: view.leftAnchor, right: view.rightAnchor, paddingLeft: 12, paddingRight: 70)
view.addSubview(closeButton)
closeButton.centerY(inView: view)
closeButton.anchor(right: view.rightAnchor, paddingRight: 12)
return view
}()
lazy var dateStack = UIStackView(arrangedSubviews: [dateLabelStack, dateContainer, dateContainerView])
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
// MARK: - Selectors
#objc func handleDateCreationTapped() {
let controller = DateCreationController()
controller.jobService = self.jobService
navigationController?.pushViewController(controller, animated: true)
}
// MARK: - Helper Functions
fileprivate func configureUI() {
setupNavigationBar()
view.backgroundColor = .groupTableViewBackground
setupServiceInfoView()
setupFormView()
}
fileprivate func setupFormView() {
showDataContainer(shouldShow: false)
setupTapGestureRecognizers()
}
fileprivate func setupTapGestureRecognizers() {
let dateTap = UITapGestureRecognizer(target: self, action: #selector(handleDateCreationTapped))
dateTap.numberOfTapsRequired = 1
dateTextField.addGestureRecognizer(dateTap)
}
func showDateContainer(shouldShow: Bool) {
if shouldShow {
dateContainer.isHidden = false
dateStack.spacing = 5
} else {
dateContainer.isHidden = true
dateStack.spacing = -18
}
}
// DateCreationController
// MARK: - Properties
private let continueButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Continue", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont(name: "AvenirNext-Medium", size: 14)
button.backgroundColor = .darkGray
button.setHeight(height: 50)
button.layer.cornerRadius = 8
button.addTarget(self, action: #selector(handleContinue), for: .touchUpInside)
return button
}()
// MARK: - Selectors
#objc func handleContinue() {
let controller = ServiceDetailController()
controller.showDateContainer(shouldShow: true)
navigationController?.popViewController(animated: true)
}
The main problem is this func in your DateCreationController:
#objc func handleContinue() {
// here, you are creating a NEW instance of ServiceDetailController
let controller = ServiceDetailController()
controller.showDateContainer(shouldShow: true)
// here, you are popping back to where you came from... the EXISTING instance of ServiceDetailController
navigationController?.popViewController(animated: true)
}
You need to use either delegate/protocol pattern or a closure.
Here's an example using a closure...
Add this var to your DateCreationController class:
var continueCallback: ((Bool)->())?
In your ServiceDetailController class, when you instantiate and push your DateCreationController, you'll also setup that closure:
#objc func handleDateCreationTapped() {
let controller = DateCreationController()
controller.jobService = self.jobService
// add the closure
controller.continueCallback = { [weak self] shouldShow in
guard let self = self else { return }
self.showDateContainer(shouldShow: shouldShow)
self.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
Then, in your button action in DateCreationController, you "call back" using that closure:
#objc func handleContinue() {
continueCallback?(true)
}
Here's a runnable example. It creates a simple yellow view as the dateContainer view, but it is hidden on load. Tapping anywhere will push to DateCreationController, which has a single "Continue" button. Tapping that button will execute the closure, where the dateContainer view will be set to visible and we'll pop back:
class ServiceDetailController: UIViewController {
var jobService: String = "abc"
// plain yellow view
let dateContainer: UIView = {
let view = UIView()
view.backgroundColor = .yellow
return view
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
let infoLabel = UILabel()
infoLabel.text = "Tap anywhere"
view.addSubview(infoLabel)
view.addSubview(dateContainer)
infoLabel.translatesAutoresizingMaskIntoConstraints = false
dateContainer.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
infoLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
dateContainer.topAnchor.constraint(equalTo: infoLabel.bottomAnchor, constant: 20.0),
dateContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dateContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dateContainer.heightAnchor.constraint(equalToConstant: 100.0),
])
setupTapGestureRecognizers()
// hide dateContainer on load
showDateContainer(shouldShow: false)
}
// MARK: - Selectors
#objc func handleDateCreationTapped() {
let controller = DateCreationController()
controller.jobService = self.jobService
// add the closure
controller.continueCallback = { [weak self] shouldShow in
guard let self = self else { return }
self.showDateContainer(shouldShow: shouldShow)
self.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
// MARK: - Helper Functions
fileprivate func setupTapGestureRecognizers() {
let dateTap = UITapGestureRecognizer(target: self, action: #selector(handleDateCreationTapped))
dateTap.numberOfTapsRequired = 1
view.addGestureRecognizer(dateTap)
}
func showDateContainer(shouldShow: Bool) {
if shouldShow {
dateContainer.isHidden = false
} else {
dateContainer.isHidden = true
}
}
}
class DateCreationController: UIViewController {
var jobService: String = "abc"
var continueCallback: ((Bool)->())?
lazy var continueButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Continue", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont(name: "AvenirNext-Medium", size: 14)
button.backgroundColor = .darkGray
button.layer.cornerRadius = 8
button.addTarget(self, action: #selector(handleContinue), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
view.addSubview(continueButton)
continueButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
continueButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
continueButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
continueButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.75),
continueButton.heightAnchor.constraint(equalToConstant: 50.0),
])
}
// MARK: - Selectors
#objc func handleContinue() {
continueCallback?(true)
}
}
I am trying to change the value of one ViewController by clicking a button inside a second ViewController with delegation.
But so far it only prints a message, but doesn't change the value.
I have this class where I defined a view:
class CounterView: UIView {
public var creditPointValue = Int()
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .cyan
self.addSubview(label)
label.text = "Credit Points: \(creditPointValue)"
label.translatesAutoresizingMaskIntoConstraints = false;
label.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
func changeCreditPointValue(value:Int){
creditPointValue = creditPointValue + value
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I am using that view inside this ViewController and I want to manipulate the variable "creditPointValue":
protocol AddCreditsDelegate {
func addCreditsToCounter()
}
class ViewController: UIViewController {
var delegate: AddCreditsDelegate?
var counterView = CounterView()
let label = UILabel()
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .white
view.addSubview(button)
button.backgroundColor = .red
button.setTitle("View2", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(changeView), for: .touchUpInside)
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
view.addSubview(counterView)
counterView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
counterView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
counterView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
//counterView.frame.size.height = 30
counterView.bottomAnchor.constraint(equalTo: counterView.topAnchor, constant: 50).isActive = true
}
#objc func changeView(){
delegate?.addCreditsToCounter()
navigationController?.pushViewController(ViewController2(), animated: true)
}
}
And inside this second ViewController I am trying to change the value by clicking the button I added to the view:
class ViewController2: UIViewController {
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(button)
button.backgroundColor = .red
button.setTitle("add Credits", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(addCreditsButton), for: .touchUpInside)
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
#objc func addCreditsButton(){
addCreditsToCounter()
}
}
extension ViewController2 : AddCreditsDelegate{
func addCreditsToCounter() {
let vc = ViewController()
vc.delegate = self
vc.counterView.creditPointValue += 5
print("pressed")
}
}
So far only the message "pressed" gets printed each time I click the button and I don't even know if I am going into the right direction with my approach trying to use delegation.
You should not create a new ViewController instance each time you call ViewController2.addCreditsToCounter, you already have a ViewController that creates a ViewController2 instance in ViewController.changeView. Just store a weak reference to ViewController in ViewController2 using delegate. In such case ViewController (not ViewController2) should conform to AddCreditsDelegate.
First of all, replace
protocol AddCreditsDelegate {
func addCreditsToCounter()
}
with
protocol AddCreditsDelegate: AnyObject {
func addCreditsToCounter()
}
Then add weak var delegate: AddCreditsDelegate? to ViewController2 and remove ViewController.delegate. Remove ViewController2.addCreditsToCounter, ViewController2 should not conform to AddCreditsDelegate. It means that in ViewController2.addCreditsButton you should call delegate?.addCreditsToCounter(), not addCreditsToCounter().
ViewController should conform to AddCreditsDelegate:
extension ViewController: AddCreditsDelegate {
func addCreditsToCounter() {
counterView.creditPointValue += 5
print("pressed")
}
}
And don't forget to initialize ViewController2.delegate. Replace your ViewController.changeView implementation with
let controller = ViewController2()
controller.delegate = self
navigationController?.pushViewController(controller, animated: true)
You create a new instance here
let vc = ViewController()
instead you need
#objc func changeView() {
let sec = ViewController2(),
sec.delegate = self
navigationController?.pushViewController( animated: true)
}
Then declare this inside the SecondVC
weak var delegate:ViewController?
Finally
func addCreditsToCounter() {
delegate?.counterView.creditPointValue += 5
print("pressed")
}
Some RootViewController presents a ParentViewController which than presents a ChildViewController.
How can I dissmiss the ChildViewController animated directly to the RootViewController whithout showing the ParentViewController again?
In Detail
Assume some presents the ParentViewController which lets the user enter some credentials to login to some user account.
Once the connection is established the ParentViewController presents the ChildViewController which shows connection / account details to the user
When the user closes the ChildViewController it should be dismissed animated (slide down, etc.). But instead of returning to the ParentViewController the user should get back directly to the RootViewController
Of course it would be possible, that the ParentViewController does not present the ChildViewController itself but (somehow) tells the RootViewController to this. This way it would be no problem to directly return from the ChildViewController to the RootViewController. However, this is NOT what I am looking for, since the RootViewController should not know about the ChildViewController or even care if the ParentViewController presents other VCs.
I am looking for a solution where the ParentViewController controls whether itself is shown after the VC it presented is dismissed or its parent (= the root VC).
Code:
typealias CompletionBlock = () -> Void
class RootViewController: UIViewController {
#IBAction func showParentVC(_ sender: Any) {
let parentVC = ParentViewController()
parentVC.completion = {
self.dismiss(animated: true, completion: nil)
}
present(parentVC, animated: true)
}
}
class ParentViewController: UIViewController {
var completion: CompletionBlock?
#IBAction func showChild(_ sender: Any) {
let childVC = ChildViewController()
childVC.completion = {
self.completion?()
}
present(childVC, animated: true)
}
}
class ChildViewController: UIViewController {
var completion: CompletionBlock?
#IBAction func close(_ sender: Any) {
completion?()
}
}
Using this code does NOT solve the described problem. If close is called on the ChildViewController the RootViewController calls self.dismiss(animated: true, completion: nil). This way the ChildViewController animates away and the ParentViewController becomes visible. Then the ParentViewController animates away and the RootViewControllerbecomes visible.
How to skip the ParentViewController and directly show the RootViewController after animating away the ChildViewController?
My recommendation is embed your RootViewController into a NavigationController (if you don't have it yet) and present both parent a child with
navigationController?.present(viewController, animated: true, completion: nil)
//instead of viewController.present(...)
And then youcan use this method from your childViewController
navigationController?.popToRootViewController(animated: true)
One approach would be to set the view's alpha to Zero when you present another VC onto the "stack" of presented VCs.
So, present the first modal VC from the "root" VC as normal. For each "child" that presents another VC, use:
present(vc, animated: true, completion: {
self.view.alpha = 0.0
})
Now, when you call-back to the "root" VC to dismiss all the VCs, you won't see the partial / flash of the intermediate VC / VCs.
Here is a complete example to test. No #IBOutlets or #IBActions ... just start with a black view controller and assign its Custom Class to MultiPresentDismissViewController:
import UIKit
class MultiPresentDismissViewController: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.font = UIFont.boldSystemFont(ofSize: 40)
v.backgroundColor = .yellow
v.text = "\"Root\" VC"
return v
}()
let showAnotherButton: UIButton = {
let v = UIButton()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.setTitle("Present a VC", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.cyan, for: .highlighted)
return v
}()
let theStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 32
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
[theLabel, showAnotherButton].forEach {
theStackView.addArrangedSubview($0)
$0.layer.borderWidth = 1.0
}
theStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(theStackView)
NSLayoutConstraint.activate([
theStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
])
showAnotherButton.addTarget(self, action: #selector(presentAnotherVC), for: .touchUpInside)
}
#objc func presentAnotherVC() -> Void {
let vc = AnotherViewController()
vc.myID = 1
present(vc, animated: true, completion: nil)
}
}
class AnotherViewController: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
v.font = UIFont.boldSystemFont(ofSize: 100)
v.backgroundColor = .yellow
return v
}()
let showAnotherButton: UIButton = {
let v = UIButton()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.setTitle("Present Another VC", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.cyan, for: .highlighted)
return v
}()
let defaultDismissButton: UIButton = {
let v = UIButton()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.setTitle("Default Dismiss All", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.cyan, for: .highlighted)
return v
}()
let theStackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.alignment = .fill
v.distribution = .fill
v.spacing = 20
return v
}()
var myID: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .random()
[theLabel, showAnotherButton, defaultDismissButton].forEach {
theStackView.addArrangedSubview($0)
$0.layer.borderWidth = 1.0
}
theStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(theStackView)
NSLayoutConstraint.activate([
theStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
theStackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
])
theLabel.text = "\(myID)"
showAnotherButton.addTarget(self, action: #selector(presentAnotherVC), for: .touchUpInside)
defaultDismissButton.addTarget(self, action: #selector(defaultDismissAll), for: .touchUpInside)
}
#objc func presentAnotherVC() -> Void {
let vc = AnotherViewController()
vc.myID = myID + 1
present(vc, animated: true, completion: {
self.view.alpha = 0.0
})
}
#objc func defaultDismissAll() -> Void {
// walk up the "presenting" hierarchy to find the "root" VC
var vc = self.presentingViewController
while vc?.presentingViewController != nil {
vc = vc?.presentingViewController
}
vc?.dismiss(animated: true, completion: nil)
}
}
extension CGFloat {
static func random() -> CGFloat {
return CGFloat(arc4random()) / CGFloat(UInt32.max)
}
}
extension UIColor {
static func random() -> UIColor {
return UIColor(red: .random(),
green: .random(),
blue: .random(),
alpha: 1.0)
}
}
It appears - at least by default, that while you can interact with the contents of a popover on a plus iPhone and dismiss it by tapping on the background, on a regular, non plus phone, the behavior is the opposite.
Does anyone know how to correct and/or configure this so that you can interact with a popover on an regular iPhone?
I have a sample that demonstrates the problem here:
https://github.com/chrisco314/iPhone-Popover-Test,
but the relevant code is:
class PresentingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
func configure() {
view.addSubview(button)
view.backgroundColor = .white
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.topAnchor.constraint(equalTo: view.topAnchor, constant: 50).isActive = true
}
lazy var button: UIButton = {
let button = UIButton()
button.layer.cornerRadius = 10
button.contentEdgeInsets = .init(top: 8, left: 8, bottom: 8, right: 8)
button.backgroundColor = .blue
button.setTitle("Show popover", for: .normal)
button.addTarget(self, action: #selector(didTap(sender:)), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#objc func didTap(sender: UIButton) {
let presented = PresentedViewController()
presented.modalPresentationStyle = .popover
let popover = presented.popoverPresentationController!
popover.delegate = self
popover.sourceRect = sender.bounds
popover.sourceView = sender
popover.permittedArrowDirections = .up
popover.backgroundColor = popover.presentedViewController.view.backgroundColor
self.present(presented, animated: true, completion: {})
}
}
extension PresentingViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
class PresentedViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
func configure() {
view.backgroundColor = .green
view.addSubview(text)
text.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
view.rightAnchor.constraint(equalTo: text.rightAnchor, constant: 20).isActive = true
text.topAnchor.constraint(equalTo: view.topAnchor, constant: 50).isActive = true
view.bottomAnchor.constraint(greaterThanOrEqualTo: text.bottomAnchor, constant: 50).isActive = true
}
lazy var text: UITextField = {
let view = UITextField()
view.text = "Placeholder"
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}()
}
Thanks!
I am thinking now that this might have been a simulator problem. I had seen this on my main project, a separate sample, and then wrote a new cleaner sample for public consumption. I then switched back and forth between different versions of the simulator and it started working again.
Weird.