// // 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: - 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.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 = 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 } } }