Merge branch 'develop' into feature/revised_accessibility

This commit is contained in:
Kevin G Christiano 2020-05-06 08:23:20 -04:00
commit 9ca88678c5
22 changed files with 1145 additions and 98 deletions

View File

@ -91,6 +91,12 @@
0A7EF86323D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift */; };
0A7EF86523D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */; };
0A7EF86723D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */; };
0A9D091D2433796500D2E6C0 /* BarsCarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D09172433796500D2E6C0 /* BarsCarouselIndicatorModel.swift */; };
0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D09182433796500D2E6C0 /* NumericCarouselIndicatorModel.swift */; };
0A9D091F2433796500D2E6C0 /* NumericIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D09192433796500D2E6C0 /* NumericIndicatorView.swift */; };
0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091A2433796500D2E6C0 /* BarsIndicatorView.swift */; };
0A9D09212433796500D2E6C0 /* CarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091B2433796500D2E6C0 /* CarouselIndicatorModel.swift */; };
0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */; };
0AA33B3A2398524F0067DD0F /* Toggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA33B392398524F0067DD0F /* Toggle.swift */; };
0AB764D124460F6300E7FE72 /* UIDatePicker+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB764D024460F6300E7FE72 /* UIDatePicker+Extension.swift */; };
0AB764D324460FA400E7FE72 /* UIPickerView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB764D224460FA400E7FE72 /* UIPickerView+Extension.swift */; };
@ -480,6 +486,7 @@
0A6682B4243769C700AD3CA1 /* TextViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewModel.swift; sourceTree = "<group>"; };
0A69F610241BDEA700F7231B /* RuleAnyRequiredModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleAnyRequiredModel.swift; sourceTree = "<group>"; };
0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownEntryField.swift; sourceTree = "<group>"; };
0A7918F423F5E7EA00772FF4 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyButton.swift; sourceTree = "<group>"; };
0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxLabel.swift; sourceTree = "<group>"; };
@ -494,6 +501,12 @@
0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDropdownEntryFieldModel.swift; sourceTree = "<group>"; };
0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateDropdownEntryFieldModel.swift; sourceTree = "<group>"; };
0A8321AE2355FE9500CB7F00 /* DigitBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigitBox.swift; sourceTree = "<group>"; };
0A9D09172433796500D2E6C0 /* BarsCarouselIndicatorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsCarouselIndicatorModel.swift; sourceTree = "<group>"; };
0A9D09182433796500D2E6C0 /* NumericCarouselIndicatorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericCarouselIndicatorModel.swift; sourceTree = "<group>"; };
0A9D09192433796500D2E6C0 /* NumericIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumericIndicatorView.swift; sourceTree = "<group>"; };
0A9D091A2433796500D2E6C0 /* BarsIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsIndicatorView.swift; sourceTree = "<group>"; };
0A9D091B2433796500D2E6C0 /* CarouselIndicatorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarouselIndicatorModel.swift; sourceTree = "<group>"; };
0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarouselIndicator.swift; sourceTree = "<group>"; };
0AA33B33239813C50067DD0F /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = "<group>"; };
0AA33B392398524F0067DD0F /* Toggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toggle.swift; sourceTree = "<group>"; };
0AB764D024460F6300E7FE72 /* UIDatePicker+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDatePicker+Extension.swift"; sourceTree = "<group>"; };
@ -891,6 +904,19 @@
path = Protocols;
sourceTree = "<group>";
};
0A9D09162433796500D2E6C0 /* CarouselIndicator */ = {
isa = PBXGroup;
children = (
0A9D091B2433796500D2E6C0 /* CarouselIndicatorModel.swift */,
0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */,
0A9D09172433796500D2E6C0 /* BarsCarouselIndicatorModel.swift */,
0A9D091A2433796500D2E6C0 /* BarsIndicatorView.swift */,
0A9D09182433796500D2E6C0 /* NumericCarouselIndicatorModel.swift */,
0A9D09192433796500D2E6C0 /* NumericIndicatorView.swift */,
);
path = CarouselIndicator;
sourceTree = "<group>";
};
0ABD1369237B18EE0081388D /* Views */ = {
isa = PBXGroup;
children = (
@ -1012,13 +1038,6 @@
path = Label;
sourceTree = "<group>";
};
94FB5B83238D892800EB2193 /* Recovered References */ = {
isa = PBXGroup;
children = (
);
name = "Recovered References";
sourceTree = "<group>";
};
AA4FC2A323F4F69600E251DB /* RightVariable */ = {
isa = PBXGroup;
children = (
@ -1350,7 +1369,6 @@
D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */,
D29DF0CD21E404D4003B2FB9 /* Products */,
D29DF0E421E4F3C7003B2FB9 /* Frameworks */,
94FB5B83238D892800EB2193 /* Recovered References */,
);
sourceTree = "<group>";
};
@ -1570,6 +1588,7 @@
D29DF17D21E69E26003B2FB9 /* Views */ = {
isa = PBXGroup;
children = (
0A9D09162433796500D2E6C0 /* CarouselIndicator */,
9445890B2385BCE300DE9FD4 /* ProgressBarModel.swift */,
01509D922327ECFB00EF99AA /* ProgressBar.swift */,
9445890D2385C3F800DE9FD4 /* MultiProgressModel.swift */,
@ -1740,6 +1759,7 @@
D21B7F70243BAC1600051ABF /* CollectionViewCell.swift */,
D264FAA92440F97600D98315 /* CollectionView.swift */,
0A5D59C323AD488600EFD9E9 /* Protocols */,
0A7918F423F5E7EA00772FF4 /* ImageView.swift */,
);
path = BaseClasses;
sourceTree = "<group>";
@ -1926,6 +1946,7 @@
D21B7F602437C5BC00051ABF /* MoleculeStackView.swift in Sources */,
0A6682A42434DB8D00AD3CA1 /* ListLeftVariableRadioButtonBodyTextModel.swift in Sources */,
AA2AD116244EE46800BBFFE3 /* ListDeviceComplexLinkMedium.swift in Sources */,
0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */,
D2E2A99423D8CCBC000B42E6 /* HeadlineBodyLinkModel.swift in Sources */,
01004F3022721C3800991ECC /* RadioButton.swift in Sources */,
D268C70E238C22D7007F2C1C /* DropDownFilterTableViewCell.swift in Sources */,
@ -1985,6 +2006,7 @@
D29DF27621E79E81003B2FB9 /* MVMCoreUILoggingHandler.m in Sources */,
C695A69623C990BC00BFB94E /* DoughnutChart.swift in Sources */,
014AA72D23C5059B006F3E93 /* StackPageTemplateModel.swift in Sources */,
0A9D091F2433796500D2E6C0 /* NumericIndicatorView.swift in Sources */,
D260106123D0C02A00764D80 /* StackItemModelProtocol.swift in Sources */,
0AE98BAF23FEF956004C5109 /* ExternalLink.swift in Sources */,
012A88C4238D86E600FE3DA1 /* CarouselItemModelProtocol.swift in Sources */,
@ -2022,9 +2044,11 @@
D2A514672213885800345BFB /* MoleculeHeaderView.swift in Sources */,
D29E28D823D21AB800ACEA85 /* StringAndMoleculeView.swift in Sources */,
01EB369023609801006832FA /* MoleculeListItemModel.swift in Sources */,
D28A838323CCBD3F00DFE4FC /* WheelModel.swift in Sources */,
0A9D09212433796500D2E6C0 /* CarouselIndicatorModel.swift in Sources */,
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */,
D28A838323CCBD3F00DFE4FC /* WheelModel.swift in Sources */,
D268C70C2386DFFD007F2C1C /* MoleculeStackItemModel.swift in Sources */,
0A9D091D2433796500D2E6C0 /* BarsCarouselIndicatorModel.swift in Sources */,
DBEFFA04225A829700230692 /* Label.swift in Sources */,
D2D6CD4022E78C1A00D701B8 /* Scroller.swift in Sources */,
0A7ECC5D243CE85300C828E8 /* DoughnutChartItemModel.swift in Sources */,
@ -2061,6 +2085,7 @@
D29DF2EF21ECEAE1003B2FB9 /* MFFonts.m in Sources */,
D22479942316AE5E003FCCF9 /* NSLayoutConstraintExtension.swift in Sources */,
D2B18B94236214AD00A9AEDC /* NavigationController.swift in Sources */,
0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */,
D29E28DA23D21AFA00ACEA85 /* StringAndMoleculeModel.swift in Sources */,
D260105D23D0BCD400764D80 /* Stack.swift in Sources */,
0A7EF85D23D8A95600B2AAD1 /* TextEntryFieldModel.swift in Sources */,
@ -2111,6 +2136,7 @@
BB2C969224330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift in Sources */,
D2D90B42240463E100DD6EC9 /* MoleculeHeaderModel.swift in Sources */,
012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */,
0A9D091E2433796500D2E6C0 /* NumericCarouselIndicatorModel.swift in Sources */,
D29DF2C921E7BFC6003B2FB9 /* MFSizeObject.m in Sources */,
0A6682B6243769C700AD3CA1 /* TextViewModel.swift in Sources */,
9445890E2385C3F800DE9FD4 /* MultiProgressModel.swift in Sources */,

View File

@ -8,6 +8,7 @@
import UIKit
open class Arrow: View {
//--------------------------------------------------
// MARK: - Properties
@ -19,6 +20,41 @@ open class Arrow: View {
return model as? ArrowModel
}
public var direction: ArrowModel.Direction {
get { return ArrowModel.Direction(rawValue: degrees) ?? .right}
set { degrees = newValue.rawValue }
}
open var isEnabled: Bool = true {
didSet {
arrowModel?.enabled = isEnabled
isUserInteractionEnabled = isEnabled
if isEnabled != oldValue {
setNeedsDisplay()
}
}
}
open var disabledColor: UIColor {
get { return arrowModel?.disabledColor.uiColor ?? .mvmCoolGray3 }
set { arrowModel?.disabledColor = Color(uiColor: newValue) }
}
open var color: UIColor {
get { return arrowModel?.color.uiColor ?? .mvmBlack }
set { arrowModel?.color = Color(uiColor: newValue) }
}
open var degrees: Float {
get { return arrowModel?.degrees ?? 0 }
set { arrowModel?.degrees = newValue }
}
open var lineWidth: CGFloat {
get { return arrowModel?.lineWidth ?? 1 }
set { arrowModel?.lineWidth = newValue }
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
@ -34,6 +70,18 @@ open class Arrow: View {
widthConstraint?.isActive = true
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: frame)
}
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
@ -51,33 +99,32 @@ open class Arrow: View {
// MARK: - Drawing
//--------------------------------------------------
/// Draws the arrow pointing to the right and then rotates the arrow x degrees counter-clockwise.
open override func draw(_ rect: CGRect) {
super.draw(rect)
arrowLayer.transform = CATransform3DIdentity
drawShapeLayer()
if let degrees = arrowModel?.degrees {
let radians = CGFloat(degrees * Float.pi / 180)
arrowLayer.transform = CATransform3DMakeRotation(-radians, 0.0, 0.0, 1.0)
}
let radians = CGFloat(degrees * Float.pi / 180)
arrowLayer.transform = CATransform3DMakeRotation(-radians, 0, 0, 1)
}
private func drawShapeLayer() {
arrowLayer.frame = bounds
arrowLayer.strokeColor = arrowModel?.color.cgColor
arrowLayer.strokeColor = isEnabled ? color.cgColor : disabledColor.cgColor
arrowLayer.fillColor = UIColor.clear.cgColor
arrowLayer.path = arrowPath()
arrowLayer.lineJoin = .miter
arrowLayer.lineCap = .butt
arrowLayer.lineWidth = arrowModel?.lineWidth ?? 1
arrowLayer.lineWidth = lineWidth
}
private func arrowPath() -> CGPath {
let length = max(bounds.size.height, bounds.size.width)
let inset = (arrowModel?.lineWidth ?? 1) / 2
let inset = lineWidth / 2
let midLength = length / 2
var startPoint = CGPoint(x: midLength, y: inset)
@ -97,4 +144,16 @@ open class Arrow: View {
return bezierPath.cgPath
}
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? ArrowModel else { return }
isEnabled = model.enabled
}
}

View File

@ -8,20 +8,47 @@
import UIKit
open class ArrowModel: MoleculeModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "arrow"
public var backgroundColor: Color?
public static var identifier: String {
return "arrow"
}
public var moleculeName: String?
public var backgroundColor: Color?
public var disabledColor: Color = Color(uiColor: .mvmCoolGray3)
public var color: Color = Color(uiColor: .mvmBlack)
public var degrees: Float = 0
public var lineWidth: CGFloat = 1
public var height: CGFloat = 12
public var width: CGFloat = 12
public var enabled: Bool = true
//--------------------------------------------------
// MARK: - Enum
//--------------------------------------------------
/// Conveniece for readability of arrow pointing direction.
public enum Direction: Float {
case right = 0
case upperRight = 45
case up = 90
case upperLeft = 135
case left = 180
case bottomLeft = 225
case down = 270
case bottomRight = 315
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init() { }
//--------------------------------------------------
// MARK: - Keys
@ -30,12 +57,13 @@ open class ArrowModel: MoleculeModelProtocol {
private enum CodingKeys: String, CodingKey {
case moleculeName
case backgroundColor
case disabledColor
case color
case degrees
case size
case lineWidth
case height
case width
case enabled
}
//--------------------------------------------------
@ -44,12 +72,21 @@ open class ArrowModel: MoleculeModelProtocol {
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
if let disabledColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledColor) {
self.disabledColor = disabledColor
}
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) {
self.color = color
}
if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) {
self.enabled = enabled
}
if let degrees = try typeContainer.decodeIfPresent(Float.self, forKey: .degrees) {
self.degrees = degrees
}
@ -59,7 +96,7 @@ open class ArrowModel: MoleculeModelProtocol {
}
if let height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height) {
self.lineWidth = height
self.height = height
}
if let width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width) {
@ -69,12 +106,14 @@ open class ArrowModel: MoleculeModelProtocol {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(disabledColor, forKey: .disabledColor)
try container.encode(color, forKey: .color)
try container.encode(degrees, forKey: .degrees)
try container.encodeIfPresent(backgroundColor, forKey: .lineWidth)
try container.encode(lineWidth, forKey: .lineWidth)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
try container.encode(enabled, forKey: .enabled)
}
}

View File

@ -0,0 +1,50 @@
//
// BarsCarouselIndicatorModel.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 3/3/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import UIKit
open class BarsCarouselIndicatorModel: CarouselIndicatorModel {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public class override var identifier: String {
return "barsCarouselIndicator"
}
public var currentIndicatorColor: Color = Color(uiColor: .mvmBlack)
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey {
case currentIndicatorColor
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
public required init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
if let currentIndicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .currentIndicatorColor) {
self.currentIndicatorColor = currentIndicatorColor
}
try super.init(from: decoder)
}
public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(currentIndicatorColor, forKey: .currentIndicatorColor)
try super.encode(to: encoder)
}
}

