How to detect changes to the BindableObjects from the View? - swiftui

In UIKit, I would have code like this:
#IBOutlet weak var itemNameField: UITextField!
#IBAction func itemNameFieldDone(_ sender: UITextField) {
thisItem.myName = sender.text ?? thisItem.myName
thisItem.modified()
}
In the model object:
func modified() {
dateModified = Date()
let cds = FoodyDataStack.thisDataStack
uuidUser = cds.uuidUser
uuidFamily = cds.uuidFamily
}
In SwiftUI:
TextField($thisItem.myName)
Declarative, nice and short. SwiftUI takes care of updating the myName property as the user types in the TextField, but how do I get the dateModified property to update at the same time?

Use the TextField initializer that includes onEditingChanged and include whatever updating code you need in the closure.
TextField($thisCategory.myName, placeholder: nil, onEditingChanged: { (changed) in
self.thisCategory.modified()
self.dataStack.didChange.send(self.dataStack)
}).textFieldStyle(.roundedBorder)

iOS 14
There is a new modifier called onChange to detect changes of any state:
struct ContentView: View {
#State var text: String = ""
var body: some View {
TextField("Title", text: $text)
.onChange(of: text, perform: { value in
print(text)
})
}
}

You can add didSet observer to myName property in your item type declaration and then call modified from there:
var myName: String = "" {
didSet {
self.modified()
}
}

Related

SwiftUI - Binding in ObservableObject

Let's say we have a parent view like:
struct ParentView: View {
#State var text: String = ""
var body: some View {
ChildView(text: $text)
}
}
Child view like:
struct ChildView: View {
#ObservedObject var childViewModel: ChildViewModel
init(text: Binding<String>) {
self.childViewModel = ChildViewModel(text: text)
}
var body: some View {
...
}
}
And a view model for the child view:
class ChildViewModel: ObservableObject {
#Published var value = false
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
...
}
Making changes on the String binding inside the child's view model makes the ChildView re-draw causing the viewModel to recreate itself and hence reset the #Published parameter to its default value. What is the best way to handle this in your opinion?
Cheers!
The best way is to use a custom struct as a single source of truth, and pass a binding into child views, e.g.
struct ChildViewConfig {
var value = false
var text: String = ""
// mutating funcs for logic
mutating func reset() {
text = ""
}
}
struct ParentView: View {
#State var config = ChildViewConfig()
var body: some View {
ChildView(config: $config)
}
}
struct ChildView: View {
#Binding var config: ChildViewConfig
var body: some View {
TextField("Text", text: $config.text)
...
Button("Reset") {
config.reset()
}
}
}
"ViewConfig can maintain invariants on its properties and be tested independently. And because ViewConfig is a value type, any change to a property of ViewConfig, like its text, is visible as a change to ViewConfig itself." [Data Essentials in SwiftUI WWDC 2020].

When optional starts as nil, changing value doesn't work

I'm using MVVM with Swift UI. I have the following Struct, ViewModel and View
struct Thing: Identifiable, Codable, Hashable {
var id: String = UUID().uuidString
var price: Double?
}
class MyViewModel: ObservableObject {
//****
#Published var things : [Thing] = [Thing(id: "abc1234")]{
didSet{
print(things)
}
}
}
struct MyView: View {
#StateObject var myViewModel: MyViewModel = MyViewModel()
private let numberFormatter: NumberFormatter
init() {
numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
numberFormatter.maximumFractionDigits = 2
}
var body: some View {
VStack{
List{
ForEach(Array($myViewModel.things.enumerated()), id: \.offset) { index, $thing in
HStack{
//AnotherView(thing: $thing)
TextField("$0.00", value: $myViewModel.things[index].price, formatter: numberFormatter)
.keyboardType(.numberPad)
}//HStack
}//ForEach
}//List
}//VStack
}//View
}
With the above code, anytime the textfield is changed the didSet print statement will show that price = nil
However if I change the line under the comment with the ***** to the following, initializing price to 0 the changes in the textfield are correctly written back to the [Thing] array and it prints that its Optional(x.xx)
#Published var things : [Thing] = [Thing(id: "abc1234", price: 0)]{
What I also just figured out is that if you use the above line with price initialized to 0, if you backspace the default $0.00 in the TextField, it sets the value back to nil, and then it never changes again.
Price should not be optional just default it to 0. There is also a mistake in the ForEach View (it is not a for loop it needs to be given an identifiable array), fix as follows:
ForEach($store.things){ $thing in
HStack{
//AnotherView(thing: $thing)
TextField("$0.00", value: $thing.price, formatter: NumberFormatter.myCurrencyFormatter)
.keyboardType(.numberPad)
}//HStack
Note the formatter needs to be a singleton, global or static because we shouldn't init objects in View init or body.

Why my views are not Rendered in SwiftUI even though the Publisher emits a value

I am having a username textField in swiftUI. I am trying to validate input with the help of publishers.
Here is my code:
View
struct UserView: View {
#StateObject private var userViewModel = UserViewModel()
init(){
UITextField.appearance().semanticContentAttribute = .forceRightToLeft
UITextField.appearance().keyboardAppearance = .dark
}
var body: some View {
SecureField("", text: $userViewModel.passwordText)
Text(userViewModel.passwordError).foregroundColor(.red)
.frame(width: 264, alignment: .trailing)
}
}
The View Model
ViewModel
final class UserViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
#Published var userText: String = ""
#Published var userTextError = ""
private var usernamevalidation: AnyPublisher<(username:String, isValid: Bool), Never> {
return $userText
.dropFirst()
.map{(username:$0, isValid: !$0.isEmpty)}
.eraseToAnyPublisher()
}
private var usernamevalidated: AnyPublisher<Bool,Never> {
return usernamevalidation
.filter{$0.isValid}
.map{$0.username.isValidUserName()}
.eraseToAnyPublisher()
}
init(){
usernamevalidation.receive(on: RunLoop.main)
.map{$0.isValid ? "": "Emptyusername "}
.assign(to: \.userTextError, on: self)
.store(in: &cancellables)
usernamevalidated.receive(on: RunLoop.main)
.map{$0 ? "" : "wrong username "}
.assign(to: \.userTextError, on: self)
.store(in: &cancellables)
}
}
Extension
extension String {
func isValidUserName() -> Bool {
let usernameRegex = "^[a-zA-Z0-9_-]*$"
let usernamepred = NSPredicate(format:"SELF MATCHES %#", usernameRegex)
return usernamepred.evaluate(with: self)
}
}
In the usernamevalidated in the init() block in the ViewModel I am assigning the error to userTextError property which should be reflected in the textview. This should happens if a special character such as # or % .. etc are entered. What happens is that sometimes the error appears in red and other no even though I try to print value of string after map operator i can see the string in printing fine. It is just the error is sometimes reflected in the view and sometimes not. Am I missing something or doing something fundamentally wrong
The problem is that both usernamevalidation and usernamevalidated are computed properties. Making them stored will solve the problem, but you can also simplify the view model by observing changes to userText, validating them and assigning to userTextError like so:
final class UserViewModel: ObservableObject {
#Published var userText: String = ""
#Published private(set) var userTextError = ""
init() {
$userText
.dropFirst()
.map { username in
guard !username.isEmpty else {
return "Username is empty"
}
guard username.isValidUserName() else {
return "Username is invalid"
}
return ""
}
.receive(on: RunLoop.main)
.assign(to: &$userTextError)
}
}
It's also worth mentioning that replacing .assign(to: \.userTextError, on: self) with .assign(to: &$userTextError) gets rid of memory leak and means you do need to store it in cancellables any more.
Instead of computable use stored properties for your publishers (to keep them alive), like
private lazy var usernamevalidation: AnyPublisher<(username:String, isValid: Bool), Never> = {
return $userText
.dropFirst()
.map{(username:$0, isValid: !$0.isEmpty)}
.eraseToAnyPublisher()
}()
Previous answer (see edits if wanted) was incorrect because then the Emptyusername error would be overwritten, which is not what we needed (my mistake).
Turns out, the issue is iOS 14 updating the UI! The fix, which I've had to use before for TextField, looks a bit unusual but does the job.
Just add this in the view body:
var body: some View {
let _ = userViewModel.userTextError
/* Rest of view as before */
}

Binding<String?> on the SwiftUI View TextField

I have the following view model:
struct RegistrationViewModel {
var firstname: String?
}
I want to bind the firstname property in the TextField as shown below:
TextField("First name", text: $registrationVM.firstname)
.textFieldStyle(RoundedBorderTextFieldStyle())
I keep getting an error that Binding is not allowed.
To bind objects your variable needs to conform to one of the new wrappers #State, #Binding, #ObservableObject, etc.
Because your RegistrationViewModel doesn't conform to View the only way to do it is to have your RegistrationViewModel conform to ObservableObject.
class RegistrationViewModel: ObservableObject {
#Published var firstname: String?
}
Once that is done you can call it View using
#ObservedObject var resgistrationVM: RegistrationViewModel = RegistrationViewModel()
or as an #EnvironmentObject
https://developer.apple.com/tutorials/swiftui/handling-user-input
Also, SwiftUI does not work well with optionals but an extension can handle that very easily.
SwiftUI Optional TextField
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
public var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue.isEmpty ? nil : newValue
}
}
}

Updating EnvironmentObject Variable by Assignment in Button

I have a throwaway project I am using to try to familiarize myself with SwiftUI. Essentially, I have various types of apples, that I have made available through an EnvironmentObject variable. The project parallels the Landmarks tutorial that I have been through, but I am expanding on the use of objects such as steppers and buttons, etc.
I am currently attempting to have a button, when pressed, save the UUID of a certain variety of apple and send it back to the original view. It is not working, and I am not sure why. It seems like a problem of the environmentObject assignment not escaping the closure for the action:. Have have set print statements and Text views to display the values of the variables at certain points. While it seems to set the variable in the closure, it doesn't escape the closure and the variable is never really updated.
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserData()))
self.window = window
window.makeKeyAndVisible()
}
}
struct AppleData: Codable, Hashable, Identifiable {
let id: UUID
var appleType: String
var numberOfBaskets: Int
var numberOfApplesPerBasket: [Int]
var fresh: Bool
static let `default` = Self(id: UUID(uuidString: "71190FD1-C8E0-4A65-996E-9CE84D200FBA")!,
appleType: "appleType",
numberOfBaskets: 1,
numberOfApplesPerBasket: [0],
fresh: true) // for purposes of automatic preview
func image(forSize size: Int) -> Image {
ImageStore.shared.image(name: appleType, size: size)
}
}
let appleData: [AppleData] = load("apples.json")
var appleUUID: UUID?
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
... // Code Omitted For Brevity
}
final class UserData: ObservableObject {
let willChange = PassthroughSubject<UserData, Never>()
var apples = appleData {
didSet {
willChange.send(self)
}
}
var appleId = appleUUID {
didSet {
willChange.send(self)
}
}
}
struct ContentView : View {
#EnvironmentObject private var userData: UserData
var body: some View {
NavigationView {
List {
ForEach(appleData) { apple in
NavigationLink(
destination: AppleDetailHost(apple: apple).environmentObject(self.userData)
) {
Text(verbatim: apple.appleType)
}
}
Text("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}
... // Code Omitted For Brevity
}
}
struct AppleDetail : View {
#EnvironmentObject var userData: UserData
#State private var basketIndex: Int = 0
var apple: AppleData
var totalApples: Int {
apple.numberOfApplesPerBasket.reduce(0, +)
}
var body: some View {
VStack {
... // Code Omitted For Brevity
}
Button(action: {
print("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
self.userData.appleId = self.apple.id
print("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}) {
Text("Use Apple")
}
Text("self.apple.id: \(self.apple.id.uuidString)")
Text("self.userData.appleId: \(self.userData.appleId?.uuidString ?? "Nil")")
}
... // Code Omitted For Brevity
}
The output of the print statements in the Button in AppleDetail is:
self.userData.appleId: Nil
self.userData.appleId: 28EE7739-5E5A-4CA4-AFF5-7A6BFE025250
The Text view that shows self.userData.appleId in ContentView is always Nil. Any help would be greatly appreciated.
In beta 5, the ObservableObject no longer uses willChange. It uses objectWillChange instead. In addition, it also autosynthesizes the subject, so you do not have to write it yourself (although you could overwrite it if you want).
On top of that, there's a new property wrapper (#Published), that will make changes on a property to have the publisher emit. No need to manually call .send(), as it will be done automatically. So if in your code, you rewrite your UserData class like this, it will work fine:
final class UserData: ObservableObject {
#Published var apples = appleData
#Published var appleId = appleUUID
}