SwiftUI - How to limit the scope of animation to only the onAppear Transition - swift

I'm new to SwiftUI and working through some sample projects to get the hang of it and I'm getting stuck on limiting the scope of the animation I set for the .transition for an AnimationModifier so it only impacts the animation of the transition and nothing else in the view.
While the separate transitions are respected for onAppear() and another for onDisappear(). The animation in the AnimatableModifier is overriding the removal over the item from the grid even when explicitly declared
I've tried explcicitly setting the Animation to the .offset transition in both the AnimatableModifier and for the CardView in the GameView, and when I do, no animation is triggered at all:
.transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-50)).animation(Animation.easeInOut(duration: 1.25).delay(delay)))
So, there's gotta be a way to limit the scope or explicitly declare the animation for transition or two separate Animations in the Animation Modifier, but I'm not finding any resources on how to move forward.
GameView.swift
struct GameView: View {
#ObservedObject var viewModel: SetGameViewModel
#State var delay: Double = 0.1
var body: some View {
GeometryReader { geometry in
VStack {
Grid(newItems: self.viewModel.newCards,
items: self.viewModel.cards.itemsAtWithIds(ids: self.viewModel.idOfCardsToDisplay)) { card in
CardView(card: card, bodyGeoProxy: geometry, delay: self.delay).onTapGesture {
self.viewModel.choose(card: card)
}
.transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-50)))
.animation(Animation.easeInOut(duration: 1.25).delay(delay))
.onAppear() {
let maxDelay: Double = Double(self.viewModel.cards.itemsAtWithIds(ids: self.viewModel.idOfCardsToDisplay).count)*0.2 + 0.2
if self.delay < 2.5 {
self.delay = self.delay + 0.2
} else if self.delay >= maxDelay {
self.delay = 0.1
}
}
}
HStack{
Button(action: {
self.viewModel.dealThreeCards()
}) {
Text("Hit Me")
}
Spacer()
Text("Score: \(self.viewModel.score)")
Spacer()
Button(action: {
self.viewModel.dealThreeCards()
}) {
Text("New Game")
}
}
}
}
}
}
GameView.swift
struct CardView: View{
var card: SetGame<SoloSetCardContent>.Card
var bodyGeoProxy: GeometryProxy
var delay: Double
var body: some View {
GeometryReader { geometry in
self.body(for: geometry)
}
}
init(card: SetGame<SoloSetCardContent>.Card, bodyGeoProxy: GeometryProxy, delay: Double) {
self.card = card
self.bodyGeoProxy = bodyGeoProxy
self.delay = delay
}
#ViewBuilder
func body(for geometryProxy: GeometryProxy) -> some View {
ZStack {
if card.isSelected {
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray)
.frame(width: geometryProxy.size.width-4, height: geometryProxy.size.height-4, alignment: .center)
.border(Color.blue, width: 2)
.animation(nil)
} else {
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray)
.frame(width: geometryProxy.size.width-4, height: geometryProxy.size.height-4, alignment: .center)
.border(Color.red, width: 2)
.animation(nil)
}
VStack {
ForEach(0..<self.card.content.deckShapes.count) { index in
VStack {
Spacer(minLength: 5)
ShapeView(setShape: self.card.content.deckShapes[index])
.frame(width: (geometryProxy.size.width-geometryProxy.size.width/5), height: geometryProxy.size.height/5, alignment: .center)
Spacer(minLength: 5)
}
}
}
}
.deal(delay: self.delay, offset: bodyGeoProxy.size)
}
}
Dealer.Swift - AnimatableModifier
struct Dealer: AnimatableModifier {
#State var show: Bool = false
var delay: Double
var offset: CGSize
var randomXLocation: CGFloat {
CGFloat.random(in: -offset.width ..< offset.width)
}
func body(content: Content) -> some View {
ZStack {
if show {
content
.transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-450)))
.animation(Animation.easeInOut(duration: 1.25).delay(delay))
}
}
.onAppear {
withAnimation {
self.show = true
}
}
.onDisappear {
withAnimation {
self.show = false
}
}
}
}
extension View {
func deal(delay: Double, offset: CGSize) -> some View {
self.modifier(Dealer(delay: delay, offset: offset))
}
}

I was able to resolve this by removing the Animation from the body content (and elsewhere) and adding to withAnimation portion of the .onAppear method in body function of the AnimationModifier
func body(content: Content) -> some View {
ZStack {
if show {
content
.transition(.asymmetric(insertion: .offset(CGSize.init(width: randomXLocation, height: -offset.height-50)),
removal: .offset(CGSize.init(width: randomXLocation, height: offset.height+50))))
}
}
.onDisappear {
withAnimation (Animation.easeInOut(duration: 1.25).delay(0)) {
self.show = false
}
}
.onAppear {
withAnimation (Animation.easeInOut(duration: 1.25).delay(self.delay)) {
self.show = true
}
}
}

