Swiftui navigationLink macOS default/selected state - swift

I build a macOS app in swiftui
i try to create a listview where the first item is preselected. i tried it with the 'selected' state of the navigationLink but it didn't work.
Im pretty much clueless and hope you guys can help me.
The code for creating this list view looks like this.
//personList
struct PersonList: View {
var body: some View {
NavigationView
{
List(personData) { person in
NavigationLink(destination: PersonDetail(person: person))
{
PersonRow(person: person)
}
}.frame(minWidth: 300, maxWidth: 300)
}
}
}
(Other views at the bottom)
This is the normal View when i open the app.
When i click on an item its open like this. Thats the state i want as default opening state when i render this view.
The Code for this view looks like this:
//PersonRow
struct PersonRow: View {
//variables definied
var person: Person
var body: some View {
HStack
{
person.image.resizable().frame(width:50, height:50)
.cornerRadius(25)
.padding(5)
VStack (alignment: .leading)
{
Text(person.firstName + " " + person.lastName)
.fontWeight(.bold)
.padding(5)
Text(person.nickname)
.padding(5)
}
Spacer()
}
}
}
//personDetail
struct PersonDetail: View {
var person : Person
var body: some View {
VStack
{
HStack
{
VStack
{
CircleImage(image: person.image)
Text(person.firstName + " " + person.lastName)
.font(.title)
Text("Turtle Rock")
.font(.subheadline)
}
Spacer()
Text("Subtitle")
.font(.subheadline)
}
Spacer()
}
.padding()
}
}
Thanks in advance!

working example. See how selection is initialized
import SwiftUI
struct Detail: View {
let i: Int
var body: some View {
Text("\(self.i)").font(.system(size: 150)).frame(maxWidth: .infinity)
}
}
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack(alignment: .top) {
NavigationView {
List {
ForEach(0 ..< 10) { (i) in
NavigationLink(destination: Detail(i: i), tag: i, selection: self.$selection) {
VStack {
Text("Row \(i)")
Divider()
}
}
}.onAppear {
if self.selection != nil {
self.selection = 0
}
}
}.frame(width: 100)
}
}.background(Color.init(NSColor.controlBackgroundColor))
}
}
screenshot

You can define a binding to the selected row and used a List reading this selection. You then initialise the selection to the first person in your person array.
Note that on macOS you do not use NavigationLink, instead you conditionally show the detail view with an if statement inside your NavigationView.
If person is not Identifiable you should add an id: \.self in the loop. This ressembles to:
struct PersonList: View {
#Binding var selectedPerson: Person?
var body: some View {
List(persons, id: \.self, selection: $selectedPerson) { person in // persons is an array of persons
PersonRow(person: person).tag(person)
}
}
}
Then in your main window:
struct ContentView: View {
// First cell will be highlighted and selected
#State private var selectedPerson: Person? = person[0]
var body: some View {
NavigationView {
PersonList(selectedPerson: $selectedPerson)
if selectedPerson != nil {
PersonDetail(person: person!)
}
}
}
}
Your struct person should be Hashable in order to be tagged in the list. If your type is simple enough, adding Hashable conformance should be sufficient:
struct Person: Hashable {
var name: String
// ...
}
There is a nice tutorial using the same principle here if you want a more complete example.

Thanks to this discussion, as a MacOS Beginner, I managed a very basic NavigationView with a list containing two NavigationLinks to choose between two views. I made it very basic to better understand. It might help other beginners.
At start up it will be the first view that will be displayed.
Just modify in ContentView.swift, self.selection = 0 by self.selection = 1 to start with the second view.
FirstView.swift
import SwiftUI
struct FirstView: View {
var body: some View {
Text("(1) Hello, I am the first view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
import SwiftUI
struct SecondView: View {
var body: some View {
Text("(2) Hello, I am the second View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var selection: Int?
var body: some View {
HStack() {
NavigationView {
List () {
NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
Text("Click Me To Display The First View")
} // End Navigation Link
NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
Text("Click Me To Display The Second View")
} // End Navigation Link
} // End list
.frame(minWidth: 350, maxWidth: 350)
.onAppear {
self.selection = 0
}
} // End NavigationView
.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
} // End HStack
} // End some View
} // End ContentView
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Result:

import SwiftUI
struct User: Identifiable {
let id: Int
let name: String
}
struct ContentView: View {
#State private var users: [User] = (1...10).map { User(id: $0, name: "user \($0)")}
#State private var selection: User.ID?
var body: some View {
NavigationView {
List(users) { user in
NavigationLink(tag: user.id, selection: $selection) {
Text("\(user.name)'s DetailView")
} label: {
Text(user.name)
}
}
Text("Select one")
}
.onAppear {
if let selection = users.first?.ID {
self.selection = selection
}
}
}
}
You can use make the default selection using onAppear (see above).

Related

Using EnvironmentObject and Binding in same struct? [Beginner]

I have managed to set up a Navigationstack that I use programatically to insert/remove views. In my FindOrderView, I want to carry over the variable "ordernumber" to my LoadingScreen(View) using Binding, but I get an error saying my LoadingScreen does not conform to type 'Equatable'. What could I be doing wrong, and is there any other efficient way to get this working?
I'm a complete beginner with SwiftUi but have searched for hours before posting this here. Thankful for any help.
NavigationRouter (Class used to control layers of views)
import Foundation
import SwiftUI
enum Route: Hashable {
case ContentView
case View1
}
final class NavigationRouter: ObservableObject {
#Published var navigationPath = NavigationPath()
func pushView(route: Route) {
navigationPath.append(route)
}
func popToRootView() {
navigationPath = .init()
}
func popToSpecificView(k: Int) {
navigationPath.removeLast(k)
}
}
Mainapp (App starts here but instantly jumps to MainScreenView())
import SwiftUI
#main
struct AvhamtningApp: App {
#StateObject var router = NavigationRouter()
var body: some Scene {
WindowGroup {
NavigationStack(path: $router.navigationPath) {
MainScreenView()
.navigationDestination(for: Route.self) { route in
switch route {
case .ContentView:
MainScreenView()
case .View1:
FindOrderView()
}
}
}.environmentObject(router)
}
}
}
MainScreenView (View with button called "Get order" that carries you over to View1 (FindOrderView))
import SwiftUI
struct MainScreenView: View {
#EnvironmentObject var router: NavigationRouter
var body: some View {
VStack() {
Text("Testing")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.yellow)
.padding(.top, 50)
Spacer()
PrimaryButton(text: "Get Order")
.onTapGesture {
router.pushView(route: .View1)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.white)).ignoresSafeArea(.all)
}
}
struct MainScreenView_Previews: PreviewProvider {
static var previews: some View {
MainScreenView()
}
}
FindOrderView (View with Keypad, if enter 7 digits then should carry you over to Loadingscreen() with the variable "ordernumber" as binding)
import SwiftUI
var list = [["1","2","3"],["4","5","6"],["7","8","9"],["","0","⌫"]]
struct FindOrderView: View {
#State private var ordernumber: String = ""
#EnvironmentObject var router: NavigationRouter
var body: some View {
VStack(spacing: 25) {
Text(ordernumber)
.font(.largeTitle)
.foregroundColor(.black)
.onChange(of: ordernumber) { newValue in
if ordernumber.count >= 7 {
router.navigationPath.append(LoadingScreen(ordernumber: $ordernumber))
}
}
Spacer()
ForEach(list.indices, id: \.self) { listindex in
HStack{
ForEach(list[listindex], id: \.self) { variableindex in
Text(variableindex)
.foregroundColor(.blue)
.font(.system(size: 60))
.onTapGesture(perform: {
if variableindex == "⌫" && !ordernumber.isEmpty {
ordernumber.removeLast()
}
else if ordernumber.count >= 7 {return}
else if variableindex == "⌫" {
()
}
else {
ordernumber += variableindex
}})
.frame(maxWidth: .infinity)
}
}
}
}.background(.white)
}
}
struct FindOrderView_Previews: PreviewProvider {
static var previews: some View {
FindOrderView()
}
}
LoadingScreen (This is where I get the error "Type 'LoadingScreen' does not conform to protocol 'Equatable'")
import SwiftUI
struct LoadingScreen: Hashable, View {
#EnvironmentObject var router: NavigationRouter
#Binding var ordernumber: String
var body: some View {
ZStack{
Text("Loading...")
.font(Font.custom("Baskerville-Bold", size: 50))
.foregroundColor(.orange)
.padding(.bottom, 200)
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .orange))
.scaleEffect(2)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.white)).ignoresSafeArea(.all)
}
}
struct LoadingScreen_Previews: PreviewProvider {
static var previews: some View {
LoadingScreen(ordernumber: .constant("constant"))
}
}
So the app starts on the MainScreenView because it's set as the root of the NavigationStack.
Your issue with navigation is that you're trying to send the view as a navigation destination value, but you need to send the route value, and switch on the navigationDestination to generate the view to be pushed.
So in your Route enum, add a route for loading:
case loading(orderNumber: String)
Then in your NavigationLink send that new route:
NavigationLink(value: Route.loadingScreen(orderNumber: orderNumber)
Then in your navigationDestination view modifier:
.navigationDestination(for: Route.self) { route in
switch route {
case .ContentView:
MainScreenView()
case .View1:
FindOrderView()
case .loading(let orderNumber):
LoadingScreen(orderNumber: orderNumber)
.environmentObject(router)
}
}
Hope this helps!

SwiftUI - Convert a scrollable TabBar into a still TabBar

currently having an issue with a tutorial I followed which creates an underlined tab bar with SwiftUI. At the current stage, the tab bar scrolls with a scrollview when a view is changed, however, instead of this happening I want the buttons to remain idle in their place as a normal (still functioning) underlined tab bar. This is currently what it looks like when a view is swiped across:
Notice how when a tab changes, instead of remaining still within the tab bar, the labels scroll over. I have tried removing the scroll view of the tab bar but just can't seem to work out how to get it to remain fixed within the screen.
The code from the tutorial looks like this:
import SwiftUI
struct ContentView: View {
#State var currentTab: Int = 0
var body: some View {
ZStack(alignment: .top) {
TabView(selection: self.$currentTab) {
View1().tag(0)
View2().tag(1)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.all)
TabBarView(currentTab: self.$currentTab)
}
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["View 1", "View 2"]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.padding(.horizontal)
}
.background(Color.white)
.frame(height: 80)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
Color.black
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.buttonStyle(.plain)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Help resolving this would be greatly appreciated! Thanks!!
Try this code. This should sole your problem. The main issue was that you used a ZStack instead of a VStack. After that, just fiddle around with the spacing and positioning of the elements you build in your VStack.
Let me know if it creates the result you want.
import SwiftUI
struct ContentView: View {
#State var currentTab: Int = 0
var body: some View {
VStack(alignment: .center, spacing: 0) {
TabBarView(currentTab: self.$currentTab)
TabView(selection: self.$currentTab) {
Text("This is view 1").tag(0)
Text("This is view 2").tag(1)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.background(Color.secondary) // <<<< Remove
}
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["View 1", "View 2"]
var body: some View {
HStack(spacing: 20) {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.padding(.horizontal)
.background(Color.orange) // <<<< Remove
.frame(height: 80)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
Color.black
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.buttonStyle(.plain)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Getting Values from own PickerView

I'm new to Swift and I'm currently developing my own Timer Application for practice purposes. (I do it without storyboard)
Now I have the Problem that i made a View called "TimePickerView" (Code below), where I created my own Picker. Then I use that TimePickerView in another part of my Application with other Views (in a View). In that View I want to pick my time but I don't know how i can get the Values of the Picker (The Picker works by the way)
This is my TimePickerView
import SwiftUI
struct TimePickerView: View {
#State private var selectedTimeIndexSecond = 0
#State private var selectedTimeIndexMinutes = 0
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(minutes[selectedTimeIndexMinutes]) min : \(seconds[selectedTimeIndexSecond]) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
func getValues() -> (Int, Int) {
return (self.minutes[selectedTimeIndexMinutes] ,self.seconds[selectedTimeIndexSecond])
}
}
and that is the View I want to use my Picker, but I don't know how I get those values from the Picker:
struct SetTimerView: View {
#State var timepicker = TimePickerView()
var body: some View {
NavigationView{
VStack{
//Select the time
timepicker
//Timer variables (This doesn't work)
var timerTime = timepicker.getValues()
var minutes = timerTime.0
var seconds = timerTime.1
Spacer()
let valid : Bool = isValid(timerTime: minutes+seconds)
//Confirm the time
NavigationLink(
destination:
getRightView(
validBool: valid,
timerTime: minutes*60 + seconds),
label: {
ConfirmButtonView(buttonText: "Confirm")
});
Spacer()
}
}
}
func isValid(timerTime : Int) -> Bool {
if (timerTime == 0) {
return false
} else {
return true
}
}
#ViewBuilder func getRightView(validBool : Bool, timerTime : Int) -> some View{
if (validBool == true) {
TimerView(userTime: CGFloat(timerTime), name: "David", isActive: true)
} else {
UnvalidTimeView()
}
}
}
I think main problem is misunderstanding conceptions between data and views.
At first you need a model witch will override your data (create it in separate swift file):
import Foundation
class Time: ObservableObject {
#Published var selectedTimeIndexMinutes: Int = 0
#Published var selectedTimeIndexSecond: Int = 0
}
Pay attention on ObservableObject so that swiftUI can easily detect changes to it that trigger any active views to redraw.
Next I try to change the value of the model in the view
import SwiftUI
struct TimePickerView: View {
#EnvironmentObject var timeData: Time
#State private var seconds : [Int] = Array(0...59)
#State private var minutes : [Int] = Array(0...59)
var body: some View {
VStack{
Text("Select Your Time")
HStack{
//minutes-Picker
Picker("select time", selection: $timeData.selectedTimeIndexMinutes, content: {
ForEach(0..<minutes.count, content: {
index in
Text("\(minutes[index]) min").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
//seconds-Picker
Picker("select time", selection: $timeData.selectedTimeIndexSecond, content: {
ForEach(0..<seconds.count, content: {
index in
Text("\(seconds[index]) sec").tag(index)
})
})
.padding()
.frame(width: 120)
.clipped()
Spacer()
}
Text("You picked the time")
.multilineTextAlignment(.center)
.font(.title2)
.padding()
Text("\(timeData.selectedTimeIndexMinutes) min : \(timeData.selectedTimeIndexSecond) sec")
.font(.title)
.bold()
.padding(.top, -14.0)
}
}
}
struct TimePickerView_Previews: PreviewProvider {
static var previews: some View {
TimePickerView()
.environmentObject(Time())
}
}
Like you can see I don't using #Blinding, instead of it I connecting our Model with a View
On the next view I can see changes, I created a new one because your example have view that don't indicated here...
import SwiftUI
struct ReuseDataFromPicker: View {
#EnvironmentObject var timeData: Time
var body: some View {
VStack{
Text("You selected")
Text("\(timeData.selectedTimeIndexMinutes) min and \(timeData.selectedTimeIndexSecond) sec")
}
}
}
struct ReuseDataFromPicker_Previews: PreviewProvider {
static var previews: some View {
ReuseDataFromPicker()
.environmentObject(Time())
}
}
And collect all in a Content View
struct ContentView: View {
var body: some View {
TabView {
TimePickerView()
.tabItem {Label("Set Timer", systemImage: "clock.arrow.2.circlepath")}
ReuseDataFromPicker()
.tabItem {Label("Show Timer", systemImage: "hourglass")}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Time())
}
}
Like that you can easily change or reuse your data on any other views

Keyboard bug when tapping on TextField in SwiftUI

I have a Main View that contains multiple Views and picks one based on the value change of a variable in a ObservabledObject, which change on buttons press.
The problem comes when I select a View that contains input fields, in that case, when I tap on a TextField, instead of showing the keyboard, it takes me back to the Homepage View.
It only happens on devices, not on simulators.
Though, it works if you set that specific view (the one with TextField) as Main View (the first case in the Switch).
Here's the ObservableObject Code:
class Watcher : ObservableObject {
#Published var currentView: String = "home"
}
Main View:
import SwiftUI
struct MainView : View {
var body: some View {
GeometryReader { geometry in
ZStack (alignment: .leading){
HomepageView()
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}
struct HomepageView : View {
#ObservedObject var watcher = Watcher()
init(){
UITabBar.appearance().isHidden = true
JSONHandler.fetchData(webService: "https://myWebsite.com/app/request.php") {
print("loaded")
}
}
var body: some View {
ZStack {
VStack {
TabView {
switch self.watcher.currentView {
case "home": //STARTING CASE
NavigationView {
DailyMenuView()
.navigationTitle("Recipes APP")
.navigationBarTitleDisplayMode(.inline)
}
.opacity(self.watcher.currentView == "newRecipe" ? 0 : 1)
case "innerMenu":
InnerMenuView(watcher: watcher)
case "members":
MembersView(watcher: watcher)
case "recipe":
RecipeView(watcher: watcher)
case "newRecipe": //TextField View
TestView()
default:
Text("Error")
}
}
}
VStack {
Spacer()
MenuTabView(watcher: watcher)
}
.edgesIgnoringSafeArea(.bottom)
}
.environment(\.colorScheme, self.watcher.currentView == "home" ? .dark : .light)
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
And last, the View containing TextField:
import SwiftUI
struct TestView: View {
#State var text: String = ""
var body: some View {
VStack {
Text("TEST VIEW")
.font(.largeTitle)
.fontWeight(.bold)
Spacer()
TextField("Write here", text: $text)
.font(.title)
.padding()
.border(Color.red, width: 2)
Spacer()
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Hope you can help me solve this mess!
If you need more information, ask me.
You need to use tag and selection.
TabView(selection: $watcher.currentView) { // < === Here
switch self.watcher.currentView {
case "home": //STARTING CASE
NavigationView {
DailyMenuView()
.navigationTitle("Recipes APP")
.navigationBarTitleDisplayMode(.inline)
}
.opacity(self.watcher.currentView == "newRecipe" ? 0 : 1)
.tag("home") // < === Here
case "innerMenu":
InnerMenuView(watcher: watcher)
.tag("innerMenu") // < === Here
case "members":
MembersView(watcher: watcher)
.tag("members") // < === Here
case "recipe":
RecipeView(watcher: watcher)
.tag("recipe") // < === Here
case "newRecipe": //TextField View
TestView()
.tag("newRecipe") // < === Here
default:
Text("Error")
}
}
I solved moving:
#ObservedObject var watcher = Watcher()
Inside the Main View as it follows:
struct MainView : View {
#ObservedObject var watcher = Watcher()
var body: some View {
GeometryReader { geometry in
ZStack (alignment: .leading){
HomepageView()
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
}

How do I handle selection in NavigationView on macOS correctly?

I want to build a trivial macOS application with a sidebar and some contents according to the selection in the sidebar.
I have a MainView which contains a NavigationView with a SidebarListStyle. It contains a List with some NavigationLinks. These have a binding for a selection.
I would expect the following things to work:
When I start my application the value of the selection is ignored. Neither is there a highlight for the item in the sidebar nor a content in the detail pane.
When I manually select an item in the sidebar it should be possible to navigate via up/down arrow keys between the items. This does not work as the selection / highlight disappears.
When I update the value of the selection-binding it should highlight the item in the list which doesn't happen.
Here is my example implementation:
enum DetailContent: Int, CaseIterable {
case first, second, third
}
extension DetailContent: Identifiable {
var id: Int { rawValue }
}
class NavigationRouter: ObservableObject {
#Published var selection: DetailContent?
}
struct DetailView: View {
#State var content: DetailContent
#EnvironmentObject var navigationRouter: NavigationRouter
var body: some View {
VStack {
Text("\(content.rawValue)")
Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
Text("Take me anywhere")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#ObservedObject var navigationRouter = NavigationRouter()
#State var detailContent: DetailContent? = .first
var body: some View {
NavigationView {
List {
Section(header: Text("Section")) {
ForEach(DetailContent.allCases) { item in
NavigationLink(
destination: DetailView(content: item),
tag: item,
selection: self.$detailContent,
label: { Text("\(item.rawValue)") }
)
}
}
}
.frame(minWidth: 250, maxWidth: 350)
}
.environmentObject(navigationRouter)
.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(navigationRouter.$selection) { output in
self.detailContent = output
}
}
}
The EnvironmentObject is used to propagate the change from inside the DetailView. If there's a better solution I'm very happy to hear about it.
So the question remains:
What am I doing wrong that this happens?
I had some hope that with Xcode 11.5 Beta 1 this would go away but that's not the case.
After finding the tutorial from Apple it became clear that you don't use NavigiationLink on macOS. Instead you bind the list and add two views to NavigationView.
With these updates to MainView and DetailView my example works perfectly:
struct DetailView: View {
#Binding var content: DetailContent?
#EnvironmentObject var navigationRouter: NavigationRouter
var body: some View {
VStack {
Text("\(content?.rawValue ?? -1)")
Button(action: { self.navigationRouter.selection = DetailContent.allCases.randomElement()!}) {
Text("Take me anywhere")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#ObservedObject var navigationRouter = NavigationRouter()
#State var detailContent: DetailContent?
var body: some View {
NavigationView {
List(selection: $detailContent) {
Section(header: Text("Section")) {
ForEach(DetailContent.allCases) { item in
Text("\(item.rawValue)")
.tag(item)
}
}
}
.frame(minWidth: 250, maxWidth: 350)
.listStyle(SidebarListStyle())
DetailView(content: $detailContent)
}
.environmentObject(navigationRouter)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onReceive(navigationRouter.$selection) { output in
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
self.detailContent = output
}
}
}
}