Why does SwiftUI update function doesn't work? - swiftui

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)
}

Related

programmatically segued navigation link reverts after sheet is displayed

I am having an issue where a programatically fired navigation link reverts to the root view after a sheet is called in the child view.
I am using the isActive method to call the child view after an async process. It seems that once the sheet is called (FilterView) the isActive variable (showResults) reverts to false.
Here's a video of the issue:
https://ibb.co/sKgbm4s
Root view variables:
#EnvironmentObject var userSettings: UserSettings
#State private var findings : [DDxFinding]
#State private var comorbids : [Comorbid]
#State private var showSheet = false
#State private var ageType : AgeType = .year
#State private var acuity : Acuity = .any
#State private var age : String = ""
#State private var sex : Sex = .any
#State private var degree : Degree = .two
let generator : DDxGenerator
let progressor : DDxProgress
#State private var progress : Float = 0
#State private var cancel : Bool = false
#State private var showResults : Bool = false
#State private var solutions : [Solution]?
init(findings: [DDxFinding] = [], comorbids : [Comorbid] = []) {
_findings = State(initialValue: findings)
_comorbids = State(initialValue: comorbids)
UITableView.appearance().sectionHeaderHeight = -1
generator = DDxGenerator(dbType: .sqlite)
progressor = DDxProgress()
generator.progressDelegate = progressor
}
var body: some View {
Root view calling code:
Section(footer: NavigationLink(destination: NavigationLazyView(DDxResultView(solutions: solutions)), isActive: $showResults) {
}.hidden()) {
let symps = findings.map {DdxFinding(id: $0.id, target: $0.targetted.intValue, root: $0.name)}
let comorbs = comorbids.map {DdxFinding(id: $0.id, root: $0.name)}
let data = DDxData(sex: sex.toInt, acuity: acuity.toInt, ageType: ageType.toInt, age: Int(age) ?? -1, degree: degree.toInt, symps: symps, comorbids: comorbs)
Button("Submit") {
if symps.isEmpty {
self.userSettings.showsAlert = true
self.userSettings.alertTitle = Text("Input atleast one finding.")
return
}
self.userSettings.showsAlert2 = true
cancel = false
progress = 0
self.userSettings.alert2Body = AnyView(CustomProgressView(progress: $progress, cancel: $cancel))
DispatchQueue.global(qos: .background).async {
do {
solutions = try generator.generateDDx(improvedData: data, db: database,
alertCode: { progress in
DispatchQueue.main.async {
self.progress = progress
}
}, cancelCode: {
return cancel
})
} catch {
solutions = nil
}
DispatchQueue.main.async {
if !cancel {
self.showResults = true
}
self.userSettings.showsAlert2 = false
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center).foregroundColor(.blue).font(.headline)
}
}
.listStyle(InsetGroupedListStyle())
Child view sheet code:
struct DDxResultView: View {
var solutions : [Solution]?
#ObservedObject var ddxObject : DDxObject
#State private var showingFilter = false
init(solutions: [Solution]?) {
self.ddxObject = DDxObject(solutions: solutions)
self.solutions = solutions
UITableView.appearance().sectionHeaderHeight = -1
}
var body: some View {
let data = self.ddxObject.sectionData
if solutions == nil {
errorView(.general)
} else {
List {
ForEach(data.indices, id: \.self) { index in
Section(header: Text(data[index].header)) {
ForEach(data[index].rows.indices, id: \.self) { index2 in
ResultRowView(data: $ddxObject.sectionData[index].rows[index2])
}
}
}
}
.sheet(isPresented: $showingFilter) {
VinFilterView(vinSelections: $ddxObject.vinSelections, showingFilter: $showingFilter)
}

#State var doesn't store value

My goal is to pass values between views, from Chooser to ThemeEditor. When the user presses an icon, I'm saving the object that I want to pass and later, using sheet and passing the newly created view with the content of the #State var.
The assignment is done successfully to the #State var themeToEdit, however it is nil when the ThemeEditor view is created in the sheet
What am I doing wrong?
struct Chooser: View {
#EnvironmentObject var store: Store
#State private var showThemeEditor = false
#State private var themeToEdit: ThemeContent?
#State private var editMode: EditMode = .inactive
#State private var isValid = false
var body: some View {
NavigationView {
List {
ForEach(self.store.games) { game in
NavigationLink(destination: gameView(game))
{
Image(systemName: "wrench.fill")
.imageScale(.large)
.opacity(editMode.isEditing ? 1 : 0)
.onTapGesture {
self.showThemeEditor = true
/* themeInfo is of type struct ThemeContent: Codable, Hashable, Identifiable */
self.themeToEdit = game.themeInfo
}
VStack (alignment: .leading) {
Text(self.store.name(for: something))
HStack{
/* some stuff */
Text(" of: ")
Text("Interesting info")
}
}
}
}
.sheet(isPresented: $showThemeEditor) {
if self.themeToEdit != nil { /* << themeToEdit is nil here - always */
ThemeEditor(forTheme: self.themeToEdit!, $isValid)
}
}
}
.environment(\.editMode, $editMode)
}
}
}
struct ThemeEditor: View {
#State private var newTheme: ThemeContent
#Binding var isValid: Bool
#State private var themeName = ""
init(forTheme theme: ThemeContent, isValid: Binding<Bool>) {
self._newTheme = State(wrappedValue: theme)
self._validThemeEdited = isValid
}
var body: some View {
....
}
}
struct ThemeContent: Codable, Hashable, Identifiable {
/* stores simple typed variables of information */
}
The .sheet content view is captured at the moment of creation, so if you want to check something inside, you need to use .sheet(item:) variant, like
.sheet(item: self.$themeToEdit) { item in
if item != nil {
ThemeEditor(forTheme: item!, $isValid)
}
}
Note: it is not clear what is ThemeContent, but it might be needed to conform it to additional protocols.
Use Binding. Change your ThemeEditor view with this.
struct ThemeEditor: View {
#Binding private var newTheme: ThemeContent?
#Binding var isValid: Bool
#State private var themeName = ""
init(forTheme theme: Binding<ThemeContent?>, isValid: Binding<Bool>) {
self._newTheme = theme
self._isValid = isValid
}
var body: some View {
....
}
}
And for sheet code
.sheet(isPresented: $showThemeEditor) {
ThemeEditor(forTheme: $themeToEdit, isValid: $isValid)
}
On Action
.onTapGesture {
/* themeInfo is of type struct ThemeContent: Codable, Hashable, Identifiable */
self.themeToEdit = game.themeInfo
self.showThemeEditor = true
}

Link #Binding to #Published with 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
}
}
}
}

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()
}
}

