SwiftUI #State is updated only once when put inside NavigationView (macOS app) - swift

Problem
I've put a simple button that increments a counter inside NavigationView, but the application appears to update #State value only once; later clicks are ignored. Switching between NavigationLinks will cause the view to update, but why it does not automatically after the counter did change?
Facts
otherView works fine after I put it outside the Navigation View.
Code
import SwiftUI
struct ContentView: View {
#State var counter = 0
var otherView: some View {
Button(action: {
counter += 1
}, label: {
Text(String(counter))
}).frame(maxWidth: .infinity, maxHeight: .infinity).padding()
}
var body: some View {
HStack {
NavigationView {
List {
NavigationLink("otherview", destination: otherView)
}.listStyle(SidebarListStyle())
}
}.frame(minWidth: 500, minHeight: 400)
}
}

Try to move the otherView into a seperate SwiftUI view like this:
struct ButtonView: View {
#State var counter = 0
var body: some View {
Button(action: {
counter += 1
}, label: {
Text(String(counter))
}).frame(maxWidth: .infinity, maxHeight: .infinity).padding()
}
}
Then in your ContentView call it like this:
struct ContentView: View {
var body: some View {
HStack {
NavigationView {
List {
NavigationLink("otherview", destination: ButtonView())
}.listStyle(SidebarListStyle())
}
}.frame(minWidth: 500, minHeight: 400)
}
}

Related

NavigationViews in TabView displayed incorrectly

I'm using SwiftUI and want to build a paged TabView with two NavigationView pages I can switch between horizontally. The pictures below show how it's supposed to work:
Here is my code for above example:
struct ContentView: View {
var body: some View {
TabView {
Page1()
Page2()
}
.tabViewStyle(.page)
}}
struct Page1: View {
var body: some View {
NavigationView {
ScrollView {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
.navigationTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}}
struct Page2: View {
var body: some View {
NavigationView {
ScrollView {
Rectangle()
.fill(.blue)
.frame(width: 100, height: 100)
}
.navigationTitle("Page 2")
}
.navigationViewStyle(StackNavigationViewStyle())
}}
Unless there's another way to do it is important that the NavigationViews are inside the TabView so that if I'm scrolling up the navigation bar title switches to .inline like you can see below:
Also, I use the StackNavigationViewStyle() because without it the pages aren't shown when I first open the app before I switch back and forth between them.
StackNavigationViewStyle() solves this but still the problem is that when I open the app for the first time the rectangles are being placed incorrectly right at the top of the screen. I then have to switch to the second page and back to get them positioned correctly:
Does anyone have an idea?
One solution is to use the Tab selection parameter. It is better not to use the StackNavigationViewStyle() with these embedded NavigationViews. What you may use is a selecter, which keeps track of the page you are on and a State variable storing the current page. This way, the NavigationView is in one place and different titels are given to each TabView item.
struct ContentView: View {
#State private var selectedTab = "1"
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
Page1()
.tag("1")
.navigationBarTitle("Page 1")
Page2()
.tag("2")
.navigationBarTitle("Page 2")
}
.tabViewStyle(.page)
}
}
}
struct Page1: View {
var body: some View {
ScrollView {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
}}
struct Page2: View {
var body: some View {
ScrollView {
Rectangle()
.fill(.blue)
.frame(width: 100, height: 100)
}
}}
Updated
When you want the NavigationTitle to be .inline when scrolled, use the following code.
struct ContentView: View {
#State private var selectedTab = "1"
var body: some View {
NavigationView {
GeometryReader { proxy in
ScrollView(showsIndicators: false) {
TabView(selection: $selectedTab) {
Page1()
.tag("1")
.navigationBarTitle("Page 1")
Page2()
.tag("2")
.navigationBarTitle("Page 2")
}
.tabViewStyle(.page)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(height: proxy.size.height)
.ignoresSafeArea()
}
}
}
}
}
struct Page1: View {
var body: some View {
VStack {
Rectangle()
.fill(.red)
.frame(width: 200, height: 200)
}
}
}
struct Page2: View {
var body: some View {
VStack {
Rectangle()
.fill(.blue)
.frame(width: 200, height: 200)
}
}
}

Global view which blurry overlays other views and can be easily called

A similar question is asked here: SwiftUI: Global Overlay That Can Be Triggered From Any View. The examples however are shown a Toast, rather than a 'blocking' view. The accepted answer changes the presenting view (the toolbar is moving up after the toast is shown). Something is wrong with the code, but I don't know what (I just started learning SwiftUI). The code below is a combined effort from some other answers.
Alright, ideally I want to call a function on e.g. an EnvironmentObject which will automatically present an overlaying View with a ProgressView, to show that something is being loaded. The user should not be able to interact with the application while it is loading. The loading screen should be a bit blurry, overlapping with the presenting view.
Below shows what I have, the Text is never shown, but my breakpoint is hit when I click on the Load button. Any ideas why the LoadingView is never shown?
import SwiftUI
import UIKit
#main
struct TextLeadingApp: App {
var body: some Scene {
WindowGroup {
ZStack {
LoadingView() // This view should always be hidden, unless it is loading
ContentView()
}
.environmentObject(Load())
}
}
}
class Load: ObservableObject {
#Published var loader = 0
func load() {
loader += 1
}
}
struct LoadingView: View {
#EnvironmentObject var load: Load
var body: some View {
if load.loader > 0 {
GeometryReader { geometry in
Text("LOADING")
.frame(
width: geometry.size.width,
height: geometry.size.height
)
.ignoresSafeArea(.all, edges: .all)
}
} else {
EmptyView().frame(width: 0, height: 0)
}
}
}
struct ContentView: View {
#EnvironmentObject var load: Load
var body: some View {
NavigationView {
Text("hi")
.toolbar {
Button("Load") {
load.load()
}
}
.navigationBarTitle(Text("A List"), displayMode: .large)
}
.navigationViewStyle(.stack)
}
}
There are a couple of things that could be adjusted with your code.
I'd make Load a #StateObject on a parent view so that the LoadingView can be conditionally displayed and not displayed all the time and just default to an EmptyView
In a ZStack, the topmost view should be last -- you have it first.
You can use .background(.ultraThinMaterial)
#main
struct TextLeadingApp: App {
var body: some Scene {
WindowGroup {
ParentView()
}
}
}
struct ParentView : View {
#StateObject private var load = Load()
var body: some View {
ZStack {
ContentView()
if load.loader > 0 {
LoadingView()
}
}.environmentObject(load)
}
}
class Load: ObservableObject {
#Published var loader = 0
func load() {
loader += 1
}
}
struct LoadingView: View {
#EnvironmentObject var load: Load
var body: some View {
VStack {
Text("LOADING")
}
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.ultraThinMaterial)
}
}
struct ContentView: View {
#EnvironmentObject var load: Load
var body: some View {
NavigationView {
Text("hi")
.onTapGesture {
print("tapped")
}
.font(.system(size: 100, weight: .bold, design: .default))
.foregroundColor(.orange)
.toolbar {
Button("Load") {
load.load()
}
}
.navigationBarTitle(Text("A List"), displayMode: .large)
}
.navigationViewStyle(.stack)
}
}
I've adjusted ContentView a bit, just to make the blur effect more obvious.
Update, with OP's request that only LoadingView responds to a change in the state, and not the parent view:
#main
struct TextLeadingApp: App {
var body: some Scene {
WindowGroup {
ZStack {
ContentView()
LoadingView()
}
.environmentObject(Load())
}
}
}
class Load: ObservableObject {
#Published var loader = 0
func load() {
loader += 1
}
}
struct LoadingView: View {
#EnvironmentObject var load: Load
var body: some View {
if load.loader > 0 {
VStack {
Text("LOADING")
}
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.ultraThinMaterial)
} else {
EmptyView()
}
}
}

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

SwiftUI macOS NavigationView Cannot Highlight First Entry

I'm attempting to create a master/detail view on macOS with SwiftUI. When the master/detail view first renders, I'd like it to immediately "highlight" / "navigate to" its first entry.
In other words, I'd like to immediately render the following: master/detail first row highlighted
I'm using NavigationView and NavigationLink on macOS to render the master/detail view:
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: Text("detail-1").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Text("link-1")
}
NavigationLink(destination: Text("detail-2").frame(maxWidth: .infinity, maxHeight: .infinity)) {
Text("link-2")
}
}
}
}
}
I've tried using both the isActive and the tag / selection options provided by NavigationLink with no luck. What might I be missing here? Is there a way to force focus on the first master/detail element using SwiftUI?
I came across this problem recently and after being stuck at the same point I found Apple's tutorial which shows that you don't use NavigationLink on macOS.
Instead you just create a NavigationView with a List and a DetailView. Then you can bind the List's selection and it works properly.
There still seems to be a bug with the highlight. The workaround is setting the selection in the next run loop after the NavigationView has appeared. :/
Here's a complete example:
enum DetailContent: Int, CaseIterable, Hashable {
case first, second, third
}
extension DetailContent: Identifiable {
var id: Int { rawValue }
}
struct DetailView: View {
#Binding var content: DetailContent?
var body: some View {
VStack {
Text("\(content?.rawValue ?? -1)")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MainView: View {
#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())
if detailContent != nil {
DetailView(content: $detailContent)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
DispatchQueue.main.async {
self.detailContent = DetailContent.allCases.randomElement()
}
}
}
}

Why is ScollView content size not adjusting properly for this SwiftUI view?

I am using Xcode beta7 and the following is the code.
This is for a MacOs app.
here is my code:
import SwiftUI
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
V1(isClicked: $isClicked)
}
}
}
struct V1: View {
#Binding var isClicked: Bool
var body: some View {
VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
Run this code and click on the Click me button.
You will see that the scrollView's contnent size does not update and stay's squished.
If i try to resize the frame of the application by using the mouse to resize the screen, then instantly, the ScrollView's content size snaps to the correct size.
Do i need to do something to get the ScrollView to do this automatically (instead of me having to manually inscrease the frame of the app with the mouse?
I got the same issue recently on iOS, watchOS. The only way, I solve it was to move the content of the scrollView as a func in your struct having the ScrollView. As shown below.
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
otherView()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func otherView() -> some View{
return VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
It looks like a bug.