321 lines
12 KiB
Swift
321 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, MVMCoreUIMoleculeViewProtocol, MoleculeListCellProtocol, ModelMoleculeViewProtocol {
|
||
|
||
open var molecule: (UIView & MVMCoreUIMoleculeViewProtocol)?
|
||
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
|
||
|
||
private 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?.topMarginPadding = 24
|
||
listItemModel?.bottomMarginPadding = 24
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.standard)
|
||
}
|
||
|
||
open func styleTallDivider() {
|
||
listItemModel?.topMarginPadding = 48
|
||
listItemModel?.bottomMarginPadding = 16
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.thin)
|
||
}
|
||
|
||
open func styleShortDivider() {
|
||
listItemModel?.topMarginPadding = 32
|
||
listItemModel?.bottomMarginPadding = 16
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.thin)
|
||
}
|
||
|
||
open func styleFooter() {
|
||
listItemModel?.topMarginPadding = 24
|
||
listItemModel?.bottomMarginPadding = 0
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.none)
|
||
}
|
||
|
||
open func styleNone() {
|
||
listItemModel?.topMarginPadding = 0
|
||
listItemModel?.bottomMarginPadding = 0
|
||
topSeparatorView?.setStyle(.none)
|
||
bottomSeparatorView?.setStyle(.none)
|
||
}
|
||
|
||
/// Adds the molecule to the view.
|
||
open func addMolecule(_ molecule: UIView & MVMCoreUIMoleculeViewProtocol) {
|
||
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 center = heroAccessoryCenter {
|
||
accessoryView?.center.y = center.y
|
||
}
|
||
}
|
||
|
||
// 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
|
||
public 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?.updateView(size)
|
||
}
|
||
|
||
open func setupView() {
|
||
selectionStyle = .none
|
||
insetsLayoutMarginsFromSafeArea = false
|
||
preservesSuperviewLayoutMargins = false
|
||
contentView.insetsLayoutMarginsFromSafeArea = false
|
||
contentView.preservesSuperviewLayoutMargins = false
|
||
}
|
||
|
||
//TODO: Model, Change to model
|
||
public func setWithModel(_ 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?.setWithModel(separator, nil, nil)
|
||
}
|
||
|
||
if let moleculeModel = model as? MoleculeModelProtocol,
|
||
let backgroundColor = moleculeModel.backgroundColor {
|
||
self.backgroundColor = backgroundColor.uiColor
|
||
}
|
||
}
|
||
|
||
open func reset() {
|
||
molecule?.reset?()
|
||
backgroundColor = .white
|
||
}
|
||
|
||
public class func nameForReuse(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
|
||
return model?.moleculeName
|
||
}
|
||
|
||
public class func estimatedHeight(forRow molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? {
|
||
return nil
|
||
}
|
||
|
||
// MARK: - Arrow
|
||
/// 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.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() {
|
||
|
||
// 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 }
|
||
let rect = Label.boundingRect(forCharacterRange: NSRange(location: hero, length: 1), in: heroLabel)
|
||
accessoryView?.center.y = contentView.convert(UIView(frame: rect).center, from: heroLabel).y
|
||
heroAccessoryCenter = accessoryView?.center
|
||
}
|
||
|
||
/// 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?.setWithModel(model, delegateObject, additionalData)
|
||
bottomSeparatorView?.setWithModel(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
|
||
}
|
||
}
|
||
}
|