I'm trying to add a full screen View over my app in SwiftUI. The purpose of this is to have a "shade" that fades in that will darken the screen and bring focus to a custom pop-up, disabling content in the background. See below for a visual example:
The View that I'm trying to add this shade over is embedded in a complex NavigationView stack (several layers deep, accessed via a NavigationLink) and also has a visible TabBar. So far I've tried embedding the NavigationView in a ZStack and adding a Rectangle() on top but to no avail, the NavigationBar and TabBar still sit on top of this view. I've also tried using using the .zIndex() modifier but this seems to have done nothing.
Any help is much appreciated,
Thanks
You do not need work on zIndex, because you cover the all screen! Even you do not need work on disable your current View for using PopUp, because again PopUp is already on top layer. zIndex would be helpful when you did not cover the screen, here is a way:
struct ContentView: View {
#State private var isPresented: Bool = Bool()
var body: some View {
NavigationView {
VStack {
Button("Show Custom PopUp View") { isPresented.toggle() }
}
.navigationTitle("Navigation Title")
}
.customPopupView(isPresented: $isPresented, popupView: { popupView })
}
var popupView: some View {
RoundedRectangle(cornerRadius: 20.0)
.fill(Color.white)
.frame(width: 300.0, height: 200.0)
.overlay(
Image(systemName: "xmark").resizable().frame(width: 10.0, height: 10.0)
.foregroundColor(Color.black)
.padding(5.0)
.background(Color.red)
.clipShape(Circle())
.padding()
.onTapGesture { isPresented.toggle() }
, alignment: .topLeading)
.overlay(Text("Custom PopUp View!"))
.transition(AnyTransition.scale)
.shadow(radius: 10.0)
}
}
struct CustomPopupView<Content, PopupView>: View where Content: View, PopupView: View {
#Binding var isPresented: Bool
#ViewBuilder let content: () -> Content
#ViewBuilder let popupView: () -> PopupView
let backgroundColor: Color
let animation: Animation?
var body: some View {
content()
.animation(nil, value: isPresented)
.overlay(isPresented ? backgroundColor.ignoresSafeArea() : nil)
.overlay(isPresented ? popupView() : nil)
.animation(animation, value: isPresented)
}
}
extension View {
func customPopupView<PopupView>(isPresented: Binding<Bool>, popupView: #escaping () -> PopupView, backgroundColor: Color = .black.opacity(0.7), animation: Animation? = .default) -> some View where PopupView: View {
return CustomPopupView(isPresented: isPresented, content: { self }, popupView: popupView, backgroundColor: backgroundColor, animation: animation)
}
}
This is how I would so it.
struct ContentView: View {
#State var showingShade = false
var body: some View {
ZStack{
// Your other views goes here
if showingShade{
Rectangle()
.ignoresSafeArea()
.foregroundColor(.black)
.opacity(0.5)
}
}
}
}
And then simply set showingShade = true when you want the shade to appear. It might be a good idea to use the same var as your PopUp.
For disabling the view you can use the .disabled() modifier on the specific view you want to disable.
Related
The question might look familiar, but I went through all solutions on this topic but none had a working approach for the latest versions of SwiftUI and iOS.
So here is my tab view, I am trying to animate when switching between the tabs. I tried binding animations by adding animation to the binding and that does not work. I also tried attaching the onChange modifier on the TabView itself which prints the correctly selected tab but it does not animate so neither approach works could someone point to the correct implementation of this?
struct MainTabScreen: View {
#State private var selectedTab = 0
var body: some View {
// The binding animation does not animate
TabView (selection: $selectedTab.animation(
.easeInOut(duration: 1.0))
) {
Home()
.tabItem {
Image(systemName: "house")
Text("Home")
}
.tag(0)
Dollar()
.tabItem {
Image(systemName: "dollarsign.circle")
Text("Dollar")
}
.tag(1)
Menu()
.tabItem {
Image(systemName: "plus")
Text("Menu")
}
.tag(2)
}
.onChange(of: selected, perform: { tab in
print("TAPPED ON :\(tab)") // prints correct tab
withAnimation(.easeInOut(duration: 2)) {
selectedTab = tab // does not animate
}
})
}
}
Most solutions online advise using .animation(.easeInOut) to the tab view itself, but this is now deprecated. Looking for a solution that works with iOS 15 and Swift 5.
SwiftUI.TabView is one of those Views that just offer the basic Apple look.
You can easily substitute that SwiftUI.TabView with your own so you can add any animations, transitions, colors that work for you app.
import SwiftUI
struct MainTabScreen: View {
#State private var selectedTab: Tabs = .home
var body: some View {
VStack{
//Present only the View that is selected
selectedTab.view()
// You can also apply transitions if you want
//.transition(.slide)
//Have the selected View take up all the available space
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
//This is the "TabView"
HStack{
ForEach(Tabs.allCases, id:\.rawValue){ tab in
tab.label()
//Change the color for the selected case
.foregroundColor(selectedTab == tab ? Color.blue : nil)
//Select the animation and change the tab using your desired abimation
.onTapGesture {
withAnimation(.easeInOut){
selectedTab = tab
}
}
//Stretch the Views so they take up the entire bottom
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
//Set the height of the TabView
.frame( height: 80, alignment: .center)
}
}
//Enum that keeps track of the New TabView's Views
enum Tabs:String,CaseIterable,CustomStringConvertible{
case home
case dollar
case menu
///Formatted name for the View
var description: String{
rawValue.capitalized
}
///Image that represents the View
#ViewBuilder func image() -> some View{
switch self {
case .home:
Image(systemName: "house")
case .dollar:
Image(systemName: "dollarsign.circle")
case .menu:
Image(systemName: "plus")
}
}
///Primary View for the tab
#ViewBuilder func view() -> some View{
switch self {
case .home:
Text("Home()")
case .dollar:
Text("Dollar()")
case .menu:
Text("Menu()")
}
}
///Label for the TabView
#ViewBuilder func label() -> some View{
switch self {
default:
VStack{
image()
Text(description)
}
}
}
}
}
struct MainTabScreen_Previews: PreviewProvider {
static var previews: some View {
MainTabScreen()
}
}
I'm trying to add to my app bottom sheet with responsive height which I can set programmatically. For this purpose I'm trying to use this video. Here is code of my view controller:
struct SecondView: View {
#State var cardShown = false
#State var cardDismissal = false
var body: some View {
Button {
cardShown.toggle()
cardDismissal.toggle()
} label: {
Text("Show card")
.bold()
.foregroundColor(Color.white)
.background(Color.red)
.frame(width: 200, height: 50)
}
BottomCard(cardShown: $cardShown, cardDismissal: $cardDismissal) {
CardContent()
}
}
}
struct CardContent:View{
var body: some View{
Text("some text")
}
}
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.opacity(cardShown ? 1 : 0)
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
but after pressing the button I don't see any pushed bottom menu. I checked and it seems that I have similar code to this video but on the video bottom sheet appears. Maybe I missed something important for menu showing. The main purpose is to show bottom menu with responsive height which will wrap elements and will be able to change menu height. I tried to use .sheet() but this element has stable height as I see. I know that from the ios 15+ we will have some solutions for this problem but I would like to create something more stable and convenient :)
iOS 16
We can have native SwiftUI resizable sheet (like UIKit). This is possible with the new .presentationDetents() modifier.
.sheet(isPresented: $showBudget) {
BudgetView()
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
Demo:
This is what I got when running your code
I got this after some adjustments to bottom card
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
Spacer()
}
}.edgesIgnoringSafeArea(.all)
.opacity(cardShown ? 1 : 0)
}
}
So you just need to set the height!
what you want to do is to have a card that only exists when there is a certain standard met.
If you want to push up a card from the bottom then you can make a view of a card and put it at the bottom of a Zstack view using a geometry reader and then make a button that only allows for that card to exist when the button is pressed INSTEAD of trying to hire it by changing its opacity. Also, make sure you move the dismissal button to the inside of the cad you have.
Heres an example you can try :
struct SecondView: View {
#State var cardShown = false
var body: some View {
GeometryReader{
ZStack {
ZStack{
// I would also suggest getting used to physically making your
//button and then giving them functionality using a "Gesture"
Text("Show Button")
.background(Rectangle())
.onTapGesture{
let animation = Animation.spring()
withAnimation(animation){
self.cardShown.toggle
}
}
}
ZStack {
if cardShown == true{
BottomCard(cardShown: $cardShown) {
CardContent()
}
}
// here you can change how far up the card comes after the button
//is pushed by changing the "0"
.offset(cardShown == false ? geometry.size.height : 0)
}
}
}
}
}
Also, you don't need to have a variable for the card being shown and a variable for the card being dismissed. Just have one "cardShown" variable and make it so that when it is TRUE the card is shown and when it is FALSE (after hitting the button on the card or hitting the initial button again.) the card goes away.
iOS 16.0+
iPadOS 16.0+
macOS 13.0+
Mac Catalyst 16.0+
tvOS 16.0+
watchOS 9.0+
Use presentationDetents(_:)
struct ContentView: View {
#State private var isBottomSheetVisible = false
var body: some View {
Button("View Settings") {
isBottomSheetVisible = true
}
.sheet(isPresented: $isBottomSheetVisible) {
Text("Bottom Sheet")
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
}
}
When a View is pressed I know through a model button.isSelected. How do I animate the view's foreground color, similar to the IOS calculators button press animation?
Something like:
White -> Grey -> White
struct ButtonView: View {
let button: ViewModel.Button
var body: some View {
let shape = Rectangle()
ZStack {
shape.fill().foregroundColor(button.isSelected ? Color.gray : Color.white)
.animation(Animation.linear(duration: 0.01))
.border(Color.black, width: 0.33)
Text(button.content)
.font(Font.system(size:32))
}
}
}
I think there are many ways to do this.
Among them, I will write an example using DispatchQueue.main.asyncAfter()
struct ContentView: View {
#State private var isSelected: Bool = false
var body: some View {
VStack {
Button {
isSelected = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2 ) {
// To change the time, change 0.2 seconds above
isSelected = false
}
} label: {
Text("Button")
.foregroundColor(isSelected ? Color.red : Color.blue)
}
}
}
}
While DispatchQueue.main.asyncAfter() will work as Taeeun answered, note how the calculator app doesn't use a set delay. Instead, it changes color when the finger presses down, then reverts back upon release.
So, you probably want something like ButtonStyle.
struct ContentView: View {
var body: some View {
ButtonView()
}
}
struct CalculatorButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding() /// no need to use `shape` + `ZStack`, normal padding is ok
.background(configuration.isPressed ? Color.gray : Color.white) /// use `isPressed` to determine if button is currently pressed or not
.animation(Animation.linear(duration: 0.01))
.cornerRadius(10)
}
}
struct ButtonView: View {
var body: some View {
ZStack {
Color.black /// for testing purposes (see the button better)
Button {} label: {
Text("Button")
.font(.system(size: 32))
}
.buttonStyle(CalculatorButtonStyle()) /// apply the style
}
}
}
Result:
I have a problem regarding behavior of an animated loading view. The loading view shows up while the network call. I have an isLoading #Published var inside my viewModel and the ActivityIndicator is shown inside a ZStack in my view. The Activityindicator is a custom view where I animate a trimmed circle - rotate it. Whenever the activity indicator is shown inside my mainView it has a weird transition when appearing- it is transitioned from the top left corner to the center of the view. Does anyone know why is this happening? I attach the structs with the code and 3 pictures with the behavior.
ActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
CircleView(animate: $animate, firstGradientColor: orangeColor, secondGradientColor: orangeColorOpaque, style: style)
}.onAppear() {
self.animate.toggle()
}
}
}
struct CircleView: View {
#Binding var animate: Bool
var firstGradientColor: Color
var secondGradientColor: Color
var style: StrokeStyle
var body: some View {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [firstGradientColor, secondGradientColor]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.transition(.opacity)
.animation(Animation.linear(duration: 0.7) .repeatForever(autoreverses: false), value: animate)
}
}
The view is use it in :
struct UserProfileView: View {
#ObservedObject var viewModel: UserProfileViewModel
#Binding var lightMode: ColorScheme
var body: some View {
NavigationView {
ZStack {
VStack(alignment: .center, spacing: 12) {
HStack {
Text(userProfileEmail)
.font(.headline)
.foregroundColor(Color(UIColor.label))
Spacer()
}.padding(.bottom, 16)
SettingsView(userProfile: $viewModel.userProfile, isDarkMode: $viewModel.isDarkMode, lightMode: $lightMode, location: viewModel.locationManager.address, viewModel: viewModel)
ButtonsView( userProfile: $viewModel.userProfile)
Spacer()
}.padding([.leading, .trailing], 12)
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
// .animation(nil)
}
}
}
}
}
I also tried with animation nil but it doesn't seem to work.
Here are the pictures:
Here is a possible solution - put it over NavigationView. Tested with Xcode 12.4 / iOS 14.4
NavigationView {
// .. your content here
}
.overlay( // << here !!
VStack {
if isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
)
I am using Xcode beta7 and the following is the code.
This is for a MacOs app.
here is my code:
import SwiftUI
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
V1(isClicked: $isClicked)
}
}
}
struct V1: View {
#Binding var isClicked: Bool
var body: some View {
VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
Run this code and click on the Click me button.
You will see that the scrollView's contnent size does not update and stay's squished.
If i try to resize the frame of the application by using the mouse to resize the screen, then instantly, the ScrollView's content size snaps to the correct size.
Do i need to do something to get the ScrollView to do this automatically (instead of me having to manually inscrease the frame of the app with the mouse?
I got the same issue recently on iOS, watchOS. The only way, I solve it was to move the content of the scrollView as a func in your struct having the ScrollView. As shown below.
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
otherView()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func otherView() -> some View{
return VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
It looks like a bug.