Animate bubble with SwiftUI - swift

I want to make a mini game for kids that have Alphabet characters floating inside a bubble so I used text with overlay image and it's working but the problem is when I tap on the bubble I don't know how to make it disappear with animation and make the text stop on current location
struct ExtractedView: View {
#State var BubbleAnimate = false
#State var AlphbetArray: [String] = ["A", "B","C","D","E","F","G"]
#State var randomNumber = Int.random(in: 0...6)
var body: some View {
Button {
BubbleAnimate = true
print("Hi")
} label: {
Text("Test Me")
}
.offset(x:100)
Text(AlphbetArray[randomNumber])
.font(.system(size: 120))
.fontWeight(.bold)
.foregroundColor(.purple)
.padding()
.overlay {
Image("bubble").resizable().frame(width: 130.0, height: 130.0).scaledToFit()
}
.animation(.linear(duration: 5).repeatForever(autoreverses: false), value: BubbleAnimate)
.offset(y:BubbleAnimate ? -300 : 300)
.onTapGesture {
print("Hello")
BubbleAnimate.toggle()
// I tried toggle but it won't work probably
}
}
}
This is the result how can I make the bubble disappear with animation and make the letter stay for 1 second period to make more animation on it like scaling etc...

There are a few considerations:
The letter will never stop in a specific place, given that it has only two positions: offset 300, or offset -300. To make it stop somewhere, the offset must have intermediate values. Solution: use a timer that modifies the offset every n milliseconds.
The bubble is always shown. To make it disappear, you must have a condition and a specific variable to trigger its visibility. Solution: create a variable that conditionally shows the bubble, and animate it using the .transition() modifier.
Variables must start with lower case letter. Just use the convention, it makes it easier to understand the code.
Here's the code:
// Don't use binary values for the offset, use absolute values
#State private var bubbleOffset = 0.0
#State private var alphabetArray: [String] = ["A", "B","C","D","E","F","G"]
#State private var randomNumber = Int.random(in: 0...6)
// Trigger the visibility of the bubble separately from the animation
#State private var showBubble = false
let maxOffset = 300.0
let minOffset = -300.0
// Use a timer to change the offset
let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Button {
// The button starts the animation; in this case,
// the animation happens only when the bubble is
// visible, but it does not have to be like that
showBubble = true
print("Hi")
} label: {
Text("Test Me")
}
.offset(x:100)
Text(alphabetArray[randomNumber])
.font(.system(size: 120))
.fontWeight(.bold)
.foregroundColor(.purple)
.padding()
.overlay {
// Show the bubble conditionally
if showBubble {
Image(systemName: "bubble.left").resizable().frame(width: 130.0, height: 130.0).scaledToFit()
// This transition will animate the disappearance of the bubble
.transition(.opacity)
}
}
// Absolute offset
.offset(y: bubbleOffset)
.onTapGesture {
withAnimation {
print("Hello")
// Trigger the disappearance of the bubble
showBubble = false
}
}
.onReceive(timer) { _ in
// In this case,
// the animation happens only when the bubble is
// visible, but it does not have to be like that
if showBubble {
// Change the offset at every emission of a new time
bubbleOffset -= 1
// Handle out-of-bounds
if bubbleOffset < minOffset {
bubbleOffset = maxOffset
}
}
}
}
}

Related

SwiftUI - detecting Long Press while keeping TabView swipable

