Checking bars indicator

This commit is contained in:
Christiano, Kevin 2021-05-24 14:53:31 +00:00 committed by Pfeil, Scott Robert
parent 345fb971cc
commit d90e68e263
4 changed files with 228 additions and 123 deletions

View File

@ -14,6 +14,37 @@ open class BarsIndicatorView: CarouselIndicator {
// MARK: - Stored Properties
//--------------------------------------------------
/// Represents the data of each individual bar.
private class IndicatorBar: View {
// Dimensions are based on InVision Design Guidelines.
static let width: CGFloat = 24
static let selectedHeight: CGFloat = 4
static let unselectedHeight: CGFloat = 1
var constraint: NSLayoutConstraint?
init() {
super.init(frame: .zero)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public required init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
fatalError("init(model:_:_:) has not been implemented")
}
override func setupView() {
super.setupView()
isAccessibilityElement = true
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccTabHint")
widthAnchor.constraint(equalToConstant: BarsIndicatorView.IndicatorBar.width).isActive = true
accessibilityTraits = .button
}
}
/// Structured container to hold and layout the indicator bars.
public let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
@ -25,11 +56,8 @@ open class BarsIndicatorView: CarouselIndicator {
return stackView
}()
public var barReferences: [(view: View, constraint: NSLayoutConstraint)] = []
// Dimensions are based on InVision Design Guidelines.
public static let indicatorBarWidth: CGFloat = 24
public static let indicatorBarHeight: (selected: CGFloat, unselected: CGFloat) = (selected: 4, unselected: 1)
/// Reference to each bar displayed.
private var barReferences: [IndicatorBar] = []
//--------------------------------------------------
// MARK: - Computed Properties
@ -37,16 +65,16 @@ open class BarsIndicatorView: CarouselIndicator {
/// Convenience to access the model.
public var barsCarouselIndicatorModel: BarsCarouselIndicatorModel? {
return model as? BarsCarouselIndicatorModel
model as? BarsCarouselIndicatorModel
}
open override var isEnabled: Bool {
didSet {
for (i, bar) in barReferences.enumerated() {
for (i, indicatorBar) in barReferences.enumerated() {
if i == currentIndex {
bar.view.backgroundColor = isEnabled ? currentIndicatorColor : disabledIndicatorColor
indicatorBar.backgroundColor = isEnabled ? currentIndicatorColor : disabledIndicatorColor
} else {
bar.view.backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor
indicatorBar.backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor
}
}
}
@ -54,21 +82,22 @@ open class BarsIndicatorView: CarouselIndicator {
/// Colors the currently selected index, unique from other indicators
public var currentIndicatorColor: UIColor {
get { return barsCarouselIndicatorModel?.currentIndicatorColor.uiColor ?? indicatorColor }
set (newColor) {
barsCarouselIndicatorModel?.currentIndicatorColor = Color(uiColor: newColor)
get { barsCarouselIndicatorModel?.currentIndicatorColor.uiColor ?? indicatorColor }
set {
barsCarouselIndicatorModel?.currentIndicatorColor = Color(uiColor: newValue)
if isEnabled && !barReferences.isEmpty {
barReferences[currentIndex].view.backgroundColor = newColor
barReferences[currentIndex].backgroundColor = newValue
}
}
}
/// The color of the indicator bars.
public override var indicatorColor: UIColor {
get { return super.indicatorColor }
set (newColor) {
super.indicatorColor = newColor
refreshBarColors(with: newColor)
get { super.indicatorColor }
set {
super.indicatorColor = newValue
refreshBarColors(with: newValue)
}
}
@ -84,7 +113,7 @@ open class BarsIndicatorView: CarouselIndicator {
isAccessibilityElement = false
NSLayoutConstraint.activate([
stackView.heightAnchor.constraint(equalToConstant: 4),
stackView.heightAnchor.constraint(equalToConstant: Padding.One),
heightAnchor.constraint(equalTo: stackView.heightAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
@ -97,44 +126,95 @@ open class BarsIndicatorView: CarouselIndicator {
// MARK: - Methods
//--------------------------------------------------
/// Updates the color of all indicator bars.
private func refreshBarColors(with color: UIColor) {
if isEnabled {
for (i, barTuple) in barReferences.enumerated() {
barTuple.view.backgroundColor = i == currentIndex ? currentIndicatorColor : color
}
guard isEnabled else { return }
for (i, indicatorBar) in barReferences.enumerated() {
indicatorBar.backgroundColor = i == currentIndex ? currentIndicatorColor : color
}
}
/// Creates all the indicato bars for display.
func generateBars() {
var bars = [(View, NSLayoutConstraint)]()
var bars = [IndicatorBar]()
for i in 0..<numberOfPages {
let bar = View()
bar.isAccessibilityElement = true
if let accessibleValueFormat = accessibilityValueFormat, let accessibleIndex = MVMCoreUIUtility.getOrdinalString(forIndex: NSNumber(value: i + 1)) {
bar.accessibilityLabel = String(format: accessibleValueFormat, accessibleIndex, numberOfPages)
}
bar.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccTabHint")
bar.accessibilityTraits = .button
bar.widthAnchor.constraint(equalToConstant: BarsIndicatorView.indicatorBarWidth).isActive = true
bar.backgroundColor = isEnabled ? (i == currentIndex ? currentIndicatorColor : indicatorColor) : disabledIndicatorColor
let barHeight = i == currentIndex ? BarsIndicatorView.indicatorBarHeight.selected : BarsIndicatorView.indicatorBarHeight.unselected
let heightConstraint = bar.heightAnchor.constraint(equalToConstant: barHeight)
heightConstraint.isActive = true
stackView.addArrangedSubview(bar)
bars.append((bar, heightConstraint))
let indicatorBar = createIndicatorBar(index: i)
stackView.addArrangedSubview(indicatorBar)
bars.append(indicatorBar)
}
accessibilityElements = stackView.arrangedSubviews
barReferences = bars
}
/// Removes an indicator bar from the view.
private func removeBar() {
let lastView = barReferences.removeLast()
stackView.removeArrangedSubview(lastView)
lastView.removeFromSuperview()
accessibilityElements = stackView.arrangedSubviews
}
/// Based of the sign of the difference indicator bars will be removed or added.
private func balanceBarCount(_ difference: Int) {
if difference > 0 {
// If positive, add n bars.
var copyBars = barReferences
for _ in 0..<difference {
let indicatorBar = createIndicatorBar(index: barReferences.count)
indicatorBar.constraint?.constant = IndicatorBar.unselectedHeight
stackView.addArrangedSubview(indicatorBar)
copyBars.append(indicatorBar)
}
accessibilityElements = stackView.arrangedSubviews
barReferences = copyBars
} else if difference < 0 {
// If negative, remove n bars.
for _ in 0..<(difference * -1) {
removeBar()
}
}
}
private func createIndicatorBar(index: Int) -> IndicatorBar {
let bar = IndicatorBar()
setAccessibilityLabel(view: bar, index: index)
bar.backgroundColor = isEnabled ? (index == currentIndex ? currentIndicatorColor : indicatorColor) : disabledIndicatorColor
let barHeight = index == currentIndex ? BarsIndicatorView.IndicatorBar.selectedHeight : BarsIndicatorView.IndicatorBar.unselectedHeight
let heightConstraint = bar.heightAnchor.constraint(equalToConstant: barHeight)
heightConstraint.isActive = true
bar.constraint = heightConstraint
return bar
}
/// Refreshes the accessibility labels to read "x of n".
private func refreshAccessibilityLabels() {
for i in 0..<barReferences.count {
setAccessibilityLabel(view: barReferences[i], index: i)
}
}
private func setAccessibilityLabel(view: UIView, index: Int) {
guard let accessibleValueFormat = accessibilityValueFormat,
let accessibleIndex = MVMCoreUIUtility.getOrdinalString(forIndex: NSNumber(value: index + 1))
else { return }
view.accessibilityLabel = String(format: accessibleValueFormat, accessibleIndex, numberOfPages)
}
public override func assessTouchOf(_ touchPoint_X: CGFloat) {
currentIndex = barReferences.firstIndex { $0.0.frame.maxX >= touchPoint_X && $0.0.frame.minX <= touchPoint_X } ?? 0
currentIndex = barReferences.firstIndex { $0.frame.maxX >= touchPoint_X && $0.frame.minX <= touchPoint_X } ?? 0
performAction()
}
@ -152,27 +232,40 @@ open class BarsIndicatorView: CarouselIndicator {
public override func reset() {
super.reset()
barReferences.forEach { $0.view.removeFromSuperview() }
barReferences.forEach { $0.removeFromSuperview() }
barReferences = []
}
public override func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) {
guard newIndex < totalCount else { return }
// Ensure that at least one bar exists.
guard !barReferences.isEmpty else {
generateBars()
return
}
let expression = {
self.barReferences[previousIndex].view.backgroundColor = self.isEnabled ? self.indicatorColor : self.disabledIndicatorColor
self.barReferences[newIndex].view.backgroundColor = self.isEnabled ? self.currentIndicatorColor : self.disabledIndicatorColor
self.barReferences[previousIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.unselected
self.barReferences[newIndex].constraint.constant = BarsIndicatorView.indicatorBarHeight.selected
self.layoutIfNeeded()
guard newIndex < totalCount else { return }
// Ensure the number of pages matches the number of bar references.
if (totalCount - barReferences.count) != 0 {
barReferences.forEach {
$0.backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor
$0.constraint?.constant = IndicatorBar.unselectedHeight
}
isAnimated ? UIView.animate(withDuration: 0.3) { expression() } : expression()
balanceBarCount(numberOfPages - barReferences.count)
refreshAccessibilityLabels()
}
let expression = { [self] in
barReferences[previousIndex].backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor
barReferences[previousIndex].constraint?.constant = IndicatorBar.unselectedHeight
barReferences[newIndex].backgroundColor = isEnabled ? currentIndicatorColor : disabledIndicatorColor
barReferences[newIndex].constraint?.constant = IndicatorBar.selectedHeight
layoutIfNeeded()
}
// Perform the animation.
isAnimated ? UIView.animate(withDuration: 0.25) { expression() } : expression()
}
}

View File

@ -6,8 +6,6 @@
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class CarouselIndicator: Control, CarouselPageControlProtocol {
//--------------------------------------------------
@ -25,7 +23,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
/// Convenience to access the model.
public var carouselIndicatorModel: CarouselIndicatorModel? {
return model as? CarouselIndicatorModel
model as? CarouselIndicatorModel
}
/// Set this closure to perform an action when a different indicator was selected.
@ -43,7 +41,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
private(set) var previousIndex = 0
public var currentIndex: Int {
get { return carouselIndicatorModel?.currentIndex ?? 0 }
get { carouselIndicatorModel?.currentIndex ?? 0 }
set (newIndex) {
previousIndex = currentIndex
carouselIndicatorModel?.currentIndex = newIndex
@ -58,22 +56,22 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
/// Holds the total number of pages displayed by the carousel.
/// Updating this property will potentially update the UI.
public var numberOfPages: Int {
get { return carouselIndicatorModel?.numberOfPages ?? 0 }
set (newTotal) {
guard numberOfPages != newTotal else { return }
get { carouselIndicatorModel?.numberOfPages ?? 0 }
set {
guard numberOfPages != newValue else { return }
carouselIndicatorModel?.numberOfPages = newTotal
carouselIndicatorModel?.numberOfPages = newValue
reset()
isHidden = (carouselIndicatorModel?.hidesForSinglePage ?? false) && newTotal <= 1
isHidden = (carouselIndicatorModel?.hidesForSinglePage ?? false) && newValue <= 1
updateUI(previousIndex: previousIndex,
newIndex: currentIndex,
totalCount: newTotal,
totalCount: newValue,
isAnimated: carouselIndicatorModel?.animated ?? true)
}
}
public var disabledIndicatorColor: UIColor {
get { return carouselIndicatorModel?.disabledIndicatorColor.uiColor ?? .mvmCoolGray3 }
get { carouselIndicatorModel?.disabledIndicatorColor.uiColor ?? .mvmCoolGray3 }
set { carouselIndicatorModel?.disabledIndicatorColor = Color(uiColor: newValue) }
}
@ -92,7 +90,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
}
var accessibilityValueFormat: String? {
return MVMCoreUIUtility.hardcodedString(withKey: (carouselIndicatorModel?.accessibilityHasSlidesInsteadOfPage ?? false) ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index")
MVMCoreUIUtility.hardcodedString(withKey: (carouselIndicatorModel?.accessibilityHasSlidesInsteadOfPage ?? false) ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index")
}
//--------------------------------------------------
@ -175,12 +173,14 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
assessTouchOf(touchPoint_X)
}
/// Determines where a touch in the indicator translate to an index selection.
func assessTouchOf(_ touchPoint_X: CGFloat) { }
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
/// Called to update the Indicator UI with the latest values.
open func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) { }
public func performAction() {
@ -200,6 +200,7 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
guard let model = model as? CarouselIndicatorModel else { return }
previousIndex = 0
indicatorColor = model.inverted ? model.indicatorColor_inverted.uiColor : model.indicatorColor.uiColor
disabledIndicatorColor = model.disabledIndicatorColor.uiColor
currentIndex = model.currentIndex
@ -214,12 +215,10 @@ open class CarouselIndicator: Control, CarouselPageControlProtocol {
//--------------------------------------------------
open override func accessibilityIncrement() {
adjustAccessibility(toPage: currentIndex + 1)
}
open override func accessibilityDecrement() {
adjustAccessibility(toPage: currentIndex - 1)
}

View File

@ -47,7 +47,7 @@ open class NumericIndicatorView: CarouselIndicator {
/// Sets the color for pageCount text, left arrow and right arrow.
public override var indicatorColor: UIColor {
get { return super.indicatorColor }
get { super.indicatorColor }
set (newColor) {
super.indicatorColor = newColor

View File

@ -53,7 +53,7 @@ open class Carousel: View {
public var collectionViewHeight: NSLayoutConstraint?
/// The view that we use for paging
public var pagingView: (UIView & CarouselPageControlProtocol)?
public var pagingView: (MoleculeViewProtocol & CarouselPageControlProtocol)?
/// If the carousel should loop after scrolling past the first and final cells.
public var loop = false
@ -207,12 +207,25 @@ open class Carousel: View {
pageIndex = 0
}
var pagingMoleculeName: String?
/// Sets up the paging molecule
open func setupPagingMolecule(_ molecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?, delegateObject: MVMCoreUIDelegateObject?) {
var pagingView: (UIView & CarouselPageControlProtocol)? = nil
if let molecule = molecule,
molecule.moleculeName == pagingMoleculeName {
pagingView?.set(with: molecule, delegateObject, nil)
pagingView?.numberOfPages = numberOfPages
return
}
var pagingView: (MoleculeViewProtocol & CarouselPageControlProtocol)? = nil
if let molecule = molecule,
(!molecule.hidesForSinglePage || numberOfPages > 1) {
pagingView = ModelRegistry.createMolecule(molecule, delegateObject: delegateObject) as? (UIView & CarouselPageControlProtocol)
pagingView = ModelRegistry.createMolecule(molecule, delegateObject: delegateObject) as? (MoleculeViewProtocol & CarouselPageControlProtocol)
pagingMoleculeName = molecule.moleculeName
} else {
pagingMoleculeName = nil
}
addPaging(view: pagingView, position: molecule?.position ?? 20)
@ -239,7 +252,7 @@ open class Carousel: View {
}
/// Adds a paging view. Centers it horizontally with the collection view. The position is the vertical distance from the center of the page view to the bottom of the collection view.
open func addPaging(view: (UIView & CarouselPageControlProtocol)?, position: CGFloat) {
open func addPaging(view: (MoleculeViewProtocol & CarouselPageControlProtocol)?, position: CGFloat) {
pagingView?.removeFromSuperview()
bottomPin?.isActive = false