ZSWTappableLabel

Version License Platform

ZSWTappableLabel 是 UILabel 的子类,用于创建可点击、可长按、支持 3D Touch 和 VoiceOver 的链接。它具有可选的高亮行为,并且不自行绘制文本。其目标是尽可能地与 UILabel 保持最小差异,仅在用户与可点击区域交互时执行额外的代码。

一个基本的可点击链接

让我们创建一个完全可点击并带有下划线的字符串

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];

这将生成一个像下面这样的标签

隐私政策

将您的控制器设置为标签的 tapDelegate 会在点击时导致以下方法调用

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"]];
}

长按

您可以选择通过在标签上设置 longPressDelegate 来支持长按。这与 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];
  }
}

您可以配置 longPressDuration 来设置识别长按所需的时间。默认值为 0.5 秒。

3D Touch

如果您已使用 peek/pop 注册了标签或包含该标签的视图以进行预览,则可以使用 ZSWTappableLabel 上的两个 tappableRegionInfo 方法之一来获取有关某个点上可点击区域的信息。有关更多信息,请参阅 头文件

当您被查询预览信息时,您可以使用来自这些方法的信息进行响应。例如,要预览 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]];
}

数据检测器

让我们使用 NSDataDetector 来查找给定字符串中我们可能想要转换为链接的子字符串

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;

这将生成一个像下面这样的标签

查看 google.com 或致电 415-555-5555?星期五下午 5 点怎么样?friday at 5pm?

我们可以连接 tapDelegate 以接收检查结果,并在用户点击链接时处理每种结果类型

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;
    }
  }
}

子字符串链接

对于子字符串链接,我建议您使用 ZSWTaggedString,它可以轻松且可本地化地创建这些属性字符串。让我们使用这个库创建一个更高级的“隐私政策”链接

查看我们的隐私政策服务条款

您可以使用简单的 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 是一个辅助功能容器,它将您的属性字符串中的子字符串作为不同的元素公开。例如,上面的字符串分解为

  1. 查看我们的(静态文本)
  2. 隐私政策(链接)
  3. (静态文本)
  4. 服务条款(链接)

这与 Safari 的行为类似,Safari 将元素分解为离散的块。

当您设置 longPressDelegate 时,链接上会添加一个额外的操作来执行长按手势。您应该配置 longPressAccessibilityActionName 以调整向用户读取的内容。

当您设置 accessibilityDelegate 时,您可以为特定链接添加自定义操作,例如

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:)]
  ];
}

您还可以更改创建的辅助功能元素的 accessibilityLabel,例如

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 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile

pod "ZSWTappableLabel", "~> 3.3"

ZSWTappableLabel 可通过 Swift Package ManagerPackage.swift 中获得,例如

.package(url: "https://github.com/zacwest/ZSWTappableLabel.git", majorVersion: 3)

要将其添加到 Xcode 项目,请通过 File > Swift Packages -> Add Package Dependency 添加 URL。

许可证

ZSWTappableLabel 在 MIT 许可证 下可用。此库是在为 Free 工作时创建的,他们允许将其开源。