I'm trying to detect a Long Press gesture on TabView that's swipable.
The issue is that it disables TabView's swipable behavior at the moment.
Applying the gesture on individual VStacks didn't work either - the long press doesn't get detected if I tap on the background.
Here's a simplified version of my code - it can be copy-pasted into Swift Playground:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var currentSlideIndex: Int = 0
#GestureState var isPaused: Bool = false
var body: some View {
let tap = LongPressGesture(minimumDuration: 0.5,
maximumDistance: 10)
.updating($isPaused) { value, state, transaction in
state = value
}
Text(isPaused ? "Paused" : "Not Paused")
TabView(selection: $currentSlideIndex) {
VStack {
Text("Slide 1")
Button(action: { print("Slide 1 Button Tapped")}, label: {
Text("Button 1")
})
}
VStack {
Text("Slide 2")
Button(action: { print("Slide 2 Button Tapped")}, label: {
Text("Button 2")
})
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(width: 400, height: 700, alignment: .bottom)
.simultaneousGesture(tap)
.onChange(of: isPaused, perform: { value in
print("isPaused: \(isPaused)")
})
}
}
PlaygroundPage.current.setLiveView(ContentView())
The overall idea is that this TabView will be rotating the slides automatically but holding a finger on any of them should pause the rotation (similar to Instagram stories). I removed that logic for simplicity.
Update: using DragGesture didn't work either.
The issue here is with the precedence of Animations in SwiftUI. Because TabView is a struct we are unable to change, its animation detection precedence cannot really be changed. The solution to this, however clunky, is to write our own custom tab view that has the expected behavior.
I apologize for how much code is here, but the behavior you described is surprisingly complex. In essence, we have a TimeLineView that is sending automatic updates to our view, telling it to change the pages, as you would see on Instagram. TimeLineView is a new feature, so if you want this to work old school, you could replace it with a Timer and its onReceive method, but I'm using this for brevity.
In the pages themselves, we are listening for this update, but only actually changing the page to the next one if there is room to do so and we are not long pressing the view. We use the .updating modifier on the LongPressGesture to know exactly when our finger is still on the screen or not. This LongPressGesture is combined in a SimultaneousGesture with a DragGesture, so that the drag can also be activated. In the drag gesture, we wait for the user's mouse/finger to traverse a certain percentage of the screen before animating the change in pages.
When sliding backwards, we initiate an async request to set the animation direction back to sliding forwards once the animation completes, so that updates received from the TimeLineView still animate in the correct direction, no matter which way we just swiped. Using custom gestures here has the added benefit that if you choose to do so, you can implement some fancy geometry effects to more closely emulate Instagram's animations. At the same time, our CustomPageView is still fully interactable, which means I can still click on button1 and see it's onTapGesture print message!
One caveat of passing in Views to a struct as a generic as I am doing in CustomTabView is that all of the views must be of the same type, which is part of the reason the pages are now reusable structs in their own right. If you have any questions about what you can / can't do with this methodology, let me know, but I've just run this in Playground same as you and it works exactly as described.
import SwiftUI
import PlaygroundSupport
// Custom Tab View to handle all the expected behaviors
struct CustomTabView<Page: View>: View {
#Binding var pageIndex: Int
var pages: [Page]
/// Primary initializer for a Custom Tab View
/// - Parameters:
/// - pageIndex: The index controlling which page we are viewing
/// - pages: The views to display on each Page
init(_ pageIndex: Binding<Int>, pages: [() -> Page]) {
self._pageIndex = pageIndex
self.pages = pages.map { $0() }
}
struct currentPage<Page: View>: View {
#Binding var pageIndex: Int
#GestureState private var isPressingDown: Bool = false
#State private var forwards: Bool = true
private let animationDuration = 0.5
var pages: [Page]
var date: Date
/// - Parameters:
/// - pageIndex: The index controlling which page we are viewing
/// - pages: The views to display on each Page
/// - date: The current date
init(_ pageIndex: Binding<Int>, pages: [Page], date: Date) {
self._pageIndex = pageIndex
self.pages = pages
self.date = date
}
var body: some View {
// Ensure that the Page fills the screen
GeometryReader { bounds in
ZStack {
// You can obviously change this to whatever you like, but it's here right now because SwiftUI will not look for gestures on a clear background, and the CustomPageView I implemented is extremely bare
Color.red
// Space the Page horizontally to keep it centered
HStack {
Spacer()
pages[pageIndex]
Spacer()
}
}
// Frame this ZStack with the GeometryReader's bounds to include the full width in gesturable bounds
.frame(width: bounds.size.width, height: bounds.size.height)
// Identify this page by its index so SwiftUI knows our views are not identical
.id("page\(pageIndex)")
// Specify the transition type
.transition(getTransition())
.gesture(
// Either of these Gestures are allowed
SimultaneousGesture(
// Case 1, we perform a Long Press
LongPressGesture(minimumDuration: 0.1, maximumDistance: .infinity)
// Sequence this Gesture before an infinitely long press that will never trigger
.sequenced(before: LongPressGesture(minimumDuration: .infinity))
// Update the isPressingDown value
.updating($isPressingDown) { value, state, _ in
switch value {
// This means the first Gesture completed
case .second(true, nil):
// Update the GestureState
state = true
// We don't need to handle any other case
default: break
}
},
// Case 2, we perform a Drag Gesture
DragGesture(minimumDistance: 10)
.onChanged { onDragChange($0, bounds.size) }
)
)
}
// If the user releases their finger, set the slide animation direction back to forwards
.onChange(of: isPressingDown) { newValue in
if !newValue { forwards = true }
}
// When we receive a signal from the TimeLineView
.onChange(of: date) { _ in
// If the animation is not pause and there are still pages left to show
if !isPressingDown && pageIndex < pages.count - 1{
// This should always say sliding forwards, because this will only be triggered automatically
print("changing pages by sliding \(forwards ? "forwards" : "backwards")")
// Animate the change in pages
withAnimation(.easeIn(duration: animationDuration)) {
pageIndex += 1
}
}
}
}
/// Called when the Drag Gesture occurs
private func onDragChange(_ drag: DragGesture.Value, _ frame: CGSize) {
// If we've dragged across at least 15% of the screen, change the Page Index
if abs(drag.translation.width) / frame.width > 0.15 {
// If we're moving forwards and there is room
if drag.translation.width < 0 && pageIndex < pages.count - 1 {
forwards = true
withAnimation(.easeInOut(duration: animationDuration)) {
pageIndex += 1
}
}
// If we're moving backwards and there is room
else if drag.translation.width > 0 && pageIndex > 0 {
forwards = false
withAnimation(.easeInOut(duration: animationDuration)) {
pageIndex -= 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
forwards = true
}
}
}
}
// Tell the view which direction to slide
private func getTransition() -> AnyTransition {
// If we are swiping left / moving forwards
if forwards {
return .asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))
}
// If we are swiping right / moving backwards
else {
return .asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing))
}
}
}
var body: some View {
ZStack {
// Create a TimeLine that updates every five seconds automatically
TimelineView(.periodic(from: Date(), by: 5)) { timeLine in
// Create a current page struct, as we cant react to timeLine.date changes in this view
currentPage($pageIndex, pages: pages, date: timeLine.date)
}
}
}
}
// This is the view that becomes the Page in our Custom Tab View, you can make it whatever you want as long as it is reusable
struct CustomPageView: View {
var title: String
var buttonTitle: String
var buttonAction: () -> ()
var body: some View {
VStack {
Text("\(title)")
Button(action: { buttonAction() }, label: { Text("\(buttonTitle)") })
}
}
}
struct ContentView: View {
#State var currentSlideIndex: Int = 0
#GestureState var isPaused: Bool = false
var body: some View {
CustomTabView($currentSlideIndex, pages: [
{
CustomPageView(title: "slide 1", buttonTitle: "button 1", buttonAction: { print("slide 1 button tapped") })
},
{
CustomPageView(title: "slide 2", buttonTitle: "button 2", buttonAction: { print("slide 2 button tapped") })
}]
)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(width: 400, height: 700, alignment: .bottom)
}
}
PlaygroundPage.current.setLiveView(ContentView())
I found the best and cleanest solution to this is just to add a clear view on top of your tabView when the slide show is active and put the gesture recognizer on that.
I haven't shown the implementation of the start stop timer which depends on your design.
private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var slideshowPlaying = false
#State var selection = 0
var body: some View {
ZStack {
TabView(selection: $selection) {
ForEach(modelArray.indices, id: \.self) { index in
SomeView()
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle())
.background(Color(.systemGroupedBackground))
.onReceive(self.timer) { _ in
if selection < modelArray.count + 1 {
selection += 1
} else {
selection = 0
}
}
if slideshowPlaying {
Color.clear
.contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0).onChanged { _ in
slideshowPlaying = false
})
}
}
}

How to stop animation in the current state in SwiftUI

Hello i wrote a simple code where i created an animation with a Circle which changes its position. I want the animation to stop when i tap on that Circle, but it doesn't stop, no matter that i set the animation to nil. Maybe my approach is wrong in some way. I will be happy if someone helps.
struct ContentView: View {
#State private var enabled = true
#State private var offset = 0.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onAppear {
withAnimation(self.enabled ? .easeIn(duration: 5) : nil) {
self.offset = Double(-UIScreen.main.bounds.size.height + 300)
}
}
.onTapGesture {
self.enabled = false
}
}
}
As #Schottky stated above, the offset value has already been set, SwiftUI is just animating the changes.
The problem is that when you tap the circle, onAppear is not called again which means enabled is not checked at all and even if it was it wouldn’t stop the offset from animating.
To solve this, we can introduce a Timer and do some small calculations, iOS comes with a built-in Timer class that lets us run code on a regular basis. So the code would look like this:
struct ContentView: View {
#State private var offset = 0.0
// 1
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
#State var animationDuration: Double = 5.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onTapGesture {
// 2
timer.upstream.connect().cancel()
}
.onReceive(timer) { _ in
// 3
animationDuration -= 0.1
if animationDuration <= 0.0 {
timer.upstream.connect().cancel()
} else {
withAnimation(.easeIn) {
self.offset += Double(-UIScreen.main.bounds.size.height + 300)/50
}
}
}
}
}
This creates a timer publisher which asks the timer to fire every 0.1 seconds. It says the timer should run on the main thread, It should run on the common run loop, which is the one you’ll want to use most of the time. (Run loops lets iOS handle running code while the user is actively doing something, such as scrolling in a list or tapping a button.)
When you tap the circle, the timer is automatically cancelled.
We need to catch the announcements (publications) by hand using a new modifier called onReceive(). This accepts a publisher as its first parameter (in this came it's our timer) and a function to run as its second, and it will make sure that function is called whenever the publisher sends its change notification (in this case, every 0.1 seconds).
After every 0.1 seconds, we reduce the duration of our animation, when the duration is over (duration = 0.0), we stop the timer otherwise we keep decreasing the offset.
Check out triggering-events-repeatedly-using-a-timer to learn more about Timer
After going through many things, I found out something that works for me. At the least for the time being and till I have time to figure out a better way.
struct WiggleAnimation<Content: View>: View {
var content: Content
#Binding var animate: Bool
#State private var wave = true
var body: some View {
ZStack {
content
if animate {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(.systemGray))
.offset(x: -25, y: -25)
}
}
.id(animate) //THIS IS THE MAGIC
.onChange(of: animate) { newValue in
if newValue {
let baseAnimation = Animation.linear(duration: 0.15)
withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
wave.toggle()
}
}
}
.rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
anchor: .center)
}
init(animate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) {
self.content = content()
self._animate = animate
}
}
Use
#State private var editMode = false
WiggleAnimation(animate: $editMode) {
VStack {
Image(systemName: image)
.resizable()
.frame(width: UIScreen.screenWidth * 0.1,
height: UIScreen.screenWidth * 0.1)
.padding()
.foregroundColor(.white)
.background(.gray)
Text(text)
.multilineTextAlignment(.center)
.font(KMFont.tiny)
.foregroundColor(.black)
}
}
What do you have to change?
make an offset as #Binding, provide from outside
How does it work?
.id(animate) modifier here does not refresh the view but just replaces it with a new one, and since you have offset as binding property, replaced view will have the correct offset.
Again this might not be the best solution but it works for my case.
What you need to understand is that only something that has already happened is being animated. It just takes longer to show. So in other words, when you set your offset to some value, it is going to animate towards that value, no matter what. What you, therefore, need to do is to set the offset to zero when tapping on the circle. The animation system is smart enough to deal with the 'conflicting' animations and cancel the previous one.
Also, a quick tip: You can do var offset: CGFloat = 0.0 so you don't have to cast it in the onAppear modifier

Get offset of element while moving using animation

How can i get the Y offset of a a moving element at the same time while it's moving?
This is the code that I'm tring the run:
import SwiftUI
struct testView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
self.showPopup.toggle()
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
.animation(.easeInOut)
}
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
I want to get the Y value of the black squar when the button is clicked the squar will move to a 0 position however I want to detect when the squar did reach the 0 value how can i do that?
Default animation duration (for those animations which do not have explicit duration parameter) is usually 0.25-0.35 (independently of where it is started & platform), so in your case it is completely safe (tested with Xcode 11.4 / iOS 13.4) to use the following approach:
withAnimation(.spring()){
self.offset = .zero
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.animationRunning = false
}
}
SwiftUI doesn't provide a callback with an animation completion, so there are two methods to accomplish detecting when the square has finished animating.
Method 1: Using AnimatableModifier. Here's a well-written Stack Overflow post on how to set that up.
Method 2: Using a Timer to run after the animation has completed. The idea here is to create a scheduled timer that runs after the timer has finished.
struct ContentView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
// add addition here with specified duration
withAnimation(.easeInOut(duration: 1), {
self.showPopup.toggle()
})
// set timer to run after the animation's specified duration
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
// run your completion code here eg.
withAnimation(.easeInOut(duration: 1)) {
self.showPopup.toggle()
}
timer.invalidate()
}
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
}
}
}
}

