Introduces TextEditor backport to iOS 14+

This commit is contained in:
Shaps Benkau 2023-05-06 23:09:41 +01:00
parent afac0c36c4
commit b092912f24
4 changed files with 742 additions and 0 deletions

View File

@ -0,0 +1,223 @@
import SwiftUI
#if os(iOS)
@available(iOS 14.0, *)
protocol ColorProvider {
var color: UIColor? { get }
}
@available(iOS 14.0, *)
struct AccentColorProvider: ColorProvider {
var color: UIColor? {
if #available(iOS 15, *) {
return .tintColor
} else {
return UIColor(Color.accentColor)
}
}
}
@available(iOS 14.0, *)
struct TintShapeStyle: ColorProvider {
var color: UIColor? {
if #available(iOS 15, *) {
return .tintColor
} else {
return UIColor(Color.accentColor)
}
}
}
@available(iOS 14.0, *)
struct ForegroundStyle: ColorProvider {
var color: UIColor? { .label }
}
@available(iOS 14.0, *)
struct BackgroundStyle: ColorProvider {
var color: UIColor? { .systemBackground }
}
@available(iOS 14.0, *)
struct UICachedDeviceRGBColor: ColorProvider {
var color: UIColor?
init(provider: Any) {
let mirror = Mirror(reflecting: provider)
let red = mirror.descendant("linearRed") as? Float ?? 1
let green = mirror.descendant("linearGreen") as? Float ?? 1
let blue = mirror.descendant("linearBlue") as? Float ?? 1
let opacity = mirror.descendant("opacity") as? Float ?? 1
let cgColor = CGColor(
colorSpace: .init(name: CGColorSpace.genericRGBLinear) ?? CGColorSpaceCreateDeviceRGB(),
components: [.init(red), .init(green), .init(blue), .init(opacity)]
)
color = cgColor.flatMap { UIColor(cgColor: $0) } ?? .label
}
}
@available(iOS 14.0, *)
struct UIDynamicCatalogSystemColor: ColorProvider {
var color: UIColor?
}
@available(iOS 14.0, *)
struct DisplayP3: ColorProvider {
var color: UIColor?
init(provider: Any) {
let mirror = Mirror(reflecting: provider)
let red = mirror.descendant("red") as? CGFloat ?? 1
let green = mirror.descendant("green") as? CGFloat ?? 1
let blue = mirror.descendant("blue") as? CGFloat ?? 1
let opacity = mirror.descendant("opacity") as? Float ?? 1
let cgColor = CGColor(
colorSpace: .init(name: CGColorSpace.displayP3) ?? CGColorSpaceCreateDeviceRGB(),
components: [.init(red), .init(green), .init(blue)]
)
color = cgColor.flatMap { UIColor(cgColor: $0).withAlphaComponent(.init(opacity)) } ?? .label
}
}
@available(iOS 14.0, *)
struct OffsetShapeStyle<T: ColorProvider>: ColorProvider {
var color: UIColor?
}
@available(iOS 14.0, *)
extension OffsetShapeStyle<SystemColorsStyle> {
init(provider: Any) {
let mirror = Mirror(reflecting: provider)
let offset = mirror.descendant("offset") as? Int ?? 0
switch offset {
case 1: color = .secondaryLabel
case 2: color = .tertiaryLabel
case 3: color = .quaternaryLabel
default: color = .label
}
print(offset)
}
}
@available(iOS 14.0, *)
struct SelectionShapeStyle: ColorProvider {
var color: UIColor? { nil }
}
@available(iOS 14.0, *)
struct SystemColorsStyle: ColorProvider {
let style: SystemColorType.Style
var color: UIColor? { style.color }
}
@available(iOS 14.0, *)
struct SystemColorType: ColorProvider {
enum Style: String {
case primary, secondary
case black, white, gray, clear
case blue, brown, cyan, green
case indigo, mint, orange, pink
case purple, red, teal, yellow
var color: UIColor {
switch self {
case .black: return .black
case .white: return .white
case .primary: return .label
case .secondary: return .secondaryLabel
case .blue: return .systemBlue
case .brown: return .systemBrown
case .clear: return .clear
case .cyan:
if #available(iOS 15, *) {
return .systemCyan
} else {
return .systemTeal
}
case .gray: return .systemGray
case .green: return .systemGreen
case .indigo: return .systemIndigo
case .mint:
if #available(iOS 15, *) {
return .systemMint
} else {
return .systemTeal
}
case .orange: return .systemOrange
case .pink: return .systemPink
case .purple: return .systemPurple
case .red: return .systemRed
case .teal: return .systemTeal
case .yellow: return .systemYellow
}
}
}
let style: Style
var color: UIColor? { style.color }
}
@available(iOS 14.0, *)
func colorProvider(from values: EnvironmentValues) -> Any? {
let mirror = Mirror(reflecting: values)
guard let provider = mirror.descendant(
"_plist", "elements", "some", "value",
"some", "storage", "box", "base"
) else {
return nil
}
return provider
}
@available(iOS 14.0, *)
func isAccentColor(provider: Any) -> Bool {
String(describing: type(of: provider)) == String(describing: AccentColorProvider.self)
}
@available(iOS 14.0, *)
func resolveColor(_ values: EnvironmentValues) -> UIColor? {
guard let provider = colorProvider(from: values) else { return nil }
return resolveColorProvider(provider)?.color
}
@available(iOS 14.0, *)
func resolveColorProvider(_ provider: Any) -> ColorProvider? {
switch String(describing: type(of: provider)) {
case String(describing: SelectionShapeStyle.self):
return SelectionShapeStyle()
case String(describing: AccentColorProvider.self):
return AccentColorProvider()
case String(describing: TintShapeStyle.self):
return TintShapeStyle()
case String(describing: ForegroundStyle.self):
return ForegroundStyle()
case String(describing: BackgroundStyle.self):
return BackgroundStyle()
case String(describing: OffsetShapeStyle<SystemColorsStyle>.self):
return OffsetShapeStyle<SystemColorsStyle>(provider: provider)
case String(describing: SystemColorType.self):
return SystemColorType(style: .init(rawValue: "\(provider)") ?? .primary)
case String(describing: SystemColorsStyle.self):
return SystemColorsStyle(style: .init(rawValue: "\(provider)") ?? .primary)
case String(describing: UICachedDeviceRGBColor.self):
return UICachedDeviceRGBColor(provider: provider)
case String(describing: UIDynamicCatalogSystemColor.self):
return UIDynamicCatalogSystemColor()
case String(describing: DisplayP3.self):
return DisplayP3(provider: provider)
case "Resolved":
return UICachedDeviceRGBColor(provider: provider)
default:
print("Unhandled color provider: \(String(describing: type(of: provider)))")
return nil
}
}
func printMirror(_ value: Any) {
let mirror = Mirror(reflecting: value)
print(mirror.subjectType)
for child in mirror.children {
print(child)
}
}
#endif

