Extension that converts Strings with basic HTML tags to SwiftUI's Text (Supports SwiftUI 1.0 / iOS 13.0).
This file contains 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
// | |
// Font+Formatting.swift | |
// | |
// Created by Felix Mau on 12.11.20. | |
// Copyright © 2020 Felix Mau. All rights reserved. | |
// | |
import Foundation | |
import SwiftUI | |
/// The supported tags inside a string we can format. | |
enum TextFormattingTag: String { | |
case strong | |
case em | |
// MARK: - Public properties | |
var openingTag: String { | |
"<\(rawValue)>" | |
} | |
var closingTag: String { | |
"</\(rawValue)>" | |
} | |
} | |
extension Text { | |
/// A text view that displays a string literal. | |
/// Furthermore the text between the given `formattingTag` will be modified` using the given `fontModifier`. | |
/// | |
/// - Parameters: | |
/// - text: The string to modify, e.g. `<strong>Lorem</strong> ipsum dolor sit <strong>amet</strong>` | |
/// - formattingTag: The tag to search for e.g. `.strong` | |
/// - fontModifier: The closure to be applied to the text between the given formatting tag. | |
/// | |
/// - Returns: A formatted `Text` with the applied font modifier between the text. | |
/// | |
/// - Note: Currently we only support converting one formatting tag per call. | |
init(_ text: String, replaceTag formattingTag: TextFormattingTag, with fontModifier: (Text) -> Text) { | |
let textSeparatedByTags = text.separatedBy(openingTag: formattingTag.openingTag, | |
closingTag: formattingTag.closingTag) | |
// Even with an empty string, we'll always have at least one entry. | |
// Therefore we don't have to check the length of the array here. | |
let firstText = Text(textSeparatedByTags[0].text) | |
// Append and format all remaining tags. | |
self = textSeparatedByTags | |
.dropFirst() | |
.reduce(firstText) { res, textSeparatedByTag in | |
var textForCurrentTag = Text(textSeparatedByTag.text) | |
if textSeparatedByTag.isBetweenTags { | |
textForCurrentTag = fontModifier(textForCurrentTag) | |
} | |
return res + textForCurrentTag | |
} | |
} | |
} | |
// MARK: - Helpers | |
private extension String { | |
// MARK: - Types | |
struct TagSeparated { | |
let text: String | |
let isBetweenTags: Bool | |
} | |
// MARK: - Public methods | |
/// Splits the current string via the opening- and closing-tag. | |
/// | |
/// E.g. having `self` set to `Lorem <strong>Ipsum</strong> Dolor`, `openingTag` set to `<strong>` and `closingTag` set to `</strong>` | |
/// will produce the following result: | |
/// | |
/// 0: `TagSeparated(text: "Lorem ", isBetweenTags: false)` | |
/// 1: `TagSeparated(text: "Ipsum", isBetweenTags: true)` | |
/// 2: `TagSeparated(text: " Dolor", isBetweenTags: false)` | |
func separatedBy(openingTag: String, closingTag: String) -> [TagSeparated] { | |
var result = [TagSeparated]() | |
// Split via opening tag, e.g. "Lorem <strong>Ipsum</strong> Dolor" into: | |
// 0: "Lorem " | |
// 1: "Ipsum</strong> Dolor" | |
let separatedByOpeningTags = components(separatedBy: openingTag) | |
separatedByOpeningTags.forEach { separatedByOpeningTag in | |
// Split via closing tag, e.g. "Ipsum</strong> Dolor" into: | |
// 0: "Ipsum" | |
// 1: " Dolor" | |
let separatedByClosingTags = separatedByOpeningTag.components(separatedBy: closingTag) | |
switch separatedByClosingTags.count { | |
case 1: | |
// Handle start of text e.g. "Lorem " of "Lorem <strong>Ipsum</strong>" | |
result.append( | |
TagSeparated(text: separatedByClosingTags[0], isBetweenTags: false) | |
) | |
case 2: | |
// Using the example shown above (line 98) this would contain "Ipsum". | |
result.append( | |
TagSeparated(text: separatedByClosingTags[0], isBetweenTags: true) | |
) | |
// Using the example shown above (line 99) this would contain " Dolor". | |
result.append( | |
TagSeparated(text: separatedByClosingTags[1], isBetweenTags: false) | |
) | |
default: | |
// We have an invalid formatted string, and therefore skip here. | |
break | |
} | |
} | |
return result | |
} | |
} |
The following code
Text("Lorem <em>Ipsum</em> Dolor", replaceTag: .strong, with: { $0.italic() })
will produce
Lorem Ipsum Dolor
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The following code
will produce