Html 不尊重父母的子女';s码迅捷

Html 不尊重父母的子女';s码迅捷,html,ios,swift,swiftui,nsattributedstring,Html,Ios,Swift,Swiftui,Nsattributedstring,我们目前正在为SwiftUI开发一个类似Twitter的客户端,我们必须将HTML内容转换为NSAttributedStrings。出于这个原因,我们正在使用样式设置HTML标记。它似乎工作正常,但当我们试图显示内容时,文本只会覆盖其他内容。我们认为这是因为它不尊重它的父母的大小,但它可能是其他的东西 我们如何解决这个问题?有没有更好的方法在SwiftUI中呈现HTML内容,而不需要使用WKWebViews,因为这会导致难以置信的性能下降 我们有什么遗漏吗 提前谢谢 我们的代码: Netwo

我们目前正在为SwiftUI开发一个类似Twitter的客户端,我们必须将HTML内容转换为
NSAttributedString
s。出于这个原因,我们正在使用样式设置HTML标记。它似乎工作正常,但当我们试图显示内容时,文本只会覆盖其他内容。我们认为这是因为它不尊重它的父母的大小,但它可能是其他的东西

我们如何解决这个问题?有没有更好的方法在SwiftUI中呈现HTML内容,而不需要使用WKWebViews,因为这会导致难以置信的性能下降

我们有什么遗漏吗

提前谢谢

我们的代码

NetworkView.swift

struct NetworkView: View {

    @ObservedObject var timeline = NetworkViewModel()

    private let size: CGFloat = 300
    private let padding: CGFloat = 10
    private let displayPublic: Bool = true

    var body: some View {

        List {
            Section {
                NavigationLink(destination: Text("F").padding()) {
                    Label("Announcements", systemImage: "megaphone")
                }
                NavigationLink(destination: Text("F").padding()) {
                    Label("Activity", systemImage: "flame")
                }
            }
            .listStyle(InsetGroupedListStyle())

            Section(header:
                Picker(selection: self.$timeline.type, label: Text("Network visibility")) {
                    Text("My community").tag(TimelineScope.local)
                    Text("Public").tag(TimelineScope.public)
                }                        .pickerStyle(SegmentedPickerStyle())
                    .padding(.top)
                    .padding(.bottom, 2)) {

                if self.timeline.statuses.isEmpty {

                    HStack {

                        Spacer()

                        VStack {

                            Spacer()

                            ProgressView(value: 0.5)
                                .progressViewStyle(CircularProgressViewStyle())

                            Text("Loading posts...")

                            Spacer()

                        }

                        Spacer()

                    }

                } else {

                    ForEach(self.timeline.statuses, id: \.self.id) { status in
                        StatusView(status: status)
                            .buttonStyle(PlainButtonStyle())
                    }

                }
            }

        }
            .listStyle(GroupedListStyle())

    }
}
import SwiftUI
import Atributika

/// A structure that computes statuses on demand from a `Status` data model.
struct StatusView: View {

    #if os(iOS)
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    #endif

    /// In Starlight, there are two ways to display mastodon statuses:
    ///     - Standard: The status is being displayed from the feed.
    ///     - Focused: The status is the main post of a thread.
    ///
    /// The ``StatusView`` instance should know what way it is being
    /// displayed so that it can display the content properly.
    /// In order to achieve this, we pass a bool to ``StatusView``, which when true,
    /// tells it that it's content should be displayed as `Focused`.
    private var isMain: Bool

    /// The ``Status`` data model whose the data will be displayed.
    var status: Status

    #if os(iOS)

    /// Using for triggering the navigation View **only**when the user taps
    /// on the content, and not when it taps on the action buttons.
    @State var goToThread: Int? = 0

    @State var showMoreActions: Bool = false

    @State var profileViewActive: Bool = false

    #endif

    private let rootStyle: Style = Style("p")
        .font(.systemFont(ofSize: 17, weight: .light))
    private let rootPresentedStyle: Style = Style("p")
        .font(.systemFont(ofSize: 20, weight: .light))
    
    private func configureLabel(_ label: AttributedLabel, size: CGFloat = 17) {
        label.numberOfLines = 0
        label.textColor = .label
        label.font = .systemFont(ofSize: size)
        label.lineBreakMode = .byWordWrapping
    }

    /// To easily use the same view on multiple platforms,
    /// we use the `body` view as a container where we load platform-specific
    /// modifiers.
    var body: some View {

        //  We use this vertical stack to load platform specific modifiers,
        //  or to load specific views when a condition is met.
        VStack {

            // Whether the post is focused or not.
            if self.isMain {

                self.presentedView

            } else {

                // To provide the best experience, we want to allow the user to easily
                // interact with a post directly from the feed. Because of that, we need
                // to add a button
                ZStack {

                    self.defaultView

                    NavigationLink(
                        destination: ThreadView(mainStatus: self.status),
                        tag: 1,
                        selection: self.$goToThread,
                        label: {
                            EmptyView()
                        }
                    )

                }

            }

        }
            .buttonStyle(PlainButtonStyle())

    }

