├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── TextView │ └── TextView.swift └── Tests ├── LinuxMain.swift └── TextViewTests ├── TextViewTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ken Mueller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "TextView", 7 | platforms: [ 8 | .iOS(.v13), 9 | .tvOS(.v13) 10 | ], 11 | products: [ 12 | .library( 13 | name: "TextView", 14 | targets: ["TextView"] 15 | ) 16 | ], 17 | targets: [ 18 | .target(name: "TextView"), 19 | .testTarget( 20 | name: "TextViewTests", 21 | dependencies: ["TextView"] 22 | ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TextView 2 | 3 | > The missing TextView in SwiftUI 4 | 5 | ## Download 6 | 7 | - File -> Swift Packages -> Add Package Dependency... 8 | - Select your project 9 | - Enter `https://github.com/kenmueller/TextView` for the package repository URL 10 | - Select **Branch**: master 11 | - Click **Finish** 12 | 13 | ## Inputs 14 | 15 | - `text: Binding` 16 | - `isEditing: Binding` 17 | - The `TextView` will modify the value when it is selected and deselected 18 | - You can also modify this value to automatically select and deselect the `TextView` 19 | - `placeholder: String? = nil` 20 | - `textAlignment: TextView.TextAlignment = .left` 21 | - `textHorizontalPadding: CGFloat = 0` 22 | - `textVerticalPadding: CGFloat = 7` 23 | - `placeholderAlignment: Alignment = .topLeading` 24 | - `placeholderHorizontalPadding: CGFloat = 4.5` 25 | - `placeholderVerticalPadding: CGFloat = 7` 26 | - `font: UIFont = .preferredFont(forTextStyle: .body)` 27 | - By default, the font is a body-style font 28 | - `textColor: UIColor = .black` 29 | - `placeholderColor: Color = .gray` 30 | - `backgroundColor: UIColor = .white` 31 | - `contentType: TextView.ContentType? = nil` 32 | - For semantic purposes only 33 | - `autocorrection: TextView.Autocorrection = .default` 34 | - `autocapitalization: TextView.Autocapitalization = .sentences` 35 | - `isSecure: Bool = false` 36 | - `isEditable: Bool = true` 37 | - `isSelectable: Bool = true` 38 | - `isScrollingEnabled: Bool = true` 39 | - `isUserInteractionEnabled: Bool = true` 40 | - `shouldWaitUntilCommit: Bool = true` 41 | - For multi-stage input methods, setting this to `false` would make the `TextView` completely unusable. 42 | - This option will ignore text changes when the user is still composing characters. 43 | - `shouldChange: TextView.ShouldChangeHandler? = nil` 44 | - Of type `(NSRange, String) -> Bool` and is called with the arguments to `textView(_:shouldChangeTextIn:replacementText:)`. 45 | 46 | ## Example 47 | 48 | ```swift 49 | import SwiftUI 50 | import TextView 51 | 52 | struct ContentView: View { 53 | @State var text = "" 54 | @State var isEditing = false 55 | 56 | var body: some View { 57 | VStack { 58 | Button(action: { 59 | self.isEditing.toggle() 60 | }) { 61 | Text("\(isEditing ? "Stop" : "Start") editing") 62 | } 63 | TextView( 64 | text: $text, 65 | isEditing: $isEditing, 66 | placeholder: "Enter text here" 67 | ) 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ## You might also find these useful... 74 | 75 | - [**Audio** - _The easiest way to play Audio in Swift_](https://github.com/kenmueller/Audio) 76 | -------------------------------------------------------------------------------- /Sources/TextView/TextView.swift: -------------------------------------------------------------------------------- 1 | #if !os(macOS) 2 | 3 | import SwiftUI 4 | 5 | @available(iOS 13.0, *) 6 | public struct TextView: View { 7 | public struct Representable: UIViewRepresentable { 8 | public final class Coordinator: NSObject, UITextViewDelegate { 9 | private let parent: Representable 10 | 11 | public init(_ parent: Representable) { 12 | self.parent = parent 13 | } 14 | 15 | private func setIsEditing(to value: Bool) { 16 | DispatchQueue.main.async { 17 | self.parent.isEditing = value 18 | } 19 | } 20 | 21 | public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { 22 | parent.shouldChange?(range, text) ?? true 23 | } 24 | 25 | public func textViewDidChange(_ textView: UITextView) { 26 | parent.text = textView.text 27 | } 28 | 29 | public func textViewDidBeginEditing(_: UITextView) { 30 | setIsEditing(to: true) 31 | } 32 | 33 | public func textViewDidEndEditing(_: UITextView) { 34 | setIsEditing(to: false) 35 | } 36 | } 37 | 38 | @Binding private var text: String 39 | @Binding private var isEditing: Bool 40 | 41 | private let textAlignment: TextAlignment 42 | private let textHorizontalPadding: CGFloat 43 | private let textVerticalPadding: CGFloat 44 | private let font: UIFont 45 | private let textColor: UIColor 46 | private let backgroundColor: UIColor 47 | private let returnType: UIReturnKeyType 48 | private let contentType: ContentType? 49 | private let autocorrection: Autocorrection 50 | private let autocapitalization: Autocapitalization 51 | private let keyboardDismissMode: UIScrollView.KeyboardDismissMode 52 | private let isSecure: Bool 53 | private let isEditable: Bool 54 | private let isSelectable: Bool 55 | private let isScrollingEnabled: Bool 56 | private let isUserInteractionEnabled: Bool 57 | private let shouldWaitUntilCommit: Bool 58 | private let shouldChange: ShouldChangeHandler? 59 | 60 | public init( 61 | text: Binding, 62 | isEditing: Binding, 63 | textAlignment: TextAlignment, 64 | textHorizontalPadding: CGFloat, 65 | textVerticalPadding: CGFloat, 66 | font: UIFont, 67 | textColor: UIColor, 68 | backgroundColor: UIColor, 69 | returnType: UIReturnKeyType, 70 | contentType: ContentType?, 71 | autocorrection: Autocorrection, 72 | autocapitalization: Autocapitalization, 73 | keyboardDismissMode: UIScrollView.KeyboardDismissMode, 74 | isSecure: Bool, 75 | isEditable: Bool, 76 | isSelectable: Bool, 77 | isScrollingEnabled: Bool, 78 | isUserInteractionEnabled: Bool, 79 | shouldWaitUntilCommit: Bool, 80 | shouldChange: ShouldChangeHandler? = nil 81 | ) { 82 | _text = text 83 | _isEditing = isEditing 84 | 85 | self.textAlignment = textAlignment 86 | self.textHorizontalPadding = textHorizontalPadding 87 | self.textVerticalPadding = textVerticalPadding 88 | self.font = font 89 | self.textColor = textColor 90 | self.backgroundColor = backgroundColor 91 | self.returnType = returnType 92 | self.contentType = contentType 93 | self.autocorrection = autocorrection 94 | self.autocapitalization = autocapitalization 95 | self.keyboardDismissMode = keyboardDismissMode 96 | self.isSecure = isSecure 97 | self.isEditable = isEditable 98 | self.isSelectable = isSelectable 99 | self.isScrollingEnabled = isScrollingEnabled 100 | self.isUserInteractionEnabled = isUserInteractionEnabled 101 | self.shouldWaitUntilCommit = shouldWaitUntilCommit 102 | self.shouldChange = shouldChange 103 | } 104 | 105 | public func makeCoordinator() -> Coordinator { 106 | .init(self) 107 | } 108 | 109 | public func makeUIView(context: Context) -> UITextView { 110 | let textView = UITextView() 111 | textView.delegate = context.coordinator 112 | return textView 113 | } 114 | 115 | public func updateUIView(_ textView: UITextView, context _: Context) { 116 | if !shouldWaitUntilCommit || textView.markedTextRange == nil { 117 | let textViewWasEmpty = textView.text.isEmpty 118 | let oldSelectedRange = textView.selectedTextRange 119 | 120 | textView.text = text 121 | textView.selectedTextRange = textViewWasEmpty 122 | ? textView.textRange( 123 | from: textView.endOfDocument, 124 | to: textView.endOfDocument 125 | ) 126 | : oldSelectedRange 127 | } 128 | 129 | textView.textAlignment = textAlignment 130 | textView.font = font 131 | textView.textColor = textColor 132 | textView.backgroundColor = backgroundColor 133 | textView.returnKeyType = returnType 134 | textView.textContentType = contentType 135 | textView.autocorrectionType = autocorrection 136 | textView.autocapitalizationType = autocapitalization 137 | textView.keyboardDismissMode = keyboardDismissMode 138 | textView.isSecureTextEntry = isSecure 139 | textView.isEditable = isEditable 140 | textView.isSelectable = isSelectable 141 | textView.isScrollEnabled = isScrollingEnabled 142 | textView.isUserInteractionEnabled = isUserInteractionEnabled 143 | 144 | textView.textContainerInset = .init( 145 | top: textVerticalPadding, 146 | left: textHorizontalPadding, 147 | bottom: textVerticalPadding, 148 | right: textHorizontalPadding 149 | ) 150 | 151 | DispatchQueue.main.async { 152 | _ = self.isEditing 153 | ? textView.becomeFirstResponder() 154 | : textView.resignFirstResponder() 155 | } 156 | } 157 | } 158 | 159 | public typealias TextAlignment = NSTextAlignment 160 | public typealias ContentType = UITextContentType 161 | public typealias Autocorrection = UITextAutocorrectionType 162 | public typealias Autocapitalization = UITextAutocapitalizationType 163 | public typealias ShouldChangeHandler = (NSRange, String) -> Bool 164 | 165 | public static let defaultFont = UIFont.preferredFont(forTextStyle: .body) 166 | 167 | @Binding private var text: String 168 | @Binding private var isEditing: Bool 169 | 170 | private let placeholder: String? 171 | private let textAlignment: TextAlignment 172 | private let textHorizontalPadding: CGFloat 173 | private let textVerticalPadding: CGFloat 174 | private let placeholderAlignment: Alignment 175 | private let placeholderHorizontalPadding: CGFloat 176 | private let placeholderVerticalPadding: CGFloat 177 | private let font: UIFont 178 | private let textColor: UIColor 179 | private let placeholderColor: Color 180 | private let backgroundColor: UIColor 181 | private let returnType: UIReturnKeyType 182 | private let contentType: ContentType? 183 | private let autocorrection: Autocorrection 184 | private let autocapitalization: Autocapitalization 185 | private let keyboardDismissMode: UIScrollView.KeyboardDismissMode 186 | private let isSecure: Bool 187 | private let isEditable: Bool 188 | private let isSelectable: Bool 189 | private let isScrollingEnabled: Bool 190 | private let isUserInteractionEnabled: Bool 191 | private let shouldWaitUntilCommit: Bool 192 | private let shouldChange: ShouldChangeHandler? 193 | 194 | public init( 195 | text: Binding, 196 | isEditing: Binding, 197 | placeholder: String? = nil, 198 | textAlignment: TextAlignment = .left, 199 | textHorizontalPadding: CGFloat = 0, 200 | textVerticalPadding: CGFloat = 7, 201 | placeholderAlignment: Alignment = .topLeading, 202 | placeholderHorizontalPadding: CGFloat = 4.5, 203 | placeholderVerticalPadding: CGFloat = 7, 204 | font: UIFont = Self.defaultFont, 205 | textColor: UIColor = .label, 206 | placeholderColor: Color = .init(.placeholderText), 207 | backgroundColor: UIColor = .clear, 208 | returnType: UIReturnKeyType = .default, 209 | contentType: ContentType? = nil, 210 | autocorrection: Autocorrection = .default, 211 | autocapitalization: Autocapitalization = .sentences, 212 | keyboardDismissMode: UIScrollView.KeyboardDismissMode = .none, 213 | isSecure: Bool = false, 214 | isEditable: Bool = true, 215 | isSelectable: Bool = true, 216 | isScrollingEnabled: Bool = true, 217 | isUserInteractionEnabled: Bool = true, 218 | shouldWaitUntilCommit: Bool = true, 219 | shouldChange: ShouldChangeHandler? = nil 220 | ) { 221 | _text = text 222 | _isEditing = isEditing 223 | 224 | self.placeholder = placeholder 225 | self.textAlignment = textAlignment 226 | self.textHorizontalPadding = textHorizontalPadding 227 | self.textVerticalPadding = textVerticalPadding 228 | self.placeholderAlignment = placeholderAlignment 229 | self.placeholderHorizontalPadding = placeholderHorizontalPadding 230 | self.placeholderVerticalPadding = placeholderVerticalPadding 231 | self.font = font 232 | self.textColor = textColor 233 | self.placeholderColor = placeholderColor 234 | self.backgroundColor = backgroundColor 235 | self.returnType = returnType 236 | self.contentType = contentType 237 | self.autocorrection = autocorrection 238 | self.autocapitalization = autocapitalization 239 | self.keyboardDismissMode = keyboardDismissMode 240 | self.isSecure = isSecure 241 | self.isEditable = isEditable 242 | self.isSelectable = isSelectable 243 | self.isScrollingEnabled = isScrollingEnabled 244 | self.isUserInteractionEnabled = isUserInteractionEnabled 245 | self.shouldWaitUntilCommit = shouldWaitUntilCommit 246 | self.shouldChange = shouldChange 247 | } 248 | 249 | private var _placeholder: String? { 250 | text.isEmpty ? placeholder : nil 251 | } 252 | 253 | private var representable: Representable { 254 | .init( 255 | text: $text, 256 | isEditing: $isEditing, 257 | textAlignment: textAlignment, 258 | textHorizontalPadding: textHorizontalPadding, 259 | textVerticalPadding: textVerticalPadding, 260 | font: font, 261 | textColor: textColor, 262 | backgroundColor: backgroundColor, 263 | returnType: returnType, 264 | contentType: contentType, 265 | autocorrection: autocorrection, 266 | autocapitalization: autocapitalization, 267 | keyboardDismissMode: keyboardDismissMode, 268 | isSecure: isSecure, 269 | isEditable: isEditable, 270 | isSelectable: isSelectable, 271 | isScrollingEnabled: isScrollingEnabled, 272 | isUserInteractionEnabled: isUserInteractionEnabled, 273 | shouldWaitUntilCommit: shouldWaitUntilCommit, 274 | shouldChange: shouldChange 275 | ) 276 | } 277 | 278 | public var body: some View { 279 | GeometryReader { geometry in 280 | ZStack { 281 | self.representable 282 | self._placeholder.map { placeholder in 283 | Text(placeholder) 284 | .font(.init(self.font)) 285 | .foregroundColor(self.placeholderColor) 286 | .padding(.horizontal, self.placeholderHorizontalPadding) 287 | .padding(.vertical, self.placeholderVerticalPadding) 288 | .frame( 289 | width: geometry.size.width, 290 | height: geometry.size.height, 291 | alignment: self.placeholderAlignment 292 | ) 293 | .onTapGesture { 294 | self.isEditing = true 295 | } 296 | } 297 | } 298 | } 299 | } 300 | } 301 | 302 | #endif 303 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import TextViewTests 3 | 4 | XCTMain([TextViewTests.allTests()]) 5 | -------------------------------------------------------------------------------- /Tests/TextViewTests/TextViewTests.swift: -------------------------------------------------------------------------------- 1 | @testable import TextView 2 | 3 | import XCTest 4 | 5 | final class TextViewTests: XCTestCase { 6 | static let allTests = [ 7 | ("testExample", testExample) 8 | ] 9 | 10 | func testExample() {} 11 | } 12 | -------------------------------------------------------------------------------- /Tests/TextViewTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | [testCase(TextViewTests.allTests)] 6 | } 7 | #endif 8 | --------------------------------------------------------------------------------