Merge branch 'feature/label_hero_character' into 'develop'

Label Hero

See merge request BPHV_MIPS/mvm_core_ui!136
This commit is contained in:
Pfeil, Scott Robert 2019-10-18 12:05:55 -04:00
commit 9ef670cfc4
5 changed files with 117 additions and 35 deletions

View File

@ -26,6 +26,9 @@ public typealias ActionBlock = () -> ()
public var sizeObject: MFSizeObject? public var sizeObject: MFSizeObject?
public var scaleSize: NSNumber? public var scaleSize: NSNumber?
/// A specific text index to use as a unique marker.
public var hero: Int?
// Used for scaling the font in updateView. // Used for scaling the font in updateView.
private var originalAttributedString: NSAttributedString? private var originalAttributedString: NSAttributedString?
@ -502,6 +505,50 @@ public typealias ActionBlock = () -> ()
let accessibleAction = customAccessibilityAction(range: range) let accessibleAction = customAccessibilityAction(range: range)
clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: accessibleAction?.hash ?? -1)) clauses.append(ActionableClause(range: range, actionBlock: actionBlock, accessibilityID: accessibleAction?.hash ?? -1))
} }
/**
Provides a text container and layout manager of how the text would appear on screen.
They are used in tandem to derive low-level TextKit results of the label.
*/
public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? {
// Must configure the attributed string to translate what would appear on screen to accurately analyze.
guard let attributedText = attributedText else { return nil }
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = textAlignment
let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
textContainer.size = bounds.size
return (textContainer, layoutManager, textStorage)
}
public static func boundingRect(forCharacterRange range: NSRange, in label: Label) -> CGRect {
guard let abstractContainer = label.abstractTextContainer() else { return CGRect() }
let textContainer = abstractContainer.0
let layoutManager = abstractContainer.1
var glyphRange = NSRange()
// Convert the range for glyphs.
layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)
return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
} }
// MARK: - Atomization // MARK: - Atomization
@ -521,6 +568,8 @@ extension Label {
clauses = [] clauses = []
Label.setUILabel(self, withJSON: json, delegate: delegateObject, additionalData: additionalData) Label.setUILabel(self, withJSON: json, delegate: delegateObject, additionalData: additionalData)
originalAttributedString = attributedText originalAttributedString = attributedText
hero = json?["hero"] as? Int
} }
public func setAsMolecule() { public func setAsMolecule() {
@ -634,26 +683,9 @@ extension UITapGestureRecognizer {
return true return true
} }
// Must configure the attributed string to translate what would appear on screen to accurately analyze. guard let abstractContainer = label.abstractTextContainer() else { return false }
guard let attributedText = label.attributedText else { return false } let textContainer = abstractContainer.0
let layoutManager = abstractContainer.1
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = label.textAlignment
let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText)
stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count))
let textStorage = NSTextStorage(attributedString: stagedAttributedString)
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
textContainer.size = label.bounds.size
let indexOfGlyph = layoutManager.glyphIndex(for: location(in: label), in: textContainer) let indexOfGlyph = layoutManager.glyphIndex(for: location(in: label), in: textContainer)

View File