SwiftUI sheet never updated after first launch

I use a modal sheet whose content is updated for each call. However, when the content is marked as #State, the view body is never updated.
Is anyone seeing this as well? Is a workaround available?
This is the calling view:
struct ContentView: View {
#State var isPresented = false
#State var i = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}
.sheet(
isPresented: $isPresented,
content: {
SheetViewNOK(i: self.i)
}
)
}
}
This does work:
struct SheetViewOK: View {
var i: Int
var body: some View {
Text("Hello \(i)") // String is always updated
}
}
This does not work. But obviously, in a real app, I need to use #State because changes made by the user need to be reflected in the sheet's content:
struct SheetViewNOK: View {
#State var i: Int
var body: some View {
Text("Hello \(i)") // String is never updated after creation
}
}
In your .sheet you are passing the value of your ContentView #State to a new #State. So it will be independent from the ContentView.
To create a connection or a binding of your ContentView #State value, you should define your SheetView var as #Binding. With this edit you will pass the binding of your state value to your sheet view.
struct SheetView: View {
#Binding var i: Int
var body: some View {
Text("Hello \(i)")
}
}
struct ContentView: View {
#State var isPresented = false
#State var i: Int = 0
var body: some View {
List {
Button("0") {
self.i = 0
self.isPresented = true
}
Button("1") {
self.i = 1
self.isPresented = true
}
}.sheet(
isPresented: $isPresented,
content: {
SheetView(i: self.$i)
})
}
}
There are 3 different ways
use a binding
use multiple .sheets
use $item
This is fully explained in this video.
Multiple Sheets in a SwiftUI View