Connect with us

Разработка

Расширяем Text в SwiftUI с помощью динамической стилизации содержимого

Эти возможности и так хороши, но мне захотелось найти более простой способ добиться того же результата. И мне кажется, что я придумал кое-что довольно приятное.

Опубликовано

/

     
     

В этой статье мы рассмотрим, как расширить SwiftUI Text с помощью кастомизации любых частей его содержимого, как отдельных слов, так и более длинных сегментов.

Нативные возможности текста

Представление Text в SwiftUI за прошедшие годы было расширено удивительными возможностями, и теперь оно позволяет нам делать многое другое, что изначально было невозможно, например, рендерить Markdown, как здесь:

Text("This is [Markdown](https://www.markdownguide.org) with *some* **formatting**")
    .foregroundStyle(.blue)
    .tint(.yellow)
    .font(.largeTitle)

Если мы просто передадим Markdown строку, Text правильно отобразит форматирование и ссылки с некоторыми параметрами стилизации.

Расширяем Text в SwiftUI с помощью динамической стилизации содержимого

Как видно из кода, вы можете применить foregroundStyle для цвета текста, tint для цвета ссылок, а также использовать синтаксис Markdown, чтобы сделать любую часть текста подчеркнутой, курсивной или жирной.

Несколько лет назад появилась возможность объединять несколько представлений Text в одно представление, например, так:

Text("I") +
Text(" **love** (❤️) ").foregroundStyle(.red) +
Text("SwiftUI!")

В сочетании с Markdown это дает нам еще больше власти над тем, как будет отображаться наш текст, поскольку мы можем применять различные модификаторы вида к различным частям результирующего текста.

Эти возможности и так хороши, но мне захотелось найти более простой способ добиться того же результата. И мне кажется, что я придумал кое-что довольно приятное.

Замены текста

Давайте воспользуемся возможностью SwiftUI объединять несколько представлений текста, чтобы сделать одну или несколько замен при создании представления текста.

Я хочу иметь простой способ передачи одной замены и другой способ передачи нескольких замен.

Вот как я хочу, чтобы это выглядело:

public struct TextReplacementView: View {
    
    /// Creates a text view with a text and a single replacement.
    init(
        _ text: String,
        replace: String,
        with replacement: @escaping (String) -> Text
    ) {
        self.init(text, replacements: [replace: replacement])
    }
    
    /// Creates a text view with a text and a multiple replacements.
    init(
        _ text: String,
        replacements: [String: (String) -> Text]
    ) {
        // Insert magic here
    }
}

Затем я создал функцию processReplacements, которая может найти одно или несколько совпадений в предоставленном тексте и заменить их на пользовательские представления Text, используя подменные конструкторы текста:

private extension Text {
    
    /// Process the replacements in a deterministic way
    static func processReplacements(
        in text: String,
        with replacements: [String: (String) -> Text]
    ) -> Text {
        
        // Create a structure to track replacement positions
        struct Replacement {
            let range: Range<String.Index>
            let pattern: String
            let replacementFunc: (String) -> Text
        }
        
        // Find all occurrences of all patterns
        var allReplacements: [Replacement] = []
        
        // Find text ranges for all specified replacements
        for (pattern, replacementFunc) in replacements {
            var searchRange = text.startIndex..<text.endIndex
            
            while let range = text.range(of: pattern, range: searchRange) {
                allReplacements.append(Replacement(
                    range: range,
                    pattern: pattern,
                    replacementFunc: replacementFunc
                ))
                searchRange = range.upperBound..<text.endIndex
            }
        }
        
        // Sort replacements by position, then by length
        // Longer patterns are handled first to handle overlaps
        allReplacements.sort { first, second in
            if first.range.lowerBound != second.range.lowerBound {
                return first.range.lowerBound < second.range.lowerBound
            }
            return first.pattern.count > second.pattern.count
        }
        
        // Process the text with non-overlapping replacements
        var result = Text("")
        var currentIndex = text.startIndex
        
        // Remove overlapping replacements
        var validReplacements: [Replacement] = []
        var lastEnd: String.Index?
        
        for replacement in allReplacements {
            if let lastEnd = lastEnd, replacement.range.lowerBound < lastEnd {
                continue // Skip overlapping replacement
            }
            validReplacements.append(replacement)
            lastEnd = replacement.range.upperBound
        }
        
        // Apply the valid replacements
        for replacement in validReplacements {
            // Add text before the replacement
            if currentIndex < replacement.range.lowerBound {
                let beforeText = text[currentIndex..<replacement.range.lowerBound]
                result = result + Text(String(beforeText))
            }
            
            // Add the replacement
            result = result + replacement.replacementFunc(replacement.pattern)
            currentIndex = replacement.range.upperBound
        }
        
        // Add any remaining text
        if currentIndex < text.endIndex {
            let remainingText = text[currentIndex..<text.endIndex]
            result = result + Text(String(remainingText))
        }
        
        return result
    }
}

