ZSWTappableLabel is a UILabel subclass for links which are tappable, long-pressable, 3D Touchable, and VoiceOverable. It has optional highlighting behavior, and does not draw text itself. Its goal is to be as minimally different from UILabel as possible, and only executes additional code when the user is interacting with a tappable region.
Let's create a string that's entirely tappable and shown with an underline:
let string = NSLocalizedString("Privacy Policy", comment: "")
let attributes: [NSAttributedString.Key: Any] = [
.tappableRegion: true,
.tappableHighlightedBackgroundColor: UIColor.lightGray,
.tappableHighlightedForegroundColor: UIColor.white,
.foregroundColor: UIColor.blue,
.underlineStyle: NSUnderlineStyle.single.rawValue,
.link: URL(string: "http://imgur.com/gallery/VgXCk")!
]
label.attributedText = NSAttributedString(string: string, attributes: attributes)
NSString *s = NSLocalizedString(@"Privacy Policy", nil);
NSDictionary *a = @{
ZSWTappableLabelTappableRegionAttributeName: @YES,
ZSWTappableLabelHighlightedBackgroundAttributeName: [UIColor lightGrayColor],
ZSWTappableLabelHighlightedForegroundAttributeName: [UIColor whiteColor],
NSForegroundColorAttributeName: [UIColor blueColor],
NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle),
NSLinkAttributeName: [NSURL URLWithString:@"http://imgur.com/gallery/VgXCk"],
};
label.attributedText = [[NSAttributedString alloc] initWithString:s attributes:a];
This results in a label which renders like:
Setting your controller as the tapDelegate
of the label results in the following method call when tapped:
func tappableLabel(
_ tappableLabel: ZSWTappableLabel,
tappedAt idx: Int,
withAttributes attributes: [NSAttributedString.Key : Any]
) {
if let url = attributes[.link] as? URL {
UIApplication.shared.openURL(url)
}
}
- (void)tappableLabel:(ZSWTappableLabel *)tappableLabel
tappedAtIndex:(NSInteger)idx
withAttributes:(NSDictionary<NSAttributedStringKey, id> *)attributes {
[[UIApplication sharedApplication] openURL:attributes[@"URL"]];
}
You may optionally support long-presses by setting a longPressDelegate
on the label. This behaves very similarly to the tapDelegate
:
func tappableLabel(
_ tappableLabel: ZSWTappableLabel,
longPressedAt idx: Int,
withAttributes attributes: [NSAttributedString.Key : Any]
) {
guard let URL = attributes[.link] as? URL else {
return
}
let activityController = UIActivityViewController(activityItems: [URL], applicationActivities: nil)
present(activityController, animated: true, completion: nil)
}
- (void)tappableLabel:(ZSWTappableLabel *)tappableLabel
longPressedAtIndex:(NSInteger)idx
withAttributes:(NSDictionary<NSAttributedStringKey, id> *)attributes {
NSURL *URL = attributes[NSLinkAttributeName];
if ([URL isKindOfClass:[NSURL class]]) {
UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:@[ URL ] applicationActivities:nil];
[self presentViewController:activityController animated:YES completion:nil];
}
}
You can configure the longPressDuration
for how long until a long-press is recognized. This defaults to 0.5 seconds.
If you've registered either the label or a view containing the label for previewing using peek/pop, you can get information about the tappable regions at a point using one of the two tappableRegionInfo
methods on ZSWTappableLabel
. See the header file for more information.
When you're queried for previewing information, you can respond using the information from these methods. For example, to preview an SFSafariViewController:
func previewingContext(
_ previewingContext: UIViewControllerPreviewing,
viewControllerForLocation location: CGPoint
) -> UIViewController? {
guard let regionInfo = label.tappableRegionInfo(
forPreviewingContext: previewingContext,
location: location
) else {
return nil
}
guard let URL = regionInfo.attributes[.link] as? URL else {
return nil
}
// convenience method that sets the rect of the previewing context
regionInfo.configure(previewingContext: previewingContext)
return SFSafariViewController(url: URL)
}
- (UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext
viewControllerForLocation:(CGPoint)location {
id<ZSWTappableLabelTappableRegionInfo> regionInfo =
[self.label tappableRegionInfoForPreviewingContext:previewingContext location:location];
if (!regionInfo) {
return nil;
}
[regionInfo configurePreviewingContext:previewingContext];
return [[SFSafariViewController alloc] initWithURL:regionInfo.attributes[NSLinkAttributeName]];
}
Let's use NSDataDetector
to find the substrings in a given string that we might want to turn into links:
let string = "check google.com or call 415-555-5555? how about friday at 5pm?"
let detector = try! NSDataDetector(types: NSTextCheckingAllSystemTypes)
let attributedString = NSMutableAttributedString(string: string, attributes: nil)
let range = NSRange(location: 0, length: (string as NSString).length)
detector.enumerateMatches(in: attributedString.string, options: [], range: range) { (result, flags, _) in
guard let result = result else { return }
var attributes = [NSAttributedString.Key: Any]()
attributes[.tappableRegion] = true
attributes[.tappableHighlightedBackgroundColor] = UIColor.lightGray
attributes[.tappableHighlightedForegroundColor] = UIColor.white
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
attributes[.init(rawValue: "NSTextCheckingResult")] = result
attributedString.addAttributes(attributes, range: result.range)
}
label.attributedText = attributedString
NSString *string = @"check google.com or call 415-555-5555? how about friday at 5pm?";
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingAllSystemTypes error:NULL];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string attributes:nil];
// the next line throws an exception if string is nil - make sure you check
[detector enumerateMatchesInString:string options:0 range:NSMakeRange(0, string.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
attributes[ZSWTappableLabelTappableRegionAttributeName] = @YES;
attributes[ZSWTappableLabelHighlightedBackgroundAttributeName] = [UIColor lightGrayColor];
attributes[ZSWTappableLabelHighlightedForegroundAttributeName] = [UIColor whiteColor];
attributes[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
attributes[@"NSTextCheckingResult"] = result;
[attributedString addAttributes:attributes range:result.range];
}];
label.attributedText = attributedString;
This results in a label which renders like:
check google.com or call 415-555-5555? how about friday at 5pm?
We can wire up the tapDelegate
to receive the checking result and handle each result type when the user taps on the link:
func tappableLabel(
tappableLabel: ZSWTappableLabel,
tappedAtIndex idx: Int,
withAttributes attributes: [NSAttributedString.Key : Any]) {
if let result = attributes[.init(rawValue: "NSTextCheckingResult")] as? NSTextCheckingResult {
switch result.resultType {
case [.address]:
print("Address components: \(result.addressComponents)")
case [.phoneNumber]:
print("Phone number: \(result.phoneNumber)")
case [.date]:
print("Date: \(result.date)")
case [.link]:
print("Link: \(result.url)")
default:
break
}
}
}
- (void)tappableLabel:(ZSWTappableLabel *)tappableLabel
tappedAtIndex:(NSInteger)idx
withAttributes:(NSDictionary<NSAttributedStringKey, id> *)attributes {
NSTextCheckingResult *result = attributes[@"NSTextCheckingResult"];
if (result) {
switch (result.resultType) {
case NSTextCheckingTypeAddress:
NSLog(@"Address components: %@", result.addressComponents);
break;
case NSTextCheckingTypePhoneNumber:
NSLog(@"Phone number: %@", result.phoneNumber);
break;
case NSTextCheckingTypeDate:
NSLog(@"Date: %@", result.date);
break;
case NSTextCheckingTypeLink:
NSLog(@"Link: %@", result.URL);
break;
default:
break;
}
}
}
For substring linking, I suggest you use ZSWTaggedString which creates these attributed strings painlessly and localizably. Let's create a more advanced 'privacy policy' link using this library:
View our Privacy Policy or Terms of Service
You can create such a string using a simple ZSWTaggedString:
let options = ZSWTaggedStringOptions()
options["link"] = .dynamic({ tagName, tagAttributes, stringAttributes in
guard let type = tagAttributes["type"] as? String else {
return [NSAttributedString.Key: Any]()
}
var foundURL: URL?
switch type {
case "privacy":
foundURL = URL(string: "http://google.com/search?q=privacy")!
case "tos":
foundURL = URL(string: "http://google.com/search?q=tos")!
default:
break
}
guard let URL = foundURL else {
return [NSAttributedString.Key: Any]()
}
return [
.tappableRegion: true,
.tappableHighlightedBackgroundColor: UIColor.lightGray,
.tappableHighlightedForegroundColor: UIColor.white,
.foregroundColor: UIColor.blue,
.underlineStyle: NSUnderlineStyle.single.rawValue,
.link: foundURL
]
})
let string = NSLocalizedString("View our <link type='privacy'>Privacy Policy</link> or <link type='tos'>Terms of Service</link>", comment: "")
label.attributedText = try? ZSWTaggedString(string: string).attributedString(with: options)
ZSWTaggedStringOptions *options = [ZSWTaggedStringOptions options];
[options setDynamicAttributes:^NSDictionary *(NSString *tagName,
NSDictionary *tagAttributes,
NSDictionary *existingStringAttributes) {
NSURL *URL;
if ([tagAttributes[@"type"] isEqualToString:@"privacy"]) {
URL = [NSURL URLWithString:@"http://google.com/search?q=privacy"];
} else if ([tagAttributes[@"type"] isEqualToString:@"tos"]) {
URL = [NSURL URLWithString:@"http://google.com/search?q=tos"];
}
if (!URL) {
return nil;
}
return @{
ZSWTappableLabelTappableRegionAttributeName: @YES,
ZSWTappableLabelHighlightedBackgroundAttributeName: [UIColor lightGrayColor],
ZSWTappableLabelHighlightedForegroundAttributeName: [UIColor whiteColor],
NSForegroundColorAttributeName: [UIColor blueColor],
NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle),
@"URL": URL
};
} forTagName:@"link"];
NSString *string = NSLocalizedString(@"View our <link type='privacy'>Privacy Policy</link> or <link type='tos'>Terms of Service</link>", nil);
label.attributedText = [[ZSWTaggedString stringWithString:string] attributedStringWithOptions:options];
ZSWTappableLabel is an accessibility container, which exposes the substrings in your attributed string as distinct elements. For example, the above string breaks down into:
View our
(static text)Privacy Policy
(link)or
(static text)Terms of Service
(link)
This is similar behavior to Safari, which breaks up elements into discrete chunks.
When you set a longPressDelegate
, an additional action on links is added to perform the long-press gesture. You should configure the longPressAccessibilityActionName
to adjust what is read to users.
When you set an accessibilityDelegate
, you can add custom actions to a particular link, for example:
func tappableLabel(
_ tappableLabel: ZSWTappableLabel,
accessibilityCustomActionsForCharacterRange characterRange: NSRange,
withAttributesAtStart attributes: [NSAttributedString.Key : Any] = [:]
) -> [UIAccessibilityCustomAction] {
return [
UIAccessibilityCustomAction(
name: NSLocalizedString("View Link Address", comment: ""),
target: self,
selector: #selector(viewLink(_:))
)
]
}
- (NSArray<UIAccessibilityCustomAction *> *)tappableLabel:(ZSWTappableLabel *)tappableLabel
accessibilityCustomActionsForCharacterRange:(NSRange)characterRange
withAttributesAtStart:(NSDictionary<NSAttributedStringKey,id> *)attributes {
return @[
[[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"View Link Address", nil)
target:self
selector:@selector(viewLink:)]
];
}
You can also change the accessibilityLabel
of the created accessibility elements, for example:
func tappableLabel(
_ tappableLabel: ZSWTappableLabel,
accessibilityLabelForCharacterRange characterRange: NSRange,
withAttributesAtStart attributes: [NSAttributedString.Key : Any] = [:]
) -> String? {
if attributes[.link] != nil {
return "Some Custom Label"
} else {
return nil
}
}
- (nullable NSString *)tappableLabel:(nonnull ZSWTappableLabel *)tappableLabel
accessibilityLabelForCharacterRange:(NSRange)characterRange
withAttributesAtStart:(nonnull NSDictionary<NSAttributedStringKey,id> *)attributes {
if (attributes[NSLinkAttributeName] != nil) {
return @"Some Custom Label";
} else {
return nil;
}
}
ZSWTappableLabel is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod "ZSWTappableLabel", "~> 3.3"
ZSWTappableLabel is available through Swift Package Manager in a Package.swift
like:
.package(url: "https://github.com/zacwest/ZSWTappableLabel.git", majorVersion: 3)
To add it to an Xcode project, add the URL via File > Swift Packages -> Add Package Dependency.
ZSWTappableLabel is available under the MIT license. This library was created while working on Free who allowed this to be open-sourced.