Home iOS & Swift Books Auto Layout by Tutorials

18
Designing Custom Controls Written by Jayven Nhan

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Standard UIKit controls are native, intuitive, and they support out-the-box features for iOS users. However, as you develop your app, you might discover that the standard controls limit you to a specific set of features that don’t meet your app’s requirements. Whether you want to implement a different user interface layout or some non-standard user interaction, understanding how to create custom controls will help you achieve the desired results.

Depending on the level of customizations you want to make, creating custom controls can require a substantial amount of work. The more customizations or features you want to add, the more work you’ll need to do. But that’s not all you need to think about. Your approach to making a custom control also dictates the amount of work you have ahead of you.

To make a custom UIKit control, you have two options: You can subclass a standard control like a UIButton. Or, you can subclass a more generic class like UIView or UIControl. Each approach has its pros and cons, which you’ll discover as you build your first custom control.

In this chapter, you’ll create a custom DJ deck user interface. In doing so, you’ll learn the following topics:

  • Subclassing and making a custom UIView.
  • Visualizing XIB/NIB files in a storyboard.
  • Customizing user interfaces according to size classes.
  • Subclassing and making a custom UIButton.
  • Subclassing and making a custom UIControl.
  • Adopting accessibility features.

Getting started

You’ll start by subclassing a UIView to create a custom view. Open the starter project. To keep this chapter focused, you’ll use an existing view and NIB file, located inside the project.

For the custom DJ deck, the layout blueprint for landscape and portrait orientations is as follows:

For this control, you’ll implement the following custom user interfaces:

  • Backlit Button.
  • Disc Spinner View.
  • Pitch Control.

These three custom user interfaces make up the custom DJ deck interface.

Applying adaptive layout to a custom view

Open DJControllerView.xib in the Xibs group.

Laying out the stack view

First, you’ll handle the layout of the backlit buttons (three top-left buttons). Implement the following layout changes:

Laying out the pitch control

Next, you’ll work on the pitch control (top-right view). Implement the following layout changes to the pitch control:

Laying out the disc spinner view

Finally, you’ll set up the disc spinner view (bottom view). Implement the following layout changes to the disc spinner view:

Making user interface variations

To begin implementing user interface variations, you’ll need to first set your Interface Builder’s preview orientation to landscape so you’ll be able to see how things look in landscape.

Integrating a custom view from NIB to storyboard

Now that you’ve configured your custom view in the NIB, you’ll integrate it into the main storyboard.

Visualizing XIB/NIB files in storyboards

Build and run, and you’ll see an empty-looking view controller. The reason for this is because you need to initialize the NIB file inside of your custom control.

extension UIView {
  func instantiateNib<T: UIView>(view: T) -> UIView {
    // 1
    let type = T.self
    // 2
    let nibName = String(describing: type)
    // 3
    let bundle = Bundle(for: type)
    // 4
    let nib = UINib(nibName: nibName, bundle: bundle)
    // 5
    guard let view = nib.instantiate(
      withOwner: self,
      options: nil).first as? UIView
      else { fatalError("Failed to instantiate: \(nibName)") }
    // 6
    return view
  }
}
private lazy var view = instantiateNib(view: self)
addSubview(view)
view.fillSuperview(self)

Preparing custom views for Interface Builder

You can segregate the user interface in Interface Builder from runtime. In other words, you can customize your custom view to different user interfaces in Interface Builder and at runtime. You can use this feature, for example, to help other developers better understand your custom view.

extension UIView {
  func addLabelDescribing<T: UIView>(
    view: T,
    insideSuperview superview: UIView
  ) {
    // 1
    let viewDescriptionLabel = ViewDescriptionLabel()
    // 2
    viewDescriptionLabel.text = String(describing: T.self)
    // 3
    superview.addSubview(viewDescriptionLabel)
    // 4
    viewDescriptionLabel.center(superview)
  }
}
override func prepareForInterfaceBuilder() {
  super.prepareForInterfaceBuilder()
  addLabelDescribing(view: self, insideSuperview: view)
}

Making a custom UIButton

In this section, you’ll create another custom control by subclassing a standard UIKit control. In particular, you’ll work with a UIButton.

// 1
private func shrinkAnimation() {
  let scale: CGFloat = 0.9
  UIView.animate(withDuration: 0.2) { [weak self] in
    self?.transform = CGAffineTransform(
      scaleX: scale, 
      y: scale)
  }
}
// 2
private func resetAnimation() {
  UIView.animate(withDuration: 0.2) { [weak self] in
      self?.transform = .identity
  }
}
// 1
override func touchesBegan(
  _ touches: Set<UITouch>,
  with event: UIEvent?
) {
  super.touchesBegan(touches, with: event)
  shrinkAnimation()
}

// 2
override func touchesEnded(
  _ touches: Set<UITouch>,
  with event: UIEvent?
) {
  super.touchesEnded(touches, with: event)
  resetAnimation()
}

override func touchesCancelled(
  _ touches: Set<UITouch>,
  with event: UIEvent?
) {
  super.touchesCancelled(touches, with: event)
  resetAnimation()
}

Making a custom UIControl