Функция перебирает все предоставленные замены, чтобы найти все совпадающие диапазоны в тексте, а затем выводит либо обычный Text, либо Text с заменами.

Результирующие инициализаторы Text берут текст и одну или несколько замен (String) -> Text, и теперь могут быть использованы очень просто как тут:

Text(
    "Some text",
    replace: "text",
    with: { Text($0).foregroundStyle(.red) }
)

Или сложно, как тут:

Text(
    "TextReplacements is a SwiftUI library that extends the Text view with ways to customize any parts of its text. The result is a Text with customized segments that can flow nicely over multiple lines.",
    replacements: [
        "TextReplacements": {
            Text($0)
                .font(.title)
                .fontWeight(.black)
                .fontDesign(.rounded)
                .foregroundColor(.green)
        },
        "SwiftUI": {
            Text($0)
                .font(.headline)
                .fontWeight(.black)
                .fontDesign(.rounded)
                .foregroundColor(.blue)
        },
        "Text": {
            Text($0)
                .fontWeight(.black)
                .fontDesign(.rounded)
                .foregroundColor(.black.opacity(0.6))
        },
        "customize": {
            Text($0)
                .italic()
                .underline()
                .font(.body)
                .fontWeight(.heavy)
                .fontDesign(.monospaced)
                .foregroundColor(.purple)
        },
        "par": {
            Text($0)
                .font(.headline)
                .fontWeight(.black)
                .fontDesign(.rounded)
                .foregroundColor(.red)
        },
        "can flow nicely over multiple lines": {
            Text($0)
                .foregroundColor(.orange)
        }
    ]
)
.padding()
#if os(visionOS)
.frame(maxWidth: 350)
.background(.ultraThickMaterial)
.background(.white.opacity(0.5))
.clipShape(.rect(cornerRadius: 10))
.padding()
.scaleEffect(2)
#endif

В результате можно кастомизировать как одно слово, так и большой кусок текста, и он красиво перетекает из строи в строку:

Расширяем Text в SwiftUI с помощью динамической стилизации содержимого

Поскольку замены представляют собой билдеры (String) -> Text, этот подход поддерживает только модификаторы представления, возвращающие Text. Это означает, что модификаторы типа .backgroundStyle не работают.

Если вы знаете способ обойти это или хотите исследовать его, просто дайте мне знать. Возможно, существуют модификаторы фона, генерирующие Text, о которых я не знаю?

Заключение

Инициализаторы Text, которые мы создали в этом посте, позволяют нам настраивать рендеринг любой части текста. Я создал проект с открытым исходным кодом, который позволит вам добавить эти функции в любое приложение.

Библиотека TextReplacements работает на всех платформах, вплоть до iOS 13, а это значит, что она работает на iOS, macOS, tvOS, watchOS и visionOS.

Я надеюсь, что вам понравится использовать TextReplacements. Хорошего стайлинга!

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: