SwiftUI - ExpandableView inside VStack - ios

I'm trying to have multiple expandable views with animation inside a VStack. I have the following code:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
ExpandableView(headerTitle: "First")
ExpandableView(headerTitle: "Second")
Spacer()
}
}
}
}
}
And the ExpandableView:
struct ExpandableView: View {
let headerTitle: String
#State private var collapsed: Bool = true
var body: some View {
Button(
action: {
self.collapsed.toggle()
},
label: {
VStack(spacing: 2) {
ZStack {
Rectangle()
.fill(.gray)
VStack {
Text("\(headerTitle) Header")
if !collapsed {
HStack {
Text("Text A")
Text("Text B")
}
}
}
}
.frame(height: collapsed ? 52 : 80)
ZStack(alignment: .top) {
Rectangle()
.fill(.gray)
.frame(height: 204)
VStack {
Text("Content A")
Text("Content B")
Text("Content C")
}
}
.frame(maxHeight: collapsed ? 0 : .none)
.clipped()
}
}
)
.buttonStyle(PlainButtonStyle())
.animation(.easeOut, value: collapsed)
}
}
The result is this:
As you can see if I open the last expandableView is opens correctly. However if I open the first one when the second is closed, it actually opens the second. It only opens correctly the first one if the second is already open. It seems the VStack is not rendering correctly itself. Any ideas why this happening?
Thanks for the help.

I migth be the way the buttons works. Here is a cleaner solution:
struct ExpandableView: View {
let headerTitle: String
#State private var collapsed: Bool = true
var body: some View {
VStack(spacing: 2) {
Button(
action: {
withAnimation(.easeOut){
self.collapsed.toggle()
}
},
label: {
VStack {
Text("\(headerTitle) Header")
if !collapsed {
HStack {
Text("Text A")
Text("Text B")
}
}
}.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
.tint(.gray)
if(!self.collapsed) {
VStack {
Divider().background(.black)
Text("Content A")
Text("Content B")
Text("Content C")
}
}
Spacer()
}
.frame(height: collapsed ? 52 : 204)
.frame(maxWidth: .infinity)
.background(.gray)
.padding()
}
}

Related

How can I make a side bar in SwiftUI in navigationstack

I am trying to make a sidebar in swiftUI that is triggered in and out from the side with a button
I have been able to make it pop in and out from the bottom using a side modifier like this
struct sideBarExample: View {
#State var showSideBar = false
var mainView: some View{
Rectangle()
.foregroundColor(.blue)
.overlay(Text("Main View"))
}
var sideBar: some View{
Rectangle()
.foregroundColor(.green)
.overlay(Text("side bar"))
}
var body: some View {
NavigationStack{
mainView
.sheet(isPresented: $showSideBar, content: {
sideBar
})
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
showSideBar.toggle()
} label: {
Image(systemName: "sidebar.left")
}
}
}
}
}
}
But ideally it should be from the side
For iPhone you have to build your own sidebar, just overlay it in a ZStack and animate in with .transition.
struct ContentView: View {
#State private var showSideBar = false
var mainView: some View{
Rectangle()
.foregroundColor(.gray)
.overlay(Text("Main View"))
}
var sideBar: some View{
Rectangle()
.foregroundColor(.green)
.overlay(Text("side bar"))
.frame(width: 200)
}
var body: some View {
NavigationStack{
ZStack(alignment: .leading) {
mainView
if showSideBar {
sideBar
.transition(.move(edge: .leading))
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation {
showSideBar.toggle()
}
} label: {
Image(systemName: "sidebar.left")
}
}
}
}
}
}
You can use the offset view modifier to move the sidebar around
Here is an example
struct SideBarExample: View {
#State var showSideBar = false
var mainView: some View{
Rectangle()
.foregroundColor(.blue)
.overlay(Text("Main View"))
}
var sideBar: some View{
HStack{
Rectangle()
.foregroundColor(.green)
.overlay(Text("side bar"))
.frame(width:250)
Spacer()
}
}
var body: some View {
NavigationStack{
ZStack{
mainView
sideBar
.offset(CGSize(width: showSideBar ? 0:-250, height: 0))
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation {
showSideBar.toggle()
}
} label: {
Image(systemName: "sidebar.left")
}
}
}
}
}
}
Here is what that looks like

How to add images to buttons inside `confirmationDialog`

I'm building my first SwiftUI app and I've run into a blocker. When a user long-presses one of my cells, I want to show a confirmationdialog with custom buttons.
Here's the code:
.confirmationDialog("", isPresented: $showLongPressMenu) {
Button {
//
} label: {
HStack {
Image(systemName: "checkmark.circle")
Text("Add completion")
}
}
Button {
//
} label: {
HStack {
Image(systemName: "note.text.badge.plus")
Text("Add Note")
}
}
Button("Cancel", role: .cancel) {}
}
This is sort-of working, here's the result:
But what I'm trying to achieve is something like this:
Any pointers would be amazing, thank you.
Here is an approach I'm working on:
struct ContentView: View {
#State private var showConfirmationDialog = false
#State private var showModifierDialog = false
var body: some View {
VStack {
Button("Show Dialog") { showConfirmationDialog = true }
Button("Show ViewMod Dialog") {
withAnimation {
showModifierDialog = true
}
}
.padding()
}
.padding()
// standard confirmationDialog
.confirmationDialog("Test", isPresented: $showConfirmationDialog) {
Button { } label: {
Label("Add completion", systemImage: "checkmark.circle")
}
Button { } label: {
Label("Add Note", systemImage: "note.text.badge.plus")
}
Button("Cancel", role: .cancel) {}
}
// custom confirmationDialog with Icons, Cancel added automatically
.customConfirmDialog(isPresented: $showModifierDialog) {
Button {
// action
showModifierDialog = false
} label: {
Label("Add completion", systemImage: "checkmark.circle")
}
Divider() // unfortunately this is still necessary
Button {
// action
showModifierDialog = false
} label: {
Label("Add Note", systemImage: "note.text.badge.plus")
}
}
}
}
// *** Custom ConfirmDialog Modifier and View extension
extension View {
func customConfirmDialog<A: View>(isPresented: Binding<Bool>, #ViewBuilder actions: #escaping () -> A) -> some View {
return self.modifier(MyCustomModifier(isPresented: isPresented, actions: actions))
}
}
struct MyCustomModifier<A>: ViewModifier where A: View {
#Binding var isPresented: Bool
#ViewBuilder let actions: () -> A
func body(content: Content) -> some View {
ZStack {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
ZStack(alignment: .bottom) {
if isPresented {
Color.primary.opacity(0.2)
.ignoresSafeArea()
.onTapGesture {
isPresented = false
}
.transition(.opacity)
}
if isPresented {
VStack {
GroupBox {
actions()
.frame(maxWidth: .infinity, alignment: .leading)
}
GroupBox {
Button("Cancel", role: .cancel) {
isPresented = false
}
.bold()
.frame(maxWidth: .infinity, alignment: .center)
}
}
.font(.title3)
.padding(8)
.transition(.move(edge: .bottom))
}
}
}
.animation(.easeInOut, value: isPresented)
}
}
Here's a quick pure SwiftUI custom Dialog I wrote using resultBuilder avoiding the Divider approach:
Package URL: https://github.com/NoeOnJupiter/ComplexDialog/
Usage:
struct ContentView: View {
#State var isPresented = false
#State var color = Color.green
var body: some View {
Button(action: {
isPresented.toggle()
}) {
Text("This is a Test")
.foregroundColor(.white)
}.padding(20)
.background(color)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.presentDialog(isPresented: $isPresented, bodyContent: {
HStack {
Image(systemName: "circle.fill")
.foregroundColor(.red)
Text("Red Color")
Spacer()
}.dialogAction {
color = .red
}
HStack {
Image(systemName: "circle.fill")
.foregroundColor(.blue)
Text("Blue Color")
Spacer()
}.dialogAction {
color = .blue
}
HStack {
Image(systemName: "circle.fill")
.foregroundColor(.green)
Text("Green Color")
Spacer()
}.dialogAction {
color = .green
}
}, cancelContent: {
HStack {
Spacer()
Text("Cancel")
.font(.title3)
.fontWeight(.semibold)
Spacer()
}.dialogAction {
}
})
}
}

I use navigationlink and I also want to use side menu

I have a NavigationView component in my iOS app that's built with SwiftUI.
I use navigationlink to change my page.In the second screen, I want to show side menu.But now I havetwo layers NavigationView in my iOS app, it's not my want to show.I want the second screen only have side menu.
is it possible or am i missing something about the way navigation is being managed in Swift?
How should I change my code?
here's my ContentView.swift:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView{
VStack {
NavigationLink(
destination: SwiftUIView_map(),
label: {
Text("Skip")
.fontWeight(.bold)
.font(.title)
.padding()
.background(Color.purple)
.cornerRadius(20)
.foregroundColor(.white)
})
HeaderView()
.offset(y:-60)
NavigationLink(
destination: Text("Login"),
label: {
Text("Login")
.fontWeight(.bold)
.font(.title)
.padding()
.background(Color.purple)
.cornerRadius(20)
.foregroundColor(.white)
})
NavigationLink(
destination: Text("Sign up"),
label: {
Text("Sign up")
.fontWeight(.bold)
.font(.title)
.padding()
.background(Color.purple)
.cornerRadius(20)
.foregroundColor(.white)
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct HeaderView: View {
var body: some View {
HStack{
VStack(alignment: .leading, spacing: 2){
Text("Just")
.font(.system(size: 60,weight:.heavy,design:.rounded))
.fontWeight(.black)
.multilineTextAlignment(.leading)
Text("test")
.font(.system(size: 60,weight:.heavy,design:.rounded))
.fontWeight(.black)
}
Spacer()
}
.padding()
}
}
The navigation sidebar:
import SwiftUI
struct SwiftUIView_map: View {
#State private var isShowing = false
var body: some View {
NavigationView{
ZStack{
if isShowing{
SideMenuView(isShowing: $isShowing)
}
HomeView()
.cornerRadius(isShowing ? 20 : 10 )
.offset(x: isShowing ? 300 : 0,y:isShowing ? 44 :0)
.scaleEffect(isShowing ? 0.8 : 1)
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()){
isShowing.toggle()
}
}, label: {
Image(systemName: "list.bullet")
.foregroundColor(.black)
}))
.navigationTitle("home")
}
}
}
}
struct SwiftUIView_map_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView_map()
}
}
struct HomeView: View {
var body: some View {
ZStack{
Color(.white)
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
.padding()
}
}
}
If you want more detailed code, please click the link
https://github.com/simpson0826/swifttest.git
You need to use the NavigationView only in ContentView(root view).
struct SwiftUIView_map: View {
#State private var isShowing = false
var body: some View {
// NavigationView{
ZStack{
if isShowing{
SideMenuView(isShowing: $isShowing)
}
HomeView()
.cornerRadius(isShowing ? 20 : 10 )
.offset(x: isShowing ? 300 : 0,y:isShowing ? 44 :0)
.scaleEffect(isShowing ? 0.8 : 1)
.navigationBarItems(trailing: Button(action: {
withAnimation(.spring()){
isShowing.toggle()
}
}, label: {
Image(systemName: "list.bullet")
.foregroundColor(.black)
}))
.navigationTitle("home")
}
// }
}
}

Implementing custom sidebar across all views - SwiftUI

OUTLINE
I have made a custom slimline sidebar that I am now implementing across the whole app. The sidebar consists of a main button that is always showing and when pressed it shows or hides the rest of the sidebar that consists of buttons navigating to other views.
I am currently implementing the sidebar across the app on each view by creating a ZStack like this:
struct MainView: View {
var body: some View {
ZStack(alignment: .topLeading) {
SideBarCustom()
Text("Hello, World!")
}
}
}
PROBLEM
I am planning on adding a GeometryReader so if the side bar is shown the rest of the content moves over. With this in mind, the way I am implementing the sidebar on every view feels clunky and a long winded way to add it. Is there a more simple/better method to add this to each view?
Sidebar Code:
struct SideBarCustom: View {
#State var isToggle = false
var names = ["Home", "Products", "Compare", "AR", "Search"]
var icons = ["house.fill", "printer.fill.and.paper.fill", "list.bullet.rectangle", "arkit", "magnifyingglass"]
var imgSize = 20
var body: some View {
GeometryReader { geo in
VStack {
Button(action: {
self.isToggle.toggle()
}, label: {
Image("hexagons")
.resizable()
.frame(width: 40, height: 40)
.padding(.bottom, 20)
})
if isToggle {
ZStack{
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.red)
.frame(width: 70, height: geo.size.height)
VStack(alignment: .center, spacing: 60) {
ForEach(Array(zip(names, icons)), id: \.0) { item in
Button(action: {
// NAVIIGATE TO VIEW
}, label: {
VStack {
Image(systemName: item.1)
.resizable()
.frame(width: CGFloat(imgSize), height: CGFloat(imgSize))
Text(item.0)
}
})
}
}
}
}
}
}
}
}
I don't think there's necessarily a reason to use GeometryReader here. The following is an example that has a dynamic width sidebar (although you could set it to a fixed value) that slides in and out. The main content view resizes itself automatically, since it's in an HStack:
struct ContentView : View {
#State private var sidebarShown = false
var body: some View {
HStack {
if sidebarShown {
CustomSidebar(sidebarShown: $sidebarShown)
.frame(maxHeight: .infinity)
.border(Color.red)
.transition(sidebarShown ? .move(edge: .leading) : .move(edge: .trailing) )
}
ZStack(alignment: .topLeading) {
MainContentView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
if !sidebarShown {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
}
}
}
}
}
struct CustomSidebar : View {
#Binding var sidebarShown : Bool
var body: some View {
VStack {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
Spacer()
Text("Hi")
Text("There")
Text("World")
Spacer()
}
}
}
struct MainContentView: View {
var body: some View {
VStack {
Text("Main content")
}
}
}

NavigationView and NavigationLink on button click in SwiftUI?

I am trying to push from login view to detail view but not able to make it.even navigation bar is not showing in login view. How to push on button click in SwiftUI? How to use NavigationLink on button click?
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Let's get you signed in.")
.bold()
.font(.system(size: 40))
.multilineTextAlignment(.leading)
.frame(width: 300, height: 100, alignment: .topLeading)
.padding(Edge.Set.bottom, 50)
Text("Email address:")
.font(.headline)
TextField("Email", text: $email)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Text("Password:")
.font(.headline)
SecureField("Password", text: $password)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Button(action: {
print("login tapped")
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal,30)
}
.navigationBarTitle(Text("Login"))
}
To fix your issue you need to bind and manage tag with NavigationLink, So create one state inside you view as follow, just add above body.
#State var selection: Int? = nil
Then update your button code as follow to add NavigationLink
NavigationLink(destination: Text("Test"), tag: 1, selection: $selection) {
Button(action: {
print("login tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
Meaning is, when selection and NavigationLink tag value will match then navigation will be occurs.
I hope this will help you.
iOS 16+
Note: Below is a simplified example of how to present a new view. For a more advanced generic example please see this answer.
In iOS 16 we can access the NavigationStack and NavigationPath.
Usage #1
A new view is activated by a simple NavigationLink:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: "NewView") {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #2
A new view is activated by a standard Button:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button {
path.append("NewView")
} label: {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #3
A new view is activated programmatically:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("Content View")
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
.onAppear {
path.append("NewView")
}
}
}
iOS 13+
The accepted answer uses NavigationLink(destination:tag:selection:) which is correct.
However, for a simple view with just one NavigationLink you can use a simpler variant: NavigationLink(destination:isActive:)
Usage #1
NavigationLink is activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
}
.navigationBarTitle(Text("Login"))
}
}
}
Usage #2
NavigationLink is hidden and activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
}
}
Usage #3
NavigationLink is hidden and activated programmatically:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
.onAppear {
self.isLinkActive = true
}
}
}
Here is a GitHub repository with different SwiftUI extensions that makes navigation easier.
Another approach:
SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: BaseView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
BaseView
import SwiftUI
struct BaseView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "view1" {
FirstView()
} else if viewRouter.currentPage == "view2" {
SecondView()
.transition(.scale)
}
}
}
}
#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
BaseView().environmentObject(ViewRouter())
}
}
#endif
ViewRouter
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "view1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
FirstView
import SwiftUI
struct FirstView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {self.viewRouter.currentPage = "view2"}) {
NextButtonContent()
}
}
}
}
#if DEBUG
struct FirstView_Previews : PreviewProvider {
static var previews: some View {
FirstView().environmentObject(ViewRouter())
}
}
#endif
struct NextButtonContent : View {
var body: some View {
return Text("Next")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
SecondView
import SwiftUI
struct SecondView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Spacer(minLength: 50.0)
Button(action: {self.viewRouter.currentPage = "view1"}) {
BackButtonContent()
}
}
}
}
#if DEBUG
struct SecondView_Previews : PreviewProvider {
static var previews: some View {
SecondView().environmentObject(ViewRouter())
}
}
#endif
struct BackButtonContent : View {
var body: some View {
return Text("Back")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
Hope this helps!
Simplest and most effective solution is :
NavigationLink(destination:ScoresTableView()) {
Text("Scores")
}.navigationBarHidden(true)
.frame(width: 90, height: 45, alignment: .center)
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.contentShape(Rectangle())
.padding(EdgeInsets(top: 16, leading: UIScreen.main.bounds.size.width - 110 , bottom: 16, trailing: 20))
ScoresTableView is the destination view.
In my opinion a cleaner way for iOS 16+ is using a state bool to present the view.
struct ButtonNavigationView: View {
#State private var isShowingSecondView : Bool = false
var body: some View {
NavigationStack {
VStack{
Button(action:{isShowingSecondView = true} ){
Text("Show second view")
}
}.navigationDestination(isPresented: $isShowingSecondView) {
Text("SecondView")
}
}
}
}
I think above answers are nice, but simpler way should be:
NavigationLink {
TargetView()
} label: {
Text("Click to go")
}

Resources