Reputation: 1773
In Swift, as shown here, you can use NSMutableAttributedString
to embed links in text.
How can I achieve this with SwiftUI
?
I implemented it as the following, but it does not look how I want it to. .
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Text("By tapping Done, you agree to the ")
Button(action: {}) {
Text("privacy policy")
}
Text(" and ")
Button(action: {}) {
Text("terms of service")
}
Text(" .")
}
}
}
Upvotes: 91
Views: 64503
Reputation: 2197
Hyperlink
Hypelink color
Bold text
Underline hyperlink Color
Text("By tapping Done, you agree to the [privacy policy](https://www..) and [Terms and Conditions](https://www.terms.)")
.padding()
Text("By tapping Done, you agree to the \(Text("[privacy policy](https://www.)")) and \(Text("[Terms and Conditions](https://www.)"))")
.tint(.red)
.padding()
Text("**By tapping Done:** you agree to the \(Text("[privacy policy](https://www.)")) and \(Text("[Terms and Conditions](https://www.)"))")
.padding()
Text("By tapping Done, you agree to the \(Text("[privacy policy](https://www.)").underline()) and \(Text("[Terms and Conditions](https://www.)").underline())")
.tint(.red)
.padding()
Text("By tapping Done, you agree to the \(Text("[privacy policy](https://www.)").underline()) and \(Text("[Terms and Conditions](https://www.)").underline())")
.padding()
Upvotes: 26
Reputation: 957
Another way to effectively do this, is to use .environment
which gives you control over what happens when the user taps a link within the text. For instance, you may want to open the url within a webview inside the app, rather than open in the device browser.
@State private var isPresentedPrivacyPolicy = false
@State private var isPresentedTermsOfUse = false
private let privacyPolicyURL = "https://privacypolicy.com"
private let termsOfUseURL = "https://termsofuse.com"
HStack{
let privacyPolicyText = "By continuing you accept our [Privacy Policy](\(privacyPolicyURL)) and [Terms of Use](\(termsOfUseURL))."
Text(privacyPolicyText)
.environment(\.openURL, OpenURLAction { url in
if url.absoluteString == privacyPolicyURL {
isPresentedPrivacyPolicy.toggle()
} else if url.absoluteString == termsOfUseURL {
isPresentedTermsOfUse.toggle()
}
return .handled
})
}
.sheet(isPresented: $isPresentedPrivacyPolicy) {
NavigationStack {
WebView(url: URL(string: privacyPolicyURL)!)
.ignoresSafeArea()
.navigationTitle("Privacy Policy")
.navigationBarTitleDisplayMode(.inline)
}
}
.sheet(isPresented: $isPresentedTermsOfUse) {
NavigationStack {
WebView(url: URL(string: termsOfUseURL)!)
.ignoresSafeArea()
.navigationTitle("Terms of Use")
.navigationBarTitleDisplayMode(.inline)
}
}
And WebView:
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
Upvotes: 4
Reputation: 14925
SwiftUI has built-in support for rendering Markdown
To create a link, enclose the link's text in brackets (e.g., [Duck Duck Go]) and then follow it immediately with the URL in parentheses (e.g., (https://duckduckgo.com)).
Text("[Privacy Policy](https://example.com)")
https://www.markdownguide.org/basic-syntax/#links
init(_ value: String)
Creates a localized string key from the given string value.
let link = "[Duck Duck Go](https://duckduckgo.com)"
Text(.init(link))
init(_ value: String)
Creates a localized string key from the given string value.
let url = "https://duckduckgo.com"
let link = "[Duck Duck Go](\(url))"
Text(.init(link))
init(_ attributedContent: AttributedString)
Creates a text view that displays styled attributed content.
let markdownLink = try! AttributedString(markdown: "[Duck Duck Go](https://duckduckgo.com)")
Text(markdownLink)
Similar question: Making parts of text bold in SwiftUI
A control for navigating to a URL.
Link("Privacy Policy", destination: URL(string: "https://example.com")!)
https://developer.apple.com/documentation/swiftui/link
Upvotes: 120
Reputation: 61
You can try this way also. I think this is simplest form solution.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text(getAttriText())
}
.environment(\.openURL, OpenURLAction(handler: { url in
if url.absoluteString.contains("privacy") {
// action
}
if url.absoluteString.contains("terms") {
// action
}
return .systemAction // change if you want to discard action
}))
}
func getAttriText() -> AttributedString {
var attriString = AttributedString("By tapping Done, you agree to the privacy policy and terms of service")
attriString.foregroundColor = .black
if let privacyRange = attriString.range(of: "privacy policy") {
attriString[privacyRange].link = URL(string: "www.apple.com/privacy")
attriString[privacyRange].underlineStyle = .single
attriString[privacyRange].foregroundColor = .blue
}
if let termsRange = attriString.range(of: "terms of service") {
attriString[termsRange].link = URL(string: "www.apple.com/terms")
attriString[termsRange].underlineStyle = .single
attriString[termsRange].foregroundColor = .blue
}
return attriString
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Upvotes: 6
Reputation: 6937
To be on the safe side, if your text is not a string literal, you will probably want to use .init
. In other words, if there's any string concatenation, interpolation, etc., you may want to use Text(.init(...))
.
Just note that .init
in this case actually refers to LocalizedStringKey.init
, so localization will still be happening, just like when you're just passing a string literal.
Here are some examples and their rendered output in Xcode 14 previews.
let foo = "Foo"
let bar = "Bar"
let link = "link"
Group {
Text("Foo [Bar](link) Baz") // ✅
Text("Foo" + " [Bar](link) Baz") // ❌
Text(foo + " [Bar](link) Baz") // ❌
Text("\(foo) [Bar](link) Baz") // ✅
Text("\(foo) [Bar](\(link)) Baz") // ❌
Text("\(foo) [\(bar)](\(link)) Baz") // ❌
}
Rectangle().height(1)
Group {
Text(.init("Foo [Bar](link) Baz")) // ✅
Text(.init("Foo" + " [Bar](link) Baz")) // ✅
Text(.init(foo + " [Bar](link) Baz")) // ✅
Text(.init("\(foo) [Bar](link) Baz")) // ✅
Text(.init("\(foo) [Bar](\(link)) Baz")) // ✅
Text(.init("\(foo) [\(bar)](\(link)) Baz")) // ✅
}
Upvotes: 8
Reputation: 21
import SwiftUI
import SwiftUIFlowLayout
public struct HyperlinkText: View {
private let subStrings: [StringWithLinks]
public init(html: String) {
let newString = html.replacingOccurrences(of: "<a href=\'(.+)\'>(.+)</a>",
with: "@&@$2#&#$1@&@",
options: .regularExpression,
range: nil)
self.subStrings = newString.components(separatedBy: "@&@").compactMap{ subString in
let arr = subString.components(separatedBy: "#&#")
return StringWithLinks(string: arr[0], link: arr[safe: 1])
}
}
public var body: some View {
FlowLayout(mode: .scrollable,
binding: .constant(false),
items: subStrings,
itemSpacing: 0) { subString in
if let link = subString.link, let url = URL(string: link) {
Text(subString.string)
.foregroundColor(Color(hexString: "#FF0000EE"))
.onTapGesture {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
.fixedSize(horizontal: false, vertical: true)
} else {
Text(subString.string).fixedSize(horizontal: false, vertical: true)
}
}
}
}
struct StringWithLinks: Hashable, Identifiable {
let id = UUID()
let string: String
let link: String?
static func == (lhs: StringWithLinks, rhs: StringWithLinks) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Upvotes: 1
Reputation: 1169
*** iOS 15 ***
You can add emails or phonenumbers like this too:
var infoText = "For any queries reach out to [email] or call [phone]"
var phone = "+45 12345678".replacingOccurrences(of: " ", with: "")
var email = "[email protected]"
var footerText: String {
return infoText
.replacingOccurrences(
of: "[email]",
with: "[\(email)](mailto:\(email))"
)
.replacingOccurrences(
of: "[phone]",
with: "[\(phone)](tel:\(phone))"
)
}
Text(.init(footerText))
Just remember the links (phone mainly, as emails doesn't have spaces) can't have spaces.
Also remember that this won't work on the simulator. You need a real device to test it.
Upvotes: 1
Reputation: 418
I tried concatenated Texts with Link in between and these are the ways for iOS 15+ and below iOS 15.
if #available(iOS 15, *) {
Text("[Seperate Link 1 ](https://www.android.com/intl/en_in/)")
.font(.caption)
.foregroundColor(Color.green)
// green color is not applied.
Text("[Seperate Link 2 ](https://www.apple.com/in/)")
.font(.caption)
.accentColor(Color.green)
// green is applied.
Text("By authorizing you agree
our ")
.font(.caption)
.foregroundColor(Color.black)
+ Text("[Terms and Conditions](https://www.android.com/intl/en_in/)")
.font(.caption)
.foregroundColor(Color.green) // default blue is applied
+ Text(" and ")
.font(.caption)
.foregroundColor(Color.black)
+ Text("[Privacy Policy](https://www.apple.com/in/)")
.font(.caption)
.foregroundColor(Color.green) // default blue
// cannot use accentColor(Color.green) here
}
else{
// lower iOS versions.
VStack{
Text("By authorizing you agree our ")
.font(.caption)
.foregroundColor(Color.black)
HStack(spacing: 4 ) {
Text("Terms and Conditions")
.font(.caption)
.foregroundColor(Color.green)
.onTapGesture {
let url = URL.init(string: "https://www.android.com/intl/en_in/")
guard let termsAndConditionURL = url, UIApplication.shared.canOpenURL(termsAndConditionURL) else { return }
UIApplication.shared.open(termsAndConditionURL)
}
Text("and")
.font(.caption)
.foregroundColor(Color.black)
Text("Privacy Policy")
.font(.caption)
.foregroundColor(Color.green)
.onTapGesture {
let url = URL.init(string: "https://www.apple.com/in/")
guard let privacyPolicyURL = url, UIApplication.shared.canOpenURL(privacyPolicyURL) else { return }
UIApplication.shared.open(privacyPolicyURL)
}
}
}
}
Upvotes: 5
Reputation: 2792
Just as @mahan mention, this works perfectly for iOS 15.0 and above by using markdown language:
Text("[Privacy Policy](https://example.com)")
But if you're using a String
variable, and put it into the Text
it wouldn't work. Example:
let privacyPolicyText = "Read our [Privacy Policy](https://example.com) here."
Text(privacyPolicyText) // Will not work
The reason is that Text
got multiple initiations. So how do we solve this? The easiest way is just to do:
let privacyPolicyText = "Read our [Privacy Policy](https://example.com) here."
Text(.init(privacyPolicyText))
Result: Read our Privacy Policy here.
Upvotes: 43
Reputation: 299
it's very simple just use LocalizedStringKey for example:
let message = "Hello, www.google.com this is just testing for hyperlinks, check this out our website https://www.apple.in thank you."
Text(LocalizedStringKey(message))
Upvotes: 29
Reputation: 762
I know it's a bit late but I solved the same problem using HTML. First I created a small helper and link model.
struct HTMLStringView: UIViewRepresentable {
let htmlContent: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
}
struct TextLink {
let url: URL
let title: String
}
Next I created function that changes String to HTML and replaces first occurrence of @link to my tappable link.
var content = "My string with @link."
var link = TextLink(url: URL(string: "https://www.facebook.com")!, title: "Facebook")
var body: some View {
let bodySize = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body).pointSize
var html = "<span style=\"font: -apple-system-body; font-size:calc(\(bodySize)px + 1.0vw)\">"
if let linkRange = content.range(of: "@link") {
let startText = content[content.startIndex ..< linkRange.lowerBound]
let endText = content[linkRange.upperBound ..< content.endIndex]
html += startText
html += "<a href=\"\(link.url.absoluteString)\">\(link.title)</a>"
html += endText
} else {
html += content
}
html += "</span>"
return HTMLStringView(htmlContent: html)
}
Upvotes: 3
Reputation: 15253
Use built-in function +
, it looks like a charm:
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Button(action: {
}) {
Text("By tapping Done, you agree to the ")
+ Text("privacy policy")
.foregroundColor(Color.blue)
+ Text(" and ")
+ Text("terms of service")
.foregroundColor(Color.blue)
+ Text(".")
}
.foregroundColor(Color.black)
}
}
}
Upvotes: -10
Reputation: 3761
It's always an option to wrap a UIKit view in UIViewRepresentable
. Just have to go through the manual process of exposing each attribute you want to change.
struct AttributedText: UIViewRepresentable {
var attributedText: NSAttributedString
init(_ attributedText: NSAttributedString) {
self.attributedText = attributedText
}
func makeUIView(context: Context) -> UITextView {
return UITextView()
}
func updateUIView(_ label: UITextView, context: Context) {
label.attributedText = attributedText
}
}
//usage: AttributedText(NSAttributedString())
Upvotes: 2
Reputation: 14378
Motjaba Hosseni is right so far there is nothing that resembles NSAttributedString in SwiftUI. This should solve your problem for the time being:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("By tapping Done, you agree to the ")
HStack(spacing: 0) {
Button("privacy policy") {}
Text(" and ")
Button("terms of service") {}
Text(".")
}
}
}
}
Upvotes: 5