    /// The status display mode when it is the thread's main post.
    var presentedView: some View {

        VStack(alignment: .leading) {

            NavigationLink(destination:
                            ProfileView(isParent: false,
                                        accountInfo: ProfileViewModel(accountID: self.status.account.id),
                                        onResumeToParent: {
                                            self.profileViewActive = false
                                        }),
                           isActive: self.$profileViewActive) {
                EmptyView()
            }

            HStack(alignment: .center) {

                ProfileImage(from: self.status.account.avatarStatic, placeholder: {
                    Circle()
                        .scaledToFit()
                        .frame(width: 50, height: 50)
                        .foregroundColor(.gray)
                })

                VStack(alignment: .leading, spacing: 5) {

                    VStack(alignment: .leading) {

                        Text("\(self.status.account.displayName)")
                            .font(.headline)

                        Text("\(self.status.account.acct)")
                            .foregroundColor(.gray)
                            .lineLimit(1)

                    }

                }

                Spacer()

                Button(action: { self.showMoreActions.toggle() }, label: {
                    Image(systemName: "ellipsis")
                        .imageScale(.large)
                })
            }

            GeometryReader { (geometry: GeometryProxy) in
                AttributedTextView(attributedText:
                                    "\(self.status.content)"
                                        .style(tags: rootPresentedStyle),
                                   configured: { label in configureLabel(label, size: 20) },
                                   maxWidth: geometry.size.width)
                    .fixedSize(horizontal: true, vertical: true)
            }

            if !self.status.mediaAttachments.isEmpty {
                AttachmentView(from: self.status.mediaAttachments[0].url) {
                    Rectangle()
                        .scaledToFit()
                        .cornerRadius(10)
                }
            }

            HStack {
                Text("\(self.status.createdAt.getDate()!.format(as: "hh:mm · dd/MM/YYYY")) · ")
                Button(action: {

                    if let application = self.status.application {
                        if let website = application.website {
                            openUrl(website)
                        }
                    }

                }, label: {
                    Text("\(self.status.application?.name ?? "Mastodon")")
                        .lineLimit(1)
                })
                    .foregroundColor(.accentColor)
                    .padding(.leading, -7)
            }
                .padding(.top)

            Divider()

            Text("\(self.status.repliesCount.roundedWithAbbreviations) ").bold()
            +
            Text("comments, ")
            +
            Text("\(self.status.reblogsCount.roundedWithAbbreviations) ").bold()
            +
            Text("boosts, and ")
            +
            Text("\(self.status.favouritesCount.roundedWithAbbreviations) ").bold()
            +
            Text("likes.")

            Divider()

            self.actionButtons
                .padding(.vertical, 5)
                .padding(.horizontal)

        }
            .buttonStyle(PlainButtonStyle())
        .navigationBarHidden(self.profileViewActive)
        .actionSheet(isPresented: self.$showMoreActions) {
            ActionSheet(title: Text("More Actions"),
                        buttons: [
                            .default(Text("View @\(self.status.account.acct)'s profile"), action: {
                                self.profileViewActive = true
                            }),
                            .destructive(Text("Mute @\(self.status.account.acct)"), action: {

                            }),
                            .destructive(Text("Block @\(self.status.account.acct)"), action: {

                            }),
                            .destructive(Text("Report @\(self.status.account.acct)"), action: {

                            }),
                            .cancel(Text("Dismiss"), action: {})
                        ]
            )
        }

    }

    var defaultView: some View {

        VStack(alignment: .leading) {

            Button(action: {
                self.goToThread = 1
            }, label: {
                HStack(alignment: .top) {

                    ProfileImage(from: self.status.account.avatarStatic, placeholder: {
                        Circle()
                            .scaledToFit()
                            .frame(width: 50, height: 50)
                            .foregroundColor(.gray)
                    })

                    VStack(alignment: .leading, spacing: 5) {

                        HStack {

                            HStack(spacing: 5) {

                                Text("\(self.status.account.displayName)")
                                    .font(.headline)
                                    .lineLimit(1)

                                Text("\(self.status.account.acct)")
                                    .foregroundColor(.gray)
                                    .lineLimit(1)

                                Text("· \(self.status.createdAt.getDate()!.getInterval())")
                                    .lineLimit(1)

                            }

                        }

                        GeometryReader { (geometry: GeometryProxy) in
                            AttributedTextView(attributedText:
                                                "\(self.status.content)"
                                                    .style(tags: rootStyle),
                                               configured: { label in configureLabel(label, size: 17) },
                                               maxWidth: geometry.size.width)
                                .fixedSize(horizontal: true, vertical: false)
                        }

                        if !self.status.mediaAttachments.isEmpty {
                            AttachmentView(from: self.status.mediaAttachments[0].previewURL) {
                                Rectangle()
                                    .scaledToFit()
                                    .cornerRadius(10)
                            }
                        }

                    }

                }
            })

            self.actionButtons
                .padding(.leading, 60)

        }
            .contextMenu(
                ContextMenu(menuItems: {

                    Button(action: {}, label: {
                        Label("Report post", systemImage: "flag")
                    })

                    Button(action: {}, label: {
                        Label("Report \(self.status.account.displayName)", systemImage: "flag")
                    })

                    Button(action: {}, label: {
                        Label("Share as Image", systemImage: "square.and.arrow.up")
                    })

                })
            )

    }

