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

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.

Related

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

Protocol method does not pass the correct parameter

Consider this case, suppose you have a subclass of UIView and a protocol i.e.
protocol MyViewProtocol
{
func didTapSelf(thisView: MyView)
}
class MyView: UIView
{
lazy var tf: UITextField =
{
let obj = UITextField()
obj.translatesAutoresizingMaskIntoConstraints = false
obj.placeholder = "Enter text"
obj.backgroundColor = .white
return obj
}()
var btn: UIButton =
{
let obj = UIButton()
obj.translatesAutoresizingMaskIntoConstraints = false
obj.addTarget(self, action: #selector(didTapBtn), for: .touchUpInside)
obj.backgroundColor = .orange
return obj
}()
var delegate: MyViewProtocol?
init(backgroundColor: UIColor, placeholder: String, delegate: MyViewProtocol?)
{
super.init(frame: .zero)
self.backgroundColor = backgroundColor
self.delegate = delegate
tf.placeholder = placeholder
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureUI()
{
addAllSubviews(tf,
btn)
NSLayoutConstraint.activate([
btn.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
btn.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
btn.topAnchor.constraint(equalTo: topAnchor, constant: 32),
tf.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
tf.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
tf.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 12),
tf.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
tf.heightAnchor.constraint(equalToConstant: 44)
])
// let gesture = UITapGestureRecognizer(target: self, action: #selector(didTapBtn))
// gesture.cancelsTouchesInView = false
// addGestureRecognizer(gesture)
}
#objc func didTapBtn()
{
delegate?.didTapSelf(thisView: self)
}
}
Now, inside your view controller, make 2 instances of MyView i.e.
lazy var myView1: MyView =
{
let obj = MyView(backgroundColor: .yellow, placeholder: "view 1", delegate: self)
obj.translatesAutoresizingMaskIntoConstraints = false
return obj
}()
lazy var myView2: MyView =
{
let obj = MyView(backgroundColor: .green, placeholder: "view 2", delegate: self)
obj.translatesAutoresizingMaskIntoConstraints = false
return obj
}()
And implement your view controller with the protocol method like this:
extension ViewController: MyViewProtocol
{
func didTapSelf(thisView: MyView)
{
if thisView == myView1
{
print("tapped view 1")
}
else if thisView == myView2
{
print("tapped view 2")
}
else
{
print("i am ambigious")
}
}
}
Now when you tap the button in myView1 or myView2 it will print correctly, i.e. tapped view 1 or 2, whichever button is tapped, but as you start editing the myView1 textField and for time when it is in editing state, no matter whichever button of myView1 or myView2 you tap, it will only print tapped view 1, or whatever view, whose textField is in editing mode.
For my knowledge, it must pass the correct self object, regardless of the editing state of textField . Why is this behaviour occurring ?
p.s this behaviour does not occur when you use a tap gesture on any of above views.
This has nothing to do, really, with the protocol or any of the other red herrings in the story. A little debugging will reveal that in fact the correct button is being tapped, but the runtime is literally sending the target–action message to the wrong MyView. The underlying reason is that in this line:
obj.addTarget(self, action: #selector(didTapBtn), for: .touchUpInside)
... the term self doesn't mean what you think it does.
To fix the issue, simply change
var btn: UIButton =
To
lazy var btn: UIButton =

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

Adding more tableViews as subview on my main scrollView depending on a UISwitch state works only after application relaunches

UPDATED BELOW!
I have a UI structure with a horizontal scrollView nesting 5 tableViews – each representing a day of the week. I have added a UISwitch to add weekend to the week, so when the user switches it on, two more tableview-subviews are added to the scrollView. So far so good, but the switch change only takes effect, when I relaunch the application. Looks like ViewDidLoad() makes it happen, but nothing else. I added a Bool variable called isWeekOn. Its state is managed from viewDidLoad:
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
dayTableViews = fiveOrSevenDayTableViews()
where fiveOrSevenTableViews() is a closure returning the array of tableviews with the proper count and dayTableViews is my local array variable.
lazy var fiveOrSevenDayTableViews: () -> [DayTableView] = {
if self.isWeekendOn == false {
return [self.mondayTableView, self.tuesdayTableview, self.wednesdayTableview, self.thursdayTableView, self.fridayTableView]
} else {
return [self.mondayTableView, self.tuesdayTableview, self.wednesdayTableview, self.thursdayTableView, self.fridayTableView, self.saturdayTableView,self.sundayTableView]
}
}
I added a didSet property observer to isWeekendOn and that also calls setupViews(), where the number of tableviews is also decided by calling fiveOrSevenTableViews closure .
var isWeekendOn: Bool = false {
didSet {
print("LessonVC IsWeekendon: ",isWeekendOn)
dayTableViews = fiveOrSevenDayTableViews()
setupViews()
print("didset daytableviews", fiveOrSevenDayTableViews().count)
}
}
Where my setupViews() looks like:
func setupViews() {
setupScrollView()
let numberOfTableViews = CGFloat(dayTableViews.count)
let stackView = UIStackView(arrangedSubviews: fiveOrSevenDayTableViews())
print("setupViews stacview subviews count", stackView.arrangedSubviews.count)
stackView.axis = .horizontal
stackView.distribution = .fillEqually
scrollView.addSubview(stackView)
setupStackViewConstraints(stackView, numberOfTableViews)
}
And setupScrollView():
private func setupScrollView() {
let numberOfTableViews = CGFloat(dayTableViews.count)
print("setupScrollview dableviews", numberOfTableViews)
scrollView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height:0)
scrollView.contentSize = CGSize(width: view.frame.width * numberOfTableViews, height: 0)
view.addSubview(scrollView)
setupScrollviewConstraints()
}
All the print statements are called properly, so I am wondering, why the changes actually do not take effect real time, and instead working only relaunch.
What I tried:
As #maniponken suggested, i made a function which looks like:
func readdStackView(_ stackView: UIStackView) { stackView.removeFromSuperview()
setupViews() }
than I call this within the isWeekendOn didSet observer. Didn't work out unfortunately.
UPDATE:
Actually when I put anything in my isWeekendon didSet observer, doesn't work! For example changing my navigationBar backgroundColor...etc Everything is reflecting on console though, in the print statements! Those functions also take effect at relaunch only.I have no idea what I am doing wrong.
UPDATE2:
Removing the tables works without problem with a local UIButton! My Problem is the following though: I have a settings view controller, which has a switch for setting 5 or 7 table views. Realtime update does not work with that switch, only with le local button, triggering an #objc func. I still need that settings panel for the user though!
Try this, it's not a stackview but it works for adding (and removing) tableviews to a ViewController.
This method is not using Storyboards
In your viewcontroller, containing the tableview
import Foundation
import UIKit
class SevenTableviews: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView1: UITableView = {
let tv = UITableView()
tv.backgroundColor = .white
tv.separatorStyle = .none
return tv
}()
let tableView2: UITableView = {
let tv = UITableView()
tv.backgroundColor = .white
tv.separatorStyle = .none
return tv
}()
let tableSwitch: UISwitch = {
let switchBtn = UISwitch()
switchBtn.addTarget(self, action: #selector(switchTables), for: .touchUpInside)
return switchBtn
}()
var isTableTwoShowing = false
let reuseIdentifier = "DaysCell"
var days = ["monday", "tuesday", "wednesday", "thursday", "friday"]
var weekendDays = ["saturday", "sunday"]
override func viewDidLoad() {
super.viewDidLoad()
setupTableview()
}
func setupTableview() {
tableView1.dataSource = self
tableView1.delegate = self
tableView1.register(DaysTableviewCell.self, forCellReuseIdentifier: reuseIdentifier)
view.addSubview(tableView1)
tableView1.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.leftAnchor, bottom: view.centerYAnchor, right: view.rightAnchor)
if isTableTwoShowing == true {
tableView2.dataSource = self
tableView2.delegate = self
tableView2.register(DaysTableviewCell.self, forCellReuseIdentifier: reuseIdentifier)
view.addSubview(tableView2)
tableView2.anchor(top: view.centerYAnchor, left: view.leftAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, right: view.rightAnchor)
}
view.addSubview(tableSwitch)
tableSwitch.anchor(bottom: view.safeAreaLayoutGuide.bottomAnchor, right: view.rightAnchor, paddingBottom: 24, paddingRight: 12)
}
#objc func switchTables() {
if tableSwitch.isOn {
isTableTwoShowing = true
setupTableview()
} else {
isTableTwoShowing = false
tableView2.removeFromSuperview()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == tableView1 {
return days.count
} else if tableView == tableView2 {
return weekendDays.count
} else {
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! DaysTableviewCell
if tableView == tableView1 {
cell.dateLabel.text = days[indexPath.row]
return cell
} else {
cell.dateLabel.text = weekendDays[indexPath.row]
return cell
}
}
}
in your tableviewCell-class:
import Foundation
import UIKit
class DaysTableviewCell: UITableViewCell {
let identifier = "DaysCell"
let cellContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.backgroundColor = Colors.boxBack
view.setCellShadow()
return view
}()
let dateLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 20)
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
func setupViews() {
selectionStyle = .none
addSubview(cellContainer)
cellContainer.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 4, paddingLeft: 8, paddingBottom: 4, paddingRight: 8, height: 35)
cellContainer.addSubview(dateLabel)
dateLabel.anchor(top: cellContainer.topAnchor, left: cellContainer.leftAnchor, bottom: cellContainer.bottomAnchor, right: cellContainer.rightAnchor, paddingTop: 4, paddingLeft: 8, paddingBottom: 4, paddingRight: 8)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I am using the same cell class for both tableviews but you can decide yourself how you want to do this.
Also, my constraints are set with an extension i found from a tutorial once:
extension UIView {
func anchor(top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, paddingTop: CGFloat? = 0, paddingLeft: CGFloat? = 0, paddingBottom: CGFloat? = 0, paddingRight: CGFloat? = 0, width: CGFloat? = nil, height: CGFloat? = nil) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop!).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddingLeft!).isActive = true
}
if let bottom = bottom {
if let paddingBottom = paddingBottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
}
if let right = right {
if let paddingRight = paddingRight {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
}
if let width = width {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if let height = height {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
}
Hope this helps
Couple notes:
You don't need to re-create / re-add your stack view ever time the switch gets changed. Add it in viewDidLoad() and then add / remove the "DayTableViews"
Use constraints for your stack view inside your scroll view, instead of calculating .contentSize.
Probably want to use an array of your "Day Tables" rather than having individual mondayTableView, tuesdayTableView, etc... vars.
Here's an example you can work from. I used a simple UIView with a centered label as a simulated "DayTableView" - should be pretty clear. Everything is via code - no #IBOutlet or #IBAction - so to test this, create a new project, add this code, and assign the startup view controller to AddToScrollViewController:
//
// AddToScrollViewController.swift
//
// Created by Don Mag on 11/15/19.
//
import UIKit
class DayTableView: UIView {
// simple UIView with a centered label
// this is just simulatig a UITableView
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .yellow
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class AddToScrollViewController: UIViewController {
let theSwitch: UISwitch = {
let v = UISwitch()
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .orange
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
v.spacing = 16
return v
}()
let mondayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Monday"
return v
}()
let tuesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Tuesday"
return v
}()
let wednesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Wednesday"
return v
}()
let thursdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Thursday"
return v
}()
let fridayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Friday"
return v
}()
let saturdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Saturday"
return v
}()
let sundayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Sunday"
return v
}()
var dayTableViews: [DayTableView] = [DayTableView]()
lazy var fiveOrSevenDayTableViews: () -> [DayTableView] = {
if self.isWeekendOn == false {
return [self.mondayTableView, self.tuesdayTableView, self.wednesdayTableView, self.thursdayTableView, self.fridayTableView]
} else {
return [self.mondayTableView, self.tuesdayTableView, self.wednesdayTableView, self.thursdayTableView, self.fridayTableView, self.saturdayTableView,self.sundayTableView]
}
}
var isWeekendOn: Bool = false {
didSet {
print("LessonVC IsWeekendon: ",isWeekendOn)
dayTableViews = fiveOrSevenDayTableViews()
setupViews()
print("didset daytableviews", fiveOrSevenDayTableViews().count)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// for each of these views...
[theSwitch, scrollView, stackView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
}
// for each of these views...
[mondayTableView, tuesdayTableView, wednesdayTableView, thursdayTableView, fridayTableView, saturdayTableView, sundayTableView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
// constrain widths to 160 (change to desired table view widths)
$0.widthAnchor.constraint(equalToConstant: 160.0).isActive = true
// give them a background color so we can see them
$0.backgroundColor = .systemBlue
}
// add the (empty) stack view to the scroll view
scrollView.addSubview(stackView)
// add the switch to the view
view.addSubview(theSwitch)
// add the scroll view to the view
view.addSubview(scrollView)
// use safe area for view elements
let g = view.safeAreaLayoutGuide
// we need to constrain the scroll view contents (the stack view, in this case)
// to the contentLayoutGuide so auto-layout can handle the content sizing
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// put switch in top-left corner
theSwitch.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
theSwitch.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
// constrain scroll view 12-pts below the switch
// and leading / trailing / bottom at Zero
scrollView.topAnchor.constraint(equalTo: theSwitch.bottomAnchor, constant: 12.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain the stack view to the scroll view's contentLayoutGuide
// with 8-pts padding on each side (easier to see the framing)
stackView.topAnchor.constraint(equalTo: sg.topAnchor, constant: 8.0),
stackView.bottomAnchor.constraint(equalTo: sg.bottomAnchor, constant: -8.0),
stackView.leadingAnchor.constraint(equalTo: sg.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: sg.trailingAnchor, constant: -8.0),
// constrain height of stack view to height of scroll view frame,
// minus 16-pts (for 8-pt padding)
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, constant: -16),
])
// add a target for the switch
theSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
// set based on saved state in UserDefaults
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
}
#objc func switchChanged(_ sender: Any) {
// switch was tapped (toggled on/off)
if let v = sender as? UISwitch {
// update state in UserDefaults
UserDefaults.standard.set(v.isOn, forKey: "switchState")
// update the UI
isWeekendOn = v.isOn
}
}
func setupViews() {
// first, remove any existing table views
stackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
// get the array of 5 or 7 table views
let a = fiveOrSevenDayTableViews()
// add the table views to the stack view
a.forEach {
stackView.addArrangedSubview($0)
}
print("setupViews stacview subviews count", stackView.arrangedSubviews.count)
}
}
Scrolled to the right with the "weekend switch" off:
Scrolled to the right immediately after turning the "weekend switch" on:
Edit
Here is a slightly different (bit more efficient) approach. Instead of adding / removing table views, simply show / hide the Saturday and Sunday tables. The stack view will automatically handle the scroll view's content size.
Full updated example:
//
// AddToScrollViewController.swift
//
// Created by Don Mag on 11/15/19.
//
import UIKit
class DayTableView: UIView {
// simple UIView with a centered label
// this is just simulatig a UITableView
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .yellow
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class AddToScrollViewController: UIViewController {
let theSwitch: UISwitch = {
let v = UISwitch()
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .orange
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
v.spacing = 16
return v
}()
let mondayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Monday"
return v
}()
let tuesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Tuesday"
return v
}()
let wednesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Wednesday"
return v
}()
let thursdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Thursday"
return v
}()
let fridayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Friday"
return v
}()
let saturdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Saturday"
return v
}()
let sundayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Sunday"
return v
}()
var isWeekendOn: Bool = false {
didSet {
print("LessonVC IsWeekendon: ",isWeekendOn)
setupViews()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// for each of these views...
[theSwitch, scrollView, stackView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
}
// for each of these views...
[mondayTableView, tuesdayTableView, wednesdayTableView, thursdayTableView, fridayTableView, saturdayTableView, sundayTableView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
// constrain widths to 160 (change to desired table view widths)
$0.widthAnchor.constraint(equalToConstant: 160.0).isActive = true
// give them a background color so we can see them
$0.backgroundColor = .systemBlue
// add them to the stack view
stackView.addArrangedSubview($0)
}
// add the stack view to the scroll view
scrollView.addSubview(stackView)
// add the switch to the view
view.addSubview(theSwitch)
// add the scroll view to the view
view.addSubview(scrollView)
// use safe area for view elements
let g = view.safeAreaLayoutGuide
// we need to constrain the scroll view contents (the stack view, in this case)
// to the contentLayoutGuide so auto-layout can handle the content sizing
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// put switch in top-left corner
theSwitch.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
theSwitch.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
// constrain scroll view 12-pts below the switch
// and leading / trailing / bottom at Zero
scrollView.topAnchor.constraint(equalTo: theSwitch.bottomAnchor, constant: 12.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain the stack view to the scroll view's contentLayoutGuide
// with 8-pts padding on each side (easier to see the framing)
stackView.topAnchor.constraint(equalTo: sg.topAnchor, constant: 8.0),
stackView.bottomAnchor.constraint(equalTo: sg.bottomAnchor, constant: -8.0),
stackView.leadingAnchor.constraint(equalTo: sg.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: sg.trailingAnchor, constant: -8.0),
// constrain height of stack view to height of scroll view frame,
// minus 16-pts (for 8-pt padding)
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, constant: -16),
])
// add a target for the switch
theSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
// set based on saved state in UserDefaults
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
// update the switch UI
theSwitch.isOn = isWeekendOn
}
#objc func switchChanged(_ sender: Any) {
// switch was tapped (toggled on/off)
if let v = sender as? UISwitch {
// update state in UserDefaults
UserDefaults.standard.set(v.isOn, forKey: "switchState")
// update the UI
isWeekendOn = v.isOn
}
}
func setupViews() {
// show or hide Sat and Sun table views
saturdayTableView.isHidden = !isWeekendOn
sundayTableView.isHidden = !isWeekendOn
}
}
Why don’t you make a horizontal collection view instead of the normal scroll view. It would be easier to call reloadData whenever you want to add or delete a cell (and of course each cell is a tableView)
Finally I have solved the problem.
UPDATE: The main reason I had to set up NotificationCenter for this matter, is that I used UITabBarController to add SettingsVC to my app instead of presenting it modally. Details below.
//Skip this part for answer
My main problem – as it turned out – was that my UISwitch was on a separate vc, called SettingsViewController.
This switch supposed to do the tableview-adding and removing on my main vc. I tried with delegate protocols, targeting shared instance of settingsVC, nothing worked, but adding a local button for this – which is definitely not what I wanted.
Then I read about NotificationCenter!
I remembered it from Apples App Development For Swift book,I read last year, but forgot since.
// So the Anwswer
After I set my constraints correctly based on the great hint of #DonMag, I set up NotificationCenter for my SettingsViewController, posting to my Main VC.
class SettingsViewController: UITableViewController {
private let reuseID = "reuseId"
lazy var switchButton: UISwitch = {
let sw = UISwitch()
sw.addTarget(self, action: #selector(switchPressed), for: .valueChanged)
sw.onTintColor = AdaptiveColors.navigationBarColor
return sw
}()
static let switchNotification = Notification.Name("SettingsController.switchNotification")
var isOn = Bool() {
didSet {
NotificationCenter.default.post(name:SettingsViewController.switchNotification, object: nil)
}
}
#objc func switchPressed(_ sender: UISwitch) {
UserDefaults.standard.set(sender.isOn, forKey: "switchState")
self.isOn = sender.isOn
}
then in the mainVC:
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
view.backgroundColor = .white
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
// The Solution:
NotificationCenter.default.addObserver(self, selector: #selector(handleRefresh), name: SettingsViewController.switchNotification, object: nil)
dayTableViews = fiveOrSevenDayTableViews()
print("daytableviews count ", dayTableViews.count)
scrollView.delegate = self
editButtonItem.title = LocalizedString.edit
navigationItem.title = localizedDays[currentPage]
setupNavigationBar()
setupButtons()
setupTableViews()
setupViews()
isWeekendOn == true ? setupCurrentDayViewFor_7days() : setupCurrentDayViewFor_5days()
}
then here in mainVC's #objc func handleRefresh() { } i am handling the removal or addition!
UPDATE:
in SettingsVC:
static let switchOnNotification = Notification.Name("SettingsController.switchOnNotification")
static let switchOffNotification = Notification.Name("SettingsController.switchOffNotification")
var isOn = Bool() {
didSet {
}
willSet {
if newValue == true {
NotificationCenter.default.post(name:SettingsViewController.switchOnNotification, object: nil)
} else if newValue == false {
NotificationCenter.default.post(name:SettingsViewController.switchOffNotification, object: nil)
}
}
}
in viewDidLoad in mainVC:
NotificationCenter.default.addObserver(self, selector: #selector(handleAddWeekendTableViews), name: SettingsViewController.switchOnNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleRemoveWeekendTableViews), name: SettingsViewController.switchOffNotification, object: nil)
#objc func handleAddWeekendTableViews() {
[saturdayTableView, sundayTableView].forEach {
stackView.addArrangedSubview($0)
dayTableViews.append($0)
}
}
#objc func handleRemoveWeekendTableViews() {
manageCurrentPage()
dayTableViews.removeLast(2)
[saturdayTableView, sundayTableView].forEach {
$0.removeFromSuperview()
}
}
This one is actually working!

