NavigationLink causing ChildView to reinitialize whenever ParentView is visible again (SwiftUI) - swift

I currently have an app where the user goes through pages of lists to make multiple selections from. (using NavigationLinks)
PROBLEM: The functionality is fine if the user simply makes their selection then moves on, however the issue is when the user goes back THEN forward to a page. I.e. ViewA -> ViewB -> View->A -> ViewB.
Doing this causes ViewB to reinitialize and delete all previous selections on that page, even if ViewA didn't update.
Note that using the back button preserves selections as expected.
EXPECTED BEHAVIOR:
I want to preserve states through navigation of these pages.
ViewA:
struct YouthEventCheckInView: View {
#StateObject var trackable = TrackableMetricsManager(metricType: TrackableMetricType.Event, isCheckin: true)
#StateObject var event = CustomMetricManager()
#StateObject var checkInViewModel = CheckInViewModel()
#State private var moveToDailyStressorsView = false
#State private var newEvent = false
var body: some View {
NavigationView {
ZStack {
ScrollView {
VStack(alignment: .leading) {
NavigationLink(destination: YouthStressorCheckInView(checkInViewModel: checkInViewModel), isActive: $moveToDailyStressorsView) {
EmptyView()
}
Button {
moveToDailyStressorsView = true
} label: {
HStack {
Text("Next")
}
.navigationTitle("Major Life Events")
.onAppear {
trackable.observeEvents()
}
}
}
ViewB (ViewC is same setup as this one):
struct YouthStressorCheckInView: View {
#StateObject var trackable = TrackableMetricsManager(metricType: TrackableMetricType.Stressor, isCheckin: true)
#StateObject var stressor = CustomMetricManager()
#ObservedObject var checkInViewModel: CheckInViewModel
#State private var moveToCopingStrategiesView = false
#State private var newStressor = false
var body: some View {
ZStack {
ScrollView {
VStack(alignment: .leading) {
NavigationLink(destination: YouthStrategyCheckInView(checkInViewModel: checkInViewModel), isActive: $moveToCopingStrategiesView) {
EmptyView()
}
Button( action: {
moveToCopingStrategiesView = true
}, label: {
HStack {
Text("Next")
})
}
}
.navigationTitle("Daily Stressors")
.onAppear {
trackable.observeStressors()
}
}
ViewModel for these views:
class ViewCheckInViewModel: ObservableObject {
struct Item: Hashable {
let name: String
let color: String
let image: String
}
#Published var loading = false
#Published var majorLifeEvents: [Item] = []
#Published var dailyStressors: [Item] = []
#Published var copingStrategies: [Item] = []
#Published var date: String = ""
func loadData(withDataStore dataStore: AWSAppSyncDataStore, checkInId: String) {
self.checkInId = checkInId
loadDate(withDataStore: dataStore)
loadMajorLifeEvents(withDataStore: dataStore)
loadDailyStressors(withDataStore: dataStore)
loadCopingStrategies(withDataStore: dataStore)
}
private func loadMajorLifeEvents(withDataStore dataStore: AWSAppSyncDataStore) {
...
}
private func loadDailyStressors(withDataStore dataStore: AWSAppSyncDataStore) {
...
}
private func loadCopingStrategies(withDataStore dataStore: AWSAppSyncDataStore) {
...
}
NOTE: Obviously some code is taken out, I left the things that I thought were necessary for this issue

Related

Passing a state variable to parent view

I have the following code:
struct BookView: View {
#State var title = ""
#State var author = ""
var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}
struct MainView: View {
#State private var presentNewBook: Bool = false
var body: some View {
NavigationView {
// ... some button that toggles presentNewBook
}.sheet(isPresented: $presentNewBook) {
let view = BookView()
view.toolbar {
ToolbarItem(placement: principal) {
TextField("Title", text: view.$title)
}
}
}
}
}
This compiles but is giving me the following error on runtime:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
How do I pass a state variable to some other outside view? I can't use ObservableObject on BookView since that would require me to change it from struct to class
In general, your state should always be owned higher up the view hierarchy. Trying to access the child state from a parent is an anti-pattern.
One option is to use #Bindings to pass the values down to child views:
struct BookView: View {
#Binding var title : String
#Binding var author : String
var body: some View {
TextField("Title", text: $title)
TextField("Author", text: $author)
}
}
struct ContentView: View {
#State private var presentNewBook: Bool = false
#State private var title = ""
#State private var author = ""
var body: some View {
NavigationView {
VStack {
Text("Title: \(title)")
Text("Author: \(author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(title: $title, author: $author)
}
}
}
Another possibility is using an ObservableObject:
class BookState : ObservableObject {
#Published var title = ""
#Published var author = ""
}
struct BookView: View {
#ObservedObject var bookState : BookState
var body: some View {
TextField("Title", text: $bookState.title)
TextField("Author", text: $bookState.author)
}
}
struct ContentView: View {
#State private var presentNewBook: Bool = false
#StateObject private var bookState = BookState()
var body: some View {
NavigationView {
VStack {
Text("Title: \(bookState.title)")
Text("Author: \(bookState.author)")
Button("Open") {
presentNewBook = true
}
}
}.sheet(isPresented: $presentNewBook) {
BookView(bookState: bookState)
}
}
}
I've altered your example views a bit because to me the structure was unclear, but the concept of owning the state at the parent level is the important element.
You can also pass a state variable among views as such:
let view = BookView(title: "foobar")
view.toolbar {
ToolbarItem(placement: principal) {
TextField("Title", text: view.$title)
}
}
Then, inside of BookView:
#State var title: String
init(title: String) {
_title = State(initialValue: title)
}
Source: How could I initialize the #State variable in the init function in SwiftUI?

How to update an element of an array in an Observable Object

Sorry if my question is silly, I am a beginner to programming. I have a Navigation Link to a detail view from a List produced from my view model's array. In the detail view, I want to be able to mutate one of the tapped-on element's properties, but I can't seem to figure out how to do this. I don't think I explained that very well, so here is the code.
// model
struct Activity: Identifiable {
var id = UUID()
var name: String
var completeDescription: String
var completions: Int = 0
}
// view model
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
}
// view
struct ActivityView: View {
#StateObject var viewModel = ActivityViewModel()
#State private var showingAddEditActivityView = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.activities, id: \.id) {
activity in
NavigationLink(destination: ActivityDetailView(activity: activity, viewModel: self.viewModel)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
}
}
.navigationBarItems(trailing: Button("Add new"){
self.showingAddEditActivityView.toggle()
})
.navigationTitle(Text("Activity List"))
}
.sheet(isPresented: $showingAddEditActivityView) {
AddEditActivityView(copyViewModel: self.viewModel)
}
}
}
// detail view
struct ActivityDetailView: View {
#State var activity: Activity
#ObservedObject var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
var tempActivity = viewModel.activities.first{ activity in activity.id == self.activity.id
}!
tempActivity.completions += 1
}
}
// Add new activity view (doesn't have anything to do with question)
struct AddEditActivityView: View {
#ObservedObject var copyViewModel : ActivityViewModel
#State private var activityName: String = ""
#State private var description: String = ""
var body: some View {
VStack {
TextField("Enter an activity", text: $activityName)
TextField("Enter an activity description", text: $description)
Button("Save"){
// I want this to be outside of my view
saveActivity()
}
}
}
func saveActivity() {
copyViewModel.activities.append(Activity(name: self.activityName, completeDescription: self.description))
print(copyViewModel.activities)
}
}
In the detail view, I am trying to update the completion count of that specific activity, and have it update my view model. The method I tried above probably doesn't make sense and obviously doesn't work. I've just left it to show what I tried.
Thanks for any assistance or insight.
The problem is here:
struct ActivityDetailView: View {
#State var activity: Activity
...
This needs to be a #Binding in order for changes to be reflected back in the parent view. There's also no need to pass in the entire viewModel in - once you have the #Binding, you can get rid of it.
// detail view
struct ActivityDetailView: View {
#Binding var activity: Activity /// here!
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
}
Text("\(activity.completeDescription)")
}
}
}
But how do you get the Binding? If you're using iOS 15, you can directly loop over $viewModel.activities:
/// here!
ForEach($viewModel.activities, id: \.id) { $activity in
NavigationLink(destination: ActivityDetailView(activity: $activity)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
And for iOS 14 or below, you'll need to loop over indices instead. But it works.
/// from https://stackoverflow.com/a/66944424/14351818
ForEach(Array(zip(viewModel.activities.indices, viewModel.activities)), id: \.1.id) { (index, activity) in
NavigationLink(destination: ActivityDetailView(activity: $viewModel.activities[index])) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}
You are changing and increment the value of tempActivity so it will not affect the main array or data source.
You can add one update function inside the view model and call from view.
The view model is responsible for this updation.
class ActivityViewModel: ObservableObject {
#Published var activities: [Activity] = []
func updateCompletionCount(for id: UUID) {
if let index = activities.firstIndex(where: {$0.id == id}) {
self.activities[index].completions += 1
}
}
}
struct ActivityDetailView: View {
var activity: Activity
var viewModel: ActivityViewModel
var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
updateCompletionCount()
}
Text("\(activity.completeDescription)")
}
}
func updateCompletionCount() {
self.viewModel.updateCompletionCount(for: activity.id)
}
}
Not needed #State or #ObservedObject for details view if don't have further action.

SwiftUI Navigation: How to switch detail view to a different item?

I'm struggling implementing the following navigation behavior:
From a list the user can select an item which triggers a detail view for this item. On this detail view there is an "Add" button in the navigation bar which opens a modal sheet for adding an other item.
Up to this point, everything works as expected.
But after adding the item, I want the detail view to show the new item. I tried to set the list selection to the id of the new item. This triggers the detail view to disappear, the list selects the new item and show the details for a very short time, then the detail view disappears again and the list is shown.
I've tried adding a bridged binding and let the list view not set the selection to nil, this solves the issue at first, but then the "Back" button isn't working anymore.
Please note: I want the "Add" button on the detail view and not on the list view as you would expect it.
Here's the full code to test:
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject private var state = AppState.shared
var body: some View {
NavigationView {
List(state.items) {item in
NavigationLink(destination: DetailView(item: item), tag: item.id, selection: self.$state.selectedId) {
Text(item.title)
}
}
.navigationBarTitle("Items")
}
}
}
struct DetailView: View {
var item: Item
#State private var showForm = false
var body: some View {
Text(item.title)
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
})
.sheet(isPresented: $showForm, content: { FormView() })
}
}
struct FormView: View {
#Environment(\.presentationMode) private var presentationMode
private var state = AppState.shared
var body: some View {
Button("Add") {
let id = self.state.items.count + 1
self.state.items.append(Item(id: id, title: "Item \(id)"))
self.presentationMode.wrappedValue.dismiss()
self.state.selectedId = id
}
}
}
class AppState: ObservableObject {
static var shared = AppState()
#Published var items: [Item] = [Item(id: 1, title: "Item 1")]
#Published var selectedId: Int?
}
struct Item: Identifiable {
var id: Int
var title: String
}
In your scenario it is needed to make navigation link destination independent, so it want be reactivated/invalidated when destination changed.
Here is possible approach. Tested with Xcode 11.7 / iOS 13.7
Updated code only:
struct ContentView: View {
#ObservedObject private var state = AppState.shared
#State private var isActive = false
var body: some View {
NavigationView {
List(state.items) {item in
HStack {
Button(item.title) {
self.state.selectedId = item.id
self.isActive = true
}
Spacer()
Image(systemName: "chevron.right").opacity(0.5)
}
}
.navigationBarTitle("Items")
.background(NavigationLink(destination: DetailView(), isActive: $isActive) { EmptyView() })
}
}
}
struct DetailView: View {
#ObservedObject private var state = AppState.shared
#State private var showForm = false
#State private var fix = UUID() // << fix for known issue with bar button misaligned after sheet
var body: some View {
Text(state.selectedId != nil ? state.items[state.selectedId! - 1].title : "")
.navigationBarItems(trailing: Button("Add") {
self.showForm = true
}.id(fix))
.sheet(isPresented: $showForm, onDismiss: { self.fix = UUID() }, content: { FormView() })
}
}