SwiftUI - how to get coordinate/position of clicked Button

Short version: How do I get the coordinates of a clicked Button in SwiftUI?
I'm looking for something like this (pseudo code) where geometry.x is the position of the clicked button in the current view:
GeometryReader { geometry in
return Button(action: { self.xPos = geometry.x}) {
HStack {
Text("Sausages")
}
}
}
Long version: I'm beginning SwiftUI and Swift so wondering how best to achieve this conceptually.
To give the concrete example I am playing with:
Imagine a tab system where I want to move an underline indicator to the position of a clicked button.
[aside]
There is a answer in this post that visually does what I am going for but it seems rather complicated: How to make view the size of another view in SwiftUI
[/aside]
Here is my outer struct which builds the tab bar and the rectangle (the current indicator) I am trying to size and position:
import SwiftUI
import UIKit
struct TabBar: View {
var tabs:ITabGroup
#State private var selected = "Popular"
#State private var indicatorX: CGFloat = 0
#State private var indicatorWidth: CGFloat = 10
#State private var selectedIndex: Int = 0
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(tabs.tabs) { tab in
EachTab(tab: tab, choice: self.$selected, tabs: self.tabs, x: self.$indicatorX, wid: self.$indicatorWidth)
}
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 40, maxHeight: 40).padding(.leading, 10)
.background(Color(UIColor(hex: "#333333")!))
Rectangle()
.frame(width: indicatorWidth, height: 3 )
.foregroundColor(Color(UIColor(hex: "#1fcf9a")!))
.animation(Animation.spring())
}.frame(height: 43, alignment: .leading)
}
}
Here is my struct that creates each tab item and includes a nested func to get the width of the clicked item:
struct EachTab: View {
// INCOMING!
var tab: ITab
#Binding var choice: String
var tabs: ITabGroup
#Binding var x: CGFloat
#Binding var wid: CGFloat
#State private var labelWidth: CGRect = CGRect()
private func tabWidth(labelText: String, size: CGFloat) -> CGFloat {
let label = UILabel()
label.text = labelText
label.font = label.font.withSize(size)
let labelWidth = label.intrinsicContentSize.width
return labelWidth
}
var body: some View {
Button(action: { self.choice = self.tab.title; self.x = HERE; self.wid = self.tabWidth(labelText: self.choice, size: 13)}) {
HStack {
// Determine Tab colour based on whether selected or default within black or green rab set
if self.choice == self.tab.title {
Text(self.tab.title).foregroundColor(Color(UIColor(hex: "#FFFFFF")!)).font(.system(size: 13)).padding(.trailing, 10).animation(nil)
} else {
Text(self.tab.title).foregroundColor(Color(UIColor(hex: "#dddddd")!)).font(.system(size: 13)).padding(.trailing, 10).animation(nil)
}
}
}
// TODO: remove default transition fade on button click
}
}
Creating a non SwiftUI UILabel to get the width of the Button seems a bit wonky. Is there a better way?
Is there a simple way to get the coordinates of the clicked SwiftUI Button?
You can use a DragGesture recogniser with a minimum drag distance of 0, which provides you the location info. However, if you combine the DragGesture with your button, the drag gesture won't be triggered on normal clicks of the button. It will only be triggered when the drag ends outside of the button.
You can get rid of the button completely, but of course then you lose the default button styling.
The view would look like this in that case:
struct MyView: View {
#State var xPos: CGFloat = 0
var body: some View {
GeometryReader { geometry in
HStack {
Text("Sausages: \(self.xPos)")
}
}.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { dragGesture in
self.xPos = dragGesture.location.x
})
}
}
The coordinateSpace parameter specifies if you want the touch position in .local or .global space. In the local space, the position is relative to the view that you've attached the gesture to. For example, if I had a Text view in the middle of the screen, my local y position would be almost 0, whereas my global y would be half of the screen height.
This tripped me up a bit, but this example shows the idea:
struct MyView2: View {
#State var localY: CGFloat = 0
#State var globalY: CGFloat = 0
var body: some View {
VStack {
Text("local y: \(self.localY)")
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded { dragGesture in
self.localY = dragGesture.location.y
})
Text("global y: \(self.globalY)")
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { dragGesture in
self.globalY = dragGesture.location.y
})
}
}
}
struct ContentView: View {
var body: some View {
VStack {
Button(action: { print("Button pressed")}) { Text("Button") }
}.simultaneousGesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onEnded { print("Changed \($0.location)") })
}
}
This solution seems to work, add a simultaneous gesture on Button, unfortunatelly it does not work if the Button is places in a Form
Turns out I solved this problem by adapting the example on https://swiftui-lab.com/communicating-with-the-view-tree-part-2/
The essence of the technique is using anchorPreference which is a means of sending data about one view back up the chain to ancestral views. I couldn't find any docs on this in the Apple world but I can attest that it works.
I'm not adding code here as the reference link also includes explanation that I don't feel qualified to re-iterate here!