    /// The post's action buttons (favourite and reblog), and also the amount of replies.
    ///
    /// If the post is focused (``isMain`` is true), the count is hidden.
    var actionButtons: some View {
        HStack {

            HStack {

                Image(systemName: "text.bubble")

                if !self.isMain {
                    Text("\(self.status.repliesCount.roundedWithAbbreviations)")
                }

            }

            Spacer()

            Button(action: {

            }, label: {

                HStack {

                    Image(systemName: "arrow.2.squarepath")

                    if !self.isMain {
                        Text("\(self.status.reblogsCount.roundedWithAbbreviations)")
                    }

                }

            })
                .foregroundColor(
                    labelColor
                )

            Spacer()

            Button(action: {

            }, label: {

                HStack {

                    Image(systemName: "heart")

                    if !self.isMain {
                        Text("\(self.status.favouritesCount.roundedWithAbbreviations)")
                    }
                }

            })
                .foregroundColor(
                    labelColor
                )

            Spacer()

            Button(action: {

            }, label: {

                Image(systemName: "square.and.arrow.up")

            })
                .foregroundColor(
                    labelColor
                )

        }

    }

}

extension StatusView {

    /// Generates a View that displays a post on Mastodon.
    ///
    /// - Parameters:
    ///     - isPresented: A boolean variable that determines whether
    ///     the status is being shown as the main post (in a thread).
    ///     - status: The identified data that the ``StatusView`` instance uses to
    ///     display posts dynamically.
    public init(isMain: Bool = false, status: Status) {
        self.isMain = isMain
        self.status = status
    }

}

struct StatusView_Previews: PreviewProvider {

    @ObservedObject static var timeline = NetworkViewModel()

    static var previews: some View {
        VStack {
            if self.timeline.statuses.isEmpty {
                HStack {

                    Spacer()

                    VStack {
                        Spacer()
                        ProgressView(value: 0.5)
                            .progressViewStyle(CircularProgressViewStyle())
                        Text("Loading status...")
                        Spacer()
                    }

                    Spacer()

                }
                    .onAppear {
                        self.timeline.fetchLocalTimeline()
                    }
            } else {
                StatusView(isMain: false, status: self.timeline.statuses[0])
            }
        }
            .frame(width: 600, height: 300)
            .previewLayout(.sizeThatFits)
    }
}
import UIKit
import SwiftUI
import Foundation
import Atributika

// Note: Implementation pulled from pending PR in Atributika on GitHub: https://github.com/psharanda/Atributika/pull/119
// Credit to rivera-ernesto for this implementation.

/// A view that displays one or more lines of text with applied styles.
struct AttributedTextView: UIViewRepresentable {
    typealias UIViewType = RestrainedLabel

    /// The attributed text for this view.
    var attributedText: AttributedText?

    /// The configuration properties for this view.
    var configured: ((AttributedLabel) -> Void)?

    @State var maxWidth: CGFloat = 300

    public func makeUIView(context: UIViewRepresentableContext<AttributedTextView>) -> RestrainedLabel {
        let new = RestrainedLabel()
        configured?(new)
        return new
    }

    public func updateUIView(_ uiView: RestrainedLabel, context: UIViewRepresentableContext<AttributedTextView>) {
        uiView.attributedText = attributedText
        uiView.maxWidth = maxWidth
    }

    class RestrainedLabel: AttributedLabel {
        var maxWidth: CGFloat = 0.0

        open override var intrinsicContentSize: CGSize {
            sizeThatFits(CGSize(width: maxWidth, height: .infinity))
        }
    }

}
StatusView.swift

struct NetworkView: View {

    @ObservedObject var timeline = NetworkViewModel()

    private let size: CGFloat = 300
    private let padding: CGFloat = 10
    private let displayPublic: Bool = true

    var body: some View {

        List {
            Section {
                NavigationLink(destination: Text("F").padding()) {
                    Label("Announcements", systemImage: "megaphone")
                }
                NavigationLink(destination: Text("F").padding()) {
                    Label("Activity", systemImage: "flame")
                }
            }
            .listStyle(InsetGroupedListStyle())

            Section(header:
                Picker(selection: self.$timeline.type, label: Text("Network visibility")) {
                    Text("My community").tag(TimelineScope.local)
                    Text("Public").tag(TimelineScope.public)
                }                        .pickerStyle(SegmentedPickerStyle())
                    .padding(.top)
                    .padding(.bottom, 2)) {

                if self.timeline.statuses.isEmpty {

                    HStack {

                        Spacer()

                        VStack {

                            Spacer()

                            ProgressView(value: 0.5)
                                .progressViewStyle(CircularProgressViewStyle())

                            Text("Loading posts...")

                            Spacer()

                        }

                        Spacer()

                    }

                } else {

                    ForEach(self.timeline.statuses, id: \.self.id) { status in
                        StatusView(status: status)
                            .buttonStyle(PlainButtonStyle())
                    }

                }
            }

        }
            .listStyle(GroupedListStyle())

    }
}
import SwiftUI
import Atributika

/// A structure that computes statuses on demand from a `Status` data model.
struct StatusView: View {

    #if os(iOS)
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    #endif

    /// In Starlight, there are two ways to display mastodon statuses:
    ///     - Standard: The status is being displayed from the feed.
    ///     - Focused: The status is the main post of a thread.
    ///
    /// The ``StatusView`` instance should know what way it is being
    /// displayed so that it can display the content properly.
    /// In order to achieve this, we pass a bool to ``StatusView``, which when true,
    /// tells it that it's content should be displayed as `Focused`.
    private var isMain: Bool

    /// The ``Status`` data model whose the data will be displayed.
    var status: Status

    #if os(iOS)

    /// Using for triggering the navigation View **only**when the user taps
    /// on the content, and not when it taps on the action buttons.
    @State var goToThread: Int? = 0

    @State var showMoreActions: Bool = false

    @State var profileViewActive: Bool = false

    #endif

    private let rootStyle: Style = Style("p")
        .font(.systemFont(ofSize: 17, weight: .light))
    private let rootPresentedStyle: Style = Style("p")
        .font(.systemFont(ofSize: 20, weight: .light))
    
    private func configureLabel(_ label: AttributedLabel, size: CGFloat = 17) {
        label.numberOfLines = 0
        label.textColor = .label
        label.font = .systemFont(ofSize: size)
        label.lineBreakMode = .byWordWrapping
    }

    /// To easily use the same view on multiple platforms,
    /// we use the `body` view as a container where we load platform-specific
    /// modifiers.
    var body: some View {

        //  We use this vertical stack to load platform specific modifiers,
        //  or to load specific views when a condition is met.
        VStack {

            // Whether the post is focused or not.
            if self.isMain {

                self.presentedView

            } else {

                // To provide the best experience, we want to allow the user to easily
                // interact with a post directly from the feed. Because of that, we need
                // to add a button
                ZStack {

                    self.defaultView

                    NavigationLink(
                        destination: ThreadView(mainStatus: self.status),
                        tag: 1,
                        selection: self.$goToThread,
                        label: {
                            EmptyView()
                        }
                    )

                }

            }

        }
            .buttonStyle(PlainButtonStyle())

    }

    /// The status display mode when it is the thread's main post.
    var presentedView: some View {

        VStack(alignment: .leading) {

            NavigationLink(destination:
                            ProfileView(isParent: false,
                                        accountInfo: ProfileViewModel(accountID: self.status.account.id),
                                        onResumeToParent: {
                                            self.profileViewActive = false
                                        }),
                           isActive: self.$profileViewActive) {
                EmptyView()
            }

            HStack(alignment: .center) {

                ProfileImage(from: self.status.account.avatarStatic, placeholder: {
                    Circle()
                        .scaledToFit()
                        .frame(width: 50, height: 50)
                        .foregroundColor(.gray)
                })

                VStack(alignment: .leading, spacing: 5) {

                    VStack(alignment: .leading) {

                        Text("\(self.status.account.displayName)")
                            .font(.headline)

                        Text("\(self.status.account.acct)")
                            .foregroundColor(.gray)
                            .lineLimit(1)

                    }

                }

                Spacer()

                Button(action: { self.showMoreActions.toggle() }, label: {
                    Image(systemName: "ellipsis")
                        .imageScale(.large)
                })
            }

            GeometryReader { (geometry: GeometryProxy) in
                AttributedTextView(attributedText:
                                    "\(self.status.content)"
                                        .style(tags: rootPresentedStyle),
                                   configured: { label in configureLabel(label, size: 20) },
                                   maxWidth: geometry.size.width)
                    .fixedSize(horizontal: true, vertical: true)
            }

            if !self.status.mediaAttachments.isEmpty {
                AttachmentView(from: self.status.mediaAttachments[0].url) {
                    Rectangle()
                        .scaledToFit()
                        .cornerRadius(10)
                }
            }

            HStack {
                Text("\(self.status.createdAt.getDate()!.format(as: "hh:mm · dd/MM/YYYY")) · ")
                Button(action: {

                    if let application = self.status.application {
                        if let website = application.website {
                            openUrl(website)
                        }
                    }

                }, label: {
                    Text("\(self.status.application?.name ?? "Mastodon")")
                        .lineLimit(1)
                })
                    .foregroundColor(.accentColor)
                    .padding(.leading, -7)
            }
                .padding(.top)

            Divider()

            Text("\(self.status.repliesCount.roundedWithAbbreviations) ").bold()
            +
            Text("comments, ")
            +
            Text("\(self.status.reblogsCount.roundedWithAbbreviations) ").bold()
            +
            Text("boosts, and ")
            +
            Text("\(self.status.favouritesCount.roundedWithAbbreviations) ").bold()
            +
            Text("likes.")

            Divider()

            self.actionButtons
                .padding(.vertical, 5)
                .padding(.horizontal)

        }
            .buttonStyle(PlainButtonStyle())
        .navigationBarHidden(self.profileViewActive)
        .actionSheet(isPresented: self.$showMoreActions) {
            ActionSheet(title: Text("More Actions"),
                        buttons: [
                            .default(Text("View @\(self.status.account.acct)'s profile"), action: {
                                self.profileViewActive = true
                            }),
                            .destructive(Text("Mute @\(self.status.account.acct)"), action: {

                            }),
                            .destructive(Text("Block @\(self.status.account.acct)"), action: {

                            }),
                            .destructive(Text("Report @\(self.status.account.acct)"), action: {

                            }),
                            .cancel(Text("Dismiss"), action: {})
                        ]
            )
        }

    }

