I know that this question has been asked a lot here and I've read all of them, but my problem is still persistent. I wanted to include the delegate pattern in my project in order to call a function in another class from one class. But it didn't work there either. So I created a completely new project to practice the pattern again. Only for 3 hours I just can't do it. My delegate still stays in my main ViewController nil.
This is my Code of my ViewController. It is the initialised ViewController and also the master of the delegate pattern, and the buttonViewController is the servant:
import UIKit
protocol ViewControllerDelegate {
func printTest(message: String)
}
class ViewController: UIViewController {
var delegate: ViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func perform(_ sender: UIButton) {
let vc = storyboard?.instantiateViewController(withIdentifier: "vc") as! ButtonViewController
if delegate == nil {
print("Ist nil")
}
delegate?.printTest(message: "Test")
present(vc, animated: true, completion: nil)
}
}
I would like to call a function in my buttonViewController if the user presses the perform button and than i want to perform a segue to my buttonViewController.
This is my ButtonViewController:
import UIKit
class ButtonViewController: UIViewController, ViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
ViewController().delegate = self
print("test")
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
#IBAction func performsegue(_ sender: UIButton) {
}
func printTest(message: String) {
print(message + "\(self)")
}
}
I've already tried to perform a segue that first viewDidLoad is called by my ButtonViewController, then to go back to the ViewController and then call the perform method again via a button to then see whether delegate is still nil, and yes it was unfortunately still nil.
Can somebody help me with this Problem?
Best regards!
The delegate pattern is confusing at first for a lot of people. I find that most of my students tend to try and do it backward at first, which is I think what is going on here as well. Usually in the delegate pattern there is one view controller (A) that presents another (B). Typically some action on B should trigger some function on A (in your case, perhaps a button press on B triggers causes A to print something).
In this scenario you would have two subclasses of UIViewController: AViewController and BViewController and a protocol BViewControllerDelegate. It would be setup as follows:
The protocol BViewControllerDelegate would have the function you want to be called on AViewController when the button in BViewController is pressed.
AViewController would conform to this protocol and implement this function.
In BViewController you would have your delegate property defined: weak var delegate: BViewControllerDelegate?.
This property would be set on an instance of BViewController by an instance of AViewController to itself (the instance of AViewController) during the presentation of BViewController.
The instance of BViewController would invoke the function on its delegate property in response to a button press.
class AViewController: UIViewController, BViewControllerDelegate {
// This is 4, this segue action is invoked by a storyboard segue in the storyboard and is responsible for setting up the destination view controller and configuring it as needed (i.e., setting its delegate property)
#IBSegueAction func makeBViewController(_ coder: NSCoder) -> BViewController {
let bViewController = BViewController(coder: coder)!
bViewController.delegate = self
return bViewController
}
// Here we accomplish 2 (and also above where we declare conformance to the protocol)
func bViewControllerDidPerformAction(viewController: BViewController, message: String) {
print(message)
}
}
protocol BViewControllerDelegate: AnyObject {
// Here we accomplish 1
func bViewControllerDidPerformAction(viewController: BViewController, message: String)
}
class BViewController: UIViewController {
// Here is 5
#IBAction func buttonPressed(_ sender: Any) {
delegate?.bViewControllerDidPerformAction(viewController: self, message: "Test Message")
}
// This is 3
weak var delegate: BViewControllerDelegate?
}
I'm not 100% clear on what you're trying to do in your code, but I believe your ViewController should be setup like AViewController and your ButtonViewController should be setup like BViewController. Also, the line of code ViewController().delegate = self does nothing, because it creates a new instance of ViewController and sets its delegate, but this new instance is immediately deallocated because it is not actually the one being used anywhere else.
Protocol / Delegate pattern is used to allow an instantiated controller (or other object) to communicate back to the class that created it.
So, in your scenario, you want ViewController to conform to ViewControllerDelegate ... when you instantiate ButtonViewController (which has a ViewControllerDelegate delegate var), you assign that ButtonViewController's delegate to self (ViewController).
Now, ButtonViewController can call protocol functions via its delegate.
protocol ViewControllerDelegate {
func printTest(message: String)
}
// make ViewController conform to ViewControllerDelegate
class ViewController: UIViewController, ViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func perform(_ sender: UIButton) {
let vc = storyboard?.instantiateViewController(withIdentifier: "vc") as! ButtonViewController
// set self as the delegate in ButtonViewController
vc.delegate = self
present(vc, animated: true, completion: nil)
}
func printTest(message: String) {
print("printTest in delegate:", message)
}
}
class ButtonViewController: UIViewController {
var delegate: ViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
print("test")
}
#IBAction func buttonTap(_ sender: UIButton) {
// call the delegate
delegate?.printTest(message: "Sending to delegate!")
}
}
Edit -- here is a complete example to run. "Main" view controller has a button and a label. Tap the button to present the ButtonVC. Button view controller has a text field and a "Save and Close" button. Enter some text in the text field, tap the Save button, and the text will be sent to the Main controller via the ViewControllerDelegate.
No #IBOutlet or #IBAction connections needed -- just assign a blank view controller's custom class to ViewController:
// make ViewController conform to ViewControllerDelegate
class ViewController: UIViewController, ViewControllerDelegate {
let btn: UIButton = {
let v = UIButton()
v.setTitle("Show Button VC", for: [])
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
return v
}()
let label: UILabel = {
let v = UILabel()
v.backgroundColor = .green
v.numberOfLines = 0
v.text = "From ButtonVC:\n"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
[label, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
btn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
label.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
btn.addTarget(self, action: #selector(showButtonsVC(_:)), for: .touchUpInside)
}
#objc func showButtonsVC(_ sender: UIButton) {
// instantiate Button View Controller
let vc = ButtonViewController()
// set self as the delegate in ButtonViewController
vc.delegate = self
// present it
present(vc, animated: true, completion: nil)
}
func printTest(message: String) {
print("Delegate received:", message)
label.text = "From ButtonVC:\n" + message
}
}
class ButtonViewController: UIViewController {
var delegate: ViewControllerDelegate?
let textField: UITextField = {
let v = UITextField()
v.borderStyle = .roundedRect
return v
}()
let btn: UIButton = {
let v = UIButton()
v.setTitle("Save and Close", for: [])
v.backgroundColor = .systemRed
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
[textField, btn].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textField.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
textField.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
textField.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
btn.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 20.0),
btn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
btn.addTarget(self, action: #selector(buttonTap(_:)), for: .touchUpInside)
}
#IBAction func buttonTap(_ sender: UIButton) {
// call the delegate
let str = textField.text ?? "No text entered..."
delegate?.printTest(message: str)
dismiss(animated: true, completion: nil)
}
}
Related
I have a long register form that consists of 4 steps (the content is not relevant), here are the mockups:
My problem is that I need to share a progress view between multiple views. This view should have an animation of growth. What would be the right and clean way to do this with UIKit? Should I create a custom Navigation Bar with that progress? Or use child controllers in some way?
I've been searching over here but the other questions I found are very old (like 7 years ago) and I don't know if there could be better solutions.
Thanks a lot!
There are various ways to do this...
One common approach is to set the "progress view" as the navigation bar's Title View -- but that won't show it below the navigation bar.
So, another approach is to subclass UINavigationController and add a "progress view" as a subview. Then, implement willShow viewController and/or didShow viewController to update the progress.
As a quick example, assuming we have 4 "steps" to navigate to...
We'll start with defining a "base" view controller, with two properties that our custom nav controller class will use:
class MyBaseVC: UIViewController {
// this will be read by ProgressNavController
// to calculate the "progress percentage"
public let numSteps: Int = 4
// this will be set by each MyBaseVC subclass,
// and will be read by ProgressNavController
public var myStepNumber: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// maybe some stuff common to the "step" controllers
}
}
Then, each "step" controller will be a subclass of MyBaseVC, and will set its "step number" (along with anything else specific to that controller):
class Step1VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 1
// maybe some other stuff specific to this "step"
}
}
class Step2VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 2
// maybe some other stuff specific to this "step"
}
}
class Step3VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 3
// maybe some other stuff specific to this "step"
}
}
class Step4VC: MyBaseVC {
override func viewDidLoad() {
super.viewDidLoad()
myStepNumber = 4
// maybe some other stuff specific to this "step"
}
}
Then we can setup our custom nav controller class like this (it's not really as complicated as it may look):
class ProgressNavController: UINavigationController, UINavigationControllerDelegate {
private let outerView = UIView()
private let innerView = UIView()
private var pctConstraint: NSLayoutConstraint!
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// for this example, we're using a simple
// green view inside a red view
// as our "progress view"
// we set it up here, but we don't add it as a subview
// until we navigate to a MyBaseVC
// we know we're setting
// outerView height to 20
// innerView height to 12 (4-points top/bottom "padding")
// so let's round the ends of the innerView
innerView.layer.cornerRadius = 8.0
outerView.backgroundColor = .systemRed
innerView.backgroundColor = .systemGreen
outerView.translatesAutoresizingMaskIntoConstraints = false
innerView.translatesAutoresizingMaskIntoConstraints = false
outerView.addSubview(innerView)
// initialize pctConstraint
pctConstraint = innerView.widthAnchor.constraint(equalTo: outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)
NSLayoutConstraint.activate([
innerView.topAnchor.constraint(equalTo: outerView.topAnchor, constant: 4.0),
innerView.leadingAnchor.constraint(equalTo: outerView.leadingAnchor, constant: 4.0),
innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor, constant: -4.0),
pctConstraint,
])
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// if the next VC to show
// is a MyBaseVC subclass
if let _ = viewController as? MyBaseVC {
// add the "progess view" if we're coming from a non-MyBaseVC controller
if outerView.superview == nil {
view.addSubview(outerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor, constant: 4.0),
outerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
outerView.heightAnchor.constraint(equalToConstant: 20.0),
])
// .alpha to Zero so we can "fade it in"
outerView.alpha = 0.0
// we just added the progress view,
// so we'll let didShow "fade it in"
// and update the progress width
} else {
self.updateProgress(viewController)
}
} else {
if outerView.superview != nil {
// we want to quickly "fade-out" and remove the "progress view"
// if the next VC to show
// is NOT a MyBaseVC subclass
UIView.animate(withDuration: 0.1, animations: {
self.outerView.alpha = 0.0
}, completion: { _ in
self.outerView.removeFromSuperview()
self.pctConstraint.isActive = false
self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: .leastNonzeroMagnitude)
self.pctConstraint.isActive = true
})
}
}
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// if the VC just shown
// is a MyBaseVC subclass
// AND
// outerView.alpha < 1.0 (meaning it was just added)
if let _ = viewController as? MyBaseVC, outerView.alpha < 1.0 {
self.updateProgress(viewController)
}
// otherwise, updateProgress() is called from willShow
}
private func updateProgress(_ viewController: UIViewController) {
if let vc = viewController as? MyBaseVC {
// update the innerView width -- the "progress"
let nSteps: CGFloat = CGFloat(vc.numSteps)
let thisStep: CGFloat = CGFloat(vc.myStepNumber)
var pct: CGFloat = .leastNonzeroMagnitude
// sanity check
// avoid error/crash if either values are Zero
if nSteps > 0.0, thisStep > 0.0 {
pct = thisStep / nSteps
}
// don't exceed 100%
pct = min(pct, 1.0)
// we can't update the multiplier directly, so
// deactivate / update / activate
self.pctConstraint.isActive = false
self.pctConstraint = self.innerView.widthAnchor.constraint(equalTo: self.outerView.widthAnchor, multiplier: pct, constant: -8.0)
self.pctConstraint.isActive = true
// if .alpha is already 1.0, this is effectively ignored
UIView.animate(withDuration: 0.1, animations: {
self.outerView.alpha = 1.0
})
// animate the "bar width"
UIView.animate(withDuration: 0.3, animations: {
self.outerView.layoutIfNeeded()
})
}
}
}
So, when we navigate to a new controller:
we check to see if it is an instance of MyBaseVC
if Yes
add the progress view (if it's not already there)
get the step number from the new controller
update the progress
if Not
remove the progress view
I put up a complete example you can check out and inspect here: https://github.com/DonMag/ProgressNavController
I'm working on a project in UIKit, without storyboards (only programmatic layout constraints) and, following this, I have a custom view controller like this:
#objc public class testController: UIViewController, QLPreviewControllerDataSource {
public override func viewDidAppear(_ animated: Bool) {
let previewController = QLPreviewController()
previewController.dataSource = self
self.view.translatesAutoresizingMaskIntoConstraints = false
previewController.view.widthAnchor.constraint(equalToConstant: 200).isActive = true
present(previewController, animated: true, completion: nil)
}
public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
guard let url = Bundle.main.url(forResource: String("beans"), withExtension: "pdf") else {
fatalError("Could not load \(index).pdf")
}
return url as QLPreviewItem
}
}
Then, in my main View Controller file, I add this testController as a subview like so:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let test = testController()
self.view.addSubview(test.view)
test.view.translatesAutoresizingMaskIntoConstraints = false
}
}
This works fine, but I'd like to be able to change my testController's programmatic layout constraints relative to it's parent view.
I've tried stuff like this in the main view controller (ViewController):
let test = testController()
self.view.addSubview(test.view)
test.view.translatesAutoresizingMaskIntoConstraints = false
test.view.widthAnchor.constraint(equalTo: 200, constant: 0).isActive = true
but this simply doesn't work/the view doesn't reflect these constraints at all and it seems like the only way I can successfully modify the constraints of the testController, is within the viewDidAppear function of the testController class.
However, if I try something like this:
public override func viewDidAppear(_ animated: Bool) {
let previewController = QLPreviewController()
previewController.dataSource = self
self.view.translatesAutoresizingMaskIntoConstraints = false
previewController.view.widthAnchor.constraint(equalToConstant: 200).isActive = true //notice how this works since it's a hardcoded 200
previewController.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0).isActive = true //this throws an error
present(previewController, animated: true, completion: nil)
}
I get an error thrown.
So I'd somehow like to access the parent of testViewController I guess, and use it for the constraints of the view. I've tried unsuccessfully using presentingViewController and parent for this, but they either return nil or throw an error.
Any help here would be appreciated.
This is sample to add view and change the constraints, in your example you have to add more constraint to test view.
class ViewController: UIViewController {
let buttonTest: UIButton = {
let button = UIButton()
button.setTitle("go to ", for: .normal)
button.backgroundColor = .green
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.addSubview(buttonTest)
buttonTest.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
buttonTest.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
buttonTest.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
])
}
#objc func buttonPressed() {
let secondView = SecondViewController()
self.view.addSubview(secondView.view)
secondView.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
secondView.view.topAnchor.constraint(equalTo: self.view.topAnchor,constant: 100),
secondView.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
secondView.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
secondView.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -100)
])
}
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
}
}
I have MainViewController and DetailViewController that are stacked together by a navigation controller. I want to pass a value from DetailViewController back to the previous controller, which is MainViewController.
First, I tried it with UINavigationControllerDelegate:
class DetailViewController: UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
(viewController as? MainViewController)?.myClass = myClass
}
}
which was to be called as DetailViewController is popped:
_ = navigationController?.popViewController(animated: true)
But, the new value doesn't get reflected on MainViewController:
class MainViewController: UIViewController {
var myClass: MyClass
private lazy var commentLabel: UILabel = {
let comment = UILabel()
comment.text = myClass.comment
comment.numberOfLines = 0
return comment
}()
}
even though when I log myClass in MainViewController, I can see that it's being passed properly.
I also tried it with a property observer so that DetailViewController can pass it to a temporary property observer instead:
var temp: MyClass? {
willSet(newValue) {
myClass = newValue
}
}
but, the view controller's interface still doesn't change.
Finally, I tried creating a delegate in MainViewController:
protocol CallBackDelegate {
func callBack(value: MyClass)
}
where the function simply passes the argument:
func callBack(value: MyClass) {
myClass = value
}
I set the delegate to self:
if let vc = self.storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
vc.delegate = self
self.navigationController?.pushViewController(vc, animated: true)
}
and invoking the function in DetailViewController:
delegate?.callBack(value: MyClass)
but, still doesn't update the interface. It seems as though passing the value isn't the issue, but having it be reflected is.
This is an example of using the protocol / delegate pattern. It's about as basic as it gets...
Start a new single-view project
add the code below
Set the class of the default view controller to MainViewController
embed it in a Navigation Controller
run the app
Then:
Tap the button labeled "Push to next VC"
Enter some text in the "Edit Me" field
Tap the "Pop back to previous VC"
See that the label has been updated with your entered text.
protocol CallBackDelegate: class {
func callback(_ val: String)
}
class MainViewController: UIViewController, CallBackDelegate {
let btn = UIButton()
let theLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
btn.translatesAutoresizingMaskIntoConstraints = false
theLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
view.addSubview(theLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
theLabel.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
theLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
theLabel.backgroundColor = .yellow
btn.backgroundColor = .red
theLabel.text = "Default text"
btn.setTitle("Push to next VC", for: [])
btn.addTarget(self, action: #selector(self.pushButtonTapped(_:)), for: .touchUpInside)
}
#objc func pushButtonTapped(_ sender: Any?) -> Void {
let vc = DetailViewController()
vc.delegate = self
self.navigationController?.pushViewController(vc, animated: true)
}
func callback(_ val: String) {
theLabel.text = val
}
}
class DetailViewController: UIViewController {
weak var delegate: CallBackDelegate?
let textField = UITextField()
let btn = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
btn.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
view.addSubview(textField)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textField.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
textField.centerXAnchor.constraint(equalTo: g.centerXAnchor),
textField.widthAnchor.constraint(equalToConstant: 200.0),
btn.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 20.0),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
textField.backgroundColor = .yellow
textField.borderStyle = .roundedRect
btn.backgroundColor = .blue
textField.placeholder = "Edit me"
btn.setTitle("Pop back to previous VC", for: [])
btn.addTarget(self, action: #selector(self.popButtonTapped(_:)), for: .touchUpInside)
}
#objc func popButtonTapped(_ sender: Any?) -> Void {
if let s = textField.text {
delegate?.callback(s)
}
self.navigationController?.popViewController(animated: true)
}
}
Doesn't seem that you are updating the UILabel value anyhow
var myClass: MyClass? {
didSet {
self.commentLabel.text = myClass?.comment
}
}
You have to update the label text itself, right now it's constant with the first load data
I want to present a fullscreen ViewController "A" to cover our loading process across ViewControllers "B" AND "C", or in other words, 1) I present ViewController A from ViewController B, 2) segue from ViewController B to ViewController C while ViewController A is showing, 3) dismiss the ViewController A into ViewController C that ViewController B segued into.
If I push from the presenter ViewController B, the presented ViewController A will disappear as well. So my question is, what's the best way to change the ViewControllers B and C in the background, while another one (ViewController A) is presented on top of them?
Thanks.
You can do this in two ways:
1.Using a navigation controller
if let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "YourVCName") as? JunctionDetailsVC {
if let navigator = navigationController {
navigator.pushViewController(viewController, animated: false)
}
}
2.Present modally from you initial VC
if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "YourVCName") as? LoginVC
{
present(vc, animated: false, completion: nil)
}
Remember to not animate because you said that users shouldn't notice the transition.
Without knowing anything else about your app, I think you'd be better off redesigning the flow and User Experience, but here is one approach to do what you want.
We start with VC-B as the root VC of a UINavigationController
On button-tap, we add a "cover view" to the navigation controller's view hierarchy
We initially position that view below the bottom of the screen
Animate it up into view
Make desired changes to VC-B's view
Instantiate and Push VC-C
Do what's needed to setup the UI on VC-C
Animate the cover view down and off-screen
Remove the cover view from the hierarchy
And here's the code. Everything is done via code - even the initial Nav Controller setup - so No Storyboard needed (go to Project General Settings and delete anything in the Main Interface field).
AppDelegate.swift
//
// AppDelegate.swift
//
// Created by Don Mag on 8/30/19.
//
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
// instantiate a UINavigationController
let navigationController = UINavigationController();
// instantiate a NavBViewController
let vcB = NavBViewController();
// set the navigation controller's first controller
navigationController.viewControllers = [ vcB ];
self.window?.rootViewController = navigationController;
self.window?.makeKeyAndVisible()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
}
func applicationDidEnterBackground(_ application: UIApplication) {
}
func applicationWillEnterForeground(_ application: UIApplication) {
}
func applicationDidBecomeActive(_ application: UIApplication) {
}
func applicationWillTerminate(_ application: UIApplication) {
}
}
ViewControllers.swift - contains CoverView, NavBViewController and NavCViewController classes
//
// ViewControllers.swift
//
// Created by Don Mag on 8/30/19.
//
import UIKit
class CoverView: UIView {
let theSpinner: UIActivityIndicatorView = {
let v = UIActivityIndicatorView()
v.translatesAutoresizingMaskIntoConstraints = false
v.style = .whiteLarge
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.textColor = .white
v.text = "Please Wait"
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .blue
// add an Activity Spinner and a label
addSubview(theSpinner)
addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
theSpinner.centerXAnchor.constraint(equalTo: theLabel.centerXAnchor),
theSpinner.bottomAnchor.constraint(equalTo: theLabel.topAnchor, constant: -100.0),
])
theSpinner.startAnimating()
}
}
class NavBViewController: UIViewController {
// this view will be added or removed while the "coverView" is up
let newViewToChange: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.textColor = .white
v.textAlignment = .center
v.text = "A New View"
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "View Controller B"
return v
}()
let theButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Tap Me", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .yellow
// add a button and a label
view.addSubview(theButton)
view.addSubview(theLabel)
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theLabel.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
}
#objc func didTap(_ sender: Any) {
// get
// the neavigation controller's view,
if let navView = navigationController?.view {
// create a "cover view"
let coverView = CoverView()
coverView.translatesAutoresizingMaskIntoConstraints = false
// add the coverView to the neavigation controller's view
navView.addSubview(coverView)
// give it a tag so we can find it from the next view controller
coverView.tag = 9999
// create a constraint with an .identifier so we can get access to it from the next view controller
let startConstraint = coverView.topAnchor.constraint(equalTo: navView.topAnchor, constant: navView.frame.height)
startConstraint.identifier = "CoverConstraint"
// position the coverView so its top is at the bottom (hidden off-screen)
NSLayoutConstraint.activate([
startConstraint,
coverView.heightAnchor.constraint(equalTo: navView.heightAnchor, multiplier: 1.0),
coverView.leadingAnchor.constraint(equalTo: navView.leadingAnchor),
coverView.trailingAnchor.constraint(equalTo: navView.trailingAnchor),
])
// we need to force auto-layout to put the coverView in the proper place
navView.setNeedsLayout()
navView.layoutIfNeeded()
// change the top constraint constant to 0 (top of the neavigation controller's view)
startConstraint.constant = 0
// animate it up
UIView.animate(withDuration: 0.3, animations: ({
navView.layoutIfNeeded()
}), completion: ({ b in
// after animation is complete, we'll change something in this VC's UI
self.doStuff()
}))
}
}
func doStuff() -> Void {
// if newView is already there, remove it
// else, add it to the view
// this will happen *while* the coverView is showing
if newViewToChange.superview != nil {
newViewToChange.removeFromSuperview()
} else {
view.addSubview(newViewToChange)
NSLayoutConstraint.activate([
newViewToChange.bottomAnchor.constraint(equalTo: view.bottomAnchor),
newViewToChange.leadingAnchor.constraint(equalTo: view.leadingAnchor),
newViewToChange.trailingAnchor.constraint(equalTo: view.trailingAnchor),
newViewToChange.heightAnchor.constraint(equalToConstant: 80.0),
])
}
// simulate it taking a full second
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// instantiate and push the next VC
// again, this will happen *while* the coverView is showing
let vc = NavCViewController()
self.navigationController?.pushViewController(vc, animated: false)
}
}
}
class NavCViewController: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.text = "View Controller C"
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
// add a label
view.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
// do whatever else needed to setup this VC
// simulate it taking 1 second to setup this view
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// get
// the neavigation controller's view,
// the view with tag 9999 (the "coverView")
// the top constraint of the coverView
if let navView = self.navigationController?.view,
let v = navView.viewWithTag(9999),
let c = (navView.constraints.first { $0.identifier == "CoverConstraint" }) {
// change the top constant of the coverView to the height of the navView
c.constant = navView.frame.height
// animate it "down"
UIView.animate(withDuration: 0.3, animations: ({
navView.layoutIfNeeded()
}), completion: ({ b in
// after animation is complete, remove the coverView
v.removeFromSuperview()
}))
}
}
}
}
When you run it, it will look like this:
Tapping "Tap Me" will slide-up a "cover view" and a new red view will be added (but you won't see it):
The sample has 2-seconds worth of delay, to simulate whatever your app is doing to set up its UI. After 2-seconds, the cover view will slide down:
Revealing the pushed VC-C (confirmed by the Back button on the Nav Bar).
Tapping Back takes you back to VC-B, where you see the new red view that was added:
So, by animating the position of the cover view, we emulate the use of present() and dismiss(), and allow the push to take place behind it.
I have an extension of UIViewController as follows to dismiss the keyboard if the user taps on the screen. Within this view I have a scroll containing textfields and two container views. The container views contain a collectionView. I want my overall viewController to dismiss when tapped around but I still want my collectionView's didSelectitemAtIndexPath to trigger. How can I achieve this?
public extension UIViewController {
public func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
public func dismissKeyboard() {
view.endEditing(true)
}
}
and
class RegisterVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
logViewLoad()
self.hideKeyboardWhenTappedAround()
let controller = self.storyboard?.instantiateViewController(withIdentifier: "CollectionViewSearchVC") as! CollectionViewSearchVC
controller.type = "SC"
addChildViewController(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.delegate = self
containerView.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
controller.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
controller.view.topAnchor.constraint(equalTo: containerView.topAnchor),
controller.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
controller.didMove(toParentViewController: self)
subContractorVC = controller
let employeeController = self.storyboard?.instantiateViewController(withIdentifier: "CollectionViewSearchVC") as! CollectionViewSearchVC
employeeController.type = "V"
employeeController.delegate = self
addChildViewController(employeeController)
employeeController.view.translatesAutoresizingMaskIntoConstraints = false
employeeContainerView.addSubview(employeeController.view)
NSLayoutConstraint.activate([
employeeController.view.leadingAnchor.constraint(equalTo: employeeContainerView.leadingAnchor),
employeeController.view.trailingAnchor.constraint(equalTo: employeeContainerView.trailingAnchor),
employeeController.view.topAnchor.constraint(equalTo: employeeContainerView.topAnchor),
employeeController.view.bottomAnchor.constraint(equalTo: employeeContainerView.bottomAnchor)
])
employeeController.didMove(toParentViewController: self)
}
Implement the delegate methods for the GestureRecogniser in your ViewController and check for the class which is being touched, if that is of kind UICollectionView, then ignore that touch and let it do the default behaviour, i.e didSelect otherwise allow tap.
class ParentViewController: UIViewController {
var tap: UITapGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
tap = UITapGestureRecognizer(target: self, action: #selector(self.dismissKeyboard))
view.addGestureRecognizer(tap!)
}
public func hideKeyboardWhenTappedAround() {
dismissKeyboard()
}
#objc public func dismissKeyboard() {
view.endEditing(true)
}
}
class RegisterVC: ParentViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
tap?.delegate = self
// YOUR EXISTING CODE HERE
}
// NEW CODE ADDED HERE
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
print("shouldReceive")
print(gestureRecognizer.view as Any) // CHECK FOR WHICH CLASS YOU ARE GETTING HERE WHEN YOU CLICK ON COLLECTIONVIEW
if (gestureRecognizer.view?.isKind(of: UICollectionView.self))! {
return false
}
return true
}
}
Try it and share the results.
You need to create a view behind the child VC's views and add the gesture to it