Apple Reference, Articles, and Opinions

Creating a custom UIView

When subclassing a UIView, you should take into consideration the use of your class. It is relatively straightforward to create a UIView Subclass that presents other non-interactive elements, however when creating a UIView subclass that contains more than one interactive element, some care should be taken to detect taps in the element that you intend.

For this example, we want to create a view that has a UIPickerView, a UILabel, and a UIButton. This might be useful for creating a custom UIPickerView that has a title and a dismiss button like so

Custom Picker View

The first step is to create a subclass of UIView with the desired UIKit objects, in this case a UIPickerView, UILabel, and UIButton

class CustomViewWithPicker: UIView {
    
    let picker = UIPickerView(frame: .zero)
    let pickerTitle = UILabel(frame: .zero)
    let button = UIButton(frame: .zero)
    
    let title: String = "Picker Title"
    let buttonName: String = "Button"
    
    // Programmatic initialization
    override init(frame: CGRect) {
        super.init(frame: frame)
        didLoad()
    }
    
    // Loading from XIB
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        didLoad()
    }
    
    func didLoad() {
        
        self.addSubview(picker)
        self.addSubview(pickerTitle)
        self.addSubview(button)
        
        picker.backgroundColor = .tertiarySystemBackground
        picker.layer.cornerRadius = 20
        picker.frame = .zero
        
        pickerTitle.text = title
        pickerTitle.font = .boldSystemFont(ofSize: 22)
        pickerTitle.textAlignment = .center
        pickerTitle.backgroundColor = .tertiarySystemBackground
        
        button.setTitle(buttonName, for: .normal)
        button.contentHorizontalAlignment = .right
        button.contentVerticalAlignment = .top
        button.isSelected = true
        
        self.updateConstraints()
    }
    
    // This is necessary to detect which UIKit element will respond to touch events
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if self.point(inside: point, with: event) {
            return super.hitTest(point, with: event)
        }
        guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
            return nil
        }
        
        for subview in subviews.reversed() {
            let convertedPoint = subview.convert(point, from: self)
            if let hitView = subview.hitTest(convertedPoint, with: event) {
                return hitView
            }
        }
        return nil
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
    }
    
    override func updateConstraints() {
        // Create Constraints
        self.translatesAutoresizingMaskIntoConstraints = false
		
		picker.translatesAutoresizingMaskIntoConstraints = false
		picker.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor).isActive = true
		picker.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor).isActive = true
		
		pickerTitle.translatesAutoresizingMaskIntoConstraints = false
		pickerTitle.heightAnchor.constraint(equalToConstant: soloButtonHeight).isActive = true
		pickerTitle.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
		pickerTitle.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 4).isActive = true
		pickerTitle.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -4).isActive = true

		button.translatesAutoresizingMaskIntoConstraints = false
		button.trailingAnchor.constraint(equalTo: self.pickerTitle.trailingAnchor, constant: -15).isActive = true
		button.centerYAnchor.constraint(equalTo: self.pickerTitle.centerYAnchor, constant: -10).isActive = true
    }
}

You must override the hitTest method and return the appropriate view

After that, in our ViewController, we must conform to the UIPickerDelegate and UIPickerViewDataSource to be able to populate the UIPickerView with data

class MyViewController : UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UIGestureRecognizerDelegate {
    
    let customView = CustomViewWithPicker()
    let labels = ["label0", "label1", "label2", "label3", "label4", "label5"]
    var selectedRow = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        
        customView.picker.delegate = self
        customView.picker.dataSource = self

        customView.button.addTarget(self, action: #selector(doneButtonTapped(_:)), for: .touchUpInside)
        
        self.view.addSubview(customView)
        
    }
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return labels.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return labels[row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectedRow = row
    }
    
    @objc func doneButtonTapped(_ selectedButton: UIButton) {
        if selectedButton.isSelected {
            print("Done Button Tapped")
        }
    }
}

That's it! You now have a custom UIView with a UIPickerView and button that you respond to user input.

Are you in need of help developing your iOS or macOS app? Do you need a custom app built from the ground up for yourself or your business? I am available for hire! Just contact me by sending me an email here or filling out the contact form on my business website

With gratitude,
Javier Solorzano