SwiftUI NavigationView on MacOS leaves padding above List - macos

I have a NavigationView that leaves what looks like padding above the List of NavigationLink's and I can't work out how to get rid of it?
Here's the view that calls the NavigationView:
struct SearchView: View {
#EnvironmentObject var networkManager:NetworkManager
var body: some View {
VStack {
SearchBar()//<- This view houses the content above the list but contains no padding after the divider and when the filesToDisplay is false it shrinks to the divider as expected
if networkManager.filesToDisplay {
ResultsView()
}
}
}
}
Here's the NavigationView:
struct ResultsView: View {
#EnvironmentObject var networkManager:NetworkManager
var body: some View {
NavigationView {
List {
ForEach(networkManager.FileList!.items) { file in
NavigationLink(destination: FileDetail(fileDetail: file)) {
FileRow(fileRow: file)
}
}
}
Rectangle().frame(maxWidth: 0, maxHeight: .infinity)
}.frame(minHeight:500, idealHeight: 550, maxHeight: .infinity)
}
}
I thought the problem might be to do with navigationBarTitle but MacOS does not appear to use this? I've tried all sorts of different padding and listRowInsets but nothing is working.

Add explicit spacing
VStack(spacing: 0) { // << here !!
SearchBar()
if networkManager.filesToDisplay {
ResultsView()
}
}

Related

How do I switch between screens in TabView and from the latter to the other View?

I created a simple collection with a button jump to the next View. From the last View there should be a transition to AddItemView, but it doesn't happen - it goes back to the first screen.
Can you tell me where I made a mistake?
What is the correct way to place the background Image on the first collection screen, so that it won't be on the following screens?
import SwiftUI
struct AddItemView: View {
var body: some View {
Text("Hallo!")
}
}
struct ContentView: View {
var colors: [Color] = [ .orange, .green, .yellow, .pink, .purple ]
var emojis: [String] = [ "👻", "🐱", "🦊" , "👺", "🎃"]
#State private var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
ForEach(0..<emojis.endIndex) { index in
VStack {
Text(emojis[index])
.font(.system(size: 150))
.frame(minWidth: 30, maxWidth: .infinity, minHeight: 0, maxHeight: 250)
.background(colors[index])
.clipShape(RoundedRectangle(cornerRadius: 30))
.padding()
.tabItem {
Text(emojis[index])
}
Button(action: {
self.tabSelection += 1
}) {
if tabSelection == emojis.endIndex {
NavigationLink(destination: AddItemView()) {
Text("Open View")
}
} else {
Text("Change to next tab")
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.tabViewStyle(PageTabViewStyle.init(indexDisplayMode: .never))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In this code, you have not to use NavigationView. It's required to navigate to the next screen. Similar concept like Push view controller if navigation controller exists. Also, remove endIndex and use indices.
struct ContentView: View {
var colors: [Color] = [ .orange, .green, .yellow, .pink, .purple ]
var emojis: [String] = [ "👻", "🐱", "🦊" , "👺", "🎃"]
#State private var tabSelection = 0
var body: some View {
NavigationView { //<- add navigation view
TabView(selection: $tabSelection) {
ForEach(emojis.indices) { index in //<-- use indices
VStack {
Text(emojis[index])
.font(.system(size: 150))
.frame(minWidth: 30, maxWidth: .infinity, minHeight: 0, maxHeight: 250)
.background(colors[index])
.clipShape(RoundedRectangle(cornerRadius: 30))
.padding()
.tabItem {
Text(emojis[index])
}
Button(action: {
self.tabSelection += 1
}) {
if tabSelection == emojis.count - 1 { //<- use count
NavigationLink(destination: AddItemView()) {
Text("Open View")
}
} else {
Text("Change to next tab")
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.tabViewStyle(PageTabViewStyle.init(indexDisplayMode: .never))
}
}
}
If you have already a navigation link from the previous screen then, the problem is you are using endIndex in the wrong way. Check this thread for correct use (https://stackoverflow.com/a/36683863/14733292).

macOS SwiftUI Navigation for a Single View

I'm attempting to create a settings view for my macOS SwiftUI status bar app. My implementation so far has been using a NavigationView, and NavigationLink, but this solution produces a half view as the settings view pushes the parent view to the side. Screenshot and code example below.
Navigation Sidebar
struct ContentView: View {
var body: some View {
VStack{
NavigationView{
NavigationLink(destination: SecondView()){
Text("Go to next view")
}}
}.frame(width: 800, height: 600, alignment: .center)}
}
struct SecondView: View {
var body: some View {
VStack{
Text("This is the second view")
}.frame(width: 800, height: 600, alignment: .center)
}
}
The little information I can find suggests that this is unavoidable using SwiftUI on macOS, because the 'full screen' NavigationView on iOS (StackNavigationViewStyle) is not available on macOS.
Is there a simple or even complex way of implementing a transition to a settings view that takes up the whole frame in SwiftUI for macOS? And if not, is it possible to use AppKit to call a View object written in SwiftUI?
Also a Swift newbie - please be gentle.
Here is a simple demo of possible approach for custom navigation-like solution. Tested with Xcode 11.4 / macOS 10.15.4
Note: background colors are used for better visibility.
struct ContentView: View {
#State private var show = false
var body: some View {
VStack{
if !show {
RootView(show: $show)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.transition(AnyTransition.move(edge: .leading)).animation(.default)
}
if show {
NextView(show: $show)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
.transition(AnyTransition.move(edge: .trailing)).animation(.default)
}
}
}
}
struct RootView: View {
#Binding var show: Bool
var body: some View {
VStack{
Button("Next") { self.show = true }
Text("This is the first view")
}
}
}
struct NextView: View {
#Binding var show: Bool
var body: some View {
VStack{
Button("Back") { self.show = false }
Text("This is the second view")
}
}
}
I've expanded upon Asperi's great suggestion and created a generic, reusable StackNavigationView for macOS (or even iOS, if you want). Some highlights:
It supports any number of subviews (in any layout).
It automatically adds a 'Back' button for each subview (just text for now, but you can swap in an icon if using macOS 11+).
Swift v5.2:
struct StackNavigationView<RootContent, SubviewContent>: View where RootContent: View, SubviewContent: View {
#Binding var currentSubviewIndex: Int
#Binding var showingSubview: Bool
let subviewByIndex: (Int) -> SubviewContent
let rootView: () -> RootContent
var body: some View {
VStack {
VStack{
if !showingSubview { // Root view
rootView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(AnyTransition.move(edge: .leading)).animation(.default)
}
if showingSubview { // Correct subview for current index
StackNavigationSubview(isVisible: self.$showingSubview) {
self.subviewByIndex(self.currentSubviewIndex)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(AnyTransition.move(edge: .trailing)).animation(.default)
}
}
}
}
init(currentSubviewIndex: Binding<Int>, showingSubview: Binding<Bool>, #ViewBuilder subviewByIndex: #escaping (Int) -> SubviewContent, #ViewBuilder rootView: #escaping () -> RootContent) {
self._currentSubviewIndex = currentSubviewIndex
self._showingSubview = showingSubview
self.subviewByIndex = subviewByIndex
self.rootView = rootView
}
private struct StackNavigationSubview<Content>: View where Content: View {
#Binding var isVisible: Bool
let contentView: () -> Content
var body: some View {
VStack {
HStack { // Back button
Button(action: {
self.isVisible = false
}) {
Text("< Back")
}.buttonStyle(BorderlessButtonStyle())
Spacer()
}
.padding(.horizontal).padding(.vertical, 4)
contentView() // Main view content
}
}
}
}
More info on #ViewBuilder and generics used can be found here.
Here's a basic example of it in use. The parent view tracks current selection and display status (using #State), allowing anything inside its subviews to trigger state changes.
struct ExampleView: View {
#State private var currentSubviewIndex = 0
#State private var showingSubview = false
var body: some View {
StackNavigationView(
currentSubviewIndex: self.$currentSubviewIndex,
showingSubview: self.$showingSubview,
subviewByIndex: { index in
self.subView(forIndex: index)
}
) {
VStack {
Button(action: { self.showSubview(withIndex: 0) }) {
Text("Show View 1")
}
Button(action: { self.showSubview(withIndex: 1) }) {
Text("Show View 2")
}
Button(action: { self.showSubview(withIndex: 2) }) {
Text("Show View 3")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
}
}
private func subView(forIndex index: Int) -> AnyView {
switch index {
case 0: return AnyView(Text("I'm View One").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.green))
case 1: return AnyView(Text("I'm View Two").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.yellow))
case 2: return AnyView(VStack {
Text("And I'm...")
Text("View Three")
}.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.orange))
default: return AnyView(Text("Inavlid Selection").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.red))
}
}
private func showSubview(withIndex index: Int) {
currentSubviewIndex = index
showingSubview = true
}
}
Note: Generics like this require all subviews to be of the same type. If that's not so, you can wrap them in AnyView, like I've done here. The AnyView wrapper isn't required if you're using a consistent type for all subviews (the root view’s type doesn’t need to match).
Heyo, so a problem I had is that I wanted to have multiple navigationView-layers, I'm not sure if that's also your attempt, but if it is: MacOS DOES NOT inherit the NavigationView.
Meaning, you need to provide your DetailView (or SecondView in your case) with it's own NavigationView. So, just embedding like [...], destination: NavigationView { SecondView() }) [...] should do the trick.
But, careful! Doing the same for iOS targets will result in unexpected behaviour. So, if you target both make sure you use #if os(macOS)!
However, when making a settings view, I'd recommend you also look into the Settings Scene provided by Apple.
Seems this didn't get fixed in Xcode 13.
Tested on Xcode 13 Big Sur, not on Monterrey though...
You can get full screen navigation with
.navigationViewStyle(StackNavigationViewStyle())

In a MacOS App using SwiftUI, how to modify the default blue background of a selected NavigationLink nested in a list?

Good day all. In a MacOS App using SwiftUI, how to modify the default blue background of a selected NavigationLink nested in a list? The list is inside a NavigationView. I could not find the solution here. What would be the code to add to the following basic exemple: Two TextView listed, and if we click on the TextView we display the correspondant View.
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()
}
}
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()
}
}
Here is the result if we click on the first row... the background is blue when selected! How to change this default color? Thanks in advance for your help.
the following code is one option to achieve what you want:
struct ContentView: View {
#State var selection: Int? = 0
#State var pressed: Int? = 0
init() {
UITableViewCell.appearance().selectionStyle = .none
UITableView.appearance().backgroundColor = UIColor.clear
}
var body: some View {
var theBinding = Binding(
get: { self.selection },
set: {
self.selection = $0
self.pressed = $0 == nil ? self.pressed : $0!
})
return HStack() {
NavigationView {
List {
NavigationLink(destination: FirstView(), tag: 0, selection: theBinding) {
Text("Click Me To Display The First View")
}.listRowBackground(self.pressed == 0 ? Color.red : Color.green)
NavigationLink(destination: SecondView(), tag: 1, selection: theBinding) {
Text("Click Me To Display The Second View")
}.listRowBackground(self.pressed == 1 ? Color.red : Color.green)
}.frame(minWidth: 350, maxWidth: 350)
.onAppear { self.pressed = 0 }
}.listStyle(SidebarListStyle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}

SwiftUI: How to replace a View with another in MacOS

I want to replace a View with another when a Button is pressed in SwiftUI on MacOS (AppKit not Catalyst).
I tried with a NavigationLink like this
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink( destination: Text("DESTINATIONVIEW").frame(maxWidth: .infinity, maxHeight: .infinity) ,
label: {Text("Next")} )
}.frame(width: 500.0, height: 500.0)
}
}
The Destination View is presented like in iPadOS, like a Master-Detail Presentation right of the Source View.
What I want, is to replace the SourceView with the Destination (almost like in iOS)
Another approach was to switch the presented view with a bool variable
struct MasterList: View
{
private let names = ["Frank", "John", "Tom", "Lisa"]
#State var showDetail: Bool = false
#State var selectedName: String = ""
var body: some View
{
VStack
{
if (showDetail)
{ DetailViewReplace(showDetail:$showDetail, text: "\(selectedName)" )
} else
{
List()
{
ForEach(names, id: \.self)
{ name in
Button(action:
{
self.showDetail.toggle()
self.selectedName = name
})
{ Text("\(name)")}
.buttonStyle( LinkButtonStyle() )
}
}.listStyle( SidebarListStyle())
}
}
}
}
struct DetailViewReplace: View {
#Binding var showDetail: Bool
let text: String
var body: some View
{
VStack
{
Text(text)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(action:
{ self.showDetail.toggle()
})
{ Text("Return")
}
}
}
}
But that seems quite cumbersome for me if dealing with many views.
Is there a best practice to do that?

Animation in ScrollView is not working ? Using Xcode 11 beta 6

I was trying to to do transition animation Scrollview but found out that how things work differently in scrollView. But still unable to do animation in it. I am providing code, please have a look. Using Xcode 11 beta 6
import SwiftUI
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
NavigationView {
ScrollView{
VStack {
Button(action: {
// withAnimation {
self.isButtonVisible.toggle()
// }
}) {
Text("Press me")
}
if isButtonVisible {
Text("sss")
.frame(height: true ? 50 : 0, alignment: .center)
.background(Color.red)
.animation(.linear(duration: 2))
// .transition(.move(edge: .top))
}
}
}
}
}}
This must be a bug, and I suggest you file a bug report with Apple. I find a workaround (see code below), but it unfortunately uncovers another bug!
In order to make the animation inside the ScrollView to work, you can encapsulate the contents in a custom view. That will fix that problem.
This will uncover a new issue, which is made evident by the borders I added to your code: when the Text view is added, it shifts parts of the contents outside the ScrollView.
You will see that this is not correct. Try starting your app with a default value isButtonVisible = true, and you will see it renders it differently.
struct ContentView : View {
var body: some View {
NavigationView {
ScrollView {
EncapsulatedView().border(Color.green)
}.border(Color.blue)
}
}
}
struct EncapsulatedView: View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Text("Filler")
Button(action: {
withAnimation(.easeInOut(duration: 2.0)) {
self.isButtonVisible.toggle()
}
}) {
Text("Press me")
}
if isButtonVisible {
Text("sss")
.frame(height: true ? 50 : 0, alignment: .center)
.transition(.opacity)
.background(Color.red)
}
Spacer()
}.border(Color.red)
}
}

Resources