Creating a Custom Control

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 Foundation
import 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:

open

Used only for classes and mostly by framework or third-party library authors. Anything can access this class, property, or method. Additionally, classes marked as open can be subclassed, and methods marked as open can be overridden outside of the module.

public

Very similar to open, but public classes can only be subclassed and public methods can only be overridden inside (not outside of) the module.

internal

The default level. Anything in the current module can access this type, property, or method. For an app, only files within the same project can access internal types, properties, and methods. If you write a third-party library, then only files within that third-party library can access them – apps that use your third-party library cannot.

fileprivate

Anything in the same source file can see this type, property, or method.

private

Anything within the enclosing scope can access this type, property, or method.

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.

Relaying actions

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset