繁体   English   中英

SwiftUI 可点击的潜台词

[英]SwiftUI tappable subtext

当点击文本的某些部分时,SwiftUI 中有什么方法可以打开浏览器。

我尝试了上述解决方案,但它不起作用,因为onTapGesture返回您无法添加到TextView

Text("Some text ").foregroundColor(Color(UIColor.systemGray)) +
Text("clickable subtext")
   .foregroundColor(Color(UIColor.systemBlue))
   .onTapGesture {

   }

我想在正文中有可点击的潜台词,这就是为什么使用HStack不起作用

iOS 15 及更高版本的更新:Text有新的Markdown格式支持,例如:

Text("Some text [clickable subtext](some url) *italic ending* ")

您可以查看带有时间码的 WWDC 会话以了解详细信息

iOS 13 和 14 的旧答案:

不幸的是,SwiftUI 中没有任何类似于 NSAttributedString 的东西。 你只有几个选择。 例如,在这个答案中,您可以看到如何使用UIViewRepresentable创建带有 click event的老式UILabel 现在唯一的 SwiftUI 方法是使用HStack

struct TappablePieceOfText: View {
    
    var body: some View {
        
        HStack(spacing: 0) {
            Text("Go to ")
                .foregroundColor(.gray)

            Text("stack overflow")
                .foregroundColor(.blue)
                .underline()
                .onTapGesture {
                    let url = URL.init(string: "https://stackoverflow.com/")
                    guard let stackOverflowURL = url, UIApplication.shared.canOpenURL(stackOverflowURL) else { return }
                    UIApplication.shared.open(stackOverflowURL)
                }
            
            Text(" and enjoy")
                .foregroundColor(.gray)
        }
        
        
    }
}

更新添加了UITextViewUIViewRepresentable的解决方案。 我结合了添加链接的所有内容,结果非常好,我认为:

import SwiftUI
import UIKit

struct TappablePieceOfText: View {
    
    var body: some View {
        TextLabelWithHyperlink()
            .frame(width: 300, height: 110)
    }
    
}

struct TextLabelWithHyperlink: UIViewRepresentable {
    
    func makeUIView(context: Context) -> UITextView {
        
        let standartTextAttributes: [NSAttributedString.Key : Any] = [
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),
            NSAttributedString.Key.foregroundColor: UIColor.gray
        ]
        
        let attributedText = NSMutableAttributedString(string: "You can go to ")
        attributedText.addAttributes(standartTextAttributes, range: attributedText.range) // check extention
        
        let hyperlinkTextAttributes: [NSAttributedString.Key : Any] = [
            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),
            NSAttributedString.Key.foregroundColor: UIColor.blue,
            NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
            NSAttributedString.Key.link: "https://stackoverflow.com"
        ]
        
        let textWithHyperlink = NSMutableAttributedString(string: "stack overflow site")
        textWithHyperlink.addAttributes(hyperlinkTextAttributes, range: textWithHyperlink.range)
        attributedText.append(textWithHyperlink)
        
        let endOfAttrString = NSMutableAttributedString(string: " end enjoy it using old-school UITextView and UIViewRepresentable")
        endOfAttrString.addAttributes(standartTextAttributes, range: endOfAttrString.range)
        attributedText.append(endOfAttrString)
        
        let textView = UITextView()
        textView.attributedText = attributedText
        
        textView.isEditable = false
        textView.textAlignment = .center
        textView.isSelectable = true
        
        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {}
    
}

HStackText的结果: HStack 和文本

UIViewRepresentableUITextView的结果:

在此处输入图像描述

更新2:这是一个NSMutableAttributedString小扩展:

extension NSMutableAttributedString {
    
    var range: NSRange {
        NSRange(location: 0, length: self.length)
    }
    
}

我没有耐心让UITextViewUIViewRepresentable工作,因此我将整个段落设为可点击,但仍保留下划线的 URL 外观/感觉。 如果您尝试将服务条款 URL 链接添加到您的应用程序,则特别有用。

在此处输入图像描述

代码相当简单:

