Deriving binding from existing SwiftUI #States - swift

I've been playing around with SwiftUI and Combine and feel like there is probably a way to get a hold of the existing #State properties in a view and create a new one.
For example, I have a password creation View which holds a password and a passwordConfirm field for the user. I want to take those two #State properties and derive a new #State that I can use in my view that asserts if the input is valid. So for simplicity: not empty and equal.
The Apple docs say there is a publisher on a binding, though I can't appear to get ahold of it.
This is some non-functioning pseudo code:
import SwiftUI
import Combine
struct CreatePasswordView : View {
#State var password = ""
#State var confirmation = ""
lazy var valid = {
return self.$password.publisher()
.combineLatest(self.$confirmation)
.map { $0 != "" && $0 == $1 }
}
var body: some View {
SecureField($password, placeholder: Text("password"))
SecureField($confirmation, placeholder: Text("confirm password"))
NavigationButton(destination: NextView()) { Text("Done") }
.disabled(!valid)
}
}
Anyone found. the appropriate way of going about this / if it's possible?
UPDATE Beta 2:
As of beta 2 publisher is available so the first half of this code now works. The second half of using the resulting publisher within the View I've still not figured out (disabled(!valid)).
import SwiftUI
import Combine
struct CreatePasswordView : View {
#State var password = ""
#State var confirmation = ""
lazy var valid = {
Publishers.CombineLatest(
password.publisher(),
confirmation.publisher(),
transform: { String($0) != "" && $0 == $1 }
)
}()
var body: some View {
SecureField($password, placeholder: Text("password"))
SecureField($confirmation, placeholder: Text("confirm password"))
NavigationButton(destination: NextView()) { Text("Done") }
.disabled(!valid)
}
}
Thanks.

I wouldn't be playing with #State/#Published as Combine is in beta at the moment, but here's a simple workaround for what you're trying to achieve.
I'd implement a view model to hold password, password confirmation, and whether it's valid or not
class ViewModel: NSObject, BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var password: String = "" {
didSet {
didChange.send(())
}
}
var passwordConfirmation: String = "" {
didSet {
didChange.send(())
}
}
var isPasswordValid: Bool {
return password == passwordConfirmation && password != ""
}
}
In this way, the view is recomputed anytime the password or the confirmation changes.
Then I would make a #ObjectBinding to the view model.
struct CreatePasswordView : View {
#ObjectBinding var viewModel: ViewModel
var body: some View {
NavigationView {
VStack {
SecureField($viewModel.password,
placeholder: Text("password"))
SecureField($viewModel.passwordConfirmation,
placeholder: Text("confirm password"))
NavigationButton(destination: EmptyView()) { Text("Done") }
.disabled(!viewModel.isPasswordValid)
}
}
}
}
I had to put the views in a NavigationView, because NavigationButton doesn't seem to enable itself if it isn't in one of them.

What you need here is a computed property. As the name suggests, its value is re-computed every time the property is accessed. If you use #State variables to calculate the property, SwiftUI will automatically re-compute the body of the View whenever valid is changed:
struct CreatePasswordView: View {
#State var password = ""
#State var confirmation = ""
private var valid: Bool {
password != "" && password == confirmation
}
var body: some View {
SecureField($password, placeholder: Text("password"))
SecureField($confirmation, placeholder: Text("confirm password"))
NavigationLink(destination: NextView()) { Text("Done") }
.disabled(!valid)
}
}

Related

SwiftUI TextField resets value and ignores binding

Using a TextField on a mac app, when I hit 'return' it resets to its original value, even if the underlying binding value is changed.
import SwiftUI
class ViewModel {
let defaultS = "Default String"
var s = ""
var sBinding: Binding<String> {
.init(get: {
print("Getting binding \(self.s)")
return self.s.count > 0 ? self.s : self.defaultS
}, set: {
print("Setting binding")
self.s = $0
})
}
}
struct ContentView: View {
#State private var vm = ViewModel()
var body: some View {
TextField("S:", text: vm.sBinding)
.padding()
}
}
Why is this? Shouldn't it 'get' the binding value and use that? (i.e. shouldn't I see my print statement "Getting binding" in the console after I hit 'return' on the textfield?).
Here you go!
class ViewModel: ObservableObject {
#Published var s = "Default String"
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
var body: some View {
TextField("S:", text: $vm.s)
.padding()
}
}
For use in multiple views, in every view where you'd like to use the model add:
#EnvironmentObject private var vm: ViewModel
But don't forget to inject the model to the main view:
ContentView().environmentObject(ViewModel())

SwiftUI - How to change/access #Published var value via toggle from View?

