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

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

Related

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

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

Cannot edit textField and textView

I have a simple class that designs the display of title and content and other controller class that displays the content after fetching it from coredata, every thing works fine but the fields are not editable, where am i making the error,
repeating the same para as the editor was asking to made more words *
I have a simple class that designs the display of title and content and other controller class that displays the content after fetching it from coredata, every thing works fine but the fields are not editable, where am i making the error,
Design Class
import UIKit
class UpdateNoteDesignView: UIView {
let notesUpdateTitle = UITextField()
let notesUpdateContent = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func commonInit() {
let updateStack = UIStackView()
updateStack.axis = .vertical
updateStack.alignment = .top
updateStack.distribution = .fill
updateStack.spacing = 5
notesUpdateTitle.translatesAutoresizingMaskIntoConstraints = false
notesUpdateContent.translatesAutoresizingMaskIntoConstraints = false
notesUpdateTitle.widthAnchor.constraint(greaterThanOrEqualToConstant: 150).isActive = true
notesUpdateContent.widthAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
notesUpdateTitle.heightAnchor.constraint(equalToConstant: 30).isActive = true
notesUpdateContent.heightAnchor.constraint(greaterThanOrEqualToConstant: 300).isActive = true
notesUpdateTitle.font = UIFont(name: "Arial", size: 30)
notesUpdateContent.font = UIFont(name: "Arial", size: 30)
notesUpdateContent.layer.cornerRadius = 5
notesUpdateContent.layer.borderWidth = 2
notesUpdateTitle.layer.borderWidth = 2
notesUpdateTitle.layer.cornerRadius = 5
updateStack.translatesAutoresizingMaskIntoConstraints = false
updateStack.addArrangedSubview(notesUpdateTitle)
updateStack.addArrangedSubview(notesUpdateContent)
addSubview(updateStack)
updateStack.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
updateStack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20).isActive = true
}
}
Controller Class
import UIKit
class UpdateNotesController: UIViewController {
let updateDesign = UpdateNoteDesignView()
var note: Note?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(updateDesign)
updateDesign.notesUpdateTitle.text = note?.title
updateDesign.notesUpdateContent.text = note?.contents
}
}
Here, the issue is that frame is not set for your view. Try to add background color to UpdateNoteDesignView. You will not able to see it.
Please add frame/constraint for UpdateNoteDesignView as shown here and make changes according to your requirement.
class ViewController: UIViewController {
var updateDesign: UpdateNoteDesignView!
// var note: Note?
override func viewDidLoad() {
super.viewDidLoad()
updateDesign = UpdateNoteDesignView()//frame: CGRect(x: 0, y: 0, width: 200, height: 260))
view.backgroundColor = UIColor.white
view.addSubview(updateDesign)
updateDesign.translatesAutoresizingMaskIntoConstraints = false
updateDesign.heightAnchor.constraint(equalToConstant: 450).isActive = true
updateDesign.widthAnchor.constraint(equalToConstant: 350).isActive = true
updateDesign.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
updateDesign.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
updateDesign.notesUpdateTitle.text = "Stack overflow"
updateDesign.notesUpdateContent.text = "Developer"
}
}

Custom resuable UIActivityIndicatorView Class programmatically

