Target Action for lazy Button does not work - swift

Using Swift 5.1.3, iOS13.3, XCode11.3,
I try to create a simple Button Target Action.
However, the Button is inside its own StackView class and moreover, is a lazy button.
Why is the Target Action not working in my Code ? i.e. callButtonMethod is never called !
Could it be because of the late API responses or is it because a lazy button cannot do target action. I am clueless at the moment.
Thank you for any help on this.
Here is my code:
class CockpitHeaderStackView: UIStackView {
weak var profileBtnDelegate: CallButtonProfileImage?
var profileImageView = UIImageView()
var profileName = "Cockpit".localized
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
if let profile = MyAPI.profile, let person = profile.person {
profileName = "\(person.firstName ?? "") \(person.lastName ?? "")"
}
MyAPI.getPicture(PictureType.avatar) { [weak self] (error, image) in
guard let self = self else { return } // check if self still alive otherwise bail out
DispatchQueue.main.async {
if let image = image {
self.profileImageView.image = image
} else {
self.profileImageView.image = #imageLiteral(resourceName: "profile-placeholder-small")
}
self.profileImageView.contentMode = .scaleAspectFill
self.axis = .horizontal
self.alignment = .bottom
self.spacing = 10.0
self.addArrangedSubview(self.titleLabel)
self.addArrangedSubview(self.button)
}
}
}
lazy var titleLabel: UILabel = {
let labelWidth: CGFloat = UIScreen.main.bounds.width - 16.0 - 10.0 - 36.0 - 16.0 // FullScreenWidth minus (Leading + Spacing + ButtonWidth + Trailing)
let label = UILabel()
label.font = AppConstants.Font.NavBar_TitleFont
label.text = profileName
label.textColor = .white
label.tintColor = .white
label.widthAnchor.constraint(equalToConstant: labelWidth).isActive = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var button: UIButton = {
let buttonWidth: CGFloat = 36.0
let button = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: buttonWidth, height: buttonWidth)))
button.setImage(self.profileImageView.image, for: .normal)
button.addTarget(self, action: #selector(callButtonMethod), for: .touchUpInside)
button.frame = CGRect(x: 0, y: 0, width: 36, height: 36)
button.layer.cornerRadius = button.frame.size.width / 2
button.layer.masksToBounds = false
button.clipsToBounds = true
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
button.heightAnchor.constraint(equalToConstant: buttonWidth).isActive = true
return button
}()
#objc func callButtonMethod() {
profileBtnDelegate?.callProfileBtnMethod()
}
}
The CockpitHeaderStackView is used to create a custom NavigationBar of my ViewController.
Here the code for the custom NavigationBar with usage of the CockpitHeaderStackView :
protocol CallButtonProfileImage: AnyObject {
func callProfileBtnMethod()
}
class MyViewController: UIViewController {
// ...
lazy var titleStackView: CockpitHeaderStackView = {
let titleStackView = CockpitHeaderStackView(frame: CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: 88.0)))
titleStackView.translatesAutoresizingMaskIntoConstraints = false
return titleStackView
}()
lazy var cockpitHeaderView: UIView = {
let cockpitHeaderView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: 88.0)))
cockpitHeaderView.addSubview(titleStackView)
titleStackView.leadingAnchor.constraint(equalTo: cockpitHeaderView.leadingAnchor, constant: 16.0).isActive = true
titleStackView.topAnchor.constraint(equalTo: cockpitHeaderView.topAnchor).isActive = true
titleStackView.trailingAnchor.constraint(equalTo: cockpitHeaderView.trailingAnchor, constant: -16.0).isActive = true
titleStackView.bottomAnchor.constraint(equalTo: cockpitHeaderView.bottomAnchor).isActive = true
return cockpitHeaderView
}()
override func viewDidLoad() {
super.viewDidLoad()
// ...
view.clipsToBounds = true
navigationController?.set_iOS12_lookAndFeel()
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationItem.largeTitleDisplayMode = .always
}
override func viewWillLayoutSubviews() {
// replace NavBar title by custom cockpitHeaderView
self.title = ""
self.navigationItem.titleView = self.cockpitHeaderView
// position the cockpitHeaderView inside the largeTitleDisplayMode NavBar
self.cockpitHeaderView.translatesAutoresizingMaskIntoConstraints = false
self.cockpitHeaderView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
if let navBarBottomAnchor = self.navigationController?.navigationBar.bottomAnchor {
if UIScreen.main.bounds.height > 568.0 {
self.cockpitHeaderView.topAnchor.constraint(equalTo: navBarBottomAnchor, constant: -48.0).isActive = true
} else {
self.cockpitHeaderView.topAnchor.constraint(equalTo: navBarBottomAnchor, constant: -46.0).isActive = true // iPhone SE space limitation
}
} else {
self.cockpitHeaderView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 89.0).isActive = true
}
}
func callProfileBtnMethod() {
print("right BarButton called here")
}
}