View File

@ -0,0 +1,176 @@
//
// BarIndicatorView.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 2/3/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import UIKit
open class BarsIndicatorView: CarouselIndicator {
//--------------------------------------------------
// MARK: - Stored Properties
//--------------------------------------------------
public let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.isAccessibilityElement = false
stackView.axis = .horizontal
stackView.alignment = .bottom
stackView.distribution = .equalSpacing
stackView.spacing = 6
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)
//--------------------------------------------------
// MARK: - Computed Properties
//--------------------------------------------------
/// Convenience to access the model.
public var barsCarouselIndicatorModel: BarsCarouselIndicatorModel? {
return model as? BarsCarouselIndicatorModel
}
open override var isEnabled: Bool {
didSet {
for (i, bar) in barReferences.enumerated() {
if i == currentIndex {
bar.view.backgroundColor = isEnabled ? currentIndicatorColor : disabledIndicatorColor
} else {
bar.view.backgroundColor = isEnabled ? indicatorColor : disabledIndicatorColor
}
}
}
}
/// 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)
if isEnabled && !barReferences.isEmpty {
barReferences[currentIndex].view.backgroundColor = newColor
}
}
}
public override var indicatorColor: UIColor {
get { return super.indicatorColor }
set (newColor) {
super.indicatorColor = newColor
if isEnabled {
for (i, barTuple) in barReferences.enumerated() {
barTuple.view.backgroundColor = i == currentIndex ? currentIndicatorColor : newColor
}
}
}
}
//--------------------------------------------------
// MARK: - Setup
//--------------------------------------------------
open override func setupView() {
super.setupView()
addSubview(stackView)
isUserInteractionEnabled = false
isAccessibilityElement = false
NSLayoutConstraint.activate([
stackView.heightAnchor.constraint(equalToConstant: 4),
heightAnchor.constraint(equalTo: stackView.heightAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
trailingAnchor.constraint(equalTo: stackView.trailingAnchor)
])
}
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
func generateBars() {
var bars = [(View, NSLayoutConstraint)]()
let ordinalFormatter = NumberFormatter()
ordinalFormatter.numberStyle = .ordinal
for i in 0..<numberOfPages {
let bar = View()
bar.accessibilityTraits = .button
bar.isAccessibilityElement = true
if let accessibleValueFormat = accessibilityValueFormat, let accessibleIndex = ordinalFormatter.string(from: NSNumber(value: i + 1)) {
bar.accessibilityLabel = String(format: accessibleValueFormat, accessibleIndex, numberOfPages)
}
bar.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccTabHint")
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))
}
accessibilityElements = stackView.arrangedSubviews
barReferences = bars
}
public override func assessTouchOf(_ touchPoint_X: CGFloat) {
currentIndex = barReferences.firstIndex { $0.0.frame.maxX >= touchPoint_X && $0.0.frame.minX <= touchPoint_X } ?? 0
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? BarsCarouselIndicatorModel else { return }
currentIndicatorColor = model.currentIndicatorColor.uiColor
}
//--------------------------------------------------
// MARK: - IndicatorViewProtocol
//--------------------------------------------------
public override func reset() {
super.reset()
barReferences.forEach { $0.view.removeFromSuperview() }
barReferences = []
}
public override func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) {
guard newIndex < totalCount else { return }
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()
}
isAnimated ? UIView.animate(withDuration: 0.3) { expression() } : expression()
}
}

