Open DatePicker popup directly on button tap - ios

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

Related

Weird horizontal shrinking animation when hiding UIButton with Configuration in UIStackView

I'm facing this weird animation issues when hiding UIButton in a StackView using the new iOS 15 Configuration. See playground:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private weak var contentStackView: UIStackView!
override func viewDidLoad() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 150)
view.backgroundColor = .white
let contentStackView = UIStackView()
contentStackView.spacing = 8
contentStackView.axis = .vertical
for _ in 1...2 {
contentStackView.addArrangedSubview(makeConfigurationButton())
}
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.addAction(buttonAction, for: .primaryActionTriggered)
view.addSubview(contentStackView)
view.addSubview(button)
contentStackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: view.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
self.contentStackView = contentStackView
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 0) {
guard let toggleElement = self?.contentStackView.arrangedSubviews[0] else { return }
toggleElement.isHidden.toggle()
toggleElement.alpha = toggleElement.isHidden ? 0 : 1
self?.contentStackView.layoutIfNeeded()
}
}
}
private func makeSystemButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("System Button", for: .normal)
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
return button
}
}
PlaygroundPage.current.liveView = MyViewController()
Which results in this animation:
But I want the animation to look like this, where the button only shrinks vertically:
Which you can replicate in the playground by just swapping contentStackView.addArrangedSubview(makeConfigurationButton()) for contentStackView.addArrangedSubview(makeSystemButton()).
I guess this has something to do with the stack view alignment, setting it to center gives me the desired animation, but then the buttons don't fill the stack view width anymore and setting the width through AutoLayout results in the same animation again... Also, having just one system button in the stack view results in the same weird animation, but why does it behave differently for two system buttons? What would be a good solution for this problem?
As you've seen, the built-in show/hide animation with UIStackView can be quirky (lots of other quirks when you really get into it).
It appears that, when using a button with UIButton.Configuration, the button's width changes from the width assigned by the stack view to its intrinsic width as the animation occurs.
We can get around that by giving the button an explicit height constraint -- but, what if we want to use the intrinsic height (which may not be known in advance)?
Instead of setting the constraint, set the button's Content Compression Resistance Priority::
button.configuration = config
// add this line
button.setContentCompressionResistancePriority(.required, for: .vertical)
return button
And we no longer get the horizontal sizing:
As you will notice, though, the button doesn't "squeeze" vertically... it gets "pushed up" outside the stack view's bounds.
We can avoid that by setting .clipsToBounds = true on the stack view:
If this effect is satisfactory, we're all set.
However, as we can see, the button is still not getting "squeezed." If that is the visual effect we want, we can use a custom "self-stylized" button instead of a Configuration button:
Of course, there is very little visual difference - and looking closely the button's text is not squeezing. If we really, really, really want that to happen, we need to animate a transform instead of using the stack view's default animation.
And... if we are taking advantage of some of the other conveniences with Configurations, using a self-stylized UIButton might not be an option.
If you want to play with the differences, here's some sample code:
class ViewController : UIViewController {
var btnStacks: [UIStackView] = []
override func viewDidLoad() {
view.backgroundColor = .systemYellow
let outerStack = UIStackView()
outerStack.axis = .vertical
outerStack.spacing = 12
for i in 1...3 {
let cv = UIView()
cv.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let label = UILabel()
label.backgroundColor = .yellow
label.font = .systemFont(ofSize: 15, weight: .light)
let st = UIStackView()
st.axis = .vertical
st.spacing = 8
if i == 1 {
label.text = "Original Configuration Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeOrigConfigurationButton())
}
}
if i == 2 {
label.text = "Resist Compression Configuration Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeConfigurationButton())
}
}
if i == 3 {
label.text = "Custom Buttons"
for _ in 1...2 {
st.addArrangedSubview(makeCustomButton())
}
}
st.translatesAutoresizingMaskIntoConstraints = false
cv.addSubview(st)
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: 28.0),
st.topAnchor.constraint(equalTo: cv.topAnchor),
st.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
st.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
cv.heightAnchor.constraint(equalToConstant: 100.0),
])
btnStacks.append(st)
outerStack.addArrangedSubview(label)
outerStack.addArrangedSubview(cv)
outerStack.setCustomSpacing(2.0, after: label)
}
// a horizontal stack view to hold a label and UISwitch
let ctbStack = UIStackView()
ctbStack.axis = .horizontal
ctbStack.spacing = 8
let label = UILabel()
label.text = "Clips to Bounds"
let ctbSwitch = UISwitch()
ctbSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
ctbStack.addArrangedSubview(label)
ctbStack.addArrangedSubview(ctbSwitch)
// put the label/switch stack in a view so we can center it
let ctbView = UIView()
ctbStack.translatesAutoresizingMaskIntoConstraints = false
ctbView.addSubview(ctbStack)
// button to toggle isHidden/alpha on the first
// button in each stack view
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.backgroundColor = .white
button.addAction(buttonAction, for: .primaryActionTriggered)
outerStack.addArrangedSubview(ctbView)
outerStack.addArrangedSubview(button)
outerStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outerStack)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
ctbStack.topAnchor.constraint(equalTo: ctbView.topAnchor),
ctbStack.bottomAnchor.constraint(equalTo: ctbView.bottomAnchor),
ctbStack.centerXAnchor.constraint(equalTo: ctbView.centerXAnchor),
])
}
#objc func switchChanged(_ sender: UISwitch) {
btnStacks.forEach { v in
v.clipsToBounds = sender.isOn
}
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0) {
guard let self = self else { return }
self.btnStacks.forEach { st in
st.arrangedSubviews[0].isHidden.toggle()
st.arrangedSubviews[0].alpha = st.arrangedSubviews[0].isHidden ? 0 : 1
}
}
}
}
private func makeOrigConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.title = "Configuration Button"
button.configuration = config
// add this line
button.setContentCompressionResistancePriority(.required, for: .vertical)
return button
}
private func makeCustomButton() -> UIButton {
let button = UIButton()
button.setTitle("Custom Button", for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.lightGray, for: .highlighted)
button.backgroundColor = .systemBlue
button.layer.cornerRadius = 6
return button
}
}
Looks like this:
Edit
Quick example of another "quirk" when it comes to hiding a stack view's arranged subview (excess code in here, but I stripped down the above example):
class MyViewController : UIViewController {
var btnStacks: [UIStackView] = []
override func viewDidLoad() {
view.backgroundColor = .systemYellow
let outerStack = UIStackView()
outerStack.axis = .vertical
outerStack.spacing = 12
let cv = UIView()
cv.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let label = UILabel()
label.backgroundColor = .yellow
label.font = .systemFont(ofSize: 15, weight: .light)
let st = UIStackView()
st.axis = .vertical
st.spacing = 8
let colors: [UIColor] = [
.cyan, .green, .yellow, .orange, .white
]
label.text = "Labels"
for j in 0..<colors.count {
let v = UILabel()
v.text = "Label"
v.textAlignment = .center
v.backgroundColor = colors[j]
if j == 2 {
v.text = "Height Constraint = 80.0"
v.heightAnchor.constraint(equalToConstant: 80.0).isActive = true
}
st.addArrangedSubview(v)
}
st.translatesAutoresizingMaskIntoConstraints = false
cv.addSubview(st)
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalToConstant: 28.0),
st.topAnchor.constraint(equalTo: cv.topAnchor),
st.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
st.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
cv.heightAnchor.constraint(equalToConstant: 300.0),
])
btnStacks.append(st)
outerStack.addArrangedSubview(label)
outerStack.addArrangedSubview(cv)
outerStack.setCustomSpacing(2.0, after: label)
// button to toggle isHidden/alpha on the first
// button in each stack view
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.backgroundColor = .white
button.addAction(buttonAction, for: .primaryActionTriggered)
outerStack.addArrangedSubview(button)
outerStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(outerStack)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
outerStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
outerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
outerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0) {
guard let self = self else { return }
self.btnStacks.forEach { st in
st.arrangedSubviews[2].isHidden.toggle()
}
}
}
}
}
When this is run and the "Toggle" button is tapped, it will be painfully obvious what's "not-quite-right."
You should add height constraint to buttons and update this constraint while animating. I edit your code just as below.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private weak var contentStackView: UIStackView!
override func viewDidLoad() {
view.frame = CGRect(x: 0, y: 0, width: 300, height: 150)
view.backgroundColor = .white
let contentStackView = UIStackView()
contentStackView.spacing = 8
contentStackView.axis = .vertical
for _ in 1...2 {
contentStackView.addArrangedSubview(makeConfigurationButton())
}
let button = UIButton(type: .system)
button.setTitle("Toggle", for: .normal)
button.addAction(buttonAction, for: .primaryActionTriggered)
view.addSubview(contentStackView)
view.addSubview(button)
contentStackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentStackView.topAnchor.constraint(equalTo: view.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
self.contentStackView = contentStackView
}
private var buttonAction: UIAction {
UIAction { [weak self] _ in
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1, delay: 0) {
guard let toggleElement = self?.contentStackView.arrangedSubviews[0] else { return }
toggleElement.isHidden.toggle()
toggleElement.alpha = toggleElement.isHidden ? 0 : 1
toggleElement.heightAnchor.constraint(equalToConstant: toggleElement.isHidden ? 0 : 50)
self?.contentStackView.layoutIfNeeded()
}
}
}
private func makeSystemButton() -> UIButton {
let button = UIButton(type: .system)
button.setTitle("System Button", for: .normal)
return button
}
private func makeConfigurationButton() -> UIButton {
let button = UIButton()
var config = UIButton.Configuration.filled()
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 50)
])
config.title = "Configuration Button"
button.configuration = config
return button
}
}
PlaygroundPage.current.liveView = MyViewController()

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.

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