Button(action: {
    let tosURL = URL.init(string: "https://www.google.com")! // add your link here
    if UIApplication.shared.canOpenURL(tosURL) {
        UIApplication.shared.open(tosURL)
    }
}, label: {
    (Text("Store.ly helps you find storage units nearby. By continuing, you agree to our ")
        + Text("Terms of Service.")
            .underline()
        )
        .frame(maxWidth: .infinity, alignment: .leading)
        .font(Font.system(size: 14, weight: .medium))
        .foregroundColor(Color.black)
        .fixedSize(horizontal: false, vertical: true)
})
    .padding([.horizontal], 20)

基于 Dhaval Bera 的代码,我放了一些结构。

struct TextLabelWithHyperLink: UIViewRepresentable {
  
  @State var tintColor: UIColor
  
  @State var hyperLinkItems: Set<HyperLinkItem>
  
  private var _attributedString: NSMutableAttributedString
  
  private var openLink: (HyperLinkItem) -> Void
  
  init (
    tintColor: UIColor,
    string: String,
    attributes: [NSAttributedString.Key : Any],
    hyperLinkItems: Set<HyperLinkItem>,
    openLink: @escaping (HyperLinkItem) -> Void
  ) {
    self.tintColor = tintColor
    self.hyperLinkItems = hyperLinkItems
    self._attributedString = NSMutableAttributedString(
      string: string,
      attributes: attributes
    )
    self.openLink = openLink
  }
  
  
  func makeUIView(context: Context) -> UITextView {
    let textView = UITextView()
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.isEditable = false
    textView.isSelectable = true
    textView.tintColor = self.tintColor
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    return textView
  }
  
  func updateUIView(_ uiView: UITextView, context: Context) {
   
    for item in hyperLinkItems {
      let subText = item.subText
      let link = item.subText.replacingOccurrences(of: " ", with: "_")
      
      _attributedString
        .addAttribute(
          .link,
          value: String(format: "https://%@", link),
          range: (_attributedString.string as NSString).range(of: subText)
        )
    }
    
    uiView.attributedText = _attributedString
  }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(parent: self)
  }
  
  class Coordinator: NSObject, UITextViewDelegate {
    var parent : TextLabelWithHyperLink
    
    init( parent: TextLabelWithHyperLink ) {
      self.parent = parent
    }
    
    func textView(
      _ textView: UITextView,
      shouldInteractWith URL: URL,
      in characterRange: NSRange,
      interaction: UITextItemInteraction
    ) -> Bool {
      
      let strPlain = URL.absoluteString
        .replacingOccurrences(of: "https://", with: "")
        .replacingOccurrences(of: "_", with: " ")
      
      if let ret = parent.hyperLinkItems.first(where: { $0.subText == strPlain }) {
        parent.openLink(ret)
      }
      
      return false
    }
  }
}

struct HyperLinkItem: Hashable {
    
  let subText : String
  let attributes : [NSAttributedString.Key : Any]?
  
  init (
    subText: String,
    attributes: [NSAttributedString.Key : Any]? = nil
  ) {
    self.subText = subText
    self.attributes = attributes
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(subText)
  }
    
  static func == (lhs: HyperLinkItem, rhs: HyperLinkItem) -> Bool {
    lhs.hashValue == rhs.hashValue
  }
}

用法:


TextLabelWithHyperLink(
  tintColor: .green,
  string: "Please contact us by filling contact form. We will contact with you shortly.  Your request will be processed in accordance with the Terms of Use and Privacy Policy.",
  attributes: [:],
  hyperLinkItems: [
    .init(subText: "processed"),
    .init(subText: "Terms of Use"),
  ],
  openLink: {
  (tappedItem) in
    print("Tapped link: \(tappedItem.subText)")
  }
)

下面是我完整的 SwiftUI 解决方案。 使用以下解决方案,您放入的任何容器都将被很好地格式化,并且您可以使您想要点击的特定文本。