    var defaultView: some View {

        VStack(alignment: .leading) {

            Button(action: {
                self.goToThread = 1
            }, label: {
                HStack(alignment: .top) {

                    ProfileImage(from: self.status.account.avatarStatic, placeholder: {
                        Circle()
                            .scaledToFit()
                            .frame(width: 50, height: 50)
                            .foregroundColor(.gray)
                    })

                    VStack(alignment: .leading, spacing: 5) {

                        HStack {

                            HStack(spacing: 5) {

                                Text("\(self.status.account.displayName)")
                                    .font(.headline)
                                    .lineLimit(1)

                                Text("\(self.status.account.acct)")
                                    .foregroundColor(.gray)
                                    .lineLimit(1)

                                Text("· \(self.status.createdAt.getDate()!.getInterval())")
                                    .lineLimit(1)

                            }

                        }

                        GeometryReader { (geometry: GeometryProxy) in
                            AttributedTextView(attributedText:
                                                "\(self.status.content)"
                                                    .style(tags: rootStyle),
                                               configured: { label in configureLabel(label, size: 17) },
                                               maxWidth: geometry.size.width)
                                .fixedSize(horizontal: true, vertical: false)
                        }

                        if !self.status.mediaAttachments.isEmpty {
                            AttachmentView(from: self.status.mediaAttachments[0].previewURL) {
                                Rectangle()
                                    .scaledToFit()
                                    .cornerRadius(10)
                            }
                        }

                    }

                }
            })

            self.actionButtons
                .padding(.leading, 60)

        }
            .contextMenu(
                ContextMenu(menuItems: {

                    Button(action: {}, label: {
                        Label("Report post", systemImage: "flag")
                    })

                    Button(action: {}, label: {
                        Label("Report \(self.status.account.displayName)", systemImage: "flag")
                    })

                    Button(action: {}, label: {
                        Label("Share as Image", systemImage: "square.and.arrow.up")
                    })

                })
            )

    }

    /// The post's action buttons (favourite and reblog), and also the amount of replies.
    ///
    /// If the post is focused (``isMain`` is true), the count is hidden.
    var actionButtons: some View {
        HStack {

            HStack {

                Image(systemName: "text.bubble")

                if !self.isMain {
                    Text("\(self.status.repliesCount.roundedWithAbbreviations)")
                }

            }

            Spacer()

            Button(action: {

            }, label: {

                HStack {

                    Image(systemName: "arrow.2.squarepath")

                    if !self.isMain {
                        Text("\(self.status.reblogsCount.roundedWithAbbreviations)")
                    }

                }

            })
                .foregroundColor(
                    labelColor
                )

            Spacer()

            Button(action: {

            }, label: {

                HStack {

                    Image(systemName: "heart")

                    if !self.isMain {
                        Text("\(self.status.favouritesCount.roundedWithAbbreviations)")
                    }
                }

            })
                .foregroundColor(
                    labelColor
                )

            Spacer()

            Button(action: {

            }, label: {

                Image(systemName: "square.and.arrow.up")

            })
                .foregroundColor(
                    labelColor
                )

        }

    }

}

extension StatusView {

    /// Generates a View that displays a post on Mastodon.
    ///
    /// - Parameters:
    ///     - isPresented: A boolean variable that determines whether
    ///     the status is being shown as the main post (in a thread).
    ///     - status: The identified data that the ``StatusView`` instance uses to
    ///     display posts dynamically.
    public init(isMain: Bool = false, status: Status) {
        self.isMain = isMain
        self.status = status
    }

}

struct StatusView_Previews: PreviewProvider {

    @ObservedObject static var timeline = NetworkViewModel()

    static var previews: some View {
        VStack {
            if self.timeline.statuses.isEmpty {
                HStack {

                    Spacer()

                    VStack {
                        Spacer()
                        ProgressView(value: 0.5)
                            .progressViewStyle(CircularProgressViewStyle())
                        Text("Loading status...")
                        Spacer()
                    }

                    Spacer()

                }
                    .onAppear {
                        self.timeline.fetchLocalTimeline()
                    }
            } else {
                StatusView(isMain: false, status: self.timeline.statuses[0])
            }
        }
            .frame(width: 600, height: 300)
            .previewLayout(.sizeThatFits)
    }
}
import UIKit
import SwiftUI
import Foundation
import Atributika

