How to hide the navigation button when using NavigationSplitView? - swift

Now that the NavigationView was deprecated, I try to use the NavigationSplitView. But I can't hide the navigation toggle button, and also can't custom the title bar (I want to keep title and add filter button).
My app screenshot
I just want to realize like the Mail.app
Mail.app screenshot
Some other app screenshot
code snippet as bellow:
// ...
var body: some View {
NavigationSplitView(columnVisibility: $columnVisibility) {
DirectoryList(selection: $selectionDir)
} content: {
PaperList(selection: $selectionPaper)
.navigationTitle(Text("Papers"))
.toolbar {
HStack {
Button {
// something todo
} label: {
Label("Experience", systemImage: "wand.and.stars")
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
} detail: {
Editor(selectionPaper?.name ?? "")
}
}
// ...

Related

How to customise navigation title in SwftUI

I have a very simple NavigationStack that I would like to customise the title, but I can't seem to find the right modifiers to achieve this.
NavigationStack {
List {
NavigationLink {
Text("My Child View")
} label: {
Label("Child View")
}
}.navigationTitle("Parent View")
}
I would like to change how the font looks for the .navigationTitle and be able to add a button to the right. When I add a .font modifier to .navigationTitle it adds it to the list items, not the title. And .navigationTitle only appears to accept a string.
I am looking to achieve the below (my button will be a + rather than a chevron).
From what I can tell, I can't use navigationTitle to achieve this. However, if I don't use that, then the back button has the wrong title (it uses the text Back instead of Parent View).
You can achieve both using the .toolbar modifier:
public var body: some View {
NavigationStack {
List {
NavigationLink {
Text("My Child View")
} label: {
Label("Child View", systemImage: "questionmark")
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Text("Parent View")
.font(.system(size: 22, weight: .bold))
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
} label: {
Image(systemName: "plus")
}
}
}
}
}

How to navigate between NavigationLink while leave a part of main window stay the same in SwiftUI

I would like to navigate between different NavigationLinks in NavigationView while some part of the main window stay the same. For example, I want to make a music app and I want to let the play controller always on top, while I can display different navigation contents (songs page, artists page...) using the rest of the window.
Like what's showed in the picture below, I want to keep the red part always there while the blue part changes.
Navigation Example Picture
My code would be like below, but it won't work correctly. The AlwaysStayView() disappears when I click any NavigationLink on sidebar. So, how can I correct it or is there any solution (prefer in SwiftUI, but framework like UIKit would also be OK). I would appreciate it.
NavigationView {
List {
NavigationLink { DiscoverView() }
label: { Label("Discover", systemImage: "magnifyingglass") }
NavigationLink { SongsView() }
label: { Label("Songs", systemImage: "music.note") }
NavigationLink { ArtistsView() }
label: { Label("Artists", systemImage: "music.mic") }
}
}
.listStyle(SidebarListStyle())
VStack {
AlwaysStayView()
SongsView()
}
}
In this case the default details view and navigated details view should be the same, but updated content can be injected in it in navigation link.
Here is a demo. Tested with Xcode 13.3 / iPadOS 15.4
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink { DetailsView { DiscoverView() } }
label: { Label("Discover", systemImage: "magnifyingglass") }
NavigationLink { DetailsView { SongsView() } }
label: { Label("Songs", systemImage: "music.note") }
NavigationLink { DetailsView { ArtistsView() } }
label: { Label("Artists", systemImage: "music.mic") }
}
.navigationBarHidden(true)
.listStyle(SidebarListStyle())
DetailsView { SongsView() } // << here default !!
}
}
}
struct DetailsView<V: View>: View {
#ViewBuilder var content: () -> V // << injected via builder !!
var body: some View {
VStack(spacing: 0) {
AlwaysStayView()
content() // << changed part here !!
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.navigationBarHidden(true)
}
}
The NavigationLink from sidebar always exchanges the whole right screen area. So you would have to put your AlwaysStayView inside the navigation links – in each. Either on top level or inside the respective detail views. Here is one example:
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink {
DetailView(title: "Always stay", color: .red).frame(height: 100)
DetailView(title: "Discover", color: .blue) }
label: { Label("Discover", systemImage: "magnifyingglass") }
NavigationLink {
DetailView(title: "Always stay", color: .red).frame(height: 100)
DetailView(title: "Songs", color: .teal) }
label: { Label("Songs", systemImage: "music.note") }
NavigationLink {
DetailView(title: "Always stay", color: .red).frame(height: 100)
DetailView(title: "Artists", color: .mint) }
label: { Label("Artists", systemImage: "music.mic") }
}
.listStyle(.sidebar)
// Standard view if no item is lelected
VStack {
DetailView(title: "Always stay", color: .red).frame(height: 100)
DetailView(title: "Songs", color: .teal)
}
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("Toolbar")
}
}
}
}
struct DetailView: View {
let title: String
let color: Color
var body: some View {
Text(title).font(.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(color)
}
}

Why doesn't the button's image background show up in toolbar using SwiftUI?

In Apple's calendar app, they provide a toolbar item that toggles its style based on some state. It essentially acts as a Toggle. I'm trying to re-create this same thing in SwiftUI and make it work well in both light and dark mode. I was able to make a view that works as intended, until I put it into the toolbar and it no longer shows the selected state. Here is my attempt:
struct ToggleButtonView: View {
#State private var isOn = false
var body: some View {
Button(action: {
isOn.toggle()
}, label: {
if isOn {
Image(systemName: "list.bullet.below.rectangle")
.accentColor(Color(.systemBackground))
.background(RoundedRectangle(cornerRadius: 5.0)
.fill(Color.accentColor)
.frame(width: 26, height: 26))
} else {
Image(systemName: "list.bullet.below.rectangle")
}
})
.accentColor(.red)
}
}
And here is how I am actually placing the button into the toolbar:
struct TestView: View {
var body: some View {
NavigationView {
ScrollView {
ForEach(0..<5) { number in
Text("Number \(number)")
}
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
ToggleButtonView()
Button(action: {}, label: {
Image(systemName: "plus")
})
}
}
.navigationTitle("Plz halp")
}
.accentColor(.red)
}
}
Here are screenshots from the calendar app. Notice the toolbar item to the left of the search icon.
you could try this:
.toolbar {
// placement as you see fit
ToolbarItem(placement: .navigationBarTrailing) {
ToggleButtonView()
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {}, label: {
Image(systemName: "plus")
})
}
}
This looks like an issue with how SwiftUI handles ToolbarItems before iOS 15. According to Asperi's answer to a similar question, "...all standards types (button, image, text, etc) are intercepted by ToolbarItem and converted into an appropriate internal representation."
Toggle buttons in SwiftUI iOS 15
Interestingly enough, iOS 15 now provides a standard solution to the use-case above using the .button toggle style, as shown in the following code:
struct ContentView: View {
#State private var isOn = false
var body: some View {
Toggle(isOn: $isOn) {
Image(systemName: "list.bullet.below.rectangle")
}
}
}