struct TermsAndPrivacyText: View {
  @State private var sheet: TermsOrPrivacySheet? = nil
  let string = "By signing up, you agree to XXXX's Terms & Conditions and Privacy Policy"
  
  
  enum TermsOrPrivacySheet: Identifiable {
    case terms, privacy
    
    var id: Int {
      hashValue
    }
  }
  
  
  func showSheet(_ string: String) {
    if ["Terms", "&", "Conditions"].contains(string) {
      sheet = .terms
    }
    else if ["Privacy", "Policy"].contains(string) {
      sheet = .privacy
    }
  }
  
  
  func fontWeight(_ string: String) -> Font.Weight {
    ["Terms", "&", "Conditions", "Privacy", "Policy"].contains(string) ? .medium : .light
  }

  
  private func createText(maxWidth: CGFloat) -> some View {
    var width = CGFloat.zero
    var height = CGFloat.zero
    let stringArray = string.components(separatedBy: " ")
    
    
    return
      ZStack(alignment: .topLeading) {
        ForEach(stringArray, id: \.self) { string in
          Text(string + " ")
            .font(Theme.Fonts.ofSize(14))
            .fontWeight(fontWeight(string))
            .onTapGesture { showSheet(string) }
            .alignmentGuide(.leading, computeValue: { dimension in
              if (abs(width - dimension.width) > maxWidth) {
                width = 0
                height -= dimension.height
              }
              
              let result = width
              if string == stringArray.last {
                width = 0
              }
              else {
                width -= dimension.width
               }
              
              return result
            })
            .alignmentGuide(.top, computeValue: { dimension in
              let result = height
              if string == stringArray.last { height = 0 }
              return result
            })
          }
      }
      .frame(maxWidth: .infinity, alignment: .topLeading)
    }
  
  
  var body: some View {
      GeometryReader { geo in
        ZStack {
          createText(maxWidth: geo.size.width)
        }
      }
      .frame(maxWidth: .infinity)
      .sheet(item: $sheet) { item in
        switch item {
        case .terms:
          TermsAndConditions()
        case .privacy:
          PrivacyPolicy()
        }
      }
  }
}

在此处输入图像描述

我使用了@АлександрГрабовский 的答案,但我还必须进行一些配置以使其适合我。 我的文本字段中有 2 个链接,它们都有自定义颜色并将用户引导到不同的页面。 我也不希望启用滚动,但如果我禁用它,高度不会得到调整,它会延伸到视图的外部。 我尝试了很多不同的东西,目前我发现了一个适合我的解决方案,所以我想我不妨在这里分享一下。

这就是我一直在寻找的

再次感谢@АлександрГрабовский 的回答,我设法做到了。 我必须做的唯一调整是:

  1. 将与文本颜色相关的链接属性设置为另一个 var,并将 UITextView 上的“linkTextAttributes”属性设置为该属性,以更改文本颜色,而字体和链接目标我使用了他的回复中建议的内容。 如果我将颜色属性设置为链接本身,文本颜色不会改变。

    让 linkAttributes: [NSAttributedString.Key : Any] = [ NSAttributedString.Key.foregroundColor: UIColor(named: "my_custom_green") ?? UIColor.blue ] textView.linkTextAttributes = linkAttributes

  2. 我不希望 UITextView 滚动,我发现保持多行高度而不滚动的唯一方法(将 isScrollEnabled 设置为 false 对我不起作用)是将 scrollRangeToVisible 设置为我拥有的最后一个字符串范围。

    textView.scrollRangeToVisible(ppWithHyperlink.range)

我不知道这是否是最好的选择,但这是我发现的……希望将来在 swiftUI 中有更好的方法来做到这一点!!!

使用UITextView的可点击字符串