Show UIView When Popping Back on a Navigation Controller

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

UIButton not calling action when UISearchBar in navigationBar is firstResponder

I want to have a UIButton in a UIViewController tappable while the user is editing text in a UISearchBar that is part of the UINavigationBar.
The UIButton is responding to touches (it shows the highlighted state), but it is not calling its action. Same problem for other UIControl classes like UISwitch.
To make it more intriguing: when placing the UISearchBar in the UIViewController's UIView, there's no problem.
What should I do in order to trigger the button action while the UISearchBar is active?
Example VC:
import UIKit
class ViewController: UIViewController {
private let button: UIButton = {
let button = UIButton(type: UIButtonType.system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Test", for: .normal)
button.setTitleColor(.green, for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
return button
}()
private let textField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.placeholder = "place"
textField.borderStyle = .bezel
textField.addTarget(self, action: #selector(textFieldTriggered), for: .allEditingEvents)
return textField
}()
private let uiSwitch: UISwitch = {
let uiSwitch = UISwitch()
uiSwitch.translatesAutoresizingMaskIntoConstraints = false
uiSwitch.addTarget(self, action: #selector(switchToggled), for: UIControlEvents.valueChanged)
return uiSwitch
}()
private let formSearchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.translatesAutoresizingMaskIntoConstraints = false
return searchBar
}()
private let navigationSearchBar: UISearchBar = {
return UISearchBar()
}()
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(button)
view.addSubview(textField)
view.addSubview(formSearchBar)
view.addSubview(uiSwitch)
NSLayoutConstraint.activate([
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: -16),
view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: button.topAnchor, constant: -16),
button.bottomAnchor.constraint(equalTo: textField.topAnchor, constant: -16),
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: textField.leadingAnchor, constant: -16),
textField.widthAnchor.constraint(equalToConstant: 100),
textField.bottomAnchor.constraint(equalTo: formSearchBar.topAnchor, constant: -16),
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: formSearchBar.leadingAnchor, constant: -16),
formSearchBar.widthAnchor.constraint(equalToConstant: 100),
formSearchBar.bottomAnchor.constraint(equalTo: uiSwitch.topAnchor, constant: -16),
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: uiSwitch.leadingAnchor, constant: -16),
])
navigationItem.titleView = navigationSearchBar
_ = navigationSearchBar.becomeFirstResponder()
}
#objc
private func buttonTapped() {
let color = button.titleColor(for: .normal)
button.setTitleColor(color == .green ? .red : .green, for: .normal)
}
#objc
private func textFieldTriggered() {
textField.placeholder = textField.placeholder == "place" ? "holder" : "place"
}
#objc
private func switchToggled() {
UIView.animate(withDuration: 0.3) {
self.uiSwitch.isOn = !self.uiSwitch.isOn
}
}
}

Resources