Related

SwiftUI stocking geometry effects and tabBar animation

Hey guys I have some issues with my code. I just experimented a bit with the matchedGeometryEffect in SwiftUI and it works great. But now I ran into some issues:
I cannot just deactivate the tabBar when the DetailView is dismissed because the view jumps up a bit.
The View transition is sometimes buggy and the console gives me (constantly) the output
Multiple inserted views in matched geometry group Pair<String, ID>(first: "bg", second: SwiftUI.Namespace.ID(id: 415)) have `isSource: true`, results are undefined.
Is there a better way to animate this smoothly and disable the tabBar?
Here is my code:
struct FullscreenView: View {
#Namespace var animationNamespace
#State var shouldShowFullsceen = false
#State var shouldShowDetails = false
var body: some View {
Input()
.padding()
.onTapGesture {
withAnimation(.interactiveSpring(
response: 0.6,
dampingFraction: 0.7,
blendDuration: 0.7
)) {
shouldShowFullsceen = true
}
}
.overlay {
if shouldShowFullsceen {
Output()
.onTapGesture {
withAnimation(.interactiveSpring(
response: 0.6,
dampingFraction: 0.7,
blendDuration: 0.7
)) {
shouldShowFullsceen = false
shouldShowDetails = false
}
}
}
}
}
}
extension FullscreenView {
#ViewBuilder
func Input() -> some View {
Content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(BackgroundView())
}
#ViewBuilder
func Output() -> some View {
DetailedContent()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(FullscreenBackground())
}
}
extension FullscreenView {
#ViewBuilder
func Content() -> some View {
Image("dog")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 300)
.matchedGeometryEffect(id: "content", in: animationNamespace)
}
}
extension FullscreenView {
#ViewBuilder
func DetailedContent() -> some View {
VStack {
Content()
ScrollView(.vertical) {
Text(dummyText)
.padding()
.opacity(shouldShowDetails ? 1 : 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
.transition(.identity)
.onAppear {
withAnimation(.interactiveSpring(
response: 0.6,
dampingFraction: 0.7,
blendDuration: 0.7
).delay(0.1)) {
shouldShowDetails = true
}
}
}
}
extension FullscreenView {
#ViewBuilder
func BackgroundView() -> some View {
Color.orange
.clipShape(RoundedRectangle(cornerRadius: 15))
.matchedGeometryEffect(id: "bg", in: animationNamespace)
}
}
extension FullscreenView {
#ViewBuilder
func FullscreenBackground() -> some View {
BackgroundView()
.ignoresSafeArea()
}
}
struct FullscreenView_Previews: PreviewProvider {
static var previews: some View {
FullscreenView()
}
}
Regarding the animation and console warning:
Don't overlay Output view. Show either the Input or the Output View with if ... else, then .matchedGeometryEffect can do the transition.
You should use .matchedGeometryEffect with isSource: specified to true, for both image and background.
get rid of .transition(.identity).
Here is the full code with comments:
struct FullscreenView: View {
#Namespace var animationNamespace
#State var shouldShowFullsceen = false
#State var shouldShowDetails = false
var body: some View {
if shouldShowFullsceen == false { // show only one matched view at a time
Input()
.padding()
.onTapGesture {
withAnimation(.interactiveSpring(
response: 0.6,
dampingFraction: 0.7,
blendDuration: 0.7
)) {
shouldShowFullsceen = true
}
}
} else { // show only one matched view at a time
Output()
.onTapGesture {
withAnimation(.interactiveSpring(
response: 0.6,
dampingFraction: 0.7,
blendDuration: 0.7
)) {
shouldShowFullsceen = false
shouldShowDetails = false
}
}
}
}
func Input() -> some View {
Content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(BackgroundView())
}
func Output() -> some View {
DetailedContent()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(FullscreenBackground())
}
func Content() -> some View {
Image(systemName: "tortoise")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 300)
.padding()
.matchedGeometryEffect(id: "content", in: animationNamespace, isSource: true) // add isSource
}
func DetailedContent() -> some View {
VStack {
Content()
ScrollView(.vertical) {
Text("dummyText")
.padding()
.opacity(shouldShowDetails ? 1 : 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
// .transition(.identity) // take this out
.onAppear {
withAnimation(.interactiveSpring(
response: 0.6,
dampingFraction: 0.7,
blendDuration: 0.7
).delay(0.1)) {
shouldShowDetails = true
}
}
}
func BackgroundView() -> some View {
Color.orange
.clipShape(RoundedRectangle(cornerRadius: 15))
.matchedGeometryEffect(id: "bg", in: animationNamespace, isSource: true) // add isSource
}
func FullscreenBackground() -> some View {
BackgroundView()
.ignoresSafeArea()
}
}

How to implement scrolling bottom down when we got to the beginning of the content, inside the scrollview using pure SwiftUI?

Here is my attempt to implement this functionality, I also tried to solve it through UIKit, it worked, but I ran into problems with dynamically changing the content of SwiftUI, which was inside UIScrollView. More precisely, the problem was in changing the height of the container
https://imgur.com/a/6du73pt
import SwiftUI
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentView: View {
#State private var offset: CGFloat = 300
var body: some View {
ZStack {
Color.yellow.ignoresSafeArea()
ScrollView(.vertical) {
ForEach(0..<100, id: \.self) { _ in
Color.red
.frame(width: 250, height: 125, alignment: .center)
}
.overlay(
GeometryReader { proxy in
let offset = proxy.frame(in: .named("scroll")).minY
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self, value: offset)
.frame(width: 0, height: 0, alignment: .center)
})
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
if value >= 0 {
offset = value + 300
}
}
.gesture(DragGesture()
.onChanged({ value in
print("scrooll")
print(value)
})
)
}
.offset(y: offset)
.gesture(DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged({ value in
offset = value.translation.height + 300
}))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Below is an example of how you can lock the ScrollView when you are at the top, and then allow the DragGesture to operate instead of scroll. I removed your PreferenceKey as it was not necessary. I also used frame reader to determine where in the scroll view the top cell was. Code is extensively commented.
struct ScrollViewWithPulldown: View {
#State private var offset: CGFloat = 300
#State private var scrollEnabled = true
#State private var cellRect: CGRect = .zero
// if the top of the cell is in view, origin.y will be greater than or equal to zero
var topInView: Bool {
cellRect.origin.y >= 0
}
var body: some View {
ZStack {
Color.yellow.ignoresSafeArea()
ScrollView {
ForEach(0..<100, id: \.self) { id in
Color.red
.id(id)
.frame(width: 250, height: 125, alignment: .center)
// This is inspired by https://www.fivestars.blog/articles/swiftui-share-layout-information/
.copyFrame(in: .named("scroll"), to: $cellRect)
.onChange(of: cellRect) { _ in
if id == 0 { // insure the first view however you need to
if topInView {
scrollEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
scrollEnabled = true
}
} else {
scrollEnabled = true
}
}
}
}
}
.disabled(!scrollEnabled)
.coordinateSpace(name: "scroll")
}
.offset(y: offset)
.gesture(DragGesture()
.onChanged({ value in
// Scrolling down
if value.translation.height > 0 && topInView {
scrollEnabled = false
print("scroll locked")
print(value)
} else { // Scrolling up
scrollEnabled = true
print("scroll up")
print(value)
}
})
.onEnded({ _ in
scrollEnabled = true
})
)
}
}
A view extension inspired by FiveStar Blog:
extension View {
func readFrame(in space: CoordinateSpace, onChange: #escaping (CGRect) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: FrameInPreferenceKey.self, value: geometryProxy.frame(in: space))
}
)
.onPreferenceChange(FrameInPreferenceKey.self, perform: onChange)
}
func copyFrame(in space: CoordinateSpace, to binding: Binding<CGRect>) -> some View {
self.readFrame(in: space) { frame in
binding.wrappedValue = frame
}
}
}

SwiftUI WatchOS: Animation behaves strangely when placed inside a list?

I have this pulse animation below which works well by itself, but when I place it inside of a List then the animation of the circles pulsing is correct but all of the circles also move vertically from the top to the center of the screen as well? Outside of a list the circles remain in the center. Why is a list causing this and how to get around?
import SwiftUI
struct HeartRatePulseView: View {
#State var animate = false
#Environment(\.scenePhase) private var scenePhase
func circlesColor() -> Color {
Color.blue
}
var body: some View {
VStack(spacing: -3) {
ZStack {
ZStack {
GeometryReader { geometry in
ZStack {
Circle().fill(circlesColor().opacity(0.25)).frame(width: geometry.size.width, height: geometry.size.height).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor().opacity(0.35)).frame(width: geometry.size.width * 0.79, height: geometry.size.height * 0.79).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor()).frame(width: geometry.size.width * 0.60, height: geometry.size.height * 0.60)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.onAppear { self.animate = true }
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
self.animate = true
} else {
self.animate = false
}
})
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : .default)
}
}
.frame(height: 145)
}
}
struct HeartRatePulseView_Previews: PreviewProvider {
static var previews: some View {
List {
HeartRatePulseView()
}
.listStyle(.carousel)
}
}
Ok, if you change the animation method slightly it works,
Note the value argument, it seems this is the new way to animate
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : nil, value: animate)
import SwiftUI
struct HeartRatePulseView: View {
#State var animate = false
#Environment(\.scenePhase) private var scenePhase
func circlesColor() -> Color {
Color.blue
}
var body: some View {
VStack(spacing: -3) {
ZStack {
ZStack {
GeometryReader { geometry in
ZStack {
Circle().fill(circlesColor().opacity(0.25)).frame(width: geometry.size.width, height: geometry.size.height).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor().opacity(0.35)).frame(width: geometry.size.width * 0.79, height: geometry.size.height * 0.79).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor()).frame(width: geometry.size.width * 0.60, height: geometry.size.height * 0.60)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.onAppear { self.animate = true }
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
self.animate = true
} else {
self.animate = false
}
})
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : nil, value: animate)
}
}
.frame(height: 145)
}
}
struct HeartRatePulseView_Previews: PreviewProvider {
static var previews: some View {
List {
HeartRatePulseView()
HeartRatePulseView()
}
.listStyle(.carousel)
}
}