SwiftUI Navigation similar to Slack

I am trying to make a navigation UI similar to the Slack app where I have the Home Screen which Overlays the Menu Navigation screen. I created a ViewModifier which makes the Home Screen Draggable. Now I need to add functionality such that when the "Home" is tapped on the blue Menu screen, the white Home View animates back to the center. My idea was to keep track of the NavigationState in a global AppState:
enum NavigationSelection {
case menu
case home
}
final class AppState: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var currentNavigationSelection: NavigationSelection = .home
}
Then when the user taps "Home", have it update the AppState's currentNavigationSelection, and have the Draggable view determine its offset based on the currentNavigationSelection. I'm really not sure about this approach and I'm having a tough time thinking about it in the new SwiftUI style. Any suggestions would be much appreciated.
The view hierarchy looks like this:
var body: some View {
ZStack {
Menu()
HomeTabView()
}
}
And the HomeTabView has a draggable ViewModifier applied:
struct Slidable: ViewModifier {
#EnvironmentObject var app: AppState
#State private var viewState = SlidableViewDragState.normal.defaultPosition
#State private var currentPosition: SlidableViewDragState = .normal {
didSet {
self.viewState = self.currentPosition.defaultPosition
}
}
func body(content: Content) -> some View {
return content
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight:
.infinity, alignment: Alignment.topLeading)
.offset(self.viewState)
.animation(.interactiveSpring())
.gesture(
DragGesture()
.onChanged({ (value) in
self.viewState = self.currentPosition.applyXTranslation(x: value.translation.width)
})
.onEnded({ (value) in
if value.translation.width < 0 && self.currentPosition == .normal {
return
}
if abs(value.translation.width) > self.currentPosition.switchThreshold {
self.currentPosition = self.currentPosition.oppositePosition
if self.currentPosition == .menuVisible {
self.app.currentNavigationSelection = .menu
}
} else {
self.viewState = self.currentPosition.defaultPosition
}
})
)
}
}
Moving both views
How do you currently define the positions of both? With my limited experience I would use a ZStack embedding HomeView and MenuView. This way you can move the views around independently.
Then you use a point variable as state, and make the DragGesture set point. Then you determine at the end of the drag what end position point is set to.
point is part of the offset-calculation. You can calculate the menu-offset with .offset(x: point.x) and the home-offset with .offset(x: -Self.maxOffset - Self.minusHomeWidth / 2 + point.x).
minusHomeWidth is the width the menu still shows when you are on the home screen.
Variables defining min and max of the point:
static let minOffset: CGFloat = 0
static let maxOffset: CGFloat = UIScreen.main.bounds.width - Self.minusHomeWidth
static let minusHomeWidth: CGFloat = UIScreen.main.bounds.width / 10
Then you can make it move to the home-view width
Button(action: {
self.point = CGPoint(x: Self.maxOffset, y: 0)
}) { Text("Go to Home") }