Link #Binding to #Published with SwiftUI - swiftui

I'm trying to figure out how to link the #Binding passed into a custom View to an #Published from that view's model. Essentially I'm trying to create a reusable integer only TextField. I'm using the below code, which works to set the integer value into the text field, but what I can't figure out is how to update the binding when the text changes.
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
}
}
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
}
}

If I understand you correctly
.onChange (of: fieldValue.value) { vl in
value = vl
}
this modifier updates the binding value to $fieldValue.value

Here is modified code to demo a possible approach (tested with Xcode 12.1 / iOS 14.1):
private class IntegerTextFieldValue: ObservableObject {
#Published var value = "" {
didSet {
let numbersOnly = value.filter { $0.isNumber }
if value != numbersOnly {
value = numbersOnly
}
if let number = Int(value) {
numberValue = number
}
}
}
#Published var numberValue: Int = 0
}
struct IntegerTextField: View {
#Binding var value: Int?
#StateObject private var fieldValue = IntegerTextFieldValue()
var placeholder = ""
var body: some View {
TextField(placeholder, text: $fieldValue.value)
.keyboardType(.numberPad)
.onAppear {
if let value = value {
fieldValue.value = "\(value)"
}
}
.onChange(of: fieldValue.numberValue) {
if $0 != self.value {
self.value = $0
}
}
}
}

Related

#Published value don't pass through views

I'm beggining with SwiftUI and I wanted to develop a small simple app to practice. I have a problem with #Published property that don't pass through views and so don't update the view.
I explain : In the first view I calculate the vMoyenne property and update it. I wanted to show this value in the next view ("Passage") to be able to use it for some other calculation but I tried many thing and the value in the "Passage" View doesn't update...
Here is the code :
ContentView.swift :
struct ContentView: View {
var body: some View {
TabView {
SpeedView().tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: Parameters()).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}
Parameters.swift
class Parameters: ObservableObject {
#Published var distance: Double?
static let units = ["m", "km"]
#Published var unit = 1
#Published var hour: Int = 0
#Published var minute: Int = 0
#Published var second: Int = 0
#Published var vMoyenne = 0.0
#Published var allure = 0.0
#Published var convertedDecimalToSeconds = 0
var time: Int?
...
func calcVMoy() -> Void{
var d = distance!
let t = Double(time!) / 3600
var unite: String {
return Parameters.units[unit]
}
var calc = 0.0
if unite == "km" {
calc = d / t
} else {
d = d / 1000
calc = d / t
}
vMoyenne = calc
}
...
init() {
}
}
**SpeedView.swift **
struct SpeedView: View {
#ObservedObject var parameters = Parameters()
...
...
Button {
showVMoy = true
disableChange = true
if parameters.distance == nil {
parameters.distance = 0
} else {
parameters.runCalc()
}
} label: {
Text("Calculer")
}
... *// Here I can show and see the calculated vMoyenne property without problem...*
...
}
And the PassageView.swift where I want to show the vMoyenne property...
struct PassageView: View {
#ObservedObject var parameters:Parameters
var body: some View {
Text("\(parameters.vMoyenne)") *//want to show the vMoyenne value that we calculate previously but it always show 0,000...*
}
}
Thanks a lot for your help !!
PS : I tried many things like using didSet but I don't understand what I did wrong...
I found some post on stackoverflow but when I tried it doesn't work...
If you update the ContentView to it should work. The problem was that the SpeedView and PassageView were not sharing the same parameters object
struct ContentView: View {
#StateObject var parameters: Parameters = .init()
var body: some View {
TabView {
SpeedView(parameters: parameters).tabItem {
Label("Vitesse", systemImage: "figure.run.circle.fill")
}
PassageView(parameters: parameters).tabItem {
Label("Passage", systemImage: "timer.circle.fill")
}
}
}
}

Using ForEach inside a Picker

I'm having issues pulling data from an Array into a picker using SwiftUI. I can correctly make a list of the data I'm interested in, but can't seem to make the same logic work to pull the data into a picker. I've coded it a few different ways but the current way I have gives this error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Text' conform to 'TableRowContent'
The code is below:
import SwiftUI
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle, content: {
ForEach(0..<model.list.count, content: { index in
Text(index.style)
})
})
}
}
The model is here:
import Foundation
struct Bumps: Identifiable{
var id: String
var style: String
}
and the ViewModel is here:
import Foundation
import Firebase
import FirebaseFirestore
class ViewModel: ObservableObject {
#Published var list = [Bumps]()
#Published var styleArray = [String]()
func getData2() {
let db = Firestore.firestore()
db.collection("bumpStop").getDocuments { bumpSnapshot, error in
//Check for errors first:
if error == nil {
//Below ensures bumpSnapshot isn't nil
if let bumpSnapshot = bumpSnapshot {
DispatchQueue.main.async {
self.list = bumpSnapshot.documents.map{ bump in
return Bumps(id: bump.documentID,
style: bump["style"] as? String ?? "")
}
}
}
}
else {
//Take care of the error
}
}
}
}
index in your ForEach is just an Int, there is no style associated with an Int. You could try this approach to make the Picker work with its ForEach:
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle) {
ForEach(model.list.indices, id: \.self) { index in
Text(model.list[index].style).tag(index)
}
}
}
}
}
EDIT-1:
Text(model.list[selectedStyle].style) will give you the required style of the selectedStyle.
However, as always when using index, you need to ensure it is valid at the time of use.
That is, use if selectedStyle < model.list.count { Text(model.list[selectedStyle].style) }.
You could also use this alternative approach that does not use index:
struct Bumps: Identifiable, Hashable { // <-- here
var id: String
var style: String
}
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedBumps = Bumps(id: "", style: "") // <-- here
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)
}
Picker("Style", selection: $selectedBumps) {
ForEach(model.list) { bumps in
Text(bumps.style).tag(bumps) // <-- here
}
}
}
.onAppear {
if let first = model.list.first {
selectedBumps = first
}
}
}
}
Then use selectedBumps, just like any Bumps, such as selectedBumps.style