View File

@ -0,0 +1,246 @@
//
// CarouselIndicator.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 1/30/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class CarouselIndicator: Control, CarouselPageControlProtocol {
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
public var topConstraint: NSLayoutConstraint?
public var bottomConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public var uiGestures: Set<UIGestureRecognizer> = []
/// Convenience to access the model.
public var carouselIndicatorModel: CarouselIndicatorModel? {
return model as? CarouselIndicatorModel
}
/// Set this closure to perform an action when a different indicator was selected.
/// Passes through oldIndex and newIndex, respectively.
public var indicatorTouchAction: ((CarouselPageControlProtocol) -> ())?
open override var isEnabled: Bool {
didSet { isUserInteractionEnabled = isEnabled }
}
//--------------------------------------------------
// MARK: - Computed Properties
//--------------------------------------------------
private(set) var previousIndex = 0
public var currentIndex: Int {
get { return carouselIndicatorModel?.currentIndex ?? 0 }
set (newIndex) {
previousIndex = currentIndex
carouselIndicatorModel?.currentIndex = newIndex
if previousIndex != newIndex {
updateUI(previousIndex: previousIndex,
newIndex: newIndex,
totalCount: numberOfPages,
isAnimated: carouselIndicatorModel?.animated ?? true)
performAction()
}
}
}
/// 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 }
carouselIndicatorModel?.numberOfPages = newTotal
reset()
isHidden = (carouselIndicatorModel?.hidesForSinglePage ?? false) && newTotal <= 1
updateUI(previousIndex: previousIndex,
newIndex: currentIndex,
totalCount: newTotal,
isAnimated: carouselIndicatorModel?.animated ?? true)
}
}
public var disabledIndicatorColor: UIColor {
get { return carouselIndicatorModel?.disabledIndicatorColor.uiColor ?? .mvmCoolGray3 }
set { carouselIndicatorModel?.disabledIndicatorColor = Color(uiColor: newValue) }
}
public var indicatorColor: UIColor {
get { return carouselIndicatorModel?.indicatorColor.uiColor ?? .mvmBlack }
set { carouselIndicatorModel?.indicatorColor = Color(uiColor: newValue) }
}
var accessibilityValueFormat: String? {
return MVMCoreUIUtility.hardcodedString(withKey: (carouselIndicatorModel?.accessibilityHasSlidesInsteadOfPage ?? false) ? "MVMCoreUIPageControlslides_currentpage_index" : "MVMCoreUIPageControl_currentpage_index")
}
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
override init(frame: CGRect) {
super.init(frame: frame)
}
convenience override init() {
self.init(frame: .zero)
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func setupView() {
super.setupView()
isAccessibilityElement = true
accessibilityTraits = .adjustable
setupGestures()
}
//--------------------------------------------------
// MARK: - UITouch
//--------------------------------------------------
private func setupGestures() {
let tap = UITapGestureRecognizer(target: self, action: #selector(indicatorTapped))
let leftSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeLeft))
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(swipeRight))
leftSwipe.direction = .left
rightSwipe.direction = .right
addGestureRecognizer(tap)
addGestureRecognizer(leftSwipe)
addGestureRecognizer(rightSwipe)
uiGestures.insert(tap)
uiGestures.insert(leftSwipe)
uiGestures.insert(rightSwipe)
}
func incrementCurrentIndex() {
currentIndex = (currentIndex + 1) % numberOfPages
}
func decrementCurrentIndex() {
let newIndex = currentIndex - 1
currentIndex = newIndex < 0 ? numberOfPages - 1 : newIndex
}
/// Increments the currentIndex value.
@objc func swipeLeft() {
incrementCurrentIndex()
}
/// Decrement the currentIndex value
@objc func swipeRight() {
decrementCurrentIndex()
}
/// Handles tap logic for Indicator
@objc func indicatorTapped(_ tapGesture: UITapGestureRecognizer?) {
let touchPoint = tapGesture?.location(in: self)
let touchPoint_X = touchPoint?.x ?? 0.0
assessTouchOf(touchPoint_X)
}
func assessTouchOf(_ touchPoint_X: CGFloat) { }
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
open func updateUI(previousIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) { }
public func performAction() {
sendActions(for: .valueChanged)
indicatorTouchAction?(self)
}
public func scrollViewDidScroll(_ collectionView: UICollectionView) { }
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? CarouselIndicatorModel else { return }
indicatorColor = model.indicatorColor.uiColor
disabledIndicatorColor = model.disabledIndicatorColor.uiColor
currentIndex = model.currentIndex
isEnabled = model.enabled
formatAccessibilityValue(index: currentIndex + 1, total: numberOfPages)
}
//--------------------------------------------------
// MARK: - Accessibility
//--------------------------------------------------
open override func accessibilityIncrement() {
adjustAccessibility(toPage: currentIndex + 1)
}
open override func accessibilityDecrement() {
adjustAccessibility(toPage: currentIndex - 1)
}
func formatAccessibilityValue(index: Int, total: Int) {
let ordinalFormatter = NumberFormatter()
ordinalFormatter.numberStyle = .ordinal
guard let accessibleFormat = accessibilityValueFormat,
let accessibleIndex = ordinalFormatter.string(from: NSNumber(value: index))
else { return }
accessibilityValue = String(format: accessibleFormat, accessibleIndex, total)
}
func adjustAccessibility(toPage index: Int) {
formatAccessibilityValue(index: index, total: numberOfPages)
if (index < numberOfPages && index >= 0) || carouselIndicatorModel?.alwaysSendAction ?? false {
carouselIndicatorModel?.animated = false
previousIndex = currentIndex
currentIndex = index
performAction()
}
}
func setTopBottomSpace(constant: CGFloat) {
bottomConstraint?.constant = constant
topConstraint?.constant = constant
}
}

