├── .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 |
--------------------------------------------------------------------------------