programmatically segued navigation link reverts after sheet is displayed - swiftui

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

Related

Struct not changing with Binding

I have this struct
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
Then I have this on ContentView
#State private var temporaryObject = MyObject()
I send this to a view, like
var body: some View {
DoSomething($temporaryObject)
}
This is DoSomething
struct DoSomething: View {
#Binding var temporaryObject:MyObject
init(_ temporaryObject:Binding<MyObject>) {
self._temporaryObject = temporaryObject
}
var body: some View {
Button(action: {
// here is the problem
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to ""
}, label: {
Text("click me")
})
When I click the button, temporaryObject.name, in theory, is changed to "kkkk" but the print line shows it is still equals to empty.
why?
this example code works well for me. Does this code (taken from your question) not work for you?
struct ContentView: View {
#State private var temporaryObject = MyObject()
var body: some View {
VStack {
DoSomething(temporaryObject: $temporaryObject)
Text(temporaryObject.name) // <-- for testing
}
}
}
struct MyObject {
var name:String
var color:String
var date:Date
init(name:String = "", color: String = "", date:Date = Date()) {
self.name = name
self.color = color
self.date = date
}
}
struct DoSomething: View {
#Binding var temporaryObject:MyObject
var body: some View {
Button(action: {
temporaryObject.name = "kkkk"
print(temporaryObject.name) // is equal to "kkkk"
}, label: {
Text("click me")
})
}
}

SwiftUICharts are not redrawn when given new data

I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}

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

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