SwiftUI: assign binding variable from json parsed object

I am trying to assign a value I fetch and parse from JSON to another view.
struct ContentView: View {
#State private var showAlert = false
#State private var showAbout = false
#State private var showModal = false
#State private var title = "hi"
#State private var isCodeSelectorPresented = false
#ObservedObject var fetch = FetchNovitads()
var body: some View {
VStack {
NavigationView {
List(fetch.Novitadss) { Novitads in
VStack(alignment: .leading) {
// 3.
Text(Novitads.name!.de)
.platformFont()
.fontWeight(.black)
Text(Novitads.textTeaser.de)
.platformFont()
.fontWeight(.medium)
.onTapGesture {
self.showModal.toggle()
// 3.
}.sheet(isPresented: self.$showModal) {
ModalView(showModal: self.$showModal,
title: self.$title)
}
In this sample code the title (defined as "hi") is passed correctly.
What I want to do however is to assign the value of Novitads.name!.de to the title variable so that I can use it in the modal view.
I just display self.$title in the ModalView Text("(String(title))")
Then you don't need binding here and pass value directly, like
ModalView(showModal: self.$showModal, title: Novitads.name!.de)
and your ModalView declaration be as
struct ModalView: View {
#Binding showModal: Bool
let title: String
/// .. all other your code
}
Note: #State private var title = "hi" can be removed at all
try assigning the title like this:
struct ContentView: View {
struct ContentView: View {
#State private var showAlert = false
#State private var showAbout = false
#State private var showModal = false
#State private var title = "hi"
#State private var isCodeSelectorPresented = false
#ObservedObject var fetch = FetchNovitads()
var body: some View {
VStack {
NavigationView {
List(fetch.Novitadss) { Novitads in
VStack(alignment: .leading) {
// 3.
Text(Novitads.name!.de)
.platformFont()
.fontWeight(.black)
Text(Novitads.textTeaser.de)
.platformFont()
.fontWeight(.medium)
.onTapGesture {
self.showModal.toggle()
// 3.
}.sheet(isPresented: self.$showModal) {
ModalView(showModal: self.$showModal, title: self.$title)
}
}
}
}
}.onAppear(perform:{ self.title = self.fetch.Novitads.name!.de })
}

How do I change the bool value of an item that comes from a struct and hence update a checklist?

Background
I am trying to build a list with a checkmark/tick box next to it. A struct is used to create the "data" for each item. This is then passed on to a class which holds an array of the items created by the struct. From here I used the observable object protocol and passed the class into a list.
Objective
I would like to be able to individually mark each item as completed when it is done.
Current Analysis
I know the image switches when I manually change the 'completed' value from false to true.
I also tested the onTapAction just to be sure it is working.
I think the problem lies in "self.one.completed.toggle()" or the binding or something I am unaware of.
struct One: Identifiable, Codable {
let id = UUID()
var item: String
var completed:Bool = false
}
class OneList: ObservableObject{
#Published var items1 = [One]()
struct ContentView: View {
#ObservedObject var itemss1 = OneList()
#ObservedObject var itemss2 = TwoList()
#ObservedObject var itemss3 = ThreeList()
#ObservedObject var itemss4 = FourList()
#State private var showingAdditem: Bool = false
#Binding var one:One
var body: some View {
NavigationView{
ZStack{
List{
Section(header: Text("Vital")){
ForEach(itemss1.items1){ item in
HStack{
Image(systemName: self.one.completed ? "checkmark.circle":"circle")
.onTapGesture {
self.one.completed.toggle()
}
Text(item.item)}
P.S. I am relatively new to Swift and Stack overflow so any other suggestions would be appreciated
In my other answer I achieved something like this with ObservableObject protocol for needed object and then playing with EnvironmentObject. Actually I didn't try to do this with other wrappers. Here is the code, where you can see switching images:
import SwiftUI
class One: Identifiable, ObservableObject { // ObservableObject requires class
let id: UUID
var item: String = "[undefined]"
#Published var completed: Bool = false // this will affect the UI
init(item: String, completed: Bool) {
id = UUID()
self.item = item
self.completed = completed
}
}
class OneList: ObservableObject{
#Published var items = [One(item: "first", completed: false),
One(item: "second", completed: false),
One(item: "third", completed: false)]
}
struct CheckboxList: View {
#EnvironmentObject var itemList: OneList
var body: some View {
List {
Section(header: Text("Vital")) {
ForEach(itemList.items.indices) { index in
VitalRow()
.environmentObject(self.itemList.items[index])
.onTapGesture {
self.itemList.items[index].completed.toggle()
}
}
}
}
}
}
struct VitalRow: View {
#EnvironmentObject var item: One
var body: some View {
HStack{
Image(systemName: item.completed ? "checkmark.circle" : "circle")
Text("\(item.item)")
}
}
}
struct CheckboxList_Previews: PreviewProvider {
static var previews: some View {
CheckboxList().environmentObject(OneList())
}
}