I'm new in making views programmatically.
I'm trying to make UIActivityIndicatorView class to make it reusable for me.
This is the class I made:
class ActivityIndicator: UIActivityIndicatorView {
let indicator = UIActivityIndicatorView()
let indicatorContainer = UIView()
func setupIndicatorView() {
indicatorContainer.isHidden = false
indicator.isHidden = false
indicator.style = .large
indicator.color = .white
indicator.startAnimating()
indicator.hidesWhenStopped = true
indicator.translatesAutoresizingMaskIntoConstraints = false
indicatorContainer.backgroundColor = .darkGray
indicatorContainer.alpha = 0.7
indicatorContainer.layer.cornerRadius = 8.0
indicatorContainer.translatesAutoresizingMaskIntoConstraints = false
addSubview(indicatorContainer)
indicatorContainer.addSubview(indicator)
func setupIndicatorContainerConstraints() {
NSLayoutConstraint.activate([
indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor),
indicatorContainer.widthAnchor.constraint(equalToConstant: frame.width / 5),
indicatorContainer.heightAnchor.constraint(equalToConstant: frame.width / 5)
])
}
func setupIndicatorViewConstraints() {
NSLayoutConstraint.activate([
indicator.centerXAnchor.constraint(equalTo: indicatorContainer.centerXAnchor),
indicator.centerYAnchor.constraint(equalTo: indicatorContainer.centerYAnchor)
])
}
setupIndicatorContainerConstraints()
setupIndicatorViewConstraints()
}
func hideIndicatorView() {
indicatorContainer.isHidden = true
indicator.stopAnimating()
indicator.isHidden = true
indicatorContainer.removeFromSuperview()
indicator.removeFromSuperview()
}
}
When I'm trying to make an instance from this class, it doesn't work in any other controller. Like this:
class SignInViewController: UIViewController {
let indicator = ActivityIndicator()
lazy var mainView: SignInView = {
let view = SignInView(delegate: self, frame: self.view.frame)
view.backgroundColor = .white
return view
}()
override func loadView() {
super.loadView()
view = mainView
}
func loginButtonTapped() {
indicator.setupIndicatorView()
}
}
I searched a lot to understand how to make it work but I haven't found a way.
You don't add it to vc's view
indicator.setupIndicatorView()
so consider
setupIndicatorView(_ view:UIView) {
.....
.....
// add it here
addSubview(indicatorContainer)
indicatorContainer.addSubview(indicator)
view.addSubview(self)
}

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

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

Animate constraints change UIViewController

I have "error" view, that is placed above navigation bar and hidden. When error occured, i want that view to smoothly show from top. I tried:
class AuthViewController: UIViewController {
let error: ErrorView = {
let error = ErrorView()
error.setup()
return error
}()
var topAnchor: NSLayoutConstraint!
var botAnchor: NSLayoutConstraint!
override func viewDidLoad() {
setupErrorView()
}
private func setupErrorView(){
view.addSubview(error)
botAnchor = error.bottomAnchor.constraint(equalTo: view.topAnchor)
botAnchor.isActive = true
topAnchor = error.topAnchor.constraint(equalTo: view.topAnchor, constant: CGFloat(Offsets.navigationAndStatusBarHeight))
topAnchor.isActive = false
error.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
error.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
func showError(_ text: String){
UIView.animate(withDuration: 2.0) {[weak self] in
guard let weakSelf = self else { return }
print("attempt to animate")
weakSelf.error.show(text)
weakSelf.botAnchor.isActive = false
weakSelf.topAnchor.isActive = true
weakSelf.view.setNeedsLayout()
}
}
}
class ErrorView: UIView {
private var label: UILabel = {
return LabelSL.regular()
}()
fileprivate func setup(){
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = Theme.Color.orange.value
addSubview(label)
}
fileprivate func show(_ text: String){
let sideOffset: CGFloat = 10
let verticalOffset: CGFloat = 10
label.text = text
label.topAnchor.constraint(equalTo: topAnchor, constant: verticalOffset).isActive = true
label.leftAnchor.constraint(equalTo: leftAnchor, constant: sideOffset).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor, constant: -sideOffset).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -verticalOffset).isActive = true
}
}
Animation should be done when func showError(_ text: String){ method called, but it's not. View just appear instantly.
You're trying to animate constraints in wrong way. You should set constraints outside of animation block and only layoutIfNeeded in animation:
func showError(_ text: String){
botAnchor.isActive = false
topAnchor.isActive = true
error.show(text)
UIView.animate(withDuration: 2.0) {
self.view.layoutIfNeeded()
}
}

Resources