@ -53,6 +53,17 @@ import UIKit
} }
} }
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
}
}
var heroAccessoryCenter: CGPoint?
func styleStandard() { func styleStandard() {
topMarginPadding = 24 topMarginPadding = 24
bottomMarginPadding = 24 bottomMarginPadding = 24
@ -73,6 +84,11 @@ import UIKit
bottomSeparatorView?.hide() bottomSeparatorView?.hide()
} }
public func willDisplay() {
alignAccessoryToHero()
}
// MARK: - Inits // MARK: - Inits
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
@ -113,15 +129,47 @@ import UIKit
contentView.preservesSuperviewLayoutMargins = false contentView.preservesSuperviewLayoutMargins = false
} }
/// 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]()
for view in views {
// 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: - MVMCoreUIMoleculeViewProtocol // MARK: - MVMCoreUIMoleculeViewProtocol
public func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { public func setWithJSON(_ json: [AnyHashable: Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {
self.json = json; self.json = json
style(with: json?.optionalStringForKey("style")) style(with: json?.optionalStringForKey("style"))
if let useHorizontalMargins = json?.optionalBoolForKey("useHorizontalMargins") { if let useHorizontalMargins = json?.optionalBoolForKey("useHorizontalMargins") {
updateViewHorizontalDefaults = useHorizontalMargins updateViewHorizontalDefaults = useHorizontalMargins
} }
if (json?.optionalBoolForKey("useVerticalMargins") ?? true) == false { if (json?.optionalBoolForKey("useVerticalMargins") ?? true) == false {
topMarginPadding = 0 topMarginPadding = 0
bottomMarginPadding = 0 bottomMarginPadding = 0
@ -146,9 +194,8 @@ import UIKit
bottomSeparatorView?.setWithJSON(separator, delegateObject: delegateObject, additionalData: additionalData) bottomSeparatorView?.setWithJSON(separator, delegateObject: delegateObject, additionalData: additionalData)
} }
guard let json = json, let moleculeJSON = json.optionalDictionaryForKey(KeyMolecule) else { guard let json = json, let moleculeJSON = json.optionalDictionaryForKey(KeyMolecule) else { return }
return
}
if molecule == nil { if molecule == nil {
if let moleculeView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject, constrainIfNeeded: true) { if let moleculeView = MVMCoreUIMoleculeMappingObject.shared()?.createMolecule(forJSON: moleculeJSON, delegateObject: delegateObject, constrainIfNeeded: true) {
contentView.addSubview(moleculeView) contentView.addSubview(moleculeView)
@ -199,10 +246,8 @@ import UIKit
// MARK: - Arrow // MARK: - Arrow
/// Adds the standard mvm style caret to the accessory view /// Adds the standard mvm style caret to the accessory view
public func addCaretViewAccessory() { @objc public func addCaretViewAccessory() {
guard accessoryView == nil else { guard accessoryView == nil else { return }
return
}
let width: CGFloat = 6 let width: CGFloat = 6
let height: CGFloat = 10 let height: CGFloat = 10
caretView = CaretView(lineThickness: CaretView.thin) caretView = CaretView(lineThickness: CaretView.thin)

View File

@ -24,7 +24,6 @@
/// Resets to default state before set with json is called again. /// Resets to default state before set with json is called again.
- (void)reset; - (void)reset;
/// For the molecule list to load more efficiently. /// For the molecule list to load more efficiently.
+ (CGFloat)estimatedHeightForRow:(nullable NSDictionary *)json delegateObject:(nullable MVMCoreUIDelegateObject *)delegateObject; + (CGFloat)estimatedHeightForRow:(nullable NSDictionary *)json delegateObject:(nullable MVMCoreUIDelegateObject *)delegateObject;

View File

@ -17,4 +17,6 @@
/// Handle action /// Handle action
- (void)didSelectCellAtIndex:(nonnull NSIndexPath *)indexPath delegateObject:(nullable MVMCoreUIDelegateObject *)delegateObject additionalData:(nullable NSDictionary *)additionalData; - (void)didSelectCellAtIndex:(nonnull NSIndexPath *)indexPath delegateObject:(nullable MVMCoreUIDelegateObject *)delegateObject additionalData:(nullable NSDictionary *)additionalData;
- (void)willDisplay;
@end @end

View File

@ -51,9 +51,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController {
// MARK: - table // MARK: - table
open override func registerWithTable() { open override func registerWithTable() {
super.registerWithTable() super.registerWithTable()
guard let moleculesInfo = moleculesInfo else { guard let moleculesInfo = moleculesInfo else { return }
return
}
for moleculeInfo in moleculesInfo { for moleculeInfo in moleculesInfo {
tableView?.register(moleculeInfo.class, forCellReuseIdentifier: moleculeInfo.identifier) tableView?.register(moleculeInfo.class, forCellReuseIdentifier: moleculeInfo.identifier)
} }
@ -88,6 +87,13 @@ open class MoleculeListTemplate: ThreeLayerTableViewController {
return cell return cell
} }
open override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let protocolCell = cell as? MoleculeListCellProtocol {
protocolCell.willDisplay?()
}
}
open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? MoleculeListCellProtocol { if let cell = tableView.cellForRow(at: indexPath) as? MoleculeListCellProtocol {
cell.didSelectCell?(atIndex: indexPath, delegateObject: delegateObject() as? MVMCoreUIDelegateObject, additionalData: nil) cell.didSelectCell?(atIndex: indexPath, delegateObject: delegateObject() as? MVMCoreUIDelegateObject, additionalData: nil)
@ -120,9 +126,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController {
open override func addMolecules(_ molecules: [[AnyHashable : Any]], sender: UITableViewCell, animation: UITableView.RowAnimation) { open override func addMolecules(_ molecules: [[AnyHashable : Any]], sender: UITableViewCell, animation: UITableView.RowAnimation) {
// This dispatch is needed to fix a race condition that can occur if this function is called during the table setup. // This dispatch is needed to fix a race condition that can occur if this function is called during the table setup.
DispatchQueue.main.async { DispatchQueue.main.async {
guard let cell = sender as? MoleculeTableViewCell, let indexPath = self.tableView?.indexPath(for: cell) else { guard let cell = sender as? MoleculeTableViewCell, let indexPath = self.tableView?.indexPath(for: cell) else { return }
return
}
var indexPaths: [IndexPath] = [] var indexPaths: [IndexPath] = []
for molecule in molecules { for molecule in molecules {
if let info = self.getMoleculeInfo(with: molecule) { if let info = self.getMoleculeInfo(with: molecule) {