Disable animation when a view appears in SwiftUI

I got a problem while trying to display a custom loading view in SwiftUI.
I created a custom struct view OrangeActivityIndicator:
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 {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false))
}.onAppear() {
self.animate.toggle()
}
}
}
I use it inside different screens or views, my problem is that it appears weirdly, for example in CampaignsView of the app I display it when the server call is in progress.
struct CampaignsView: View {
#ObservedObject var viewModel: CampaignsViewModel
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
CustomNavigationBar(campaignsNumber: viewModel.cardCampaigns.count)
.padding([.leading, .trailing], 24)
.frame(height: 25)
CarouselView(x: $viewModel.x, screen: viewModel.screen, op: $viewModel.op, count: $viewModel.index, cardCampaigns: $viewModel.cardCampaigns).frame(height: 240)
CampaignDescriptionView(idx: viewModel.index, cardCampaigns: viewModel.cardCampaigns)
.padding([.leading, .trailing], 24)
Spacer()
}
.onAppear {
self.viewModel.getCombineCampaigns()
}
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
.padding(.top, 34)
.background(Color.orBackgroundGrayColor.edgesIgnoringSafeArea(.all))
.navigationBarHidden(true)
}
}
}
The Indicator itself is correctly spinning, the problem is when it appears, it appears as a translation animation coming from the bottom to the middle of the screen. This is my viewModel with the server call and isLoading property:
class CampaignsViewModel: ObservableObject {
#Published var index: Int = 0
#Published var cardCampaigns: [CardCampaign] = [CardCampaign]()
#Published var isLoading: Bool = false
var cancellable: AnyCancellable?
func getCombineCampaigns() {
self.isLoading = true
let campaignLoader = CampaignLoader()
cancellable = campaignLoader.getCampaigns()
.receive(on: DispatchQueue.main)
//Handle Events operator is used for debugging.
.handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
receiveOutput: { print("Receive output: \($0)") },
receiveCompletion: { print("Receive completion: \($0)") },
receiveCancel: { print("Receive cancel") },
receiveRequest: { print("Receive request: \($0)") })
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { campaignResult in
self.isLoading = false
guard let campaignsList = campaignResult.content else {
return
}
self.cardCampaigns = campaignsList.map { campaign in
return CardCampaign(campaign: campaign)
}
self.moveToFirstCard()
}
}
}
Use animation with linked related state
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7)
.repeatForever(autoreverses: false),
value: animate) // << here !!
}.onAppear() {
self.animate.toggle()
}
}
SwiftUI 3
(iOS 15)
use the new API
.animation(.default, value: isAppeared)
SwiftUI 1, 2
Like below, when I first load a view, the button was animated.
So, I used the GeometryReader.
The animation is set to nil while the coordinates of the dummy are changing.
RecordView.swift
struct RecordView: View {
var body: some View {
Button(action: { }) {
Color(.red)
.frame(width: 38, height: 38)
.cornerRadius(6)
.padding(34)
.overlay(Circle().stroke(Color(.red), lineWidth: 8))
}
.buttonStyle(ButtonStyle1()) // 👈 customize the button.
}
}
ButtonStyle.swift
import SwiftUI
struct ButtonStyle1: ButtonStyle {
// In my case, the #State isn't worked, so I used class.
#ObservedObject private var data = ButtonStyle1Data()
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
// Dummy for tracking the view status
GeometryReader {
Color.clear
.preference(key: FramePreferenceKey.self, value: $0.frame(in: .global))
}
.frame(width: 0, height: 0)
.onPreferenceChange(FramePreferenceKey.self) { frame in
guard !data.isAppeared else { return }
// ⬇️ This is the key
data.isLoaded = 0 <= frame.origin.x
}
// Content View
configuration.label
.opacity(configuration.isPressed ? 0.5 : 1)
.scaleEffect(configuration.isPressed ? 0.92 : 1)
.animation(data.isAppeared ? .easeInOut(duration: 0.18) : nil)
}
}
}
ButtonStyleData.swift
final class ButtonStyle1Data: ObservableObject {
#Published var isAppeared = false
}
FramePreferenceKey.swift
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}
Result