View File

@ -0,0 +1,301 @@
import SwiftUI
#if os(iOS)
@available(iOS 14.0, *)
protocol FontProvider {
func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor
}
@available(iOS 14.0, *)
extension FontProvider {
func font(with traitCollection: UITraitCollection?) -> UIFont {
return UIFont(descriptor: fontDescriptor(with: traitCollection), size: 0)
}
}
@available(iOS 14.0, *)
protocol FontModifier {
func modify(_ fontDescriptor: inout UIFontDescriptor)
}
@available(iOS 14.0, *)
protocol StaticFontModifier: FontModifier {
init()
}
@available(iOS 14.0, *)
struct TextStyleProvider: FontProvider {
var style: UIFont.TextStyle
var design: UIFontDescriptor.SystemDesign?
func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor {
UIFont
.preferredFont(forTextStyle: style, compatibleWith: traitCollection)
.fontDescriptor
.withDesign(design ?? .default)!
}
}
@available(iOS 14.0, *)
struct SystemProvider: FontProvider {
var size: CGFloat
var design: UIFontDescriptor.SystemDesign
var weight: UIFont.Weight?
func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor {
UIFont.systemFont(ofSize: size)
.fontDescriptor
.addingAttributes([
.traits: [
UIFontDescriptor.TraitKey.weight: (weight ?? .regular).rawValue
]
])
.withDesign(design)!
}
}
@available(iOS 14.0, *)
struct NamedProvider: FontProvider {
var name: String
var size: CGFloat
var textStyle: UIFont.TextStyle?
func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor {
if let textStyle = textStyle {
let metrics = UIFontMetrics(forTextStyle: textStyle )
return UIFontDescriptor(fontAttributes: [
.family: name,
.size: metrics.scaledValue(for: size, compatibleWith: traitCollection)
])
} else {
return UIFontDescriptor(fontAttributes: [
.family: name,
.size: size
])
}
}
}
@available(iOS 14.0, *)
struct StaticModifierProvider<M: StaticFontModifier>: FontProvider {
var base: FontProvider
func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor {
var descriptor = base.fontDescriptor(with: traitCollection)
M().modify(&descriptor)
return descriptor
}
}
@available(iOS 14.0, *)
struct ModifierProvider<M: FontModifier>: FontProvider {
var base: FontProvider
var modifier: M
func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor {
var descriptor = base.fontDescriptor(with: traitCollection)
modifier.modify(&descriptor)
return descriptor
}
}
@available(iOS 14.0, *)
struct ItalicModifier: StaticFontModifier {
init() { }
func modify(_ fontDescriptor: inout UIFontDescriptor) {
var traits = fontDescriptor.symbolicTraits
traits.insert(.traitItalic)
fontDescriptor = fontDescriptor.addingAttributes([
.traits: [
UIFontDescriptor.TraitKey.symbolic: traits.rawValue
]
])
}
}
@available(iOS 14.0, *)
struct BoldModifier: StaticFontModifier {
init() { }
func modify(_ fontDescriptor: inout UIFontDescriptor) {
var traits = fontDescriptor.symbolicTraits
traits.insert(.traitBold)
fontDescriptor = fontDescriptor.addingAttributes([
.traits: [
UIFontDescriptor.TraitKey.symbolic: traits.rawValue,
UIFontDescriptor.TraitKey.weight: nil
]
])
}
}
@available(iOS 14.0, *)
struct WeightModifier: FontModifier {
var weight: UIFont.Weight?
func modify(_ fontDescriptor: inout UIFontDescriptor) {
fontDescriptor = fontDescriptor.addingAttributes([
.traits: [
UIFontDescriptor.TraitKey.weight: (weight ?? .regular).rawValue
]
])
}
}
@available(iOS 14.0, *)
struct LeadingModifier: FontModifier {
var leading: Font.Leading?
func modify(_ fontDescriptor: inout UIFontDescriptor) {
var traits = fontDescriptor.symbolicTraits
switch leading {
case .loose:
traits.insert(.traitLooseLeading)
traits.remove(.traitTightLeading)
case .tight:
traits.remove(.traitLooseLeading)
traits.insert(.traitTightLeading)
default:
traits.remove(.traitLooseLeading)
traits.remove(.traitTightLeading)
}
fontDescriptor = fontDescriptor.addingAttributes([
.traits: [
UIFontDescriptor.TraitKey.symbolic: traits.rawValue
]
])
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
@available(iOS 14.0, *)
func resolveFont(_ font: Font) -> FontProvider? {
let mirror = Mirror(reflecting: font)
guard let provider = mirror.descendant("provider", "base") else {
return nil
}
return resolveFontProvider(provider)
}
@available(iOS 14.0, *)
func resolveFontProvider(_ provider: Any) -> FontProvider? {
let mirror = Mirror(reflecting: provider)
switch String(describing: type(of: provider)) {
case String(describing: TextStyleProvider.self):
guard let style = mirror.descendant("style") as? Font.TextStyle else {
return nil
}
let design = mirror.descendant("design") as? Font.Design
return TextStyleProvider(style: style.uiTextStyle, design: design?.uiSystemDesign)
case String(describing: StaticModifierProvider<ItalicModifier>.self):
guard let base = mirror.descendant("base", "provider", "base") else {
return nil
}
return resolveFontProvider(base).map(StaticModifierProvider<ItalicModifier>.init)
case String(describing: StaticModifierProvider<BoldModifier>.self):
guard let base = mirror.descendant("base", "provider", "base") else {
return nil
}
return resolveFontProvider(base).map(StaticModifierProvider<BoldModifier>.init)
case String(describing: ModifierProvider<WeightModifier>.self):
guard let base = mirror.descendant("base", "provider", "base") else {
return nil
}
let weight = mirror.descendant("modifier", "weight") as? Font.Weight
let modifier = WeightModifier(weight: weight?.uiFontWeight)
return resolveFontProvider(base).map { ModifierProvider(base: $0, modifier: modifier) }
case String(describing: ModifierProvider<LeadingModifier>.self):
guard let base = mirror.descendant("base", "provider", "base") else {
return nil
}
let leading = mirror.descendant("modifier", "leading") as? Font.Leading
let modifier = LeadingModifier(leading: leading)
return resolveFontProvider(base).map { ModifierProvider(base: $0, modifier: modifier) }
case String(describing: SystemProvider.self):
guard let size = mirror.descendant("size") as? CGFloat,
let design = mirror.descendant("design") as? Font.Design else {
return nil
}
let weight = mirror.descendant("weight") as? Font.Weight
return SystemProvider(size: size, design: design.uiSystemDesign, weight: weight?.uiFontWeight)
case String(describing: NamedProvider.self):
guard let name = mirror.descendant("name") as? String,
let size = mirror.descendant("size") as? CGFloat else {
return nil
}
let textStyle = mirror.descendant("textStyle") as? Font.TextStyle
return NamedProvider(name: name, size: size, textStyle: textStyle?.uiTextStyle)
default:
// Not exhaustive, more providers need to be handled here.
return nil
}
}
extension Font.Weight {
var uiFontWeight: UIFont.Weight {
switch self {
case .ultraLight: return .ultraLight
case .light: return .light
case .thin: return .thin
case .medium: return .medium
case .semibold: return .semibold
case .bold: return .bold
case .heavy: return .heavy
case .black: return .black
default: return .regular
}
}
}
extension Font.Design {
var uiSystemDesign: UIFontDescriptor.SystemDesign {
switch self {
case .monospaced: return .monospaced
case .rounded: return .rounded
case .serif: return .serif
default: return .`default`
}
}
}
extension Font.TextStyle {
var uiTextStyle: UIFont.TextStyle {
switch self {
case .caption: return .caption1
case .caption2: return .caption2
case .footnote: return .footnote
case .callout: return .callout
case .subheadline: return .subheadline
case .headline: return .headline
case .title: return .title1
case .title2: return .title2
case .title3: return .title3
case .largeTitle: return .largeTitle
default: return .body
}
}
}
extension LegibilityWeight {
var uiLegibilityWeight: UILegibilityWeight {
switch self {
case .bold: return .bold
default: return .regular
}
}
}
#endif

View File

@ -0,0 +1,81 @@
import SwiftUI
#if os(iOS)
@available(iOS 15, *)
extension DynamicTypeSize {
var uiContentSizeCategory: UIContentSizeCategory {
switch self {
case .xSmall: return .extraSmall
case .small: return .small
case .medium: return .medium
case .large: return .large
case .xLarge: return .extraLarge
case .xxLarge: return .extraExtraLarge
case .xxxLarge: return .extraExtraExtraLarge
case .accessibility1: return .accessibilityMedium
case .accessibility2: return .accessibilityLarge
case .accessibility3: return .accessibilityExtraLarge
case .accessibility4: return .accessibilityExtraExtraLarge
case .accessibility5: return .accessibilityExtraExtraExtraLarge
default: return .large
}
}
}
extension LayoutDirection {
var uiLayoutDirection: UITraitEnvironmentLayoutDirection {
switch self {
case .leftToRight: return .leftToRight
case .rightToLeft: return .rightToLeft
default: return .leftToRight
}
}
}
extension TextAlignment {
var nsTextAlignment: NSTextAlignment {
switch self {
case .leading: return .left
case .center: return .center
case .trailing: return .right
}
}
}
extension ContentSizeCategory {
var uiContentSizeCategory: UIContentSizeCategory {
switch self {
case .extraSmall: return .extraSmall
case .small: return .small
case .medium: return .medium
case .large: return .large
case .extraLarge: return .extraLarge
case .extraExtraLarge: return .extraExtraLarge
case .extraExtraExtraLarge: return .extraExtraExtraLarge
case .accessibilityMedium: return .accessibilityMedium
case .accessibilityLarge: return .accessibilityLarge
case .accessibilityExtraLarge: return .accessibilityExtraLarge
case .accessibilityExtraExtraLarge: return .accessibilityExtraExtraLarge
case .accessibilityExtraExtraExtraLarge: return .accessibilityExtraExtraExtraLarge
default: return .large
}
}
}
extension EnvironmentValues {
var uiTraitCollection: UITraitCollection {
var traits: [UITraitCollection] = [
.init(legibilityWeight: legibilityWeight?.uiLegibilityWeight ?? .unspecified),
.init(layoutDirection: layoutDirection.uiLayoutDirection),
]
if #available(iOS 15, *) {
traits.append(.init(preferredContentSizeCategory: dynamicTypeSize.uiContentSizeCategory))
} else {
traits.append(.init(preferredContentSizeCategory: sizeCategory.uiContentSizeCategory))
}
return UITraitCollection(traitsFrom: traits)
}
}
#endif

View File

@ -0,0 +1,137 @@
import SwiftUI
#if os(iOS)
@available(iOS 14.0, *)
extension Backport where Wrapped == Any {
/// A view that can display and edit long-form text.
///
/// A text editor view allows you to display and edit multiline, scrollable text in your apps user interface. By default, the text editor view styles the text using characteristics inherited from the environment, like font(_:), foregroundColor(_:), and multilineTextAlignment(_:).
///
/// You create a text editor by adding a TextEditor instance to the body of your view, and initialize it by passing in a Binding to a string variable in your app.
///
/// To style the text, use the standard view modifiers to configure a system font, set a custom font, or change the color of the views text.
/// In this example, the view renders the editors text in gray with a custom font:
///
/// struct TextEditingView: View {
/// @State private var fullText: String = "This is some editable text..."
///
/// var body: some View {
/// TextEditor(text: $fullText)
/// .foregroundColor(Color.gray)
/// .font(.custom("HelveticaNeue", size: 13))
/// .lineSpacing(5)
/// }
/// }
///
/// > The order of some modifiers matter with this implementation. PLEASE REPORT ISSUES ON THE REPO!
///
/// Specifically, its recommended to place `foregroundColor` modifiers BEFORE `font` modifiers to ensure things work as expected.
///
public struct TextEditor: View {
@Environment(\.self) private var environment
@Binding var text: String
/// Creates a plain text editor.
///
/// Use a TextEditor instance to create a view in which users can enter and edit long-form text.
/// In this example, the text editor renders gray text using the 13 point Helvetica Neue font with 5 points of spacing between each line:
///
/// struct TextEditingView: View {
/// @State private var fullText: String = "This is some editable text..."
///
/// var body: some View {
/// TextEditor(text: $fullText)
/// .foregroundColor(Color.gray)
/// .font(.custom("HelveticaNeue", size: 13))
/// .lineSpacing(5)
/// }
/// }
///
/// You can define the styling for the text within the view, including the text color, font, and line spacing. You define these styles by applying standard view modifiers to the view. The default text editor doesnt support rich text, such as styling of individual elements within the editors view. The styles you set apply globally to all text in the view.
///
/// - Parameter text: A `Binding` to the variable containing the text to edit.
public init(text: Binding<String>) {
_text = text
}
private var isAccented: Bool {
guard let provider = colorProvider(from: environment) else { return false }
return isAccentColor(provider: provider)
}
public var body: some View {
Representable(parent: self)
.blendMode(!environment.isEnabled && isAccented ? .luminosity: .normal)
}
struct Representable: UIViewRepresentable {
let parent: TextEditor
func makeCoordinator() -> Coordinator {
.init(parent: parent)
}
func makeUIView(context: Context) -> UIView {
context.coordinator.view
}
func updateUIView(_ view: UIView, context: Context) {
context.coordinator.update(parent: parent)
}
}
final class Coordinator: NSObject, UITextViewDelegate {
let view = UITextView(frame: .zero)
var parent: TextEditor
init(parent: TextEditor) {
self.parent = parent
}
func update(parent: TextEditor) {
self.parent = parent
guard view.text != parent.text else { return }
view.delegate = self
view.adjustsFontForContentSizeCategory = true
view.autocapitalizationType = .allCharacters
view.backgroundColor = .clear
switch parent.environment.disableAutocorrection {
case .some(true):
view.autocorrectionType = .yes
case .some(false):
view.autocorrectionType = .no
case nil:
view.autocorrectionType = .default
}
let style = NSMutableParagraphStyle()
style.lineSpacing = parent.environment.lineSpacing
style.alignment = parent.environment.multilineTextAlignment.nsTextAlignment
view.textColor = resolveColor(parent.environment) ?? .label
view.font = resolveFont(parent.environment.font ?? .body)?
.font(with: parent.environment.uiTraitCollection)
?? .preferredFont(forTextStyle: .body)
view.typingAttributes = [
.paragraphStyle: style,
.foregroundColor: view.textColor ?? .label,
.font: view.font ?? .preferredFont(forTextStyle: .body)
]
view.text = parent.text
}
func textViewDidChange(_ textView: UITextView) {
DispatchQueue.main.async { [weak self] in
self?.parent.text = textView.text
}
}
}
}
}
#endif