call label postion and size from outside of class (Swift4)

What I am trying to do is assign the position and size of a label from outside a class. Then within 2 separate classes call the label to add text to it. This would save time a lot of time if this would work.
let backbutton = UILabel!
backbutton.translatesAutoresizingMaskIntoConstraints = false
backbutton.leftAnchor.constraint(equalTo: _, constant: 20).isActive = true
backbutton.topAnchor.constraint(equalTo: _, constant: 125).isActive = true
backbutton.widthAnchor.constraint(equalToConstant: 50).isActive = true
backbutton.heightAnchor.constraint(equalToConstant: 50).isActive = true
class nineViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
backbutton.text = String("red")
}
}
class two: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
backbutton.text = String("two")
}
}
Create a Utilities class separately to use the functions that are inside it globally.
Utilities:
class Utilities: NSObject
{
class func createLabel(on view: UIView, horizontalAnchors hAnchors: (leading: CGFloat, leadingView: UIView, trailing: CGFloat, trailingView: UIView), verticalAnchors vAnchors: (top: CGFloat, topView: UIView, bottom: CGFloat, bottomView: UIView)) -> UILabel {
let label = UILabel()
view.addSubview(label)
label.backgroundColor = UIColor.red
label.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: hAnchors.leadingView.leadingAnchor, constant: hAnchors.leading).isActive = true
label.trailingAnchor.constraint(equalTo: hAnchors.trailingView.trailingAnchor, constant: -hAnchors.trailing).isActive = true
label.topAnchor.constraint(equalTo: vAnchors.topView.topAnchor, constant: vAnchors.top).isActive = true
label.bottomAnchor.constraint(equalTo: vAnchors.bottomView.topAnchor, constant: -vAnchors.bottom).isActive = true
return label
}
class func createLabel(on view: UIView, positionAnchors pAnchors: (leading: CGFloat, leadingView: UIView, top: CGFloat, topView: UIView), size: (width: CGFloat, height: CGFloat)) -> UILabel {
let label = UILabel()
view.addSubview(label)
label.backgroundColor = UIColor.red
label.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: pAnchors.leadingView.leadingAnchor, constant: pAnchors.leading).isActive = true
label.topAnchor.constraint(equalTo: pAnchors.topView.topAnchor, constant: pAnchors.top).isActive = true
label.widthAnchor.constraint(equalToConstant: size.width).isActive = true
label.heightAnchor.constraint(equalToConstant: size.height).isActive = true
return label
}
}
In ViewController:
#IBOutlet weak var autoLayedoutLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let originY: CGFloat = 50
let spacing: CGFloat = 16
let width: CGFloat = 300
let height: CGFloat = 50
let label = Utilities.createLabel(on: view, positionAnchors: (spacing, view, originY, view), size: (width, height))
label.text = "Label with Position Anchors & Size"
label.backgroundColor = UIColor.red
let label2 = Utilities.createLabel(on: view, horizontalAnchors: (spacing, view, spacing, view), verticalAnchors: (spacing + height, label, spacing, autoLayedoutLabel))
label2.text = "Label with Horizontal & Vertical Anchors"
label2.backgroundColor = UIColor.green
}
You can have different variable for buttonText and set his position and size in his setter like
var buttonText:String {
didSet{
backButton.text = buttonText
setFontAndPosition()
}
}
and in viewController just set the value
override func viewDidLoad() {
super.viewDidLoad()
buttonText = "red"
}
I found it's feasible to directly use global UILable. If you don't need to manage too many labels, this is the simplest way.
A TabBarcontroller is used for testing here.
let backbutton = UILabel()
class MyTabBarController : UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setViewControllers([SettingViewController(), NineViewController(), TwoViewController()], animated: false)
}
}
class SettingViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "setting", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
self.view.addSubview(backbutton)
backbutton.text = "cool"
backbutton.translatesAutoresizingMaskIntoConstraints = false
backbutton.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 20).isActive = true
backbutton.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 125).isActive = true
backbutton.widthAnchor.constraint(equalToConstant: 50).isActive = true
backbutton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
class NineViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "nine", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
backbutton.text = String("red")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
backbutton.text = String("red-Appear")
}
}
class TwoViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "two", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
backbutton.text = String("two")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
backbutton.text = String("two-Appear")
}
}
If you prefer defining the label inside one class. You may define the global UILabel as this:
weak var backbutton: UILabel!
class SettingViewController: UIViewController {
let mybutton = UILabel()
backbutton = mybutton
// continue
}
You don't need to change any other codes.
Now is the second part of the story. If you wanna setup a global UILabel outside any view, is that possible. Without constraints it's very simple like this:
let backbutton: UILabel! = {
let button = UILabel()
button.text = "test"
button.frame = CGRect.init(x: 200, y: 200, width: 50, height: 50)
return button
}()
The setting View changes like this :
class SettingViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "setting", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
self.view.addSubview(backbutton)
}
}
It's clear there is only one line in the SettingVC. But if you need to use constraints, what should we do? Everything else is fine, but the position of UILabel constraints depends on the superView of UILabel. So an extension can be used here to make things easier.
let specialLabelTag = 1001
let backbutton: UILabel! = {
let button = UILabel()
button.tag = specialLabelTag
button.text = "test" // for test purpose
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 50).isActive = true
button.heightAnchor.constraint(equalToConstant: 50).isActive = true
return button
}()
extension UILabel{
override open func didMoveToSuperview() {
superview?.didMoveToSuperview()
if(tag == specialLabelTag){
leftAnchor.constraint(equalTo: superview!.leftAnchor, constant: 20).isActive = true
topAnchor.constraint(equalTo: superview!.topAnchor, constant: 125).isActive = true
}
}
The tag used in extension is to identify the global UILabel in order not to affect other UILabels. Only position constraints are needed in the extension. SettingUP vc is as same as before.
Now you can build a label without any view class. But you have to add them somewhere and modify the text as you like. Hope this is the answer to the question.
BTW, you can subclass the UILabel to MyUILabel with above code and then make it global (just put outside any class). It would be much easier because you don't need to use specialLabelTag.
let backbutton = MyUILabel()

Resources