SwiftUI automatically sizing bottom sheet

There are a lot of examples of bottom sheet out there for SwiftUI, however they all specify some type of maximum height the sheet can grow to using a GeometryReader. What I would like is to create a bottom sheet that becomes only as tall as the content within it. I've come up with the solution below using preference keys, but there must be a better solution. Perhaps using some type of dynamic scrollView is the solution?
struct ContentView: View{
#State private var offset: CGFloat = 0
#State private var size: CGSize = .zero
var body: some View{
ZStack(alignment:.bottom){
VStack{
Button(offset == 0 ? "Hide" : "Show"){
withAnimation(.linear(duration: 0.2)){
if offset == 0{
offset = size.height
} else {
offset = 0
}
}
}
.animation(nil)
.padding()
.font(.largeTitle)
Spacer()
}
BottomView(offset: $offset, size: $size)
}.edgesIgnoringSafeArea(.all)
}
}
struct BottomView: View{
#Binding var offset: CGFloat
#Binding var size: CGSize
var body: some View{
VStack(spacing: 0){
ForEach(0..<5){ value in
Rectangle()
.fill(value.isMultiple(of: 2) ? Color.blue : Color.red)
.frame(height: 100)
}
}
.offset(x: 0, y: offset)
.getSize{
size = $0
offset = $0.height
}
}
}
struct SizePreferenceKey: PreferenceKey {
struct SizePreferenceData {
let bounds: Anchor<CGRect>
}
static var defaultValue: [SizePreferenceData] = []
static func reduce(value: inout [SizePreferenceData], nextValue: () -> [SizePreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct SizePreferenceModifier: ViewModifier {
let onAppear: (CGSize)->Void
func body(content: Content) -> some View {
content
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { [SizePreferenceKey.SizePreferenceData( bounds: $0)] })
.backgroundPreferenceValue(SizePreferenceKey.self) { preferences in
GeometryReader { geo in
Color.clear
.onAppear{
let size = CGSize(width: geo.size.width, height: geo.size.height)
onAppear(size)
}
}
}
}
}
extension View{
func getSize(_ onAppear: #escaping (CGSize)->Void) -> some View {
return self.modifier(SizePreferenceModifier(onAppear: onAppear))
}
}
Talk about over engineering the problem. All you have to do is specify a height of 0 if you want the sheet to be hidden, and not specify a height when it's shown. Additionally set the frame alignment to be top.
struct ContentView: View{
#State private var hide = false
var body: some View{
ZStack(alignment: .bottom){
Color.blue
.overlay(
Text("Is hidden : \(hide.description)").foregroundColor(.white)
.padding(.bottom, 200)
)
.onTapGesture{
hide.toggle()
}
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
.layoutPriority(2)
}
}
.layoutPriority(1)
.frame(height: hide ? 0 : nil, alignment: .top)
.animation(.linear(duration: 0.2))
}.edgesIgnoringSafeArea(.all)
}
}
My approach is SwiftUI Sheet based solution feel free to check the gist
you just need to add the modifier to the view and let iOS do the rest for you, no need to re-do the math ;)
Plus you will have the sheet native behavior (swipe to dismiss) and i added "tap elsewhere" to dismiss.
struct ContentView: View {
#State var activeSheet: Bool = false
#State var activeBottomSheet: Bool = false
var body: some View {
VStack(spacing: 16){
Button {
activeSheet.toggle()
} label: {
HStack {
Text("Activate Normal sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
Button {
activeBottomSheet.toggle()
} label: {
HStack {
Text("Activate Bottom sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
}
.sheet(isPresented: $activeSheet) {
// Regular sheet
sheetView
}
.sheet(isPresented: $activeBottomSheet) {
// Responsive sheet
sheetView
.asResponsiveSheet()
}
}
var sheetView: some View {
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
}
}
}
iPhone:
iPad :