Open Mandala.xcodeproj and create a new Swift file named ImageSelector. Define a new UIControl subclass within this file.
Listing 18.1 Creating the ImageSelector
class (ImageSelector.swift
)
import Foundationimport UIKit class ImageSelector: UIControl { }
The interface for this control will be set up much like the existing stack view of buttons. The primary difference, in terms of code, is that the ImageSelector will not be tied directly to the array of emoji images. Instead, it will hold on to an arbitrary array of images, allowing the control to be flexible and reusable.
Let’s start re-creating the interface. Add a property for a horizontal stack view and configure some of its attributes.
Listing 18.2 Adding a stack view property (ImageSelector.swift
)
class ImageSelector: UIControl { private let selectorStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal stackView.distribution = .fillEqually stackView.alignment = .center stackView.spacing = 12.0 stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() }
This stack view is an implementation detail of the ImageSelector type. In other words, no other types need to know about this property. To keep other files from being able to access selectorStackView, the property has been marked as private
.
This is called access control. Access control allows you to define what can access the properties and methods on your types. There are five levels of access control that can be applied to types, properties, and methods:
Now, implement a method that will configure the view hierarchy for the control.
Listing 18.3 Configuring the view hierarchy (ImageSelector.swift
)
private func configureViewHierarchy() { addSubview(selectorStackView) NSLayoutConstraint.activate([ selectorStackView.leadingAnchor.constraint(equalTo: leadingAnchor), selectorStackView.trailingAnchor.constraint(equalTo: trailingAnchor), selectorStackView.topAnchor.constraint(equalTo: topAnchor), selectorStackView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) }
The control should be able to be created either programmatically or within an interface file (such as a storyboard), and the view hierarchy needs to be configured in both cases. Override the initializer used for both of these situations and call the method you just created to configure the view hierarchy.
Listing 18.4 Overriding the control initializers (ImageSelector.swift
)
override init(frame: CGRect) { super.init(frame: frame) configureViewHierarchy() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configureViewHierarchy() }
Next, add properties to manage the images, buttons, and selected index. Also add the method that will be called when a button is tapped. This code will be nearly identical to the code in MoodSelectionViewController.
Listing 18.5 Adding properties to manage the images (ImageSelector.swift
)
var selectedIndex = 0 private var imageButtons: [UIButton] = [] { didSet { oldValue.forEach { $0.removeFromSuperview() } imageButtons.forEach { selectorStackView.addArrangedSubview($0)} } } var images: [UIImage] = [] { didSet { imageButtons = images.map { image in let imageButton = UIButton() imageButton.setImage(image, for: .normal) imageButton.imageView?.contentMode = .scaleAspectFit imageButton.adjustsImageWhenHighlighted = false imageButton.addTarget(self, action: #selector(imageButtonTapped(_:)), for: .touchUpInside) return imageButton } selectedIndex = 0 } } @objc private func imageButtonTapped(_ sender: UIButton) { guard let buttonIndex = imageButtons.firstIndex(of: sender) else { preconditionFailure("The buttons and images are not parallel.") } selectedIndex = buttonIndex }
The imageButtons property stores the images. When it is set, it creates and updates the array of buttons. This, in turn, updates the stack view to remove the existing buttons and add the new buttons.
When a button is tapped, the control needs to signal that its value has changed. To accomplish this, you call the sendActions(for:) method on the control, passing in the type of event that has occurred.
Update imageButtonTapped(_:) to send the associated actions.
Listing 18.6 Sending control event actions (ImageSelector.swift
)
@objc private func imageButtonTapped(_ sender: UIButton) { guard let buttonIndex = imageButtons.firstIndex(of: sender) else { preconditionFailure("The buttons and images are not parallel.") } selectedIndex = buttonIndex sendActions(for: .valueChanged) }
The .valueChanged event is one of the UIControl.Events that were discussed in Chapter 5. UISwitch, UISlider, and UISegmentedControl are common controls that utilize the .valueChanged event.
The sendActions(for:) method will look through all the target-action pairs that have been registered with this control for the specified event (in this case, .valueChanged) and will call the action method on that target. All this is being handled for you by the UIControl superclass. Later in this chapter, you will register the MoodSelectionViewController as a target-action pair with the control and associate it with the .valueChanged control event.
The control is now ready for use, so let’s update the view controller to take advantage of this control.