├── .gitignore ├── Package.swift ├── LICENSE.md ├── Sources └── TextView │ ├── Representable.swift │ ├── UIFont.swift │ ├── Coordinator.swift │ ├── Modifiers.swift │ └── TextView.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TextView", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | .library( 13 | name: "TextView", 14 | targets: ["TextView"] 15 | ), 16 | ], 17 | targets: [ 18 | .target(name: "TextView") 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Shaps Benkau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Sources/TextView/Representable.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension TextView { 4 | struct Representable: UIViewRepresentable { 5 | 6 | @Binding var text: NSAttributedString 7 | @Binding var calculatedHeight: CGFloat 8 | 9 | let foregroundColor: UIColor 10 | let autocapitalization: UITextAutocapitalizationType 11 | var multilineTextAlignment: TextAlignment 12 | let font: UIFont 13 | let returnKeyType: UIReturnKeyType? 14 | let clearsOnInsertion: Bool 15 | let autocorrection: UITextAutocorrectionType 16 | let truncationMode: NSLineBreakMode 17 | let isEditable: Bool 18 | let isSelectable: Bool 19 | let isScrollingEnabled: Bool 20 | let enablesReturnKeyAutomatically: Bool? 21 | var autoDetectionTypes: UIDataDetectorTypes = [] 22 | var allowsRichText: Bool 23 | 24 | var onEditingChanged: (() -> Void)? 25 | var shouldEditInRange: ((Range, String) -> Bool)? 26 | var onCommit: (() -> Void)? 27 | 28 | func makeUIView(context: Context) -> UIKitTextView { 29 | context.coordinator.textView 30 | } 31 | 32 | func updateUIView(_ view: UIKitTextView, context: Context) { 33 | context.coordinator.update(representable: self) 34 | } 35 | 36 | @discardableResult func makeCoordinator() -> Coordinator { 37 | Coordinator( 38 | text: $text, 39 | calculatedHeight: $calculatedHeight, 40 | shouldEditInRange: shouldEditInRange, 41 | onEditingChanged: onEditingChanged, 42 | onCommit: onCommit 43 | ) 44 | } 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This library is now deprecated in favour of a more complete and SwiftUI friendly `TextEditor` backport: 4 | 5 | https://github.com/shaps80/SwiftUIBackports 6 | > See it in action here: https://twitter.com/shaps/status/1654972428286668800?s=20 7 | 8 | ![TextEditor Backport](https://pbs.twimg.com/media/FvekWp2XwAICDRq?format=jpg&name=large) 9 | 10 | --- 11 | 12 | # TextView 13 | 14 | > Also available as a part of my [SwiftUI+ Collection](https://benkau.com/packages.json) – just add it to Xcode 13+ 15 | 16 | Provides a SwiftUI multi-line TextView implementation with support for iOS v13+ 17 | 18 | ## WIP 19 | 20 | - [ ] Improved formatting support 21 | 22 | ## Features 23 | 24 | - Configure all properties via modifiers 25 | - Multi-line text 26 | - Placeholder 27 | - No predefined design, full-flexibility to design in Swift UI 28 | - UIFont extensions to give you SwiftUI Font APIs 29 | - Auto-sizes height to fit content as you type 30 | 31 | ## Example 32 | 33 | ```swift 34 | TextView($text) 35 | .placeholder("Enter some text") { view in 36 | view.foregroundColor(.gray) 37 | } 38 | .padding(10) 39 | .overlay( 40 | RoundedRectangle(cornerRadius: 10) 41 | .stroke(lineWidth: 1) 42 | .foregroundColor(Color(.placeholderText)) 43 | ) 44 | .padding() 45 | ``` 46 | 47 | ## Installation 48 | 49 | The code is packaged as a framework. You can install manually (by copying the files in the `Sources` directory) or using Swift Package Manager (**preferred**) 50 | 51 | To install using Swift Package Manager, add this to the `dependencies` section of your `Package.swift` file: 52 | 53 | `.package(url: "https://github.com/SwiftUI-Plus/TextView.git", .upToNextMinor(from: "1.0.0"))` 54 | 55 | ## Other Packages 56 | 57 | If you want easy access to this and more packages, add the following collection to your Xcode 13+ configuration: 58 | 59 | `https://benkau.com/packages.json` 60 | -------------------------------------------------------------------------------- /Sources/TextView/UIFont.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension UIFont { 4 | static var caption2: UIFont = .preferredFont(forTextStyle: .caption2) 5 | static var caption: UIFont = .preferredFont(forTextStyle: .caption1) 6 | static var footnote: UIFont = .preferredFont(forTextStyle: .footnote) 7 | static var callout: UIFont = .preferredFont(forTextStyle: .callout) 8 | static var body: UIFont = .preferredFont(forTextStyle: .body) 9 | static var subheadline: UIFont = .preferredFont(forTextStyle: .subheadline) 10 | static var headline: UIFont = .preferredFont(forTextStyle: .headline) 11 | static var title3: UIFont = .preferredFont(forTextStyle: .title3) 12 | static var title2: UIFont = .preferredFont(forTextStyle: .title2) 13 | static var title: UIFont = .preferredFont(forTextStyle: .title1) 14 | static var largeTitle: UIFont = .preferredFont(forTextStyle: .largeTitle) 15 | } 16 | 17 | public extension UIFont { 18 | 19 | enum Leading { 20 | case loose 21 | case tight 22 | } 23 | 24 | private func addingAttributes(_ attributes: [UIFontDescriptor.AttributeName: Any]) -> UIFont { 25 | return UIFont(descriptor: fontDescriptor.addingAttributes(attributes), size: pointSize) 26 | } 27 | 28 | static func system(size: CGFloat, weight: UIFont.Weight, design: UIFontDescriptor.SystemDesign = .default) -> UIFont { 29 | let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor 30 | .addingAttributes([ 31 | UIFontDescriptor.AttributeName.traits: [ 32 | UIFontDescriptor.TraitKey.weight: weight.rawValue 33 | ] 34 | ]).withDesign(design)! 35 | return UIFont(descriptor: descriptor, size: size) 36 | } 37 | 38 | static func system(_ style: UIFont.TextStyle, design: UIFontDescriptor.SystemDesign = .default) -> UIFont { 39 | let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style).withDesign(design)! 40 | return UIFont(descriptor: descriptor, size: 0) 41 | } 42 | 43 | func weight(_ weight: UIFont.Weight) -> UIFont { 44 | return addingAttributes([ 45 | UIFontDescriptor.AttributeName.traits: [ 46 | UIFontDescriptor.TraitKey.weight: weight.rawValue 47 | ] 48 | ]) 49 | } 50 | 51 | func italic() -> UIFont { 52 | let descriptor = fontDescriptor.withSymbolicTraits(.traitItalic)! 53 | return UIFont(descriptor: descriptor, size: 0) 54 | } 55 | 56 | func bold() -> UIFont { 57 | let descriptor = fontDescriptor.withSymbolicTraits(.traitBold)! 58 | return UIFont(descriptor: descriptor, size: 0) 59 | } 60 | 61 | func leading(_ leading: Leading) -> UIFont { 62 | let descriptor = fontDescriptor.withSymbolicTraits(leading == .loose ? .traitLooseLeading : .traitTightLeading)! 63 | return UIFont(descriptor: descriptor, size: 0) 64 | } 65 | 66 | func smallCaps() -> UIFont { 67 | return addingAttributes([ 68 | .featureSettings: [ 69 | [ 70 | UIFontDescriptor.FeatureKey.featureIdentifier: kLowerCaseType, 71 | UIFontDescriptor.FeatureKey.typeIdentifier: kLowerCaseSmallCapsSelector 72 | ], 73 | [ 74 | UIFontDescriptor.FeatureKey.featureIdentifier: kUpperCaseType, 75 | UIFontDescriptor.FeatureKey.typeIdentifier: kUpperCaseSmallCapsSelector 76 | ] 77 | ] 78 | ]) 79 | } 80 | 81 | func lowercaseSmallCaps() -> UIFont { 82 | return addingAttributes([ 83 | .featureSettings: [ 84 | [ 85 | UIFontDescriptor.FeatureKey.featureIdentifier: kLowerCaseType, 86 | UIFontDescriptor.FeatureKey.typeIdentifier: kLowerCaseSmallCapsSelector 87 | ] 88 | ] 89 | ]) 90 | } 91 | 92 | func uppercaseSmallCaps() -> UIFont { 93 | return addingAttributes([ 94 | .featureSettings: [ 95 | [ 96 | UIFontDescriptor.FeatureKey.featureIdentifier: kUpperCaseType, 97 | UIFontDescriptor.FeatureKey.typeIdentifier: kUpperCaseSmallCapsSelector 98 | ] 99 | ] 100 | ]) 101 | } 102 | 103 | func monospacedDigit() -> UIFont { 104 | return addingAttributes([ 105 | .featureSettings: [ 106 | [ 107 | UIFontDescriptor.FeatureKey.featureIdentifier: kNumberSpacingType, 108 | UIFontDescriptor.FeatureKey.typeIdentifier: kMonospacedNumbersSelector 109 | ] 110 | ] 111 | ]) 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /Sources/TextView/Coordinator.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension TextView.Representable { 4 | final class Coordinator: NSObject, UITextViewDelegate { 5 | 6 | internal let textView: UIKitTextView 7 | 8 | private var originalText: NSAttributedString = .init() 9 | private var text: Binding 10 | private var calculatedHeight: Binding 11 | 12 | var onCommit: (() -> Void)? 13 | var onEditingChanged: (() -> Void)? 14 | var shouldEditInRange: ((Range, String) -> Bool)? 15 | 16 | init(text: Binding, 17 | calculatedHeight: Binding, 18 | shouldEditInRange: ((Range, String) -> Bool)?, 19 | onEditingChanged: (() -> Void)?, 20 | onCommit: (() -> Void)? 21 | ) { 22 | textView = UIKitTextView() 23 | textView.backgroundColor = .clear 24 | textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 25 | 26 | self.text = text 27 | self.calculatedHeight = calculatedHeight 28 | self.shouldEditInRange = shouldEditInRange 29 | self.onEditingChanged = onEditingChanged 30 | self.onCommit = onCommit 31 | 32 | super.init() 33 | textView.delegate = self 34 | } 35 | 36 | func textViewDidBeginEditing(_ textView: UITextView) { 37 | originalText = text.wrappedValue 38 | } 39 | 40 | func textViewDidChange(_ textView: UITextView) { 41 | text.wrappedValue = NSAttributedString(attributedString: textView.attributedText) 42 | recalculateHeight() 43 | onEditingChanged?() 44 | } 45 | 46 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 47 | if onCommit != nil, text == "\n" { 48 | onCommit?() 49 | originalText = NSAttributedString(attributedString: textView.attributedText) 50 | textView.resignFirstResponder() 51 | return false 52 | } 53 | 54 | return true 55 | } 56 | 57 | func textViewDidEndEditing(_ textView: UITextView) { 58 | // this check is to ensure we always commit text when we're not using a closure 59 | if onCommit != nil { 60 | text.wrappedValue = originalText 61 | } 62 | } 63 | 64 | } 65 | 66 | } 67 | 68 | extension TextView.Representable.Coordinator { 69 | 70 | func update(representable: TextView.Representable) { 71 | textView.attributedText = representable.text 72 | textView.font = representable.font 73 | textView.adjustsFontForContentSizeCategory = true 74 | textView.textColor = representable.foregroundColor 75 | textView.autocapitalizationType = representable.autocapitalization 76 | textView.autocorrectionType = representable.autocorrection 77 | textView.isEditable = representable.isEditable 78 | textView.isSelectable = representable.isSelectable 79 | textView.isScrollEnabled = representable.isScrollingEnabled 80 | textView.dataDetectorTypes = representable.autoDetectionTypes 81 | textView.allowsEditingTextAttributes = representable.allowsRichText 82 | 83 | switch representable.multilineTextAlignment { 84 | case .leading: 85 | textView.textAlignment = textView.traitCollection.layoutDirection ~= .leftToRight ? .left : .right 86 | case .trailing: 87 | textView.textAlignment = textView.traitCollection.layoutDirection ~= .leftToRight ? .right : .left 88 | case .center: 89 | textView.textAlignment = .center 90 | } 91 | 92 | if let value = representable.enablesReturnKeyAutomatically { 93 | textView.enablesReturnKeyAutomatically = value 94 | } else { 95 | textView.enablesReturnKeyAutomatically = onCommit == nil ? false : true 96 | } 97 | 98 | if let returnKeyType = representable.returnKeyType { 99 | textView.returnKeyType = returnKeyType 100 | } else { 101 | textView.returnKeyType = onCommit == nil ? .default : .done 102 | } 103 | 104 | if !representable.isScrollingEnabled { 105 | textView.textContainer.lineFragmentPadding = 0 106 | textView.textContainerInset = .zero 107 | } 108 | 109 | recalculateHeight() 110 | textView.setNeedsDisplay() 111 | } 112 | 113 | private func recalculateHeight() { 114 | let newSize = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .greatestFiniteMagnitude)) 115 | guard calculatedHeight.wrappedValue != newSize.height else { return } 116 | 117 | DispatchQueue.main.async { // call in next render cycle. 118 | self.calculatedHeight.wrappedValue = newSize.height 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /Sources/TextView/Modifiers.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension TextView { 4 | 5 | /// Specifies whether or not this view allows rich text 6 | /// - Parameter enabled: If `true`, rich text editing controls will be enabled for the user 7 | func allowsRichText(_ enabled: Bool) -> TextView { 8 | var view = self 9 | view.allowRichText = enabled 10 | return view 11 | } 12 | 13 | /// Specify a placeholder text 14 | /// - Parameter placeholder: The placeholder text 15 | func placeholder(_ placeholder: String) -> TextView { 16 | self.placeholder(placeholder) { $0 } 17 | } 18 | 19 | /// Specify a placeholder with the specified configuration 20 | /// 21 | /// Example: 22 | /// 23 | /// TextView($text) 24 | /// .placeholder("placeholder") { view in 25 | /// view.foregroundColor(.red) 26 | /// } 27 | func placeholder(_ placeholder: String, _ configure: (Text) -> V) -> TextView { 28 | var view = self 29 | let text = Text(placeholder) 30 | view.placeholderView = AnyView(configure(text)) 31 | return view 32 | } 33 | 34 | /// Specify a custom placeholder view 35 | func placeholder(_ placeholder: V) -> TextView { 36 | var view = self 37 | view.placeholderView = AnyView(placeholder) 38 | return view 39 | } 40 | 41 | /// Enables auto detection for the specified types 42 | /// - Parameter types: The types to detect 43 | func autoDetectDataTypes(_ types: UIDataDetectorTypes) -> TextView { 44 | var view = self 45 | view.autoDetectionTypes = types 46 | return view 47 | } 48 | 49 | /// Specify the foreground color for the text 50 | /// - Parameter color: The foreground color 51 | func foregroundColor(_ color: UIColor) -> TextView { 52 | var view = self 53 | view.foregroundColor = color 54 | return view 55 | } 56 | 57 | /// Specifies the capitalization style to apply to the text 58 | /// - Parameter style: The capitalization style 59 | func autocapitalization(_ style: UITextAutocapitalizationType) -> TextView { 60 | var view = self 61 | view.autocapitalization = style 62 | return view 63 | } 64 | 65 | /// Specifies the alignment of multi-line text 66 | /// - Parameter alignment: The text alignment 67 | func multilineTextAlignment(_ alignment: TextAlignment) -> TextView { 68 | var view = self 69 | view.multilineTextAlignment = alignment 70 | return view 71 | } 72 | 73 | /// Specifies the font to apply to the text 74 | /// - Parameter font: The font to apply 75 | func font(_ font: UIFont) -> TextView { 76 | var view = self 77 | view.font = font 78 | return view 79 | } 80 | 81 | /// Specifies the font weight to apply to the text 82 | /// - Parameter weight: The font weight to apply 83 | func fontWeight(_ weight: UIFont.Weight) -> TextView { 84 | font(font.weight(weight)) 85 | } 86 | 87 | /// Specifies if the field should clear its content when editing begins 88 | /// - Parameter value: If true, the field will be cleared when it receives focus 89 | func clearOnInsertion(_ value: Bool) -> TextView { 90 | var view = self 91 | view.clearsOnInsertion = value 92 | return view 93 | } 94 | 95 | /// Disables auto-correct 96 | /// - Parameter disable: If true, autocorrection will be disabled 97 | func disableAutocorrection(_ disable: Bool?) -> TextView { 98 | var view = self 99 | if let disable = disable { 100 | view.autocorrection = disable ? .no : .yes 101 | } else { 102 | view.autocorrection = .default 103 | } 104 | return view 105 | } 106 | 107 | /// Specifies whether the text can be edited 108 | /// - Parameter isEditable: If true, the text can be edited via the user's keyboard 109 | func isEditable(_ isEditable: Bool) -> TextView { 110 | var view = self 111 | view.isEditable = isEditable 112 | return view 113 | } 114 | 115 | /// Specifies whether the text can be selected 116 | /// - Parameter isSelectable: If true, the text can be selected 117 | func isSelectable(_ isSelectable: Bool) -> TextView { 118 | var view = self 119 | view.isSelectable = isSelectable 120 | return view 121 | } 122 | 123 | /// Specifies whether the field can be scrolled. If true, auto-sizing will be disabled 124 | /// - Parameter isScrollingEnabled: If true, scrolling will be enabled 125 | func enableScrolling(_ isScrollingEnabled: Bool) -> TextView { 126 | var view = self 127 | view.isScrollingEnabled = isScrollingEnabled 128 | return view 129 | } 130 | 131 | /// Specifies the type of return key to be shown during editing, for the device keyboard 132 | /// - Parameter style: The return key style 133 | func returnKey(_ style: UIReturnKeyType?) -> TextView { 134 | var view = self 135 | view.returnKeyType = style 136 | return view 137 | } 138 | 139 | /// Specifies whether the return key should auto enable/disable based on the current text 140 | /// - Parameter value: If true, when the text is empty the return key will be disabled 141 | func automaticallyEnablesReturn(_ value: Bool?) -> TextView { 142 | var view = self 143 | view.enablesReturnKeyAutomatically = value 144 | return view 145 | } 146 | 147 | /// Specifies the truncation mode for this field 148 | /// - Parameter mode: The truncation mode 149 | func truncationMode(_ mode: Text.TruncationMode) -> TextView { 150 | var view = self 151 | switch mode { 152 | case .head: view.truncationMode = .byTruncatingHead 153 | case .tail: view.truncationMode = .byTruncatingTail 154 | case .middle: view.truncationMode = .byTruncatingMiddle 155 | @unknown default: 156 | fatalError("Unknown text truncation mode") 157 | } 158 | return view 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /Sources/TextView/TextView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A SwiftUI TextView implementation that supports both scrolling and auto-sizing layouts 4 | public struct TextView: View { 5 | 6 | @Environment(\.layoutDirection) private var layoutDirection 7 | 8 | @Binding private var text: NSAttributedString 9 | @Binding private var isEmpty: Bool 10 | 11 | @State private var calculatedHeight: CGFloat = 44 12 | 13 | private var onEditingChanged: (() -> Void)? 14 | private var shouldEditInRange: ((Range, String) -> Bool)? 15 | private var onCommit: (() -> Void)? 16 | 17 | var placeholderView: AnyView? 18 | var foregroundColor: UIColor = .label 19 | var autocapitalization: UITextAutocapitalizationType = .sentences 20 | var multilineTextAlignment: TextAlignment = .leading 21 | var font: UIFont = .preferredFont(forTextStyle: .body) 22 | var returnKeyType: UIReturnKeyType? 23 | var clearsOnInsertion: Bool = false 24 | var autocorrection: UITextAutocorrectionType = .default 25 | var truncationMode: NSLineBreakMode = .byTruncatingTail 26 | var isEditable: Bool = true 27 | var isSelectable: Bool = true 28 | var isScrollingEnabled: Bool = false 29 | var enablesReturnKeyAutomatically: Bool? 30 | var autoDetectionTypes: UIDataDetectorTypes = [] 31 | var allowRichText: Bool 32 | 33 | /// Makes a new TextView with the specified configuration 34 | /// - Parameters: 35 | /// - text: A binding to the text 36 | /// - shouldEditInRange: A closure that's called before an edit it applied, allowing the consumer to prevent the change 37 | /// - onEditingChanged: A closure that's called after an edit has been applied 38 | /// - onCommit: If this is provided, the field will automatically lose focus when the return key is pressed 39 | public init(_ text: Binding, 40 | shouldEditInRange: ((Range, String) -> Bool)? = nil, 41 | onEditingChanged: (() -> Void)? = nil, 42 | onCommit: (() -> Void)? = nil 43 | ) { 44 | _text = Binding( 45 | get: { NSAttributedString(string: text.wrappedValue) }, 46 | set: { text.wrappedValue = $0.string } 47 | ) 48 | 49 | _isEmpty = Binding( 50 | get: { text.wrappedValue.isEmpty }, 51 | set: { _ in } 52 | ) 53 | 54 | self.onCommit = onCommit 55 | self.shouldEditInRange = shouldEditInRange 56 | self.onEditingChanged = onEditingChanged 57 | 58 | allowRichText = false 59 | } 60 | 61 | /// Makes a new TextView that supports `NSAttributedString` 62 | /// - Parameters: 63 | /// - text: A binding to the attributed text 64 | /// - onEditingChanged: A closure that's called after an edit has been applied 65 | /// - onCommit: If this is provided, the field will automatically lose focus when the return key is pressed 66 | public init(_ text: Binding, 67 | onEditingChanged: (() -> Void)? = nil, 68 | onCommit: (() -> Void)? = nil 69 | ) { 70 | _text = text 71 | _isEmpty = Binding( 72 | get: { text.wrappedValue.string.isEmpty }, 73 | set: { _ in } 74 | ) 75 | 76 | self.onCommit = onCommit 77 | self.onEditingChanged = onEditingChanged 78 | 79 | allowRichText = true 80 | } 81 | 82 | public var body: some View { 83 | Representable( 84 | text: $text, 85 | calculatedHeight: $calculatedHeight, 86 | foregroundColor: foregroundColor, 87 | autocapitalization: autocapitalization, 88 | multilineTextAlignment: multilineTextAlignment, 89 | font: font, 90 | returnKeyType: returnKeyType, 91 | clearsOnInsertion: clearsOnInsertion, 92 | autocorrection: autocorrection, 93 | truncationMode: truncationMode, 94 | isEditable: isEditable, 95 | isSelectable: isSelectable, 96 | isScrollingEnabled: isScrollingEnabled, 97 | enablesReturnKeyAutomatically: enablesReturnKeyAutomatically, 98 | autoDetectionTypes: autoDetectionTypes, 99 | allowsRichText: allowRichText, 100 | onEditingChanged: onEditingChanged, 101 | shouldEditInRange: shouldEditInRange, 102 | onCommit: onCommit 103 | ) 104 | .frame( 105 | minHeight: isScrollingEnabled ? 0 : calculatedHeight, 106 | maxHeight: isScrollingEnabled ? .infinity : calculatedHeight 107 | ) 108 | .background( 109 | placeholderView? 110 | .foregroundColor(Color(.placeholderText)) 111 | .multilineTextAlignment(multilineTextAlignment) 112 | .font(Font(font)) 113 | .padding(.horizontal, isScrollingEnabled ? 5 : 0) 114 | .padding(.vertical, isScrollingEnabled ? 8 : 0) 115 | .opacity(isEmpty ? 1 : 0), 116 | alignment: .topLeading 117 | ) 118 | } 119 | 120 | } 121 | 122 | final class UIKitTextView: UITextView { 123 | 124 | override var keyCommands: [UIKeyCommand]? { 125 | return (super.keyCommands ?? []) + [ 126 | UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(escape(_:))) 127 | ] 128 | } 129 | 130 | @objc private func escape(_ sender: Any) { 131 | resignFirstResponder() 132 | } 133 | 134 | } 135 | 136 | struct RoundedTextView: View { 137 | @State private var text: NSAttributedString = .init() 138 | 139 | var body: some View { 140 | VStack(alignment: .leading) { 141 | TextView($text) 142 | .padding(.leading, 25) 143 | 144 | GeometryReader { _ in 145 | TextView($text) 146 | .placeholder("Enter some text") 147 | .padding(10) 148 | .overlay( 149 | RoundedRectangle(cornerRadius: 10) 150 | .stroke(lineWidth: 1) 151 | .foregroundColor(Color(.placeholderText)) 152 | ) 153 | .padding() 154 | } 155 | .background(Color(.systemBackground).edgesIgnoringSafeArea(.all)) 156 | 157 | Button { 158 | text = NSAttributedString(string: "This is interesting", attributes: [ 159 | .font: UIFont.preferredFont(forTextStyle: .headline) 160 | ]) 161 | } label: { 162 | Spacer() 163 | Text("Interesting?") 164 | Spacer() 165 | } 166 | .padding() 167 | } 168 | } 169 | } 170 | 171 | struct TextView_Previews: PreviewProvider { 172 | static var previews: some View { 173 | RoundedTextView() 174 | } 175 | } 176 | --------------------------------------------------------------------------------