Why does the edgesIgnoringSafeArea modifier get applied after a transition ends in SwiftUI? - swift

I want to overlay the UIScreen with an opacity transition. This is my view:
struct ContentView: View {
#State private var overlayUIScreen: Bool = false
var body: some View {
ZStack {
if overlayUIScreen {
Rectangle()
.edgesIgnoringSafeArea(.top)
.frame(width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
.foregroundColor(Color.gray)
.transition(.opacity)
}
Button("Overlay?") {
withAnimation {
self.overlayUIScreen.toggle()
}
}
}
}
}
For some reason the Safe Area changes color after the transition is already finished.
Why does this happen and what can I do to fix this behavior?

Another solution would be to move the frame to modify the ZStack instead of the Rectangle
struct ContentView: View {
#State private var overlayUIScreen: Bool = false
var body: some View {
ZStack {
if overlayUIScreen {
Rectangle()
.edgesIgnoringSafeArea(.top)
.foregroundColor(Color.gray)
.transition(.opacity)
}
Button("Overlay?") {
withAnimation {
self.overlayUIScreen.toggle()
}
}
}
.frame(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
}
}

A workaround would be to already have your view in the hierarchy and attribute it's opacity to a #State private var like so:
struct ContentView: View {
#State private var overlayOpacity: Double = 0.0
var body: some View {
ZStack {
Rectangle()
.edgesIgnoringSafeArea(.top)
.frame(width: UIScreen.main.bounds.size.width,
height: UIScreen.main.bounds.size.height)
.opacity(overlayOpacity)
.foregroundColor(Color.gray)
Button("Overlay?") {
withAnimation {
if self.overlayOpacity == 0.0 {
self.overlayOpacity = 1.0
} else {
self.overlayOpacity = 0.0
}
}
}
} .transition(.opacity)
}
}

Related

ScrollView stops components from expanding

I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
Here is my code:
import SwiftUI
struct DisciplineView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
Card(cardTitle: "Notes")
Card(cardTitle: "Planner")
Card(cardTitle: "Homeworks / Exams")
}
.ignoresSafeArea()
}
}
}
struct DisciplineV_Previews: PreviewProvider {
static var previews: some View {
DisciplineView()
}
}
import SwiftUI
struct Card: View {
#State var cardTitle = ""
#State private var isTapped = false
var body: some View {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.foregroundColor(.gray.opacity(0.2))
.frame(width: .infinity, height: isTapped ? .infinity : 50)
.background(
VStack {
cardInfo
if(isTapped) { Spacer() }
}
.padding(isTapped ? 10 : 0)
)
}
var cardInfo: some View {
HStack {
Text(cardTitle)
.font(.title).bold()
.foregroundColor(isTapped ? .white : .black)
.padding(.leading, 10)
Spacer()
Image(systemName: isTapped ? "arrowtriangle.up.square.fill" : "arrowtriangle.down.square.fill")
.padding(.trailing, 10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
}
}
here is almost the same as I would like to have, but I would like the first one to be on the whole screen and stop the ScrollView while appearing.
Thank you!
Described above:
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
I think this is pretty much what you are trying to achieve.
Basically, you have to scroll to the position of the recently presented view and disable the scroll. The scroll have to be disabled enough time to avoid continuing to the next item but at the same time, it have to be enabled soon enough to give the user the feeling that it is scrolling one item at once.
struct ContentView: View {
#State private var canScroll = true
#State private var itemInScreen = -1
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0...10, id: \.self) { item in
Text("\(item)")
.onAppear {
withAnimation {
proxy.scrollTo(item)
canScroll = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
canScroll = true
}
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Color.blue)
}
}
}
.disabled(!canScroll)
}
.ignoresSafeArea()
}
}

How to hide a swiftUI view by touching anywhere outside of it

I have a view that call an alert that is another smaller view, whenever the second View is shown, I want to hide it when clicking outside of it.
How can I do that?
struct AlertView: View{
let screenSize = UIScreen.main.bounds
#Binding var alertIsShown: Bool
var body: some View{
VStack{
Button("Cancel") {
self.alertIsShown=false
}
}.padding()
.frame(width: screenSize.width * 0.85, height: screenSize.height * 0.6)
.background(Color(red: 0.4627, green: 0.8392, blue: 1.0))
.clipShape(RoundedRectangle(cornerRadius: 20.0, style: .continuous))
.offset(y: alertIsShown ? 0 : screenSize.height)
.animation(.spring())
.shadow(color: Color(.white), radius: 6, x: -9, y: -0)
}
}
}
For the main view that call the alert:
struct MainView: View {
#State private var alertIsShown = false
#State var liveOrdersList: [String] = ["item-1", "item-2"]
var body: some View {
VStack{
NavigationView{
List{
ForEach(liveOrdersList, id: \.self) { order in
HStack {
VStack(alignment: .leading){
Text("\(order.totalPrice)")
}
Spacer()
Button("add") {
withAnimation(.linear(duration: 0.3)) {
alertIsShown.toggle()
}
}
}
}
}
}
}
}
if alertIsShown{ //here I call the aler
AlertView(alertIsShown: $alertIsShown)
}
}
The list of buttons call the alert view.
How can I hide it when tapping outside of it?
You could try this approach, using .simultaneousGesture(...), as shown in this example code, to hide the AlertView by touching anywhere outside of it.
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#State var alertIsShown = false
// for testing
#State var liveOrdersList: [String] = ["item-1", "item-2", "item-3", "item-4", "item-5"]
var body: some View {
NavigationView {
VStack {
List {
ForEach(liveOrdersList, id: \.self) { order in
HStack {
VStack(alignment: .leading) {
Text("\(order)")
}
Spacer()
Button("add") {
withAnimation(.linear(duration: 0.3)) {
alertIsShown.toggle()
}
}
}
}
}
// -- here
.simultaneousGesture(alertIsShown ? TapGesture().onEnded {
alertIsShown = false
} : nil)
if alertIsShown {
AlertView(alertIsShown: $alertIsShown)
}
}
}
}
}