Why does SwiftUI update function doesn't work?

There are 2 views (structs).
First view has a #state update:
struct SettingsView: View {
#State private var lang = 0
#State private var languages = ["English", "Spanish"]
#State private var text1 = "Close"
#State private var text2 = "Settings"
#State var show = false
#State var update = false
var body: some View {
ZStack{
Button(action: {
self.show.toggle()
}) {
Text("Choose language")
}
if self.$show.wrappedValue {
GeometryReader {proxy in
ChooseLanguage(show: self.$show, update: self.$update)
}.background(Color.black.opacity(0.65)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation{
self.show.toggle()
}
})
}
}.onAppear{
switch UserDefaults.standard.string(forKey: "languageSettings"){
case "en": self.lang = 0
case "es": self.lang = 1
default: return
}
self.updateLanguage()
}
func updateLanguage(){
if self.lang == 1 {
self.text1 = "Cerrar"
self.text2 = "Configuración"
self.languages = ["Inglés", "Español"]
} else {
self.text1 = "Close"
self.text2 = "Settings"
self.languages = ["English", "Spanish"]
}
}
}
}
The second view has #Binding update:
import SwiftUI
struct ChooseLanguage : View {
var languages = UserDefaults.standard.stringArray(forKey: "langlist")
#Binding var show: Bool
#Binding var update: Bool
var body: some View {
ZStack {
VStack {
Button(action: {
UserDefaults.standard.set("en", forKey: "languageSettings")
UserDefaults.standard.set(["English", "Spanish"], forKey: "langlist")
self.show.toggle()
self.update = true
}) {
Text(languages![0])
}
Button(action: {
UserDefaults.standard.set("es", forKey: "languageSettings")
UserDefaults.standard.set(["Inglés", "Español"], forKey: "langlist")
self.show.toggle()
self.update = true
}) {
Text(languages![1])
}
}
}
}
}
When I call the func updateLanguage() before the .onAppear only errors appear.
Why I can update the values with function from the onAppear and I can't do this from the wrappedValue?
if self.$update.wrappedValue {
self.updateLanguage()
self.update.toggle()
}
This part doesn't work if to place before }.onAppear
As far as I see, you can make it so much easier with using init() method for your view.
There you can declare and initialize all your #State variables with the correct value (depending on your UserDefaults)
Just to show you an example:
struct SetView: View {
#State private var lang : Int
#State private var languages : [String]
#State private var text1 : String
#State private var text2 : String
#State var show = false
#State var update = false
init()
{
var state : Int = 0
switch UserDefaults.standard.string(forKey: "languageSettings")
{
case "en": state = 0
case "es": state = 1
default:
//Default value here
state = 0
}
if state == 1 {
self._lang = State(initialValue: state)
self._text1 = State(initialValue: "Cerrar")
self._text2 = State(initialValue: "Configuración")
self._languages = State(initialValue: ["Inglés", "Español"])
} else {
self._lang = State(initialValue: state)
self._text1 = State(initialValue: "Close")
self._text2 = State(initialValue: "Settings")
self._languages = State(initialValue: ["English", "Spanish"])
}
}
You won't need onAppear method at all, when you initialize your State variables with the correct value from the beginning.
I haven't tested it yet. Code is just out of my mind above.
Instead of calling the function after the #State upload value has been changed easier to send bindings for each text to the popup view.
GeometryReader {proxy in
ChooseLanguage(show: self.$show,
text1: self.$text1,
text2: self.$text2,
languages: self.$languages)
}

SwiftUI Picker desn't bind with ObservedObject

I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}

