329 lines
12 KiB
Swift
329 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 {
|
||
|
||
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 style(with styleString: String?) {
|
||
guard let styleString = styleString else {
|
||
return
|
||
}
|
||
switch styleString {
|
||
case "standard":
|
||
styleStandard()
|
||
case "shortDivider":
|
||
styleShortDivider()
|
||
case "tallDivider":
|
||
styleTallDivider()
|
||
case "sectionFooter":
|
||
styleFooter()
|
||
case "none":
|
||
styleNone()
|
||
default: break
|
||
}
|
||
}
|
||
|
||
open func styleStandard() {
|
||
listItemModel?.topPadding = 24
|
||
listItemModel?.bottomPadding = 24
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.standard)
|
||
}
|
||
|
||
open func styleTallDivider() {
|
||
listItemModel?.topPadding = 48
|
||
listItemModel?.bottomPadding = 16
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.thin)
|
||
}
|
||
|
||
open func styleShortDivider() {
|
||
listItemModel?.topPadding = 32
|
||
listItemModel?.bottomPadding = 16
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.thin)
|
||
}
|
||
|
||
open func styleFooter() {
|
||
listItemModel?.topPadding = 24
|
||
listItemModel?.bottomPadding = 0
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.none)
|
||
}
|
||
|
||
open func styleNone() {
|
||
listItemModel?.topPadding = 0
|
||
listItemModel?.bottomPadding = 0
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.none)
|
||
}
|
||
|
||
/// 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()
|
||
}
|
||
|
||
//TODO: ModelProtocol, Change to model
|
||
open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||
guard let model = model as? ListItemModelProtocol else { return }
|
||
|
||
self.listItemModel = model
|
||
style(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
|
||
}
|
||
|
||
open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
|
||
return model.moleculeName
|
||
}
|
||
|
||
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||
return 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.size = .small(.vertical)
|
||
if let size = caret.size?.dimensions() {
|
||
caret.frame = CGRect(origin: CGPoint.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.
|
||
public 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(.standard)
|
||
bottomSeparatorView?.setStyle(.standard)
|
||
}
|
||
setSeparatorFrequency(model?.frequency ?? .allExceptTop, indexPath: indexPath)
|
||
}
|
||
|
||
public func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
|
||
//TODO: Use object when handleAction is rewrote to handle action model
|
||
if let actionMap = self.listItemModel?.action?.toJSON() {
|
||
MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject)
|
||
}
|
||
}
|
||
|
||
public 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
|
||
}
|
||
}
|
||
}
|