- Вопрос или проблема
- Ответ или решение
- Оптимизация производительности пользовательского интерфейса при работе с текстовым Markdown в SwiftUI
- 1. Изменение обработки текста
- 2. Оптимизация рендеринга компонентов
- 3. Использование draw вместо layout
- 4. Проверка и минимизация затрат на Кэширование
- 5. Асинхронные операции
- Заключение
Вопрос или проблема
У меня есть базовый прокручиваемый вид с контентом, и весь контент находится внутри LazyVStack. Когда у меня много подвидов с моим собственным текстовым представлением в формате markdown, прокручиваемый вид становится очень медленным и с фризами/глитчами. Я предполагаю, что это связано с тем, что мой собственный вид требует много ресурсов для разделения каждой части текста и его отображения. Учитывая строку в Swift UI, я просто хочу стилизовать каждый компонент (хэштег, @тег, веб-ссылка) по-разному и обрабатывать клики на каждом из этих компонентов по-разному. Как я могу улучшить свой подход, чтобы это не сказалось негативно на прокручиваемом представлении? Если существует лучший способ подойти к этому, я был бы признателен за помощь.
Как это работает:
Я разделяю каждую строку на массив компонентов внутри init. Затем каждый компонент располагается внутри CustomLayout
, который является пользовательским макетом, который располагает подвиды последовательно слева направо, сверху вниз, чтобы воспроизвести непрерывную строку.
код для копирования
import SwiftUI
private let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
struct MarkDownText: View {
let text: String
let links: [NSTextCheckingResult]
init (_ text: String) {
self.text = text
let nsText = text as NSString
let wholeString = NSRange(location: 0, length: nsText.length)
links = linkDetector.matches(in: text, options: [], range: wholeString)
}
var body: some View {
MarkDownTextView(text: text, links: links)
}
}
struct MarkDownTextView: View {
@Environment(\.colorScheme) var colorScheme
enum Component {
case text(String)
case link(String, URL)
case username(String)
case hashtag(String)
}
let text: String
@State var components: [Component]
init(text: String, links: [NSTextCheckingResult]) {
self.text = text
let nsText = text as NSString
var components: [Component] = []
var index = 0
for result in links {
if result.range.location > index {
let sub_str = nsText.substring(with: NSRange(location: index, length: result.range.location - index))
let ats = separateTextAndTags(input: sub_str)
ats.forEach { element in
if element.hasPrefix("@") && element.count < 13 {
components.append(.username(element))
} else if element.hasPrefix("#") && element.count < 40 {
components.append(.hashtag(element))
} else {
components.append(.text(element))
}
}
}
components.append(.link(nsText.substring(with: result.range), result.url!))
index = result.range.location + result.range.length
}
if index < nsText.length {
let sub_str = nsText.substring(from: index)
let ats = separateTextAndTags(input: sub_str)
ats.forEach { element in
if element.hasPrefix("@") && element.count < 13 {
components.append(.username(element))
} else if element.hasPrefix("#") && element.count < 40 {
components.append(.hashtag(element))
} else {
components.append(.text(element))
}
}
}
_components = State(initialValue: components)
}
var body: some View {
CustomLayout(spacing: 0){
ForEach(Array(components.enumerated()), id: \.offset) { _, component in
switch component {
case .text(let text):
Text(verbatim: text).font(.subheadline).multilineTextAlignment(.leading)
case .link(let text, _):
Button {
if let url = URL(string: text) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
} label: {
Text(verbatim: text).foregroundColor(.blue).font(.subheadline)
.multilineTextAlignment(.leading)
}
case .username(let text):
Button(action: {
Text(text)
.font(.subheadline).bold()
.foregroundStyle(.red)
}, label: {
Text(text)
.font(.subheadline).bold()
.multilineTextAlignment(.leading)
.foregroundStyle(.orange)
})
case .hashtag(let hash):
NavigationLink {
Text("\(hash) view")
} label: {
Text(hash)
.italic()
.font(.subheadline).bold()
.foregroundStyle(.blue)
}
}
}
}
}
}
func separateTextAndTags(input: String) -> [String] {
var resultArray: [String] = []
var currentToken = ""
for character in input {
if character == "@" || character == "#" {
if !currentToken.isEmpty {
resultArray.append(currentToken)
currentToken = ""
}
currentToken += String(character)
} else if character.isWhitespace {
if currentToken.count > 1 && (currentToken.hasPrefix("@") || currentToken.hasPrefix("#")) {
resultArray.append(currentToken)
currentToken = ""
}
currentToken += String(character)
} else {
currentToken += String(character)
}
}
if !currentToken.isEmpty {
resultArray.append(currentToken)
}
return resultArray
}
struct CustomLayout: Layout {
var alignment: Alignment = .leading
var spacing: CGFloat = 0
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxWidth = proposal.width ?? 0
var height: CGFloat = 0
let rows = generateRows(maxWidth, proposal, subviews)
for (index, row) in rows.enumerated() {
if index == (rows.count - 1) {
height += row.maxHeight(proposal)
} else {
height += row.maxHeight(proposal) + spacing
}
}
return .init(width: maxWidth, height: height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var origin = bounds.origin
let maxWidth = bounds.width
let rows = generateRows(maxWidth, proposal, subviews)
for row in rows {
let leading: CGFloat = bounds.maxX - maxWidth
let trailing = bounds.maxX - (row.reduce(CGFloat.zero) { partialResult, view in
let width = view.sizeThatFits(proposal).width
if view == row.last {
return partialResult + width
}
return partialResult + width + spacing
})
let center = (trailing + leading) / 2
origin.x = (alignment == .leading ? leading : alignment == .trailing ? trailing : center)
for view in row {
let viewSize = view.sizeThatFits(proposal)
view.place(at: origin, proposal: proposal)
origin.x += (viewSize.width + spacing)
}
origin.y += (row.maxHeight(proposal) + spacing)
}
}
func generateRows(_ maxWidth: CGFloat, _ proposal: ProposedViewSize, _ subviews: Subviews) -> [[LayoutSubviews.Element]] {
var row: [LayoutSubviews.Element] = []
var rows: [[LayoutSubviews.Element]] = []
var origin = CGRect.zero.origin
for view in subviews {
let viewSize = view.sizeThatFits(proposal)
if (origin.x + viewSize.width + spacing) > maxWidth {
rows.append(row)
row.removeAll()
origin.x = 0
row.append(view)
origin.x += (viewSize.width + spacing)
} else {
row.append(view)
origin.x += (viewSize.width + spacing)
}
}
if !row.isEmpty {
rows.append(row)
row.removeAll()
}
return rows
}
}
Ответ или решение
Оптимизация производительности пользовательского интерфейса при работе с текстовым Markdown в SwiftUI
Ваша проблема со снижением производительности при использовании пользовательского текстового Markdown-вида в LazyVStack
связана с высокими затратами на рендеринг и сложными вычислениями в вашем коде. При значительном количестве подвидов, содержащих сложную разметку, в результате возникают "фрейми" и подвисания. Чтобы решить эту проблему, нужно оптимизировать как обработку данных, так и сам интерфейс. Рассмотрим несколько подходов к улучшению производительности.
1. Изменение обработки текста
Сначала необходимо обратить внимание на способ обработки текстовых данных. Ваша текущая реализация разбивает текст на компоненты в init
, используя NSDataDetector
. Это может быть затратным, особенно при обработке больших объемов текста. Вместо этого рассмотрите возможность предварительной обработки текста в отдельном потоке или использования механизма кэширования для хранения результатов:
@State private var componentsCache: [Component] = []
init(text: String, links: [NSTextCheckingResult]) {
self.text = text
if let cachedComponents = componentsCache[text] {
self.components = cachedComponents
} else {
// Обработка текста и добавление в кэш
self.components = processText(text, links: links)
componentsCache[text] = self.components
}
}
private func processText(_ text: String, links: [NSTextCheckingResult]) -> [Component] {
// Ваша текущая логика обработки текста
}
2. Оптимизация рендеринга компонентов
Каждый компонент вашего MarkDownTextView
порождает новую View
, что может привести к задержкам при прокрутке. Постарайтесь минимизировать количество создаваемых представлений, используя статические представления или обновляя только те компоненты, которые действительно изменяются:
- Используйте
Identifiable
, чтобы SwiftUI мог оптимально обновлять компоненты. - Избегайте использования
State
, если это не нужно. Рассмотрите возможность использованияBinding
для передачи данных в подвиды.
Пример изменения кода для обеспечения лучшей идентификации:
struct ComponentView: View {
let component: MarkDownTextView.Component
var body: some View {
switch component {
case .text(let text):
Text(verbatim: text)
.font(.subheadline)
case .link(let text, _):
Button(action: {/* открыть ссылку */}) {
Text(text)
.foregroundColor(.blue)
}
case .username(let text):
// аналогично для username
case .hashtag(let hash):
// аналогично для hashtag
}
}
}
3. Использование draw
вместо layout
Если вы не нуждаетесь в динамическом изменении расположения элементов (например, если длина текста статична), рассматривайте возможность использования draw
метода для отрисовки текста вместо использования сложныхигачи.
4. Проверка и минимизация затрат на Кэширование
Кэширование может значительно улучшить производительность, но если кэш становится слишком большим, он может оказать отрицательное влияние на память. Убедитесь, что ваше кэширование ответственно и содержит только необходимую информацию.
5. Асинхронные операции
Если ваш текст очень длинный, вы можете попробовать обрабатывать тексты в фоновом потоке, используя DispatchQueue.global().async
, а затем возвращаться в главный поток для обновления UI:
DispatchQueue.global().async {
let processedComponents = self.processText(text, links: links)
DispatchQueue.main.async {
self.components = processedComponents
}
}
Заключение
Оптимизация производительности вашего интерфейса при использовании text markdown в SwiftUI требует внимательного подхода к обработке данных и рендерингу компонентов. Использование кэширования, минимизация количества подвидов и отложенная обработка могут помочь значительно улучшить плавность прокрутки и общее восприятие приложения. Важно тщательно анализировать каждый шаг и проверять, как изменения влияют на производительность. Таким образом, вы сможете создать более отзывчивый и эффективный интерфейс для ваших пользователей.