// Note: Implementation pulled from pending PR in Atributika on GitHub: https://github.com/psharanda/Atributika/pull/119
// Credit to rivera-ernesto for this implementation.

/// A view that displays one or more lines of text with applied styles.
struct AttributedTextView: UIViewRepresentable {
    typealias UIViewType = RestrainedLabel

    /// The attributed text for this view.
    var attributedText: AttributedText?

    /// The configuration properties for this view.
    var configured: ((AttributedLabel) -> Void)?

    @State var maxWidth: CGFloat = 300

    public func makeUIView(context: UIViewRepresentableContext<AttributedTextView>) -> RestrainedLabel {
        let new = RestrainedLabel()
        configured?(new)
        return new
    }

    public func updateUIView(_ uiView: RestrainedLabel, context: UIViewRepresentableContext<AttributedTextView>) {
        uiView.attributedText = attributedText
        uiView.maxWidth = maxWidth
    }

    class RestrainedLabel: AttributedLabel {
        var maxWidth: CGFloat = 0.0

        open override var intrinsicContentSize: CGSize {
            sizeThatFits(CGSize(width: maxWidth, height: .infinity))
        }
    }

}
AttributedTextView.swift

struct NetworkView: View {

    @ObservedObject var timeline = NetworkViewModel()

    private let size: CGFloat = 300
    private let padding: CGFloat = 10
    private let displayPublic: Bool = true

    var body: some View {

        List {
            Section {
                NavigationLink(destination: Text("F").padding()) {
                    Label("Announcements", systemImage: "megaphone")
                }
                NavigationLink(destination: Text("F").padding()) {
                    Label("Activity", systemImage: "flame")
                }
            }
            .listStyle(InsetGroupedListStyle())

            Section(header:
                Picker(selection: self.$timeline.type, label: Text("Network visibility")) {
                    Text("My community").tag(TimelineScope.local)
                    Text("Public").tag(TimelineScope.public)
                }                        .pickerStyle(SegmentedPickerStyle())
                    .padding(.top)
                    .padding(.bottom, 2)) {

                if self.timeline.statuses.isEmpty {

                    HStack {

                        Spacer()

                        VStack {

                            Spacer()

                            ProgressView(value: 0.5)
                                .progressViewStyle(CircularProgressViewStyle())

                            Text("Loading posts...")

                            Spacer()

                        }

                        Spacer()

                    }

                } else {

                    ForEach(self.timeline.statuses, id: \.self.id) { status in
                        StatusView(status: status)
                            .buttonStyle(PlainButtonStyle())
                    }

                }
            }

        }
            .listStyle(GroupedListStyle())

    }
}
import SwiftUI
import Atributika

/// A structure that computes statuses on demand from a `Status` data model.
struct StatusView: View {

    #if os(iOS)
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    #endif

    /// In Starlight, there are two ways to display mastodon statuses:
    ///     - Standard: The status is being displayed from the feed.
    ///     - Focused: The status is the main post of a thread.
    ///
    /// The ``StatusView`` instance should know what way it is being
    /// displayed so that it can display the content properly.
    /// In order to achieve this, we pass a bool to ``StatusView``, which when true,
    /// tells it that it's content should be displayed as `Focused`.
    private var isMain: Bool

    /// The ``Status`` data model whose the data will be displayed.
    var status: Status

    #if os(iOS)

    /// Using for triggering the navigation View **only**when the user taps
    /// on the content, and not when it taps on the action buttons.
    @State var goToThread: Int? = 0

    @State var showMoreActions: Bool = false

    @State var profileViewActive: Bool = false

    #endif

    private let rootStyle: Style = Style("p")
        .font(.systemFont(ofSize: 17, weight: .light))
    private let rootPresentedStyle: Style = Style("p")
        .font(.systemFont(ofSize: 20, weight: .light))
    
    private func configureLabel(_ label: AttributedLabel, size: CGFloat = 17) {
        label.numberOfLines = 0
        label.textColor = .label
        label.font = .systemFont(ofSize: size)
        label.lineBreakMode = .byWordWrapping
    }

    /// To easily use the same view on multiple platforms,
    /// we use the `body` view as a container where we load platform-specific
    /// modifiers.
    var body: some View {

        //  We use this vertical stack to load platform specific modifiers,
        //  or to load specific views when a condition is met.
        VStack {

            // Whether the post is focused or not.
            if self.isMain {

                self.presentedView

            } else {

                // To provide the best experience, we want to allow the user to easily
                // interact with a post directly from the feed. Because of that, we need
                // to add a button
                ZStack {

                    self.defaultView

                    NavigationLink(
                        destination: ThreadView(mainStatus: self.status),
                        tag: 1,
                        selection: self.$goToThread,
                        label: {
                            EmptyView()
                        }
                    )

                }

            }

        }
            .buttonStyle(PlainButtonStyle())

    }

