From b951ea4ff7edcc5fd9b0968f162bf4a19b813a73 Mon Sep 17 00:00:00 2001 From: Rico Crescenzio Date: Wed, 13 Apr 2022 20:12:47 +0200 Subject: [PATCH] Multiple adjustments and fix - Fixed styling modifiers to work only with attributes - Added textColor modifier - Improved documentation - Added initializers with Foundation.Formatter support --- README.md | 7 +- .../SUITextFieldExample/ContentView.swift | 67 +++++--- .../ResponderNavigatorView.swift | 1 + .../EnvironmentModifiers.md | 61 ++++++- .../SwiftUITextField.docc/SwiftUITextField.md | 8 +- .../DefaultAttributesMergePolicy.swift | 20 +++ .../{ => TextField}/Environments.swift | 137 +++++++++++----- .../TextField/{ => Internal}/TextType.swift | 0 .../{ => Internal}/_SUITextField.swift | 0 .../TextField/SUITextField.swift | 154 ++++++++++++++++-- 10 files changed, 364 insertions(+), 91 deletions(-) create mode 100644 Sources/SwiftUITextField/TextField/DefaultAttributesMergePolicy.swift rename Sources/SwiftUITextField/{ => TextField}/Environments.swift (74%) rename Sources/SwiftUITextField/TextField/{ => Internal}/TextType.swift (100%) rename Sources/SwiftUITextField/TextField/{ => Internal}/_SUITextField.swift (100%) diff --git a/README.md b/README.md index 5577b7c..fb7c85e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ * A default `ResponderNavigatorView` usable as an `InputAccessoryView` to navigate through text fields * Attributed placeholder * `ParseableFormatStyle` when using `iOS 15` +* `Foundation.Formatter` when using `pre-iOS 15` * DocC documented! ## Installation @@ -125,10 +126,8 @@ Use Apple `DocC` generated documentation, from Xcode, `Product > Build Documenta ## Known Issues - When an external keyboard is connected and the software keyboard is hidden, -on iOS 15 there is small layout jump when switching from a text field with custom input view and one -with normal software keyboard -- On iOS 14 this behavior is worse: sometimes the system tries to re-layout the component infinitely, leading to a -stack overflow and crash! Need to understand what actually happens under the hood... +on iOS 13/15 there are small layout jumps when switching from a text fields +- On iOS 14 this behavior is worse: the system tries to re-layout the component infinitely! Need to understand what actually happens under the hood... ## Found a bug or want new feature? diff --git a/SUITextFieldExample/SUITextFieldExample/ContentView.swift b/SUITextFieldExample/SUITextFieldExample/ContentView.swift index 1025ec0..a8dd813 100644 --- a/SUITextFieldExample/SUITextFieldExample/ContentView.swift +++ b/SUITextFieldExample/SUITextFieldExample/ContentView.swift @@ -15,6 +15,7 @@ struct ContentView: View { case second case third case fourth + case fifth } @State private var toggleFont = false @@ -27,13 +28,30 @@ struct ContentView: View { let attributes: [NSAttributedString.Key: Any] = [ .kern: 5, - .foregroundColor: UIColor.systemOrange + .foregroundColor: UIColor.systemOrange, + .font: UIFont.systemFont(ofSize: 20, weight: .black), + .paragraphStyle: { + let p = NSMutableParagraphStyle() + p.alignment = .right + return p + }() + ] + + let moreKern: [NSAttributedString.Key: Any] = [ + .kern: 15, ] let placeholderAttributes: [NSAttributedString.Key: Any] = [ .kern: 5, .foregroundColor: UIColor.systemGray ] + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .full + formatter.timeStyle = .medium + return formatter + }() var body: some View { NavigationView { @@ -59,6 +77,7 @@ struct ContentView: View { } .responder($focus, equals: .first) .uiTextFieldTextLeftViewMode(.whileEditing) + .uiTextFieldDefaultTextAttributes(moreKern, mergePolicy: .keepNew) SUITextField(text: $text) .inputAccessoryView { navigator @@ -68,37 +87,41 @@ struct ContentView: View { SUITextField( value: $myDouble, format: .number, - placeholder: AttributedString("Hey there").mergingAttributes(AttributeContainer(placeholderAttributes)) + placeholder: AttributedString("Hey there") + .mergingAttributes(AttributeContainer(placeholderAttributes)) ) .inputAccessoryView { navigator } .responder($focus, equals: .third) - SUITextField(value: $date, format: .dateTime) - .inputAccessoryView { - navigator - } - .inputView { - datePicker - } - .responder($focus, equals: .fourth) - } else { - SUITextField(text: .constant(date.description)) - .inputAccessoryView { - navigator - } - .inputView { - datePicker - } - .responder($focus, equals: .third) + .uiTextFieldTextColor(.systemGreen) } + SUITextField(value: $date, formatter: formatter) + .inputAccessoryView { + navigator + } + .inputView { + datePicker + } + .responder($focus, equals: .fourth) + .uiTextFieldFont(.systemFont(ofSize: 14, weight: .light)) } + .uiTextFieldDefaultTextAttributes(attributes) .padding() + SUITextField(value: $date, formatter: formatter) + .inputAccessoryView { + navigator + } + .inputView { + datePicker + } + .responder($focus, equals: .fifth) + .uiTextFieldDefaultTextAttributes(attributes, mergePolicy: .keepOld) + .uiTextFieldTextColor(.systemRed) + .uiTextFieldFont(.italicSystemFont(ofSize: 12)) } - .uiTextFieldDefaultTextAttributes(attributes) - .uiTextFieldFont(toggleFont ? .monospacedSystemFont(ofSize: 50, weight: .medium) : nil) + .uiTextFieldFont(.systemFont(ofSize: 14, weight: .light)) .uiTextFieldBorderStyle(.roundedRect) - .uiTextFieldAdjustsFontSizeToFitWidth(.enabled(minSize: 8)) .navigationBarTitle("SUITextField") } } diff --git a/Sources/SwiftUITextField/Responder Navigator/ResponderNavigatorView.swift b/Sources/SwiftUITextField/Responder Navigator/ResponderNavigatorView.swift index 333781c..b511a92 100644 --- a/Sources/SwiftUITextField/Responder Navigator/ResponderNavigatorView.swift +++ b/Sources/SwiftUITextField/Responder Navigator/ResponderNavigatorView.swift @@ -93,6 +93,7 @@ where Responder: Hashable, BackButton: View, NextButton: View, CenterView: View, } .padding(.horizontal, 16) .padding(.vertical, 10) + .frame(minHeight: 44) } private var currentIndex: Int? { diff --git a/Sources/SwiftUITextField/SwiftUITextField.docc/EnvironmentModifiers.md b/Sources/SwiftUITextField/SwiftUITextField.docc/EnvironmentModifiers.md index cadb280..f5741cf 100644 --- a/Sources/SwiftUITextField/SwiftUITextField.docc/EnvironmentModifiers.md +++ b/Sources/SwiftUITextField/SwiftUITextField.docc/EnvironmentModifiers.md @@ -63,6 +63,56 @@ VStack { .uiTextFieldFont(.systemFont(ofSize: 16, weight: .light)) ``` +## Dive deep into Default Text Attributes + +Using ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)`` you can specify how to apply default attributes +to every text field. +Since this modifier works using environment, another modifier at higher view hierarchy might +have been applied. + +Following example apply `kern`, `foregroundColor` and `font` to text fields which bind `password` and `repeatPassoword`; +the text field which bind `username` apply **only** the new kern and get rid of previously set attributes: + +```swift +let attributes: [NSAttributedString.Key: Any] = [ + .kern: 5, + .foregroundColor: UIColor.systemOrange, + .font: UIFont.systemFont(ofSize: 20, weight: .black), +] + +let moreKern: [NSAttributedString.Key: Any] = [ + .kern: 15, +] + +var body: some View { + VStack { + SUITextField(text: $username) + .uiTextFieldDefaultTextAttributes(kern) + SUITextField(text: $password) + SUITextField(text: $repeatPassword) + } + .uiTextFieldDefaultTextAttributes(attributes) +} +``` + +If you want to override `kern` and keep other old attributes, you will use ``DefaultAttributesMergePolicy`` like: + +```swift +SUITextField(text: $username) + .uiTextFieldDefaultTextAttributes(kern, mergePolicy: .keepNew) +``` + +Finally, there are cases where you might merge new attributes but keep old ones when same key is found, +you can use `.keepOld`. Following example apply a `font`, a `textColor` and then the default attributes +applies **only** the `kern` becuase policy is `.keepOld` + +```swift +SUITextField(text: $text) + .uiTextFieldDefaultTextAttributes(attributes, mergePolicy: .keepOld) + .uiTextFieldTextColor(.systemRed) + .uiTextFieldFont(.italicSystemFont(ofSize: 12)) +``` + ## Get the environment value Each modifier apply an environment, so you can get this value using `@Environment` property wrapper. @@ -79,9 +129,16 @@ struct MyView: View { ## Topics -### Modifiers +### Modify appearence - ``SUITextField/uiTextFieldFont(_:)`` +- ``SUITextField/uiTextFieldTextColor(_:)`` +- ``SUITextField/uiTextFieldTextAlignment(_:)`` +- ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)`` +- ``DefaultAttributesMergePolicy`` + +### Modify behavior + - ``SUITextField/uiTextFieldReturnKeyType(_:)`` - ``SUITextField/uiTextFieldSecureTextEntry(_:)`` - ``SUITextField/uiTextFieldClearButtonMode(_:)`` @@ -90,10 +147,8 @@ struct MyView: View { - ``SUITextField/uiTextFieldAutocorrectionType(_:)`` - ``SUITextField/uiTextFieldKeyboardType(_:)`` - ``SUITextField/uiTextFieldTextContentType(_:)`` -- ``SUITextField/uiTextFieldTextAlignment(_:)`` - ``SUITextField/uiTextFieldTextLeftViewMode(_:)`` - ``SUITextField/uiTextFieldTextRightViewMode(_:)`` -- ``SUITextField/uiTextFieldDefaultTextAttributes(_:)`` - ``SUITextField/uiTextFieldSpellCheckingType(_:)`` - ``SUITextField/uiTextFieldPasswordRules(_:)`` - ``SUITextField/uiTextFieldAdjustsFontSizeToFitWidth(_:)`` diff --git a/Sources/SwiftUITextField/SwiftUITextField.docc/SwiftUITextField.md b/Sources/SwiftUITextField/SwiftUITextField.docc/SwiftUITextField.md index 5c783c3..d337de3 100644 --- a/Sources/SwiftUITextField/SwiftUITextField.docc/SwiftUITextField.md +++ b/Sources/SwiftUITextField/SwiftUITextField.docc/SwiftUITextField.md @@ -9,7 +9,10 @@ A `SwiftUI` wrapper of `UITextField` that allows more customization and programm ``SUITextField`` is a wrapper of `UITextField` that allows to navigate programmatically through responders, allows to set a custom `inputView`, `inputAccessoryView` while editing, as well as `leftView` and `rightView`. -You can also use all the `UITextFieldDelegate` methods, all exposed as `SwiftUI` modifiers. +You can also use all the `UITextFieldDelegate` methods, all exposed as `SwiftUI` modifiers. 😎 + +On `iOS 15`, you can use the new `ParseableFormatStyle` to bind the text field to a custom value; on `pre-iOS 15` +a similar API is exposed using `Foundation.Formatter` 🥳 All these additional customization are passed as `SwiftUI` views/modifiers, allowing to use its lovely declarative API! 🎉 @@ -65,6 +68,9 @@ struct ContentView: View { // more code... ``` +You can create a text field with a combination of text, plain placeholder, attributed placeholder, custom value and +formatters. Check all the provided initializers in ``SUITextField``! + ## Topics ### Essential views diff --git a/Sources/SwiftUITextField/TextField/DefaultAttributesMergePolicy.swift b/Sources/SwiftUITextField/TextField/DefaultAttributesMergePolicy.swift new file mode 100644 index 0000000..ffd27b0 --- /dev/null +++ b/Sources/SwiftUITextField/TextField/DefaultAttributesMergePolicy.swift @@ -0,0 +1,20 @@ +// +// DefaultAttributesMergePolicy.swift +// +// +// Created by Rico Crescenzio on 13/04/22. +// + +/// Used in ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)`` +/// to tell the modifier how to apply new attributes. +public enum DefaultAttributesMergePolicy { + + /// When two dict of attributes contains same key, old value is kept. + case keepOld + + /// When two dict of attributes contains same key, new value is kept. + case keepNew + + /// The old dict of attributes is completely removed in place of the new one. + case rewriteAll +} diff --git a/Sources/SwiftUITextField/Environments.swift b/Sources/SwiftUITextField/TextField/Environments.swift similarity index 74% rename from Sources/SwiftUITextField/Environments.swift rename to Sources/SwiftUITextField/TextField/Environments.swift index 178af8a..4b204c8 100644 --- a/Sources/SwiftUITextField/Environments.swift +++ b/Sources/SwiftUITextField/TextField/Environments.swift @@ -15,10 +15,6 @@ private struct ResponderStorageKey: EnvironmentKey { static let defaultValue: ResponderStorage? = nil } -private struct UIFontEnvironmentKey: EnvironmentKey { - static let defaultValue: UIFont? = nil -} - private struct UIReturnKeyTypeEnvironmentKey: EnvironmentKey { static let defaultValue: UIReturnKeyType = .default } @@ -51,10 +47,6 @@ private struct UITextFieldTextContentTypeEnvironmentKey: EnvironmentKey { static let defaultValue: UITextContentType? = nil } -private struct UITextFieldTextAlignmentEnvironmentKey: EnvironmentKey { - static let defaultValue: NSTextAlignment = .left -} - private struct UITextFieldLeftViewModeEnvironmentKey: EnvironmentKey { static let defaultValue: UITextField.ViewMode = .never } @@ -97,8 +89,32 @@ public extension EnvironmentValues { /// The `UIFont` of ``SUITextField``, applied using ``SUITextField/uiTextFieldFont(_:)``. var uiTextFieldFont: UIFont? { - get { self[UIFontEnvironmentKey.self] } - set { self[UIFontEnvironmentKey.self] = newValue?.copy() as? UIFont } + get { uiTextFieldDefaultTextAttributes?[.font] as? UIFont } + set { + if var attributes = uiTextFieldDefaultTextAttributes { + attributes[.font] = newValue?.copy() + uiTextFieldDefaultTextAttributes = attributes + } else if let newValue = newValue { + uiTextFieldDefaultTextAttributes = [.font: newValue.copy()] + } else { + uiTextFieldDefaultTextAttributes = nil + } + } + } + + /// The `UIColor` of ``SUITextField``, applied using ``SUITextField/uiTextFieldFont(_:)``. + var uiTextFieldTextColor: UIColor? { + get { uiTextFieldDefaultTextAttributes?[.foregroundColor] as? UIColor } + set { + if var attributes = uiTextFieldDefaultTextAttributes { + attributes[.foregroundColor] = newValue?.copy() + uiTextFieldDefaultTextAttributes = attributes + } else if let newValue = newValue { + uiTextFieldDefaultTextAttributes = [.foregroundColor: newValue.copy()] + } else { + uiTextFieldDefaultTextAttributes = nil + } + } } /// The `UIReturnKeyType` of ``SUITextField``, applied using ``SUITextField/uiTextFieldReturnKeyType(_:)``. @@ -157,9 +173,23 @@ public extension EnvironmentValues { /// The `NSTextAlignment` of the ``SUITextField``, /// applied using ``SUITextField/uiTextFieldTextAlignment(_:)``. - var uiTextFieldTextAlignment: NSTextAlignment { - get { self[UITextFieldTextAlignmentEnvironmentKey.self] } - set { self[UITextFieldTextAlignmentEnvironmentKey.self] = newValue } + var uiTextFieldTextAlignment: NSTextAlignment? { + get { (uiTextFieldDefaultTextAttributes?[.paragraphStyle] as? NSParagraphStyle)?.alignment } + set { + if var attributes = uiTextFieldDefaultTextAttributes { + let paragraphStyle = (attributes[.paragraphStyle] as? NSParagraphStyle)? + .mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle() + paragraphStyle.alignment = newValue ?? .left + attributes[.paragraphStyle] = paragraphStyle + uiTextFieldDefaultTextAttributes = attributes + } else if let newValue = newValue { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = newValue + uiTextFieldDefaultTextAttributes = [.paragraphStyle: paragraphStyle] + } else { + uiTextFieldDefaultTextAttributes = nil + } + } } /// The `UITextField.ViewMode` of the ``SUITextField/leftView(view:)``, @@ -177,7 +207,7 @@ public extension EnvironmentValues { } /// The attributes dictionary of the ``SUITextField`` that styles active text, - /// applied using ``SUITextField/uiTextFieldDefaultTextAttributes(_:)``. + /// applied using ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)``. var uiTextFieldDefaultTextAttributes: [NSAttributedString.Key: Any]? { get { self[UITextFieldDefaultTextAttributesEnvironmentKey.self] } set { self[UITextFieldDefaultTextAttributesEnvironmentKey.self] = newValue } @@ -210,39 +240,27 @@ public extension View { /// Sets the default `UIFont` for ``SUITextField`` in this view. /// - /// Use `uiTextFieldFont(_:)` to apply a specific font to all of the ``SUITextField`` in a view. - /// - /// The example below shows the effects of applying fonts to individual - /// views and to view hierarchies. Font information flows down the view - /// hierarchy as part of the environment, and remains in effect unless - /// overridden at the level of an individual view or view container. - /// - /// Here, the outermost `VStack` applies a 16-point system font as a - /// default font to text fields contained in that `VStack`. Inside that stack, - /// the example applies a 20-point bold system font to just the first text - /// field; this explicitly overrides the default. The remaining stack and the - /// views contained with it continue to use the 16-point system font set by - /// their containing view: - /// - /// ```swift - /// VStack { - /// SUITextField(text: $text, placeholder: "this text field has 20-point bold system font") - /// .uiTextFieldFont(.systemFont(ofSize: 20, weight: .bold)) + /// Setting `nil` will restore the system font. /// - /// VStack { - /// SUITextField(text: $text, placeholder: "this two text fields") - /// SUITextField(text: $text, placeholder: "have same font applied from modifier") - /// } - /// } - /// .uiTextFieldFont(.systemFont(ofSize: 16, weight: .light)) - /// ``` + /// - Note: This modifier overrides font in ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)`` /// /// - Parameter font: The default font to use in this view. - /// /// - Returns: A view with the default font set to the value you supply. func uiTextFieldFont(_ font: UIFont?) -> some View { environment(\.uiTextFieldFont, font) } + + /// Sets the text color for all ``SUITextField`` in this view. + /// + /// Setting `nil` will restore the system color. + /// + /// - Note: This modifier has higher priority than ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)`` + /// + /// - Parameter color: The `UIColor` to be applied on all ``SUITextField`` in this view. + /// - Returns: A view with the text color you supply. + func uiTextFieldTextColor(_ color: UIColor?) -> some View { + environment(\.uiTextFieldTextColor, color) + } /// Sets the return key type of the keyboard for all ``SUITextField`` in this view. /// @@ -316,6 +334,8 @@ public extension View { /// Sets the text alignment for all ``SUITextField`` in this view. /// + /// - Note: This modifier has higher priority than ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)`` + /// /// - Parameter textAlignment: The `NSTextAlignment` to be applied. /// - Returns: A view with the chosen text alignment applied. func uiTextFieldTextAlignment(_ textAlignment: NSTextAlignment) -> some View { @@ -345,12 +365,36 @@ public extension View { /// Sets attributes to the generated `NSAttributedString` for all ``SUITextField`` in this view. /// /// You can style the text/font using this modifier, allowing the text fields to be high customized. - /// Setting this modifier will override default styling of font, color and text alignment. /// - /// - Parameter defaultTextAttributes: A dictionary of attributes applied to the `NSAttributedString` of text field. + /// - Note: This modifier has low priority than font, color and text alignment modifiers. + /// + /// - Parameters: + /// - defaultTextAttributes: A dictionary of attributes applied to the `NSAttributedString` of text field. + /// - mergePolicy: Indicates how apply the new attributes. If you used this modifier in a container view and you + /// want to apply more styling, you can decided if the new style will merge with the old one and keep either new or old values + /// for same key (using ``DefaultAttributesMergePolicy/keepNew`` and ``DefaultAttributesMergePolicy/keepOld``). + /// If you want to apply **only** the new style and get rid of the old one, + /// use ``DefaultAttributesMergePolicy/rewriteAll``. Default is ``DefaultAttributesMergePolicy/rewriteAll`` /// - Returns: A view with the chosen text attributes applied. - func uiTextFieldDefaultTextAttributes(_ defaultTextAttributes: [NSAttributedString.Key: Any]?) -> some View { - environment(\.uiTextFieldDefaultTextAttributes, defaultTextAttributes) + func uiTextFieldDefaultTextAttributes( + _ defaultTextAttributes: [NSAttributedString.Key: Any]?, + mergePolicy: DefaultAttributesMergePolicy = .rewriteAll + ) -> some View { + transformEnvironment(\.uiTextFieldDefaultTextAttributes) { attributes in + var newAttributes = attributes + switch mergePolicy { + case .keepOld: + if let defaultTextAttributes = defaultTextAttributes { + newAttributes = (newAttributes ?? [:]).merging(defaultTextAttributes) { old, new in old } + } + case .keepNew: + if let defaultTextAttributes = defaultTextAttributes { + newAttributes = (newAttributes ?? [:]).merging(defaultTextAttributes) { old, new in new } + } + case .rewriteAll: newAttributes = defaultTextAttributes + } + attributes = newAttributes + } } /// Sets the spell checking type for all ``SUITextField`` in this view. @@ -371,8 +415,11 @@ public extension View { /// Sets whether or not all ``SUITextField`` in this view should resize text based on text field width. /// - /// - Parameter fontSizeWidthAdjustment: The ``FontSizeWidthAdjustment`` to be applied. + /// - Parameter fontSizeWidthAdjustment: The ``SwiftUITextField/FontSizeWidthAdjustment`` to be applied. /// - Returns: A view with the adjustment behavior you supply. + /// + /// - Note: When using ``SUITextField/uiTextFieldDefaultTextAttributes(_:mergePolicy:)`` + /// this modifier is ignored by the underlying `UITextField`. func uiTextFieldAdjustsFontSizeToFitWidth(_ fontSizeWidthAdjustment: FontSizeWidthAdjustment) -> some View { environment(\.uiTextFieldAdjustsFontSizeToFitWidth, fontSizeWidthAdjustment) } diff --git a/Sources/SwiftUITextField/TextField/TextType.swift b/Sources/SwiftUITextField/TextField/Internal/TextType.swift similarity index 100% rename from Sources/SwiftUITextField/TextField/TextType.swift rename to Sources/SwiftUITextField/TextField/Internal/TextType.swift diff --git a/Sources/SwiftUITextField/TextField/_SUITextField.swift b/Sources/SwiftUITextField/TextField/Internal/_SUITextField.swift similarity index 100% rename from Sources/SwiftUITextField/TextField/_SUITextField.swift rename to Sources/SwiftUITextField/TextField/Internal/_SUITextField.swift diff --git a/Sources/SwiftUITextField/TextField/SUITextField.swift b/Sources/SwiftUITextField/TextField/SUITextField.swift index c62e0e6..f3ced47 100644 --- a/Sources/SwiftUITextField/TextField/SUITextField.swift +++ b/Sources/SwiftUITextField/TextField/SUITextField.swift @@ -173,6 +173,129 @@ public extension SUITextField { } +public extension SUITextField { + + private init(value: Binding, formatter: Formatter, placeholder: TextType?, defaultValue: V?) + where InputView == EmptyView, InputAccessoryView == EmptyView, LeftView == EmptyView, RightView == EmptyView { + let binding = Binding { + formatter.string(for: value.wrappedValue) ?? "" + } set: { newValue in + var newObj: AnyObject? = nil + formatter.getObjectValue(&newObj, for: newValue, errorDescription: nil) + guard let newObj = (newObj as? V) ?? defaultValue else { return } + value.wrappedValue = newObj + } + self.init( + text: binding, + placeholder: placeholder, + autoSizeInputView: false, + leftView: { EmptyView() }, + rightView: { EmptyView() }, + inputView: { EmptyView() }, + inputAccessoryView: { EmptyView() } + ) + } + + /// Creates a text field that applies a format style to a bound value and a plain placeholder. + /// + /// Use this initializer to create a text field that binds to a bound value, using a `Foundation.Formatter` to convert to and from this type. + /// Changes to the bound value update the string displayed by the text field. + /// Editing the text field updates the bound value, as long as the formatter can parse the text. + /// If the formatter can’t parse the input, the bound value remains unchanged. + /// + /// The following example uses a Double as the bound value, and a `NumberFormatter` + /// instance to convert to and from a string representation. As the user types, the bound value updates, + /// which in turn updates three `Text` views that use different format styles. + /// If the user enters text that doesn’t represent a valid `Double`, the bound value doesn’t update. + /// + ///```swift + ///@State private var myDouble: Double = 0.673 + ///@State private var numberFormatter: NumberFormatter = { + /// var nf = NumberFormatter() + /// nf.numberStyle = .decimal + /// return nf + ///}() + /// + ///var body: some View { + /// VStack { + /// SUITextField( + /// value: $myDouble, + /// formatter: numberFormatter + /// ) + /// Text(myDouble, format: .number) + /// Text(myDouble, format: .number.precision(.significantDigits(5))) + /// Text(myDouble, format: .number.notation(.scientific)) + /// } + ///} + ///``` + /// - Parameters: + /// - value: The underlying value to edit. + /// - formatter: A subclasses of foundation `Formatter` to use when converting between the string the user edits and the + /// underlying value of type `V`. If formatter can’t perform the conversion, the text field leaves binding.value unchanged. + /// If the user stops editing the text in an invalid state, the text field updates the field’s text to the last known valid value. + /// - placeholder: The plain string placeholder. + /// - defaultValue: A default value to apply if conversion fails. Default is `nil` which mean bound value doesn't update in case of failure. + init(value: Binding, formatter: Formatter, placeholder: String? = nil, defaultValue: V? = nil) + where InputView == EmptyView, InputAccessoryView == EmptyView, LeftView == EmptyView, RightView == EmptyView { + self.init( + value: value, + formatter: formatter, + placeholder: placeholder.map { .plain($0) }, + defaultValue: defaultValue + ) + } + + /// Creates a text field that applies a format style to a bound value and an attributed placeholder. + /// + /// Use this initializer to create a text field that binds to a bound value, using a `Foundation.Formatter` to convert to and from this type. + /// Changes to the bound value update the string displayed by the text field. + /// Editing the text field updates the bound value, as long as the formatter can parse the text. + /// If the formatter can’t parse the input, the bound value remains unchanged. + /// + /// The following example uses a Double as the bound value, and a `NumberFormatter` + /// instance to convert to and from a string representation. As the user types, the bound value updates, + /// which in turn updates three `Text` views that use different format styles. + /// If the user enters text that doesn’t represent a valid `Double`, the bound value doesn’t update. + /// + ///```swift + ///@State private var myDouble: Double = 0.673 + ///@State private var numberFormatter: NumberFormatter = { + /// var nf = NumberFormatter() + /// nf.numberStyle = .decimal + /// return nf + ///}() + /// + ///var body: some View { + /// VStack { + /// SUITextField( + /// value: $myDouble, + /// formatter: numberFormatter + /// ) + /// Text(myDouble, format: .number) + /// Text(myDouble, format: .number.precision(.significantDigits(5))) + /// Text(myDouble, format: .number.notation(.scientific)) + /// } + ///} + ///``` + /// - Parameters: + /// - value: The underlying value to edit. + /// - formatter: A subclasses of foundation `Formatter` to use when converting between the string the user edits and the + /// underlying value of type `V`. If formatter can’t perform the conversion, the text field leaves binding.value unchanged. + /// If the user stops editing the text in an invalid state, the text field updates the field’s text to the last known valid value. + /// - placeholder: The `NSAttributedString` placeholder. + /// - defaultValue: A default value to apply if conversion fails. Default is `nil` which mean bound value doesn't update in case of failure. + init(value: Binding, formatter: Formatter, placeholder: NSAttributedString, defaultValue: V? = nil) + where InputView == EmptyView, InputAccessoryView == EmptyView, LeftView == EmptyView, RightView == EmptyView { + self.init( + value: value, + formatter: formatter, + placeholder: .attributed(placeholder), + defaultValue: defaultValue + ) + } + +} + @available(iOS 15, *) public extension SUITextField { @@ -188,7 +311,7 @@ public extension SUITextField { private init(value: Binding, format: F, placeholder: TextType? = nil, defaultValue: F.FormatInput? = nil) where F: ParseableFormatStyle, F.FormatOutput == String, InputView == EmptyView, InputAccessoryView == EmptyView, LeftView == EmptyView, RightView == EmptyView { - let binding = Binding.init { + let binding = Binding { format.format(value.wrappedValue) } set: { newValue in guard let newValue = (try? format.parseStrategy.parse(newValue)) ?? defaultValue else { return } @@ -766,13 +889,19 @@ public extension SUITextField { uiView[keyPath: keyPath] = value } } - uiView.text = text + + if let attributes = context.environment.uiTextFieldDefaultTextAttributes { + uiView.defaultTextAttributes = attributes + } + switch placeholder { case .attributed(let text): applyIfDifferent(value: text, at: \.attributedPlaceholder) case .plain(let text): applyIfDifferent(value: text, at: \.placeholder) case nil: applyIfDifferent(value: nil, at: \.placeholder) } - applyIfDifferent(value: context.environment.uiTextFieldFont, at: \.font) + + uiView.text = text + applyIfDifferent(value: context.environment.uiReturnKeyType, at: \.returnKeyType) applyIfDifferent(value: context.environment.uiTextFieldSecureTextEntry, at: \.isSecureTextEntry) applyIfDifferent(value: context.environment.uiTextFieldClearButtonMode, at: \.clearButtonMode) @@ -781,17 +910,12 @@ public extension SUITextField { applyIfDifferent(value: context.environment.uiTextAutocapitalizationType, at: \.autocapitalizationType) applyIfDifferent(value: context.environment.uiTextFieldTextContentType, at: \.textContentType) applyIfDifferent(value: context.environment.uiTextFieldKeyboardType, at: \.keyboardType) - applyIfDifferent(value: context.environment.uiTextFieldTextAlignment, at: \.textAlignment) applyIfDifferent(value: context.environment.uiTextFieldTextLeftViewMode, at: \.leftViewMode) applyIfDifferent(value: context.environment.uiTextFieldTextRightViewMode, at: \.rightViewMode) applyIfDifferent(value: context.environment.isEnabled, at: \.isEnabled) applyIfDifferent(value: context.environment.uiTextFieldSpellCheckingType, at: \.spellCheckingType) applyIfDifferent(value: context.environment.uiTextFieldPasswordRules, at: \.passwordRules) - - if let attributes = context.environment.uiTextFieldDefaultTextAttributes { - uiView.defaultTextAttributes = attributes - } - + switch context.environment.uiTextFieldAdjustsFontSizeToFitWidth { case .disabled: applyIfDifferent(value: false, at: \.adjustsFontSizeToFitWidth) @@ -801,13 +925,11 @@ public extension SUITextField { applyIfDifferent(value: minSize, at: \.minimumFontSize) } - DispatchQueue.main.async { - context.coordinator.inputViewController?.rootView = inputView - context.coordinator.inputAccessoryViewController?.rootView = inputAccessoryView - - context.coordinator.leftView?.rootView = leftView - context.coordinator.rightView?.rootView = rightView - } + context.coordinator.inputViewController?.rootView = inputView + context.coordinator.inputAccessoryViewController?.rootView = inputAccessoryView + + context.coordinator.leftView?.rootView = leftView + context.coordinator.rightView?.rootView = rightView updateProxy?(uiView) }