Show UIView When Popping Back on a Navigation Controller - ios

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)
}
}

Related

How to change firstResponder with buttons on keyboard toolbar between UITextFields in UITableViewCells

I guess that it is a low brainer I'm struggling with, but unfortunately all my searches in this forum and other sources didn't give me a glue yet.
I'm creating a shopping list app for iOS. In the Viewcontroller for the entry of the shoppinglist positions I'm showing only the relevant entry fields depending on the kind of goods to be put on the shopping list.
Hence I have set up a tableView with different prototype cells and some of them contain UITextFields to handle this dynamic setup.
I have defined a toolbar for the keyboard containing one button at the right to hide the keyboard (which works) and two buttons ("next" & "back") on the left to jump to the next respectively previous input field, which should then become first responder, cursor set in this field and showing the keyboard.
Unfortunately this handing over of the firstResponder isn't working and the cursor is not set to the next/previous input field and sometimes even the keyboard disappears.
Jumping back doesn't work at all and the keyboard disappears always when the next active field is part of a different prototype cell (e.g. moving forward from the field for "brand" to the field for "quantity".
Has anyone a solution for it?
For the handling I have defined two notifications:
let keyBoardBarBackNotification = Notification.Name("keyBoardBarBackNotification")
let keyBoardBarNextNotification = Notification.Name("keyBoardBarNextNotification")
And the definition of the toolbar is done in the extension of UIViewController:
func setupKeyboardBar() -> UIToolbar {
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 50))
let leftButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(leftButtonTapped))
leftButton.tintColor = UIColor.systemBlue
let nextButton = UIBarButtonItem(image: UIImage(systemName: "chevron.right"), style: .plain, target: self, action: #selector(nextButtonTapped))
nextButton.tintColor = UIColor.systemBlue
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let fixSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(image: UIImage(systemName: "keyboard.chevron.compact.down"), style: .plain, target: self, action: #selector(doneButtonTapped))
doneButton.tintColor = UIColor.darkGray
toolbar.setItems([fixSpace, leftButton, fixSpace, nextButton, flexSpace, doneButton], animated: true)
toolbar.sizeToFit()
return toolbar
}
#objc func leftButtonTapped() {
view.endEditing(true)
NotificationCenter.default.post(Notification(name: keyBoardBarBackNotification))
}
#objc func nextButtonTapped() {
view.endEditing(true)
NotificationCenter.default.post(Notification(name: keyBoardBarNextNotification))
}
#objc func doneButtonTapped() {
view.endEditing(true)
}
}
In the viewController I have setup routines for the keyboard handling and a routine "switchActiveField" to determine the next actual field that should become the firstResponder:
class AddPositionVC: UIViewController {
#IBOutlet weak var menue: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.menue.delegate = self
self.menue.dataSource = self
self.menue.separatorStyle = .none
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleBackButtonPressed), name: keyBoardBarBackNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleNextButtonPressed), name: keyBoardBarNextNotification, object: nil)
}
enum TableCellType: String {
case product = "Product:"
case brand = "Brand:"
case quantity = "Quantity:"
case price = "Price:"
case shop = "Shop:"
// ...
}
var actualField = TableCellType.product // field that becomes firstResponder
// Arrray, defining the fields to be diplayed
var menueList: Array<TableCellType> = [.product, .brand, .quantity, .shop
]
// Array with IndexPath of displayed fields
var tableViewIndex = Dictionary<TableCellType, IndexPath>()
#objc func handleKeyboardDidShow(notification: NSNotification) {
guard let endframeKeyboard = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey]
as? CGRect else { return }
let insets = UIEdgeInsets( top: 0, left: 0, bottom: endframeKeyboard.size.height - 60, right: 0 )
self.menue.contentInset = insets
self.menue.scrollIndicatorInsets = insets
self.scrollToMenuezeile(self.actualField)
self.view.layoutIfNeeded()
}
#objc func handleKeyboardWillHide() {
self.menue.contentInset = .zero
self.view.layoutIfNeeded()
}
#objc func handleBackButtonPressed() {
switchActiveField(self.actualField, back: true)
}
#objc func handleNextButtonPressed() {
switchActiveField(self.actualField, back: false)
}
// Definition, which field should become next firstResponder
func switchActiveField(_ art: TableCellType, back bck: Bool) {
switch art {
case .brand:
self.actualField = bck ? .product : .quantity
case .quantity:
self.actualField = bck ? .brand : .shop
case .price:
self.actualField = bck ? .quantity : .shop
case .product:
self.actualField = bck ? .shop : .brand
case .shop:
self.actualField = bck ? .price : .product
// ....
}
if let index = self.tableViewIndex[self.actualField] {
self.menue.reloadRows(at: [index], with: .automatic)
}
}
}
And the extension for the tableView is:
extension AddPositionVC: UITableViewDelegate, UITableViewDataSource {
func scrollToMenuezeile(_ art: TableCellType) {
if let index = self.tableViewIndex[art] {
self.menue.scrollToRow(at: index, at: .bottom, animated: false)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menueList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableCellType = self.menueList[indexPath.row]
self.tableViewIndex[tableCellType] = indexPath
switch tableCellType {
case .product, .brand, .shop:
let cell = tableView.dequeueReusableCell(withIdentifier: "LabelTextFieldCell", for: indexPath) as! LabelTextFieldCell
cell.item.text = tableCellType.rawValue
cell.itemInput.inputAccessoryView = self.setupKeyboardBar()
cell.itemInput.text = "" // respective Input
if self.actualField == tableCellType {
cell.itemInput.becomeFirstResponder()
}
return cell
case .quantity, .price:
let cell = tableView.dequeueReusableCell(withIdentifier: "QuantityPriceCell", for: indexPath) as! QuantityPriceCell
cell.quantity.inputAccessoryView = self.setupKeyboardBar()
cell.quantity.text = "" // respective Input
cell.price.inputAccessoryView = self.setupKeyboardBar()
cell.price.text = "" // respective Input
if self.actualField == .price {
cell.price.becomeFirstResponder()
} else if self.actualField == .quantity {
cell.quantity.becomeFirstResponder()
}
return cell
}
}
}
//*********************************************
// MARK: - tableViewCells
//*********************************************
class LabelTextFieldCell: UITableViewCell, UITextFieldDelegate {
override func awakeFromNib() {
super.awakeFromNib()
itemInput.delegate = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
self.itemInput.resignFirstResponder()
}
#IBOutlet weak var item: UILabel!
#IBOutlet weak var itemInput: UITextField!
}
class QuantityPriceCell: UITableViewCell, UITextFieldDelegate {
override func awakeFromNib() {
super.awakeFromNib()
self.quantity.delegate = self
self.price.delegate = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
textField.resignFirstResponder()
}
#IBOutlet weak var quantity: UITextField!
#IBOutlet weak var price: UITextField!
}
Thanks for your support.
There are various ways to approach this... In fact, it's easy to find open-source 3rd-party libraries with lots of features -- just search (Google or wherever) for swift ios form builder.
But, if you'd like to work on it on your own, the basic idea is:
add your text fields to an array
add a class-level var/property such as var activeField: UITextField?
for each field, on textFieldDidBeginEditing:
self.activeField = textField
when the user taps the "Next" button:
guard let aField = self.activeField,
let idx = self.textFields.firstIndex(of: aField)
else { return }
if idx == self.textFields.count - 1 {
// "wrap around" to first field
textFields.first?.becomeFirstResponder()
} else {
// "move to" next field
textFields[idx + 1].becomeFirstResponder()
}
If all your fields are "on-screen" it's pretty straight-forward.
If they won't fit vertically (particularly when the keyboard is showing), if they're all in a scroll view, again, pretty straight-forward.
It gets complicated when putting them in cells in a tableView, for several reasons:
cells are not necessarily generated in order, so you have to write a bunch more code to put move from field-to-field in the correct order
if you have more cells than will fit on-screen, the "next field" may not exist! For example, suppose you have 8 rows... only 5 rows fit... you're editing the field in the last row and tap the Next button. You want to move to the field in Row 0, but Row 0 won't exist until you scroll back up to the top.
To add repeating similar-but-varying "rows," we don't need to use a table view.
For example, if we have a UIStackView with .axis = .vertical:
for i in 1...10 {
let label = UILabel()
label.text = "Row \(i)"
stackView.addArrangedSubview(label)
}
We've now added 10 single-label "cells."
So, for your task, instead of using a table view with your LabelTextFieldCell, we can write this function:
func buildLabelTextFieldView(labelText str: String) -> UIView {
let aView = UIView()
let label: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let field: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
label.text = str
self.textFields.append(field)
aView.addSubview(label)
aView.addSubview(field)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
label.firstBaselineAnchor.constraint(equalTo: field.firstBaselineAnchor),
field.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8.0),
field.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
field.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
field.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
])
return aView
}
and a similar (but slightly more complex):
func buildQuantityPriceView() -> UIView {
let aView = UIView()
...
return aView
}
then use it similarly to cellForRowAt:
for i in 0..<menueList.count {
let tableCellType = menueList[i]
var rowView: UIView!
switch tableCellType {
case .product, .brand, .shop:
rowView = buildLabelTextFieldView(labelText: tableCellType.rawValue)
case .quantity, .price:
rowView = buildQuantityPriceView()
}
stackView.addArrangedSubview(rowView)
}
If we add that stackView to a scrollView, we have a scrollable "Form."
Here's a complete example you can try out (no #IBOutlet or #IBAction connections ... just set a blank view controller's class to FormVC):
class FormVC: UIViewController, UITextFieldDelegate {
var textFields: [UITextField] = []
let scrollView = UIScrollView()
var menueList: Array<TableCellType> = [.product, .brand, .quantity, .shop]
lazy var kbToolBar: UIToolbar = {
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 50))
let leftButton = UIBarButtonItem(image: UIImage(systemName: "chevron.left"), style: .plain, target: self, action: #selector(leftButtonTapped))
leftButton.tintColor = UIColor.systemBlue
let nextButton = UIBarButtonItem(image: UIImage(systemName: "chevron.right"), style: .plain, target: self, action: #selector(nextButtonTapped))
nextButton.tintColor = UIColor.systemBlue
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let fixSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(image: UIImage(systemName: "keyboard.chevron.compact.down"), style: .plain, target: self, action: #selector(doneButtonTapped))
doneButton.tintColor = UIColor.darkGray
toolbar.setItems([fixSpace, leftButton, fixSpace, nextButton, flexSpace, doneButton], animated: true)
toolbar.sizeToFit()
return toolbar
}()
var activeField: UITextField?
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 32
stackView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stackView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -16.0),
stackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 8.0),
stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -8.0),
stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -8.0),
stackView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -16.0),
])
for i in 0..<menueList.count {
let tableCellType = menueList[i]
var rowView: UIView!
switch tableCellType {
case .product, .brand, .shop:
rowView = buildLabelTextFieldView(labelText: tableCellType.rawValue)
case .quantity, .price:
rowView = buildQuantityPriceView()
}
stackView.addArrangedSubview(rowView)
}
// we've added all the labels and fields
// and our textFields array contains all the fields in order
// we want all the "first/left" labels to be equal widths
guard let firstLabel = stackView.arrangedSubviews.first?.subviews.first as? UILabel
else {
fatalError("We did something wrong in our setup!")
}
stackView.arrangedSubviews.forEach { v in
// skip the first one
if v != stackView.arrangedSubviews.first {
if let thisLabel = v.subviews.first as? UILabel {
thisLabel.widthAnchor.constraint(equalTo: firstLabel.widthAnchor).isActive = true
}
}
}
// set inputAccessoryView and delegate on all the text fields
textFields.forEach { v in
v.inputAccessoryView = kbToolBar
v.delegate = self
}
// prevent keyboard from hiding scroll view elements
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
// during dev, use "if true" and set some colors so we can see view framing
if false {
view.backgroundColor = .systemYellow
scrollView.backgroundColor = .yellow
stackView.layer.borderColor = UIColor.red.cgColor
stackView.layer.borderWidth = 1
stackView.arrangedSubviews.forEach { v in
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
}
}
}
#objc func leftButtonTapped() {
guard let aField = self.activeField,
let idx = self.textFields.firstIndex(of: aField)
else { return }
if idx == 0 {
textFields.last?.becomeFirstResponder()
} else {
textFields[idx - 1].becomeFirstResponder()
}
}
#objc func nextButtonTapped() {
guard let aField = self.activeField,
let idx = self.textFields.firstIndex(of: aField)
else { return }
if idx == self.textFields.count - 1 {
textFields.first?.becomeFirstResponder()
} else {
textFields[idx + 1].becomeFirstResponder()
}
}
#objc func doneButtonTapped() {
view.endEditing(true)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.activeField = textField
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.activeField = nil
}
#objc func adjustForKeyboard(notification: Notification) {
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardScreenEndFrame = keyboardValue.cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == UIResponder.keyboardWillHideNotification {
self.scrollView.contentInset = .zero
} else {
self.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
}
self.scrollView.scrollIndicatorInsets = self.scrollView.contentInset
}
}
We'll put our "Row View" builder funcs in extensions, just to keep the code separated and a bit more readable:
extension FormVC {
func buildLabelTextFieldView(labelText str: String) -> UIView {
let aView = UIView()
let label: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let field: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
label.text = str
self.textFields.append(field)
aView.addSubview(label)
aView.addSubview(field)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
label.firstBaselineAnchor.constraint(equalTo: field.firstBaselineAnchor),
field.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8.0),
field.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
field.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
field.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
])
return aView
}
}
extension FormVC {
func buildQuantityPriceView() -> UIView {
let aView = UIView()
let labelA: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let fieldA: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
labelA.text = "Quantity:"
self.textFields.append(fieldA)
let labelB: UILabel = {
let v = UILabel()
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let fieldB: UITextField = {
let v = UITextField()
v.borderStyle = .bezel
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
labelB.text = "Price:"
self.textFields.append(fieldB)
aView.addSubview(labelA)
aView.addSubview(fieldA)
aView.addSubview(labelB)
aView.addSubview(fieldB)
NSLayoutConstraint.activate([
labelA.leadingAnchor.constraint(equalTo: aView.leadingAnchor, constant: 0.0),
labelA.firstBaselineAnchor.constraint(equalTo: fieldA.firstBaselineAnchor),
fieldA.leadingAnchor.constraint(equalTo: labelA.trailingAnchor, constant: 8.0),
fieldA.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
fieldA.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
labelB.leadingAnchor.constraint(equalTo: fieldA.trailingAnchor, constant: 8.0),
labelB.firstBaselineAnchor.constraint(equalTo: fieldB.firstBaselineAnchor),
fieldB.leadingAnchor.constraint(equalTo: labelB.trailingAnchor, constant: 8.0),
fieldB.topAnchor.constraint(equalTo: aView.topAnchor, constant: 0.0),
fieldB.bottomAnchor.constraint(equalTo: aView.bottomAnchor, constant: 0.0),
fieldB.trailingAnchor.constraint(equalTo: aView.trailingAnchor, constant: 0.0),
// we want both fields to be equal widths
fieldB.widthAnchor.constraint(equalTo: fieldA.widthAnchor),
])
return aView
}
}
When running, it looks like this:
If you add some more "rows" - or, easier, increase the stack view spacing, such as stackView.spacing = 100 - you'll see how it continues to work with the scrollView when the keyboard is showing.
Of course, you mention in your comments: "...more entry fields (e.g. date with a Datepicker, etc.)", so you'd need to write new "row builder" funcs and add some logic to Next tap going to/from a Picker instead of a textField.
But, you may find this a helpful starting point.

Open DatePicker popup directly on button tap

Trying to open the Date picker popup on button tap but it is showing a label instead of picker, and on tapping that label the picker opens. Below is code:
#IBAction func changeMonth(_ sender: Any) {
let picker : UIDatePicker = UIDatePicker()
picker.datePickerMode = UIDatePicker.Mode.date
picker.addTarget(self, action: #selector(dueDateChanged(sender:)), for: UIControl.Event.valueChanged)
let pickerSize : CGSize = picker.sizeThatFits(CGSize.zero)
picker.frame = CGRect(x:0.0, y:250, width:pickerSize.width, height:460)
self.view.addSubview(picker)
}
#objc func dueDateChanged(sender:UIDatePicker){
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .none
btnMonth.setTitle(dateFormatter.string(from: sender.date), for: .normal)
}
Need this to open on button tap:
But this is being shown:
and on tapping of this date label, picker opens. I am not getting why picker not opening directly. Please guide what's wrong in above code.
Here is a very simple example of embedding the date picker in a view, then showing / hiding that view on a button tap:
class MyDatePicker: UIView {
var changeClosure: ((Date)->())?
var dismissClosure: (()->())?
let dPicker: UIDatePicker = {
let v = UIDatePicker()
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let blurEffect = UIBlurEffect(style: .dark)
let blurredEffectView = UIVisualEffectView(effect: blurEffect)
let pickerHolderView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.layer.cornerRadius = 8
return v
}()
[blurredEffectView, pickerHolderView, dPicker].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
addSubview(blurredEffectView)
pickerHolderView.addSubview(dPicker)
addSubview(pickerHolderView)
NSLayoutConstraint.activate([
blurredEffectView.topAnchor.constraint(equalTo: topAnchor),
blurredEffectView.leadingAnchor.constraint(equalTo: leadingAnchor),
blurredEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
blurredEffectView.bottomAnchor.constraint(equalTo: bottomAnchor),
pickerHolderView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
pickerHolderView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
pickerHolderView.centerYAnchor.constraint(equalTo: centerYAnchor),
dPicker.topAnchor.constraint(equalTo: pickerHolderView.topAnchor, constant: 20.0),
dPicker.leadingAnchor.constraint(equalTo: pickerHolderView.leadingAnchor, constant: 20.0),
dPicker.trailingAnchor.constraint(equalTo: pickerHolderView.trailingAnchor, constant: -20.0),
dPicker.bottomAnchor.constraint(equalTo: pickerHolderView.bottomAnchor, constant: -20.0),
])
if #available(iOS 14.0, *) {
dPicker.preferredDatePickerStyle = .inline
} else {
// use default
}
dPicker.addTarget(self, action: #selector(didChangeDate(_:)), for: .valueChanged)
let t = UITapGestureRecognizer(target: self, action: #selector(tapHandler(_:)))
blurredEffectView.addGestureRecognizer(t)
}
#objc func tapHandler(_ g: UITapGestureRecognizer) -> Void {
dismissClosure?()
}
#objc func didChangeDate(_ sender: UIDatePicker) -> Void {
changeClosure?(sender.date)
}
}
class ViewController: UIViewController {
let myPicker: MyDatePicker = {
let v = MyDatePicker()
return v
}()
let myButton: UIButton = {
let v = UIButton()
v.setTitle("Show Picker", for: [])
v.setTitleColor(.white, for: .normal)
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
[myButton, myPicker].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// custom picker view should cover the whole view
myPicker.topAnchor.constraint(equalTo: g.topAnchor),
myPicker.leadingAnchor.constraint(equalTo: g.leadingAnchor),
myPicker.trailingAnchor.constraint(equalTo: g.trailingAnchor),
myPicker.bottomAnchor.constraint(equalTo: g.bottomAnchor),
myButton.centerXAnchor.constraint(equalTo: g.centerXAnchor),
myButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
myButton.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.75),
])
// hide custom picker view
myPicker.isHidden = true
// add closures to custom picker view
myPicker.dismissClosure = { [weak self] in
guard let self = self else {
return
}
self.myPicker.isHidden = true
}
myPicker.changeClosure = { [weak self] val in
guard let self = self else {
return
}
print(val)
// do something with the selected date
}
// add button action
myButton.addTarget(self, action: #selector(tap(_:)), for: .touchUpInside)
}
#objc func tap(_ sender: Any) {
myPicker.isHidden = false
}
}
Here's how it looks on start:
tapping the button will show the custom view:
and here's how it looks on iOS 13 (prior to the new UI):

