├── docs └── images │ ├── input-view.png │ ├── no-stretching.png │ ├── text-binding.png │ ├── value-binding.png │ ├── padding-toggled-on.png │ ├── padding-toggled-off.png │ ├── custom-text-field-class.png │ └── horizontal-stretching.png ├── AllTests ├── UITestingApp │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── UITestingApp.swift │ └── ContentView.swift ├── AllTests.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ ├── xcshareddata │ │ └── xcschemes │ │ │ ├── AllTests.xcscheme │ │ │ └── UITestingApp.xcscheme │ └── project.pbxproj └── UITests │ └── UITests.swift ├── Examples └── Example │ ├── Example │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ExampleApp.swift │ ├── Pages │ │ ├── Views │ │ │ └── PaddedTextField.swift │ │ ├── TextBindingPage.swift │ │ ├── ConfigurePage.swift │ │ ├── StretchingPage.swift │ │ ├── ValueBindingPage.swift │ │ ├── CustomTextFieldPage.swift │ │ ├── FocusBindingPage.swift │ │ └── InputViewPage.swift │ └── ContentView.swift │ ├── Example.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── project.pbxproj │ └── ExampleUITests │ └── ExampleUITests.swift ├── Tests └── UIKitTextFieldTests │ ├── Utils.swift │ ├── __Snapshots__ │ └── UIKitTextFieldTests │ │ ├── testInputViews.1.png │ │ ├── testInputViews.2.png │ │ ├── testStretching.1.png │ │ ├── testStretching.2.png │ │ ├── testStretching.3.png │ │ └── testStretching.4.png │ ├── UIKitTextFieldTests+UIControl.swift │ ├── UIKitTextFieldTests+UITextInputTraits.swift │ ├── UIKitTextFieldTests+UITextFieldDelegate.swift │ └── UIKitTextFieldTests.swift ├── .gitignore ├── Sources └── UIKitTextField │ ├── UITextFieldProtocol.swift │ ├── InputViewContent.swift │ ├── BaseUITextField.swift │ ├── InputViewContentController.swift │ ├── UIKitTextField.swift │ ├── UIKitTextField+Configuration+UITextFieldDelegate.swift │ ├── InputViewManager.swift │ ├── UIKitTextField+Configuration+UITextInputTraits.swift │ ├── UIKitTextField+Coordinator.swift │ └── UIKitTextField+Configuration.swift ├── Package.resolved ├── Package.swift ├── LICENSE ├── .github └── workflows │ └── test.yml └── README.md /docs/images/input-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/input-view.png -------------------------------------------------------------------------------- /docs/images/no-stretching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/no-stretching.png -------------------------------------------------------------------------------- /docs/images/text-binding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/text-binding.png -------------------------------------------------------------------------------- /docs/images/value-binding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/value-binding.png -------------------------------------------------------------------------------- /docs/images/padding-toggled-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/padding-toggled-on.png -------------------------------------------------------------------------------- /AllTests/UITestingApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/images/padding-toggled-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/padding-toggled-off.png -------------------------------------------------------------------------------- /Examples/Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/images/custom-text-field-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/custom-text-field-class.png -------------------------------------------------------------------------------- /docs/images/horizontal-stretching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/docs/images/horizontal-stretching.png -------------------------------------------------------------------------------- /AllTests/UITestingApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/Utils.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCTestCase { 4 | func wait(for time: TimeInterval) { 5 | RunLoop.current.run(until: Date() + time) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /AllTests/UITestingApp/UITestingApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct UITestingApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Examples/Example/Example/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct ExampleApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testInputViews.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testInputViews.1.png -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testInputViews.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testInputViews.2.png -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.1.png -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.2.png -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.3.png -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinceplusplus/uikit-textfield/HEAD/Tests/UIKitTextFieldTests/__Snapshots__/UIKitTextFieldTests/testStretching.4.png -------------------------------------------------------------------------------- /AllTests/AllTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AllTests/UITestingApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/UITextFieldProtocol.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | public protocol UITextFieldProtocol: UITextField { 6 | var inputViewController: UIInputViewController? { get set } 7 | var inputAccessoryViewController: UIInputViewController? { get set } 8 | } 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /AllTests/AllTests.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/Views/PaddedTextField.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import UIKitTextField 3 | 4 | class PaddedTextField: BaseUITextField { 5 | var padding = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) { 6 | didSet { 7 | setNeedsLayout() 8 | } 9 | } 10 | 11 | public override func textRect(forBounds bounds: CGRect) -> CGRect { 12 | super.textRect(forBounds: bounds).inset(by: padding) 13 | } 14 | 15 | public override func editingRect(forBounds bounds: CGRect) -> CGRect { 16 | super.editingRect(forBounds: bounds).inset(by: padding) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/TextBindingPage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | 4 | struct TextBindingPage: View { 5 | @State var name: String = "" 6 | 7 | var body: some View { 8 | VStack(alignment: .leading) { 9 | UIKitTextField( 10 | config: .init() 11 | .value(text: $name) 12 | ) 13 | .border(Color.black) 14 | Text("\(name.isEmpty ? "Please enter your name above" : "Hello \(name)")") 15 | } 16 | .padding() 17 | } 18 | } 19 | 20 | struct TextBindingPage_Previews: PreviewProvider { 21 | static var previews: some View { 22 | TextBindingPage() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/InputViewContent.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import SwiftUI 4 | 5 | public struct InputViewContent where UITextFieldType: UITextFieldProtocol { 6 | let content: ((_ uiTextField: UITextFieldType) -> AnyView)? 7 | 8 | init(content: ((_ uiTextField: UITextFieldType) -> AnyView)?) { 9 | self.content = content 10 | } 11 | 12 | public static func view( 13 | @ViewBuilder content: @escaping (_ uiTextField: UITextFieldType) -> Content 14 | ) -> Self 15 | where 16 | Content: View 17 | { 18 | .init(content: { .init(content($0)) }) 19 | } 20 | 21 | public static var none: Self { .init(content: nil) } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/BaseUITextField.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | open class BaseUITextField: UITextField { 6 | private var _inputViewController: UIInputViewController? 7 | private var _inputAccessoryViewController: UIInputViewController? 8 | 9 | open override var inputViewController: UIInputViewController? { 10 | get { _inputViewController } 11 | set { _inputViewController = newValue } 12 | } 13 | 14 | open override var inputAccessoryViewController: UIInputViewController? { 15 | get { _inputAccessoryViewController } 16 | set { _inputAccessoryViewController = newValue } 17 | } 18 | } 19 | 20 | extension BaseUITextField: UITextFieldProtocol {} 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SnapshotTesting", 6 | "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", 10 | "version": "1.9.0" 11 | } 12 | }, 13 | { 14 | "package": "ViewInspector", 15 | "repositoryURL": "https://github.com/nalexn/ViewInspector.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "6b88c4ec1fa20cf38f2138052e63c8e79df5d76e", 19 | "version": "0.9.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/UIKitTextFieldTests+UIControl.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import XCTest 4 | import SwiftUI 5 | @testable import UIKitTextField 6 | import ViewInspector 7 | 8 | extension UIKitTextFieldTests { 9 | func testIsEnabled() throws { 10 | // NOTE: it's not so obvious that the built-in `.disabled()` alone will suffice updating 11 | // the `.isEnabled` property 12 | let view = UIKitTextField(config: .init()) 13 | .disabled(true) 14 | ViewHosting.host(view: view) 15 | defer { 16 | ViewHosting.expel() 17 | } 18 | 19 | let textField = try view.inspect().find(UIKitTextField.self).actualView().uiView() 20 | XCTAssertEqual(textField.isEnabled, false) 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/InputViewContentController.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import SwiftUI 4 | 5 | internal protocol InputViewContentControllerDelegate: AnyObject { 6 | func viewWillLayoutSubviews(_ viewController: InputViewContentController) 7 | func viewDidLayoutSubviews(_ viewController: InputViewContentController) 8 | } 9 | 10 | internal class InputViewContentController: UIHostingController { 11 | weak var delegate: InputViewContentControllerDelegate? 12 | 13 | override func viewWillLayoutSubviews() { 14 | super.viewWillLayoutSubviews() 15 | delegate?.viewWillLayoutSubviews(self) 16 | } 17 | 18 | override func viewDidLayoutSubviews() { 19 | super.viewDidLayoutSubviews() 20 | delegate?.viewDidLayoutSubviews(self) 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/UIKitTextField.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import SwiftUI 4 | 5 | public struct UIKitTextField: UIViewRepresentable where UITextFieldType: UITextFieldProtocol { 6 | public typealias UIViewType = UITextFieldType 7 | 8 | var config: Configuration 9 | @State var forceUpdateCounter: Int = 0 10 | 11 | public init(config: Configuration) { 12 | self.config = config 13 | } 14 | 15 | public func makeCoordinator() -> Coordinator { 16 | .init(self) 17 | } 18 | 19 | public func makeUIView(context: Context) -> UITextFieldType { 20 | context.coordinator.textField 21 | } 22 | 23 | public func updateUIView(_ textField: UITextFieldType, context: Context) { 24 | context.coordinator.update(with: self) 25 | } 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /AllTests/AllTests.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SnapshotTesting", 6 | "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", 10 | "version": "1.9.0" 11 | } 12 | }, 13 | { 14 | "package": "ViewInspector", 15 | "repositoryURL": "https://github.com/nalexn/ViewInspector.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "6b88c4ec1fa20cf38f2138052e63c8e79df5d76e", 19 | "version": "0.9.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/ConfigurePage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | 4 | struct ConfigurePage: View { 5 | @State var text = "some text..." 6 | @State var pads = true 7 | 8 | var body: some View { 9 | VStack { 10 | Toggle("Padding", isOn: $pads) 11 | UIKitTextField( 12 | config: .init { 13 | PaddedTextField() 14 | } 15 | .value(text: $text) 16 | .configure { uiTextField in 17 | uiTextField.padding = pads ? .init(top: 4, left: 8, bottom: 4, right: 8) : .zero 18 | } 19 | ) 20 | .border(Color.black) 21 | } 22 | .padding() 23 | } 24 | } 25 | 26 | struct ConfigurePage_Previews: PreviewProvider { 27 | static var previews: some View { 28 | ConfigurePage() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 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: "uikit-textfield", 8 | platforms: [ 9 | .iOS(.v14), 10 | ], 11 | products: [ 12 | .library( 13 | name: "UIKitTextField", 14 | targets: ["UIKitTextField"]), 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.9.0"), 18 | .package(url: "https://github.com/nalexn/ViewInspector.git", from: "0.9.1"), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "UIKitTextField", 23 | dependencies: [] 24 | ), 25 | .testTarget( 26 | name: "UIKitTextFieldTests", 27 | dependencies: [ 28 | "UIKitTextField", 29 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), 30 | "ViewInspector", 31 | ], 32 | resources: [.process("__Snapshots__")] 33 | ), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/StretchingPage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | 4 | struct StretchingPage: View { 5 | var body: some View { 6 | VStack { 7 | UIKitTextField( 8 | config: .init { 9 | PaddedTextField() 10 | } 11 | .stretches(horizontal: true, vertical: false) 12 | ) 13 | .border(Color.black) 14 | 15 | UIKitTextField( 16 | config: .init { 17 | PaddedTextField() 18 | } 19 | .stretches(horizontal: false, vertical: false) 20 | ) 21 | .border(Color.black) 22 | 23 | Button { 24 | UIApplication.shared.sendAction( 25 | #selector(UIApplication.resignFirstResponder), 26 | to: nil, 27 | from: nil, 28 | for: nil 29 | ) 30 | } label: { 31 | Text("Dismiss keyboard") 32 | } 33 | } 34 | .padding() 35 | } 36 | } 37 | 38 | struct StretchingPage_Previews: PreviewProvider { 39 | static var previews: some View { 40 | StretchingPage() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Examples/Example/Example/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ContentView: View { 4 | var body: some View { 5 | NavigationView { 6 | VStack { 7 | NavigationLink(destination: TextBindingPage()) { 8 | Text("Text Binding") 9 | } 10 | NavigationLink(destination: ValueBindingPage()) { 11 | Text("Value Binding") 12 | } 13 | NavigationLink(destination: FocusBindingPage()) { 14 | Text("Focus Binding") 15 | } 16 | NavigationLink(destination: CustomTextFieldPage()) { 17 | Text("Custom Text Field") 18 | } 19 | NavigationLink(destination: StretchingPage()) { 20 | Text("Stretching") 21 | } 22 | NavigationLink(destination: InputViewPage()) { 23 | Text("Input View") 24 | } 25 | NavigationLink(destination: ConfigurePage()) { 26 | Text("Custom Configuration") 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | struct ContentView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | ContentView() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vincent Cheung 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 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - '**.md' 14 | 15 | env: 16 | DEVELOPER_DIR: /Applications/Xcode_13.3.1.app/Contents/Developer 17 | 18 | jobs: 19 | test: 20 | runs-on: 21 | - macos-12 22 | 23 | steps: 24 | - name: Check out 25 | uses: actions/checkout@v2 26 | 27 | - name: Build 28 | run: | 29 | swift --version 30 | swift build 31 | 32 | - name: Test 33 | run: | 34 | xcodebuild -project AllTests/AllTests.xcodeproj -scheme AllTests test -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 13' -enableCodeCoverage YES 35 | 36 | - name: Test example code 37 | run: | 38 | xcodebuild -project Examples/Example/Example.xcodeproj -scheme Example test -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 13' 39 | 40 | - name: Upload code coverage 41 | env: 42 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 43 | run: | 44 | bash <(curl -s https://codecov.io/bash) 45 | -------------------------------------------------------------------------------- /Examples/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Expression", 6 | "repositoryURL": "https://github.com/nicklockwood/Expression.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "8fb79f95df29f98565039586fe722323f91ce0c7", 10 | "version": "0.13.6" 11 | } 12 | }, 13 | { 14 | "package": "measurement-reader", 15 | "repositoryURL": "https://github.com/vinceplusplus/measurement-reader.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "5997fcb76e31b6fb91c362670d6f63cfc4f20d00", 19 | "version": "2.1.1" 20 | } 21 | }, 22 | { 23 | "package": "SnapshotTesting", 24 | "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", 28 | "version": "1.9.0" 29 | } 30 | }, 31 | { 32 | "package": "ViewInspector", 33 | "repositoryURL": "https://github.com/nalexn/ViewInspector.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "6b88c4ec1fa20cf38f2138052e63c8e79df5d76e", 37 | "version": "0.9.1" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/ValueBindingPage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | 4 | struct ValueBindingPage: View { 5 | @State var value: Int = 0 6 | @State var optionalValue: Int? = 0 7 | 8 | var body: some View { 9 | VStack(alignment: .leading) { 10 | Text("Enter a number:") 11 | UIKitTextField( 12 | config: .init() 13 | .value(value: $value, format: .number) 14 | ) 15 | .border(Color.black) 16 | // NOTE: avoiding the formatting behavior which comes from Text() 17 | Text("Your input: \("\(value)")") 18 | .accessibilityIdentifier("result0") 19 | 20 | Divider() 21 | 22 | Text("Enter an optional number:") 23 | UIKitTextField( 24 | config: .init() 25 | .value(value: $optionalValue, format: .number) 26 | ) 27 | .border(Color.black) 28 | Text("Your input: \(optionalValue.flatMap { "\($0)" } ?? "nil")") 29 | .accessibilityIdentifier("result1") 30 | 31 | Divider() 32 | 33 | Button { 34 | UIApplication.shared.sendAction( 35 | #selector(UIApplication.resignFirstResponder), 36 | to: nil, 37 | from: nil, 38 | for: nil 39 | ) 40 | } label: { 41 | Text("Dismiss keyboard") 42 | } 43 | } 44 | .padding() 45 | } 46 | } 47 | 48 | struct ValueBindingPage_Previews: PreviewProvider { 49 | static var previews: some View { 50 | ValueBindingPage() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/CustomTextFieldPage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | 4 | class CustomTextField: BaseUITextField { 5 | let padding = UIEdgeInsets(top: 4, left: 8 + 32 + 8, bottom: 4, right: 8) 6 | public override func textRect(forBounds bounds: CGRect) -> CGRect { 7 | super.textRect(forBounds: bounds).inset(by: padding) 8 | } 9 | public override func editingRect(forBounds bounds: CGRect) -> CGRect { 10 | super.editingRect(forBounds: bounds).inset(by: padding) 11 | } 12 | } 13 | 14 | struct CustomTextFieldPage: View { 15 | @State var isFocused = false 16 | var body: some View { 17 | VStack { 18 | UIKitTextField( 19 | config: .init { 20 | CustomTextField() 21 | } 22 | .focused($isFocused) 23 | .textContentType(.emailAddress) 24 | .keyboardType(.emailAddress) 25 | .autocapitalizationType(UITextAutocapitalizationType.none) 26 | .autocorrectionType(.no) 27 | ) 28 | .background(alignment: .leading) { 29 | HStack(spacing: 0) { 30 | Color.clear.frame(width: 8) 31 | ZStack { 32 | Image(systemName: "mail") 33 | } 34 | .frame(width: 32) 35 | } 36 | } 37 | .border(Color.black) 38 | 39 | Button { 40 | isFocused = false 41 | } label: { 42 | Text("Dismiss") 43 | } 44 | } 45 | .padding() 46 | } 47 | } 48 | 49 | struct CustomTextFieldPage_Previews: PreviewProvider { 50 | static var previews: some View { 51 | CustomTextFieldPage() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/UIKitTextField+Configuration+UITextFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | public extension UIKitTextField.Configuration { 6 | func shouldBeginEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self { 7 | var config = self 8 | config.shouldBeginEditingHandler = handler 9 | return config 10 | } 11 | 12 | func onBeganEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self { 13 | var config = self 14 | config.onBeganEditingHandler = handler 15 | return config 16 | } 17 | 18 | func shouldEndEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self { 19 | var config = self 20 | config.shouldEndEditingHandler = handler 21 | return config 22 | } 23 | 24 | func onEndedEditing(handler: @escaping (_ uiTextField: UITextFieldType, _ reason: UITextField.DidEndEditingReason) -> Void) -> Self { 25 | var config = self 26 | config.onEndedEditingHandler = handler 27 | return config 28 | } 29 | 30 | func shouldChangeCharacters(handler: @escaping (_ uiTextField: UITextFieldType, _ range: NSRange, _ replacementString: String) -> Bool) -> Self { 31 | var config = self 32 | config.shouldChangeCharactersHandler = handler 33 | return config 34 | } 35 | 36 | func onChangedSelection(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self { 37 | var config = self 38 | config.onChangedSelectionHandler = handler 39 | return config 40 | } 41 | 42 | func shouldClear(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self { 43 | var config = self 44 | config.shouldClearHandler = handler 45 | return config 46 | } 47 | 48 | func shouldReturn(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self { 49 | var config = self 50 | config.shouldReturnHandler = handler 51 | return config 52 | } 53 | } 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /AllTests/UITestingApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Examples/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/FocusBindingPage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | 4 | struct FocusBindingPage: View { 5 | @State var isFocused: Bool = false 6 | @State var focusedField: String? 7 | 8 | var body: some View { 9 | VStack(alignment: .leading) { 10 | Group { 11 | Text("isFocused: \(isFocused ? "true" : "false")") 12 | .accessibilityIdentifier("result0") 13 | UIKitTextField( 14 | config: .init() 15 | .focused($isFocused) 16 | ) 17 | .border(Color.black) 18 | Button { 19 | isFocused.toggle() 20 | } label: { 21 | Text("Toggle Focus") 22 | } 23 | } 24 | 25 | Divider() 26 | 27 | Group { 28 | Text("Focus: \(focusedField.flatMap { "\($0)" } ?? "nil")") 29 | .accessibilityIdentifier("result1") 30 | HStack { 31 | UIKitTextField( 32 | config: .init() 33 | .focused($focusedField, equals: "field1a") 34 | ) 35 | .border(Color.black) 36 | UIKitTextField( 37 | config: .init() 38 | .focused($focusedField, equals: "field1b") 39 | ) 40 | .border(Color.black) 41 | } 42 | HStack { 43 | Button { 44 | focusedField = "field1a" 45 | } label: { 46 | Text("Focus") 47 | .frame(maxWidth: .infinity) 48 | } 49 | .accessibilityIdentifier("focusButton1a") 50 | Button { 51 | focusedField = "field1b" 52 | } label: { 53 | Text("Focus") 54 | .frame(maxWidth: .infinity) 55 | } 56 | .accessibilityIdentifier("focusButton1b") 57 | } 58 | Button { 59 | focusedField = nil 60 | } label: { 61 | Text("Unfocus") 62 | .frame(maxWidth: .infinity) 63 | } 64 | } 65 | 66 | Divider() 67 | 68 | Button { 69 | UIApplication.shared.sendAction( 70 | #selector(UIApplication.resignFirstResponder), 71 | to: nil, 72 | from: nil, 73 | for: nil 74 | ) 75 | } label: { 76 | Text("Dismiss") 77 | } 78 | } 79 | .padding() 80 | } 81 | } 82 | 83 | struct FocusBindingPage_Previews: PreviewProvider { 84 | static var previews: some View { 85 | FocusBindingPage() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /AllTests/AllTests.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 27 | 33 | 34 | 35 | 36 | 37 | 47 | 48 | 54 | 55 | 57 | 58 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/InputViewManager.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import SwiftUI 4 | 5 | internal class InputViewManager where UITextFieldType: UITextFieldProtocol { 6 | let textField: UITextFieldType 7 | let inputViewContentController: InputViewContentController 8 | let inputViewContent: UIView 9 | let inputViewController: UIInputViewController 10 | let inputView: UIInputView 11 | var lastIntrinsicContentSize: CGSize? 12 | 13 | init(with textField: UITextFieldType, content: Content) where Content: View { 14 | self.textField = textField 15 | 16 | inputViewContentController = .init(rootView: .init(content)) 17 | 18 | inputViewContent = inputViewContentController.view 19 | inputViewContent.translatesAutoresizingMaskIntoConstraints = false 20 | 21 | inputViewController = .init() 22 | inputView = inputViewController.inputView! 23 | inputView.translatesAutoresizingMaskIntoConstraints = false 24 | inputView.allowsSelfSizing = true 25 | 26 | inputViewController.addChild(inputViewContentController) 27 | inputView.addSubview(inputViewContent) 28 | inputViewContent.leadingAnchor.constraint(equalTo: inputView.leadingAnchor).isActive = true 29 | inputViewContent.trailingAnchor.constraint(equalTo: inputView.trailingAnchor).isActive = true 30 | inputViewContent.topAnchor.constraint(equalTo: inputView.topAnchor).isActive = true 31 | inputViewContent.bottomAnchor.constraint(equalTo: inputView.bottomAnchor).isActive = true 32 | inputViewContentController.didMove(toParent: inputViewController) 33 | 34 | inputViewContentController.delegate = self 35 | } 36 | 37 | func update(with content: Content) where Content: View { 38 | inputViewContentController.rootView = .init(content) 39 | } 40 | } 41 | 42 | extension InputViewManager: InputViewContentControllerDelegate { 43 | func viewWillLayoutSubviews(_: InputViewContentController) {} 44 | 45 | func viewDidLayoutSubviews(_: InputViewContentController) { 46 | inputViewContent.invalidateIntrinsicContentSize() 47 | if inputViewContent.intrinsicContentSize != lastIntrinsicContentSize { 48 | lastIntrinsicContentSize = inputViewContent.intrinsicContentSize 49 | // NOTE: reloadInputViews() seems to sometimes cause flickering of the paste button on suggestion bar on iPad. 50 | // even a button press will trigger viewDidLayoutSubviews(), so only call it when necessary 51 | textField.reloadInputViews() 52 | } 53 | } 54 | } 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/UIKitTextField+Configuration+UITextInputTraits.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import UIKit 4 | 5 | public extension UIKitTextField.Configuration { 6 | func keyboardType(_ keyboardType: UIKeyboardType?) -> Self { 7 | var config = self 8 | config.keyboardType = keyboardType 9 | return config 10 | } 11 | 12 | func keyboardAppearance(_ keyboardAppearance: UIKeyboardAppearance?) -> Self { 13 | var config = self 14 | config.keyboardAppearance = keyboardAppearance 15 | return config 16 | } 17 | 18 | func returnKeyType(_ returnKeyType: UIReturnKeyType?) -> Self { 19 | var config = self 20 | config.returnKeyType = returnKeyType 21 | return config 22 | } 23 | 24 | func textContentType(_ textContentType: UITextContentType?) -> Self { 25 | var config = self 26 | config.textContentType = textContentType 27 | return config 28 | } 29 | 30 | func isSecureTextEntry(_ isSecureTextEntry: Bool?) -> Self { 31 | var config = self 32 | config.isSecureTextEntry = isSecureTextEntry 33 | return config 34 | } 35 | 36 | func enablesReturnKeyAutomatically(_ enablesReturnKeyAutomatically: Bool?) -> Self { 37 | var config = self 38 | config.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically 39 | return config 40 | } 41 | 42 | func autocapitalizationType(_ autocapitalizationType: UITextAutocapitalizationType?) -> Self { 43 | var config = self 44 | config.autocapitalizationType = autocapitalizationType 45 | return config 46 | } 47 | 48 | func autocorrectionType(_ autocorrectionType: UITextAutocorrectionType?) -> Self { 49 | var config = self 50 | config.autocorrectionType = autocorrectionType 51 | return config 52 | } 53 | 54 | func spellCheckingType(_ spellCheckingType: UITextSpellCheckingType?) -> Self { 55 | var config = self 56 | config.spellCheckingType = spellCheckingType 57 | return config 58 | } 59 | 60 | func smartQuotesType(_ smartQuotesType: UITextSmartQuotesType?) -> Self { 61 | var config = self 62 | config.smartQuotesType = smartQuotesType 63 | return config 64 | } 65 | 66 | func smartDashesType(_ smartDashesType: UITextSmartDashesType?) -> Self { 67 | var config = self 68 | config.smartDashesType = smartDashesType 69 | return config 70 | } 71 | 72 | func smartInsertDeleteType(_ smartInsertDeleteType: UITextSmartInsertDeleteType?) -> Self { 73 | var config = self 74 | config.smartInsertDeleteType = smartInsertDeleteType 75 | return config 76 | } 77 | } 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /AllTests/UITests/UITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension XCUIApplication { 4 | func tapKey(_ key: String) { 5 | // NOTE: need to skip keyboard onboarding, https://developer.apple.com/forums/thread/650826 6 | let keyButton = self.keys[key] 7 | if !keyButton.isHittable { 8 | // NOTE: sometimes it might need time to come into existence 9 | _ = keyButton.waitForExistence(timeout: 1) 10 | // NOTE: if still not hittable, there should be an onboarding screen 11 | if !keyButton.isHittable { 12 | self.buttons["Continue"].tap() 13 | } 14 | } 15 | keyButton.tap() 16 | } 17 | } 18 | 19 | class UITests: XCTestCase { 20 | override func setUpWithError() throws { 21 | continueAfterFailure = false 22 | } 23 | 24 | override func tearDownWithError() throws {} 25 | 26 | func testInputValidation() throws { 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | app.otherElements.buttons["Input Validation"].tap() 31 | 32 | let textField = app.textFields.element(boundBy: 0) 33 | 34 | XCTAssert(textField.exists) 35 | 36 | #if targetEnvironment(simulator) 37 | app.buttons["Disable Hardware Keyboard"].tap() 38 | #endif 39 | 40 | textField.tap() 41 | 42 | XCTAssertEqual(textField.value as? String, "") 43 | 44 | app.tapKey("A") 45 | app.tapKey("A") 46 | app.tapKey("A") 47 | 48 | XCTAssert(textField.value as? String == "") 49 | 50 | app.tapKey("more") 51 | 52 | app.tapKey("1") 53 | app.tapKey("2") 54 | app.tapKey("3") 55 | app.tapKey(".") 56 | app.tapKey(".") 57 | app.tapKey(".") 58 | app.tapKey("1") 59 | app.tapKey("1") 60 | app.tapKey("1") 61 | 62 | XCTAssert(textField.value as? String == "123.111") 63 | 64 | app.tapKey("more") 65 | 66 | app.tapKey("b") 67 | app.tapKey("c") 68 | 69 | XCTAssert(textField.value as? String == "123.111") 70 | 71 | app.tapKey("delete") 72 | app.tapKey("delete") 73 | app.tapKey("delete") 74 | app.tapKey("delete") 75 | app.tapKey("delete") 76 | app.tapKey("delete") 77 | app.tapKey("delete") 78 | 79 | XCTAssert(textField.value as? String == "") 80 | 81 | app.buttons["Restore Hardware Keyboard"].tap() 82 | 83 | textField.typeText("1234...4a3b2c1d") 84 | 85 | XCTAssert(textField.value as? String == "1234.4321") 86 | } 87 | 88 | func testInputViews() throws { 89 | let app = XCUIApplication() 90 | app.launch() 91 | 92 | app.otherElements.buttons["Input Views"].tap() 93 | 94 | let textField = app.textFields["Use custom input views to input"] 95 | 96 | XCTAssert(textField.exists) 97 | 98 | textField.tap() 99 | app.buttons["1"].tap() 100 | app.buttons["2"].tap() 101 | app.buttons["3"].tap() 102 | app.buttons["4"].tap() 103 | app.buttons["5"].tap() 104 | app.buttons["6"].tap() 105 | app.buttons["7"].tap() 106 | app.buttons["8"].tap() 107 | app.buttons["9"].tap() 108 | app.buttons["🐵"].tap() 109 | app.buttons["🐶"].tap() 110 | app.buttons["🦊"].tap() 111 | 112 | XCTAssertEqual(textField.value as? String, "123456789🐵🐶🦊") 113 | 114 | app.buttons["⌫"].tap() 115 | app.buttons["⌫"].tap() 116 | app.buttons["⌫"].tap() 117 | app.buttons["⌫"].tap() 118 | XCTAssertEqual(textField.value as? String, "12345678") 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /AllTests/AllTests.xcodeproj/xcshareddata/xcschemes/UITestingApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/UIKitTextFieldTests+UITextInputTraits.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import XCTest 4 | import SwiftUI 5 | @testable import UIKitTextField 6 | import ViewInspector 7 | 8 | extension UIKitTextFieldTests { 9 | func testKeyboardType() throws { 10 | let view = UIKitTextField( 11 | config: .init() 12 | .keyboardType(.emailAddress) 13 | ) 14 | ViewHosting.host(view: view) 15 | defer { 16 | ViewHosting.expel() 17 | } 18 | 19 | let textField = try view.inspect().actualView().uiView() 20 | XCTAssertEqual(textField.keyboardType, .emailAddress) 21 | } 22 | 23 | func testKeyboardAppearance() throws { 24 | let view = UIKitTextField( 25 | config: .init() 26 | .keyboardAppearance(.dark) 27 | ) 28 | ViewHosting.host(view: view) 29 | defer { 30 | ViewHosting.expel() 31 | } 32 | 33 | let textField = try view.inspect().actualView().uiView() 34 | XCTAssertEqual(textField.keyboardAppearance, .dark) 35 | } 36 | 37 | func testReturnKeyType() throws { 38 | let view = UIKitTextField( 39 | config: .init() 40 | .returnKeyType(.go) 41 | ) 42 | ViewHosting.host(view: view) 43 | defer { 44 | ViewHosting.expel() 45 | } 46 | 47 | let textField = try view.inspect().actualView().uiView() 48 | XCTAssertEqual(textField.returnKeyType, .go) 49 | } 50 | 51 | func testTextContentType() throws { 52 | let view = UIKitTextField( 53 | config: .init() 54 | .textContentType(.emailAddress) 55 | ) 56 | ViewHosting.host(view: view) 57 | defer { 58 | ViewHosting.expel() 59 | } 60 | 61 | let textField = try view.inspect().actualView().uiView() 62 | XCTAssertEqual(textField.textContentType, .emailAddress) 63 | } 64 | 65 | func testIsSecureTextEntry() throws { 66 | let view = UIKitTextField( 67 | config: .init() 68 | .isSecureTextEntry(true) 69 | ) 70 | ViewHosting.host(view: view) 71 | defer { 72 | ViewHosting.expel() 73 | } 74 | 75 | let textField = try view.inspect().actualView().uiView() 76 | XCTAssertEqual(textField.isSecureTextEntry, true) 77 | } 78 | 79 | func testEnablesReturnKeyAutomatically() throws { 80 | let view = UIKitTextField( 81 | config: .init() 82 | .enablesReturnKeyAutomatically(true) 83 | ) 84 | ViewHosting.host(view: view) 85 | defer { 86 | ViewHosting.expel() 87 | } 88 | 89 | let textField = try view.inspect().actualView().uiView() 90 | XCTAssertEqual(textField.enablesReturnKeyAutomatically, true) 91 | } 92 | 93 | func testAutocapitalizationType() throws { 94 | let view = UIKitTextField( 95 | config: .init() 96 | .autocapitalizationType(.words) 97 | ) 98 | ViewHosting.host(view: view) 99 | defer { 100 | ViewHosting.expel() 101 | } 102 | 103 | let textField = try view.inspect().actualView().uiView() 104 | XCTAssertEqual(textField.autocapitalizationType, .words) 105 | } 106 | 107 | func testAutocorrectionType() throws { 108 | let view = UIKitTextField( 109 | config: .init() 110 | .autocorrectionType(.no) 111 | ) 112 | ViewHosting.host(view: view) 113 | defer { 114 | ViewHosting.expel() 115 | } 116 | 117 | let textField = try view.inspect().actualView().uiView() 118 | XCTAssertEqual(textField.autocorrectionType, .no) 119 | } 120 | 121 | func testSpellCheckingType() throws { 122 | let view = UIKitTextField( 123 | config: .init() 124 | .spellCheckingType(.no) 125 | ) 126 | ViewHosting.host(view: view) 127 | defer { 128 | ViewHosting.expel() 129 | } 130 | 131 | let textField = try view.inspect().actualView().uiView() 132 | XCTAssertEqual(textField.spellCheckingType, .no) 133 | } 134 | 135 | func testSmartQuotesType() throws { 136 | let view = UIKitTextField( 137 | config: .init() 138 | .smartQuotesType(.no) 139 | ) 140 | ViewHosting.host(view: view) 141 | defer { 142 | ViewHosting.expel() 143 | } 144 | 145 | let textField = try view.inspect().actualView().uiView() 146 | XCTAssertEqual(textField.smartQuotesType, .no) 147 | } 148 | 149 | func testSmartDashesType() throws { 150 | let view = UIKitTextField( 151 | config: .init() 152 | .smartDashesType(.yes) 153 | ) 154 | ViewHosting.host(view: view) 155 | defer { 156 | ViewHosting.expel() 157 | } 158 | 159 | let textField = try view.inspect().actualView().uiView() 160 | XCTAssertEqual(textField.smartDashesType, .yes) 161 | } 162 | 163 | func testSmartInsertDeleteType() throws { 164 | let view = UIKitTextField( 165 | config: .init() 166 | .smartInsertDeleteType(.no) 167 | ) 168 | ViewHosting.host(view: view) 169 | defer { 170 | ViewHosting.expel() 171 | } 172 | 173 | let textField = try view.inspect().actualView().uiView() 174 | XCTAssertEqual(textField.smartInsertDeleteType, .no) 175 | } 176 | } 177 | 178 | #endif 179 | -------------------------------------------------------------------------------- /AllTests/UITestingApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | 4 | // HACK: disable hardware keyboard for ui testing 5 | // https://stackoverflow.com/a/57618331 6 | // https://github.com/nst/iOS-Runtime-Headers/blob/master/PrivateFrameworks/UIKitCore.framework/UIKeyboardInputMode.h 7 | #if targetEnvironment(simulator) 8 | class HardwareKeyboardDisablingHack { 9 | static let shared = HardwareKeyboardDisablingHack() 10 | 11 | private let setter = NSSelectorFromString("setHardwareLayout:") 12 | private let getter = NSSelectorFromString("hardwareLayout") 13 | private(set) var hardwareKeyboardIsDisabled = false 14 | private var originalState = [UITextInputMode: AnyObject]() 15 | 16 | func disableHardwareKeyboard() { 17 | if !hardwareKeyboardIsDisabled { 18 | UITextInputMode.activeInputModes.forEach { 19 | if 20 | $0.responds(to: getter) && $0.responds(to: setter), 21 | let value = $0.perform(getter) 22 | { 23 | originalState[$0] = value.takeUnretainedValue() 24 | $0.perform(setter, with: nil) 25 | } 26 | } 27 | 28 | hardwareKeyboardIsDisabled = true 29 | } 30 | } 31 | 32 | func enableHardwareKeyboard() { 33 | if hardwareKeyboardIsDisabled { 34 | originalState.forEach { 35 | $0.key.perform(setter, with: $0.value) 36 | } 37 | 38 | hardwareKeyboardIsDisabled = false 39 | } 40 | } 41 | } 42 | #endif 43 | 44 | struct InputValidationPage: View { 45 | @State var originalTextInputModeState: [UITextInputMode: AnyObject] = [:] 46 | 47 | var body: some View { 48 | VStack(alignment: .leading) { 49 | Text("Enter a number") 50 | UIKitTextField( 51 | config: .init() 52 | .shouldChangeCharacters { uiTextField, range, replacementString in 53 | if 54 | let oldText = uiTextField.text, 55 | let range = Range(range, in: oldText) 56 | { 57 | let newText = oldText.replacingCharacters(in: range, with: replacementString) 58 | if newText == "" || Double(newText) != nil { 59 | return true 60 | } 61 | } 62 | return false 63 | } 64 | ) 65 | .border(Color.black) 66 | 67 | #if targetEnvironment(simulator) 68 | Button { 69 | HardwareKeyboardDisablingHack.shared.disableHardwareKeyboard() 70 | } label: { 71 | Text("Disable Hardware Keyboard") 72 | } 73 | Button { 74 | HardwareKeyboardDisablingHack.shared.enableHardwareKeyboard() 75 | } label: { 76 | Text("Restore Hardware Keyboard") 77 | } 78 | #endif 79 | } 80 | } 81 | } 82 | 83 | struct KeyButton: View { 84 | let uiTextField: UITextField 85 | let text: String 86 | 87 | var body: some View { 88 | Button { 89 | uiTextField.insertText(text) 90 | } label: { 91 | Text(text) 92 | .frame(maxWidth: .infinity) 93 | .frame(height: 120) 94 | .background(Color.white) 95 | } 96 | } 97 | } 98 | 99 | struct InputViewPage: View { 100 | @State var state = 0 101 | var body: some View { 102 | VStack { 103 | UIKitTextField( 104 | config: .init() 105 | .placeholder("Use custom input views to input") 106 | .inputView(content: .view(content: { uiTextField in 107 | VStack(spacing: 1) { 108 | HStack(spacing: 1) { 109 | KeyButton(uiTextField: uiTextField, text: "1") 110 | KeyButton(uiTextField: uiTextField, text: "2") 111 | KeyButton(uiTextField: uiTextField, text: "3") 112 | } 113 | HStack(spacing: 1) { 114 | KeyButton(uiTextField: uiTextField, text: "4") 115 | KeyButton(uiTextField: uiTextField, text: "5") 116 | KeyButton(uiTextField: uiTextField, text: "6") 117 | } 118 | HStack(spacing: 1) { 119 | KeyButton(uiTextField: uiTextField, text: "7") 120 | KeyButton(uiTextField: uiTextField, text: "8") 121 | KeyButton(uiTextField: uiTextField, text: "9") 122 | } 123 | } 124 | .background(Color.gray) 125 | .border(Color.gray) 126 | })) 127 | .inputAccessoryView(content: .view { uiTextField in 128 | HStack(spacing: 1) { 129 | KeyButton(uiTextField: uiTextField, text: "🐵") 130 | KeyButton(uiTextField: uiTextField, text: "🐶") 131 | KeyButton(uiTextField: uiTextField, text: "🦊") 132 | Button { 133 | uiTextField.deleteBackward() 134 | } label: { 135 | Text("⌫") 136 | .frame(maxWidth: .infinity) 137 | .frame(height: 120) 138 | .background(Color.white) 139 | } 140 | } 141 | .background(Color.gray) 142 | .border(Color.gray) 143 | .clipped() 144 | }) 145 | ) 146 | .border(Color.black) 147 | } 148 | } 149 | } 150 | 151 | struct ContentView: View { 152 | var body: some View { 153 | NavigationView { 154 | VStack { 155 | NavigationLink(destination: InputValidationPage()) { 156 | Text("Input Validation") 157 | } 158 | NavigationLink(destination: InputViewPage()) { 159 | Text("Input Views") 160 | } 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Examples/Example/Example/Pages/InputViewPage.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKitTextField 3 | import MeasurementReader 4 | import Expression 5 | 6 | extension AnyView { 7 | init(content: () -> Self) { 8 | self.init(content()) 9 | } 10 | } 11 | 12 | struct KeyStyle: ButtonStyle { 13 | let width: CGFloat 14 | var height: CGFloat = 80 15 | func makeBody(configuration: Configuration) -> some View { 16 | configuration.label 17 | .foregroundColor(configuration.isPressed ? Color.white : Color.gray) 18 | .frame(maxWidth: width) 19 | .frame(height: height) 20 | .background(Color.black.opacity(configuration.isPressed ? 0.5 : 1)) 21 | .cornerRadius(16) 22 | } 23 | } 24 | 25 | struct KeyButton: View { 26 | let text: String 27 | let width: CGFloat 28 | var height: CGFloat = 80 29 | var action: () -> Void 30 | 31 | var body: some View { 32 | Button { 33 | action() 34 | } label: { 35 | Text("\(text)") 36 | } 37 | .buttonStyle(KeyStyle(width: width, height: height)) 38 | } 39 | } 40 | 41 | struct KeyPad: View { 42 | let spacing: CGFloat = 8 43 | let uiTextField: UITextField 44 | let onEvaluate: () -> Void 45 | @State var usesShortButtons = false 46 | 47 | var body: some View { 48 | // NOTE: for getting available width 49 | SimpleSizeReader { proxy in 50 | AnyView { 51 | let unitWidth = max(0, ((proxy.maxSize()?.width ?? 0) - 3 * spacing) / 4) 52 | let unitHeight: CGFloat = usesShortButtons ? 40 : 80 53 | func generic(_ text: String, width: CGFloat = unitWidth, height: CGFloat = unitHeight, action: @escaping () -> Void) -> some View { 54 | KeyButton(text: text, width: width, height: height) { 55 | action() 56 | } 57 | } 58 | func insert(_ text: String, width: CGFloat = unitWidth, height: CGFloat = unitHeight) -> some View { 59 | generic(text, width: width, height: height) { 60 | uiTextField.insertText(text) 61 | } 62 | } 63 | return .init( 64 | VStack(spacing: 0) { 65 | Color.clear 66 | .frame(height: 0) 67 | .measure(proxy) 68 | Toggle("Short Buttons", isOn: $usesShortButtons) 69 | .toggleStyle(.button) 70 | Spacer().frame(height: 8) 71 | VStack(spacing: 8) { 72 | HStack(spacing: 8) { 73 | generic("clear") { uiTextField.text = "" } 74 | insert("/") 75 | insert("*") 76 | generic("⌫") { uiTextField.deleteBackward() } 77 | } 78 | HStack(spacing: 8) { 79 | insert("7") 80 | insert("8") 81 | insert("9") 82 | insert("-") 83 | } 84 | HStack(spacing: 8) { 85 | insert("4") 86 | insert("5") 87 | insert("6") 88 | insert("+") 89 | } 90 | HStack(spacing: 8) { 91 | VStack { 92 | HStack { 93 | insert("1") 94 | insert("2") 95 | insert("3") 96 | } 97 | HStack { 98 | insert("0", width: unitWidth * 2 + spacing) 99 | insert(".") 100 | } 101 | } 102 | generic("enter", height: unitHeight * 2 + spacing) { 103 | onEvaluate() 104 | } 105 | } 106 | } 107 | } 108 | .padding(.top, 16) 109 | .padding(.horizontal, 8) 110 | .background(Color.gray) 111 | ) 112 | } 113 | } 114 | } 115 | } 116 | 117 | struct InputViewPage: View { 118 | @State var expression = "" 119 | @State var result = "" 120 | @State var isFocused = false 121 | 122 | var body: some View { 123 | VStack(alignment: .leading) { 124 | UIKitTextField( 125 | config: .init { 126 | PaddedTextField() 127 | } 128 | .placeholder("Enter your expression") 129 | .value(text: $expression) 130 | .focused($isFocused) 131 | .inputView(content: .view { uiTextField in 132 | KeyPad(uiTextField: uiTextField, onEvaluate: onEvaluate) 133 | }) 134 | .inputAccessoryView(content: .view { _ in 135 | HStack { 136 | Text("Preview: \(compute(expression: expression))") 137 | .frame(maxWidth: .infinity) 138 | } 139 | .background(Color.black) 140 | .foregroundColor(Color.white) 141 | }) 142 | .shouldReturn { _ in 143 | onEvaluate() 144 | return false 145 | } 146 | ) 147 | .padding(4) 148 | .border(Color.black) 149 | 150 | Text("Result: \(result)") 151 | 152 | Divider() 153 | 154 | Button { 155 | isFocused = false 156 | } label: { 157 | Text("Dismiss") 158 | } 159 | } 160 | .padding() 161 | } 162 | 163 | func compute(expression: String) -> String { 164 | if let result = try? Expression(expression).evaluate() { 165 | return "\(result)" 166 | } else { 167 | return "nil" 168 | } 169 | } 170 | 171 | func onEvaluate() { 172 | self.result = compute(expression: expression) 173 | } 174 | } 175 | 176 | struct InputViewPage_Previews: PreviewProvider { 177 | static var previews: some View { 178 | InputViewPage() 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/UIKitTextFieldTests+UITextFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import XCTest 4 | import SwiftUI 5 | @testable import UIKitTextField 6 | import ViewInspector 7 | 8 | extension UIKitTextFieldTests { 9 | func testUITextFieldDelegate() throws { 10 | // NOTE: test whether we interface correctly with UITextFieldDelegate 11 | struct CallCaptures { 12 | // NOTE: arguments terminated by Void 13 | var shouldBeginEditing = (UITextField, Void)(.init(), ()) 14 | var onBeganEditing = (UITextField, Void)(.init(), ()) 15 | var shouldEndEditing = (UITextField, Void)(.init(), ()) 16 | var onEndedEditing = ((UITextField, UITextField.DidEndEditingReason), Void)((.init(), .committed), ()) 17 | var shouldChangeCharacters = ((UITextField, NSRange, String), Void)((.init(), .init(), ""), ()) 18 | var onChangedSelection = (UITextField, Void)(.init(), ()) 19 | var shouldClear = (UITextField, Void)(.init(), ()) 20 | var shouldReturn = (UITextField, Void)(.init(), ()) 21 | } 22 | var captures = CallCaptures() 23 | 24 | var shouldBeginEditing = false 25 | var shouldEndEditing = false 26 | var shouldChangeCharacters = false 27 | var shouldClear = false 28 | var shouldReturn = false 29 | 30 | let view = UIKitTextField( 31 | config: .init() 32 | .shouldBeginEditing { uiTextField in 33 | captures.shouldBeginEditing = (uiTextField, ()) 34 | return shouldBeginEditing 35 | } 36 | .onBeganEditing { uiTextField in 37 | captures.onBeganEditing = (uiTextField, ()) 38 | } 39 | .shouldEndEditing { uiTextField in 40 | captures.shouldEndEditing = (uiTextField, ()) 41 | return shouldEndEditing 42 | } 43 | .onEndedEditing { uiTextField, reason in 44 | captures.onEndedEditing = ((uiTextField, reason), ()) 45 | } 46 | .shouldChangeCharacters { uiTextField, range, replacementString in 47 | captures.shouldChangeCharacters = ((uiTextField, range, replacementString), ()) 48 | return shouldChangeCharacters 49 | } 50 | .onChangedSelection { uiTextField in 51 | captures.onChangedSelection = (uiTextField, ()) 52 | } 53 | .shouldClear { uiTextField in 54 | captures.shouldClear = (uiTextField, ()) 55 | return shouldClear 56 | } 57 | .shouldReturn { uiTextField in 58 | captures.shouldReturn = (uiTextField, ()) 59 | return shouldReturn 60 | } 61 | ) 62 | ViewHosting.host(view: view) 63 | defer { 64 | ViewHosting.expel() 65 | } 66 | 67 | wait(for: 0.1) 68 | 69 | let textField = try view.inspect().actualView().uiView() 70 | 71 | captures = .init() 72 | 73 | shouldBeginEditing = false 74 | XCTAssertEqual(textField.delegate?.textFieldShouldBeginEditing?(textField), false) 75 | XCTAssertEqual(captures.shouldBeginEditing.0, textField) 76 | shouldBeginEditing = true 77 | XCTAssertEqual(textField.delegate?.textFieldShouldBeginEditing?(textField), true) 78 | XCTAssertEqual(captures.shouldBeginEditing.0, textField) 79 | 80 | textField.delegate?.textFieldDidBeginEditing?(textField) 81 | XCTAssertEqual(captures.onBeganEditing.0, textField) 82 | 83 | shouldEndEditing = false 84 | XCTAssertEqual(textField.delegate?.textFieldShouldEndEditing?(textField), false) 85 | XCTAssertEqual(captures.shouldEndEditing.0, textField) 86 | shouldEndEditing = true 87 | XCTAssertEqual(textField.delegate?.textFieldShouldEndEditing?(textField), true) 88 | XCTAssertEqual(captures.shouldEndEditing.0, textField) 89 | 90 | textField.delegate?.textFieldDidEndEditing?(textField, reason: .committed) 91 | XCTAssertEqual(captures.onEndedEditing.0.0, textField) 92 | XCTAssertEqual(captures.onEndedEditing.0.1, .committed) 93 | 94 | shouldChangeCharacters = false 95 | XCTAssertEqual( 96 | textField.delegate?.textField?( 97 | textField, 98 | shouldChangeCharactersIn: .init(location: 12, length: 34), 99 | replacementString: "abc" 100 | ), 101 | false 102 | ) 103 | XCTAssertEqual(captures.shouldChangeCharacters.0.0, textField) 104 | XCTAssertEqual(captures.shouldChangeCharacters.0.1, .init(location: 12, length: 34)) 105 | XCTAssertEqual(captures.shouldChangeCharacters.0.2, "abc") 106 | 107 | shouldChangeCharacters = true 108 | XCTAssertEqual( 109 | textField.delegate?.textField?( 110 | textField, 111 | shouldChangeCharactersIn: .init(location: 34, length: 56), 112 | replacementString: "def" 113 | ), 114 | true 115 | ) 116 | XCTAssertEqual(captures.shouldChangeCharacters.0.0, textField) 117 | XCTAssertEqual(captures.shouldChangeCharacters.0.1, .init(location: 34, length: 56)) 118 | XCTAssertEqual(captures.shouldChangeCharacters.0.2, "def") 119 | 120 | textField.delegate?.textFieldDidChangeSelection?(textField) 121 | XCTAssertEqual(captures.onChangedSelection.0, textField) 122 | 123 | shouldClear = false 124 | XCTAssertEqual(textField.delegate?.textFieldShouldClear?(textField), false) 125 | XCTAssertEqual(captures.shouldClear.0, textField) 126 | shouldClear = true 127 | XCTAssertEqual(textField.delegate?.textFieldShouldClear?(textField), true) 128 | XCTAssertEqual(captures.shouldClear.0, textField) 129 | 130 | shouldReturn = false 131 | XCTAssertEqual(textField.delegate?.textFieldShouldReturn?(textField), false) 132 | XCTAssertEqual(captures.shouldReturn.0, textField) 133 | shouldReturn = true 134 | XCTAssertEqual(textField.delegate?.textFieldShouldReturn?(textField), true) 135 | XCTAssertEqual(captures.shouldReturn.0, textField) 136 | } 137 | } 138 | 139 | #endif 140 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/UIKitTextField+Coordinator.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import SwiftUI 4 | 5 | public extension UIKitTextField { 6 | class Coordinator: NSObject, UITextFieldDelegate { 7 | var wrapper: UIKitTextField 8 | 9 | let textField: UITextFieldType 10 | 11 | let inputViewManager: InputViewManager 12 | let inputAccessoryViewManager: InputViewManager 13 | 14 | var lastForceUpdateCounter: Int = -1 15 | 16 | init(_ wrapper: UIKitTextField) { 17 | self.wrapper = wrapper 18 | 19 | textField = wrapper.config.makeTextFieldHandler() 20 | 21 | inputViewManager = .init(with: textField, content: EmptyView()) 22 | inputAccessoryViewManager = .init(with: textField, content: EmptyView()) 23 | 24 | super.init() 25 | 26 | textField.delegate = self 27 | } 28 | 29 | func updateInputView( 30 | with inputViewContent: InputViewContent, 31 | inputViewManager: InputViewManager, 32 | keyPath: ReferenceWritableKeyPath 33 | ) { 34 | let inputViewController: UIInputViewController? 35 | if let content = inputViewContent.content { 36 | inputViewManager.update(with: content(textField)) 37 | inputViewController = inputViewManager.inputViewController 38 | } else { 39 | inputViewManager.update(with: EmptyView()) 40 | inputViewController = nil 41 | } 42 | if textField[keyPath: keyPath] != inputViewController { 43 | textField[keyPath: keyPath] = inputViewController 44 | textField.reloadInputViews() 45 | } 46 | } 47 | 48 | func updateUITextInputTraits(with config: Configuration) { 49 | textField.keyboardType = config.keyboardType ?? .default 50 | textField.keyboardAppearance = config.keyboardAppearance ?? .default 51 | textField.returnKeyType = config.returnKeyType ?? .default 52 | textField.textContentType = config.textContentType 53 | textField.isSecureTextEntry = config.isSecureTextEntry ?? false 54 | textField.enablesReturnKeyAutomatically = config.enablesReturnKeyAutomatically ?? false 55 | textField.autocapitalizationType = config.autocapitalizationType ?? .sentences 56 | textField.autocorrectionType = config.autocorrectionType ?? .default 57 | textField.spellCheckingType = config.spellCheckingType ?? .default 58 | textField.smartQuotesType = config.smartQuotesType ?? .default 59 | textField.smartDashesType = config.smartDashesType ?? .default 60 | textField.smartInsertDeleteType = config.smartInsertDeleteType ?? .default 61 | } 62 | 63 | func update(with wrapper: UIKitTextField) { 64 | lastForceUpdateCounter = wrapper.forceUpdateCounter 65 | 66 | self.wrapper = wrapper 67 | 68 | let config = wrapper.config 69 | 70 | config.valueState?.updateViewValue(textField) 71 | 72 | textField.placeholder = config.placeholder 73 | textField.font = config.font 74 | if let textColor = config.textColor { 75 | textField.textColor = .init(textColor) 76 | } else { 77 | textField.textColor = nil 78 | } 79 | textField.textAlignment = config.textAlignment ?? .left 80 | textField.clearsOnBeginEditing = config.clearsOnBeginEditing ?? false 81 | textField.clearsOnInsertion = config.clearsOnInsertion ?? false 82 | textField.clearButtonMode = config.clearButtonMode ?? .never 83 | 84 | // NOTE: since it would trigger additional events, and state change is not allowed inside update(). 85 | // it's cleaner to do outside update() to let all events be handled in a way to allow state 86 | // change without the use of some `isInsideUpdate` variable 87 | if let focusControl = config.focusState { 88 | func shouldFocus() -> Bool { 89 | focusControl.isSet() && !textField.isFirstResponder 90 | } 91 | func shouldBlur() -> Bool { 92 | focusControl.isUnset() && textField.isFirstResponder 93 | } 94 | 95 | if shouldFocus() { 96 | DispatchQueue.main.async { 97 | if shouldFocus() { 98 | self.textField.becomeFirstResponder() 99 | } 100 | } 101 | } else if shouldBlur() { 102 | DispatchQueue.main.async { 103 | if shouldBlur() { 104 | self.textField.resignFirstResponder() 105 | } 106 | } 107 | } 108 | } 109 | 110 | // https://stackoverflow.com/a/59193838 111 | textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 112 | textField.setContentCompressionResistancePriority(.defaultLow, for: .vertical) 113 | textField.setContentHuggingPriority(config.stretchesHorizontally ? .defaultLow : .defaultHigh, for: .horizontal) 114 | textField.setContentHuggingPriority(config.stretchesVertically ? .defaultLow : .defaultHigh, for: .vertical) 115 | 116 | updateInputView( 117 | with: wrapper.config.inputViewContent, 118 | inputViewManager: inputViewManager, 119 | keyPath: \.inputViewController 120 | ) 121 | 122 | updateInputView( 123 | with: wrapper.config.inputAccessoryViewContent, 124 | inputViewManager: inputAccessoryViewManager, 125 | keyPath: \.inputAccessoryViewController 126 | ) 127 | 128 | updateUITextInputTraits(with: config) 129 | 130 | config.configureHandler(textField) 131 | } 132 | 133 | // UITextFieldDelegate 134 | // @objc members cannot live in extension, https://stackoverflow.com/a/48403602 135 | public func textFieldShouldBeginEditing(_: UITextField) -> Bool { 136 | wrapper.config.shouldBeginEditingHandler(textField) 137 | } 138 | 139 | public func textFieldDidBeginEditing(_: UITextField) { 140 | wrapper.config.focusState?.set() 141 | wrapper.config.onBeganEditingHandler(textField) 142 | } 143 | 144 | public func textFieldShouldEndEditing(_: UITextField) -> Bool { 145 | return wrapper.config.shouldEndEditingHandler(textField) 146 | } 147 | 148 | public func textFieldDidEndEditing(_: UITextField, reason: UITextField.DidEndEditingReason) { 149 | wrapper.forceUpdateCounter += 1 150 | if wrapper.config.focusState?.isSet() == true { 151 | wrapper.config.focusState?.unset() 152 | } 153 | wrapper.config.onEndedEditingHandler(textField, reason) 154 | } 155 | 156 | public func textField(_: UITextField, shouldChangeCharactersIn range: NSRange, replacementString: String) -> Bool { 157 | wrapper.config.shouldChangeCharactersHandler(textField, range, replacementString) 158 | } 159 | 160 | public func textFieldDidChangeSelection(_: UITextField) { 161 | wrapper.config.valueState?.onViewValueChanged(textField) 162 | wrapper.config.onChangedSelectionHandler(textField) 163 | } 164 | 165 | public func textFieldShouldClear(_: UITextField) -> Bool { 166 | wrapper.config.shouldClearHandler(textField) 167 | } 168 | 169 | public func textFieldShouldReturn(_: UITextField) -> Bool { 170 | wrapper.config.shouldReturnHandler(textField) 171 | } 172 | } 173 | } 174 | 175 | #endif 176 | -------------------------------------------------------------------------------- /Examples/Example/ExampleUITests/ExampleUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | // HACK: https://stackoverflow.com/questions/32897757/is-there-a-way-to-find-if-the-xcuielement-has-focus-or-not/35915719#35915719 4 | extension XCUIElement { 5 | var hasKeyboardFocus: Bool { 6 | (value(forKey: "hasKeyboardFocus") as? Bool) ?? false 7 | } 8 | } 9 | 10 | extension XCTestCase { 11 | func wait(for time: TimeInterval) { 12 | RunLoop.current.run(until: .now + time) 13 | } 14 | } 15 | 16 | class ExampleUITests: XCTestCase { 17 | 18 | override func setUpWithError() throws { 19 | continueAfterFailure = false 20 | } 21 | 22 | override func tearDownWithError() throws {} 23 | 24 | func testTextBinding() throws { 25 | let app = XCUIApplication() 26 | app.launch() 27 | 28 | app.otherElements.buttons["Text Binding"].tap() 29 | 30 | XCTAssert(app.staticTexts["Please enter your name above"].exists) 31 | 32 | let textField = app.textFields.element(boundBy: 0) 33 | 34 | textField.tap() 35 | textField.typeText("John") 36 | 37 | XCTAssertFalse(app.staticTexts["Please enter your name above"].exists) 38 | XCTAssert(app.staticTexts["Hello John"].exists) 39 | } 40 | 41 | func testValueBinding() throws { 42 | let app = XCUIApplication() 43 | app.launch() 44 | 45 | app.otherElements.buttons["Value Binding"].tap() 46 | 47 | let textField0 = app.textFields.element(boundBy: 0) 48 | let textField1 = app.textFields.element(boundBy: 1) 49 | let result0 = app.staticTexts["result0"] 50 | let result1 = app.staticTexts["result1"] 51 | 52 | XCTAssert(textField0.exists) 53 | XCTAssert(textField1.exists) 54 | XCTAssert(result0.exists) 55 | XCTAssert(result1.exists) 56 | 57 | textField0.tap() 58 | textField0.typeText("\(XCUIKeyboardKey.delete.rawValue)1234abcd1234") 59 | XCTAssertEqual(textField0.value as? String, "1234abcd1234") 60 | XCTAssertEqual(result0.label, "Your input: 1234") 61 | 62 | textField1.tap() 63 | XCTAssertEqual(textField0.value as? String, "1,234") 64 | 65 | textField0.tap() 66 | textField0.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 5)) 67 | XCTAssertEqual(textField0.value as? String, "") 68 | XCTAssertEqual(result0.label, "Your input: 1") 69 | 70 | textField1.tap() 71 | textField1.typeText("\(XCUIKeyboardKey.delete.rawValue)1234abcd1234") 72 | XCTAssertEqual(textField1.value as? String, "1234abcd1234") 73 | XCTAssertEqual(result1.label, "Your input: 1234") 74 | 75 | textField0.tap() 76 | XCTAssertEqual(textField1.value as? String, "1,234") 77 | 78 | textField1.tap() 79 | textField1.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 5)) 80 | XCTAssertEqual(textField1.value as? String, "") 81 | XCTAssertEqual(result1.label, "Your input: nil") 82 | } 83 | 84 | func testFocusBinding() throws { 85 | let app = XCUIApplication() 86 | app.launch() 87 | 88 | app.otherElements.buttons["Focus Binding"].tap() 89 | 90 | let textField0 = app.textFields.element(boundBy: 0) 91 | let textField1a = app.textFields.element(boundBy: 1) 92 | let textField1b = app.textFields.element(boundBy: 2) 93 | let focusButton0 = app.buttons["Toggle Focus"] 94 | let focusButton1a = app.buttons["focusButton1a"] 95 | let focusButton1b = app.buttons["focusButton1b"] 96 | let result0 = app.staticTexts["result0"] 97 | let result1 = app.staticTexts["result1"] 98 | 99 | // tapping on text fields 100 | XCTAssertEqual(result0.label, "isFocused: false") 101 | XCTAssertEqual(result1.label, "Focus: nil") 102 | 103 | textField0.tap() 104 | XCTAssertEqual(result0.label, "isFocused: true") 105 | XCTAssertEqual(result1.label, "Focus: nil") 106 | 107 | textField1a.tap() 108 | XCTAssertEqual(result0.label, "isFocused: false") 109 | XCTAssertEqual(result1.label, "Focus: field1a") 110 | 111 | textField1b.tap() 112 | XCTAssertEqual(result0.label, "isFocused: false") 113 | XCTAssertEqual(result1.label, "Focus: field1b") 114 | 115 | textField0.tap() 116 | XCTAssertEqual(result0.label, "isFocused: true") 117 | XCTAssertEqual(result1.label, "Focus: nil") 118 | 119 | wait(for: 0.5) 120 | app.buttons["Dismiss"].tap() 121 | wait(for: 0.5) 122 | XCTAssertEqual(result0.label, "isFocused: false") 123 | XCTAssertEqual(result1.label, "Focus: nil") 124 | 125 | // using buttons to set focus 126 | focusButton0.tap() 127 | XCTAssertEqual(result0.label, "isFocused: true") 128 | XCTAssertEqual(result1.label, "Focus: nil") 129 | XCTAssertEqual(textField0.hasKeyboardFocus, true) 130 | XCTAssertEqual(textField1a.hasKeyboardFocus, false) 131 | XCTAssertEqual(textField1b.hasKeyboardFocus, false) 132 | focusButton0.tap() 133 | XCTAssertEqual(result0.label, "isFocused: false") 134 | XCTAssertEqual(result1.label, "Focus: nil") 135 | XCTAssertEqual(textField0.hasKeyboardFocus, false) 136 | XCTAssertEqual(textField1a.hasKeyboardFocus, false) 137 | XCTAssertEqual(textField1b.hasKeyboardFocus, false) 138 | 139 | focusButton0.tap() 140 | XCTAssertEqual(result0.label, "isFocused: true") 141 | XCTAssertEqual(result1.label, "Focus: nil") 142 | XCTAssertEqual(textField0.hasKeyboardFocus, true) 143 | XCTAssertEqual(textField1a.hasKeyboardFocus, false) 144 | XCTAssertEqual(textField1b.hasKeyboardFocus, false) 145 | focusButton1a.tap() 146 | XCTAssertEqual(result0.label, "isFocused: false") 147 | XCTAssertEqual(result1.label, "Focus: field1a") 148 | XCTAssertEqual(textField0.hasKeyboardFocus, false) 149 | XCTAssertEqual(textField1a.hasKeyboardFocus, true) 150 | XCTAssertEqual(textField1b.hasKeyboardFocus, false) 151 | focusButton1b.tap() 152 | XCTAssertEqual(result0.label, "isFocused: false") 153 | XCTAssertEqual(result1.label, "Focus: field1b") 154 | XCTAssertEqual(textField0.hasKeyboardFocus, false) 155 | XCTAssertEqual(textField1a.hasKeyboardFocus, false) 156 | XCTAssertEqual(textField1b.hasKeyboardFocus, true) 157 | app.buttons["Unfocus"].tap() 158 | XCTAssertEqual(result0.label, "isFocused: false") 159 | XCTAssertEqual(result1.label, "Focus: nil") 160 | XCTAssertEqual(textField0.hasKeyboardFocus, false) 161 | XCTAssertEqual(textField1a.hasKeyboardFocus, false) 162 | XCTAssertEqual(textField1b.hasKeyboardFocus, false) 163 | focusButton1b.tap() 164 | XCTAssertEqual(result0.label, "isFocused: false") 165 | XCTAssertEqual(result1.label, "Focus: field1b") 166 | XCTAssertEqual(textField0.hasKeyboardFocus, false) 167 | XCTAssertEqual(textField1a.hasKeyboardFocus, false) 168 | XCTAssertEqual(textField1b.hasKeyboardFocus, true) 169 | app.buttons["Dismiss"].tap() 170 | XCTAssertEqual(result0.label, "isFocused: false") 171 | XCTAssertEqual(result1.label, "Focus: nil") 172 | XCTAssertEqual(textField0.hasKeyboardFocus, false) 173 | XCTAssertEqual(textField1a.hasKeyboardFocus, false) 174 | XCTAssertEqual(textField1b.hasKeyboardFocus, false) 175 | } 176 | 177 | func testInputView() throws { 178 | let app = XCUIApplication() 179 | app.launch() 180 | 181 | app.otherElements.buttons["Input View"].tap() 182 | 183 | let textField = app.textFields.element(boundBy: 0) 184 | 185 | textField.tap() 186 | 187 | app.buttons["1"].tap() 188 | XCTAssert(app.staticTexts["Preview: 1.0"].exists) 189 | app.buttons["+"].tap() 190 | XCTAssert(app.staticTexts["Preview: nil"].exists) 191 | app.buttons["2"].tap() 192 | XCTAssert(app.staticTexts["Preview: 3.0"].exists) 193 | app.buttons["*"].tap() 194 | XCTAssert(app.staticTexts["Preview: nil"].exists) 195 | app.buttons["3"].tap() 196 | XCTAssert(app.staticTexts["Preview: 7.0"].exists) 197 | app.buttons["-"].tap() 198 | XCTAssert(app.staticTexts["Preview: nil"].exists) 199 | app.buttons["4"].tap() 200 | XCTAssert(app.staticTexts["Preview: 3.0"].exists) 201 | XCTAssert(app.staticTexts["Result: "].exists) 202 | app.buttons["enter"].tap() 203 | 204 | XCTAssertEqual(textField.value as? String, "1+2*3-4") 205 | XCTAssert(app.staticTexts["Preview: 3.0"].exists) 206 | XCTAssert(app.staticTexts["Result: 3.0"].exists) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Sources/UIKitTextField/UIKitTextField+Configuration.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import SwiftUI 4 | 5 | public extension UIKitTextField { 6 | struct ValueState { 7 | let updateViewValue: (_ textField: UITextFieldType) -> Void 8 | let onViewValueChanged: (_ textField: UITextFieldType) -> Void 9 | } 10 | } 11 | 12 | public extension UIKitTextField { 13 | struct FocusState { 14 | let isSet: () -> Bool 15 | let isUnset: () -> Bool 16 | let set: () -> Void 17 | let unset: () -> Void 18 | } 19 | } 20 | 21 | public extension UIKitTextField { 22 | struct Configuration { 23 | var makeTextFieldHandler: () -> UITextFieldType 24 | 25 | var valueState: ValueState? = nil 26 | 27 | var placeholder: String? 28 | var font: UIFont? 29 | var textColor: Color? 30 | var textAlignment: NSTextAlignment? 31 | 32 | var clearsOnBeginEditing: Bool? 33 | var clearsOnInsertion: Bool? 34 | 35 | var clearButtonMode: UITextField.ViewMode? 36 | 37 | var focusState: FocusState? = nil 38 | 39 | var stretchesHorizontally = true 40 | var stretchesVertically = false 41 | 42 | var inputViewContent: InputViewContent = .none 43 | var inputAccessoryViewContent: InputViewContent = .none 44 | 45 | // UITextFieldDelegate 46 | var shouldBeginEditingHandler: (_ uiTextField: UITextFieldType) -> Bool = { _ in true } 47 | var onBeganEditingHandler: (_ uiTextField: UITextFieldType) -> Void = { _ in } 48 | var shouldEndEditingHandler: (_ uiTextField: UITextFieldType) -> Bool = { _ in true } 49 | var onEndedEditingHandler: (_ uiTextField: UITextFieldType, _ reason: UITextField.DidEndEditingReason) -> Void = { _, _ in } 50 | var shouldChangeCharactersHandler: (_ uiTextField: UITextFieldType, _ range: NSRange, _ replacementString: String) -> Bool = { _, _, _ in true } 51 | var onChangedSelectionHandler: (_ uiTextField: UITextFieldType) -> Void = { _ in } 52 | var shouldClearHandler: (_ uiTextField: UITextFieldType) -> Bool = { _ in true } 53 | var shouldReturnHandler: (_ uiTextField: UITextFieldType) -> Bool = { _ in true } 54 | 55 | // UITextInputTraits 56 | var keyboardType: UIKeyboardType? 57 | var keyboardAppearance: UIKeyboardAppearance? 58 | var returnKeyType: UIReturnKeyType? 59 | var textContentType: UITextContentType? 60 | var isSecureTextEntry: Bool? 61 | var enablesReturnKeyAutomatically: Bool? 62 | var autocapitalizationType: UITextAutocapitalizationType? 63 | var autocorrectionType: UITextAutocorrectionType? 64 | var spellCheckingType: UITextSpellCheckingType? 65 | var smartQuotesType: UITextSmartQuotesType? 66 | var smartDashesType: UITextSmartDashesType? 67 | var smartInsertDeleteType: UITextSmartInsertDeleteType? 68 | 69 | // extra configuration 70 | var configureHandler: (_ uiTextField: UITextFieldType) -> Void = { _ in } 71 | 72 | public init() where UITextFieldType == BaseUITextField { 73 | makeTextFieldHandler = { .init() } 74 | } 75 | 76 | public init(_ makeUITextField: @escaping () -> UITextFieldType) { 77 | makeTextFieldHandler = makeUITextField 78 | } 79 | } 80 | } 81 | 82 | public extension UIKitTextField.Configuration { 83 | func value( 84 | updateViewValue: @escaping (_ textField: UITextFieldType) -> Void, 85 | onViewValueChanged: @escaping (_ textField: UITextFieldType) -> Void 86 | ) -> Self { 87 | var config = self 88 | config.valueState = .init( 89 | updateViewValue: updateViewValue, 90 | onViewValueChanged: onViewValueChanged 91 | ) 92 | return config 93 | } 94 | 95 | func value(text: Binding) -> Self { 96 | value( 97 | updateViewValue: { textField in 98 | if textField.text != text.wrappedValue { 99 | textField.text = text.wrappedValue 100 | } 101 | }, 102 | onViewValueChanged: { textField in 103 | text.wrappedValue = textField.text ?? "" 104 | } 105 | ) 106 | } 107 | 108 | @available(iOS 15.0, *) 109 | func value(value: Binding, format: F) -> Self 110 | where 111 | F: ParseableFormatStyle, 112 | F.FormatOutput == String 113 | { 114 | self.value( 115 | value: Binding( 116 | get: { 117 | value.wrappedValue 118 | }, 119 | set: { 120 | if let newValue = $0 { 121 | value.wrappedValue = newValue 122 | } 123 | } 124 | ), 125 | format: format 126 | ) 127 | } 128 | 129 | @available(iOS 15.0, *) 130 | func value(value: Binding, format: F) -> Self 131 | where 132 | F: ParseableFormatStyle, 133 | F.FormatOutput == String 134 | { 135 | self.value( 136 | updateViewValue: { textField in 137 | if !textField.isFirstResponder { 138 | if let wrappedValue = value.wrappedValue { 139 | let text = format.format(wrappedValue) 140 | if text != textField.text { 141 | textField.text = text 142 | } 143 | } else { 144 | textField.text = "" 145 | } 146 | } 147 | }, 148 | onViewValueChanged: { textField in 149 | if textField.isFirstResponder { 150 | if let newValue = try? format.parseStrategy.parse(textField.text ?? "") { 151 | value.wrappedValue = newValue 152 | } else { 153 | value.wrappedValue = nil 154 | } 155 | } 156 | } 157 | ) 158 | } 159 | 160 | func value(value: Binding, formatter: Formatter) -> Self { 161 | self.value( 162 | value: Binding( 163 | get: { 164 | value.wrappedValue 165 | }, 166 | set: { 167 | if let newValue = $0 { 168 | value.wrappedValue = newValue 169 | } 170 | } 171 | ), 172 | formatter: formatter 173 | ) 174 | } 175 | 176 | func value(value: Binding, formatter: Formatter) -> Self { 177 | self.value( 178 | updateViewValue: { textField in 179 | if !textField.isFirstResponder { 180 | if let wrappedValue = value.wrappedValue { 181 | if 182 | let text = formatter.string(for: wrappedValue), 183 | text != textField.text 184 | { 185 | textField.text = text 186 | } 187 | } else { 188 | textField.text = "" 189 | } 190 | } 191 | }, 192 | onViewValueChanged: { textField in 193 | if textField.isFirstResponder { 194 | var objectValue: AnyObject? = nil 195 | if 196 | formatter.getObjectValue(&objectValue, for: textField.text ?? "", errorDescription: nil), 197 | let newValue = objectValue as? V 198 | { 199 | value.wrappedValue = newValue 200 | } else { 201 | value.wrappedValue = nil 202 | } 203 | } 204 | } 205 | ) 206 | } 207 | 208 | func placeholder(_ placeholder: String?) -> Self { 209 | var config = self 210 | config.placeholder = placeholder 211 | return config 212 | } 213 | 214 | func font(_ font: UIFont?) -> Self { 215 | var config = self 216 | config.font = font 217 | return config 218 | } 219 | 220 | func textColor(_ color: Color?) -> Self { 221 | var config = self 222 | config.textColor = color 223 | return config 224 | } 225 | 226 | func textAlignment(_ textAlignment: NSTextAlignment?) -> Self { 227 | var config = self 228 | config.textAlignment = textAlignment 229 | return config 230 | } 231 | 232 | func clearsOnBeginEditing(_ clearsOnBeginEditing: Bool?) -> Self { 233 | var config = self 234 | config.clearsOnBeginEditing = clearsOnBeginEditing 235 | return config 236 | } 237 | 238 | func clearsOnInsertion(_ clearsOnInsertion: Bool?) -> Self { 239 | var config = self 240 | config.clearsOnInsertion = clearsOnInsertion 241 | return config 242 | } 243 | 244 | func clearButtonMode(_ clearButtonMode: UITextField.ViewMode?) -> Self { 245 | var config = self 246 | config.clearButtonMode = clearButtonMode 247 | return config 248 | } 249 | 250 | func focused(_ binding: Binding) -> Self { 251 | var config = self 252 | config.focusState = .init( 253 | isSet: { 254 | binding.wrappedValue 255 | }, 256 | isUnset: { 257 | !binding.wrappedValue 258 | }, 259 | set: { 260 | if !binding.wrappedValue { 261 | binding.wrappedValue = true 262 | } 263 | }, 264 | unset: { 265 | if binding.wrappedValue { 266 | binding.wrappedValue = false 267 | } 268 | } 269 | ) 270 | return config 271 | } 272 | 273 | func focused(_ binding: Binding, equals value: Value?) -> Self where Value: Hashable { 274 | var config = self 275 | config.focusState = .init( 276 | isSet: { 277 | binding.wrappedValue == value 278 | }, 279 | isUnset: { 280 | binding.wrappedValue == nil 281 | }, 282 | set: { 283 | if binding.wrappedValue != value { 284 | binding.wrappedValue = value 285 | } 286 | }, 287 | unset: { 288 | if binding.wrappedValue != nil { 289 | binding.wrappedValue = nil 290 | } 291 | } 292 | ) 293 | return config 294 | } 295 | 296 | func stretches(horizontal: Bool, vertical: Bool) -> Self { 297 | var config = self 298 | config.stretchesHorizontally = horizontal 299 | config.stretchesVertically = vertical 300 | return config 301 | } 302 | 303 | func inputView(content: InputViewContent) -> Self { 304 | var config = self 305 | config.inputViewContent = content 306 | return config 307 | } 308 | 309 | func inputAccessoryView(content: InputViewContent) -> Self { 310 | var config = self 311 | config.inputAccessoryViewContent = content 312 | return config 313 | } 314 | } 315 | 316 | // extra configuration 317 | public extension UIKitTextField.Configuration { 318 | func configure(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self { 319 | var config = self 320 | config.configureHandler = handler 321 | return config 322 | } 323 | } 324 | 325 | #endif 326 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | 10 | # uikit-textfield 11 | 12 | `uikit-textfield` offers `UIKitTextField` which makes using `UITextField` in SwiftUI a breeze. 13 | 14 | From data binding, focus binding, handling callbacks from `UITextFieldDelegate`, 15 | custom `UITextField` subclass, horizontal/vertical stretching, `inputView`/`inputAccessoryView`, 16 | to extra configuration, `UIKitTextField` is the complete solution. 17 | 18 | - [Installation](#installation) 19 | - [Usage](#usage) 20 | - [Data Binding](#data-binding) 21 | - [Text](#text) 22 | - [Formatted Value](#formatted-value) 23 | - [Custom Data Binding](#custom-data-binding) 24 | - [Placeholder](#placeholder) 25 | - [Font](#font) 26 | - [Text Color](#text-color) 27 | - [Clear on Begin Editing](#clear-on-begin-editing) 28 | - [Clear on Insertion](#clear-on-insertion) 29 | - [Clear Button Mode](#clear-button-mode) 30 | - [Focus Binding](#focus-binding) 31 | - [Bool](#bool) 32 | - [Hashable](#hashable) 33 | - [Stretching](#stretching) 34 | - [Input View / Input Accessory View](#input-view--input-accessory-view) 35 | - [Custom Text Field Class](#custom-text-field-class) 36 | - [Extra Configuration](#extra-configuration) 37 | - [UITextFieldDelegate](#uitextfielddelegate) 38 | - [UITextInputTraits](#uitextinputtraits) 39 | - [Examples](#examples) 40 | - [Additional Notes](#additional-notes) 41 | 42 | 43 | ## Installation 44 | 45 | To install through Xcode, follow the [official guide](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) to add the following your Xcode project 46 | ``` 47 | https://github.com/vinceplusplus/uikit-textfield.git 48 | ``` 49 | 50 | To install through Swift Package Manager, add the following as package dependency and target dependency respectively 51 | ``` 52 | .package(url: "https://github.com/vinceplusplus/uikit-textfield.git", from: "2.0") 53 | ``` 54 | ``` 55 | .product(name: "UIKitTextField", package: "uikit-textfield") 56 | ``` 57 | 58 | ## Usage 59 | 60 | All configurations are done using `UIKitTextField.Configuration` and are in turn passed to 61 | `UIKitTextField`'s `.init(config: )` 62 | 63 | ### Data binding 64 | 65 | #### Text 66 | 67 | ```swift 68 | func value(text: Binding) -> Self 69 | ``` 70 | 71 | ```swift 72 | @State var name: String = "" 73 | 74 | var body: some View { 75 | VStack(alignment: .leading) { 76 | UIKitTextField( 77 | config: .init() 78 | .value(text: $name) 79 | ) 80 | .border(Color.black) 81 | Text("\(name.isEmpty ? "Please enter your name above" : "Hello \(name)")") 82 | } 83 | .padding() 84 | } 85 | ``` 86 | 87 | same height 88 | 89 | #### Formatted value 90 | 91 | ```swift 92 | @available(iOS 15.0, *) 93 | func value(value: Binding, format: F) -> Self 94 | where 95 | F: ParseableFormatStyle, 96 | F.FormatOutput == String 97 | 98 | @available(iOS 15.0, *) 99 | func value(value: Binding, format: F) -> Self 100 | where 101 | F: ParseableFormatStyle, 102 | F.FormatOutput == String 103 | 104 | func value(value: Binding, formatter: Formatter) -> Self 105 | 106 | func value(value: Binding, formatter: Formatter) -> Self 107 | ``` 108 | 109 | When the text field is not the first responder, it will take value from the binding and display in the specified formatted way 110 | 111 | When the text field is the first responder, every change will try to update the binding. If it's a binding of a non optional value, 112 | an invalid input will preserve the last value. If it's a binding of an optional value, an invalid input will set the value to `nil`. 113 | 114 | ```swift 115 | @State var value: Int = 0 116 | 117 | // ... 118 | 119 | Text("Enter a number:") 120 | 121 | UIKitTextField( 122 | config: .init() 123 | .value(value: $value, format: .number) 124 | //.value(value: $value, formatter: NumberFormatter()) 125 | ) 126 | .border(Color.black) 127 | 128 | // NOTE: avoiding the formatting behavior which comes from Text() 129 | Text("Your input: \("\(value)")") 130 | ``` 131 | 132 | same height 133 | 134 | #### Custom Data Binding 135 | 136 | ```swift 137 | func value( 138 | updateViewValue: @escaping (_ textField: UITextFieldType) -> Void, 139 | onViewValueChanged: @escaping (_ textField: UITextFieldType) -> Void 140 | ) -> Self 141 | ``` 142 | 143 | ### Placeholder 144 | 145 | ```swift 146 | func placeholder(_ placeholder: String?) -> Self 147 | ``` 148 | 149 | ```swift 150 | UIKitTextField( 151 | config: .init() 152 | .placeholder("some placeholder...") 153 | ) 154 | ``` 155 | 156 | ### Font 157 | 158 | ```swift 159 | func font(_ font: UIFont?) -> Self 160 | ``` 161 | 162 | Note that since there are no ways to convert back from a `Font` to `UIFont`, the configuration 163 | can only take a `UIFont` 164 | 165 | ```swift 166 | UIKitTextField( 167 | config: .init() 168 | .font(.systemFont(ofSize: 16)) 169 | ) 170 | ``` 171 | 172 | ### Text Color 173 | 174 | ```swift 175 | func textColor(_ color: Color?) -> Self 176 | ``` 177 | 178 | ```swift 179 | UIKitTextField( 180 | config: .init() 181 | .textColor(.red) 182 | ) 183 | ``` 184 | 185 | ### Text Alignment 186 | 187 | ```swift 188 | func textAlignment(_ textAlignment: NSTextAlignment?) -> Self 189 | ``` 190 | 191 | ```swift 192 | UIKitTextField( 193 | config: .init() 194 | .textAlignment(.center) 195 | ) 196 | ``` 197 | 198 | ### Clear on Begin Editing 199 | 200 | ```swift 201 | func clearsOnBeginEditing(_ clearsOnBeginEditing: Bool?) -> Self 202 | ``` 203 | 204 | ```swift 205 | UIKitTextField( 206 | config: .init() 207 | .clearsOnBeginEditing(true) 208 | ) 209 | ``` 210 | 211 | ### Clear on Insertion 212 | 213 | ```swift 214 | func clearsOnInsertion(_ clearsOnInsertion: Bool?) -> Self 215 | ``` 216 | 217 | ```swift 218 | UIKitTextField( 219 | config: .init() 220 | .clearsOnInsertion(true) 221 | ) 222 | ``` 223 | 224 | ### Clear Button Mode 225 | 226 | ```swift 227 | func clearButtonMode(_ clearButtonMode: UITextField.ViewMode?) -> Self 228 | ``` 229 | 230 | ```swift 231 | UIKitTextField( 232 | config: .init() 233 | .clearButtonMode(.always) 234 | ) 235 | ``` 236 | 237 | ### Focus Binding 238 | 239 | Similar to `@FocusState`, we could use an orindary `@State` to do a 2 way focus binding 240 | 241 | #### Bool 242 | 243 | ```swift 244 | func focused(_ binding: Binding) -> Self 245 | ``` 246 | 247 | ```swift 248 | @State var name = "" 249 | @State var isFocused = false 250 | 251 | VStack { 252 | Text("Your name:") 253 | UIKitTextField( 254 | config: .init() 255 | .value(text: $name) 256 | .focused($isFocused) 257 | ) 258 | Button { 259 | if name.isEmpty { 260 | isFocused = true 261 | } else { 262 | isFocused = false 263 | } 264 | } label: { 265 | Text("Submit") 266 | } 267 | } 268 | ``` 269 | 270 | #### Hashable 271 | 272 | ```swift 273 | func focused(_ binding: Binding, equals value: Value?) -> Self where Value: Hashable 274 | ``` 275 | 276 | ```swift 277 | enum Field { 278 | case firstName 279 | case lastName 280 | } 281 | 282 | @State var firstName = "" 283 | @State var lastName = "" 284 | @State var focusedField: Field? 285 | 286 | VStack { 287 | Text("First Name:") 288 | UIKitTextField( 289 | config: .init() 290 | .value(text: $firstName) 291 | .focused(focusedField, equals: .firstName) 292 | ) 293 | Text("Last Name:") 294 | UIKitTextField( 295 | config: .init() 296 | .value(text: $lastName) 297 | .focused(focusedField, equals: .lastName) 298 | ) 299 | Button { 300 | if firstName.isEmpty { 301 | focusedField = .firstName 302 | } else if lastName.isEmpty { 303 | focusedField = .lastName 304 | } else { 305 | focusedField = nil 306 | } 307 | } label: { 308 | Text("Submit") 309 | } 310 | } 311 | ``` 312 | 313 | ### Stretching 314 | 315 | By default, `UIKitTextField` will stretch horizontally but not vertically 316 | 317 | ```swift 318 | func stretches(horizontal: Bool, vertical: Bool) -> Self 319 | ``` 320 | 321 | ```swift 322 | UIKitTextField( 323 | config: .init { 324 | PaddedTextField() 325 | } 326 | .stretches(horizontal: true, vertical: false) 327 | ) 328 | .border(Color.black) 329 | ``` 330 | 331 | Note that `PaddedTextField` is just a simple internally padded `UITextField`, see more in custom init 332 | 333 | horizontal stretching 334 | 335 | ```swift 336 | UIKitTextField( 337 | config: .init { 338 | PaddedTextField() 339 | } 340 | .stretches(horizontal: false, vertical: false) 341 | ) 342 | .border(Color.black) 343 | ``` 344 | 345 | no stretching 346 | 347 | ### Input View / Input Accessory View 348 | 349 | Supporting `UITextField.inputView` and `UITextField.inputAccessoryView` by accepting a user defined `SwiftUI` view for each of them 350 | 351 | ```swift 352 | func inputView(content: InputViewContent) -> Self 353 | 354 | func inputAccessoryView(content: InputViewContent) -> Self 355 | ``` 356 | 357 | ```swift 358 | VStack(alignment: .leading) { 359 | UIKitTextField( 360 | config: .init { 361 | PaddedTextField() 362 | } 363 | .placeholder("Enter your expression") 364 | .value(text: $expression) 365 | .focused($isFocused) 366 | .inputView(content: .view { uiTextField in 367 | KeyPad(uiTextField: uiTextField, onEvaluate: onEvaluate) 368 | }) 369 | .shouldReturn { _ in 370 | onEvaluate() 371 | return false 372 | } 373 | ) 374 | .padding(4) 375 | .border(Color.black) 376 | 377 | Text("Result: \(result)") 378 | 379 | Divider() 380 | 381 | Button { 382 | isFocused = false 383 | } label: { 384 | Text("Dismiss") 385 | } 386 | } 387 | .padding() 388 | ``` 389 | 390 | Implementation of `KeyPad` can be found in `InputViewPage` from the example code. But the idea is to accept a `UITextField` 391 | parameter and render some buttons that do `uiTextField.insertText("...")` or `uiTextField.deleteBackward()`, like the 392 | following: 393 | 394 | ```swift 395 | struct CustomKeyboard: View { 396 | let uiTextField: UITextField 397 | var body: some View { 398 | VStack { 399 | HStack { 400 | Button { 401 | uiTextField.insertText("1") 402 | } label: { 403 | Text("1") 404 | } 405 | Button { 406 | uiTextField.insertText("2") 407 | } label: { 408 | Text("2") 409 | } 410 | Button { 411 | uiTextField.insertText("3") 412 | } label: { 413 | Text("3") 414 | } 415 | } 416 | HStack { /* ... */ } 417 | // ... 418 | } 419 | } 420 | } 421 | ``` 422 | 423 | input view 424 | 425 | ### Custom Text Field Class 426 | 427 | ```swift 428 | init(_ makeUITextField: @escaping () -> UITextFieldType) 429 | ``` 430 | 431 | A common use case of a `UITextField` subclass is to provide some internal padding which is also tappable. The following 432 | example demonstrates some extra leading padding to accomodate even an icon image 433 | 434 | ```swift 435 | class CustomTextField: BaseUITextField { 436 | let padding = UIEdgeInsets(top: 4, left: 8 + 32 + 8, bottom: 4, right: 8) 437 | public override func textRect(forBounds bounds: CGRect) -> CGRect { 438 | super.textRect(forBounds: bounds).inset(by: padding) 439 | } 440 | public override func editingRect(forBounds bounds: CGRect) -> CGRect { 441 | super.editingRect(forBounds: bounds).inset(by: padding) 442 | } 443 | } 444 | 445 | UIKitTextField( 446 | config: .init { 447 | CustomTextField() 448 | } 449 | .focused($isFocused) 450 | .textContentType(.emailAddress) 451 | .keyboardType(.emailAddress) 452 | .autocapitalizationType(UITextAutocapitalizationType.none) 453 | .autocorrectionType(.no) 454 | ) 455 | .background(alignment: .leading) { 456 | HStack(spacing: 0) { 457 | Color.clear.frame(width: 8) 458 | ZStack { 459 | Image(systemName: "mail") 460 | } 461 | .frame(width: 32) 462 | } 463 | } 464 | .border(Color.black) 465 | ``` 466 | 467 | custom text field class 468 | 469 | `UITextFieldType` needs to conform to `UITextFieldProtocol` which is shown below: 470 | 471 | ```swift 472 | public protocol UITextFieldProtocol: UITextField { 473 | var inputViewController: UIInputViewController? { get set } 474 | var inputAccessoryViewController: UIInputViewController? { get set } 475 | } 476 | ``` 477 | 478 | Basically, it needs have `inputViewController` and `inputAccessoryViewController` writable so the support 479 | for custom input view and custom input accessory view will work 480 | 481 | For most use cases, `BaseUITextField`, which provides baseline implementation of `UITextFieldProtocol`, 482 | can be subclassed to add more user defined behavior 483 | 484 | ### Extra Configuration 485 | 486 | ```swift 487 | func configure(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self 488 | ``` 489 | 490 | If there are configurations that `UIKitTextField` doesn't support out of the box, this is the place where we could add them. 491 | The extra configuration will be executed at the end of `updateUIView()` after applying all supported configuration (like data binding, etc) 492 | 493 | ```swift 494 | class PaddedTextField: BaseUITextField { 495 | var padding = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) { 496 | didSet { 497 | setNeedsLayout() 498 | } 499 | } 500 | 501 | public override func textRect(forBounds bounds: CGRect) -> CGRect { 502 | super.textRect(forBounds: bounds).inset(by: padding) 503 | } 504 | 505 | public override func editingRect(forBounds bounds: CGRect) -> CGRect { 506 | super.editingRect(forBounds: bounds).inset(by: padding) 507 | } 508 | } 509 | 510 | @State var text = "some text..." 511 | @State var pads = true 512 | 513 | var body: some View { 514 | VStack { 515 | Toggle("Padding", isOn: $pads) 516 | UIKitTextField( 517 | config: .init { 518 | PaddedTextField() 519 | } 520 | .value(text: $text) 521 | .configure { uiTextField in 522 | uiTextField.padding = pads ? .init(top: 4, left: 8, bottom: 4, right: 8) : .zero 523 | } 524 | ) 525 | .border(Color.black) 526 | } 527 | .padding() 528 | } 529 | ``` 530 | 531 | The above example provides a button to toggle internal padding 532 | 533 | padding toggled on 534 | padding toggled off 535 | 536 | ### UITextFieldDelegate 537 | 538 | `UITextFieldDelegate` is fully supported 539 | 540 | ```swift 541 | func shouldBeginEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self 542 | 543 | func onBeganEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self 544 | 545 | func shouldEndEditing(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self 546 | 547 | func onEndedEditing(handler: @escaping (_ uiTextField: UITextFieldType, _ reason: UITextField.DidEndEditingReason) -> Void) -> Self 548 | 549 | func shouldChangeCharacters(handler: @escaping (_ uiTextField: UITextFieldType, _ range: NSRange, _ replacementString: String) -> Bool) -> Self 550 | 551 | func onChangedSelection(handler: @escaping (_ uiTextField: UITextFieldType) -> Void) -> Self 552 | 553 | func shouldClear(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self 554 | 555 | func shouldReturn(handler: @escaping (_ uiTextField: UITextFieldType) -> Bool) -> Self 556 | ``` 557 | 558 | ### UITextInputTraits 559 | 560 | Most of the commonly used parts of `UITextInputTraits` are supported 561 | 562 | ```swift 563 | func keyboardType(_ keyboardType: UIKeyboardType?) -> Self 564 | 565 | func keyboardAppearance(_ keyboardAppearance: UIKeyboardAppearance?) -> Self 566 | 567 | func returnKeyType(_ returnKeyType: UIReturnKeyType?) -> Self 568 | 569 | func textContentType(_ textContentType: UITextContentType?) -> Self 570 | 571 | func isSecureTextEntry(_ isSecureTextEntry: Bool?) -> Self 572 | 573 | func enablesReturnKeyAutomatically(_ enablesReturnKeyAutomatically: Bool?) -> Self 574 | 575 | func autocapitalizationType(_ autocapitalizationType: UITextAutocapitalizationType?) -> Self 576 | 577 | func autocorrectionType(_ autocorrectionType: UITextAutocorrectionType?) -> Self 578 | 579 | func spellCheckingType(_ spellCheckingType: UITextSpellCheckingType?) -> Self 580 | 581 | func smartQuotesType(_ smartQuotesType: UITextSmartQuotesType?) -> Self 582 | 583 | func smartDashesType(_ smartDashesType: UITextSmartDashesType?) -> Self 584 | 585 | func smartInsertDeleteType(_ smartInsertDeleteType: UITextSmartInsertDeleteType?) -> Self 586 | ``` 587 | 588 | ## Examples 589 | 590 | `Examples/Example` is an example app that demonstrate how to use `UIKitTextField` 591 | 592 | ## Additional Notes 593 | 594 | `UITextField.isEnabled` is actually supported by the vanilla `.disabled(/* ... */)` which might not be very obvious 595 | -------------------------------------------------------------------------------- /Tests/UIKitTextFieldTests/UIKitTextFieldTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | 3 | import XCTest 4 | import SwiftUI 5 | @testable import UIKitTextField 6 | import ViewInspector 7 | import SnapshotTesting 8 | 9 | extension UIKitTextField: Inspectable {} 10 | 11 | final class UIKitTextFieldTests: XCTestCase { 12 | func testTextBinding() throws { 13 | class ViewModel: ObservableObject { 14 | @Published var text: String = "1234" 15 | } 16 | struct ContentView: View, Inspectable { 17 | @ObservedObject var viewModel: ViewModel 18 | var body: some View { 19 | UIKitTextField( 20 | config: .init() 21 | .value(text: $viewModel.text) 22 | ) 23 | } 24 | } 25 | 26 | let viewModel = ViewModel() 27 | let view = ContentView(viewModel: viewModel) 28 | ViewHosting.host(view: view) 29 | defer { 30 | ViewHosting.expel() 31 | } 32 | 33 | let textField = try view.inspect().find(UIKitTextField.self).actualView().uiView() 34 | XCTAssertEqual(textField.text, "1234") 35 | 36 | viewModel.text = "12345" 37 | wait(for: 0.1) 38 | XCTAssertEqual(textField.text, "12345") 39 | 40 | textField.becomeFirstResponder() 41 | textField.insertText("6") 42 | wait(for: 0.1) 43 | XCTAssertEqual(viewModel.text, "123456") 44 | 45 | textField.deleteBackward() 46 | wait(for: 0.1) 47 | XCTAssertEqual(viewModel.text, "12345") 48 | } 49 | 50 | @available(iOS 15.0, *) 51 | func testFormatBinding() throws { 52 | class ViewModel: ObservableObject { 53 | @Published var value: Double = 123 54 | } 55 | struct ContentView: View, Inspectable { 56 | @ObservedObject var viewModel: ViewModel 57 | var body: some View { 58 | UIKitTextField( 59 | config: .init() 60 | .value(value: $viewModel.value, format: .number) 61 | ) 62 | } 63 | } 64 | 65 | let viewModel = ViewModel() 66 | let view = ContentView(viewModel: viewModel) 67 | ViewHosting.host(view: view) 68 | defer { 69 | ViewHosting.expel() 70 | } 71 | 72 | let textField = try view.inspect().find(UIKitTextField.self).actualView().uiView() 73 | 74 | XCTAssertEqual(textField.text, "123") 75 | 76 | viewModel.value = 1234 77 | wait(for: 0.1) 78 | XCTAssertEqual(textField.text, "1,234") 79 | 80 | textField.becomeFirstResponder() 81 | textField.insertText("5") 82 | wait(for: 0.1) 83 | XCTAssertEqual(textField.text, "1,2345") 84 | XCTAssertEqual(viewModel.value, 12345) 85 | 86 | textField.resignFirstResponder() 87 | wait(for: 0.1) 88 | XCTAssertEqual(textField.text, "12,345") 89 | XCTAssertEqual(viewModel.value, 12345) 90 | } 91 | 92 | func testFormatterBinding() throws { 93 | class ViewModel: ObservableObject { 94 | @Published var value: Double = 123 95 | } 96 | struct ContentView: View, Inspectable { 97 | @ObservedObject var viewModel: ViewModel 98 | var body: some View { 99 | UIKitTextField( 100 | config: .init() 101 | .value(value: $viewModel.value, formatter: formatter) 102 | ) 103 | } 104 | var formatter: Formatter { 105 | let formatter = NumberFormatter() 106 | formatter.numberStyle = .decimal 107 | return formatter 108 | } 109 | } 110 | 111 | let viewModel = ViewModel() 112 | let view = ContentView(viewModel: viewModel) 113 | ViewHosting.host(view: view) 114 | defer { 115 | ViewHosting.expel() 116 | } 117 | 118 | let textField = try view.inspect().find(UIKitTextField.self).actualView().uiView() 119 | 120 | XCTAssertEqual(textField.text, "123") 121 | 122 | viewModel.value = 1234 123 | wait(for: 0.1) 124 | XCTAssertEqual(textField.text, "1,234") 125 | 126 | textField.becomeFirstResponder() 127 | textField.insertText("5") 128 | wait(for: 0.1) 129 | XCTAssertEqual(textField.text, "1,2345") 130 | // NOTE: `Formatter` has a slightly different behavior than `ParseableFormatStyle` 131 | XCTAssertEqual(viewModel.value, 1234) 132 | 133 | textField.resignFirstResponder() 134 | wait(for: 0.1) 135 | XCTAssertEqual(textField.text, "1,234") 136 | XCTAssertEqual(viewModel.value, 1234) 137 | } 138 | 139 | @available(iOS 15.0, *) 140 | func testOptionalFormatBinding() throws { 141 | class ViewModel: ObservableObject { 142 | @Published var value: Double? = 123 143 | } 144 | struct ContentView: View, Inspectable { 145 | @ObservedObject var viewModel: ViewModel 146 | var body: some View { 147 | UIKitTextField( 148 | config: .init() 149 | .value(value: $viewModel.value, format: .number) 150 | ) 151 | } 152 | } 153 | 154 | let viewModel = ViewModel() 155 | let view = ContentView(viewModel: viewModel) 156 | ViewHosting.host(view: view) 157 | defer { 158 | ViewHosting.expel() 159 | } 160 | 161 | let textField = try view.inspect().find(UIKitTextField.self).actualView().uiView() 162 | 163 | XCTAssertEqual(textField.text, "123") 164 | 165 | viewModel.value = 1234 166 | wait(for: 0.1) 167 | XCTAssertEqual(textField.text, "1,234") 168 | 169 | textField.becomeFirstResponder() 170 | textField.deleteBackward() 171 | textField.deleteBackward() 172 | textField.deleteBackward() 173 | textField.deleteBackward() 174 | textField.deleteBackward() 175 | textField.insertText("abc") 176 | wait(for: 0.1) 177 | XCTAssertEqual(textField.text, "abc") 178 | XCTAssertEqual(viewModel.value, nil) 179 | 180 | textField.resignFirstResponder() 181 | wait(for: 0.1) 182 | XCTAssertEqual(textField.text, "") 183 | XCTAssertEqual(viewModel.value, nil) 184 | } 185 | 186 | func testOptionalFormatterBinding() throws { 187 | class ViewModel: ObservableObject { 188 | @Published var value: Double? = 123 189 | } 190 | struct ContentView: View, Inspectable { 191 | @ObservedObject var viewModel: ViewModel 192 | var body: some View { 193 | UIKitTextField( 194 | config: .init() 195 | .value(value: $viewModel.value, formatter: formatter) 196 | ) 197 | } 198 | var formatter: Formatter { 199 | let formatter = NumberFormatter() 200 | formatter.numberStyle = .decimal 201 | return formatter 202 | } 203 | } 204 | 205 | let viewModel = ViewModel() 206 | let view = ContentView(viewModel: viewModel) 207 | ViewHosting.host(view: view) 208 | defer { 209 | ViewHosting.expel() 210 | } 211 | 212 | let textField = try view.inspect().find(UIKitTextField.self).actualView().uiView() 213 | 214 | XCTAssertEqual(textField.text, "123") 215 | 216 | viewModel.value = 1234 217 | wait(for: 0.1) 218 | XCTAssertEqual(textField.text, "1,234") 219 | 220 | textField.becomeFirstResponder() 221 | textField.deleteBackward() 222 | textField.deleteBackward() 223 | textField.deleteBackward() 224 | textField.deleteBackward() 225 | textField.deleteBackward() 226 | textField.insertText("abc") 227 | wait(for: 0.1) 228 | XCTAssertEqual(textField.text, "abc") 229 | XCTAssertEqual(viewModel.value, nil) 230 | 231 | textField.resignFirstResponder() 232 | wait(for: 0.1) 233 | XCTAssertEqual(textField.text, "") 234 | XCTAssertEqual(viewModel.value, nil) 235 | } 236 | 237 | func testPlaceholder() throws { 238 | let placeholder = "Enter something ..." 239 | let view = UIKitTextField( 240 | config: .init() 241 | .placeholder(placeholder) 242 | ) 243 | ViewHosting.host(view: view) 244 | defer { 245 | ViewHosting.expel() 246 | } 247 | 248 | let textField = try view.inspect().actualView().uiView() 249 | XCTAssertEqual(textField.placeholder, placeholder) 250 | } 251 | 252 | func testFont() throws { 253 | let font = UIFont.systemFont(ofSize: 16) 254 | let view = UIKitTextField( 255 | config: .init() 256 | .font(font) 257 | ) 258 | ViewHosting.host(view: view) 259 | defer { 260 | ViewHosting.expel() 261 | } 262 | 263 | let textField = try view.inspect().actualView().uiView() 264 | XCTAssertEqual(textField.font, font) 265 | } 266 | 267 | func testTextColor() throws { 268 | let textColor = Color.red 269 | let view = UIKitTextField( 270 | config: .init() 271 | .textColor(textColor) 272 | ) 273 | ViewHosting.host(view: view) 274 | defer { 275 | ViewHosting.expel() 276 | } 277 | 278 | let textField = try view.inspect().actualView().uiView() 279 | XCTAssertEqual(textField.textColor?.isEqual(UIColor(textColor)), true) 280 | } 281 | 282 | func testTextAlignment() throws { 283 | let view = UIKitTextField( 284 | config: .init() 285 | .textAlignment(.center) 286 | ) 287 | ViewHosting.host(view: view) 288 | defer { 289 | ViewHosting.expel() 290 | } 291 | 292 | let textField = try view.inspect().actualView().uiView() 293 | XCTAssertEqual(textField.textAlignment, .center) 294 | } 295 | 296 | func testClearsOnBeginEditing() throws { 297 | let view = UIKitTextField( 298 | config: .init() 299 | .clearsOnBeginEditing(true) 300 | ) 301 | ViewHosting.host(view: view) 302 | defer { 303 | ViewHosting.expel() 304 | } 305 | 306 | let textField = try view.inspect().actualView().uiView() 307 | XCTAssertEqual(textField.clearsOnBeginEditing, true) 308 | } 309 | 310 | func testClearsOnInsertion() throws { 311 | let view = UIKitTextField( 312 | config: .init() 313 | .clearsOnInsertion(true) 314 | ) 315 | ViewHosting.host(view: view) 316 | defer { 317 | ViewHosting.expel() 318 | } 319 | 320 | let textField = try view.inspect().actualView().uiView() 321 | XCTAssertEqual(textField.clearsOnInsertion, true) 322 | } 323 | 324 | func testClearButtonMode() throws { 325 | let view = UIKitTextField( 326 | config: .init() 327 | .clearButtonMode(.always) 328 | ) 329 | ViewHosting.host(view: view) 330 | defer { 331 | ViewHosting.expel() 332 | } 333 | 334 | let textField = try view.inspect().actualView().uiView() 335 | XCTAssertEqual(textField.clearButtonMode, .always) 336 | } 337 | 338 | func testStretching() throws { 339 | assertSnapshot( 340 | matching: ZStack { 341 | UIKitTextField( 342 | config: .init() 343 | .value(text: .constant("Abcdef")) 344 | .stretches(horizontal: false, vertical: false) 345 | ) 346 | .border(Color.green) 347 | } 348 | .frame(width: 200, height: 200) 349 | , 350 | as: .image 351 | ) 352 | 353 | assertSnapshot( 354 | matching: ZStack { 355 | UIKitTextField( 356 | config: .init() 357 | .value(text: .constant("Abcdef")) 358 | .stretches(horizontal: true, vertical: false) 359 | ) 360 | .border(Color.green) 361 | } 362 | .frame(width: 200, height: 200) 363 | , 364 | as: .image 365 | ) 366 | 367 | assertSnapshot( 368 | matching: ZStack { 369 | UIKitTextField( 370 | config: .init() 371 | .value(text: .constant("Abcdef")) 372 | .stretches(horizontal: false, vertical: true) 373 | ) 374 | .border(Color.green) 375 | } 376 | .frame(width: 200, height: 200) 377 | , 378 | as: .image 379 | ) 380 | 381 | assertSnapshot( 382 | matching: ZStack { 383 | UIKitTextField( 384 | config: .init() 385 | .value(text: .constant("Abcdef")) 386 | .stretches(horizontal: true, vertical: true) 387 | ) 388 | .border(Color.green) 389 | } 390 | .frame(width: 200, height: 200) 391 | , 392 | as: .image 393 | ) 394 | } 395 | 396 | func testBoolFocusState() throws { 397 | class ViewModel: ObservableObject { 398 | @Published var isFocused: Bool = false 399 | } 400 | struct ContentView: View, Inspectable { 401 | @ObservedObject var viewModel: ViewModel 402 | var body: some View { 403 | UIKitTextField( 404 | config: .init() 405 | .focused($viewModel.isFocused) 406 | ) 407 | } 408 | } 409 | 410 | let viewModel = ViewModel() 411 | let view = ContentView(viewModel: viewModel) 412 | ViewHosting.host(view: view) 413 | defer { 414 | ViewHosting.expel() 415 | } 416 | 417 | wait(for: 0.1) 418 | 419 | let textField = try view.inspect().find(UIKitTextField.self).actualView().uiView() 420 | 421 | XCTAssertEqual(textField.isFirstResponder, false) 422 | 423 | viewModel.isFocused = true 424 | wait(for: 0.1) 425 | XCTAssertEqual(textField.isFirstResponder, true) 426 | 427 | viewModel.isFocused = false 428 | wait(for: 0.1) 429 | XCTAssertEqual(textField.isFirstResponder, false) 430 | 431 | textField.becomeFirstResponder() 432 | wait(for: 0.1) 433 | XCTAssertEqual(viewModel.isFocused, true) 434 | 435 | textField.resignFirstResponder() 436 | wait(for: 0.1) 437 | XCTAssertEqual(viewModel.isFocused, false) 438 | } 439 | 440 | func testValueFocusState() throws { 441 | // NOTE: ViewInspector seems to have trouble locating the second text field, instead, 442 | // it returns the first one as the second one 443 | let textField0 = BaseUITextField() 444 | let textField1 = BaseUITextField() 445 | 446 | enum Field { 447 | case field0 448 | case field1 449 | } 450 | class ViewModel: ObservableObject { 451 | @Published var focusedField: Field? 452 | } 453 | struct ContentView: View, Inspectable { 454 | let textField0: BaseUITextField 455 | let textField1: BaseUITextField 456 | @ObservedObject var viewModel: ViewModel 457 | var body: some View { 458 | return VStack { 459 | UIKitTextField( 460 | config: .init { 461 | textField0 462 | } 463 | .value(text: .constant("abcdef")) 464 | .focused($viewModel.focusedField, equals: .field0) 465 | ) 466 | UIKitTextField( 467 | config: .init { 468 | textField1 469 | } 470 | .value(text: .constant("123456")) 471 | .focused($viewModel.focusedField, equals: .field1) 472 | ) 473 | } 474 | .frame(width: 200, height: 200) 475 | } 476 | } 477 | 478 | let viewModel = ViewModel() 479 | let view = ContentView( 480 | textField0: textField0, 481 | textField1: textField1, 482 | viewModel: viewModel 483 | ) 484 | ViewHosting.host(view: view) 485 | defer { 486 | ViewHosting.expel() 487 | } 488 | 489 | wait(for: 0.1) 490 | 491 | XCTAssertEqual(textField0.isFirstResponder, false) 492 | XCTAssertEqual(textField1.isFirstResponder, false) 493 | 494 | viewModel.focusedField = .field0 495 | wait(for: 0.1) 496 | XCTAssertEqual(textField0.isFirstResponder, true) 497 | XCTAssertEqual(textField1.isFirstResponder, false) 498 | 499 | viewModel.focusedField = .field1 500 | wait(for: 0.1) 501 | XCTAssertEqual(textField0.isFirstResponder, false) 502 | XCTAssertEqual(textField1.isFirstResponder, true) 503 | 504 | viewModel.focusedField = nil 505 | wait(for: 0.1) 506 | XCTAssertEqual(textField0.isFirstResponder, false) 507 | XCTAssertEqual(textField1.isFirstResponder, false) 508 | 509 | textField0.becomeFirstResponder() 510 | wait(for: 0.1) 511 | XCTAssertEqual(viewModel.focusedField, .field0) 512 | 513 | textField1.becomeFirstResponder() 514 | wait(for: 0.1) 515 | XCTAssertEqual(viewModel.focusedField, .field1) 516 | 517 | textField1.resignFirstResponder() 518 | wait(for: 0.1) 519 | XCTAssertEqual(viewModel.focusedField, nil) 520 | } 521 | 522 | func testInputViews() throws { 523 | struct KeyButton: View { 524 | let uiTextField: UITextField 525 | let text: String 526 | 527 | var body: some View { 528 | Button {} label: { 529 | Text(text) 530 | .frame(maxWidth: .infinity) 531 | .frame(height: 100) 532 | .background(Color.white) 533 | } 534 | } 535 | } 536 | let textField = BaseUITextField() 537 | let view = UIKitTextField( 538 | config: .init { 539 | textField 540 | } 541 | .value(text: .constant("abc")) 542 | .inputView(content: .view { uiTextField in 543 | VStack(spacing: 1) { 544 | HStack(spacing: 1) { 545 | KeyButton(uiTextField: uiTextField, text: "1") 546 | KeyButton(uiTextField: uiTextField, text: "2") 547 | KeyButton(uiTextField: uiTextField, text: "3") 548 | } 549 | HStack(spacing: 1) { 550 | KeyButton(uiTextField: uiTextField, text: "4") 551 | KeyButton(uiTextField: uiTextField, text: "5") 552 | KeyButton(uiTextField: uiTextField, text: "6") 553 | } 554 | HStack(spacing: 1) { 555 | KeyButton(uiTextField: uiTextField, text: "7") 556 | KeyButton(uiTextField: uiTextField, text: "8") 557 | KeyButton(uiTextField: uiTextField, text: "9") 558 | } 559 | } 560 | .background(Color.gray) 561 | .border(Color.gray) 562 | }) 563 | .inputAccessoryView(content: .view { uiTextField in 564 | HStack(spacing: 1) { 565 | KeyButton(uiTextField: uiTextField, text: "🐵") 566 | KeyButton(uiTextField: uiTextField, text: "🐶") 567 | KeyButton(uiTextField: uiTextField, text: "🦊") 568 | } 569 | .background(Color.gray) 570 | .border(Color.gray) 571 | .clipped() 572 | }) 573 | ) 574 | .frame(width: 200, height: 100) 575 | 576 | ViewHosting.host(view: view) 577 | defer { 578 | ViewHosting.expel() 579 | } 580 | 581 | wait(for: 0.1) 582 | 583 | textField.becomeFirstResponder() 584 | 585 | wait(for: 1) 586 | 587 | assertSnapshot(matching: textField.inputViewController!.view.layer, as: .image) 588 | assertSnapshot(matching: textField.inputAccessoryViewController!.view.layer, as: .image) 589 | } 590 | 591 | func testCustomTextField() throws { 592 | class CustomTextField: BaseUITextField {} 593 | 594 | let customTextField = CustomTextField() 595 | 596 | let view = UIKitTextField( 597 | config: .init { 598 | customTextField 599 | } 600 | ) 601 | ViewHosting.host(view: view) 602 | defer { 603 | ViewHosting.expel() 604 | } 605 | 606 | let textField = try view.inspect().actualView().uiView() 607 | XCTAssertEqual(textField, customTextField) 608 | XCTAssert(type(of: textField) === CustomTextField.self) 609 | } 610 | 611 | func testConfigure() throws { 612 | var capture = (UITextField, Void)(.init(), ()) 613 | 614 | let view = UIKitTextField( 615 | config: .init() 616 | .configure { uiTextField in 617 | capture = (uiTextField, ()) 618 | } 619 | ) 620 | ViewHosting.host(view: view) 621 | defer { 622 | ViewHosting.expel() 623 | } 624 | 625 | wait(for: 0.1) 626 | 627 | let textField = try view.inspect().actualView().uiView() 628 | XCTAssertEqual(capture.0, textField) 629 | } 630 | } 631 | 632 | #endif 633 | -------------------------------------------------------------------------------- /AllTests/AllTests.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 55139ABB28128C7000A20C2E /* UITestingApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55139ABA28128C7000A20C2E /* UITestingApp.swift */; }; 11 | 55139ABD28128C7000A20C2E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55139ABC28128C7000A20C2E /* ContentView.swift */; }; 12 | 55139ABF28128C7100A20C2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 55139ABE28128C7100A20C2E /* Assets.xcassets */; }; 13 | 55139AC228128C7100A20C2E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 55139AC128128C7100A20C2E /* Preview Assets.xcassets */; }; 14 | 55139AE928128CAB00A20C2E /* UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55139AE828128CAB00A20C2E /* UITests.swift */; }; 15 | 55139AF82812904F00A20C2E /* UIKitTextField in Frameworks */ = {isa = PBXBuildFile; productRef = 55139AF72812904F00A20C2E /* UIKitTextField */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXContainerItemProxy section */ 19 | 55139AEC28128CAB00A20C2E /* PBXContainerItemProxy */ = { 20 | isa = PBXContainerItemProxy; 21 | containerPortal = 55139AAC28128B9500A20C2E /* Project object */; 22 | proxyType = 1; 23 | remoteGlobalIDString = 55139AB628128C7000A20C2E; 24 | remoteInfo = UITestingApp; 25 | }; 26 | /* End PBXContainerItemProxy section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 55139AB728128C7000A20C2E /* UITestingApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UITestingApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 30 | 55139ABA28128C7000A20C2E /* UITestingApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestingApp.swift; sourceTree = ""; }; 31 | 55139ABC28128C7000A20C2E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 32 | 55139ABE28128C7100A20C2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | 55139AC128128C7100A20C2E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 34 | 55139AE628128CAB00A20C2E /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 55139AE828128CAB00A20C2E /* UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITests.swift; sourceTree = ""; }; 36 | 55139AF628128E7200A20C2E /* uikit-textfield */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "uikit-textfield"; path = ..; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 55139AB428128C7000A20C2E /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | 55139AF82812904F00A20C2E /* UIKitTextField in Frameworks */, 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | 55139AE328128CAB00A20C2E /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 55139AAB28128B9500A20C2E = { 59 | isa = PBXGroup; 60 | children = ( 61 | 55139AF628128E7200A20C2E /* uikit-textfield */, 62 | 55139AB928128C7000A20C2E /* UITestingApp */, 63 | 55139AE728128CAB00A20C2E /* UITests */, 64 | 55139AB828128C7000A20C2E /* Products */, 65 | 55139AF128128D0700A20C2E /* Frameworks */, 66 | ); 67 | sourceTree = ""; 68 | }; 69 | 55139AB828128C7000A20C2E /* Products */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 55139AB728128C7000A20C2E /* UITestingApp.app */, 73 | 55139AE628128CAB00A20C2E /* UITests.xctest */, 74 | ); 75 | name = Products; 76 | sourceTree = ""; 77 | }; 78 | 55139AB928128C7000A20C2E /* UITestingApp */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 55139ABA28128C7000A20C2E /* UITestingApp.swift */, 82 | 55139ABC28128C7000A20C2E /* ContentView.swift */, 83 | 55139ABE28128C7100A20C2E /* Assets.xcassets */, 84 | 55139AC028128C7100A20C2E /* Preview Content */, 85 | ); 86 | path = UITestingApp; 87 | sourceTree = ""; 88 | }; 89 | 55139AC028128C7100A20C2E /* Preview Content */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 55139AC128128C7100A20C2E /* Preview Assets.xcassets */, 93 | ); 94 | path = "Preview Content"; 95 | sourceTree = ""; 96 | }; 97 | 55139AE728128CAB00A20C2E /* UITests */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 55139AE828128CAB00A20C2E /* UITests.swift */, 101 | ); 102 | path = UITests; 103 | sourceTree = ""; 104 | }; 105 | 55139AF128128D0700A20C2E /* Frameworks */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | ); 109 | name = Frameworks; 110 | sourceTree = ""; 111 | }; 112 | /* End PBXGroup section */ 113 | 114 | /* Begin PBXNativeTarget section */ 115 | 55139AB628128C7000A20C2E /* UITestingApp */ = { 116 | isa = PBXNativeTarget; 117 | buildConfigurationList = 55139AD928128C7100A20C2E /* Build configuration list for PBXNativeTarget "UITestingApp" */; 118 | buildPhases = ( 119 | 55139AB328128C7000A20C2E /* Sources */, 120 | 55139AB428128C7000A20C2E /* Frameworks */, 121 | 55139AB528128C7000A20C2E /* Resources */, 122 | ); 123 | buildRules = ( 124 | ); 125 | dependencies = ( 126 | ); 127 | name = UITestingApp; 128 | packageProductDependencies = ( 129 | 55139AF72812904F00A20C2E /* UIKitTextField */, 130 | ); 131 | productName = UITestingApp; 132 | productReference = 55139AB728128C7000A20C2E /* UITestingApp.app */; 133 | productType = "com.apple.product-type.application"; 134 | }; 135 | 55139AE528128CAB00A20C2E /* UITests */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = 55139AEE28128CAB00A20C2E /* Build configuration list for PBXNativeTarget "UITests" */; 138 | buildPhases = ( 139 | 55139AE228128CAB00A20C2E /* Sources */, 140 | 55139AE328128CAB00A20C2E /* Frameworks */, 141 | 55139AE428128CAB00A20C2E /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | 55139AED28128CAB00A20C2E /* PBXTargetDependency */, 147 | ); 148 | name = UITests; 149 | productName = UITests; 150 | productReference = 55139AE628128CAB00A20C2E /* UITests.xctest */; 151 | productType = "com.apple.product-type.bundle.ui-testing"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 55139AAC28128B9500A20C2E /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | BuildIndependentTargetsInParallel = 1; 160 | LastSwiftUpdateCheck = 1330; 161 | LastUpgradeCheck = 1330; 162 | TargetAttributes = { 163 | 55139AB628128C7000A20C2E = { 164 | CreatedOnToolsVersion = 13.3; 165 | }; 166 | 55139AE528128CAB00A20C2E = { 167 | CreatedOnToolsVersion = 13.3; 168 | TestTargetID = 55139AB628128C7000A20C2E; 169 | }; 170 | }; 171 | }; 172 | buildConfigurationList = 55139AAF28128B9500A20C2E /* Build configuration list for PBXProject "AllTests" */; 173 | compatibilityVersion = "Xcode 13.0"; 174 | developmentRegion = en; 175 | hasScannedForEncodings = 0; 176 | knownRegions = ( 177 | en, 178 | Base, 179 | ); 180 | mainGroup = 55139AAB28128B9500A20C2E; 181 | productRefGroup = 55139AB828128C7000A20C2E /* Products */; 182 | projectDirPath = ""; 183 | projectRoot = ""; 184 | targets = ( 185 | 55139AB628128C7000A20C2E /* UITestingApp */, 186 | 55139AE528128CAB00A20C2E /* UITests */, 187 | ); 188 | }; 189 | /* End PBXProject section */ 190 | 191 | /* Begin PBXResourcesBuildPhase section */ 192 | 55139AB528128C7000A20C2E /* Resources */ = { 193 | isa = PBXResourcesBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | 55139AC228128C7100A20C2E /* Preview Assets.xcassets in Resources */, 197 | 55139ABF28128C7100A20C2E /* Assets.xcassets in Resources */, 198 | ); 199 | runOnlyForDeploymentPostprocessing = 0; 200 | }; 201 | 55139AE428128CAB00A20C2E /* Resources */ = { 202 | isa = PBXResourcesBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXResourcesBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 55139AB328128C7000A20C2E /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 55139ABD28128C7000A20C2E /* ContentView.swift in Sources */, 216 | 55139ABB28128C7000A20C2E /* UITestingApp.swift in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | 55139AE228128CAB00A20C2E /* Sources */ = { 221 | isa = PBXSourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | 55139AE928128CAB00A20C2E /* UITests.swift in Sources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXSourcesBuildPhase section */ 229 | 230 | /* Begin PBXTargetDependency section */ 231 | 55139AED28128CAB00A20C2E /* PBXTargetDependency */ = { 232 | isa = PBXTargetDependency; 233 | target = 55139AB628128C7000A20C2E /* UITestingApp */; 234 | targetProxy = 55139AEC28128CAB00A20C2E /* PBXContainerItemProxy */; 235 | }; 236 | /* End PBXTargetDependency section */ 237 | 238 | /* Begin XCBuildConfiguration section */ 239 | 55139AB028128B9500A20C2E /* Debug */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 243 | }; 244 | name = Debug; 245 | }; 246 | 55139AB128128B9500A20C2E /* Release */ = { 247 | isa = XCBuildConfiguration; 248 | buildSettings = { 249 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 250 | }; 251 | name = Release; 252 | }; 253 | 55139ADA28128C7100A20C2E /* Debug */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | ALWAYS_SEARCH_USER_PATHS = NO; 257 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 258 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 261 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 262 | CLANG_ENABLE_MODULES = YES; 263 | CLANG_ENABLE_OBJC_ARC = YES; 264 | CLANG_ENABLE_OBJC_WEAK = YES; 265 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 266 | CLANG_WARN_BOOL_CONVERSION = YES; 267 | CLANG_WARN_COMMA = YES; 268 | CLANG_WARN_CONSTANT_CONVERSION = YES; 269 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 270 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 271 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 272 | CLANG_WARN_EMPTY_BODY = YES; 273 | CLANG_WARN_ENUM_CONVERSION = YES; 274 | CLANG_WARN_INFINITE_RECURSION = YES; 275 | CLANG_WARN_INT_CONVERSION = YES; 276 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 278 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 279 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 280 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 281 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 282 | CLANG_WARN_STRICT_PROTOTYPES = YES; 283 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 284 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 285 | CLANG_WARN_UNREACHABLE_CODE = YES; 286 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 287 | CODE_SIGN_STYLE = Automatic; 288 | COPY_PHASE_STRIP = NO; 289 | CURRENT_PROJECT_VERSION = 1; 290 | DEBUG_INFORMATION_FORMAT = dwarf; 291 | DEVELOPMENT_ASSET_PATHS = "\"UITestingApp/Preview Content\""; 292 | ENABLE_PREVIEWS = YES; 293 | ENABLE_STRICT_OBJC_MSGSEND = YES; 294 | ENABLE_TESTABILITY = YES; 295 | GCC_C_LANGUAGE_STANDARD = gnu11; 296 | GCC_DYNAMIC_NO_PIC = NO; 297 | GCC_NO_COMMON_BLOCKS = YES; 298 | GCC_OPTIMIZATION_LEVEL = 0; 299 | GCC_PREPROCESSOR_DEFINITIONS = ( 300 | "DEBUG=1", 301 | "$(inherited)", 302 | ); 303 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 304 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 305 | GCC_WARN_UNDECLARED_SELECTOR = YES; 306 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 307 | GCC_WARN_UNUSED_FUNCTION = YES; 308 | GCC_WARN_UNUSED_VARIABLE = YES; 309 | GENERATE_INFOPLIST_FILE = YES; 310 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 311 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 312 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 313 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 314 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 315 | LD_RUNPATH_SEARCH_PATHS = ( 316 | "$(inherited)", 317 | "@executable_path/Frameworks", 318 | ); 319 | MARKETING_VERSION = 1.0; 320 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 321 | MTL_FAST_MATH = YES; 322 | ONLY_ACTIVE_ARCH = YES; 323 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.UITestingApp"; 324 | PRODUCT_NAME = "$(TARGET_NAME)"; 325 | SDKROOT = iphoneos; 326 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 327 | SWIFT_EMIT_LOC_STRINGS = YES; 328 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 329 | SWIFT_VERSION = 5.0; 330 | TARGETED_DEVICE_FAMILY = "1,2"; 331 | }; 332 | name = Debug; 333 | }; 334 | 55139ADB28128C7100A20C2E /* Release */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ALWAYS_SEARCH_USER_PATHS = NO; 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 340 | CLANG_ANALYZER_NONNULL = YES; 341 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 342 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 343 | CLANG_ENABLE_MODULES = YES; 344 | CLANG_ENABLE_OBJC_ARC = YES; 345 | CLANG_ENABLE_OBJC_WEAK = YES; 346 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 347 | CLANG_WARN_BOOL_CONVERSION = YES; 348 | CLANG_WARN_COMMA = YES; 349 | CLANG_WARN_CONSTANT_CONVERSION = YES; 350 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 351 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 352 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 353 | CLANG_WARN_EMPTY_BODY = YES; 354 | CLANG_WARN_ENUM_CONVERSION = YES; 355 | CLANG_WARN_INFINITE_RECURSION = YES; 356 | CLANG_WARN_INT_CONVERSION = YES; 357 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 358 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 359 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 360 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 361 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 362 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 363 | CLANG_WARN_STRICT_PROTOTYPES = YES; 364 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 365 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 366 | CLANG_WARN_UNREACHABLE_CODE = YES; 367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 368 | CODE_SIGN_STYLE = Automatic; 369 | COPY_PHASE_STRIP = NO; 370 | CURRENT_PROJECT_VERSION = 1; 371 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 372 | DEVELOPMENT_ASSET_PATHS = "\"UITestingApp/Preview Content\""; 373 | ENABLE_NS_ASSERTIONS = NO; 374 | ENABLE_PREVIEWS = YES; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu11; 377 | GCC_NO_COMMON_BLOCKS = YES; 378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNUSED_FUNCTION = YES; 383 | GCC_WARN_UNUSED_VARIABLE = YES; 384 | GENERATE_INFOPLIST_FILE = YES; 385 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 386 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 387 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 388 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 389 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 390 | LD_RUNPATH_SEARCH_PATHS = ( 391 | "$(inherited)", 392 | "@executable_path/Frameworks", 393 | ); 394 | MARKETING_VERSION = 1.0; 395 | MTL_ENABLE_DEBUG_INFO = NO; 396 | MTL_FAST_MATH = YES; 397 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.UITestingApp"; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | SDKROOT = iphoneos; 400 | SWIFT_COMPILATION_MODE = wholemodule; 401 | SWIFT_EMIT_LOC_STRINGS = YES; 402 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 403 | SWIFT_VERSION = 5.0; 404 | TARGETED_DEVICE_FAMILY = "1,2"; 405 | VALIDATE_PRODUCT = YES; 406 | }; 407 | name = Release; 408 | }; 409 | 55139AEF28128CAB00A20C2E /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ALWAYS_SEARCH_USER_PATHS = NO; 413 | CLANG_ANALYZER_NONNULL = YES; 414 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 415 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 416 | CLANG_ENABLE_MODULES = YES; 417 | CLANG_ENABLE_OBJC_ARC = YES; 418 | CLANG_ENABLE_OBJC_WEAK = YES; 419 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 420 | CLANG_WARN_BOOL_CONVERSION = YES; 421 | CLANG_WARN_COMMA = YES; 422 | CLANG_WARN_CONSTANT_CONVERSION = YES; 423 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 424 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 425 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 426 | CLANG_WARN_EMPTY_BODY = YES; 427 | CLANG_WARN_ENUM_CONVERSION = YES; 428 | CLANG_WARN_INFINITE_RECURSION = YES; 429 | CLANG_WARN_INT_CONVERSION = YES; 430 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 431 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 432 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 433 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 434 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 435 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 436 | CLANG_WARN_STRICT_PROTOTYPES = YES; 437 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 438 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 439 | CLANG_WARN_UNREACHABLE_CODE = YES; 440 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 441 | CODE_SIGN_STYLE = Automatic; 442 | COPY_PHASE_STRIP = NO; 443 | CURRENT_PROJECT_VERSION = 1; 444 | DEBUG_INFORMATION_FORMAT = dwarf; 445 | ENABLE_STRICT_OBJC_MSGSEND = YES; 446 | ENABLE_TESTABILITY = YES; 447 | GCC_C_LANGUAGE_STANDARD = gnu11; 448 | GCC_DYNAMIC_NO_PIC = NO; 449 | GCC_NO_COMMON_BLOCKS = YES; 450 | GCC_OPTIMIZATION_LEVEL = 0; 451 | GCC_PREPROCESSOR_DEFINITIONS = ( 452 | "DEBUG=1", 453 | "$(inherited)", 454 | ); 455 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 456 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 457 | GCC_WARN_UNDECLARED_SELECTOR = YES; 458 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 459 | GCC_WARN_UNUSED_FUNCTION = YES; 460 | GCC_WARN_UNUSED_VARIABLE = YES; 461 | GENERATE_INFOPLIST_FILE = YES; 462 | MARKETING_VERSION = 1.0; 463 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 464 | MTL_FAST_MATH = YES; 465 | ONLY_ACTIVE_ARCH = YES; 466 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.UITests"; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SDKROOT = iphoneos; 469 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 470 | SWIFT_EMIT_LOC_STRINGS = NO; 471 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 472 | SWIFT_VERSION = 5.0; 473 | TARGETED_DEVICE_FAMILY = "1,2"; 474 | TEST_TARGET_NAME = UITestingApp; 475 | }; 476 | name = Debug; 477 | }; 478 | 55139AF028128CAB00A20C2E /* Release */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | ALWAYS_SEARCH_USER_PATHS = NO; 482 | CLANG_ANALYZER_NONNULL = YES; 483 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 484 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 485 | CLANG_ENABLE_MODULES = YES; 486 | CLANG_ENABLE_OBJC_ARC = YES; 487 | CLANG_ENABLE_OBJC_WEAK = YES; 488 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 489 | CLANG_WARN_BOOL_CONVERSION = YES; 490 | CLANG_WARN_COMMA = YES; 491 | CLANG_WARN_CONSTANT_CONVERSION = YES; 492 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 493 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 494 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 495 | CLANG_WARN_EMPTY_BODY = YES; 496 | CLANG_WARN_ENUM_CONVERSION = YES; 497 | CLANG_WARN_INFINITE_RECURSION = YES; 498 | CLANG_WARN_INT_CONVERSION = YES; 499 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 500 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 501 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 502 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 503 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 504 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 505 | CLANG_WARN_STRICT_PROTOTYPES = YES; 506 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 507 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 508 | CLANG_WARN_UNREACHABLE_CODE = YES; 509 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 510 | CODE_SIGN_STYLE = Automatic; 511 | COPY_PHASE_STRIP = NO; 512 | CURRENT_PROJECT_VERSION = 1; 513 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 514 | ENABLE_NS_ASSERTIONS = NO; 515 | ENABLE_STRICT_OBJC_MSGSEND = YES; 516 | GCC_C_LANGUAGE_STANDARD = gnu11; 517 | GCC_NO_COMMON_BLOCKS = YES; 518 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 519 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 520 | GCC_WARN_UNDECLARED_SELECTOR = YES; 521 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 522 | GCC_WARN_UNUSED_FUNCTION = YES; 523 | GCC_WARN_UNUSED_VARIABLE = YES; 524 | GENERATE_INFOPLIST_FILE = YES; 525 | MARKETING_VERSION = 1.0; 526 | MTL_ENABLE_DEBUG_INFO = NO; 527 | MTL_FAST_MATH = YES; 528 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.UITests"; 529 | PRODUCT_NAME = "$(TARGET_NAME)"; 530 | SDKROOT = iphoneos; 531 | SWIFT_COMPILATION_MODE = wholemodule; 532 | SWIFT_EMIT_LOC_STRINGS = NO; 533 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 534 | SWIFT_VERSION = 5.0; 535 | TARGETED_DEVICE_FAMILY = "1,2"; 536 | TEST_TARGET_NAME = UITestingApp; 537 | VALIDATE_PRODUCT = YES; 538 | }; 539 | name = Release; 540 | }; 541 | /* End XCBuildConfiguration section */ 542 | 543 | /* Begin XCConfigurationList section */ 544 | 55139AAF28128B9500A20C2E /* Build configuration list for PBXProject "AllTests" */ = { 545 | isa = XCConfigurationList; 546 | buildConfigurations = ( 547 | 55139AB028128B9500A20C2E /* Debug */, 548 | 55139AB128128B9500A20C2E /* Release */, 549 | ); 550 | defaultConfigurationIsVisible = 0; 551 | defaultConfigurationName = Release; 552 | }; 553 | 55139AD928128C7100A20C2E /* Build configuration list for PBXNativeTarget "UITestingApp" */ = { 554 | isa = XCConfigurationList; 555 | buildConfigurations = ( 556 | 55139ADA28128C7100A20C2E /* Debug */, 557 | 55139ADB28128C7100A20C2E /* Release */, 558 | ); 559 | defaultConfigurationIsVisible = 0; 560 | defaultConfigurationName = Release; 561 | }; 562 | 55139AEE28128CAB00A20C2E /* Build configuration list for PBXNativeTarget "UITests" */ = { 563 | isa = XCConfigurationList; 564 | buildConfigurations = ( 565 | 55139AEF28128CAB00A20C2E /* Debug */, 566 | 55139AF028128CAB00A20C2E /* Release */, 567 | ); 568 | defaultConfigurationIsVisible = 0; 569 | defaultConfigurationName = Release; 570 | }; 571 | /* End XCConfigurationList section */ 572 | 573 | /* Begin XCSwiftPackageProductDependency section */ 574 | 55139AF72812904F00A20C2E /* UIKitTextField */ = { 575 | isa = XCSwiftPackageProductDependency; 576 | productName = UIKitTextField; 577 | }; 578 | /* End XCSwiftPackageProductDependency section */ 579 | }; 580 | rootObject = 55139AAC28128B9500A20C2E /* Project object */; 581 | } 582 | -------------------------------------------------------------------------------- /Examples/Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 559E11EA2816532E007CCB2F /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E11E92816532E007CCB2F /* ExampleApp.swift */; }; 11 | 559E11EC2816532E007CCB2F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E11EB2816532E007CCB2F /* ContentView.swift */; }; 12 | 559E11EE2816532F007CCB2F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 559E11ED2816532F007CCB2F /* Assets.xcassets */; }; 13 | 559E11F12816532F007CCB2F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 559E11F02816532F007CCB2F /* Preview Assets.xcassets */; }; 14 | 559E11FA281654D4007CCB2F /* UIKitTextField in Frameworks */ = {isa = PBXBuildFile; productRef = 559E11F9281654D4007CCB2F /* UIKitTextField */; }; 15 | 559E11FD28167A69007CCB2F /* MeasurementReader in Frameworks */ = {isa = PBXBuildFile; productRef = 559E11FC28167A69007CCB2F /* MeasurementReader */; }; 16 | 559E12002817873C007CCB2F /* Expression in Frameworks */ = {isa = PBXBuildFile; productRef = 559E11FF2817873C007CCB2F /* Expression */; }; 17 | 559E120228178E69007CCB2F /* TextBindingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E120128178E69007CCB2F /* TextBindingPage.swift */; }; 18 | 559E120528178E9F007CCB2F /* ValueBindingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E120428178E9F007CCB2F /* ValueBindingPage.swift */; }; 19 | 559E120728178EB9007CCB2F /* FocusBindingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E120628178EB9007CCB2F /* FocusBindingPage.swift */; }; 20 | 559E120928178EEA007CCB2F /* CustomTextFieldPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E120828178EEA007CCB2F /* CustomTextFieldPage.swift */; }; 21 | 559E120B28178F09007CCB2F /* StretchingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E120A28178F09007CCB2F /* StretchingPage.swift */; }; 22 | 559E120D28178F3A007CCB2F /* InputViewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E120C28178F3A007CCB2F /* InputViewPage.swift */; }; 23 | 559E120F28178FFB007CCB2F /* PaddedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E120E28178FFB007CCB2F /* PaddedTextField.swift */; }; 24 | 559E121128179449007CCB2F /* ConfigurePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E121028179449007CCB2F /* ConfigurePage.swift */; }; 25 | 559E121A2818B297007CCB2F /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559E12192818B297007CCB2F /* ExampleUITests.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 559E121D2818B297007CCB2F /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 559E11DE2816532E007CCB2F /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 559E11E52816532E007CCB2F; 34 | remoteInfo = Example; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 559E11E62816532E007CCB2F /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 559E11E92816532E007CCB2F /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; 41 | 559E11EB2816532E007CCB2F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 42 | 559E11ED2816532F007CCB2F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 559E11F02816532F007CCB2F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 44 | 559E11F728165453007CCB2F /* uikit-textfield */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "uikit-textfield"; path = ../..; sourceTree = ""; }; 45 | 559E120128178E69007CCB2F /* TextBindingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBindingPage.swift; sourceTree = ""; }; 46 | 559E120428178E9F007CCB2F /* ValueBindingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueBindingPage.swift; sourceTree = ""; }; 47 | 559E120628178EB9007CCB2F /* FocusBindingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusBindingPage.swift; sourceTree = ""; }; 48 | 559E120828178EEA007CCB2F /* CustomTextFieldPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextFieldPage.swift; sourceTree = ""; }; 49 | 559E120A28178F09007CCB2F /* StretchingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StretchingPage.swift; sourceTree = ""; }; 50 | 559E120C28178F3A007CCB2F /* InputViewPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewPage.swift; sourceTree = ""; }; 51 | 559E120E28178FFB007CCB2F /* PaddedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddedTextField.swift; sourceTree = ""; }; 52 | 559E121028179449007CCB2F /* ConfigurePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurePage.swift; sourceTree = ""; }; 53 | 559E12172818B297007CCB2F /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 559E12192818B297007CCB2F /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; 55 | /* End PBXFileReference section */ 56 | 57 | /* Begin PBXFrameworksBuildPhase section */ 58 | 559E11E32816532E007CCB2F /* Frameworks */ = { 59 | isa = PBXFrameworksBuildPhase; 60 | buildActionMask = 2147483647; 61 | files = ( 62 | 559E11FD28167A69007CCB2F /* MeasurementReader in Frameworks */, 63 | 559E12002817873C007CCB2F /* Expression in Frameworks */, 64 | 559E11FA281654D4007CCB2F /* UIKitTextField in Frameworks */, 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | 559E12142818B297007CCB2F /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 559E11DD2816532E007CCB2F = { 79 | isa = PBXGroup; 80 | children = ( 81 | 559E11F728165453007CCB2F /* uikit-textfield */, 82 | 559E11E82816532E007CCB2F /* Example */, 83 | 559E12182818B297007CCB2F /* ExampleUITests */, 84 | 559E11E72816532E007CCB2F /* Products */, 85 | 559E11F8281654D4007CCB2F /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | 559E11E72816532E007CCB2F /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 559E11E62816532E007CCB2F /* Example.app */, 93 | 559E12172818B297007CCB2F /* ExampleUITests.xctest */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 559E11E82816532E007CCB2F /* Example */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 559E120328178E90007CCB2F /* Pages */, 102 | 559E11E92816532E007CCB2F /* ExampleApp.swift */, 103 | 559E11EB2816532E007CCB2F /* ContentView.swift */, 104 | 559E11ED2816532F007CCB2F /* Assets.xcassets */, 105 | 559E11EF2816532F007CCB2F /* Preview Content */, 106 | ); 107 | path = Example; 108 | sourceTree = ""; 109 | }; 110 | 559E11EF2816532F007CCB2F /* Preview Content */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 559E11F02816532F007CCB2F /* Preview Assets.xcassets */, 114 | ); 115 | path = "Preview Content"; 116 | sourceTree = ""; 117 | }; 118 | 559E11F8281654D4007CCB2F /* Frameworks */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | ); 122 | name = Frameworks; 123 | sourceTree = ""; 124 | }; 125 | 559E120328178E90007CCB2F /* Pages */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 559E1212281795D7007CCB2F /* Views */, 129 | 559E120128178E69007CCB2F /* TextBindingPage.swift */, 130 | 559E120428178E9F007CCB2F /* ValueBindingPage.swift */, 131 | 559E120628178EB9007CCB2F /* FocusBindingPage.swift */, 132 | 559E120828178EEA007CCB2F /* CustomTextFieldPage.swift */, 133 | 559E120A28178F09007CCB2F /* StretchingPage.swift */, 134 | 559E120C28178F3A007CCB2F /* InputViewPage.swift */, 135 | 559E121028179449007CCB2F /* ConfigurePage.swift */, 136 | ); 137 | path = Pages; 138 | sourceTree = ""; 139 | }; 140 | 559E1212281795D7007CCB2F /* Views */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 559E120E28178FFB007CCB2F /* PaddedTextField.swift */, 144 | ); 145 | path = Views; 146 | sourceTree = ""; 147 | }; 148 | 559E12182818B297007CCB2F /* ExampleUITests */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 559E12192818B297007CCB2F /* ExampleUITests.swift */, 152 | ); 153 | path = ExampleUITests; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXNativeTarget section */ 159 | 559E11E52816532E007CCB2F /* Example */ = { 160 | isa = PBXNativeTarget; 161 | buildConfigurationList = 559E11F42816532F007CCB2F /* Build configuration list for PBXNativeTarget "Example" */; 162 | buildPhases = ( 163 | 559E11E22816532E007CCB2F /* Sources */, 164 | 559E11E32816532E007CCB2F /* Frameworks */, 165 | 559E11E42816532E007CCB2F /* Resources */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | ); 171 | name = Example; 172 | packageProductDependencies = ( 173 | 559E11F9281654D4007CCB2F /* UIKitTextField */, 174 | 559E11FC28167A69007CCB2F /* MeasurementReader */, 175 | 559E11FF2817873C007CCB2F /* Expression */, 176 | ); 177 | productName = Example; 178 | productReference = 559E11E62816532E007CCB2F /* Example.app */; 179 | productType = "com.apple.product-type.application"; 180 | }; 181 | 559E12162818B297007CCB2F /* ExampleUITests */ = { 182 | isa = PBXNativeTarget; 183 | buildConfigurationList = 559E12212818B297007CCB2F /* Build configuration list for PBXNativeTarget "ExampleUITests" */; 184 | buildPhases = ( 185 | 559E12132818B297007CCB2F /* Sources */, 186 | 559E12142818B297007CCB2F /* Frameworks */, 187 | 559E12152818B297007CCB2F /* Resources */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | 559E121E2818B297007CCB2F /* PBXTargetDependency */, 193 | ); 194 | name = ExampleUITests; 195 | productName = ExampleUITests; 196 | productReference = 559E12172818B297007CCB2F /* ExampleUITests.xctest */; 197 | productType = "com.apple.product-type.bundle.ui-testing"; 198 | }; 199 | /* End PBXNativeTarget section */ 200 | 201 | /* Begin PBXProject section */ 202 | 559E11DE2816532E007CCB2F /* Project object */ = { 203 | isa = PBXProject; 204 | attributes = { 205 | BuildIndependentTargetsInParallel = 1; 206 | LastSwiftUpdateCheck = 1330; 207 | LastUpgradeCheck = 1330; 208 | TargetAttributes = { 209 | 559E11E52816532E007CCB2F = { 210 | CreatedOnToolsVersion = 13.3.1; 211 | }; 212 | 559E12162818B297007CCB2F = { 213 | CreatedOnToolsVersion = 13.3.1; 214 | TestTargetID = 559E11E52816532E007CCB2F; 215 | }; 216 | }; 217 | }; 218 | buildConfigurationList = 559E11E12816532E007CCB2F /* Build configuration list for PBXProject "Example" */; 219 | compatibilityVersion = "Xcode 13.0"; 220 | developmentRegion = en; 221 | hasScannedForEncodings = 0; 222 | knownRegions = ( 223 | en, 224 | Base, 225 | ); 226 | mainGroup = 559E11DD2816532E007CCB2F; 227 | packageReferences = ( 228 | 559E11FB28167A69007CCB2F /* XCRemoteSwiftPackageReference "measurement-reader" */, 229 | 559E11FE2817873C007CCB2F /* XCRemoteSwiftPackageReference "Expression" */, 230 | ); 231 | productRefGroup = 559E11E72816532E007CCB2F /* Products */; 232 | projectDirPath = ""; 233 | projectRoot = ""; 234 | targets = ( 235 | 559E11E52816532E007CCB2F /* Example */, 236 | 559E12162818B297007CCB2F /* ExampleUITests */, 237 | ); 238 | }; 239 | /* End PBXProject section */ 240 | 241 | /* Begin PBXResourcesBuildPhase section */ 242 | 559E11E42816532E007CCB2F /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | 559E11F12816532F007CCB2F /* Preview Assets.xcassets in Resources */, 247 | 559E11EE2816532F007CCB2F /* Assets.xcassets in Resources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | 559E12152818B297007CCB2F /* Resources */ = { 252 | isa = PBXResourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXResourcesBuildPhase section */ 259 | 260 | /* Begin PBXSourcesBuildPhase section */ 261 | 559E11E22816532E007CCB2F /* Sources */ = { 262 | isa = PBXSourcesBuildPhase; 263 | buildActionMask = 2147483647; 264 | files = ( 265 | 559E120228178E69007CCB2F /* TextBindingPage.swift in Sources */, 266 | 559E120F28178FFB007CCB2F /* PaddedTextField.swift in Sources */, 267 | 559E120928178EEA007CCB2F /* CustomTextFieldPage.swift in Sources */, 268 | 559E121128179449007CCB2F /* ConfigurePage.swift in Sources */, 269 | 559E120D28178F3A007CCB2F /* InputViewPage.swift in Sources */, 270 | 559E11EC2816532E007CCB2F /* ContentView.swift in Sources */, 271 | 559E120528178E9F007CCB2F /* ValueBindingPage.swift in Sources */, 272 | 559E11EA2816532E007CCB2F /* ExampleApp.swift in Sources */, 273 | 559E120B28178F09007CCB2F /* StretchingPage.swift in Sources */, 274 | 559E120728178EB9007CCB2F /* FocusBindingPage.swift in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | 559E12132818B297007CCB2F /* Sources */ = { 279 | isa = PBXSourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | 559E121A2818B297007CCB2F /* ExampleUITests.swift in Sources */, 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | /* End PBXSourcesBuildPhase section */ 287 | 288 | /* Begin PBXTargetDependency section */ 289 | 559E121E2818B297007CCB2F /* PBXTargetDependency */ = { 290 | isa = PBXTargetDependency; 291 | target = 559E11E52816532E007CCB2F /* Example */; 292 | targetProxy = 559E121D2818B297007CCB2F /* PBXContainerItemProxy */; 293 | }; 294 | /* End PBXTargetDependency section */ 295 | 296 | /* Begin XCBuildConfiguration section */ 297 | 559E11F22816532F007CCB2F /* Debug */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ALWAYS_SEARCH_USER_PATHS = NO; 301 | CLANG_ANALYZER_NONNULL = YES; 302 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 304 | CLANG_ENABLE_MODULES = YES; 305 | CLANG_ENABLE_OBJC_ARC = YES; 306 | CLANG_ENABLE_OBJC_WEAK = YES; 307 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 308 | CLANG_WARN_BOOL_CONVERSION = YES; 309 | CLANG_WARN_COMMA = YES; 310 | CLANG_WARN_CONSTANT_CONVERSION = YES; 311 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 312 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 313 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 314 | CLANG_WARN_EMPTY_BODY = YES; 315 | CLANG_WARN_ENUM_CONVERSION = YES; 316 | CLANG_WARN_INFINITE_RECURSION = YES; 317 | CLANG_WARN_INT_CONVERSION = YES; 318 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 319 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 320 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 321 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 322 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 323 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 324 | CLANG_WARN_STRICT_PROTOTYPES = YES; 325 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 326 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 327 | CLANG_WARN_UNREACHABLE_CODE = YES; 328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 329 | COPY_PHASE_STRIP = NO; 330 | DEBUG_INFORMATION_FORMAT = dwarf; 331 | ENABLE_STRICT_OBJC_MSGSEND = YES; 332 | ENABLE_TESTABILITY = YES; 333 | GCC_C_LANGUAGE_STANDARD = gnu11; 334 | GCC_DYNAMIC_NO_PIC = NO; 335 | GCC_NO_COMMON_BLOCKS = YES; 336 | GCC_OPTIMIZATION_LEVEL = 0; 337 | GCC_PREPROCESSOR_DEFINITIONS = ( 338 | "DEBUG=1", 339 | "$(inherited)", 340 | ); 341 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 342 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 343 | GCC_WARN_UNDECLARED_SELECTOR = YES; 344 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 345 | GCC_WARN_UNUSED_FUNCTION = YES; 346 | GCC_WARN_UNUSED_VARIABLE = YES; 347 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 348 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 349 | MTL_FAST_MATH = YES; 350 | ONLY_ACTIVE_ARCH = YES; 351 | SDKROOT = iphoneos; 352 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 353 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 354 | }; 355 | name = Debug; 356 | }; 357 | 559E11F32816532F007CCB2F /* Release */ = { 358 | isa = XCBuildConfiguration; 359 | buildSettings = { 360 | ALWAYS_SEARCH_USER_PATHS = NO; 361 | CLANG_ANALYZER_NONNULL = YES; 362 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 363 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_ENABLE_OBJC_WEAK = YES; 367 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 368 | CLANG_WARN_BOOL_CONVERSION = YES; 369 | CLANG_WARN_COMMA = YES; 370 | CLANG_WARN_CONSTANT_CONVERSION = YES; 371 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 372 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 373 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 374 | CLANG_WARN_EMPTY_BODY = YES; 375 | CLANG_WARN_ENUM_CONVERSION = YES; 376 | CLANG_WARN_INFINITE_RECURSION = YES; 377 | CLANG_WARN_INT_CONVERSION = YES; 378 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 380 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 381 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 382 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 383 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 384 | CLANG_WARN_STRICT_PROTOTYPES = YES; 385 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 386 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 387 | CLANG_WARN_UNREACHABLE_CODE = YES; 388 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 389 | COPY_PHASE_STRIP = NO; 390 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 391 | ENABLE_NS_ASSERTIONS = NO; 392 | ENABLE_STRICT_OBJC_MSGSEND = YES; 393 | GCC_C_LANGUAGE_STANDARD = gnu11; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 396 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 397 | GCC_WARN_UNDECLARED_SELECTOR = YES; 398 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 399 | GCC_WARN_UNUSED_FUNCTION = YES; 400 | GCC_WARN_UNUSED_VARIABLE = YES; 401 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 402 | MTL_ENABLE_DEBUG_INFO = NO; 403 | MTL_FAST_MATH = YES; 404 | SDKROOT = iphoneos; 405 | SWIFT_COMPILATION_MODE = wholemodule; 406 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 407 | VALIDATE_PRODUCT = YES; 408 | }; 409 | name = Release; 410 | }; 411 | 559E11F52816532F007CCB2F /* Debug */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 415 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 416 | CODE_SIGN_STYLE = Automatic; 417 | CURRENT_PROJECT_VERSION = 1; 418 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 419 | ENABLE_PREVIEWS = YES; 420 | GENERATE_INFOPLIST_FILE = YES; 421 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 422 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 423 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 424 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 425 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 426 | LD_RUNPATH_SEARCH_PATHS = ( 427 | "$(inherited)", 428 | "@executable_path/Frameworks", 429 | ); 430 | MARKETING_VERSION = 1.0; 431 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.Example"; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | SWIFT_EMIT_LOC_STRINGS = YES; 434 | SWIFT_VERSION = 5.0; 435 | TARGETED_DEVICE_FAMILY = "1,2"; 436 | }; 437 | name = Debug; 438 | }; 439 | 559E11F62816532F007CCB2F /* Release */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 444 | CODE_SIGN_STYLE = Automatic; 445 | CURRENT_PROJECT_VERSION = 1; 446 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; 447 | ENABLE_PREVIEWS = YES; 448 | GENERATE_INFOPLIST_FILE = YES; 449 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 450 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 451 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 452 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 453 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 454 | LD_RUNPATH_SEARCH_PATHS = ( 455 | "$(inherited)", 456 | "@executable_path/Frameworks", 457 | ); 458 | MARKETING_VERSION = 1.0; 459 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.Example"; 460 | PRODUCT_NAME = "$(TARGET_NAME)"; 461 | SWIFT_EMIT_LOC_STRINGS = YES; 462 | SWIFT_VERSION = 5.0; 463 | TARGETED_DEVICE_FAMILY = "1,2"; 464 | }; 465 | name = Release; 466 | }; 467 | 559E121F2818B297007CCB2F /* Debug */ = { 468 | isa = XCBuildConfiguration; 469 | buildSettings = { 470 | CODE_SIGN_STYLE = Automatic; 471 | CURRENT_PROJECT_VERSION = 1; 472 | GENERATE_INFOPLIST_FILE = YES; 473 | MARKETING_VERSION = 1.0; 474 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.ExampleUITests"; 475 | PRODUCT_NAME = "$(TARGET_NAME)"; 476 | SWIFT_EMIT_LOC_STRINGS = NO; 477 | SWIFT_VERSION = 5.0; 478 | TARGETED_DEVICE_FAMILY = "1,2"; 479 | TEST_TARGET_NAME = Example; 480 | }; 481 | name = Debug; 482 | }; 483 | 559E12202818B297007CCB2F /* Release */ = { 484 | isa = XCBuildConfiguration; 485 | buildSettings = { 486 | CODE_SIGN_STYLE = Automatic; 487 | CURRENT_PROJECT_VERSION = 1; 488 | GENERATE_INFOPLIST_FILE = YES; 489 | MARKETING_VERSION = 1.0; 490 | PRODUCT_BUNDLE_IDENTIFIER = "com.uikit-textfield.ExampleUITests"; 491 | PRODUCT_NAME = "$(TARGET_NAME)"; 492 | SWIFT_EMIT_LOC_STRINGS = NO; 493 | SWIFT_VERSION = 5.0; 494 | TARGETED_DEVICE_FAMILY = "1,2"; 495 | TEST_TARGET_NAME = Example; 496 | }; 497 | name = Release; 498 | }; 499 | /* End XCBuildConfiguration section */ 500 | 501 | /* Begin XCConfigurationList section */ 502 | 559E11E12816532E007CCB2F /* Build configuration list for PBXProject "Example" */ = { 503 | isa = XCConfigurationList; 504 | buildConfigurations = ( 505 | 559E11F22816532F007CCB2F /* Debug */, 506 | 559E11F32816532F007CCB2F /* Release */, 507 | ); 508 | defaultConfigurationIsVisible = 0; 509 | defaultConfigurationName = Release; 510 | }; 511 | 559E11F42816532F007CCB2F /* Build configuration list for PBXNativeTarget "Example" */ = { 512 | isa = XCConfigurationList; 513 | buildConfigurations = ( 514 | 559E11F52816532F007CCB2F /* Debug */, 515 | 559E11F62816532F007CCB2F /* Release */, 516 | ); 517 | defaultConfigurationIsVisible = 0; 518 | defaultConfigurationName = Release; 519 | }; 520 | 559E12212818B297007CCB2F /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { 521 | isa = XCConfigurationList; 522 | buildConfigurations = ( 523 | 559E121F2818B297007CCB2F /* Debug */, 524 | 559E12202818B297007CCB2F /* Release */, 525 | ); 526 | defaultConfigurationIsVisible = 0; 527 | defaultConfigurationName = Release; 528 | }; 529 | /* End XCConfigurationList section */ 530 | 531 | /* Begin XCRemoteSwiftPackageReference section */ 532 | 559E11FB28167A69007CCB2F /* XCRemoteSwiftPackageReference "measurement-reader" */ = { 533 | isa = XCRemoteSwiftPackageReference; 534 | repositoryURL = "https://github.com/vinceplusplus/measurement-reader.git"; 535 | requirement = { 536 | kind = upToNextMajorVersion; 537 | minimumVersion = 2.1.1; 538 | }; 539 | }; 540 | 559E11FE2817873C007CCB2F /* XCRemoteSwiftPackageReference "Expression" */ = { 541 | isa = XCRemoteSwiftPackageReference; 542 | repositoryURL = "https://github.com/nicklockwood/Expression.git"; 543 | requirement = { 544 | kind = upToNextMajorVersion; 545 | minimumVersion = 0.13.6; 546 | }; 547 | }; 548 | /* End XCRemoteSwiftPackageReference section */ 549 | 550 | /* Begin XCSwiftPackageProductDependency section */ 551 | 559E11F9281654D4007CCB2F /* UIKitTextField */ = { 552 | isa = XCSwiftPackageProductDependency; 553 | productName = UIKitTextField; 554 | }; 555 | 559E11FC28167A69007CCB2F /* MeasurementReader */ = { 556 | isa = XCSwiftPackageProductDependency; 557 | package = 559E11FB28167A69007CCB2F /* XCRemoteSwiftPackageReference "measurement-reader" */; 558 | productName = MeasurementReader; 559 | }; 560 | 559E11FF2817873C007CCB2F /* Expression */ = { 561 | isa = XCSwiftPackageProductDependency; 562 | package = 559E11FE2817873C007CCB2F /* XCRemoteSwiftPackageReference "Expression" */; 563 | productName = Expression; 564 | }; 565 | /* End XCSwiftPackageProductDependency section */ 566 | }; 567 | rootObject = 559E11DE2816532E007CCB2F /* Project object */; 568 | } 569 | --------------------------------------------------------------------------------