316 lines
12 KiB
Swift
316 lines
12 KiB
Swift
//
|
||
// TableViewCell.swift
|
||
// MVMCoreUI
|
||
//
|
||
// Created by Scott Pfeil on 10/29/19.
|
||
// Copyright © 2019 Verizon Wireless. All rights reserved.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
|
||
@objcMembers open class TableViewCell: UITableViewCell, MoleculeViewProtocol, MoleculeListCellProtocol, MVMCoreViewProtocol, MFButtonProtocol {
|
||
//--------------------------------------------------
|
||
// MARK: - Properties
|
||
//--------------------------------------------------
|
||
|
||
open var molecule: MoleculeViewProtocol?
|
||
open var listItemModel: ListItemModelProtocol?
|
||
public let containerHelper = ContainerHelper()
|
||
|
||
// For the accessory view convenience.
|
||
private var caretView: CaretView?
|
||
private var caretViewWidthSizeObject: MFSizeObject?
|
||
private var caretViewHeightSizeObject: MFSizeObject?
|
||
|
||
// For separation between cells.
|
||
public var topSeparatorView: Line?
|
||
public var bottomSeparatorView: Line?
|
||
|
||
/// For subclasses that want to use a custom accessory view.
|
||
open var customAccessoryView = false
|
||
|
||
public var heroAccessoryCenter: CGPoint?
|
||
|
||
private var initialSetupPerformed = false
|
||
|
||
//--------------------------------------------------
|
||
// MARK: - Styling
|
||
//--------------------------------------------------
|
||
|
||
open func styleLine(with style: ListItemStyle?) {
|
||
switch style {
|
||
case .standard?:
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.secondary)
|
||
case .shortDivider?:
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.primary)
|
||
case .tallDivider?:
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.primary)
|
||
case .sectionFooter?:
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.none)
|
||
case ListItemStyle.none?:
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.none)
|
||
default: break
|
||
}
|
||
}
|
||
|
||
/// Default state.
|
||
open func styleStandard() {
|
||
MFStyler.setMarginsFor(self, size: MVMCoreUIUtility.getWidth(), defaultHorizontal: true, top: Padding.Component.VerticalMarginSpacing, bottom: Padding.Component.VerticalMarginSpacing)
|
||
styleLine(with: .standard)
|
||
}
|
||
|
||
/// Adds the molecule to the view.
|
||
open func addMolecule(_ molecule: MoleculeViewProtocol) {
|
||
contentView.addSubview(molecule)
|
||
containerHelper.constrainView(molecule)
|
||
self.molecule = molecule
|
||
}
|
||
|
||
open override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
|
||
// Ensures accessory view aligns to the center y derived from the
|
||
if let _ = heroAccessoryCenter {
|
||
_ = alignAccessoryToHero()
|
||
}
|
||
}
|
||
|
||
// MARK: - Inits
|
||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||
initialSetup()
|
||
}
|
||
|
||
public required init?(coder aDecoder: NSCoder) {
|
||
super.init(coder: aDecoder)
|
||
initialSetup()
|
||
}
|
||
|
||
public func initialSetup() {
|
||
if !initialSetupPerformed {
|
||
initialSetupPerformed = true
|
||
setupView()
|
||
}
|
||
}
|
||
|
||
// MARK: - MFViewProtocol
|
||
open func updateView(_ size: CGFloat) {
|
||
containerHelper.updateViewMargins(self, model: listItemModel, size: size)
|
||
|
||
if accessoryView != nil {
|
||
// Smaller left margin if accessory view.
|
||
var margin = directionalLayoutMargins
|
||
margin.trailing = 16
|
||
contentView.directionalLayoutMargins = margin
|
||
|
||
// Update caret automatically.
|
||
if let caretView = caretView, let widthObject = caretViewWidthSizeObject, let heightObject = caretViewHeightSizeObject {
|
||
caretView.frame = CGRect(x: 0, y: 0, width: widthObject.getValueBased(onSize: size), height: heightObject.getValueBased(onSize: size))
|
||
}
|
||
} else {
|
||
contentView.directionalLayoutMargins = directionalLayoutMargins
|
||
}
|
||
|
||
topSeparatorView?.updateView(size)
|
||
bottomSeparatorView?.updateView(size)
|
||
(molecule as? MVMCoreViewProtocol)?.updateView(size)
|
||
}
|
||
|
||
open func setupView() {
|
||
selectionStyle = .none
|
||
insetsLayoutMarginsFromSafeArea = false
|
||
preservesSuperviewLayoutMargins = false
|
||
contentView.insetsLayoutMarginsFromSafeArea = false
|
||
contentView.preservesSuperviewLayoutMargins = false
|
||
styleStandard()
|
||
}
|
||
|
||
//--------------------------------------------------
|
||
// MARK: - MoleculeViewProtocol
|
||
//--------------------------------------------------
|
||
|
||
open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||
guard let model = model as? ListItemModelProtocol else { return }
|
||
|
||
self.listItemModel = model
|
||
styleLine(with: model.style)
|
||
|
||
// Add the caret if there is an action and it's not declared hidden.
|
||
if !customAccessoryView {
|
||
if let _ = model.action, !(model.hideArrow ?? false) {
|
||
addCaretViewAccessory()
|
||
} else {
|
||
accessoryView = nil
|
||
}
|
||
}
|
||
|
||
// override the separator
|
||
if let separator = model.line {
|
||
addSeparatorsIfNeeded()
|
||
bottomSeparatorView?.set(with: separator, nil, nil)
|
||
}
|
||
|
||
if let moleculeModel = model as? MoleculeModelProtocol,
|
||
let backgroundColor = moleculeModel.backgroundColor {
|
||
self.backgroundColor = backgroundColor.uiColor
|
||
}
|
||
|
||
// align if needed.
|
||
containerHelper.set(with: model, for: molecule as? MVMCoreUIViewConstrainingProtocol)
|
||
}
|
||
|
||
open func reset() {
|
||
molecule?.reset()
|
||
styleStandard()
|
||
backgroundColor = .white
|
||
}
|
||
|
||
// MARK: Overridables
|
||
// Base classes need to implement these functions otherwise swift won't respect the subclass functions and use the ones in the protocol extension instead.
|
||
|
||
open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { model.moleculeName }
|
||
|
||
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { nil }
|
||
|
||
open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? { nil }
|
||
|
||
// MARK: - Caret View
|
||
/// Adds the standard mvm style caret to the accessory view
|
||
@objc public func addCaretViewAccessory() {
|
||
|
||
guard accessoryView == nil else { return }
|
||
|
||
let caret = CaretView(lineWidth: 1)
|
||
caret.translatesAutoresizingMaskIntoConstraints = true
|
||
caret.isAccessibilityElement = true
|
||
caret.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccTabHint")
|
||
caret.accessibilityTraits = .button
|
||
caret.size = .small(.vertical)
|
||
if let size = caret.size?.dimensions() {
|
||
caret.frame = CGRect(origin: .zero, size: size)
|
||
caretViewWidthSizeObject = MFSizeObject(standardSize: size.width, standardiPadPortraitSize: 9)
|
||
caretViewHeightSizeObject = MFSizeObject(standardSize: size.height, standardiPadPortraitSize: 16)
|
||
}
|
||
caretView = caret
|
||
accessoryView = caret
|
||
}
|
||
|
||
/// NOTE: Should only be called when displayed or about to be displayed.
|
||
public func alignAccessoryToHero() -> CGPoint? {
|
||
|
||
// Layout call required to force draw in memory to get dimensions of subviews.
|
||
layoutIfNeeded()
|
||
guard let heroLabel = findHeroLabel(views: contentView.subviews), let hero = heroLabel.hero else { return nil }
|
||
let rect = Label.boundingRect(forCharacterRange: NSRange(location: hero, length: 1), in: heroLabel)
|
||
let y = convert(UIView(frame: rect).center, from: heroLabel).y
|
||
accessoryView?.center.y = y
|
||
heroAccessoryCenter = accessoryView?.center
|
||
return heroAccessoryCenter ?? CGPoint(x: 0, y: y)
|
||
}
|
||
|
||
/// Traverses the view hierarchy for a 🦸♂️ heroic Label.
|
||
private func findHeroLabel(views: [UIView]) -> Label? {
|
||
|
||
if views.isEmpty {
|
||
return nil
|
||
}
|
||
|
||
var queue = [UIView]()
|
||
|
||
// Reversed the array to first check views at the front.
|
||
for view in views.reversed() {
|
||
// Only one Label will have a hero in a table cell.
|
||
if let label = view as? Label, label.hero != nil {
|
||
return label
|
||
}
|
||
|
||
queue.append(contentsOf: view.subviews)
|
||
}
|
||
|
||
return findHeroLabel(views: queue)
|
||
}
|
||
|
||
// MARK: - MoleculeListCellProtocol
|
||
/// For when the separator between cells shows using json and frequency. Default is type: standard, frequency: allExceptTop.
|
||
open func setLines(with model: LineModel?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?, indexPath: IndexPath) {
|
||
addSeparatorsIfNeeded()
|
||
if let model = model {
|
||
topSeparatorView?.set(with: model, delegateObject, additionalData)
|
||
bottomSeparatorView?.set(with: model, delegateObject, additionalData)
|
||
} else {
|
||
topSeparatorView?.setStyle(.secondary)
|
||
bottomSeparatorView?.setStyle(.secondary)
|
||
}
|
||
setSeparatorFrequency(model?.frequency ?? .allExceptTop, indexPath: indexPath)
|
||
}
|
||
|
||
open func willSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Bool { true }
|
||
|
||
open func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
|
||
guard let action = listItemModel?.action else { return }
|
||
Button.performButtonAction(with: action, button: self, delegateObject: delegateObject, additionalData: additionalData)
|
||
}
|
||
|
||
open func didDeselectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { }
|
||
|
||
open func willDisplay() {
|
||
_ = alignAccessoryToHero()
|
||
}
|
||
|
||
// MARK: - Separator
|
||
open func addSeparatorsIfNeeded() {
|
||
if topSeparatorView == nil {
|
||
let line = Line()
|
||
line.setStyle(.none)
|
||
addSubview(line)
|
||
NSLayoutConstraint.pinViewTop(toSuperview: line, useMargins: false, constant: 0).isActive = true
|
||
NSLayoutConstraint.pinViewLeft(toSuperview: line, useMargins: true, constant: 0).isActive = true
|
||
NSLayoutConstraint.pinViewRight(toSuperview: line, useMargins: true, constant: 0).isActive = true
|
||
topSeparatorView = line
|
||
}
|
||
if bottomSeparatorView == nil {
|
||
let line = Line()
|
||
line.setStyle(.none)
|
||
addSubview(line)
|
||
NSLayoutConstraint.pinViewBottom(toSuperview: line, useMargins: false, constant: 0).isActive = true
|
||
NSLayoutConstraint.pinViewLeft(toSuperview: line, useMargins: true, constant: 0).isActive = true
|
||
NSLayoutConstraint.pinViewRight(toSuperview: line, useMargins: true, constant: 0).isActive = true
|
||
bottomSeparatorView = line
|
||
}
|
||
}
|
||
|
||
/// For when the separator between cells shows.
|
||
public func setSeparatorFrequency(_ separatorFrequency: LineModel.Frequency, indexPath: IndexPath) {
|
||
switch separatorFrequency {
|
||
case .all:
|
||
if indexPath.row == 0 {
|
||
topSeparatorView?.isHidden = false
|
||
} else {
|
||
topSeparatorView?.isHidden = true
|
||
}
|
||
bottomSeparatorView?.isHidden = false
|
||
case .allExceptBottom:
|
||
topSeparatorView?.isHidden = false
|
||
bottomSeparatorView?.isHidden = true
|
||
case .between:
|
||
if indexPath.row == 0 {
|
||
topSeparatorView?.isHidden = true
|
||
} else {
|
||
topSeparatorView?.isHidden = false
|
||
}
|
||
bottomSeparatorView?.isHidden = true
|
||
case .allExceptTop:
|
||
fallthrough
|
||
default:
|
||
topSeparatorView?.isHidden = true
|
||
bottomSeparatorView?.isHidden = false
|
||
}
|
||
}
|
||
}
|