View File

@ -0,0 +1,119 @@
//
// CarouselIndicatorModel.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 2/3/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public class var identifier: String {
return ""
}
public var backgroundColor: Color?
public var moleculeName: String?
public var numberOfPages: Int = 0
/// Sets the current Index to focus on.
public var currentIndex: Int = 0
public var animated: Bool = true
public var hidesForSinglePage: Bool = false
/// Set true to make the accessibility value as "Slide #currentPage of #totalPage", otherwise will be "Page #currentPage of #totalPage", default is false
public var accessibilityHasSlidesInsteadOfPage: Bool = false
public var enabled: Bool = true
public var disabledIndicatorColor: Color = Color(uiColor: .mvmCoolGray3)
public var indicatorColor: Color = Color(uiColor: .mvmBlack)
public var position: Float?
/// Allows sendActions() to trigger even if index is already at min/max index.
public var alwaysSendAction = false
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey {
case moleculeName
case backgroundColor
case currentIndex
case numberOfPages
case alwaysSendAction
case animated
case hidesForSinglePage
case accessibilityHasSlidesInsteadOfPage
case enabled
case disabledIndicatorColor
case indicatorColor
case position
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
moleculeName = try typeContainer.decodeIfPresent(String.self, forKey: .moleculeName)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
if let currentIndex = try typeContainer.decodeIfPresent(Int.self, forKey: .currentIndex) {
self.currentIndex = currentIndex
}
if let alwaysSendAction = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysSendAction) {
self.alwaysSendAction = alwaysSendAction
}
if let position = try typeContainer.decodeIfPresent(Float.self, forKey: .position) {
self.position = position
}
if let animated = try typeContainer.decodeIfPresent(Bool.self, forKey: .animated) {
self.animated = animated
}
if let hidesForSinglePage = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidesForSinglePage) {
self.hidesForSinglePage = hidesForSinglePage
}
if let accessibilityHasSlidesInsteadOfPage = try typeContainer.decodeIfPresent(Bool.self, forKey: .accessibilityHasSlidesInsteadOfPage) {
self.accessibilityHasSlidesInsteadOfPage = accessibilityHasSlidesInsteadOfPage
}
if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) {
self.enabled = enabled
}
if let disabledIndicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledIndicatorColor) {
self.disabledIndicatorColor = disabledIndicatorColor
}
if let indicatorColor = try typeContainer.decodeIfPresent(Color.self, forKey: .indicatorColor) {
self.indicatorColor = indicatorColor
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encode(numberOfPages, forKey: .numberOfPages)
try container.encode(currentIndex, forKey: .currentIndex)
try container.encode(alwaysSendAction, forKey: .alwaysSendAction)
try container.encode(animated, forKey: .animated)
try container.encode(hidesForSinglePage, forKey: .hidesForSinglePage)
try container.encode(accessibilityHasSlidesInsteadOfPage, forKey: .accessibilityHasSlidesInsteadOfPage)
try container.encode(enabled, forKey: .enabled)
try container.encode(disabledIndicatorColor, forKey: .disabledIndicatorColor)
try container.encode(indicatorColor, forKey: .indicatorColor)
try container.encodeIfPresent(position, forKey: .position)
}
}