    /// The status display mode when it is the thread's main post.
    var presentedView: some View {

        VStack(alignment: .leading) {

            NavigationLink(destination:
                            ProfileView(isParent: false,
                                        accountInfo: ProfileViewModel(accountID: self.status.account.id),
                                        onResumeToParent: {
                                            self.profileViewActive = false
                                        }),
                           isActive: self.$profileViewActive) {
                EmptyView()
            }

            HStack(alignment: .center) {

                ProfileImage(from: self.status.account.avatarStatic, placeholder: {
                    Circle()
                        .scaledToFit()
                        .frame(width: 50, height: 50)
                        .foregroundColor(.gray)
                })

                VStack(alignment: .leading, spacing: 5) {

                    VStack(alignment: .leading) {

                        Text("\(self.status.account.displayName)")
                            .font(.headline)

                        Text("\(self.status.account.acct)")
                            .foregroundColor(.gray)
                            .lineLimit(1)

                    }

                }

                Spacer()

                Button(action: { self.showMoreActions.toggle() }, label: {
                    Image(systemName: "ellipsis")
                        .imageScale(.large)
                })
            }

            GeometryReader { (geometry: GeometryProxy) in
                AttributedTextView(attributedText:
                                    "\(self.status.content)"
                                        .style(tags: rootPresentedStyle),
                                   configured: { label in configureLabel(label, size: 20) },
                                   maxWidth: geometry.size.width)
                    .fixedSize(horizontal: true, vertical: true)
            }

            if !self.status.mediaAttachments.isEmpty {
                AttachmentView(from: self.status.mediaAttachments[0].url) {
                    Rectangle()
                        .scaledToFit()
                        .cornerRadius(10)
                }
            }

            HStack {
                Text("\(self.status.createdAt.getDate()!.format(as: "hh:mm · dd/MM/YYYY")) · ")
                Button(action: {

                    if let application = self.status.application {
                        if let website = application.website {
                            openUrl(website)
                        }
                    }

                }, label: {
                    Text("\(self.status.application?.name ?? "Mastodon")")
                        .lineLimit(1)
                })
                    .foregroundColor(.accentColor)
                    .padding(.leading, -7)
            }
                .padding(.top)

            Divider()

            Text("\(self.status.repliesCount.roundedWithAbbreviations) ").bold()
            +
            Text("comments, ")
            +
            Text("\(self.status.reblogsCount.roundedWithAbbreviations) ").bold()
            +
            Text("boosts, and ")
            +
            Text("\(self.status.favouritesCount.roundedWithAbbreviations) ").bold()
            +
            Text("likes.")

            Divider()

            self.actionButtons
                .padding(.vertical, 5)
                .padding(.horizontal)

        }
            .buttonStyle(PlainButtonStyle())
        .navigationBarHidden(self.profileViewActive)
        .actionSheet(isPresented: self.$showMoreActions) {
            ActionSheet(title: Text("More Actions"),
                        buttons: [
                            .default(Text("View @\(self.status.account.acct)'s profile"), action: {
                                self.profileViewActive = true
                            }),
                            .destructive(Text("Mute @\(self.status.account.acct)"), action: {

                            }),
                            .destructive(Text("Block @\(self.status.account.acct)"), action: {

                            }),
                            .destructive(Text("Report @\(self.status.account.acct)"), action: {

                            }),
                            .cancel(Text("Dismiss"), action: {})
                        ]
            )
        }

    }

    var defaultView: some View {

        VStack(alignment: .leading) {

            Button(action: {
                self.goToThread = 1
            }, label: {
                HStack(alignment: .top) {

                    ProfileImage(from: self.status.account.avatarStatic, placeholder: {
                        Circle()
                            .scaledToFit()
                            .frame(width: 50, height: 50)
                            .foregroundColor(.gray)
                    })

                    VStack(alignment: .leading, spacing: 5) {

                        HStack {

                            HStack(spacing: 5) {

                                Text("\(self.status.account.displayName)")
                                    .font(.headline)
                                    .lineLimit(1)

                                Text("\(self.status.account.acct)")
                                    .foregroundColor(.gray)
                                    .lineLimit(1)

                                Text("· \(self.status.createdAt.getDate()!.getInterval())")
                                    .lineLimit(1)

                            }

                        }

                        GeometryReader { (geometry: GeometryProxy) in
                            AttributedTextView(attributedText:
                                                "\(self.status.content)"
                                                    .style(tags: rootStyle),
                                               configured: { label in configureLabel(label, size: 17) },
                                               maxWidth: geometry.size.width)
                                .fixedSize(horizontal: true, vertical: false)
                        }

                        if !self.status.mediaAttachments.isEmpty {
                            AttachmentView(from: self.status.mediaAttachments[0].previewURL) {
                                Rectangle()
                                    .scaledToFit()
                                    .cornerRadius(10)
                            }
                        }

                    }

                }
            })

            self.actionButtons
                .padding(.leading, 60)

        }
            .contextMenu(
                ContextMenu(menuItems: {

                    Button(action: {}, label: {
                        Label("Report post", systemImage: "flag")
                    })

                    Button(action: {}, label: {
                        Label("Report \(self.status.account.displayName)", systemImage: "flag")
                    })

                    Button(action: {}, label: {
                        Label("Share as Image", systemImage: "square.and.arrow.up")
                    })

                })
            )

    }

