diff --git a/packages/core/platforms/ios/src/UIView+NativeScript.m b/packages/core/platforms/ios/src/UIView+NativeScript.m index b00e96e7a..51485226c 100644 --- a/packages/core/platforms/ios/src/UIView+NativeScript.m +++ b/packages/core/platforms/ios/src/UIView+NativeScript.m @@ -85,6 +85,7 @@ } ]; } + BOOL isLabel = [self isKindOfClass:[UILabel class]]; if (lineHeight > 0) { NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; paragraphStyle.lineSpacing = lineHeight; @@ -92,10 +93,11 @@ if ([self isKindOfClass:[UIButton class]]) { paragraphStyle.alignment = ((UIButton*)self).titleLabel.textAlignment; } else { + // Paragraph alignment is also important for tappable spans as NSTextContainer takes it into account paragraphStyle.alignment = ((UILabel*)self).textAlignment; } - if ([self isKindOfClass:[UILabel class]]) { + if (isLabel) { // make sure a possible previously set line break mode is not lost when line height is specified paragraphStyle.lineBreakMode = ((UILabel*)self).lineBreakMode; } @@ -103,9 +105,15 @@ 0, attrText.length }]; - } else if ([self isKindOfClass:[UITextView class]]) { + } else if (isLabel || [self isKindOfClass:[UITextView class]]) { NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; - paragraphStyle.alignment = ((UITextView*)self).textAlignment; + + if (isLabel) { + // It's important to set paragraph alignment for link tap to work on multi-line spans as NSTextContainer takes it into account + paragraphStyle.alignment = ((UILabel*)self).textAlignment; + } else { + paragraphStyle.alignment = ((UITextView*)self).textAlignment; + } [attrText addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:(NSRange){ 0, attrText.length diff --git a/packages/core/ui/text-base/index.ios.ts b/packages/core/ui/text-base/index.ios.ts index e523f59a3..b52994014 100644 --- a/packages/core/ui/text-base/index.ios.ts +++ b/packages/core/ui/text-base/index.ios.ts @@ -33,8 +33,23 @@ class UILabelClickHandlerImpl extends NSObject { public linkTap(tapGesture: UITapGestureRecognizer) { const owner = this._owner.get(); if (owner) { - // https://stackoverflow.com/a/35789589 const label = owner.nativeTextViewProtected; + + // This offset along with setting paragraph style alignment will achieve perfect horizontal alignment for NSTextContainer + let offsetXMultiplier: number; + switch (owner.textAlignment) { + case 'center': + offsetXMultiplier = 0.5; + break; + case 'right': + offsetXMultiplier = 1.0; + break; + default: + offsetXMultiplier = 0.0; + break; + } + const offsetYMultiplier: number = 0.5; // Text is vertically aligned to center + const layoutManager = NSLayoutManager.alloc().init(); const textContainer = NSTextContainer.alloc().initWithSize(CGSizeZero); const textStorage = NSTextStorage.alloc().initWithAttributedString(owner.nativeTextViewProtected['attributedText']); @@ -51,27 +66,45 @@ class UILabelClickHandlerImpl extends NSObject { const locationOfTouchInLabel = tapGesture.locationInView(label); const textBoundingBox = layoutManager.usedRectForTextContainer(textContainer); - const textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y); - + const textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * offsetXMultiplier - textBoundingBox.origin.x, (labelSize.height - textBoundingBox.size.height) * offsetYMultiplier - textBoundingBox.origin.y); const locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x, locationOfTouchInLabel.y - textContainerOffset.y); - const indexOfCharacter = layoutManager.characterIndexForPointInTextContainerFractionOfDistanceBetweenInsertionPoints(locationOfTouchInTextContainer, textContainer, null); + // Check if tap was inside text bounding rect + if (CGRectContainsPoint(textBoundingBox, locationOfTouchInTextContainer)) { + // According to Apple docs, if no glyph is under point, the nearest glyph is returned + const glyphIndex = layoutManager.glyphIndexForPointInTextContainerFractionOfDistanceThroughGlyph(locationOfTouchInTextContainer, textContainer, null); + // In order to determine whether the tap point actually lies within the bounds + // of the glyph returned, we call the method below and test + // whether the point falls in the rectangle returned by that method + const glyphRect = layoutManager.boundingRectForGlyphRangeInTextContainer( + { + location: glyphIndex, + length: 1, + }, + textContainer + ); - let span: Span = null; - // try to find the corresponding span using the spanRanges - for (let i = 0; i < owner._spanRanges.length; i++) { - const range = owner._spanRanges[i]; - if (range.location <= indexOfCharacter && range.location + range.length > indexOfCharacter) { - if (owner.formattedText && owner.formattedText.spans.length > i) { - span = owner.formattedText.spans.getItem(i); + // Ensure that an actual glyph was tapped + if (CGRectContainsPoint(glyphRect, locationOfTouchInTextContainer)) { + const indexOfCharacter = layoutManager.characterIndexForGlyphAtIndex(glyphIndex); + + let span: Span = null; + // Try to find the corresponding span using the spanRanges + for (let i = 0; i < owner._spanRanges.length; i++) { + const range = owner._spanRanges[i]; + if (range.location <= indexOfCharacter && range.location + range.length > indexOfCharacter) { + if (owner.formattedText && owner.formattedText.spans.length > i) { + span = owner.formattedText.spans.getItem(i); + } + break; + } } - break; - } - } - if (span && span.tappable) { - // if the span is found and tappable emit the linkTap event - span._emit(Span.linkTapEvent); + if (span && span.tappable) { + // if the span is found and tappable emit the linkTap event + span._emit(Span.linkTapEvent); + } + } } } }