View File

@ -0,0 +1,20 @@
//
// NumericCarouselIndicatorModel.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 3/3/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import UIKit
open class NumericCarouselIndicatorModel: CarouselIndicatorModel {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public class override var identifier: String {
return "numericCarouselIndicator"
}
}

View File

@ -0,0 +1,126 @@
//
// NumericIndicatorView.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 2/3/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import UIKit
open class NumericIndicatorView: CarouselIndicator {
//--------------------------------------------------
// MARK: - Outlets
//--------------------------------------------------
/// Text to display the current count of total pages for viewing.
open var pageCount: Label = {
let label = Label.commonLabelB2(true)
label.isAccessibilityElement = false
label.setContentCompressionResistancePriority(.required, for: .vertical)
label.textAlignment = .center
return label
}()
let leftArrow: Arrow = {
let arrow = Arrow(model: ArrowModel(), nil, nil)
arrow.isAccessibilityElement = false
arrow.direction = .left
arrow.pinHeightAndWidth()
return arrow
}()
let rightArrow: Arrow = {
let arrow = Arrow(model: ArrowModel(), nil, nil)
arrow.pinHeightAndWidth()
return arrow
}()
//--------------------------------------------------
// MARK: - Computed Properties
//--------------------------------------------------
open override var isEnabled: Bool {
didSet { setViewColor(isEnabled ? indicatorColor : disabledIndicatorColor) }
}
/// Sets the color for pageCount text, left arrow and right arrow.
public override var indicatorColor: UIColor {
get { return super.indicatorColor }
set (newColor) {
super.indicatorColor = newColor
if isEnabled {
setViewColor(newColor)
}
}
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
open override func updateView(_ size: CGFloat) {
super.updateView(size)
pageCount.updateView(size)
}
//--------------------------------------------------
// MARK: - Setup
//--------------------------------------------------
open override func setupView() {
super.setupView()
accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "swipe_to_select_with_action_hint")
addSubview(pageCount)
addSubview(leftArrow)
addSubview(rightArrow)
NSLayoutConstraint.activate([
pageCount.centerXAnchor.constraint(equalTo: centerXAnchor),
pageCount.topAnchor.constraint(equalTo: topAnchor),
bottomAnchor.constraint(equalTo: pageCount.bottomAnchor),
leftArrow.centerYAnchor.constraint(equalTo: centerYAnchor),
rightArrow.centerYAnchor.constraint(equalTo: centerYAnchor),
leftArrow.leadingAnchor.constraint(equalTo: leadingAnchor),
pageCount.leadingAnchor.constraint(equalTo: leftArrow.trailingAnchor, constant: Padding.Two),
rightArrow.leadingAnchor.constraint(equalTo: pageCount.trailingAnchor, constant: Padding.Two),
trailingAnchor.constraint(equalTo: rightArrow.trailingAnchor)
])
}
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
public override func assessTouchOf(_ touchPoint_X: CGFloat) {
if touchPoint_X > bounds.width / 2 {
incrementCurrentIndex()
} else {
decrementCurrentIndex()
}
}
private func setViewColor(_ newColor: UIColor) {
pageCount.textColor = newColor
leftArrow.color = newColor
rightArrow.color = newColor
rightArrow.setNeedsDisplay()
leftArrow.setNeedsDisplay()
}
//--------------------------------------------------
// MARK: - IndicatorViewProtocol
//--------------------------------------------------
open override func updateUI(previousIndex oldIndex: Int, newIndex: Int, totalCount: Int, isAnimated: Bool) {
pageCount.text = "\(newIndex + 1)/\(totalCount)"
formatAccessibilityValue(index: newIndex + 1, total: totalCount)
layoutIfNeeded()
}
}

View File