SwiftUI - How to make a class have a menu?

I have a button (chevron) that shows at the side of a search bar like this:
This button has a lot of customizations, so I decided to give it its own class, to reduce code pollution and make the code neat.
This is the class's body...
var body: some View {
Button(action: {
onTap()
}) {
Image(systemName: "chevron.right.square").renderingMode(.original)
.renderingMode(.template)
.foregroundColor(color)
}
.font(fontSymbol)
Spacer().frame(width: 10)
}
The Image inside a Button is just me trying other options. I have created this as just the image without the button too.
The problem is this:
I will create an instance of this class on the main view.
I want that to display a menu when it is tapped.
This is how I am using it on the main class
MenuButton()
.contextMenu {
Button(action: {
}) {
Text("option 1")
}
Button(action: {
}) {
Text("option 2")
}
Button(action: {
}) {
Text("option 3")
}
}
This is the result...
The box is shown already expanded in height.
I was expecting a popover to appear only after tapping on the chevron and that popover to be pointed to the chevron or something like that.
Remove the spacer to locate button where it should be
.font(fontSymbol)
//Spacer().frame(width: 10) // << this one
and context menu works by long press as expected

How to apply a context menu to buttons in a SwiftUI list row?

When I'm long-pressing on a button in list row, all of context menus for all buttons are shown. It looks like the whole list row is selected. How can I make it so that the context menu is shown only for the pressed button?
I've seen this question which is somewhat related, and tried BorderlessButtonStyle(). It enables the indiviual buttons in the row to be clickable, but it doesn't solve the context menu problem. I've also tried using .onTapGesture() instead of Button(), but that didn't work either.
In the example below I'm expecting to see only Action for button 1 when long-pressing on Button 1 - but Action for button 2 is also shown.
struct ContentView: View {
var body: some View {
List {
ForEach((1..<3), content: { _ in
ListButton()
})
}
}
}
struct ListButton: View {
var body: some View {
HStack {
Spacer()
Button("Button 1") { }
.buttonStyle(BorderlessButtonStyle())
.contextMenu(menuItems: {
Text("Action for button 1")
})
Spacer()
Button("Button 2") { }
.buttonStyle(BorderlessButtonStyle())
.contextMenu(menuItems: {
Text("Action for button 2")
})
Spacer()
}
}
}
You could use Menu instead of ContextMenu and use the label of the menu to display your button; as alternative solution.
However, to give the buttons gesture more priority than the row itself, you can add .highPriorityGesture(TapGesture()) to the end of you buttons.
You can achieve what you want by using Menu instead of ContextMenu like this:
struct ListButton: View {
var body: some View {
HStack {
Spacer()
Button("Button 1") { }
.buttonStyle(BorderlessButtonStyle())
.contextMenu(menuItems: {
Text("Action for button 1")
})
Spacer()
// Here I used menu instead of the button 2
Menu {
Text("Action for button 2")
} label: {
Button("Button 2") { }
.buttonStyle(BorderlessButtonStyle())
}.highPriorityGesture(TapGesture())
Spacer()
}
}
}
Just switch to ScrollView that fixes your problem!