Last active
May 4, 2025 10:01
-
-
Save jerrypm/f73660aaa73c453089cd40cdfd5fce66 to your computer and use it in GitHub Desktop.
attributesString minimal iOS 15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| /* | |
| Available for iOS 15 | |
| */ | |
| struct AttributedStringView: View { | |
| let htmlString: String | |
| let font: Font | |
| let textColor: Color | |
| @Binding var isExpanded: Bool | |
| @State private var fullText: AttributedString = .init() | |
| @State private var truncatedText: AttributedString = .init() | |
| @State private var isTruncated: Bool = false | |
| // Number of lines to show when collapsed | |
| let collapsedLineLimit: Int? | |
| init( | |
| htmlString: String, | |
| font: Font = .body, | |
| textColor: Color = .primary, | |
| isExpanded: Binding<Bool> = .constant(false), | |
| collapsedLineLimit: Int? = nil | |
| ) { | |
| self.htmlString = htmlString | |
| self.font = font | |
| self.textColor = textColor | |
| self._isExpanded = isExpanded | |
| self.collapsedLineLimit = collapsedLineLimit | |
| } | |
| var body: some View { | |
| VStack(alignment: .leading, spacing: 8) { | |
| Text(isExpanded ? fullText : truncatedText) | |
| .font(font) | |
| .foregroundColor(textColor) | |
| .lineLimit(isExpanded ? nil : collapsedLineLimit) | |
| .fixedSize(horizontal: false, vertical: true) | |
| .animation(.easeInOut(duration: 0.3), value: isExpanded) | |
| } | |
| .onAppear { | |
| // Parse the HTML on component appear | |
| let attributedString = parseHTML(from: htmlString) | |
| fullText = attributedString | |
| truncatedText = attributedString | |
| // Check if text would be truncated with the collapsed line limit | |
| DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { | |
| // We use a slight delay to let the view layout first | |
| checkIfTextIsTruncated() | |
| } | |
| } | |
| } | |
| // Function to check if text is truncated | |
| private func checkIfTextIsTruncated() { | |
| // This is a simplified approach - in a real app you might need a more robust solution | |
| // to check if text is actually truncated | |
| let estimatedTextHeight = fullText.characters.count / 40 // rough estimate of chars per line | |
| if let lineLimit = collapsedLineLimit { | |
| isTruncated = estimatedTextHeight > lineLimit | |
| } else { | |
| isTruncated = false | |
| } | |
| } | |
| // Convert HTML to AttributedString | |
| private func parseHTML(from htmlString: String) -> AttributedString { | |
| guard let data = htmlString.data(using: .utf8) else { | |
| return AttributedString(htmlString) | |
| } | |
| do { | |
| // Convert HTML to NSAttributedString | |
| let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [ | |
| .documentType: NSAttributedString.DocumentType.html, | |
| .characterEncoding: String.Encoding.utf8.rawValue | |
| ] | |
| if let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) { | |
| if let attributedString = try? AttributedString(attributedString) { | |
| return attributedString | |
| } | |
| } | |
| } | |
| // Fallback to plain text if HTML parsing fails | |
| return AttributedString(htmlString) | |
| } | |
| } | |
| struct ExampleView: View { | |
| // @State private var isTruncated: Bool = false | |
| @State private var isExpanded: Bool = false | |
| @State private var mockData: String = | |
| """ | |
| <p>This is a sample paragraph demonstrating <strong>bold text</strong> and some random formatting.</p> | |
| <p>Here is a new line <br><br> followed by more text with <strong>bold formatting</strong> as an example.</p> | |
| <p>And here's another line to show Swift-style formatting: "This is a formatted string: ??"</p> | |
| """ | |
| var body: some View { | |
| VStack { | |
| ScrollView { | |
| VStack(alignment: .leading, spacing: 16) { | |
| Text("Details") | |
| .font(.title) | |
| .fontWeight(.bold) | |
| .padding(.top) | |
| AttributedStringView( | |
| htmlString: mockData, | |
| font: .system(.body), | |
| // textColor: .secondary | |
| textColor: .secondary, | |
| isExpanded: $isExpanded, | |
| collapsedLineLimit: 4 | |
| ) | |
| Button(action: { | |
| withAnimation(.easeInOut(duration: 0.3)) { | |
| isExpanded.toggle() | |
| } | |
| }) { | |
| HStack { | |
| Spacer() | |
| Text(isExpanded ? "Hide" : "Show") | |
| .font(.system(.body)) | |
| .foregroundColor(.blue) | |
| Image(systemName: isExpanded ? "chevron.up" : "chevron.down") | |
| .frame(width: 12, height: 12) | |
| .foregroundColor(.blue) | |
| .rotationEffect(.degrees(isExpanded ? 180 : 0)) | |
| .animation(.easeInOut(duration: 0.3), value: isExpanded) | |
| Spacer() | |
| } | |
| } | |
| .padding(.bottom, 8) | |
| } | |
| .padding() | |
| .background(Color.white) | |
| .cornerRadius(8) | |
| .shadow(radius: 2) | |
| Spacer() | |
| } | |
| .safeAreaPadding(.top, 64) | |
| } | |
| .padding() | |
| .background(Color(UIColor.systemGray6)) | |
| .edgesIgnoringSafeArea(.all) | |
| .safeAreaPadding(.top, 24) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment