My app running on Swift UI, and my main page is Home(), In the home page there is NavigationView and NavigationLink(destination: SaveThePlanet()), I have hide the Navigation View on the main page "Home", its also hide in SaveThePlanet().
How can I unhide the navigation back button in the SaveThePlanet() page?
import SwiftUI
struct Home: View {
#State var show = false
#State var showSaveThePlanet = false
var body: some View {
NavigationView {
ZStack {
Color.gray
ContentView()
.blur(radius: show ? 10 : 0)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: showSaveThePlanet ? 10 : 0)
.scaleEffect(showSaveThePlanet ? 0.90 : 1)
.animation(.default)
leftIcon(show: $show)
.offset(x: 0, y: showSaveThePlanet ? 300 : 70)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: show ? 10 : 0)
.animation(.easeInOut)
SaveThePlanet()
.background(Color("Bg"))
.cornerRadius(10)
.shadow(color: Color("Green-Sh"), radius: 10, x: 0, y: 0)
.animation(.spring())
.offset(y: showSaveThePlanet ? 120 : UIScreen.main.bounds.height)
.padding()
rightIcon(show: $showSaveThePlanet)
.offset(x: 0, y: 70)
.animation(.easeInOut)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: show ? 10 : 0)
.opacity(showSaveThePlanet ? 0 : 1)
rightIconClose(show: $showSaveThePlanet)
.offset(x: 0, y: 70)
.animation(.easeInOut)
.scaleEffect(show ? 0.90 : 1)
.blur(radius: show ? 10 : 0)
.opacity(showSaveThePlanet ? 1 : 0)
MenuView(show: $show)
}
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("Home")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(false)
}
}
}
What worked for me : have an #State property on your first view, that determines whether or not you can show the navigation bar. Then pass that property on to all subsequent views via #Binding, so that it is the 'single source of truth' for whether or not the navigation bar should show.
#State private var navBarHidden = false
Then on your main view, reference that property for the navBarHidden property, and set the title. Also add an onAppear closure, which will set that hidden property for when this view re-appears, ie if we pop back here from a detail view.
var body: some View {
NavigationView {
NavigationLink(
destination: DetailView(navBarHidden: self.$navBarHidden)
) {
Text("Go to detail view")
}
}
.navigationBarTitle("")
.navigationBarHidden(self.navBarHidden)
.onAppear(perform: {
self.navBarHidden = true
})
}
Then on a subsequent detail view, pass that navBarHidden property on as an #Binding (its passed in above)
#Binding var navBarHidden : Bool
var body: some View {
Text("Hello Detail View!")
.navigationBarTitle("Detail")
.onAppear() {
self.navBarHidden = false
}
}
And when the onAppear() is called above in the detail view, it sets that original property to false for hidden, which shows the nav bar. And when you click back to return to the home view, the onAppear() of the home view is called again, which sets it back to hidden = true.
I'm answering because I think the solution nowadays is pretty easier. So for people having the same problem just add a
.navigationBarHidden(true)
on your Home() component and you should be fine. This solution works for sure in Swift 5.5, there is to do the onAppear and onDisappear trick of the other answers. It will hide only the navigation bar on the view you specified
It's a little hard to tell based on the code you've posted, but it looks like you are trying to present a view that slides up from the bottom when showSaveThePlanet is true, and also show the navigation bar only when that view appears.
This can be accomplished by setting .navigationBarHidden(!showSaveThePlanet) anywhere in your body property. Note that your code does not use NavigationLink anywhere to push a new view onto the NavigationView stack, so you would not get a back button. You can add your own button to dismiss the sheet using .navigationBarItems(leading:)
Here is a simplified example showing what I mean.
struct ContentView: View {
#State private var detailShowing = false
var body: some View {
NavigationView {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
Color.gray.edgesIgnoringSafeArea(.all)
// A card-like view that is initially offscreen,
// and slides on when detailShowing == true
DetailView()
.offset(x: 0, y: detailShowing ? 120 : UIScreen.main.bounds.height)
.animation(.spring())
// Just here to change state
Button("Toggle") {
self.detailShowing.toggle()
}
.padding()
.offset(x: 0, y: detailShowing ? 0 : 44)
.animation(.none)
}
// This is the key modifier
.navigationBarHidden(!detailShowing)
.navigationBarTitle("Detail View", displayMode: .inline)
.navigationBarItems(leading: Button("Close") {
self.detailShowing = false
})
}
}
}
struct DetailView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
RoundedRectangle(cornerRadius: 15).fill(Color.secondary).frame(width: 300, height: 500)
Text("Detail Content")
.padding()
}
}
}
Best way that I found is toggling the navBarHidden in destination views. This way incomplete swipe to pop gestures will not remove the navigation bar.
So in your main view you would write
#State private var navBarHidden = true
var body: some View {
NavigationView {
NavigationLink(
destination: DetailView(navBarHidden: self.$navBarHidden)
) {
Text("Go to detail view")
}
}
.navigationBarTitle("")
.navigationBarHidden(self.navBarHidden)
}
And in your destination view:
#Binding var navBarHidden : Bool
var body: some View {
Text("Hello Detail View!")
.navigationBarTitle("Detail")
.onAppear {
self.navBarHidden = false
}
.onDisappear {
self.navBarHidden = true
}
}
Related
I'm trying two write a custom Datepicker so that I can modify the button style. As far as I know, SwiftUI doesn't allow to modify it. My base design based on this answer that I've already improved it.
struct CustomDatePickerView: View {
#State private var showPicker = false
#State var selectedText: String
#State var selectedDateLocal : Date = Date()
var body: some View {
VStack {
Button {
withAnimation {
showPicker.toggle()
}
} label: {
Text(selectedText)
.padding()
.padding(.horizontal)
.foregroundColor(.black)
.background(
RoundedRectangle( cornerRadius:10, style: .continuous).fill(Color.yellow.opacity(0.2))
)
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(Color.black, lineWidth: 1)
)
.id(selectedText)
}
.background(
DatePicker("", selection: $selectedDateLocal, in: closedRange, displayedComponents: [.date,.hourAndMinute])
.datePickerStyle(.graphical)
.frame(width: 400, height: 400)
.clipped()
.background(Color.yellow.opacity(0.1).cornerRadius(10))
.opacity(showPicker ? 1 : 0 )
.offset(x: 0, y: 230)
).onChange(of: selectedDateLocal) { newValue in
let format = DateFormatter()
format.timeStyle = .none
format.dateStyle = .short
print("Name changed to \(format.string(from: newValue))!")
selectedText = format.string(from: newValue)
withAnimation {
showPicker.toggle()
}
}
}
}
}
It was working excellent on test view (sorry image upload failure).
When it is placed on the real application, the background/overlay was behind of other views.
I cannot change the Z-level since the UI a bit complicated.
How can we show the DatePicker displayed on background/overlay on top of everything as the DatePicker does? What is the proper way to do this?
I have a TabView that is showing 4 different views. The whole tabView is set to:
.tabViewStyle(PageTabViewStyle())
(So I can make it Swipe-enabled instead of using the buttons)
I also have an HStack of simple rectangles that should highlight different colors when their respective pages are displayed. (The first rectangle will be showed as blue when the first tab is shown, the second rectangle is blue when the second page is active, etc.) I'm doing this with an onAppear on each of the views.
ViewOne()
.onAppear {
print("1")
activeTab = 1
}
If I only use two views in the tabView, this works fine. But as soon as I add the third, the onAppear goes crazy. When I swipe to the third view the print statement adds to the console in quick succession:
3
1
4
And the blue rectangle is the 4th one. The 4th page throws it off even more and things just gets strange.
My question is this: are the tabViews sort of like the dequeResuableCell of a UIKit TableView, or am I doing something wrong? Might it be a bug? and if so, is there a workaround for this that doesn't use onAppear?
EDIT:
Here's the full code implementation:
VStack {
HStack {
Rectangle()
.foregroundColor((activeTab == 1) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 2) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 3) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 4) ? .blue : .red)
}
.frame(height: 100)
.padding()
TabView(selection: $activeTab) {
// each one of the Views below has the .onAppear attached to them
viewTwo(activeTab: $activeTab)
viewTwo(activeTab: $activeTab)
viewThree(activeTab: $activeTab)
viewFour(activeTab: $activeTab)
}
.tabViewStyle(PageTabViewStyle())
}
}
So it turns out I was overcomplicating this A LOT! Rather than attempting to put an .onAppear on each of the associated views (which I've come to realize is not always perfectly reliable), I just used:
.onChange(of: activeTab, perform: { value in
print(activeTab)
})
This stabilized the code and performs the way I wanted it to.
My full code is below if it might be able to help anyone!
struct EvalView: View {
#State var activeTab = 1
var body: some View {
VStack {
HStack {
Rectangle()
.foregroundColor((activeTab == 1) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 2) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 3) ? .blue : .red)
Rectangle()
.foregroundColor((activeTab == 4) ? .blue : .red)
}
.frame(height: 100)
.padding()
TabView(selection: $activeTab) {
viewOne(activeTab: $activeTab)
.tag(1)
viewTwo(activeTab: $activeTab)
.tag(2)
viewThree(activeTab: $activeTab)
.tag(3)
viewFour(activeTab: $activeTab)
.tag(4)
}
.tabViewStyle(PageTabViewStyle())
.onChange(of: activeTab, perform: { value in
print(activeTab)
})
}
}
}
struct viewOne: View {
#Binding var activeTab: Int
var body: some View {
Text("1")
}
}
struct viewTwo: View {
#Binding var activeTab: Int
var body: some View {
Text("2")
}
}
struct viewThree: View {
#Binding var activeTab: Int
var body: some View {
Text("3")
}
}
struct viewFour: View {
#Binding var activeTab: Int
var body: some View {
Text("4")
}
}
struct EvalView_Previews: PreviewProvider {
static var previews: some View {
EvalView()
}
}
I tried to do a app that pop out a temporary alert that only appear for 1 or 2 seconds. It’s something like App Store rating.
But I don’t know what this called in swiftui. Can anyone answer me?
That is just a view that is shown or hidden conditionally. Here is a complete example that uses a ZStack to place the thank you view over the other view content. The thank you view is either present or not based upon the #State variable showThankYou. DispatchQueue.main.asyncAfter is used to remove the view after 3 seconds.
struct ContentView: View {
#State private var showThankYou = false
var body: some View {
ZStack {
VStack {
Spacer()
Text("Stuff in the view")
Spacer()
Button("submit") {
showThankYou = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.showThankYou = false
}
}
Spacer()
Text("More stuff in the View")
Spacer()
}
if showThankYou {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(Color.gray)
.frame(width: 250, height: 250)
.overlay(
VStack {
Text("Submitted").font(.largeTitle)
Text("Thanks for your feedback").font(.body)
}
)
}
}
}
}
I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}
In SwiftUI, I want a button to appear from off screen by dropping in from the top into a final position when the view is initially displayed, I'm not asking for animation when the button is pressed.
I have tried:
Button(action: {}) {
Text("Button")
}.offset(x: 0.0, y: 100.0).animation(.basic(duration: 5))
but no joy.
If you would like to play with offset, this can get you started.
struct ContentView : View {
#State private var offset: Length = 0
var body: some View {
Button(action: {}) { Text("Button") }
.offset(x: 0.0, y: offset)
.onAppear {
withAnimation(.basic(duration: 5)) { self.offset = 100.0 }
}
}
}
I first suggested a .transition(.move(.top)), but I am updating my answer. Unless your button is on the border of the screen, it may not be a good fit. The move is limited to the size of the moved view. So you may need to use offset after all!
Note that to make it start way out of the screen, the initial value of offset can be negative.
First of all you need to create a transition. You could create an extension for AnyTransition or just create a variable. Use the move() modifier to tell the transition to move the view in from a specific edge
let transition = AnyTransition.move(edge: .top);
This alone only works if the view is at the edge of the screen. If your view is more towards the center you can use the combined() modifier to combine another transition such as offset() to add additional offset
let transition = AnyTransition
.move(edge: .top)
.combined(with:
.offset(
.init(width: 0, height: 100)
)
);
This transition will be for both showing and removing a view although you can use AnyTransition.asymmetric() to use different transitions for showing and removing a view
Next create a showButton bool (name this whatever) which will handle showing the button. This will use the #State property wrapper so SwiftUI will refresh the UI when changed.
#State var showButton: Bool = false;
Next you need to add the transition to your button and wrap your button within an if statement checking if the showButton bool is true
if (self.showButton == true) {
Button(action: { }) {
Text("Button")
}
.transition(transition);
}
Finally you can update the showButton bool to true or false within an animation block to animate the button transition. toggle() just reverses the state of the bool
withAnimation {
self.showButton.toggle();
}
You can put your code in onAppear() and set the bool to true so the button is shown when the view appears. You can call onAppear() on most things like a VStack
.onAppear {
withAnimation {
self.showButton = true;
}
}
Check the Apple docs to see what is available for AnyTransition https://developer.apple.com/documentation/swiftui/anytransition
Presents a message box on top with animation:
import SwiftUI
struct MessageView: View {
#State private var offset: CGFloat = -200.0
var body: some View {
VStack {
HStack(alignment: .center) {
Spacer()
Text("Some message")
.foregroundColor(Color.white)
.font(Font.system(.headline).bold())
Spacer()
}.frame(height: 100)
.background(Color.gray.opacity(0.3))
.offset(x: 0.0, y: self.offset)
.onAppear {
withAnimation(.easeOut(duration: 1.5)) { self.offset = 000.0
}
}
Spacer()
}
}
}
For those that do want to start from a Button that moves when you tap on it, try this:
import SwiftUI
struct ContentView : View {
#State private var xLoc: CGFloat = 0
var body: some View {
Button("Tap me") {
withAnimation(.linear(duration: 2)) { self.xLoc+=50.0 }
}.offset(x: xLoc, y: 0.0)
}
}
Or alternatively (can replace Text with anything):
Button(action: {
withAnimation(.linear(duration: 2)) { self.xLoc+=50.0 }
} )
{ Text("Tap me") }.offset(x: xLoc, y: 0.0)