I'm making a simple password generation app. The idea was simple.
There are 2 toggles in the View that are bound to ObservableObject class two #Published bool vars. The class should return a different complexity of generated password to a new View dependently on published vars true/false status after clicking generate button.
Toggles indeed change published var status to true/false (when I print it on toggle) and the destination view does show the password for false/false combination but for some reason, after clicking generate, they always stay false unless I manually change their value to true. Can toggles change permanently the value of #Published var values somehow?
I can't seem to find a suitable workaround. Any solutions how to make this work?
MainView
import SwiftUI
struct MainView: View {
#ObservedObject var manager = PasswordManager()
var body: some View {
NavigationView() {
VStack {
ZStack {
Toggle(isOn: $manager.includeNumbers) {
Text("Include numbers")
.italic()
}
}
ZStack {
Toggle(isOn: $manager.includeCharacters) {
Text("Include special characters")
.italic()
}
}
NavigationLink(destination: PasswordView(), label: {
Text("Generate")
})
}
.padding(80)
}
}
PasswordManager
import Foundation
class PasswordManager: ObservableObject {
#Published var includeNumbers = false
#Published var includeCharacters = false
let letters = ["A", "B", "C", "D", "E"]
let numbers = ["1", "2", "3", "4", "5"]
let specialCharacters = ["!", "#", "#", "$", "%"]
var password: String = ""
func generatePassword() -> String {
password = ""
if includeNumbers == false && includeCharacters == false {
for _ in 1...5 {
password += letters.randomElement()!
}
}
else if includeNumbers && includeCharacters {
for _ in 1...3 {
password += letters.randomElement()!
password += numbers.randomElement()!
password += specialCharacters.randomElement()!
}
}
return password
}
}
View that shows password
import SwiftUI
struct PasswordView: View {
#ObservedObject var manager = PasswordManager()
var body: some View {
Text(manager.generatePassword())
}
}
The problem is caused by the fact that your PasswordView creates its own PasswordManager. Instead, you need to inject it from the parent view.
You should never initialise an #ObservedObject inside the View itself, since whenever the #ObservedObject's objectWillChange emits a value, it will reload the view and hence create a new object. You either need to inject the #ObservedObject or declare it as #StateObject if you are targeting iOS 14.
PasswordView needs to have PasswordManager injected from MainView, since they need to use the same instance to have shared state. In MainView, you can use #StateObject if targeting iOS 14, otherwise you should inject PasswordManager even there.
import SwiftUI
struct PasswordView: View {
#ObservedObject private var manager: PasswordManager
init(manager: PasswordManager) {
self.manager = manager
}
var body: some View {
Text(manager.generatePassword())
}
}
struct MainView: View {
#StateObject private var manager = PasswordManager()
var body: some View {
NavigationView() {
VStack {
ZStack {
Toggle(isOn: $manager.includeNumbers) {
Text("Include numbers")
.italic()
}
}
ZStack {
Toggle(isOn: $manager.includeCharacters) {
Text("Include special characters")
.italic()
}
}
NavigationLink(destination: PasswordView(manager: manager), label: {
Text("Generate")
})
}
.padding(80)
}
}
}

How to correctly handle Picker in Update Views (SwiftUI)

I'm quite new to SwiftUI and I'm wondering how I should use a picker in an update view correctly.
At the moment I have a form and load the data in with .onAppear(). That works fine but when I try to pick something and go back to the update view the .onAppear() gets called again and I loose the picked value.
In the code it looks like this:
import SwiftUI
struct MaterialUpdateView: View {
// Bindings
#State var material: Material
// Form Values
#State var selectedUnit = ""
var body: some View {
VStack(){
List() {
Section(header: Text("MATERIAL")){
// Picker for the Unit
Picker(selection: $selectedUnit, label: Text("Einheit")) {
ForEach(API().units) { unit in
Text("\(unit.name)").tag(unit.name)
}
}
}
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Does anyone has experience with that problem or am I doing something terribly wrong?
You need to create a custom binding which we will implement in another subview. This subview will be initialised with the binding vars selectedUnit and material
First, make your MaterialUpdateView:
struct MaterialUpdateView: View {
// Bindings
#State var material : Material
// Form Values
#State var selectedUnit = ""
var body: some View {
NavigationView {
VStack(){
List() {
Section(header: Text("MATERIAL")) {
MaterialPickerView(selectedUnit: $selectedUnit, material: $material)
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Then, below, add your MaterialPickerView, as shown:
Disclaimer: You need to be able to access your API() from here, so move it or add it in this view. As I have seen that you are re-instanciating it everytime, maybe it is better that you store its instance with let api = API() and then refer to it with api, and even pass it to this view as such!
struct MaterialPickerView: View {
#Binding var selectedUnit: String
#Binding var material : Material
#State var idx: Int = 0
var body: some View {
let binding = Binding<Int>(
get: { self.idx },
set: {
self.idx = $0
self.selectedUnit = API().units[self.idx].name
self.material.unit = self.selectedUnit
})
return Picker(selection: binding, label: Text("Einheit")) {
ForEach(API().units.indices) { i in
Text(API().units[i].name).tag(API().units[i].name)
}
}
}
}
That should do,let me know if it works!

onReceive is not triggered after published value has changed

The point is that after successfully adding a record to the database (when I tap confirm button), the view should be closed. The problem is that it does not close after adding, .onReceive does not triggered, although the publisher PassthroughSubject sends a new value. There is one thing: the view will close if an alert was triggered before clicking the confirm button (for example, when not all fields are filled in, a warning is displayed)
Also properties viewModel.name and viewModel.name (for TextField's) after adding a record become equal to empty strings, although I do not explicitly assign such a value to them anywhere in the code (as if a new instance of the view model is created where such default values are)
View:
struct AddChallengeView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var viewModel = AddChallengeViewModel()
var body: some View {
Form{
Section(header: Text("Name")){
TextField("Type challenge name", text: $viewModel.name) //viewModel.name == "" after new record added
}
Section(header: Text("Description")){
TextEditor(text: $viewModel.description) //viewModel.description == "" after new record added
}
//...
Section{
Button(action: { viewModel.addChallenge()}){
HStack{
Spacer()
Text("Submit").bold()
Spacer()
}
}
}
}.alert(isPresented: $viewModel.showErrorAlert){
Alert(title: Text("Please, set all values!"))
}
.onReceive(viewModel.viewDismissalModePublisher) { shouldDismiss in
print("new value received") //not printed
if shouldDismiss {
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
ViewModel:
class AddChallengeViewModel: ObservableObject{
var viewDismissalModePublisher = PassthroughSubject<Bool, Never>()
private var shouldDismissView = false {
print("sending new value") //printed
didSet {
viewDismissalModePublisher.send(shouldDismissView)
}
}
#Published var showErrorAlert = false
#Published var name = ""
#Published var description = ""
//...
func addChallenge () {
//...
if (name != "" && description != "" && grounds.count != 0){
Firestore.firestore().collection("users").document(Auth.auth().currentUser!.uid).collection("challenges").addDocument(data: [
"name": "\(name)",
"description": "\(description)",
"grounds": grounds
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("setting new value") //printed
self.shouldDismissView = true
}
}
} else {
showErrorAlert.toggle()
}
}
}
UPDATE: I found a solution. We need to replace #ObservedObject var viewModel = AddChallengeViewModel() with #StateObject var viewModel = AddChallengeViewModel() in AddChallengeView but why does it work?
but why does it work?
Most probably you use AddChallengeView in NavigationView (or another container, which recreates content during workflow), so having
#ObservedObject var viewModel = AddChallengeViewModel()
creates new instance of AddChallengeViewModel class on each such view re-creation, so any previous changes are lost. However
#StateObject var viewModel = AddChallengeViewModel()
preserves instance of model (created at first time) and injects it into new view of same type re-created in same place of view hierarchy. Moreover, new view is notified about all changes of that same model.
Actually #StateObject property wrapper gives same behaviour for ObservableObject as #State gives for value types.

How to Add Max length for a character for Swift UI

Hi i am creating a to do application and i am facing a problem when a user entering some characters to a UIText field i remember there was a way in SWIFT 5 to put a max length but i can't find one in SWIFT UI can someone send me a link or guide me step by step HOW CAN I ADD A MAX LENTGH TO A SWIFT UI PROJECT TO THE TEXT FIELD! THANKS
I tried to find it Everywhere but i can't
struct NewTaskView: View {
var taskStore: TaskStore
#Environment(\.presentationMode) var presentationMode
#State var text = ""
#State var priority: Task.Priority = .Низкий
var body: some View {
Form {
TextField("Название задания", text: $text)
VStack {
Text("Приоритет")
.multilineTextAlignment(.center)
Picker("Priority", selection: $priority.caseIndex) {
ForEach(Task.Priority.allCases.indices) { priorityIndex in
Text(
Task.Priority.allCases[priorityIndex].rawValue
.capitalized
)
.tag(priorityIndex)
}
}
.pickerStyle( SegmentedPickerStyle() )
}
I want to put max length to a text field where is written TextField("Название задания", text: $text)
It seems like this can be achieved with Combine, by creating a wrapper around the text and opening a 2 way subscription, with the text subscribing to the TextField and the TextField subscribing to the ObservableObject. I'd say the way it works its quite logical from a Reactive point of view but would have liked to find a cleaner solution that didn't require another object to be created.
import SwiftUI
import Combine
class TextBindingManager: ObservableObject {
#Published var text = "" {
didSet {
if text.count > characterLimit && oldValue.count <= characterLimit {
text = oldValue
}
}
}
let characterLimit = 5
}
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager()
var body: some View {
TextField("Placeholder", text: $textBindingManager.text)
}
}
I read this article. please check here
This is my whole code. I don't use EnvironmentObject.
struct ContentView: View {
#ObservedObject private var restrictInput = RestrictInput(5)
var body: some View {
Form {
TextField("input text", text: $restrictInput.text)
}
}
class RestrictInput: ObservableObject {
#Published var text = ""
private var canc: AnyCancellable!
init (_ maxLength: Int) {
canc = $text
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { String($0.prefix(maxLength)) }
.assign(to: \.text, on: self)
}
deinit {
canc.cancel()
}
}