SwiftUI: Button with offset not working in ScrollView - ios

I'm trying to make this Profile Button to work inside a ScrollView, that when tapped, a modal view should come up. But for some reason, when I add some offset to the Button, to make it inline with the Navigation Title, the Button stops working.
Does anyone know a fix for this?
Here's my code:
import SwiftUI
struct TestView: View {
#State var showMenu = false
var body: some View {
NavigationView {
ScrollView {
VStack {
HStack {
Spacer()
Image(systemName: "person.crop.circle")
.font(.system(size: 36))
.foregroundColor(.blue)
.frame(width: 44, height: 44)
.onTapGesture {
showMenu.toggle()
}
.offset(y: -64)
}
Spacer()
}
.padding()
.sheet(isPresented: $showMenu) {
MenuView()
}
}
.navigationTitle("Browse")
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}

Related

SwiftUI Toolbar item getting clipped when back button is pressed

I've run in to a strange behavior in SwiftUI that I can't seem to work around.
Given the following simple example app I experience this behavior: The toolbar item renders correctly on the initial run, but navigating away and returning it gets clipped.
Sample code to recreate this:
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("Hello, world!")
.padding()
.navigationTitle("View 1")
.toolbar {
Circle()
.fill(Color.red)
.frame(width: 150, height: 150, alignment: .center)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
View2.swift
import SwiftUI
struct View2: View {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
}
}
struct View2_Previews: PreviewProvider {
static var previews: some View {
View2()
}
}
It is clipped by navigation bar as seen on view debug below so it is just rendering issue (should be clipped always):
A possible solution is to use that widget (Circle in the case) above NavigationView and align it with toolbar item.
Here is main part:
.toolbar {
Color.clear
.frame(width: 150)
.overlay(GeometryReader {
Color.clear.preference(key: ViewPointKey.self,
value: [$0.frame(in: .global).center])
})
}
//...
Circle().fill(Color.red)
.frame(width: 150, height: 150)
.position(x: pos.x, y: pos.y) // << here !!
//...
.onPreferenceChange(ViewPointKey.self) {
pos = $0.first ?? .zero
}
Complete findings and code is here

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

How to show the onboarding view as a sheet?

I'm trying to show my onboarding view as a sheet. Is there anyone here who can help me?
Here's how it looks right now:
I want it to be displayed as a sheet from top to bottom. I tried to do it, but it doesn't work. Is there a solution to this?
Here's the code:
Login view
import SwiftUI
struct LoginView: View {
#AppStorage("needsAppOnboarding") private var needsAppOnboarding: Bool = false
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
.sheet(isPresented: $needsAppOnboarding) {
OnboardingView()
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
OnboardingView
import SwiftUI
struct OnboardingView: View {
var body: some View {
VStack(spacing: 20) {
Spacer()
Image("wulkanowy-svg")
.resizable()
.frame(width: 200, height: 200, alignment: .top)
.foregroundColor(Color("OnboardingColor"))
VStack(spacing: 20) {
Text("onboarding.description.title")
.font(.headline)
.multilineTextAlignment(.center)
Text("onboarding.description.content")
.font(.subheadline)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 20)
Spacer()
OnboardingButtonView()
.padding()
}
}
}
struct WulkanowyCardView_Previews: PreviewProvider {
static var previews: some View {
Group {
OnboardingView().previewLayout(.fixed(width: 320, height: 640))
}
}
}
OnboardingButtonView
import SwiftUI
struct OnboardingButtonView: View {
#AppStorage("needsAppOnboarding") var needsAppOnboarding: Bool = true
var body: some View {
Button(action: {
needsAppOnboarding = false
}, label: {
Text("onboarding.continue")
})
.padding(10)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color("OnboardingColor"))
.foregroundColor(.white)
.font(.title)
.cornerRadius(20)
}
}
struct OnboardingButtonView_Previews: PreviewProvider {
static var previews: some View {
OnboardingButtonView()
.previewLayout(.sizeThatFits)
}
}

SwiftUI unable to click buttons in overlay/popover screen

I have a view that I present at the top of other views like a popover view, inside the view I have a couple of buttons. When I add a single button in the overplayed view and tap the button it works. However if I added multiple buttons and try to tap the buttons they don't work. Instead it clicks to the components bellow the view.
I would like to add multiple buttons and click them on the overlay view, I'm not sure what my mistake is on this code:
Here is my code:
struct MenuContent: View {
var body: some View {
List() {
ForEach(0..<2) { _ in
HStack {
ForEach(0..<4) { _ in
Button(action: {
print("tapped button")
}) {
VStack {
Text("Rev")
Image("trash.fill")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
}
}.background(Color.blue)
}
}
}
}
}
}
OverlayView
struct OverlayMenu: View {
let width: CGFloat
#Binding var show: Bool
var body: some View {
return ZStack {
HStack {
MenuContent()
.frame(width: self.width, height: 160)
.cornerRadius(10, antialiased: false)
.offset(x: self.show ? 0 : -self.width, y: 285)
.animation(.spring())
Spacer()
}
.shadow(radius: 20)
}
}
}
ContentView
struct ContentView: View {
#State var show = true
var body: some View {
OverlayMenu(width: 350,
show: $show)
}
}
I think there is some trouble with List rows and tap gestures on them. You can deal with it if you want or you may try VStack instead of List and use Divider to divide "rows" and taps on the buttons will be handle as you expect. I changed your example a little to show how it works, I think you can handle design by yourself then:
struct MenuContent: View {
#State var hits = 0
var body: some View {
VStack {
Text("\(hits)")
Divider().frame(width: 250)
ForEach(0..<2) { _ in
ButtonsLine(hits: self.$hits)
Divider().frame(width: 250)
}
}
}
}
struct ButtonsLine: View {
#Binding var hits: Int
var body: some View {
HStack {
ForEach(0..<4) { value in
Button(action: {
print("tapped button")
self.hits += value + 1
}) {
ButtonDesign()
}
}
}
}
}
struct ButtonDesign: View {
var body: some View {
VStack {
Text("Rev")
.foregroundColor(.black)
Image(systemName: "trash")
.resizable()
.scaledToFit()
.foregroundColor(.red)
.frame(width: 60, height: 60)
}
.shadow(radius: 20)
}
}
and the result is:

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