SwiftUI - Nested links within NavigationStack inside a NavigationSplitView not working - swift

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

Related

Configure preview for ObservedObject and State object

So I am still learning Swift and for some reason, I am having the hardest trouble with the previews and how to configure them.
So I have the following code:
struct MainView: View {
// The app's model that the containing scene passes in
#ObservedObject var model: MainViewModel
#State var activeTab = 0
var body: some View {
VStack {
TabView(selection: $activeTab) {
Group {
WorldView(model: model, activeTab: $activeTab)
.tabItem {
Label(Tabs.explore.rawValue, systemImage: Tabs.explore.icon)
.environment(\.symbolVariants, .none)
}
.tag(0)
ListView(model: model, activeTab: $activeTab)
.tabItem {
Label(Tabs.list.rawValue, systemImage: Tabs.list.icon)
.environment(\.symbolVariants, .none)
}
.tag(1)
FavoritesView(activeTab: $activeTab)
.tabItem {
Label(Tabs.favorite.rawValue, systemImage: Tabs.favorite.icon)
.environment(\.symbolVariants, .none)
}
.tag(2)
ProfileView(model: model, activeTab: $activeTab)
.tabItem {
Label(Tabs.profile.rawValue, systemImage: Tabs.profile.icon)
.environment(\.symbolVariants, .none)
}
.tag(3)
}
.environmentObject(model)
}
.tint(.accentColor)
.onChange(of: activeTab, perform: { value in
log.info("\n 🟢: (MainView: 46) - User has selected tab: \(value).")
print("")
})
}
.onAppear() {
model.fetchPlaces()
}
}
}
Then I have the preview, as such:
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(model: model, activeTab: activeTab)
}
}
I am getting the two errors on the previews:
Cannot find 'activeTab' in scope
Cannot find 'model' in scope
If I define it as such:
struct MainView_Previews: PreviewProvider {
#ObservedObject var model: MainViewModel
#State var activeTab = 0
static var previews: some View {
MainView(model: model, activeTab: activeTab)
}
}
I get the following errors:
Instance member 'activeTab' cannot be used on type 'MainView_Previews'
Instance member 'model' cannot be used on type 'MainView_Previews'
Does anyone know how I can configure the preview so that it works properly and doesn't crash?
This happens because you are either passing in non existing paremeters in Previews, or because you cannot initialize objects in previews. Instead, do this:
struct MyExamplePreviews: PreviewProvider{
static var previews: some View {
MainView(model: MainViewModel(), activeTab: 0)
}
}
This will allow you to preview the UI. What this does:
Creates a new model that is passed in at the top level - again, you cannot create this anywhere but right here in the previews
Makes it so that activeTab will be set to 0 - you can have multiple preview devices with different tabs if needed. See the docs for Previews to learn more.
I recommend the below approach, then you can use #EnvironmentObject var model: Model in any View that needs it and don't have to pass it into every View.
class Model: ObservableObject {
#Published var items: [Item] = []
static var shared = Model()
static var preview = Model(preview: true)
init(preview: Bool) {
if preview {
items = // set some test items
return
}
load()
}
fun load(){
// load items from disk
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Model.shared)
}
}
}
struct ContentView: View {
#EnvironmentObject var: model: Model
var body: some View {
NavigationStack {
List {
ForEach($model.items) $item in {
TextField("Title" text: $item.title)
}
.navigationTitle("Items")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Model.preview)
}
}

Propertly break down and pass data between views

So I'm still learning Swift and I wanted to cleanup some code and break down views, but I can't seem to figure out how to pass data between views, so I wanted to reach out and check with others.
So let's say that I have MainView() which previously had this:
struct MainView: View {
#ObservedObject var model: MainViewModel
if let item = model.selectedItem {
HStack(alignment: .center, spacing: 3) {
Text(item.title)
}
}
}
Now I created a SecondView() and changed the MainView() content to this:
struct MainView: View {
#ObservedObject var model: MainViewModel
if let item = model.selectedItem {
SecondView(item: item)
}
}
Inside SecondView(), how can I access the item data so that I can use item.title inside SecondView() now?
In order to pass item to SecondView, declare item as a let property and then when you call it with SecondView(item: item), SecondView can refer to item.title.
Here is a complete example expanding on your code:
import SwiftUI
struct Item {
let title = "Test Title"
}
class MainViewModel: ObservableObject {
#Published var selectedItem: Item? = Item()
}
struct MainView: View {
#ObservedObject var model: MainViewModel
var body: some View {
if let item = model.selectedItem {
SecondView(item: item)
}
}
}
struct SecondView: View {
let item: Item
var body: some View {
Text(item.title)
}
}
struct ContentView: View {
#StateObject private var model = MainViewModel()
var body: some View {
MainView(model: model)
}
}

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

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.

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.

How to add something to a favorite list in SwiftUI?

I'm trying to create a Favorite list where I can add different items but it doesn't work. I made a simple code to show you what's going on.
// BookData gets data from Json
struct BookData: Codable {
var titolo: String
var descrizione: String
}
class FavoriteItems: ObservableObject {
#Published var favItems: [String] = []
}
struct ContentView: View {
#ObservedObject var bookData = BookDataLoader()
#ObservedObject var favoriteItems = FavoriteItems()
var body: some View {
NavigationView {
List {
NavigationLink(destination: FavoriteView()) {
Text("Go to favorites")
}
ForEach(0 ..< bookData.booksData.count) { num in
HStack {
Text("\(self.bookData.booksData[num].titolo)")
Button(action: {
self.favoriteItems.favItems.append(self.bookData.booksData[num].titolo)
}) {
Image(systemName: "heart")
}
}
}
}
}
}
}
struct FavoriteView: View {
#ObservedObject var favoriteItems = FavoriteItems()
var body: some View {
List {
ForEach (0 ..< favoriteItems.favItems.count) { num in
Text("\(self.favoriteItems.favItems[num])")
}
}
}
}
When I launch the app I can go to the Favorite View but after adding an Item I cannot.
My aim is to add an Item to Favorites and be able to save it once I close the app
The view model favoriteItems inside ContentView needs to be passed into FavoriteView because you need a reference of favoriteItems to reload FavoriteView when you add a new data.
Change to
NavigationView(destination: FavoriteView(favoriteItems: favoriteItems)) #ObservedObject var favoriteItems: FavoriteItems
It will be fine.
Thanks, X_X