struct TextLabelWithHyperlink: UIViewRepresentable {

@State var tintColor: UIColor = UIColor.black
@State var arrTapableString: [String] = []

var configuration = { (view: UITextView) in }
var openlink = {(strtext: String) in}

func makeUIView(context: Context) -> UITextView {
    
    let textView = UITextView()
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.isEditable = false
    textView.isSelectable = true
    textView.tintColor = self.tintColor
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    return textView
}

func updateUIView(_ uiView: UITextView, context: Context) {
    configuration(uiView)
    let stringarr  = NSMutableAttributedString(attributedString: uiView.attributedText)
    for strlink in arrTapableString{
        let link = strlink.replacingOccurrences(of: " ", with: "_")
        stringarr.addAttribute(.link, value: String(format: "https://%@", link), range: (stringarr.string as NSString).range(of: strlink))
    }
    uiView.attributedText = stringarr
}

func makeCoordinator() -> Coordinator {
    Coordinator(parent: self)
}

class Coordinator: NSObject,UITextViewDelegate {
    var parent : TextLabelWithHyperlink
    init(parent: TextLabelWithHyperlink) {
        self.parent = parent
    }
    
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        let strPlain = URL.absoluteString.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "_", with: " ")
        if (self.parent.arrTapableString.contains(strPlain)) {
            self.parent.openlink(strPlain)
        }
        return false
    }
    
}}

在 swiftui 中实现

TextLabelWithHyperlink(arrTapableString: ["Terms of Use", "Privacy Policy"]) { (textView) in
                            let string = "Please contact us by filling contact form. We will contact with you shortly.  Your request will be processed in accordance with the Terms of Use and Privacy Policy."
                            
                            let attrib = NSMutableAttributedString(string: string, attributes: [.font: UIFont(name: Poodlife_Font.oxygen_regular, size: 14)!,.foregroundColor:  UIColor.black])
                            
                            attrib.addAttributes([.font: UIFont(name: Font.oxygen_bold, size: 14)!,
                                                  .foregroundColor:  UIColor.black], range: (string as NSString).range(of: "Terms of Use"))
                            
                            attrib.addAttributes([.font: UIFont(name: Font.oxygen_bold, size: 14)!,
                                                  .foregroundColor:  UIColor.black,
                                                  .link: "Privacy_Policy"], range: (string as NSString).range(of: "Privacy Policy"))
                            
                            textView.attributedText = attrib
                        } openlink: { (tappedString) in
                            print("Tapped link:\(tappedString)")
                        }

在 iOS 15 你可以试试

Text("Apple website: [click here](https://apple.com)")

从 iOS 15 开始,您可以将AttributedStringMarkdown与 Text 一起使用。

使用Markdown的示例:

Text("Plain text. [This is a tappable link](https://stackoverflow.com)")

AttributedString使您可以更好地控制格式。 例如,您可以更改链接颜色:

var string = AttributedString("Plain text. ")
        
var tappableText = AttributedString("I am tappable!")
tappableText.link = URL(string: "https://stackoverflow.com")
tappableText.foregroundColor = .green

string.append(tappableText)

Text(string)

这是它的样子:

可点击的文本

附注:如果您希望您的可点击文本具有与在浏览器中打开 URL 不同的行为,您可以为您的应用定义自定义 URL 方案 然后,您将能够使用onOpenURL(perform:)处理链接上的点击事件,该事件注册处理程序以在视图接收到视图所在的场景或窗口的 url 时调用。

对于 iOS 14

我使用了像Down这样的第三方库。 这比创建自己的解析引擎要简单得多。

import SwiftUI
import Down

struct ContentView: View {
    @State var source = NSAttributedString()

    var body: some View {
        VStack {
            TextView(attributedText: source)
                .padding(.horizontal)
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity, minHeight: 64, maxHeight: 80, alignment: .leading)
                .background(Color( red: 236/255, green: 236/255, blue: 248/255))
                .cornerRadius(10)
                .padding()
        }
        .onAppear {
            let down = Down(markdownString: "Work hard to get what you like, otherwise you'll be forced to just like what you get! [tap here](https://apple.com)")
            source = try! down.toAttributedString(.default, stylesheet: "* {font-family: 'Avenir Black'; font-size: 15}")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct TextView: UIViewRepresentable {
    var attributedText: NSAttributedString

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isEditable = false
        textView.backgroundColor = .clear
        textView.isUserInteractionEnabled = true
        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.attributedText = attributedText
    }
}

在此处输入图像描述

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM