macOS SwiftUI Navigation for a Single View - macos

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

Related

Customizing the macOS toolbar with Swift UI

I'm working on a macOS app, with the view layers written in SwiftUI. I know that iOS toolbars can have the background color changed at least, but when I try to do this in macOS, it doesn't behave as I'd expect.
Here's a (simplified) example:
struct ContentView: View {
var body: some View {
NavigationView {
Collections()
.layoutPriority(0)
Photos()
.frame(maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.background(Color.Alt.black)
.layoutPriority(1)
}
.toolbar {
Toolbar().background(Color.red500)
}
}
}
struct Toolbar: View {
var body: some View {
Group {
Slider(value: 250, in: 150...400) {
Text("Toolbar.PreviewSize")
} minimumValueLabel: {
Image(systemName: "photo").resizable().scaledToFit().frame(width: 15)
} maximumValueLabel: {
Image(systemName: "photo").resizable().scaledToFit().frame(width: 23)
} onEditingChanged: { _ in
// do nothing
}.frame(minWidth: 200)
Spacer()
Text("Toolbar.SelectionCount")
Spacer()
AddPhotosButton()
}
}
}
Which produces something like this, which as you can see, doesn't apply the background color to the entire toolbar, just to the items in the toolbar:
I'm guessing I could make my own WindowToolbarStyle style, but there's no documentation on the protocol!
If I make my own toolbar as a View rather than with the .toolbar modifier, I can't read the safe area insets for the window traffic buttons when the sidebar is collapsed, resulting in a complete mess:
Thanks for any help!
I recommend
{
.toolbar {
Toolbar()
}.toolbarBackground(Color.gray)
}

SwiftUI NavigationView on MacOS leaves padding above List

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

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

Dark mode in SwiftUI Preview doesn't have a dark background with Xcode 11.4

Does anyone have the same problem, that Xcode (11.4) doesn't show a dark background, when previewing in dark mode?
Steps to reproduce:
1) Create a new project, a Single View App
2) Add the .environment-modifier to the preview:
Group {
ContentView()
.environment(\.colorScheme, .light)
ContentView()
.environment(\.colorScheme, .dark)
}
You get this result:
Setting \.colorScheme in the environment is deprecated, so instead use the .preferedColorScheme modifier. For example,
ContentView()
.preferredColorScheme(.dark)
As m-reza-f mentioned in a similar question, this is a bug in Xcode (which is still active as this answer is posted).
I'll add that instead of wrapping your actual body code in a NavigationView, you can simply wrap the previews code in your PreviewProvider in a NavigationView instead, to achieve the same results:
struct ContentView_Previews: PreviewProvider {
Group {
NavigationView {
ContentView()
.environment(\.colorScheme, .light)
}
NavigationView {
ContentView()
.environment(\.colorScheme, .light)
}
}
}
try this:
#available(iOS 13.0, *)
public struct DarkView<Content> : View where Content : View {
var darkContent: Content
var on: Bool
public init(_ on: Bool, #ViewBuilder content: () -> Content) {
self.darkContent = content()
self.on = on
}
public var body: some View {
ZStack {
if on {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.black)
.edgesIgnoringSafeArea(.all)
darkContent.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).background(Color.black).colorScheme(.dark)
} else {
darkContent
}
}
}
}
#available(iOS 13.0, *)
extension View {
#available(iOS 13.0, *)
public func darkModeFix(_ on: Bool = true) -> DarkView<Self> {
DarkView(on) {
self
}
}
}
and then
yourView()
.environment(\.colorScheme, .dark)
.darkModeFix()

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?

Resources