Standard UIKit controls inherit from UIControl. Standard controls include UIButton, UISegmentedControl, UITextField, UISlider, UISwitch, UIPageControl and more.

Customizing size class variations

The knob will consist of only a static image with no user interactivity. Your job is to have it show up and hidden in the corresponding size classes.

private lazy var view = instantiateNib(view: self)

Implementing control features for vertical slider

The vertical slider will have an interactive slidable thumb that enables users to adjust the control’s value. In addition, the vertical slider will also implement Accessibility features.

Setting up data properties

Your control will store various data properties to implement different business and presentation logic. First, add the following properties to PitchControl:

// 1
private let minValue: CGFloat = 0
private let maxValue: CGFloat = 10
// 2
private var value: CGFloat = 1 {
  didSet {
    print("Value:", value)
  }
}
// 3
private var previousTouchLocation = CGPoint()
// 1
private var valueRange: CGFloat {
  return maxValue - minValue + 1
}
// 2
private var halfThumbImageViewHeight: CGFloat {
  return thumbImageView.bounds.height / 2
}
// 3
private var distancePerUnit: CGFloat {
  return (sliderBackgroundImageView.bounds.height / valueRange)
    - (halfThumbImageViewHeight / 2)
}

Implementing touch tracking handlers

The touch tracking handler methods derived from UIControl. You’ll now override the touch tracking handler methods to integrate custom logic into your project.

override func beginTracking(
  _ touch: UITouch,
  with event: UIEvent?
) -> Bool {
  super.beginTracking(touch, with: event)
  // 1
  previousTouchLocation = touch.location(in: self)
  // 2
  let isTouchingThumbImageView = thumbImageView.frame
    .contains(previousTouchLocation)
  // 3
  thumbImageView.isHighlighted = isTouchingThumbImageView
  // 4
  return isTouchingThumbImageView
}
override func continueTracking(
  _ touch: UITouch,
  with event: UIEvent?
) -> Bool {
  super.continueTracking(touch, with: event)
  // 1
  let touchLocation = touch.location(in: self)
  let deltaLocation = touchLocation.y
    - previousTouchLocation.y
  // 2
  let deltaValue = (maxValue - minValue)
    * deltaLocation / bounds.height
  // 3
  previousTouchLocation = touchLocation
}
// 1
value = boundValue(
  value + deltaValue,
  toLowerValue: minValue,
  andUpperValue: maxValue)
// 2
let isTouchingBackgroundImage =
  sliderBackgroundImageView.frame
    .contains(previousTouchLocation)
if isTouchingBackgroundImage {
  thumbImageViewTopConstraint.constant =
    touchLocation.y - self.halfThumbImageViewHeight
}
// 3
return true
override func endTracking(
  _ touch: UITouch?,
  with event: UIEvent?
) {
  super.endTracking(touch, with: event)
  thumbImageView.isHighlighted = false
}

Implementing accessibility features.

Especially on Apple’s platform, iOS users are accustomed to and expect Accessibility support, so your custom control should support Accessibility features. By implementing Accessibility features, you make your app usable by a larger audience. This section focuses on the implementation of Accessibility features and assumes that you have familiarity with using VoiceOver.

Setting up the basics

At the moment, you won’t even be able to select the custom control when using the app in assistive mode. Making the control selectable is one thing, but you’ll also need to make the control’s purpose clear. The assistive users shouldn’t need to spend time figuring out what to do or how to use your custom control.

private func setupAccessibilityElements() {
  // 1
  isAccessibilityElement = true
  // 2
  accessibilityLabel = "Pitch"
  // 3
  accessibilityTraits = [.adjustable]
  // 4
  accessibilityHint = "Adjust pitch"
}
setupAccessibilityElements()

Implementing value adjustability

Implementing value adjustability isn’t an easy task. When a VoiceOver user wants to change the value of your control, you’ll need to think about a value increment/decrement that fits your user’s criteria.

private let valueIncrement: CGFloat = 1
override var accessibilityValue: String? {
  get {
    return "\(Int(value))"
  }
  set {
    super.accessibilityValue = newValue
  }
}
fileprivate enum Direction {
  case up
  case down
}
private func slideThumbInDirection(_ direction: Direction) {
  // 1
  let valueChange: CGFloat
  switch direction {
  case .up:
    valueChange = valueIncrement
  case .down:
    valueChange = valueIncrement * -1
  }
  // 2
  let newValue = value + valueChange
  if newValue < minValue {
    value = minValue
  } else if newValue > maxValue {
    value = maxValue
  } else {
    value = newValue
  }
  // 3
  thumbImageViewTopConstraint.constant =
    value * distancePerUnit
}
override func accessibilityIncrement() {
  super.accessibilityIncrement()
  slideThumbInDirection(.down)
}

override func accessibilityDecrement() {
  super.accessibilityDecrement()
  slideThumbInDirection(.up)
}

Key points

  • Custom controls allow you to create interactive user interfaces to your app’s specifications.
  • Creating custom controls is fun; however, standard controls are intuitive to iOS users and support out-the-box application features, so use standard controls over custom controls whenever possible.
  • Custom controls aren’t complete without adopting accessibility features.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.