iOS 14 and below

In iOS 14 and below, you’ll likely want to use a representable to accomplish this. Here we wrap a UITextView and leverage NSAttributedString

struct LinkTextView: UIViewRepresentable {
    var attributedString: NSAttributedString
    @Binding var height: CGFloat
    
    var onLinkTap: ((URL) -> Void)?

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.isEditable = false
        textView.isScrollEnabled = true
        textView.delegate = context.coordinator
        textView.backgroundColor = .clear
        textView.dataDetectorTypes = [.link]
        textView.attributedText = attributedString
        // Other options as desired...
        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
        textView.attributedText = attributedString
        updateHeight(for: textView)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var parent: LinkTextView

        init(_ parent: LinkTextView) {
            self.parent = parent
        }

        func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
            parent.onLinkTap?(URL)
            return false // Return false to prevent the default action (opening the URL in a browser)
        }
    }
    
    private func updateHeight(for textView: UITextView) {
        let newSize = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat.infinity))
        if newSize.height != height {
            // Avoid modifying state during update
            Task { @MainActor in
                self.height = newSize.height
            }
        }
    }
}

// Usage
struct ContentView: View {
    @State private var attributedTextHeight: CGFloat = .zero
    private var attributedText: NSMutableAttributedString = {
        let text = "Hello apple.com. This is some more text!"
        
        let attributedString = NSMutableAttributedString(string: text)

        if let linkRange = text.range(of: "apple.com.") {
            attributedString.addAttribute(.link, value: "#", range: NSRange(linkRange, in: text))
        }

        // Apply other styles as needed...        
        return attributedString
    }()

    var body: View {
        LinkTextView(attributedString: attributedText, height: $attributedTextHeight) { url in
            // Handle link tap here...
        }
        .frame(height: attributedTextHeight)
    }
}

iOS 15 and above

It’s recommended that you migrate away from using a representable, and use a more modern alternative

Markdown:

struct ContentView: View {
    var body: some View {
         Text("Hello [apple.com](https://www.apple.com/). This is some more text!")
    }
}

Text itself accepts AttributedString now

struct ContentView: View {
    let markdownLink = try! AttributedString(markdown: "Hello [apple.com](https://www.apple.com/). This is some more text!")

    var body: some View {
         Text(markdownLink)
    }
}