ZStack blocks animation SwiftUI

So my goal is to be able to show a custom view from time to time over a SwiftUI tabview, so I thought I would place them both in a ZStack like this
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation(Animation.linear(duration: 10)) {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
}
This works just fine, but when I try to use withAnimation() no animation gets triggered. How can I make the overlaying view, disappear with animation?
Use .animation modifier with container, like below, so container could animate removing view
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
show = false // << withAnimation not needed anymore
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
.animation(Animation.linear(duration: 10), value: show) // << here !!
So I found a solution and what I think is the cause of this. My hypothesis is that the animation modifier does not handle ZIndex IF it is not explicitly set.
One solution to this is to set ZIndex to the view that should be on the top to something higher than the other view. Like this:
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
.zIndex(.infinity) // <-- this here makes the animation work
}
}
}

Fit GeometryReader to drawer contents

How can I fit the GeometryReader to the middle ExpandingDrawer contents?
(Copy & Paste-able):
import SwiftUI
struct ExpandingDrawerButton: View {
#Binding var isExpanded: Bool
var body: some View {
Button(action: { withAnimation { isExpanded.toggle() } }) {
Text(isExpanded ? "Close" : "Open")
}
}
}
struct ExpandingDrawer<Content: View>: View {
#Binding var isExpanded: Bool
var content: () -> Content
var body: some View {
content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: nil, maxHeight: contentHeight)
.allowsHitTesting(isExpanded)
.clipped()
.transition(.slide)
}
private var contentHeight: CGFloat? {
isExpanded ? nil : CGFloat(0)
}
}
struct DrawerTestView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
struct ContentView: View {
#State var isExpanded = false
var body: some View {
GeometryReader { geo in
VStack(spacing: 0) {
top
.frame(height: geo.size.height * 1/4)
middle
bottom
.frame(height: geo.size.width)
}
}
}
var top: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
Rectangle()
.foregroundColor(.blue.opacity(0.2))
ExpandingDrawerButton(isExpanded: $isExpanded)
.padding()
}
}
var middle: some View {
ExpandingDrawer(isExpanded: $isExpanded) {
middleContent
}
}
var middleContent: some View {
GeometryReader { geo in
VStack {
ForEach(0..<10) { _ in
Button(action: {}) { Text("Random shit") }
}
Text("Don't know how tall...")
Text("Height can change...")
Text("But does need to fit snug (no extra space)")
}
}
}
var bottom: some View {
ZStack {
Rectangle()
.foregroundColor(.red)
.aspectRatio(1, contentMode: .fit)
VStack {
Text("Needs to be a square...")
Text("Okay if pushed below edge of screen...")
}
}
}
}
}
I've tried various combinations of .fixedSize() and .aspectRatio() but I'm struggling...
To avoid GeometryReader changing our views, we can put it in an .overlay().
Example:
struct ContentView: View {
class Storage {
var geo: GeometryProxy! {
didSet {
print(geo.size)
}
}
}
let storage = Storage()
/* ... */
}
var middleContent: some View {
VStack {
ForEach(0..<10) { _ in
Button(action: {}) { Text("Random stuff") }
}
Text("Don't know how tall...")
Text("Height can change...")
Text("But does need to fit snug (no extra space)")
}
.overlay(
GeometryReader { geo in
Rectangle()
.fill(Color.clear)
let _ = storage.geo = geo
}
)
}
You can now use storage.geo to access the GeometryProxy of this view.

Why is my view not transitioning when first appeared on screen in swiftUI

I want a simple view to scale from 0 to 1 when the app first loads. How ever its not happening.Look at my code here:
struct ContentView: View {
#State private var isLoadng = false
var body: some View {
ZStack {
if isLoadng {
Color.red
.cornerRadius(20)
.frame(width: 150, height: 200)
.transition(.scale)
}
}
.onAppear {
withAnimation(.easeInOut(duration: 2)) {
self.isLoadng = true
}
}
}
}
The view just popping up without any transition
Here is modified part. Tested with Xcode 12
var body: some View {
ZStack {
if isLoadng {
Color.red
.cornerRadius(20)
.frame(width: 150, height: 200)
.transition(.scale)
}
}
.animation(.easeInOut(duration: 2))
.onAppear {
self.isLoadng = true
}
}