mvm_core_ui/MVMCoreUI/BaseClasses/TableViewCell.swift

335 lines
13 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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: UIImageView?
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(.none)
case .tallDivider?:
topSeparatorView?.setStyle(.none)
bottomSeparatorView?.setStyle(.none)
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
}
contentView.insetsLayoutMarginsFromSafeArea = listItemModel?.useSafeAreaInsets == true
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)
if let accessibilityIdentifier = model.accessibilityIdentifier {
self.accessibilityIdentifier = accessibilityIdentifier
}
// 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?.setStyle(separator.type)
}
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)
if let traits = model.accessibilityTraits {
accessibilityTraits.update(with: traits)
}
if let accessibilityText = model.accessibilityText {
accessibilityLabel = accessibilityText
isAccessibilityElement = true
}
accessibilityValue = model.accessibilityValue
}
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 peakingImageView = UIImageView(image: MVMCoreUIUtility.imageNamed("peakingRightArrow")?.withRenderingMode(.alwaysTemplate))
peakingImageView.translatesAutoresizingMaskIntoConstraints = true
peakingImageView.alpha = 0
peakingImageView.tintColor = .black
peakingImageView.isAccessibilityElement = true
peakingImageView.accessibilityHint = MVMCoreUIUtility.hardcodedString(withKey: "AccTabHint")
peakingImageView.accessibilityTraits = .button
let accessorySize = CGRect(origin: .zero, size: CGSize(width: 13.3, height: 13.3))
peakingImageView.frame = accessorySize
caretViewWidthSizeObject = MFSizeObject(standardSize: accessorySize.width, standardiPadPortraitSize: 16.6)
caretViewHeightSizeObject = MFSizeObject(standardSize: accessorySize.height, standardiPadPortraitSize: 16.6)
caretView = peakingImageView
accessoryView = peakingImageView
}
/// 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?.setStyle(model.type)
bottomSeparatorView?.setStyle(model.type)
} 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 }
Task(priority: .userInitiated) {
try await 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
}
}
}