NumberField or how to make TextField input a Double, Float or other numbers with dot

According to comment in this question I made a custom SwifUI View based on a TextField. It use numeric keypad, you can't input there nothing but numbers and point, there can be only one point (dot), and you can pass a Bindable Double #State value through the View for input.
But there is a bug: when you deleting a last zero in "xxx.0" - zero still comes out. When you deleting a dot - zero becomes a part of integer, so it goes to "xxx0"
Any idea how to fix it? I tried to make value an integer when deleting last number before dot - but I can't catch the moment when there is only one last dot in a string.
here's full code:
import SwiftUI
import Combine
struct DecimalTextField: View {
public let placeHolder: String
#Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
#Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
switch substring.count{
case 0:
if self.numericValue != 0{
self.numericValue = 0
}
case 1 :
var newValue: Double = 0
if let lastChar = substring[0].last{
if lastChar == Character("."){
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
self.numericValue = newValue
default:
self.numericValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
#Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
#ObservedObject private var viewModel: DecimalTextFieldViewModel
init(placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: numericValue.wrappedValue == Double.zero ? "" : String(numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
#State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField(placeHolder: "123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
The only way to avoid loss of the decimal point and trailing 0's while typing is to keep track of the integral and fractional digits of the string across invocations of the numeric field, which means keeping the digit precision values as part of the state of the superview. See this gist for a fully functional (Swift 5) view that uses this technique. To see what happens if you don't keep the digit precision in the superview, compare the behavior of the first and second fields in the preview below: the first will handle typing as expected, the second will remove any trailing .0 as soon as the value changes.
I'm not sure if I really do everything right, but it looks like I fix that.
here's the code:
import SwiftUI
import Combine
fileprivate func getTextOn(double: Double) -> String{
let rounded = double - Double(Int(double)) == 0
var result = ""
if double != Double.zero{
result = rounded ? String(Int(double)) : String(double)
}
return result
}
struct DecimalTextField: View {
public let placeHolder: String
#Binding var numericValue: Double
private class DecimalTextFieldViewModel: ObservableObject {
#Published var text = ""{
didSet{
DispatchQueue.main.async {
let substring = self.text.split(separator: Character("."), maxSplits: 2)
if substring.count == 0{
if self.numericValue != 0{
self.numericValue = 0
}
}else if substring.count == 1{
var newValue: Double = 0
if let lastChar = substring[0].last{
let ch = String(lastChar)
if ch == "."{
newValue = Double(String(substring[0]).dropLast()) ?? 0
}else{
newValue = Double(String(substring[0])) ?? 0
}
}
if self.numericValue != newValue{
self.numericValue = newValue
}
}else{
let newValue = Double(String("\(String(substring[0])).\(String(substring[1]))")) ?? 0
if self.numericValue != newValue{
self.numericValue = newValue
}
}
}
}
}
private var subCancellable: AnyCancellable!
private var validCharSet = CharacterSet(charactersIn: "1234567890.")
#Binding private var numericValue: Double{
didSet{
DispatchQueue.main.async {
if String(self.numericValue) != self.text {
self.text = String(self.numericValue)
}
}
}
}
init(numericValue: Binding<Double>, text: String) {
self.text = text
self._numericValue = numericValue
subCancellable = $text.sink { val in
//check if the new string contains any invalid characters
if val.rangeOfCharacter(from: self.validCharSet.inverted) != nil {
//clean the string (do this on the main thread to avoid overlapping with the current ContentView update cycle)
DispatchQueue.main.async {
self.text = String(self.text.unicodeScalars.filter {
self.validCharSet.contains($0)
})
}
}
}
}
deinit {
subCancellable.cancel()
}
}
#ObservedObject private var viewModel: DecimalTextFieldViewModel
init(_ placeHolder: String = "", numericValue: Binding<Double>){
self._numericValue = numericValue
self.placeHolder = placeHolder
self.viewModel = DecimalTextFieldViewModel(numericValue: self._numericValue, text: getTextOn(double: numericValue.wrappedValue))
}
var body: some View {
TextField(placeHolder, text: $viewModel.text)
.keyboardType(.decimalPad)
}
}
struct testView: View{
#State var numeric: Double = 0
var body: some View{
return VStack(alignment: .center){
Text("input: \(String(numeric))")
DecimalTextField("123", numericValue: $numeric)
}
}
}
struct decimalTextField_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
But in debug I noticed the code in didSet executes several times. Not sure what is my mistake leading to that. Any suggestion?