Variable 3-Column NavigationView SwiftUI - swift

Problem
I would like to be able to change the number of columns in a Navigation View depending on the sidebar selection. i.e. Most views will have the desired 3-column layout (sidebar > list > detail) but one will have a two column layout (sidebar > detail). I tried to set this up directly in the top layer of the navigation view but this didn't change anything.
NavigationView{
SidebarView()
if selection != .explore {
ListView()
}
DetailView()
}
In the above example, if the selection is 'explore' there should only be a sidebar and a detail view.
Any ideas on how to achieve this?
Code to reproduce
I would want "searchView" to take up the full width. Meaning just a sidebar and search view should appear
Run on macOS or iPadOS
import SwiftUI
enum SidebarSelection {
case library
case notes
case search
}
struct ContentView: View {
#State var selection : SidebarSelection? = SidebarSelection.library
var body: some View {
NavigationView {
List {
NavigationLink(destination: ListView(), tag: SidebarSelection.library, selection: $selection){
Label("Library", systemImage: "book")
}
.tag(SidebarSelection.library)
NavigationLink(destination: ListView(), tag: SidebarSelection.notes, selection: $selection){
Label("Notes", systemImage: "doc.text")
}
.tag(SidebarSelection.notes)
NavigationLink(destination: SearchView(), tag: SidebarSelection.search, selection: $selection){
Label("Search", systemImage: "magnifyingglass")
}
.tag(SidebarSelection.search)
}
.listStyle(SidebarListStyle())
Text("List View")
if selection != .search {
Text("Detail View")
}
}
}
}
struct ListView: View {
var body: some View{
List {
ForEach(0..<10){ index in
NavigationLink(destination: Text("DetailView: \(index)")){
Text("Link to \(index) detail view")
}
}
}
}
}
struct SearchView: View {
var body: some View {
Text("Full width search view")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

What you can do is quite easy actually, to achieve this, you just have to be bit more specific for SwiftUI and use the #ViewBuilder property wrapper.
struct ContentView: View {
#ViewBuilder
var body: some View {
if(twoColumns == true){
NavigationView{
SidebarView()
DetailView()
}
} else {
NavigationView{
SidebarView()
ListView()
DetailView()
}
}
}
}

Related

SwiftUI NavigationView Starting Inside Itself

So I've got a NavigationView embedded inside of a TabView that is in the Page View Style. Upon first load the NavigationView will start inside itself, and then once reloaded it shows normally. I am unsure as to what is causing this. And I've made a GIF to better illustrate the problem:
And here is my code:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
SettingsView()
EmptyView()
EmptyView()
EmptyView()
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
struct SettingsView: View {
var body: some View {
NavigationView {
List {
Section(header: Text("General")) {
NavigationLink(destination: SettingsItem(title: "1")) {
Text("1")
}
NavigationLink(destination: SettingsItem(title: "2")) {
Text("2")
}
NavigationLink(destination: SettingsItem(title: "3")) {
Text("3")
}
NavigationLink(destination: SettingsItem(title: "4")) {
Text("4")
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Settings")
}
}
}
struct SettingsItem: View {
#State var title = "Title"
var body: some View {
NavigationView {
List {
}
}
.navigationTitle(title)
}
}
struct EmptyView: View {
var body: some View {
ZStack{
Color.green
Text("Empty View")
.padding()
}
.border(Color.black, width: 8)
}
}
Believe it or not, this has nothing to do with the TabView(). This is actually an issue in the SettingsView(). SwiftUI is defaulting to a split view. As a result, you are getting an empty view because you haven't actually navigated to any particular view, with the Settings back button showing. The fix to make this behave as you would expect is to add a .navigationViewStyle(StackNavigationViewStyle() on the NavigationView in SettingsView which forces SettingsView to display as a single view like this:
struct ContentView: View {
var body: some View {
TabView {
SettingsView()
BlankView(color: .green)
BlankView(color: .blue)
BlankView(color: .red)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
struct SettingsView: View {
var body: some View {
NavigationView {
List {
Section(header: Text("General")) {
NavigationLink(destination: SettingsItem(title: "1")) {
Text("1")
}
NavigationLink(destination: SettingsItem(title: "2")) {
Text("2")
}
NavigationLink(destination: SettingsItem(title: "3")) {
Text("3")
}
NavigationLink(destination: SettingsItem(title: "4")) {
Text("4")
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Settings")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct SettingsItem: View {
#State var title = "Title"
var body: some View {
NavigationView {
List {
}
}
.navigationTitle(title)
}
}
struct BlankView: View {
let color: Color
var body: some View {
ZStack{
color
Text("Blank View")
.padding()
}
.border(Color.black, width: 8)
}
}
Also, I edited your code a bit for clarity. EmptyView() is already designated by the system, so I changed it to BlankView() I am not sure why the compiler doesn't have a problem with you naming a struct EmptyView(), but you really shouldn't use that name. I also had the BlankView() show different colors so it was clear you were navigating to separate views.

SwiftUI TabView Animation

I am currently facing a pb on my app.
I would like to animate the insertion and removal of items that are controlled by SwiftUI TabView.
Here is the simplest view I can come with that reproduce the problem
struct ContentView: View {
#State private var selection: Int = 1
var body: some View {
TabView(selection: $selection.animation(),
content: {
Text("Tab Content 1")
.transition(.slide) //could be anything this is for example
.tabItem { Text("tab1") }.tag(1)
.onAppear() {print("testApp")}
.onDisappear(){print("testDis")}
Text("Tab Content 2")
.transition(.slide)
.tabItem { Text("tab2") }.tag(2)
})
}
}
Actually when hitting a tabItem. It switches instantly from "Tab Content 1" to "Tab Content 2" and I would like to animate it (not the tab item button the actuel tab content). The On Appear and onDisapear are corectly called as expected hence all transition should be triggered.
If someone has an idea to start working with I would be very happy
Thanks
1.with .transition() we only specify which transition should happen.
2.Transition occur (as expected), only when explicit animation occurs.
3.Animation occurs when change happened(State, Binding)
here is one of possible approaches.
struct ContentView: View {
#State private var selection: Int = 1
var body: some View {
TabView(selection: $selection,
content: {
ItemView(text:"1")
.tabItem { Text("tab1") }.tag(1)
ItemView(text: "2")
.tabItem { Text("tab2") }.tag(2)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ItemView: View {
let text: String
#State var hidden = true
var body: some View {
VStack {
if !hidden {
Text("Tab Content " + text)
.transition(.slide)
}
}
.onAppear() { withAnimation {
hidden = false
}}
.onDisappear(){hidden = true}
}
}

Weird list view when I add navigationBarItem

to make it short: I want to have the same view of the list like in the first image i shared. But when I add a navigation bar item the list looks strange to me. It this a bug of the new version of Swift/XCode or needs something to be changed?
Code:
import SwiftUI
import CoreData
struct ContentView: View {
var body: some View {
NavigationView {
List{
Text("test1")
Text("test2")
Text("test3")
}
.navigationTitle("Test")
// .navigationBarItems(leading:
// Text("Test")
// )
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Try using .navigationViewStyle as below:
struct ContentView: View {
#State private var isFullScreen = false
var body: some View {
NavigationView {
List{
Text("One")
Text("Two")
}
.navigationTitle("Testt")
.navigationBarItems(leading: Text("Add"))
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Hey! Give This A Try!
var body: some View {
NavigationView {
List{
Text("One")
Text("Two")
}
.navigationTitle("Testt, displayMode: .inline)
.navigationBarItems(leading: Text("Add"))
}
}
}

SwiftUI TabView with List not refreshing after objected deleted from / added to Core Data

Description:
When an object in a list (created from a fetchrequest) is deleted from a context, and the context is saved, the list does not properly update.
Error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value (Thrown on line 5 below)
struct DetailView: View {
#ObservedObject var event: Event
var body: some View {
Text("\(event.timestamp!, formatter: dateFormatter)")
.navigationBarTitle(Text("Detail"))
}
}
Steps to reproduce:
Create a new Master Detail App project with SwiftUI and Core Data.
In the ContentView, set the body to a TabView with the first tab being the prebuilt NavigationView, and add a second arbitrary tab.
struct ContentView: View {
#Environment(\.managedObjectContext)
var viewContext
var body: some View {
TabView {
NavigationView {
MasterView()
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation { Event.create(in: self.viewContext) }
}
) {
Image(systemName: "plus")
}
)
Text("Detail view content goes here")
.navigationBarTitle(Text("Detail"))
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.tabItem { Text("Main") }
Text("Other Tab")
.tabItem { Text("Other Tab") }
}
}
}
Add a few items. Interact with those items in any way.
Change tabs.
Change back to Main Tab.
Attempt to delete an item.
I found a pure SwiftUI working solution:
/// This View that init the content view when selection match tag.
struct SyncView<Content: View>: View {
#Binding var selection: Int
var tag: Int
var content: () -> Content
#ViewBuilder
var body: some View {
if selection == tag {
content()
} else {
Spacer()
}
}
}
You can use it then in this way:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
SyncView(selection: $selection, tag: 0) {
ViewThatNeedsRefresh()
}
.tabItem { Text("First") }
.tag(0)
Text("Second View")
.font(.title)
.tabItem { Text("Second") }
.tag(1)
}
}
}
You can use the SyncView for each view that needs a refresh.

Create a NavigationLink without back button SwiftUI

Im trying to link a button action in SomeView1() to navigate to a someView2() without having the back button at the top of the screen. Instead, I want to add another button in SomeView2() that will navigate back to SomeView1(). is this possible in SwiftUI yet?
SomeView1()
struct SomeView1: View {
var body: some View {
NavigationView {
VStack {
//...view's content
NavigationLink(destination: SomeView2()) {
Text("go to SomeView2")
}
Spacer()
}
}
}
}
SomeView2()
struct SomeView2: View {
var body: some View {
NavigationView {
VStack {
//...view's content
NavigationLink(destination: SomeView1()) {
Text("go to SomeView1")
}
Spacer()
}
}
}
}
this is what it looks like:
The right way to get what you want here is to use the presentationMode environment variable:
import SwiftUI
struct View2: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("POP")
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("PUSH")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can do something like this in SomeView2():
NavigationView {
VStack {
//...view's content
NavigationLink(destination: SomeView1()) {
Text("go to SomeView1")
}
Spacer()
}
}.navigationBarBackButtonHidden(true)
I believe that you should use only one NavigationView for the whole navigation process. Now you have three NavigationViews inside each other, which produces three back buttons.
So in your case it would become something like this:
struct SomeView1InsideNavigationView: View { // This should be the first view you present
var body: some View {
NavigationView { // Use NavigationView only once
SomeView1()
}
}
}
struct SomeView1: View {
var body: some View {
VStack { // Do *not* use NavigationView here
//...view's content
NavigationLink(destination: SomeView2()) {
Text("go to SomeView2")
}
Spacer()
}
}
}
struct SomeView2: View {
var body: some View {
VStack { // Do *not* use NavigationView here
//...view's content
NavigationLink(destination: SomeView1()) {
Text("go to SomeView1")
}
Spacer()
}
}
}