diff --git a/MVMCoreUI/Atoms/Views/Label.swift b/MVMCoreUI/Atoms/Views/Label.swift index 52ee43d1..109f2f97 100644 --- a/MVMCoreUI/Atoms/Views/Label.swift +++ b/MVMCoreUI/Atoms/Views/Label.swift @@ -37,10 +37,15 @@ public typealias ActionBlock = () -> () return !text.isEmpty || !attributedText.string.isEmpty } + public var getRange: NSRange { + return NSRange(location: 0, length: text?.count ?? 0) + } + //------------------------------------------------------ // MARK: - Multi-Action Text //------------------------------------------------------ + /// Data store of the tappable ranges of the text. public var clauses: [ActionableClause] = [] { didSet { isUserInteractionEnabled = !clauses.isEmpty @@ -113,6 +118,15 @@ public typealias ActionBlock = () -> () standardFontSize = size } + /// Convenience to init Label with a link comprised of range, actionMap and delegateObject + @objc convenience public init(text: String, range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { + self.init() + self.text = text + if let actionBlock = createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) { + setTextLinkState(range: range, actionBlock: actionBlock) + } + } + //------------------------------------------------------ // MARK: - Factory Functions //------------------------------------------------------ @@ -308,8 +322,9 @@ public typealias ActionBlock = () -> () guard let actionLabel = label as? Label else { continue } actionLabel.addActionAttributes(range: range, string: attributedString) - let actionBlock = actionLabel.createActionBlockFrom(actionMap: attribute, additionalData: additionalData, delegateObject: delegate) - actionLabel.appendActionableClause(range: range, actionBlock: actionBlock) + if let actionBlock = actionLabel.createActionBlockFor(actionMap: attribute, additionalData: additionalData, delegateObject: delegate) { + actionLabel.appendActionableClause(range: range, actionBlock: actionBlock) + } default: continue @@ -484,7 +499,7 @@ public typealias ActionBlock = () -> () } /// Call to detect in the attributedText contains an NSTextAttachment. - func textContainsTextAttachment() -> Bool { + func containsTextAttachment() -> Bool { guard let attributedText = attributedText else { return false } @@ -589,7 +604,7 @@ extension Label { } } -// MARK: - Multi-Action Functionality +// MARK: - Multi-Link Functionality extension Label { /// Applied to existing text. Removes underlines of tappable links and assoated actionable clauses. @@ -608,15 +623,17 @@ extension Label { clauses = [] } - public func createActionBlockFrom(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> ActionBlock { + public func createActionBlockFor(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> ActionBlock? { return { [weak self] in - if let wSelf = self, (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(wSelf, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true { + guard let self = self else { return } + + if (delegateObject as? MVMCoreUIDelegateObject)?.buttonDelegate?.button?(self, shouldPerformActionWithMap: actionMap, additionalData: additionalData) ?? true { MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) } } } - func addActionAttributes(range: NSRange, string: NSMutableAttributedString?) { + private func addActionAttributes(range: NSRange, string: NSMutableAttributedString?) { guard let string = string else { return } string.addAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue], range: range) @@ -640,8 +657,7 @@ extension Label { */ @objc public func addTappableLinkAttribute(range: NSRange, actionBlock: @escaping ActionBlock) { - setActionAttributes(range: range) - appendActionableClause(range: range, actionBlock: actionBlock) + setTextLinkState(range: range, actionBlock: actionBlock) } /** @@ -655,15 +671,35 @@ extension Label { */ @objc public func addTappableLinkAttribute(range: NSRange, actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { + if let actionBlock = createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) { + setTextLinkState(range: range, actionBlock: actionBlock) + } + } + + /// Converts the entire text into a link. All characters will be underlined and the intrinsic bounds will respond to tap. + @objc public func makeTextButton(actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { + + if let actionBlock = createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) { + setTextLinkState(range: getRange, actionBlock: actionBlock) + } + } + + /// Converts the entire text into a link. All characters will be underlined and the intrinsic bounds will respond to tap. + @objc public func makeTextButton(actionBlock: @escaping ActionBlock) { + + setTextLinkState(range: getRange, actionBlock: actionBlock) + } + + /// Underlines the tappable region and stores the tap logic for interation. + private func setTextLinkState(range: NSRange, actionBlock: @escaping ActionBlock) { + setActionAttributes(range: range) - let actionBlock = createActionBlockFrom(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) appendActionableClause(range: range, actionBlock: actionBlock) } @objc private func textLinkTapped(_ gesture: UITapGestureRecognizer) { for clause in clauses { - // This determines if we tapped on the desired range of text. if let range = clause.range, gesture.didTapAttributedTextInLabel(self, inRange: range) { clause.performAction() @@ -687,9 +723,33 @@ extension UITapGestureRecognizer { let textContainer = abstractContainer.0 let layoutManager = abstractContainer.1 - let indexOfGlyph = layoutManager.glyphIndex(for: location(in: label), in: textContainer) + let tapLocation = location(in: label) + let indexOfGlyph = layoutManager.glyphIndex(for: tapLocation, in: textContainer) + let intrinsicWidth = label.intrinsicContentSize.width - return NSLocationInRange(indexOfGlyph, targetRange) + // Assert that tapped occured within acceptable bounds based on alignment. + switch label.textAlignment { + case .right: + if tapLocation.x < label.bounds.width - intrinsicWidth { + return false + } + case .center: + let halfBounds = label.bounds.width / 2 + let halfIntrinsicWidth = intrinsicWidth / 2 + + if tapLocation.x > halfBounds + halfIntrinsicWidth { + return false + } else if tapLocation.x < halfBounds - halfIntrinsicWidth { + return false + } + default: // Left align + if tapLocation.x > intrinsicWidth { + return false + } + } + + // Affirms that the tap occured in the desired rect of provided by the target range. + return layoutManager.boundingRect(forGlyphRange: targetRange, in: textContainer).contains(tapLocation) && NSLocationInRange(indexOfGlyph, targetRange) } } diff --git a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift index ff2e751d..070cc0f7 100644 --- a/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift +++ b/MVMCoreUI/Atoms/Views/LabelWithInternalButton.swift @@ -207,7 +207,7 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt @objc public func setActionMap(_ actionMap: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { - actionBlock = label?.createActionBlockFrom(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) + actionBlock = label?.createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) } //------------------------------------------------------ @@ -377,7 +377,7 @@ public typealias CoreObjectActionLoadPresentDelegate = MVMCoreActionDelegateProt actionText = actionMap?.optionalStringForKey(KeyTitle) backText = actionMap?.optionalStringForKey(KeyTitlePostfix) text = getTextFromStringComponents() - actionBlock = label?.createActionBlockFrom(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) + actionBlock = label?.createActionBlockFor(actionMap: actionMap, additionalData: additionalData, delegateObject: delegateObject) setLabelAttributes() } diff --git a/MVMCoreUI/Atoms/Views/ViewConstrainingView.m b/MVMCoreUI/Atoms/Views/ViewConstrainingView.m index c6b1023e..66a3a83d 100644 --- a/MVMCoreUI/Atoms/Views/ViewConstrainingView.m +++ b/MVMCoreUI/Atoms/Views/ViewConstrainingView.m @@ -314,10 +314,14 @@ [self.molecule updateView:size]; [MFStyler setMarginsForView:self size:size defaultHorizontal:self.updateViewHorizontalDefaults top:(self.updateViewVerticalDefaults ? self.topMarginPadding : 0) bottom:(self.updateViewVerticalDefaults ? self.bottomMarginPadding : 0)]; UIEdgeInsets margins = [MVMCoreUIUtility getMarginsForView:self]; - [self setLeftPinConstant:margins.left]; - [self setRightPinConstant:margins.right]; - [self setTopPinConstant:margins.top]; - [self setBottomPinConstant:margins.bottom]; + if (self.updateViewHorizontalDefaults) { + [self setLeftPinConstant:margins.left]; + [self setRightPinConstant:margins.right]; + } + if (self.updateViewVerticalDefaults) { + [self setTopPinConstant:margins.top]; + [self setBottomPinConstant:margins.bottom]; + } } #pragma mark - MVMCoreUIMoleculeViewProtocol