I'm passing to TextField published variable
TextField("First name", text: $state.firstName)
I want to control imputes: Ignore spaces, if it's entered from the left
Where and how can I do it?
It is possible to do with proxy binding, like below
TextField("First name", text: Binding(
get: { self.state.firstName },
set: {
var newValue = $0
// fix newValue here as needed
self.state.firstName = newValue
}))
In your ViewModel add a checker that will automatically check every keystroke and fix the white space at first index.
import Foundation
import Combine
class ViewModel: ObservableObject {
#Published var value: String = ""
var previousAmount = 0.0
var validStringChecker: AnyCancellable? = nil
init() {
validStringChecker = $value.sink { val in
if val.first == " " {
var newValue = val
newValue.remove(at: newValue.firstIndex(of: " ")!)
DispatchQueue.main.async {
self.value = newValue
}
}
}
}
}
Use your TextField in your ContentView like:
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
TextField("First Name", text: $viewModel.value)
.textFieldStyle(RoundedBorderTextFieldStyle()).padding()
}
}
}
Related
I am learning SwiftUI and Combine to make a simple rent-splitting app. I am trying to follow the MVVM pattern and therefore have a Model, ViewModel and View as follows:
Model:
import Foundation
import Combine
struct Amounts {
var myMonthlyIncome : String = ""
var housemateMonthlyIncome : String = ""
var totalRent : String = ""
}
ViewModel:
import Foundation
import Combine
class FairRentViewModel : ObservableObject {
var amount: Amounts
init(_ amount: Amounts){
self.amount = amount
}
var myMonthlyIncome : String { return amount.myMonthlyIncome }
var housemateMonthlyIncome : String { return amount.housemateMonthlyIncome }
var totalRent : String { return amount.totalRent }
var yourShare: Double {
guard let totalRent = Double(totalRent) else { return 0 }
guard let myMonthlyIncome = Double(myMonthlyIncome) else { return 0 }
guard let housemateMonthlyIncome = Double(housemateMonthlyIncome) else { return 0 }
let totalIncome = Double(myMonthlyIncome + housemateMonthlyIncome)
let percentage = myMonthlyIncome / totalIncome
let value = Double(totalRent * percentage)
return Double(round(100*value)/100)
}
}
View:
import SwiftUI
import Combine
struct FairRentView: View {
#ObservedObject private var viewModel: FairRentViewModel
init(viewModel: FairRentViewModel){
self.viewModel = viewModel
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter the total monthly rent:")) {
TextField("Total rent", text: $viewModel.amount.totalRent)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter your monthly income:")) {
TextField("Your monthly wage", text: $viewModel.amount.myMonthlyIncome)
.keyboardType(.decimalPad)
}
Section(header: Text("Enter your housemate's monhtly income:")) {
TextField("Housemate's monthly income", text: $viewModel.amount.housemateMonthlyIncome)
.keyboardType(.decimalPad)
}
Section {
Text("Your share: £\(viewModel.yourShare, specifier: "%.2f")")
}
}
.navigationBarTitle("FairRent")
}
}
}
struct FairRentView_Previews: PreviewProvider {
static var previews: some View {
FairRentView(viewModel: FairRentViewModel(Amounts()))
}
}
The entry point:
#main
struct FairRentCalculatorApp: App {
var body: some Scene {
WindowGroup {
FairRentView(viewModel: FairRentViewModel(Amounts(myMonthlyIncome: "", housemateMonthlyIncome: "", totalRent: "")))
}
}
}
I want the yourShare value to update as the other properties are entered by the user in the form. This is what I have been trying to achieve with the above code. Can anyone please help point me in the right direction? I'm very new to SwiftUI + Combine and am trying my best to code cleanly so any other pointers are also welcome.
Thanks
You need something to signal to SwiftUI that a view needs to be updated.
ObservableObject objects have two ways to do that. One is directly via self.objectWillChange publisher, and the other - more common - is through its #Published properties that, when changed, use the objectWillChange automatically.
So, in your case, all you need to is mark amount property as #Published. Because it's a struct - a value-type - any change to its properties also changes the whole object:
#Published var amount: Amounts
Because the computed property yourShare is only ever updated when amount is updated, this would just work. The view would recompute itself with the now-updated yourShare.
I have the following code
struct ContentView: View {
#ObservedObject var list = ModelList.shared
var body: some View {
NavigationView {
List(list.sorted()) {object in
NavigationLink(destination: ModelView(object: object)) {
Text(object.title)
}
}
}
}
}
struct ModelView: View {
#State var object: ModelObject
var body: some View {
VStack {
Text(object.title)
TextField("Label", text: self.$object.text) // xxxxx Error on this line
.onChange(of: self.$object.text) { newValue in
print("Text changed to \(self.$object.text)!")
}
Button("Use") {
self.object.updateDate = Date()
print("title: \(object.title) - text: \(object.text) - date: \(object.updateDate)")
ModelList.shared.objectWillChange.send()
}
}
}
}
class ModelObject: ObservableObject {
#Published var updateDate: Date = Date()
let title: String
var text: String
init(title: String) {
self.title = title
self.text = ""
print(self)
}
}
I do get the error - Instance method 'onChange(of:perform:)' requires that 'Binding' conform to 'Equatable' on line XXXXX
However if I remove the textfield on change line then it compiles and have the code working. But I want to have some action be done when the Textfield get changed and the data to be saved in the struct in the array?
What am I missing here?
Thank you.
.onChange(of:perform:) doesn't take a Binding. Just pass the value. The same is true in the print statement:
TextField("Label", text: self.$object.text)
.onChange(of: self.object.text) { newValue in // removed $
print("Text changed to \(self.object.text)!") // removed $
}
Here is a minimal testable example that demonstrates the problem:
struct ContentView: View {
#State private var string = "hello"
var body: some View {
TextField("Label", text: self.$string)
.onChange(of: self.$string) { newValue in // remove $ here
print("Text changed to \(self.$string)") // remove $ here
}
}
}
I am trying to validate user input in a TextField by removing certain characters using a regular expression. Unfortunately, I am running into problems with the didSet method of the text var calling itself recursively.
import SwiftUI
import Combine
class TextValidator: ObservableObject {
#Published var text = "" {
didSet {
print("didSet")
text = text.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression
) // `\W` is an escape sequence that matches non-word characters.
}
}
}
struct ContentView: View {
#ObservedObject var textValidator = TextValidator()
var body: some View {
TextField("Type Here", text: $textValidator.text)
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
On the swift docs (see the AudioChannel struct), Apple provides an example in which a property is re-assigned within its own didSet method and explicitly notes that this does not cause the didSet method to be called again. I did some testing in a playground and confirmed this behavior. However, things seem to work differently when I use an ObservableObject and a Published variable.
How do I prevent the didSet method from calling itself recursively?
I tried the examples in this post, but none of them worked. Apple may have changed things since then, so this post is NOT a duplicate of that one.
Also, setting the text back to oldValue within the didSet method upon encountering invalid characters would mean that if a user pastes text, then the entire text would be removed, as opposed to only the invalid characters being removed. So that option won't work.
Since SwiftUI 2 you can check the input using the onChange method and do any validations or changes there:
TextField("", value: $text)
.onChange(of: text) { [text] newValue in
// do any validation or alteration here.
// 'text' is the old value, 'newValue' is the new one.
}
Try to validate what you want in the TextField onRecive method like this:
class TextValidator: ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#ObservedObject var textValidator = TextValidator()
var body: some View {
TextField("Type Here", text: $textValidator.text)
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onReceive(Just(textValidator.text)) { newValue in
let value = newValue.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression)
if value != newValue {
self.textValidator.text = value
}
print(newValue)
}
}
}
Here is possible approach using proxy binding, which still also allow separation of view & view model logic
class TextValidator: ObservableObject {
#Published var text = ""
func validate(_ value: String) -> String {
value.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression
)
}
}
struct ContentView: View {
#ObservedObject var textValidator = TextValidator()
var body: some View {
let validatingText = Binding<String>(
get: { self.textValidator.text },
set: { self.textValidator.text = self.textValidator.validate($0) }
)
return TextField("Type Here", text: validatingText)
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
2021 | SwiftUI 2
Custom extension usage:
TextField("New Branch name", text: $model.newNameUnified)
.ignoreSymbols( symbols: [" ", "\n"], string: $model.newNameUnified )
Extension:
#available(OSX 11.0, *)
public extension TextField {
func ignoreSymbols(symbols: [Character], string: Binding<String>) -> some View {
self.modifier( IgnoreSymbols(symbols: symbols, string: string) )
}
}
#available(OSX 11.0, *)
public struct IgnoreSymbols: ViewModifier {
var symbols: [Character]
var string: Binding<String>
public func body (content: Content) -> some View
{
content.onChange(of: string.wrappedValue) { value in
var newValue = value
for symbol in symbols {
newValue = newValue.replace(of: "\(symbol)", to: "")
}
if value != newValue {
string.wrappedValue = newValue
}
}
}
}
Here's what I came up with:
struct ValidatableTextField: View {
let placeholder: String
#State private var text = ""
var validation: (String) -> Bool
#Binding private var sourceText: String
init(_ placeholder: String, text: Binding<String>, validation: #escaping (String) -> Bool) {
self.placeholder = placeholder
self.validation = validation
self._sourceText = text
self.text = text.wrappedValue
}
var body: some View {
TextField(placeholder, text: $text)
.onChange(of: text) { newValue in
if validation(newValue) {
self.sourceText = newValue
} else {
self.text = sourceText
}
}
}
}
Usage:
ValidatableTextField("Placeholder", text: $text, validation: { !$0.contains("%") })
Note: this code doesn't solve specifically your problem but shows how to deal with validations in general.
Change body to this to solve your problem:
TextField(placeholder, text: $text)
.onChange(of: text) { newValue in
let value = newValue.replacingOccurrences(of: "\\W", with: "", options: .regularExpression)
if value != newValue {
self.sourceText = newValue
self.text = sourceText
}
}
Since didSet and willSet are always called when setting values, and objectWillChange triggers an update to the TextField (which triggers didSet again), a loop was created when the underlying value is updated unconditionally in didSet.
Updating the underlying value conditionally breaks the loop.
For example:
import Combine
class TextValidator: ObservableObject {
#Published var text = "" {
didSet {
if oldValue == text || text == acceptableValue(oldValue) {
return
}
text = acceptableValue(text)
}
}
var acceptableValue: (String) -> String = { $0 }
}
import SwiftUI
struct TestingValidation: View {
#StateObject var textValidator: TextValidator = {
let o = TextValidator()
o.acceptableValue = { $0.replacingOccurrences(
of: "\\W", with: "", options: .regularExpression) }
return o
}()
#StateObject var textValidator2: TextValidator = {
let o = TextValidator()
o.acceptableValue = { $0.replacingOccurrences(
of: "\\D", with: "", options: .regularExpression) }
return o
}()
var body: some View {
VStack {
Text("Word characters only")
TextField("Type here", text: $textValidator.text)
Text("Digits only")
TextField("Type here", text: $textValidator2.text)
}
.padding(.horizontal, 20.0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disableAutocorrection(true)
.autocapitalization(.none)
}
}
I have a SwiftUI screen with three textfields. When you run the code and tap the Clear button, you'll see three completely empty textfields. Expected is that you'd see the placeholder text, but that only appears in each textfield when it receives focus (i.e. user taps inside the field).
class UserInput: ObservableObject {
#Published var text1 = "some text"
#Published var text2 = "some more text"
#Published var text3 = "and this is the final input"
func clear() {
self.text1 = ""
self.text2 = ""
self.text3 = ""
}
}
struct ContentView: View {
#ObservedObject var userInput = UserInput()
var body: some View {
Form {
TextField("Type something in text1", text: self.$userInput.text1)
TextField("Type something in text2", text: self.$userInput.text2)
TextField("Type something in text3", text: self.$userInput.text3)
Button("Clear all fields", action: self.userInput.clear)
}
}
}
Is there something I'm missing, or is there a workaround for this behavior?
I found a workaround. Basically I send a special character that the user could never type, then catch that and clear the field "locally", in the form itself. It works, and restores the placeholder as one would expect.
As workarounds go, this one is pretty ugly.
class UserInput: ObservableObject {
static let clearCode = String.Element(Unicode.Scalar(7))
#Published var text1 = "some text"
#Published var text2 = "some more text"
#Published var text3 = "and this is the final input"
func clear() {
self.text1 = String(Self.clearCode)
self.text2 = String(Self.clearCode)
self.text3 = String(Self.clearCode)
}
}
struct ContentView: View {
#ObservedObject var userInput = UserInput()
var body: some View {
Form {
TextField("Type something in text1", text: self.$userInput.text1)
.onReceive(self.userInput.text1.publisher) { newValue in
if newValue == UserInput.clearCode {
self.userInput.text1 = ""
}
}
TextField("Type something in text2", text: self.$userInput.text2)
.onReceive(self.userInput.text2.publisher) { newValue in
if newValue == UserInput.clearCode {
self.userInput.text2 = ""
}
}
TextField("Type something in text3", text: self.$userInput.text3)
.onReceive(self.userInput.text3.publisher) { newValue in
if newValue == UserInput.clearCode {
self.userInput.text3 = ""
}
}
Button("Clear all fields", action: self.userInput.clear)
}
}
}
I tried the following solution but that didn't provide a workaround and still leaves the placeholder cleared.
class UserInput: ObservableObject {
let clearPublisher = PassthroughSubject<Bool, Never>()
// ...
func clear() {
self.clearPublisher.send(true)
}
}
struct ContentView: View {
// ...
TextField("Type something in text1", text: self.$userInput.text1)
.onReceive(self.userInput.clearPublisher) { _ in
self.userInput.text1 = ""
}
// ...
I want to delete an object which is marked as #ObjectBinding, in order to clean up some TextFields for example.
I tried to set the object reference to nil, but it didn't work.
import SwiftUI
import Combine
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var text = "" { didSet { didChange.send() } }
}
class B {
var property = "asdf"
}
struct DetailView : View {
#ObjectBinding var myObject: A = A() //#ObjectBinding var myObject: A? = A() -> Gives an error.
#State var mySecondObject: B? = B()
var body: some View {
VStack {
TextField($myObject.text, placeholder: Text("Enter some text"))
Button(action: {
self.test()
}) {
Text("Clean up")
}
}
}
func test() {
//myObject = nil
mySecondObject = nil
}
}
If I try to use an optional with #ObjectBinding, I'm getting the Error
"Cannot convert the value of type 'ObjectBinding' to specified type
'A?'".
It just works with #State.
Regards
You can do something like this:
class A: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var form = FormData() { didSet { didChange.send() } }
struct FormData {
var firstname = ""
var lastname = ""
}
func cleanup() {
form = FormData()
}
}
struct DetailView : View {
#ObjectBinding var myObject: A = A()
var body: some View {
VStack {
TextField($myObject.form.firstname, placeholder: Text("Enter firstname"))
TextField($myObject.form.lastname, placeholder: Text("Enter lastname"))
Button(action: {
self.myObject.cleanup()
}) {
Text("Clean up")
}
}
}
}
I absolutely agree with #kontiki , but you should remember to don't use #State when variable can get outside. #ObjectBinding right way in this case. Also all new way of memory management already include optional(weak) if they need it.
Check this to get more information about memory management in SwiftUI
Thats how to use #ObjectBinding
struct DetailView : View {
#ObjectBinding var myObject: A
and
DetailView(myObject: A())