// // 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(.standard) case .shortDivider?: topSeparatorView?.setStyle(.none) bottomSeparatorView?.setStyle(.thin) case .tallDivider?: topSeparatorView?.setStyle(.none) bottomSeparatorView?.setStyle(.thin) 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? { return model.moleculeName } open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { return nil } open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { 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.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. 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]?) { guard let action = listItemModel?.action else { return } Button.performButtonAction(with: action, button: self, delegateObject: delegateObject, additionalData: additionalData) } 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 } } }