    /// The post's action buttons (favourite and reblog), and also the amount of replies.
    ///
    /// If the post is focused (``isMain`` is true), the count is hidden.
    var actionButtons: some View {
        HStack {

            HStack {

                Image(systemName: "text.bubble")

                if !self.isMain {
                    Text("\(self.status.repliesCount.roundedWithAbbreviations)")
                }

            }

            Spacer()

            Button(action: {

            }, label: {

                HStack {

                    Image(systemName: "arrow.2.squarepath")

                    if !self.isMain {
                        Text("\(self.status.reblogsCount.roundedWithAbbreviations)")
                    }

                }

            })
                .foregroundColor(
                    labelColor
                )

            Spacer()

            Button(action: {

            }, label: {

                HStack {

                    Image(systemName: "heart")

                    if !self.isMain {
                        Text("\(self.status.favouritesCount.roundedWithAbbreviations)")
                    }
                }

            })
                .foregroundColor(
                    labelColor
                )

            Spacer()

            Button(action: {

            }, label: {

                Image(systemName: "square.and.arrow.up")

            })
                .foregroundColor(
                    labelColor
                )

        }

    }

}

extension StatusView {

    /// Generates a View that displays a post on Mastodon.
    ///
    /// - Parameters:
    ///     - isPresented: A boolean variable that determines whether
    ///     the status is being shown as the main post (in a thread).
    ///     - status: The identified data that the ``StatusView`` instance uses to
    ///     display posts dynamically.
    public init(isMain: Bool = false, status: Status) {
        self.isMain = isMain
        self.status = status
    }

}

struct StatusView_Previews: PreviewProvider {

    @ObservedObject static var timeline = NetworkViewModel()

    static var previews: some View {
        VStack {
            if self.timeline.statuses.isEmpty {
                HStack {

                    Spacer()

                    VStack {
                        Spacer()
                        ProgressView(value: 0.5)
                            .progressViewStyle(CircularProgressViewStyle())
                        Text("Loading status...")
                        Spacer()
                    }

                    Spacer()

                }
                    .onAppear {
                        self.timeline.fetchLocalTimeline()
                    }
            } else {
                StatusView(isMain: false, status: self.timeline.statuses[0])
            }
        }
            .frame(width: 600, height: 300)
            .previewLayout(.sizeThatFits)
    }
}
import UIKit
import SwiftUI
import Foundation
import Atributika

// Note: Implementation pulled from pending PR in Atributika on GitHub: https://github.com/psharanda/Atributika/pull/119
// Credit to rivera-ernesto for this implementation.

/// A view that displays one or more lines of text with applied styles.
struct AttributedTextView: UIViewRepresentable {
    typealias UIViewType = RestrainedLabel

    /// The attributed text for this view.
    var attributedText: AttributedText?

    /// The configuration properties for this view.
    var configured: ((AttributedLabel) -> Void)?

    @State var maxWidth: CGFloat = 300

    public func makeUIView(context: UIViewRepresentableContext<AttributedTextView>) -> RestrainedLabel {
        let new = RestrainedLabel()
        configured?(new)
        return new
    }

    public func updateUIView(_ uiView: RestrainedLabel, context: UIViewRepresentableContext<AttributedTextView>) {
        uiView.attributedText = attributedText
        uiView.maxWidth = maxWidth
    }

    class RestrainedLabel: AttributedLabel {
        var maxWidth: CGFloat = 0.0

        open override var intrinsicContentSize: CGSize {
            sizeThatFits(CGSize(width: maxWidth, height: .infinity))
        }
    }

}
导入UIKit
导入快捷键
进口基金会
进口心房肌
//注意:从GitHub上Atributika的挂起PR中提取实现:https://github.com/psharanda/Atributika/pull/119
//这一实施归功于rivera ernesto。
///显示具有应用样式的一行或多行文字的视图。
结构AttributedTextView:UIViewRepresentable{
typealias UIViewType=受限标签
///此视图的属性文本。
var attributedText:attributedText?
///此视图的配置属性。
配置的变量:((AttributedLabel)->Void)?
@状态变量maxWidth:CGFloat=300
public func makeUIView(上下文:UIViewRepresentableContext)->RestrainedLabel{
let new=RestrainedLabel()
已配置?(新)
还新
}
public func UpdateUI视图(uiView:RestrainedLabel,context:UIViewRepresentableContext){
uiView.attributedText=attributedText
uiView.maxWidth=maxWidth
}
类限制标签:AttributedLabel{
var maxWidth:CGFloat=0.0
打开覆盖变量intrinsicContentSize:CGSize{
sizeThatFits(CGSize(宽度:maxWidth,高度:无穷大))
}
}
}