My published variables in my view Model get reset to their default values when i run the code - swift

I have been having problems with updating a published variable in my model, so I tried to replicate the problem with a very basic and simple set of files/codes. So basically in NavLink view, there is a navigation link, which when clicked, it updates the published variable in ListRepository model by giving it a string value of "yes", prints it to the console then navigates to its destination which is called ContentView view. The problem is in ContentView, I tried to print the data contained in the published variable called selectedFolderId hoping it will print "yes", but i noticed that instead of printing the value that was set in NavLink view, it instead printed the default value of "", which was not what was set in NavLink view. Please can anyone explain the reason for this behaviour and explain to me how it can fix this as i am very new in swift ui. That will mean alot.
Please find the supporting files below:
import SwiftUI
struct NavLink: View {
#StateObject var listRepository = ListRepository()
var body: some View {
NavigationView{
ScrollView {
NavigationLink("Hello world", destination: ContentView(listRepository: listRepository))
Text("Player 1")
Text("Player 2")
Text("Player 3")
}
.simultaneousGesture(TapGesture().onEnded{
listRepository.selectedFolderId = "yes"
listRepository.md()
})
.navigationTitle("Players")
}
}
}
struct NavLink_Previews: PreviewProvider {
static var previews: some View {
NavLink()
}
}
import Foundation
class ListRepository: ObservableObject {
#Published var selectedFolderId = ""
func md(){
print("=====")
print(self.selectedFolderId)
print("======")
}
}
import SwiftUI
struct ContentView: View {
#ObservedObject var taskListVM = ShoppingListItemsViewModel()
#ObservedObject var listRepository:ListRepository
var body: some View {
VStack{
Text("content 1")
Text("content 2")
Text("content 3")
}
.onAppear{
taskListVM.addTask()
print("========")
print(listRepository.selectedFolderId)
print("========")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ShoppingListItemsViewModel: ObservableObject {
#Published var listRepository = ListRepository()
#Published var taskCellViewModels = [ShoppingListItemCellViewModel]()
private var cancellables = Set<AnyCancellable>()
init() {
listRepository.$tasks
.map { lists in
lists.map { list in
ShoppingListItemCellViewModel(task: list)
}
}
.assign(to: \.taskCellViewModels, on: self)
.store(in: &cancellables)
}
func addTask() {
listRepository.addTask(task)
}
}

This is a common issue when you first deal with data flow in an app. The problem is straightforward. In your 'NavLink' view you are creating one version of ListRepository, and in ContentView you create a separate and different version of ListRepository. What you need to do is pass the ListRepository created in NavLink into ContentView when you call it. Here is one example as to how:
struct NavLink: View {
#StateObject var listRepository = ListRepository() // Create as StateObject, not ObservedObject
var body: some View {
NavigationView{
ScrollView {
NavigationLink("Hello world", destination: ContentView(listRepository: listRepository)) // Pass it here
Text("Player 1")
Text("Player 2")
Text("Player 3")
}
.simultaneousGesture(TapGesture().onEnded{
listRepository.selectedFolderId = "yes"
listRepository.md()
})
.navigationTitle("Players")
}
}
}
struct ContentView: View {
#ObservedObject var listRepository: ListRepository // Do not create it here, just receive it
var body: some View {
VStack{
Text("content 1")
Text("content 2")
Text("content 3")
}
.onAppear{
print("========")
print(listRepository.selectedFolderId)
print("========")
}
}
}
You should also notice that I created ListRepository as a StateObject. The view that originally creates an ObservableObject must create it as a StateObject or you can get undesirable side effects.

Related

NavigationLink destination immediately disappear after loading

I'm quite new to Swift and I'm struggling with this implementation. I moved my code to playground so it's easier for you to debug if you copy/paste on your Xcode.
Basically once loading View3 the view immediately disappear and you are pushed back to View2.
I identify the issue to be with the categories array on the View2. If I separate the 3 categories in 3 NavigationLink (instead of looping) the code works just fine.
I just don't understand why this doesn't work and I'd love if you help me find out what's wrong with this implementation?
import UIKit
import Foundation
import SwiftUI
import PlaygroundSupport
struct Article: Identifiable {
var id = UUID()
var title : String
}
struct Category: Identifiable {
var id = UUID()
var name : String
}
class DataManager: ObservableObject{
#Published var articles : [Article] = []
func getArticles(){
let article = Article(title: "Just a test")
if !articles.contains(where: {$0.title == article.title}) {
articles.append(article)
}
}
func clearArticles(){
articles.removeAll()
}
}
struct View1: View{
#StateObject var dataManager = DataManager()
var body: some View {
NavigationView{
List(){
NavigationLink(destination: View2(), label: { Text("View 2") })
}
.navigationBarTitle("View 1")
.listStyle(.insetGrouped)
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(dataManager)
}
}
struct View2: View{
let categories = [
Category(name: "Category 1"),
Category(name: "Category 2"),
Category(name: "Category 3")
]
var body: some View {
List(categories){ category in
NavigationLink(destination: View3(category: category), label: { Text(category.name) })
}
.navigationBarTitle("View 2")
.navigationBarTitleDisplayMode(.inline)
}
}
struct View3: View{
#EnvironmentObject var dataManager: DataManager
let category: Category
var body: some View {
List(dataManager.articles) { article in
HStack {
Text(article.title)
}
}.task{
dataManager.getArticles()
}.onDisappear(){
dataManager.clearArticles()
}
.navigationTitle("View 3")
.navigationBarTitleDisplayMode(.inline)
}
}
PlaygroundPage.current.setLiveView(View1())
This is the resulting behaviour:
The Category id is not stable so every time the View2 is init, a new array with different ids is created so the ForEach is creating new NavigationLinks which are not active so the previously active one is gone and it reacts by popping it off the nav stack, try this:
struct Category: Identifiable {
var id: String {
name
}
var name : String
}
But it would be better if you put them in the data store object so you aren't creating them every time View2 is init.

SwiftUI - Nested links within NavigationStack inside a NavigationSplitView not working

I'm playing around with the new navigation API's offered in ipadOS16/macOS13, but having some trouble working out how to combine NavigationSplitView, NavigationStack and NavigationLink together on macOS 13 (Testing on a Macbook Pro M1). The same code does work properly on ipadOS.
I'm using a two-column NavigationSplitView. Within the 'detail' section I have a list of SampleModel1 instances wrapped in a NavigationStack. On the List I've applied navigationDestination's for both SampleModel1 and SampleModel2 instances.
When I select a SampleModel1 instance from the list, I navigate to a detailed view that itself contains a list of SampleModel2 instances. My intention is to navigate further into the NavigationStack when clicking on one of the SampleModel2 instances but unfortunately this doesn't seem to work. The SampleModel2 instances are selectable but no navigation is happening.
When I remove the NavigationSplitView completely, and only use the NavigationStack the problem does not arise, and i can successfully navigate to the SampleModel2 instances.
Here's my sample code:
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
enum NavItem {
case first
}
var body: some View {
NavigationSplitView {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
} detail: {
SampleListView()
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
#State var path = NavigationPath()
#State var selection: SampleModel1.ID? = nil
var body: some View {
NavigationStack(path: $path) {
List(SampleModel1.samples, selection: $selection) { model in
NavigationLink("\(model.id)", value: model)
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
var body: some View {
Text("Model 1 ID \(model.id)")
List (SampleModel2.samples) { model2 in
NavigationLink("\(model2.id)", value: model2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I removed this unclear ZStack and all works fine. Xcode 14b3 / iOS 16
// ZStack { // << this !!
SampleListView()
// }
Apple just releases macos13 beta 5 and they claimed this was resolved through feedback assistant, but unfortunately this doesn't seem to be the case.
I cross-posted this question on the apple developers forum and user nkalvi posted a workaround for this issue. I’ll post his example code here for future reference.
import SwiftUI
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
#State var path = NavigationPath()
enum NavItem: Hashable, Equatable {
case first
}
var body: some View {
NavigationSplitView {
List {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
}
} detail: {
SampleListView(path: $path)
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
// Get the selection from DetailView and append to path
// via .onChange
#State var selection2: SampleModel2? = nil
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
VStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel1.samples) { model in
NavigationLink("Model1: \(model.id)", value: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
.navigationTitle("navigationDestination(for: SampleModel2.self)")
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model, path: $path, selection2: $selection2)
.navigationTitle("navigationDestination(for: SampleModel1.self)")
}
.navigationTitle("First")
}
.onChange(of: selection2) { newValue in
path.append(newValue!)
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
#Binding var path: NavigationPath
#Binding var selection2: SampleModel2?
var body: some View {
NavigationStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel2.samples, selection: $selection2) { model2 in
NavigationLink("Model2: \(model2.id)", value: model2)
// This also works (without .onChange):
// Button(model2.id.uuidString) {
// path.append(model2)
// }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Use Swift ObservableObject to change view label when UserSettings change

I’ve created a small sample project in Swift Playgrounds to debug an issue I’ve encountered. This sample project contains the a primary ContentView with a single Text field and a button that opens Settings in a modal view.
When I open Settings and change the a setting via a picker, I would like to see the corresponding Text label change in my ContentView. In the current project, I’m using the #ObservableObject Type Alias to track the change, and I see that the setting changes correctly, but the view is not updated. If I restart the preview in Playgrounds, the view is updated with the changed setting. I would expect the Text label to change in real-time.
The code is as follows:
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var userSettings = UserSettings()
#State var isModal: Bool = false
var body: some View {
VStack {
Text("Setting: " + userSettings.pickerSetting)
.fontWeight(.semibold)
.font(.title)
Button(action: {
self.isModal = true
}) {
Image(systemName: "gear")
.font(.title)
}
.padding()
.foregroundColor(.white)
.background(Color.gray)
.cornerRadius(40)
.sheet(isPresented: $isModal, content: {
UserSettingsView()
})
.environmentObject(userSettings)
}
}
}
UserSettings.swift
import Foundation
class UserSettings: ObservableObject {
#Published var pickerSetting: String {
didSet {
UserDefaults.standard.set(pickerSetting, forKey: "pickerSetting")
}
}
public var pickerSettings = ["Setting 1", "Setting 2", "Setting 3"]
init() {
self.pickerSetting = UserDefaults.standard.object(forKey: "pickerSetting") as? String ?? "Setting 1"
}
}
UserSettingsView.swift
import SwiftUI
struct UserSettingsView: View {
#ObservedObject var userSettings = UserSettings()
var body: some View {
NavigationView {
Form {
Section(header: Text("")) {
Picker(selection: $userSettings.pickerSetting, label: Text("Picker Setting")) {
ForEach(userSettings.pickerSettings, id: \.self) { setting in
Text(setting)
}
}
}
}
.navigationBarTitle("Settings")
}
}
}
This happening because you have created two instances of UserSettings. One each in ContentView and UserSettingsView.
If you want to keep using .environmentObject(userSettings) the you need to use #EnvironmentObject var userSettings: UserSettings in UserSettingsView.
Otherwise you can drop the .environmentObject and use an #ObservedObject in UserSettingsView.

Clear SwiftUI list in NavigationView does not properly go back to default

The simple navigation demo below demonstrate an example of my issue:
A SwiftUI list inside a NavigationView is filled up with data from the data model Model.
The list items can be selected and the NavigationView is linking another view on the right (demonstrated here with destination)
There is the possibility to clear the data from the model - the SwiftUI list gets empty
The model data can be filled with new data at a later point in time
import SwiftUI
// Data model
class Model: ObservableObject {
// Example data
#Published var example: [String] = ["Data 1", "Data 2", "Data 3"]
#Published var selected: String?
}
// View
struct ContentView: View {
#ObservedObject var data: Model = Model()
var body: some View {
VStack {
// button to empty data set
Button(action: {
data.selected = nil
data.example.removeAll()
}) {
Text("Empty Example data")
}
NavigationView {
// data list
List {
ForEach(data.example, id: \.self) { element in
// navigation to "destination"
NavigationLink(destination: destination(element: element), tag: element, selection: $data.selected) {
Text(element)
}
}
}
// default view when nothing is selected
Text("Nothing selected")
}
}
}
func destination(element: String) -> some View {
return Text("\(element) selected")
}
}
What happens when I click the "Empty Example data" button is that the list will be properly cleared up. However, the selection is somehow persistent and the NavigationView will not jump back to the default view when nothing is selected:
I would expect the view Text("Nothing selected") being loaded.
Do I overlook something important?
When you change the data.example
in the button, the left panel changes because the List has changed.
However the right side do not, because no change has occurred there, only the List has changed.
The ForEach does not re-paint the "destination" views.
You have an "ObservedObject" and its purpose is to keep track of the changes, so using that
you can achieve what you want, like this:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class Model: ObservableObject {
#Published var example: [String] = ["Data 1", "Data 2", "Data 3"]
#Published var selected: String?
}
struct ContentView: View {
#StateObject var data: Model = Model()
var body: some View {
VStack {
Button(action: {
data.selected = nil
data.example.removeAll()
}) {
Text("Empty Example data")
}
NavigationView {
List {
ForEach(data.example, id: \.self) { element in
NavigationLink(destination: DestinationView(),
tag: element,
selection: $data.selected) {
Text(element)
}
}
}
Text("Nothing selected")
}.environmentObject(data)
}
}
}
struct DestinationView: View {
#EnvironmentObject var data: Model
var body: some View {
Text("\(data.selected ?? "")")
}
}

Is it possible to perform an action on NavigationLink tap?

I have a simple View showing a list of 3 items. When the user taps on an item, it navigates to the next view. This works fine. However, I would like to also perform an action (set a variable in a View Model) when a list item is tapped.
Is this possible? Here's the code:
import SwiftUI
struct SportSelectionView: View {
#EnvironmentObject var workoutSession: WorkoutManager
let sports = ["Swim", "Bike", "Run"]
var body: some View {
List(sports, id: \.self) { sport in
NavigationLink(destination: ContentView().environmentObject(workoutSession)) {
Text(sport)
}
}.onAppear() {
// Request HealthKit store authorization.
self.workoutSession.requestAuthorization()
}
}
}
struct DisciplineSelectionView_Previews: PreviewProvider {
static var previews: some View {
SportSelectionView().environmentObject(WorkoutManager())
}
}
The easiest way I've found to get around this issue is to add an .onAppear call to the destination view of the NavigationLink. Technically, the action will happen when the ContentView() appears and not when the NavigationLink is clicked.. but the difference will be milliseconds and probably irrelevant.
NavigationLink(destination:
ContentView()
.environmentObject(workoutSession)
.onAppear {
// add action here
}
)
Here's a solution that is a little different than the onAppear approach. By creating your own Binding for isActive in the NavigationLink, you can introduce a side effect when it's set. I've implemented it here all within the view, but I would probably do this in an ObservableObject if I were really putting it into practice:
struct ContentView: View {
#State var _navLinkActive = false
var navLinkBinding : Binding<Bool> {
Binding<Bool> { () -> Bool in
return _navLinkActive
} set: { (newValue) in
if newValue {
print("Side effect")
}
_navLinkActive = newValue
}
}
var body: some View {
NavigationView {
NavigationLink(
destination: Text("Dest"),
isActive: navLinkBinding,
label: {
Text("Navigate")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}