SwiftUI tappable subtext
Update for iOS 15 and higher:There is a new Markdown
formatting support for Text
, such as:
Text("Some text [clickable subtext](some url) *italic ending* ")
you may check WWDC session with a timecode for details
The old answer for iOS 13 and 14:
Unfortunately there is nothing that resembles NSAttributedString in SwiftUI. And you have only a few options. In this answer you can see how to use UIViewRepresentable
for creating an old-school UILabel
with click event, for example. But now the only SwiftUI way is to use 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) } }}
UPDATEAdded solution with UITextView
and UIViewRepresentable
. I combined everything from added links and the result is quite good, I think:
import SwiftUIimport UIKitstruct 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) {} }
result of UIViewRepresentable
and UITextView
:
UPDATE 2:here is a NSMutableAttributedString
little extension:
extension NSMutableAttributedString { var range: NSRange { NSRange(location: 0, length: self.length) } }
I didn't have the patience to make the UITextView
and UIViewRepresentable
work, so instead I made the whole paragraph tappable but still kept the underscored URL look/feel. Especially helpful if you are trying to add Terms of Service URL link to your app.
The code is fairly simple:
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)
Base on Dhaval Bera's code, I put some struct.
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 }}
Usage:
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)") })