I finally found "a solution": The lazy initialisations seem to be the cause of the error-behaviour.
In fact, when I replace all lazy initialisation and also eliminate the StackView (called CockpitHeaderStackView) and put everything in non-lazy let-constants, then it works !!
Here is the final and relevant code (i.e. I placed everything inside the viewWillAppearmethod):
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let cardsHorizontalController = CardsHorizontalController()
self.view.addSubview(cardsHorizontalController.view)
cardsHorizontalController.view.translatesAutoresizingMaskIntoConstraints = false
cardsHorizontalController.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100.0).isActive = true
cardsHorizontalController.view.heightAnchor.constraint(equalToConstant: 279).isActive = true
cardsHorizontalController.view.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
navigationController?.set_iOS12_lookAndFeel()
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationItem.largeTitleDisplayMode = .always
PeaxAPI.getPicture(PictureType.avatar) { [weak self] (error, image) in
guard let self = self else { return } // check if self still alive otherwise bail out
DispatchQueue.main.async {
if let image = image {
self.profileImageView.image = image
} else {
self.profileImageView.image = #imageLiteral(resourceName: "profile-placeholder-small")
}
self.profileImageView.contentMode = .scaleAspectFill
if let profile = PeaxAPI.profile, let person = profile.person {
self.profileName = "\(person.firstName ?? "") \(person.lastName ?? "")"
}
let titleStackView = UIStackView(frame: CGRect(origin: .zero, size: CGSize(width: self.view.bounds.width, height: 88.0)))
titleStackView.isUserInteractionEnabled = true
titleStackView.translatesAutoresizingMaskIntoConstraints = false
titleStackView.axis = .horizontal
titleStackView.alignment = .bottom
titleStackView.spacing = 10.0
let labelWidth: CGFloat = UIScreen.main.bounds.width - 16.0 - 10.0 - 36.0 - 16.0 // FullScreenWidth minus (Leading + Spacing + ButtonWidth + Trailing)
let label = UILabel()
label.font = AppConstants.Font.NavBar_TitleFont
label.text = self.profileName
label.textColor = .white
label.tintColor = .white
label.widthAnchor.constraint(equalToConstant: labelWidth).isActive = true
label.translatesAutoresizingMaskIntoConstraints = false
let buttonWidth: CGFloat = 36.0
let button = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: buttonWidth, height: buttonWidth)))
button.setImage(self.profileImageView.image, for: .normal)
button.isUserInteractionEnabled = true
button.addTarget(self, action: #selector(self.callProfileBtnMethod), for: .touchUpInside)
button.frame = CGRect(x: 0, y: 0, width: 36, height: 36)
button.layer.cornerRadius = button.frame.size.width / 2
button.layer.masksToBounds = false
button.clipsToBounds = true
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
button.heightAnchor.constraint(equalToConstant: buttonWidth).isActive = true
titleStackView.addArrangedSubview(label)
titleStackView.addArrangedSubview(button)
let cockpitHeaderView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: self.view.bounds.width, height: 88.0)))
cockpitHeaderView.isUserInteractionEnabled = true
cockpitHeaderView.addSubview(titleStackView)
titleStackView.leadingAnchor.constraint(equalTo: cockpitHeaderView.leadingAnchor, constant: 16.0).isActive = true
titleStackView.topAnchor.constraint(equalTo: cockpitHeaderView.topAnchor).isActive = true
titleStackView.trailingAnchor.constraint(equalTo: cockpitHeaderView.trailingAnchor, constant: -16.0).isActive = true
titleStackView.bottomAnchor.constraint(equalTo: cockpitHeaderView.bottomAnchor).isActive = true
// replace NavBar title by custom cockpitHeaderView
self.title = ""
self.navigationItem.titleView = cockpitHeaderView
cockpitHeaderView.sizeToFit()
}
}
}

I had to add addTarget after adding the button as a subview and that worked

Related

swift tap gesture handler not invoked

I have the following custom view implementation :
import UIKit
class ProfileTableHeaderView: UITableViewHeaderFooterView {
private var statusText : String = ""
private let fullNameLabel: UILabel = {
let view = UILabel()
view.text = "Hipster Pinguin"
view.font = UIFont.systemFont(ofSize: 18, weight: .bold)
view.textColor = .black
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let avatarImage: UIImageView = {
let view = UIImageView()
view.clipsToBounds = true
view.layer.borderWidth = 3
view.layer.borderColor = UIColor.white.cgColor
view.image = UIImage(named: "avatar")
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 100/2
view.translatesAutoresizingMaskIntoConstraints = false
// let tapGesture = UITapGestureRecognizer(target : self, action : #selector(avatarImagePressHandler))
// view.isUserInteractionEnabled = true
// view.addGestureRecognizer(tapGesture)
return view
}()
let statusLabel: UILabel = {
let view = UILabel()
view.text = "Waiting for something"
view.font = UIFont.systemFont(ofSize: 14, weight: .regular)
view.textColor = .gray
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let statusTextField: UITextField = {
let view = TextFieldWithPadding()
view.placeholder = "add smth to show as status"
view.layer.cornerRadius = 12
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.black.cgColor
view.backgroundColor = .white
view.font = UIFont.systemFont(ofSize: 15, weight: .regular)
view.textColor = .black
view.backgroundColor = .white.withAlphaComponent(0)
view.addTarget(self, action: #selector(statusTextChanged), for : .editingChanged)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let setStatusButton: UIButton = {
let view = UIButton()
view.setTitle("Show status", for: .normal)
view.setTitleColor(.white, for : .normal)
view.backgroundColor = UIColor(named: "myColor")
view.layer.cornerRadius = 14
view.layer.shadowRadius = 4
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.7
view.layer.shadowOffset = CGSize(width: 4, height: 4)
view.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
setupViews()
}
#objc func avatarImagePressHandler()
{
print("avatar pressed")
}
#objc func buttonPressed()
{
statusLabel.text = statusText
}
#objc func statusTextChanged(_ textField: UITextField)
{
statusText = textField.text ?? ""
}
required init?(coder: NSCoder) {
fatalError("should not be called")
}
private func setupViews()
{
contentView.addSubview(avatarImage)
contentView.addSubview(fullNameLabel)
contentView.addSubview(statusLabel)
contentView.addSubview(statusTextField)
contentView.addSubview(setStatusButton)
let tapGesture = UITapGestureRecognizer(target : self, action : #selector(avatarImagePressHandler))
avatarImage.isUserInteractionEnabled = true
avatarImage.addGestureRecognizer(tapGesture)
let constraints = [
avatarImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
avatarImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
avatarImage.widthAnchor.constraint(equalToConstant: 100),
avatarImage.heightAnchor.constraint(equalToConstant: 100),
fullNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 27),
fullNameLabel.leadingAnchor.constraint(equalTo: avatarImage.trailingAnchor, constant: 16),
fullNameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
statusLabel.topAnchor.constraint(equalTo: fullNameLabel.bottomAnchor, constant: 10),
statusLabel.leadingAnchor.constraint(equalTo: fullNameLabel.leadingAnchor),
statusLabel.trailingAnchor.constraint(equalTo: fullNameLabel.trailingAnchor),
statusTextField.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 10),
statusTextField.heightAnchor.constraint(equalToConstant: 40),
statusTextField.leadingAnchor.constraint(equalTo: statusLabel.leadingAnchor),
statusTextField.trailingAnchor.constraint(equalTo: statusLabel.trailingAnchor),
setStatusButton.topAnchor.constraint(equalTo: avatarImage.bottomAnchor, constant: 16),
setStatusButton.leadingAnchor.constraint(equalTo: avatarImage.leadingAnchor),
setStatusButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
setStatusButton.heightAnchor.constraint(equalToConstant: 50)
]
NSLayoutConstraint.activate(constraints)
}
}
If I try to setup a tap gesture recognizer inside lambda I see no print inside the console, but if I configure it inside setupViews everything is fine. Why does it work this way? What am I missing?

Why don't my selector functions get called when I clicked on my radio buttons in my Xcode Swift project

I have a custom UIView class called SortView with two radio buttons and neither of their respective selector functions are being called when I click on them. I have added an instance of the SortView class to a parent class with view.addView(). Here is my custom class:
class SortView: UIView {
// MARK: - Properties
lazy var driverSortRadioButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(named: "Radio Button - Unselected"), for: .normal)
button.setDimensions(height: 25, width: 25)
button.backgroundColor = .clear
button.contentMode = .scaleAspectFill
button.addTarget(self, action: #selector(handleDriverSortRadioButton), for: .touchUpInside)
return button
}()
lazy var pickupTimeSortRadioButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage(named: "Radio Button - Unselected"), for: .normal)
button.setDimensions(height: 25, width: 25)
button.backgroundColor = .clear
button.contentMode = .scaleAspectFill
button.addTarget(self, action: #selector(handlePickupTimeSortRadioButton), for: .touchUpInside)
return button
}()
private let driverSortTitleLabel: UILabel = {
let label = UILabel()
label.text = "Sort by driver:"
label.textAlignment = .left
label.textColor = .white
label.font = UIFont(name: "AvenirNext-DemiBold", size: 18)
label.backgroundColor = .clear
return label
}()
private let pickupTimeSortTitleLabel: UILabel = {
let label = UILabel()
label.text = "Sort by pickup time:"
label.textAlignment = .left
label.textColor = .white
label.font = UIFont(name: "AvenirNext-DemiBold", size: 18)
label.backgroundColor = .clear
return label
}()
private let driverSortTextField: UITextField = {
let tf = UITextField()
tf.textColor = .black
tf.textAlignment = .center
tf.placeholder = "Louise"
tf.font = UIFont(name: "AvenirNext-DemiBold", size: 15)
tf.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
tf.setWidth(width: 150)
tf.layer.cornerRadius = 5
return tf
}()
private let pickupTimeSortTextField: UITextField = {
let tf = UITextField()
tf.textColor = .black
tf.textAlignment = .center
tf.placeholder = "2:00pm"
tf.font = UIFont(name: "AvenirNext-DemiBold", size: 15)
tf.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
tf.setWidth(width: 150)
tf.layer.cornerRadius = 5
return tf
}()
private enum radioButtonStates {
case driver
case pickupTime
}
private var radioButtonState = radioButtonStates.driver
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Selectors
#objc func handleDriverSortRadioButton() {
print("DEBUG: driver sort radio button clicked")
if radioButtonState != .driver {
pickupTimeSortRadioButton.setImage(UIImage(named: "Radio Button - Unselected"), for: .normal)
pickupTimeSortTextField.isEnabled = false
pickupTimeSortTextField.text = ""
radioButtonState = .driver
driverSortRadioButton.setImage(UIImage(named: "Radio Button - Selected"), for: .normal)
driverSortTextField.isEnabled = true
driverSortTextField.text = ""
}
}
#objc func handlePickupTimeSortRadioButton() {
print("DEBUG: pickup time sort radio button clicked")
if radioButtonState != .pickupTime {
driverSortRadioButton.setImage(UIImage(named: "Radio Button - Unselected"), for: .normal)
driverSortTextField.isEnabled = false
driverSortTextField.text = ""
radioButtonState = .pickupTime
pickupTimeSortRadioButton.setImage(UIImage(named: "Radio Button - Selected"), for: .normal)
pickupTimeSortTextField.isEnabled = true
pickupTimeSortTextField.text = ""
}
}
// MARK: - Helper Functions
private func configureUI() {
let driverSortStackView = UIStackView(arrangedSubviews: [driverSortRadioButton,
driverSortTitleLabel,
driverSortTextField])
driverSortStackView.axis = .horizontal
driverSortStackView.distribution = .fill
driverSortStackView.spacing = 5
let pickupTimeSortStackView = UIStackView(arrangedSubviews: [pickupTimeSortRadioButton,
pickupTimeSortTitleLabel,
pickupTimeSortTextField])
pickupTimeSortStackView.axis = .horizontal
pickupTimeSortStackView.distribution = .fill
pickupTimeSortStackView.spacing = 5
let stackView = UIStackView(arrangedSubviews:[driverSortStackView,
pickupTimeSortStackView])
stackView.axis = .vertical
stackView.distribution = .fill
stackView.spacing = 10
self.addSubview(stackView)
stackView.centerX(inView: self)
stackView.centerY(inView: self)
}
}
The functions which are not being called are handleDriverSortRadioButton and handlePickupTimeSortRadioButton. Here is instantiation:
private lazy var sortView = SortView()
and here is where I use it in my parent UIViewController class:
view.addSubview(sortView)
sortView.anchor(top:tableView.bottomAnchor,
left: view.leftAnchor,
right: view.rightAnchor,
paddingTop: 40,
paddingLeft: 32,
paddingRight: 32)
I'm going to guess that the problem is that a containing view (perhaps one of the stack views) has zero size. The result would be that its subviews are visible but not tappable.
Here is a debugging utility method you can use to track down this sort of thing:
extension UIView {
#objc func reportSuperviews(filtering:Bool = true) {
var currentSuper : UIView? = self.superview
print("reporting on \(self)\n")
while let ancestor = currentSuper {
let ok = ancestor.bounds.contains(ancestor.convert(self.frame, from: self.superview))
let report = "it is \(ok ? "inside" : "OUTSIDE") \(ancestor)\n"
if !filtering || !ok { print(report) }
currentSuper = ancestor.superview
}
}
}
Wait until your interface is all set up and the buttons are untappable, and then call that on one of the untappable buttons to get a report in the console.
Fixed it by adding with and height to the bottom stack view
sortView.anchor(top:tableView.bottomAnchor,
left: view.leftAnchor,
right: view.rightAnchor,
paddingTop: 40,
paddingLeft: 32,
paddingRight: 32,
width: view.frame.width,
height: 100)

textField will not move in programable scrollview

I have a UIScrollView. I have a UITextField and a UILabel. For some reason my UITextField it will not move in my scroll view but my UILablel will. I have added the UITextField the same way as I added my UILabel to the scrollview using scrollView.addSubview(textField). Anyone see what I am doing wrong on this one?
import Foundation
import UIKit
//here
protocol AddContactDelegate {
func addContact(contact: Contact)
}
class AddContactController: UIViewController {
//delegate
var delegate: AddContactDelegate?
//TextField
let textField: UITextField = {
let tf = UITextField()
tf.placeholder = "Add Your Workout Title"
tf.textAlignment = .center
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
override func viewDidLoad() {
super.viewDidLoad()
//making scroll view
let screensize: CGRect = UIScreen.main.bounds
let screenWidth = screensize.width
let screenHeight = screensize.height
var scrollView: UIScrollView!
scrollView = UIScrollView(frame: CGRect(x: 0, y: 120, width: screenWidth, height: screenHeight))
scrollView.contentSize = CGSize(width: screenWidth, height: 2000)
view.addSubview(scrollView)
//setting up how view looks-----
view.backgroundColor = .white
//top buttons
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDone))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCancel))
//view elements
view.addSubview(textField)
scrollView.addSubview(textField)
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
textField.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
textField.widthAnchor.constraint(equalToConstant: view.frame.width - 64).isActive = true
textField.becomeFirstResponder()
//excercise label
let excerciseNumber = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 21))
excerciseNumber.center = CGPoint(x: view.frame.width / 2 , y: view.frame.height / 20)
excerciseNumber.textAlignment = .center
excerciseNumber.text = "Workout Title"
self.view.addSubview(excerciseNumber)
scrollView.addSubview(excerciseNumber)
}
//done button
#objc func handleDone(){
print("done")
guard let fullname = textField.text, textField.hasText else {
print("handle error here")
return
}
let contact = Contact(fullname: fullname, hello: "hi")
delegate?.addContact(contact: contact)
print(contact.fullname)
print(contact.hello)
}
//cancel button
#objc func handleCancel(){
self.dismiss(animated: true, completion: nil )
}
}
As you make the center x and y of the textfield with view not scrollView , You need
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textField.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
textField.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
textField.widthAnchor.constraint(equalToConstant: view.frame.width - 64)
])
Also remove
view.addSubview(textField)
self.view.addSubview(excerciseNumber)

How do I prevent one UIView from being hidden by another UIView?

I'm creating a custom, reusable segmented controller using UIViews and I'm having a problem with overlapping views. It currently looks like this:
You can see that the blue selector is under the buttons but I want it to sit at the bottom and be four pixels high. To do this, I have:
let numberOfButtons = CGFloat(buttonTitles.count)
let selectorWidth = frame.width / numberOfButtons
let selectorYPosition = frame.height - 3 <--- This cause it to be hidden behind the button
selector = UIView(frame: CGRect(x: 0, y: selectorYPosition, width: selectorWidth, height: 4))
selector.layer.cornerRadius = 0
selector.backgroundColor = selectorColor
addSubview(selector)
bringSubviewToFront(selector) <--- I thought this would work but it does nothing
which results in the selector UIView being hidden behind the segment UIView (I have the Y position set to - 3 so you can see how it's being covered up. I actually want it to be - 4, but that makes it disappear entirely):
I thought using bringSubviewToFront() would bring it in front of the segment UIView but it doesn't seem to do anything. I've looked through Apple View Programming Guide and lots of SO threads but can't find an answer.
Can anybody help me see what I'm missing?
Full code:
class CustomSegmentedControl: UIControl {
var buttons = [UIButton]()
var selector: UIView!
var selectedButtonIndex = 0
var borderWidth: CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
var borderColor: UIColor = UIColor.black {
didSet {
layer.borderColor = borderColor.cgColor
}
}
var separatorBorderColor: UIColor = UIColor.lightGray {
didSet {
}
}
var commaSeparatedTitles: String = "" {
didSet {
updateView()
}
}
var textColor: UIColor = .lightGray {
didSet {
updateView()
}
}
var selectorColor: UIColor = .blue {
didSet {
updateView()
}
}
var selectorTextColor: UIColor = .black {
didSet {
updateView()
}
}
func updateView() {
buttons.removeAll()
subviews.forEach { $0.removeFromSuperview() }
// create buttons
let buttonTitles = commaSeparatedTitles.components(separatedBy: ",")
for buttonTitle in buttonTitles {
let button = UIButton(type: .system)
button.setTitle(buttonTitle, for: .normal)
button.setTitleColor(textColor, for: .normal)
button.backgroundColor = UIColor.white
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
buttons.append(button)
}
// make first button selected
buttons[0].setTitleColor(selectorTextColor, for: .normal)
let numberOfButtons = CGFloat(buttonTitles.count)
let selectorWidth = frame.width / numberOfButtons
let selectorYPosition = frame.height - 3
selector = UIView(frame: CGRect(x: 0, y: selectorYPosition, width: selectorWidth, height: 4))
selector.layer.cornerRadius = 0
selector.backgroundColor = selectorColor
addSubview(selector)
bringSubviewToFront(selector)
let stackView = UIStackView(arrangedSubviews: buttons)
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fillEqually
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
stackView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
}
#objc func buttonTapped(button: UIButton) {
for (buttonIndex, btn) in buttons.enumerated() {
btn.setTitleColor(textColor, for: .normal)
if btn == button {
let numberOfButtons = CGFloat(buttons.count)
let selectorStartPosition = frame.width / numberOfButtons * CGFloat(buttonIndex)
UIView.animate(withDuration: 0.3, animations: { self.selector.frame.origin.x = selectorStartPosition })
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
sendActions(for: .valueChanged)
}
}
You are covering up your selector with the stackView.
You need to do:
bringSubviewToFront(selector)
after you have added all of the views. Move that line to the bottom of updateView().

Trying to make sense of UILabel behavior.

I am trying to replicate Apple's calculator UI layout.
Here is a gif of what I have so far.
The problems that I am encountering mostly have to do with the UILables.
As seen in the gif above, I am experiencing the following problems:
On device rotation, the labels "L1" and "L2" pop, instead of transitioning smoothly.
The labels on the brown colored buttons disappear when transitioning back to portrait.
For the labels "L1" and "L2" I have tried experimenting with the content mode and constraints, however, I still get clunky transitions.
As for the disappearing labels, instead of hiding/unhiding the stack view to make the layout appear and disappear via it's is hidden property, I instead tried using constraints on the stack view to handle the transition, however, the results remain the same.
I have also looked online and tried some suggestions, however, most answers were outdated or simply did not work.
The code is very straight forward, it primarily consists of setting up the views and its constraints.
extension UIStackView {
convenience init(axis: UILayoutConstraintAxis, distribution: UIStackViewDistribution = .fill) {
self.init()
self.axis = axis
self.distribution = distribution
self.translatesAutoresizingMaskIntoConstraints = false
}
}
class Example: UIView {
let mainStackView = UIStackView(axis: .vertical, distribution: .fill)
let subStackView = UIStackView(axis: .horizontal, distribution: .fillProportionally)
let portraitStackView = UIStackView(axis: .vertical, distribution: .fillEqually)
let landscapeStackView = UIStackView(axis: .vertical, distribution: .fillEqually)
var containerView: UIView = {
$0.backgroundColor = .darkGray
$0.translatesAutoresizingMaskIntoConstraints = false
return $0
}(UIView(frame: .zero))
let mainView: UIView = {
$0.backgroundColor = .blue
$0.translatesAutoresizingMaskIntoConstraints = false
return $0
}(UIView(frame: .zero))
let labelView: UIView = {
$0.backgroundColor = .red
$0.translatesAutoresizingMaskIntoConstraints = false
return $0
}(UIView(frame: .zero))
var labelOne: UILabel!
var labelTwo: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
autoresizingMask = [.flexibleWidth, .flexibleHeight]
labelOne = createLabel(text: "L1")
labelOne.translatesAutoresizingMaskIntoConstraints = false
labelOne.backgroundColor = .darkGray
labelTwo = createLabel(text: "L2")
labelTwo.translatesAutoresizingMaskIntoConstraints = false
labelTwo.backgroundColor = .black
landscapeStackView.isHidden = true
mainView.addSubview(labelView)
labelView.addSubview(labelOne)
labelView.addSubview(labelTwo)
addSubview(mainStackView)
mainStackView.addArrangedSubview(mainView)
setButtonStackView()
setStackViewConstriants()
setDisplayViewConstriants()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setStackViewConstriants() {
mainStackView.translatesAutoresizingMaskIntoConstraints = false
mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true
mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
func setDisplayViewConstriants() {
mainView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 288/667).isActive = true
labelView.heightAnchor.constraint(equalTo: mainView.heightAnchor, multiplier: 128/288).isActive = true
labelView.centerYAnchor.constraint(equalTo: mainView.centerYAnchor).isActive = true
labelView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 24).isActive = true
labelView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor, constant: -24).isActive = true
labelOne.heightAnchor.constraint(equalTo: labelTwo.heightAnchor, multiplier: 88/32).isActive = true
labelOne.trailingAnchor.constraint(equalTo: labelView.trailingAnchor).isActive = true
labelOne.leadingAnchor.constraint(equalTo: labelView.leadingAnchor).isActive = true
labelOne.topAnchor.constraint(equalTo: labelView.topAnchor).isActive = true
labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor).isActive = true
labelTwo.trailingAnchor.constraint(equalTo: labelView.trailingAnchor).isActive = true
labelTwo.leadingAnchor.constraint(equalTo: labelOne.leadingAnchor).isActive = true
labelTwo.bottomAnchor.constraint(equalTo: labelView.bottomAnchor).isActive = true
}
func createLabel(text: String) -> UILabel {
let label = UILabel(frame: .zero)
label.text = text
label.font = UIFont.init(name: "Arial-BoldMT", size: 60)
label.textColor = .white
label.textAlignment = .right
label.contentMode = .right
label.minimumScaleFactor = 0.1
label.adjustsFontSizeToFitWidth = true
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
func createButton(text: String) -> UIButton {
let button = UIButton(type: .custom)
button.setTitle(text, for: .normal)
button.setTitleColor(.white, for: .normal)
button.layer.borderColor = UIColor.white.cgColor
button.layer.borderWidth = 1
button.titleLabel?.font = UIFont.init(name: "Arial-BoldMT", size: 60)
button.titleLabel?.minimumScaleFactor = 0.1
button.titleLabel?.adjustsFontSizeToFitWidth = true
button.titleLabel?.translatesAutoresizingMaskIntoConstraints = false
button.titleLabel?.leadingAnchor.constraint(equalTo: button.leadingAnchor).isActive = true
button.titleLabel?.trailingAnchor.constraint(equalTo: button.trailingAnchor).isActive = true
button.titleLabel?.topAnchor.constraint(equalTo: button.topAnchor).isActive = true
button.titleLabel?.bottomAnchor.constraint(equalTo: button.bottomAnchor).isActive = true
button.titleLabel?.textAlignment = .center
button.titleLabel?.contentMode = .scaleAspectFill
button.titleLabel?.numberOfLines = 0
return button
}
func setButtonStackView() {
for _ in 1...5 {
let stackView = UIStackView(axis: .horizontal, distribution: .fillEqually)
for _ in 1...4 {
let button = createButton(text: "0")
button.backgroundColor = .brown
stackView.addArrangedSubview(button)
}
landscapeStackView.addArrangedSubview(stackView)
}
for _ in 1...5 {
let stackView = UIStackView(axis: .horizontal, distribution: .fillEqually)
for _ in 1...4 {
let button = createButton(text: "0")
button.backgroundColor = .purple
stackView.addArrangedSubview(button)
}
portraitStackView.addArrangedSubview(stackView)
}
subStackView.addArrangedSubview(landscapeStackView)
subStackView.addArrangedSubview(portraitStackView)
mainStackView.addArrangedSubview(subStackView)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
if UIDevice.current.orientation.isLandscape && landscapeStackView.isHidden == true {
self.landscapeStackView.isHidden = false
}
if UIDevice.current.orientation.isPortrait && landscapeStackView.isHidden == false {
self.landscapeStackView.isHidden = true
}
self.layoutIfNeeded()
}
}
Overview:
Do things incrementally with separate components / view controllers (easier to debug)
The below solution is only for labels L1 and L2.
For the calculator buttons, it would be best to use a UICollectionViewController. (I haven't implemented it, add as a child view controller)
Code:
private func setupLabels() {
view.backgroundColor = .red
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
let label1 = UILabel()
label1.text = "L1"
label1.textColor = .white
label1.backgroundColor = .darkGray
label1.textAlignment = .right
label1.font = UIFont.preferredFont(forTextStyle: .title1)
let label2 = UILabel()
label2.text = "L2"
label2.textColor = .white
label2.backgroundColor = .black
label2.textAlignment = .right
label2.font = UIFont.preferredFont(forTextStyle: .caption1)
stackView.addArrangedSubview(label1)
stackView.addArrangedSubview(label2)
}