ZSWTappableLabel is a UILabel
subclass powered by NSAttributedStrings which allows you to tap or long-press on certain regions, with optional highlight behavior. It does not draw text itself and executes a minimal amount of code unless 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: [NSAttributedStringKey: Any] = [
.tappableRegion: true,
.tappableHighlightedBackgroundColor: UIColor.lightGray,
.tappableHighlightedForegroundColor: UIColor.white,
.foregroundColor: UIColor.blue,
.underlineStyle: NSUnderlineStyle.styleSingle.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: [NSAttributedStringKey : 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: [NSAttributedStringKey : 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.
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 = [NSAttributedStringKey: Any]()
attributes[.tappableRegion] = true
attributes[.tappableHighlightedBackgroundColor] = UIColor.lightGray
attributes[.tappableHighlightedForegroundColor] = UIColor.white
attributes[.underlineStyle] = NSUnderlineStyle.styleSingle.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: [NSAttributedStringKey : 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 [NSAttributedStringKey: AnyObject]()
}
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 [NSAttributedStringKey: AnyObject]()
}
return [
.tappableRegion: true,
.tappableHighlightedBackgroundColor: UIColor.lightGray,
.tappableHighlightedForegroundColor: UIColor.white,
.foregroundColor: UIColor.blue,
.underlineStyle: NSUnderlineStyle.styleSingle.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)
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.
ZSWTappableLabel uses gesture recognizers internally and works well with other gesture recognizers:
- If there are no tappable regions, internal gesture recognizers are disabled.
- If a touch occurs within a tappable region, all other gesture recognizers are failed if the label is interested in them.
- If a touch occurs outside a tappable region, internal gesture recognizers fail themselves.
For example, if you place a UITapGestureRecognizer on the label, it will only fire when the user does not interact with a tappable region.
ZSWTappableLabel is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod "ZSWTappableLabel", "~> 2.0"
ZSWTappableLabel is available under the MIT license. This library was created while working on Free who allowed this to be open-sourced.