iOS Swift protocol & delegate

Why my delegation is not working?
With my sample the action button works when clicked, but for some reason it does not reach the didAction function in my second controller.
protocol HomeControllerDelegate: class {
func didAction()
}
class HomeController: UIViewController {
weak var delegate: HomeControllerDelegate?
private let actionButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Action", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .black
button.setHeight(50)
button.setWidth(100)
button.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(actionButton)
actionButton.centerX(inView: view)
actionButton.centerY(inView: view)
}
#objc func handleAction() {
print("DEBUG: Handle Action Button delegate here....")
delegate?.didAction()
}
}
class SecondController: UIViewController {
let homeController = HomeController()
override func viewDidLoad() {
super.viewDidLoad()
homeController.delegate = self
}
}
extension SecondController: HomeControllerDelegate {
func didAction() {
print("DEBUG: In SecondController - didAction()")
}
}
Many thanks for the guidance, it has made me look at the fundamentals and also what to research.
I have created the below which solves my problem.
A container controller that Instantiates two ViewControlers and sets them as childVC's
Then created a protocol on FirstChildVC and SecondChildVC is the delegate
All is working now an I understand a lot better.
class ContainerController: UIViewController {
let secondChildVC = SecondChildVC()
let firstChildVC = FirstChildVC()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
addFirstChildVC()
addSecondChildVC()
}
func addSecondChildVC(){
addChild(secondChildVC)
view.addSubview(secondChildVC.view)
secondChildVC.didMove(toParent: self)
setSecondChildVCConstraints()
}
func addFirstChildVC(){
addChild(firstChildVC)
view.addSubview(firstChildVC.view)
firstChildVC.delegateActionButton = secondChildVC
firstChildVC.didMove(toParent: self)
setFirstChildVCConstraints()
}
func setFirstChildVCConstraints() {
firstChildVC.view.translatesAutoresizingMaskIntoConstraints = false
firstChildVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
firstChildVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
firstChildVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
firstChildVC.view.heightAnchor.constraint(equalToConstant: 200).isActive = true
}
func setSecondChildVCConstraints() {
secondChildVC.view.translatesAutoresizingMaskIntoConstraints = false
secondChildVC.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
secondChildVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
secondChildVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
secondChildVC.view.heightAnchor.constraint(equalToConstant: 200).isActive = true
}
}
protocol FirstChildVCDelegate: class {
func didAction(data: String)
}
class FirstChildVC: UIViewController {
weak var delegateActionButton: FirstChildVCDelegate!
private let actionButton: UIButton = {
let button = UIButton(type: .system)
let buttonHeight = 30
button.setTitle("Button", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .black
button.layer.cornerRadius = CGFloat(buttonHeight / 2)
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: CGFloat(buttonHeight)).isActive = true
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 100).isActive = true
button.addTarget(self, action: #selector(handleAction), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink
configureActionButtonView()
}
func configureActionButtonView() {
view.addSubview(actionButton)
actionButton.translatesAutoresizingMaskIntoConstraints = false
actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
actionButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
#objc func handleAction() {
delegateActionButton.didAction(data: "Button Pressed")
}
}
class SecondChildVC: UIViewController {
private let actionButtonLabel: UILabel = {
let label = UILabel()
label.text = "test"
label.font = .systemFont(ofSize: 18)
label.textColor = .white
label.isHidden = true
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPurple
configureActionButtonLabel()
}
func configureActionButtonLabel() {
view.addSubview(actionButtonLabel)
actionButtonLabel.translatesAutoresizingMaskIntoConstraints = false
actionButtonLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
actionButtonLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
}
extension SecondChildVC: FirstChildVCDelegate {
func didAction(data: String) {
actionButtonLabel.isHidden.toggle()
actionButtonLabel.text = data
}
}
Initial State
State once Button pressed

What error am I making when trying to pass data between ViewControllers using closures?

My main View Controller has an embedded UITabbarController in it. There are 2 subVCs: viewControllerONE & viewControllerTWO.
viewControllerONE displays a label showing the user's current score, and viewControllerTWO displays a button, pressing which should display an error window on viewControllerONE.
(For the sake of simplicity, the user has to manually navigate to viewControllerONE using the tab bar to see the error, ie, pressing the button on viewControllerTWO doesn't take you to viewControllerONE and then display the errorWindow.)
The error window is a simple UIView class.
From SO, I have learnt that the best way to pass data in swift isn't delegates but closures, as the former is more of an objective C design, and so I've used closures to pass data between view controllers.
So here is my viewControllerONE code:
class ViewControllerONE: UIViewController {
var score = 10
lazy var scoreLabel: UILabel = {
let label = UILabel()
label.text = String(score)
label.font = .systemFont(ofSize: 80)
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(scoreLabel)
let VC = ViewControllerTWO()
VC.callback = { [weak self ] in
let error = errorView()
self?.view.addSubview(error)
}
NSLayoutConstraint.activate([
scoreLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
scoreLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
and here is my viewControllerTWO code:
class ViewControllerTWO: UIViewController {
var callback: (() -> Void)?
let buttonTwo: UIButton = {
let button = UIButton()
button.setTitle("HIT THIS BUTTON!", for: .normal)
button.backgroundColor = .systemBlue
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(buttonTwoPressed), for: .touchUpInside)
button.layer.cornerRadius = 8
return button
}()
#objc func buttonTwoPressed() {
print("PRESSEDDDD")
callback?()
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(buttonTwo)
NSLayoutConstraint.activate([
buttonTwo.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonTwo.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
And here is the error view:
class ErrorView: UIView {
fileprivate let dismissButton: UIButton = {
let button = UIButton()
button.setTitle("DISMISS!!!!", for: .normal)
button.backgroundColor = .systemBlue
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(dismissButtonPressed), for: .touchUpInside)
button.layer.cornerRadius = 12
return button
}()
#objc func dismissButtonPressed() {
self.errorGoAway()
}
fileprivate let errorViewBox: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
v.layer.cornerRadius = 24
return v
}()
#objc fileprivate func errorGoAway() {
self.alpha = 0
}
#objc fileprivate func errorShow() {
self.alpha = 1
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(errorGoAway)))
self.backgroundColor = UIColor.gray
self.backgroundColor?.withAlphaComponent(0.8)
self.frame = UIScreen.main.bounds
self.addSubview(errorViewBox)
errorViewBox.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
errorViewBox.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
errorViewBox.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7).isActive = true
errorViewBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.45).isActive = true
errorViewBox.addSubview(dismissButton)
dismissButton.leadingAnchor.constraint(equalTo: errorViewBox.leadingAnchor).isActive = true
dismissButton.trailingAnchor.constraint(equalTo: errorViewBox.trailingAnchor).isActive = true
dismissButton.centerYAnchor.constraint(equalTo: errorViewBox.centerYAnchor).isActive = true
dismissButton.heightAnchor.constraint(equalTo: errorViewBox.heightAnchor, multiplier: 0.15).isActive = true
errorShow()
}
You need to get your controller from tabBarController instead of create new instance:
class ViewControllerONE: UIViewController {
var score = 10
lazy var scoreLabel: UILabel = {
let label = UILabel()
label.text = String(score)
label.font = .systemFont(ofSize: 80)
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
label.textColor = .blue
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(scoreLabel)
var VC: ViewControllerTWO?
let arrayOfVC = self.tabBarController?.viewControllers ?? []
for item in arrayOfVC {
if let secondVC = item as? ViewControllerTWO {
VC = secondVC
break
}
}
VC?.callback = { [weak self ] in
let error = ErrorView()
self?.view.addSubview(error)
}
NSLayoutConstraint.activate([
scoreLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
scoreLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}

Interacting with contents of UIPopoverPresentationController on regular sized iPhones?

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.

Resources