@ -44,6 +44,10 @@ import UIKit
super.init(frame: .zero)
}
public override init(frame: CGRect) {
super.init(frame: frame)
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

View File

@ -137,6 +137,8 @@ import Foundation
// Other Organisms
MoleculeObjectMapping.shared()?.register(viewClass: Carousel.self, viewModelClass: CarouselModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: BarsIndicatorView.self, viewModelClass: BarsCarouselIndicatorModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: NumericIndicatorView.self, viewModelClass: NumericCarouselIndicatorModel.self)
// Designed List Items
MoleculeObjectMapping.shared()?.register(viewClass: ListLeftVariableIconWithRightCaret.self, viewModelClass: ListLeftVariableIconWithRightCaretModel.self)

View File

@ -8,11 +8,12 @@
import Foundation
@objcMembers public class ListOneColumnFullWidthTextAllTextAndLinks: TableViewCell {
//-----------------------------------------------------
// MARK: - Outlets
//-----------------------------------------------------
var stack: Stack<StackModel>
let eyebrow = Label.commonLabelB3(true)
let headline = Label.commonLabelH3(true)
@ -26,7 +27,7 @@ import Foundation
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
stack = Stack<StackModel>.createStack(with: [eyebrow, headline, subHeadline, body, link])
stack.stackModel?.spacing = 0
stack.stackModel?.molecules[4].spacing = 2
stack.stackModel?.molecules[4].spacing = 8
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
@ -37,13 +38,14 @@ import Foundation
//-----------------------------------------------------
// MARK: - View Lifecycle
//-----------------------------------------------------
override open func setupView() {
super.setupView()
addMolecule(stack)
stack.restack()
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?){
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? ListOneColumnFullWidthTextAllTextAndLinksModel else { return }
stack.updateContainedMolecules(with: [model.eyebrow,

View File

@ -10,17 +10,30 @@ import Foundation
@objcMembers public class CarouselItemModel: MoleculeCollectionItemModel, CarouselItemModelProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public override class var identifier: String {
return "carouselItem"
}
public var peakingUI: Bool?
public var peakingArrowColor: Color?
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey {
case peakingUI
case peakingArrowColor
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
peakingUI = try typeContainer.decodeIfPresent(Bool.self, forKey: .peakingUI)

View File

@ -21,11 +21,19 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco
case backgroundColor
}
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init(with moleculeModel: MoleculeModelProtocol) {
molecule = moleculeModel
super.init()
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
molecule = try typeContainer.decodeModel(codingKey: .molecule)

View File

@ -8,6 +8,15 @@
import UIKit
/// Contracts behavior between carousel and its page control.
public protocol CarouselPageControlProtocol {
var currentIndex: Int { get set }
var numberOfPages: Int { get set }
var indicatorTouchAction: ((CarouselPageControlProtocol) -> ())? { get set }
func scrollViewDidScroll(_ collectionView: UICollectionView)
}
open class Carousel: View {
public let collectionView = CollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
@ -30,7 +39,7 @@ open class Carousel: View {
/// The models for the molecules.
var molecules: [MoleculeModelProtocol]?
/// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%.
public var itemAlignment = UICollectionView.ScrollPosition.left
@ -41,10 +50,11 @@ open class Carousel: View {
public var collectionViewHeight: NSLayoutConstraint?
/// The view that we use for paging
public var pagingView: (UIView & MVMCoreUIPagingProtocol)?
public var pagingView: (UIView & CarouselPageControlProtocol)?
/// If the carousel should loop after scrolling past the first and final cells.
var loop = false
private var dragging = false
// For adding pager
@ -81,6 +91,8 @@ open class Carousel: View {
}
// MARK: - MVMCoreViewProtocol
//--------------------------------------------------
open override func setupView() {
super.setupView()
collectionView.dataSource = self
@ -103,16 +115,21 @@ open class Carousel: View {
layoutCollection()
}
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
// MARK: - MVMCoreUIMoleculeViewProtocol
//--------------------------------------------------
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
self.delegateObject = delegateObject
super.set(with: model, delegateObject, additionalData)
guard let carouselModel = model as? CarouselModel else { return }
collectionView.backgroundColor = backgroundColor
collectionView.layer.borderColor = backgroundColor?.cgColor
collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0
backgroundColor = .white
registerCells(with: carouselModel, delegateObject: delegateObject)
setupLayout(with: carouselModel)
prepareMolecules(with: carouselModel)
@ -129,60 +146,71 @@ open class Carousel: View {
setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject)
pageIndex = carouselModel.index
pagingView?.setPage(carouselModel.index)
pagingView?.currentIndex = carouselModel.index
collectionView.reloadData()
}
//--------------------------------------------------
// MARK: - JSON Setters
//--------------------------------------------------
/// Updates the layout being used
func setupLayout(with carouselModel: CarouselModel?) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = CGFloat(carouselModel?.spacing ?? 1)
layout.minimumInteritemSpacing = 0
collectionView.collectionViewLayout = layout
}
func prepareMolecules(with carouselModel: CarouselModel?) {
guard let newMolecules = carouselModel?.molecules else {
numberOfPages = 0
molecules = nil
return
}
numberOfPages = newMolecules.count
molecules = newMolecules
if carouselModel?.loop ?? false && newMolecules.count > 2 {
// Sets up the row data with buffer cells on each side (for illusion of endless scroll... also has one more buffer cell on each side in case we can peek that cell).
loop = true
molecules?.insert(newMolecules.last!, at: 0)
molecules?.insert(newMolecules[(newMolecules.count - 2)], at: 0)
molecules?.append(newMolecules.first!)
molecules?.append(newMolecules[1])
}
pageIndex = 0
}
/// Sets up the paging molecule
open func setupPagingMolecule(_ molecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?, delegateObject: MVMCoreUIDelegateObject?) {
var pagingView: (UIView & MVMCoreUIPagingProtocol)? = nil
var pagingView: (UIView & CarouselPageControlProtocol)? = nil
if let molecule = molecule {
pagingView = MoleculeObjectMapping.shared()?.createMolecule(molecule, delegateObject: delegateObject) as? (UIView & MVMCoreUIPagingProtocol)
pagingView = MoleculeObjectMapping.shared()?.createMolecule(molecule, delegateObject: delegateObject) as? (UIView & CarouselPageControlProtocol)
}
addPaging(view: pagingView, position: (CGFloat(molecule?.position ?? 20)))
addPaging(view: pagingView , position: (CGFloat(molecule?.position ?? 20)))
}
/// Registers the cells with the collection view
func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) {
for molecule in carouselModel.molecules {
if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) {
collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier)
}
}
}
//--------------------------------------------------
// MARK: - Convenience
//--------------------------------------------------
/// Returns the (identifier, class) of the molecule for the given map.
func getMoleculeInfo(with molecule: MoleculeModelProtocol, delegateObject: MVMCoreUIDelegateObject?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? {
guard let className = MoleculeObjectMapping.shared()?.getMoleculeClass(molecule) else {
@ -192,44 +220,47 @@ 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 & MVMCoreUIPagingProtocol)?, position: CGFloat) {
open func addPaging(view: (UIView & CarouselPageControlProtocol)?, position: CGFloat) {
pagingView?.removeFromSuperview()
guard let pagingView = view else {
bottomPin?.isActive = false
bottomPin?.isActive = false
guard var pagingView = view else {
bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
bottomPin?.isActive = true
return
}
pagingView.translatesAutoresizingMaskIntoConstraints = false
addSubview(pagingView)
pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: pagingView.centerYAnchor, constant: position).isActive = true
bottomAnchor.constraint(greaterThanOrEqualTo: pagingView.bottomAnchor).isActive = true
bottomPin?.isActive = false
bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
bottomPin?.priority = .defaultLow
bottomPin?.isActive = true
pagingView.setNumberOfPages(numberOfPages)
pagingView.numberOfPages = numberOfPages
(pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill)
pagingView.setPagingTouch { [weak self] (pager) in
MVMCoreDispatchUtility.performBlock(onMainThread: {
guard let localSelf = self else {
return
}
let currentPage = pager.currentPage()
localSelf.pageIndex = currentPage
localSelf.updateModelIndex()
localSelf.goTo(localSelf.currentIndex, animated: !UIAccessibility.isVoiceOverRunning)
})
pageIndex = pagingView.currentIndex
pagingView.indicatorTouchAction = { [weak self] pager in
DispatchQueue.main.async {
guard let self = self else { return }
let currentPage = pager.currentIndex
self.pageIndex = currentPage
self.updateModelIndex()
self.goTo(self.currentIndex, animated: !UIAccessibility.isVoiceOverRunning)
}
}
self.pagingView = pagingView
}
open func showPeaking(_ peaking: Bool) {
if peaking && !UIAccessibility.isVoiceOverRunning {
// Show overlay and arrow in peaking Cell
let visibleItemsPaths = collectionView.indexPathsForVisibleItems.sorted { $0.row < $1.row }
if let firstItem = visibleItemsPaths.first, firstItem.row != currentIndex {
(collectionView.cellForItem(at: firstItem) as? CarouselItem)?.setPeaking(true, animated: true)
}
@ -245,9 +276,8 @@ open class Carousel: View {
}
public func setAccessiblity(_ cell: UICollectionViewCell?, index: Int) {
guard let cell = cell else {
return
}
guard let cell = cell else { return }
if index == currentIndex {
cell.accessibilityElementsHidden = false
var array = cell.accessibilityElements
@ -260,7 +290,7 @@ open class Carousel: View {
}
}
self.accessibilityElements = array
accessibilityElements = array
} else {
cell.accessibilityElementsHidden = true
}
@ -285,9 +315,9 @@ extension Carousel: UICollectionViewDataSource {
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let molecule = molecules?[indexPath.row],
let moleculeInfo = getMoleculeInfo(with: molecule, delegateObject: nil) else {
return UICollectionViewCell()
}
let moleculeInfo = getMoleculeInfo(with: molecule, delegateObject: nil)
else { return UICollectionViewCell() }
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: moleculeInfo.identifier, for: indexPath)
if let protocolCell = cell as? MoleculeViewProtocol {
protocolCell.reset()
@ -302,6 +332,7 @@ extension Carousel: UICollectionViewDataSource {
extension Carousel: UIScrollViewDelegate {
func goTo(_ index: Int, animated: Bool) {
showPeaking(false)
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index)
currentIndex = index
@ -317,12 +348,12 @@ extension Carousel: UIScrollViewDelegate {
guard loop else { return }
let lastPageIndex = numberOfPages + 1
let goToIndex = {(index: Int) in
let goToIndex = { (index: Int) in
self.goTo(index, animated: false)
self.collectionView.layoutIfNeeded()
self.pagingView?.setPage(self.pageIndex)
self.pagingView?.currentIndex = self.pageIndex
}
if currentIndex < 2 {
// If on a "buffer" last row (which is the first index), go to the real last row secretly. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking.
goToIndex(lastPageIndex)
@ -333,14 +364,15 @@ extension Carousel: UIScrollViewDelegate {
}
func checkForDraggingOutOfBounds(_ scrollView: UIScrollView) {
guard loop, dragging else {
return
}
guard loop, dragging else { return }
// Checks if the user is not paging but attempting to drag endlessly and goes out of bounds. Caps the index.
if let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing {
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)
let index = scrollView.contentOffset.x / (itemWidth + separatorWidth)
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
if index < 1 {
currentIndex = 0
updateModelIndex()
@ -359,15 +391,17 @@ extension Carousel: UIScrollViewDelegate {
//checkForDraggingOutOfBounds(scrollView)
// Let the pager know our progress if needed.
pagingView?.scrollViewDidScroll?(collectionView)
pagingView?.scrollViewDidScroll(collectionView)
}
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
dragging = true
showPeaking(false)
}
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
dragging = false
targetContentOffset.pointee = scrollView.contentOffset
@ -376,11 +410,13 @@ extension Carousel: UIScrollViewDelegate {
// We switch cards if we pass the velocity threshold or position threshold (currently 50%).
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)
var cellToSwipeTo = Int(scrollView.contentOffset.x/(itemWidth + separatorWidth) + 0.5)
var cellToSwipeTo = Int(scrollView.contentOffset.x / (itemWidth + separatorWidth) + 0.5)
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
let velocityThreshold: CGFloat = 1.1
if velocity.x > velocityThreshold {
cellToSwipeTo = currentIndex + 1
} else if velocity.x < -velocityThreshold {
cellToSwipeTo = currentIndex - 1
}
@ -393,9 +429,7 @@ extension Carousel: UIScrollViewDelegate {
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
// Cycle to other end if on buffer cell.
handleUserOnBufferCell()
pagingView?.setPage(pageIndex)
pagingView?.currentIndex = pageIndex
showPeaking(true)
}
}

View File

@ -8,8 +8,16 @@
import UIKit
@objcMembers public class CarouselModel: MoleculeModelProtocol {
public static var identifier: String = "carousel"
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String {
return "carousel"
}
public var backgroundColor: Color?
public var molecules: [CarouselItemModel]
public var index: Int = 0
@ -21,11 +29,15 @@ import UIKit
public var itemAlignment: UICollectionView.ScrollPosition?
public var pagingMolecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?
public init(molecules: [CarouselItemModel]){
public init(molecules: [CarouselItemModel]) {
self.molecules = molecules
}
private enum CodingKeys: String, CodingKey {
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey {
case moleculeName
case backgroundColor
case molecules
@ -37,9 +49,13 @@ import UIKit
case itemWidthPercent
case itemAlignment
case pagingMolecule
}
required public init(from decoder: Decoder) throws {
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
molecules = try typeContainer.decode([CarouselItemModel].self, forKey: .molecules)
index = try typeContainer.decodeIfPresent(Int.self, forKey: .index) ?? 0
@ -65,5 +81,5 @@ import UIKit
try container.encode(itemWidthPercent, forKey: .itemWidthPercent)
try container.encode(itemAlignment, forKey: .itemAlignment)
try container.encodeModelIfPresent(pagingMolecule, forKey: .pagingMolecule)
}
}
}

View File

@ -45,7 +45,7 @@ extension MoleculeViewProtocol {
}
// Do nothing, optionals.
public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {}
public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {}
public func reset() {}
public static func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {

View File

@ -0,0 +1,99 @@
//
// ImageView.swift
// MVMCoreUI
//
// Created by Kevin Christiano on 2/13/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import UIKit
open class ImageView: UIImageView, ModelMoleculeViewProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var model: MoleculeModelProtocol?
private var initialSetupPerformed = false
//--------------------------------------------------
// MARK: - Initialization
//--------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: .zero)
initialSetup()
}
override init(image: UIImage?) {
super.init(image: image)
initialSetup()
}
public convenience init() {
self.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
initialSetup()
}
//--------------------------------------------------
// MARK: - Lifecycle
//--------------------------------------------------
public func initialSetup() {
if !initialSetupPerformed {
initialSetupPerformed = true
setupView()
}
}
//--------------------------------------------------
// MARK: - ModelMoleculeViewProtocol
//--------------------------------------------------
public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) {
self.model = model
if let backgroundColor = model?.backgroundColor {
self.backgroundColor = backgroundColor.uiColor
}
}
open class func nameForReuse(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
return model?.moleculeName
}
open class func estimatedHeight(forRow molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
return nil
}
open class func requiredModules(_ molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? {
return nil
}
}
// MARK:- MVMCoreViewProtocol
extension ImageView: MVMCoreViewProtocol {
open func updateView(_ size: CGFloat) { }
/// Will be called only once.
open func setupView() {
translatesAutoresizingMaskIntoConstraints = false
insetsLayoutMarginsFromSafeArea = false
MVMCoreUIUtility.setMarginsFor(self, leading: 0, top: 0, trailing: 0, bottom: 0)
}
}
// MARK:- MVMCoreUIMoleculeViewProtocol
extension ImageView: MVMCoreUIMoleculeViewProtocol {
open func reset() {
backgroundColor = .clear
}
open func setAsMolecule() { }
}

View File

@ -8,11 +8,16 @@
import UIKit
@objcMembers open class View: UIView, MoleculeViewProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var model: MoleculeModelProtocol?
private var initialSetupPerformed = false
//--------------------------------------------------
// MARK: - Initialization
//--------------------------------------------------
@ -21,7 +26,7 @@ import UIKit
super.init(frame: .zero)
initialSetup()
}
public convenience init() {
self.init(frame: .zero)
}
@ -38,12 +43,15 @@ import UIKit
}
}
// MARK:- MoleculeViewProtocol
//--------------------------------------------------
// MARK: - MoleculeViewProtocol
//--------------------------------------------------
open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
self.model = model
if let backgroundColor = model.backgroundColor {
self.model = model
if let backgroundColor = model.backgroundColor {
self.backgroundColor = backgroundColor.uiColor
}
}
}
open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
@ -66,8 +74,8 @@ import UIKit
// MARK:- MVMCoreViewProtocol
extension View: MVMCoreViewProtocol {
open func updateView(_ size: CGFloat) {}
open func updateView(_ size: CGFloat) { }
/// Will be called only once.
open func setupView() {
translatesAutoresizingMaskIntoConstraints = false

View File

@ -69,8 +69,8 @@
// MARK: Carousel
"MVMCoreUIPageControl_currentpage_index" = "page %ld of %ld";
"MVMCoreUIPageControlslides_currentpage_index" = "slide %ld of %ld";
"MVMCoreUIPageControl_currentpage_index" = "page %@ of %d";
"MVMCoreUIPageControlslides_currentpage_index" = "slide %@ of %d";
// MARK: Styler

View File

@ -48,8 +48,8 @@
"AccOff" = "apagado";
"AccToggleHint" = "toca dos veces para alternar";
// Carousel
"MVMCoreUIPageControl_currentpage_index" = "página %ld de %ld";
"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %ld of %ld";
"MVMCoreUIPageControl_currentpage_index" = "página %@ de %d";
"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d";
//Styler
"CountDownDay" = " día";
"CountDownHour" = " hora";

View File

@ -50,8 +50,8 @@
"AccOff" = "apagado";
"AccToggleHint" = "toca dos veces para alternar";
// Carousel
"MVMCoreUIPageControl_currentpage_index" = "página %ld de %ld";
"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %ld of %ld";
"MVMCoreUIPageControl_currentpage_index" = "página %@ de %d";
"MVMCoreUIPageControlslides_currentpage_index" = "diapositiva %@ of %d";
//Styler
"CountDownDay" = " día";
"CountDownHour" = " hora";