mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Support link attributes with AttributedText view
This commit is contained in:
parent
c3806e2eff
commit
c5e0afc2bb
4 changed files with 113 additions and 4 deletions
|
|
@ -11,6 +11,8 @@
|
|||
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; };
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; };
|
||||
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */; };
|
||||
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA735108257BF96D00EA9CF8 /* AttributedText.swift */; };
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */; };
|
||||
CAA1CB2D255A5262003FD669 /* AppleAPI in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2C255A5262003FD669 /* AppleAPI */; };
|
||||
CAA1CB2F255A5262003FD669 /* XcodesKit in Frameworks */ = {isa = PBXBuildFile; productRef = CAA1CB2E255A5262003FD669 /* XcodesKit */; };
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB34255A5AD5003FD669 /* SignInCredentialsView.swift */; };
|
||||
|
|
@ -42,6 +44,8 @@
|
|||
CA538A0C255A4F1A00E64DD7 /* AppleAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AppleAPI; path = Xcodes/AppleAPI; sourceTree = "<group>"; };
|
||||
CA538A0F255A4F3300E64DD7 /* XcodesKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = XcodesKit; path = Xcodes/XcodesKit; sourceTree = "<group>"; };
|
||||
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeTextView.swift; sourceTree = "<group>"; };
|
||||
CA735108257BF96D00EA9CF8 /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = "<group>"; };
|
||||
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+.swift"; sourceTree = "<group>"; };
|
||||
CA8FB5F8256E0F9400469DA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
CA8FB61C256E115700469DA5 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = "<group>"; };
|
||||
CA8FB64D256E17B100469DA5 /* XcodesTest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = XcodesTest.entitlements; sourceTree = "<group>"; };
|
||||
|
|
@ -133,6 +137,8 @@
|
|||
CAA1CB50255A5D16003FD669 /* SignIn */,
|
||||
CA378F982466567600A58CE0 /* AppState.swift */,
|
||||
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */,
|
||||
CA735108257BF96D00EA9CF8 /* AttributedText.swift */,
|
||||
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */,
|
||||
CA44901E2463AD34003D8213 /* Tag.swift */,
|
||||
CAD2E7A52449575000113D76 /* Assets.xcassets */,
|
||||
CAD2E7AA2449575000113D76 /* Main.storyboard */,
|
||||
|
|
@ -267,12 +273,14 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */,
|
||||
CA44901F2463AD34003D8213 /* Tag.swift in Sources */,
|
||||
CA378F992466567600A58CE0 /* AppState.swift in Sources */,
|
||||
CAD2E7A42449574E00113D76 /* ContentView.swift in Sources */,
|
||||
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */,
|
||||
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
|
||||
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
|
||||
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
|
||||
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
|
||||
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
|
||||
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
|
||||
|
|
|
|||
73
Xcodes/AttributedText.swift
Normal file
73
Xcodes/AttributedText.swift
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import SwiftUI
|
||||
|
||||
/// A text view that supports NSAttributedStrings, based on NSTextView.
|
||||
public struct AttributedText: View {
|
||||
private let attributedString: NSAttributedString
|
||||
private let linkTextAttributes: [NSAttributedString.Key: Any]?
|
||||
@State private var actualSize: CGSize = .zero
|
||||
|
||||
public init(_ attributedString: NSAttributedString, linkTextAttributes: [NSAttributedString.Key: Any]? = nil) {
|
||||
self.attributedString = attributedString
|
||||
self.linkTextAttributes = linkTextAttributes
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
InnerAttributedStringText(
|
||||
attributedString: self.attributedString,
|
||||
actualSize: $actualSize
|
||||
)
|
||||
// Limit the height to what's needed for the text
|
||||
.frame(height: actualSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: InnerAttributedStringText
|
||||
|
||||
fileprivate struct InnerAttributedStringText: NSViewRepresentable {
|
||||
private let attributedString: NSAttributedString
|
||||
@Binding var actualSize: CGSize
|
||||
|
||||
internal init(attributedString: NSAttributedString, actualSize: Binding<CGSize>) {
|
||||
self.attributedString = attributedString
|
||||
self._actualSize = actualSize
|
||||
}
|
||||
|
||||
func makeNSView(context: NSViewRepresentableContext<Self>) -> NSTextView {
|
||||
let textView = NSTextView()
|
||||
textView.backgroundColor = .clear
|
||||
textView.textContainer?.lineFragmentPadding = 0
|
||||
textView.textContainerInset = .zero
|
||||
textView.isEditable = false
|
||||
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
textView.isSelectable = true
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateNSView(_ label: NSTextView, context _: NSViewRepresentableContext<Self>) {
|
||||
// This must happen on the next run loop so that we don't update the view hierarchy while already in the middle of an update
|
||||
DispatchQueue.main.async {
|
||||
label.textStorage?.setAttributedString(attributedString)
|
||||
// Calculates the height based on the current frame
|
||||
label.layoutManager?.ensureLayout(for: label.textContainer!)
|
||||
actualSize = label.layoutManager!.usedRect(for: label.textContainer!).size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import SwiftUI
|
||||
struct AttributedText_Previews: PreviewProvider {
|
||||
static var linkExample: NSAttributedString {
|
||||
let string = "The next word is a link. This is some more text to test how this wraps when it's too long."
|
||||
let s = NSMutableAttributedString(string: string)
|
||||
s.addAttribute(.link, value: URL(string: "https://robotsandpencils.com")!, range: NSRange(string.range(of: "link")!, in: string))
|
||||
return s
|
||||
}
|
||||
|
||||
static var previews: some SwiftUI.View {
|
||||
Group {
|
||||
// Previews don't work unless they're running, because detecting and setting the size happens on the next run loop
|
||||
AttributedText(linkExample)
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Xcodes/NSAttributedString+.swift
Normal file
28
Xcodes/NSAttributedString+.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import Foundation
|
||||
|
||||
public extension NSAttributedString {
|
||||
func addingAttribute(_ attribute: NSAttributedString.Key, value: Any, range: NSRange) -> NSAttributedString {
|
||||
let copy = mutableCopy() as! NSMutableAttributedString
|
||||
copy.addAttribute(attribute, value: value, range: range)
|
||||
return copy
|
||||
}
|
||||
|
||||
func addingAttribute(_ attribute: NSAttributedString.Key, value: Any) -> NSAttributedString {
|
||||
addingAttribute(attribute, value: value, range: NSRange(string.startIndex ..< string.endIndex, in: string))
|
||||
}
|
||||
|
||||
/// Detects URLs and adds a NSAttributedString.Key.link attribute with the URL value
|
||||
func convertingURLsToLinkAttributes() -> NSAttributedString {
|
||||
guard
|
||||
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue),
|
||||
let copy = self.mutableCopy() as? NSMutableAttributedString
|
||||
else { return self }
|
||||
|
||||
let matches = detector.matches(in: self.string, options: [], range: NSRange(string.startIndex..<string.endIndex, in: string))
|
||||
for match in matches where match.url != nil {
|
||||
copy.addAttribute(.link, value: match.url!, range: match.range)
|
||||
}
|
||||
|
||||
return copy
|
||||
}
|
||||
}
|
||||
|
|
@ -18,10 +18,10 @@ struct SignInPhoneListView: View {
|
|||
}
|
||||
.frame(height: 200)
|
||||
} else {
|
||||
// TODO: This should be a clickable hyperlink
|
||||
Text("Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915.")
|
||||
// lineLimit doesn't work, fixedSize(horizontal: false, vertical: true) is too large in an Alert
|
||||
.frame(height: 50)
|
||||
AttributedText(
|
||||
NSAttributedString(string: "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication. See https://support.apple.com/en-ca/HT204915.")
|
||||
.convertingURLsToLinkAttributes()
|
||||
)
|
||||
}
|
||||
|
||||
HStack {
|
||||
|
|
|
|||
Loading…
Reference in a new issue