The problem is, that .onHover doesn't work on Path. In this case is will be one wrong area as hovered detected. How can I make .onHover modifier for the Path?
For example .onHover with rectangle is works, but I don't now why. But rectangle is now good idea for my case, because is most be resizable.
struct CenterCategoryPlanView : View {
#State private var isMouseHover = false
// MARK: Points
#Binding var startPoint: CGPoint
#Binding var endPoint: CGPoint
#Binding var categoryEntity : CategoryEntity
// MARK: Sizes
#Binding var monthColumnSize : CGSize
var body: some View {
Path { (path) in
path.move(to: startPoint )
path.addLine(to: endPoint)
path.closeSubpath()
}
.stroke(style: StrokeStyle(lineWidth: 30, lineCap: .square))
.onHover(perform: { value in
print("isHovered: \(value)")
})
.foregroundColor(Color(hex: categoryEntity.color ?? ""))
Text(categoryEntity.name ?? "-")
.position(x: max(monthColumnSize.width, (endPoint.x + startPoint.x) / 2), y: startPoint.y)
.aspectRatio(1, contentMode: .fill)
.font(.title2)
}
}
Call all views
struct CategoryRowView: View {
#Binding var startPoint: CGPoint
#Binding var endPoint: CGPoint
#Binding var categoryEntity : CategoryEntity
#Binding var isCategorySelected : Bool
#Binding var spacing : Double
#Binding var monthColumnSize : CGSize
#Binding var sliderSize : CGSize
#State private var isMouseHover = false
#State var fromMonth = 2
#State var toMonth = 4
#State var itemRowNr : Int = 1
var body: some View {
Text("")
.onAppear(){
CaclulateStartPositions()
}
ZStack{
CenterCategoryPlanView(startPoint: $startPoint, endPoint: $endPoint, categoryEntity: $categoryEntity, monthColumnSize: $monthColumnSize, sliderSize: $sliderSize)
if( isMouseHover || true ){
// LEFT
LeftCategoryPlanView(startPoint: $startPoint, endPoint: $endPoint, sliderSize: $sliderSize, monthColumnSize: $monthColumnSize, isCategorySelected: $isCategorySelected)
//RIGHT
RightCategoryPlanView(startPoint: $startPoint, endPoint: $endPoint, sliderSize: $sliderSize, monthColumnSize: $monthColumnSize, isCategorySelected: $isCategorySelected)
}
}
Color.clear
}
private func CaclulateStartPositions(){
startPoint.x = monthColumnSize.width * CGFloat(fromMonth - 1) + sliderSize.width
startPoint.y = 80
endPoint.x = monthColumnSize.width * CGFloat(toMonth) - sliderSize.width
endPoint.y = 80
print("startPoint.x: \(startPoint.x)")
print("startPoint.y: \(startPoint.y)")
}
}
Left slider
struct LeftCategoryPlanView : View {
#Binding var startPoint: CGPoint
#Binding var endPoint: CGPoint
#Binding var sliderSize : CGSize
#Binding var monthColumnSize : CGSize
#Binding var isCategorySelected : Bool
var LeftSliderPos : CGPoint {
get{
return CGPoint(x: startPoint.x - sliderSize.width / 2, y: startPoint.y)
}
}
var body: some View {
Rectangle()
.frame(width: sliderSize.width, height: sliderSize.height, alignment: .leading)
.position(LeftSliderPos)
.foregroundColor(.gray)
.gesture(DragGesture()
.onChanged { (value) in
if(value.location.x < endPoint.x)
{
if(value.location.x < (endPoint.x - monthColumnSize.width / 2)){
self.startPoint = CGPoint(x: value.location.x , y: startPoint.y)
isCategorySelected = true
}
}
}
.onEnded({ value in
isCategorySelected = false
withAnimation(.easeIn(duration: 0.3)) {
startPoint.x = startPoint.x - startPoint.x.truncatingRemainder(dividingBy: monthColumnSize.width) + sliderSize.width + 1
}
}))
}
}
Right slider
struct RightCategoryPlanView : View {
#Binding var startPoint: CGPoint
#Binding var endPoint: CGPoint
#Binding var sliderSize : CGSize
#Binding var monthColumnSize : CGSize
#Binding var isCategorySelected : Bool
var RightSliderPos : CGPoint {
get{
return CGPoint(x: endPoint.x + sliderSize.width / 2, y: startPoint.y)
}
}
var body: some View {
Rectangle()
.frame(width: sliderSize.width, height: sliderSize.height)
.position(RightSliderPos)
.foregroundColor(.gray)
.gesture(DragGesture()
.onChanged { (value) in
if(value.location.x > (startPoint.x + monthColumnSize.width / 2)){
self.endPoint = CGPoint(x: value.location.x, y: endPoint.y)
isCategorySelected = true
}
}
.onEnded({ value in
isCategorySelected = false
withAnimation(.easeIn(duration: 0.3)) {
endPoint.x = endPoint.x - endPoint.x.truncatingRemainder(dividingBy: monthColumnSize.width) + monthColumnSize.width - sliderSize.width - 1
}
}))
}
}
Related
I have created a carousel cards in SwiftUI, it is working on the DragGesture
I want to achieve same experience using scrollview i.e. same design and functionalities using scrollview instead of Drag-gesture
I have created Sample using scrollview but it has some limitation
Here is ScreenShot the upper carousel is using scrollview and lower one using Drag Gesture
import SwiftUI
struct Item: Identifiable {
var id: Int
var title: String
var color: Color
var isSelected: Bool
}
class Store: ObservableObject {
#Published var items: [Item]
let colors: [Color] = [.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo,.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo]
init() {
items = []
for i in 0...15 {
let new = Item(id: i, title: "Item \(i)", color: colors[i], isSelected: false)
items.append(new)
}
}
}
struct ContentView: View {
#StateObject var store = Store()
#State private var draggingItem = 0.0
#State var activeIndex: Int = 0
#State var selectedIndex: Int = 0
#State private var snappedItem = 0.0
let gridItems = [
GridItem(.flexible())
]
var body: some View {
VStack {
Spacer()
Text("Selected Index: \(store.items[selectedIndex].id)")
.fontWeight(.bold)
.padding()
Text("Acticted Index: \(activeIndex)")
.fontWeight(.bold)
.padding()
Spacer()
ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { scrollview in
LazyHGrid(rows: gridItems, alignment: .center, spacing: 25) {
ForEach(0..<store.items.count, id: \.self) { index in
GeometryReader { proxy in
let scale = getScale(proxy: proxy)
ZStack() {
Circle()
.fill(store.items[index].color)
Text(store.items[index].title)
.font(.body)
.fontWeight(.light)
}.frame(width: 70, height: 70)
.onTapGesture {
withAnimation {
print("Color: ",store.items[index].color)
print("ID: ",store.items[index].id)
print("Title: ",store.items[index].title)
selectedIndex = index
draggingItem = Double(selectedIndex)
activeIndex = selectedIndex
}
}
.overlay(Circle()
.stroke(selectedIndex == index ? .black : .clear, lineWidth: selectedIndex == index ? 2 : 0))
.scrollSnappingAnchor(.bounds)
.scaleEffect(.init(width: (scale * 1.2) , height: (scale * 1.2)))
.animation(.easeOut(duration: 0.2), value: 0)
.padding(.vertical)
.onChange(of: selectedIndex) { newValue in
withAnimation {
scrollview.scrollTo(selectedIndex, anchor: .center)
}
}
.zIndex(1.0 - abs(distance(store.items[index].id)) * 0.1)
} //End Geometry
.frame(width: 70, height: 150)
} //End ForEach
} //End Grid
}
}
ZStack {
ForEach(0..<store.items.count, id: \.self) { index in
ZStack {
Circle()
.fill(store.items[index].color)
Text(store.items[index].title)
.padding()
}
.frame(width: 100, height: 100)
.onTapGesture { loc in
print("Color: ",store.items[index].color)
print("ID: ",store.items[index].id)
print("Title: ",store.items[index].title)
selectedIndex = index
withAnimation(.linear) {
draggingItem = Double(store.items[index].id)
activeIndex = index
}
}
.overlay(Circle()
.stroke(activeIndex == index ? .white : .clear, lineWidth: activeIndex == index ? 2 : 0))
.scaleEffect(1.0 - abs(distance(store.items[index].id)) * 0.15 )
.offset(x: myXOffset(store.items[index].id), y: 0)
.zIndex(1.0 - abs(distance(store.items[index].id)) * 0.1)
}
}
.gesture(
DragGesture()
.onChanged { value in
draggingItem = (snappedItem) + value.translation.width / 100
}
.onEnded { value in
withAnimation {
snappedItem = draggingItem
draggingItem = round(draggingItem).remainder(dividingBy: Double(store.items.count))
//Get the active Item index
self.activeIndex = store.items.count + Int(draggingItem)
if self.activeIndex > store.items.count || Int(draggingItem) >= 0 {
self.activeIndex = Int(draggingItem)
}
}
}
)
}
}
func distance(_ item: Int) -> Double {
return (draggingItem - Double(item)).remainder(dividingBy: Double(store.items.count))
}
func myXOffset(_ item: Int) -> Double {
let angle = Double.pi * 2 / Double(store.items.count) * distance(item)
return sin(angle) * 200
}
func getScale(proxy: GeometryProxy) -> CGFloat {
let midPoint: CGFloat = 200
let viewFrame = proxy.frame(in: CoordinateSpace.global)
var scale: CGFloat = 1.0
let deltaXAnimationThreshold: CGFloat = 70
let diffFromCenter = abs(midPoint - viewFrame.origin.x - deltaXAnimationThreshold / 2)
if diffFromCenter < deltaXAnimationThreshold {
scale = 1 + (deltaXAnimationThreshold - diffFromCenter) / 300
}
return scale
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The idea is this: an object has a start position (A) and an end position (B). When the start button is pressed, the object moves from Ax, Ay to Bx, By. At an arbitrary point in time, you can stop its movement and continue.
I tried to implement this with a timer that moves the object every n time periods. It works, but the problem is that the timer eats up a lot of memory and when ten objects are created, the application freezes.
I do not ask you to do the task for me, but I will be very grateful if you tell me where to dig
Code currently in use
typealias RemainingDurationProvider<Value: VectorArithmetic> = (Value) -> TimeInterval
typealias AnimationWithDurationProvider = (TimeInterval) -> Animation
extension Animation {
static let instant = Animation.linear(duration: 0.0001)
}
struct PausableAnimationModifier<Value: VectorArithmetic>: AnimatableModifier {
#Binding var binding: Value
#Binding var paused: Bool
private let targetValue: Value
private let remainingDuration: RemainingDurationProvider<Value>
private let animation: AnimationWithDurationProvider
var animatableData: Value
init(binding: Binding<Value>, targetValue: Value, remainingDuration: #escaping RemainingDurationProvider<Value>, animation: #escaping AnimationWithDurationProvider, paused: Binding<Bool>) {
_binding = binding
self.targetValue = targetValue
self.remainingDuration = remainingDuration
self.animation = animation
_paused = paused
animatableData = binding.wrappedValue
}
func body(content: Content) -> some View {
content
.onChange(of: paused) { isPaused in
if isPaused {
withAnimation(.instant) {
binding = animatableData
}
} else {
withAnimation(animation(remainingDuration(animatableData))) {
binding = targetValue
}
}
}
}
}
extension View {
func pausableAnimation<Value: VectorArithmetic>(binding: Binding<Value>, targetValue: Value, remainingDuration: #escaping RemainingDurationProvider<Value>, animation: #escaping AnimationWithDurationProvider, paused: Binding<Bool>) -> some View {
self.modifier(PausableAnimationModifier(binding: binding, targetValue: targetValue, remainingDuration: remainingDuration, animation: animation, paused: paused))
}
}
struct TestView: View {
#State private var isPaused = false
#State private var offsetX: Double = .zero
#State private var startOffsetX: Double = .zero
#State private var endOffsetX: Double = 200.0
#State private var offsetY: Double = .zero
#State private var startPositionY: Double = .zero
#State private var endPositionY: Double = 500.0
private let duration: TimeInterval = 6
private var remainingDurationForX: RemainingDurationProvider<Double> {
{ currentAngle in duration * (1 - (currentAngle - startOffsetX) / (endOffsetX - startOffsetX)) }
}
private var remainingDurationForY: RemainingDurationProvider<Double> {
{ currentAngle in duration * (1 - (currentAngle - startPositionY) / (endPositionY - startPositionY)) }
}
private let animation: AnimationWithDurationProvider = { duration in
.linear(duration: duration)
}
var body: some View {
VStack {
ZStack {
VStack {
HStack {
Rectangle()
.frame(width: 50, height: 50)
.offset(x: offsetX, y: offsetY)
.pausableAnimation(binding: $offsetX,
targetValue: endOffsetX,
remainingDuration: remainingDurationForX,
animation: animation,
paused: $isPaused)
.pausableAnimation(binding: $offsetY,
targetValue: endPositionY,
remainingDuration: remainingDurationForY,
animation: animation,
paused: $isPaused)
Spacer()
}
Spacer()
}
}
HStack {
ControllButton(text: “Start”, action: {
offsetX = startOffsetX
offsetY = startPositionY
withAnimation(animation(duration)) {
offsetX = endOffsetX
offsetY = endPositionY
}
})
ControllButton(text: “NewPosition”, action: {
startOffsetX = endOffsetX
startPositionY = endPositionY
endOffsetX = 300
endPositionY = 30
})
ControllButton(text: isPaused ? “Resume” : “Pause”, action: {
isPaused = !isPaused
})
ControllButton(text: “Stop”, action: {
offsetX = .zero
offsetY = .zero
})
}
.padding(.bottom)
}
}
}
struct ControllButton: View {
var text: String
var action: () -> ()
var body: some View {
Button(text) {
action()
}
.padding()
.background(Color.yellow)
.frame(height: 35)
.cornerRadius(10)
}
}
Not sure the exact intent of your code, but here's something you could try:
struct ContentView: View {
#State var recs: [(AnyView, UUID)] = []
#State var isPaused: Bool = true
#State var shouldUpdate: Bool = false
var body: some View {
VStack {
ZStack {
ForEach(recs, id: \.1) { $0.0 }
}
Spacer()
HStack {
Button(isPaused ? "Start" : "Pause") {
isPaused.toggle()
}
Button("Add") {
recs.append(
(AnyView(
Rectangle()
.frame(width: 50, height: 50)
.pausableAnimation(
startingPosition: .init(x: .random(in: 0..<300), y: 0),
endingPosition: .init(x: .random(in: 0..<300), y: .random(in: 300..<700)),
isPaused: $isPaused,
shoudUpdate: $shouldUpdate
)
), UUID())
)
}
Button("Update") {
shouldUpdate = true
}
Button("Reset") {
isPaused = true
recs.removeAll()
}
}
.buttonStyle(.borderedProminent)
.tint(.orange)
}
}
}
extension CGPoint {
func isCloseTo(_ other: CGPoint) -> Bool {
(self.x - other.x) * (self.x - other.x) + (self.y - other.y) * (self.y - other.y) < 10
}
}
struct PausableAnimation: ViewModifier {
#State var startingPosition: CGPoint
#State var endingPosition: CGPoint
#Binding var isPaused: Bool
#Binding var shouldUpdate: Bool
private let publisher = Timer.TimerPublisher(interval: 0.1, runLoop: .main, mode: .default).autoconnect()
#State private var fired = 0
func body(content: Content) -> some View {
content
.position(startingPosition)
.animation(.linear, value: startingPosition)
.onReceive(publisher) { _ in
if !isPaused && !startingPosition.isCloseTo(endingPosition){
startingPosition.x += (endingPosition.x - startingPosition.x) / CGFloat(20 - fired)
startingPosition.y += (endingPosition.y - startingPosition.y) / CGFloat(20 - fired)
fired += 1
}
}
.onChange(of: shouldUpdate) { newValue in
if newValue == true {
updatePosition()
shouldUpdate = false
}
}
}
func updatePosition() {
endingPosition = .init(x: .random(in: 0..<300), y: .random(in: 0..<700))
fired = 0
}
}
extension View {
func pausableAnimation(startingPosition: CGPoint, endingPosition: CGPoint, isPaused: Binding<Bool>, shoudUpdate: Binding<Bool>) -> some View {
modifier(
PausableAnimation(
startingPosition: startingPosition,
endingPosition: endingPosition,
isPaused: isPaused,
shouldUpdate: shoudUpdate
)
)
}
}
Basically the idea here is in each PausableAnimation, a TimerPublisher is created to fire every 0.1 second. Anytime the publisher fires, it will move from startingPosition and endingPosition by 1/20 of the distance between them.
I used a CGPoint to keep both x and y information in a single variable. However, using two separate variables shouldn't change much of the code beside needing to pass in more data in the initializer.
I wrapped the modified view in a (AnyView, UUID), so I can add more of them into an array and display them through ForEach, dynamically.
Feel free to play around, I hope the idea is clear.
What I'm trying to do is basically having some images in a TabView and being able to zoom and pan on those images.
Right now I got this quite nicely implemented but I struggle to find a solution so that it is not possible to pan outside the bounds of the image.
This is what I currently have:
I want to try having the image clip to the side of the screen so you don't pan the image out of the visible area.
So far this it what my code looks like:
struct FinalImageSwipeView: View {
#ObservedObject var viewModel: ImageChatViewModel
#State var currentImage: UUID
#State var fullPreview: Bool = false
#GestureState var draggingOffset: CGSize = .zero
var body: some View {
TabView(selection: $currentImage) {
ForEach(viewModel.images) { image in
GeometryReader{ proxy in
let size = proxy.size
PinchAndPanImage(image: UIImage(named: image.imageName)!,
fullPreview: $fullPreview)
.frame(width: size.width, height: size.height)
.contentShape(Rectangle())
}
.tag(image.id)
.ignoresSafeArea()
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.ignoresSafeArea()
// upper navigation bar
.overlay(
ImageSwipeViewNavigationBar(fullPreview: $fullPreview, hideSwipeView: viewModel.hideSwipeView),
alignment: .top
)
// bottom image scrollview
.overlay(
ImageSwipeViewImageSelection(viewModel: viewModel,
currentImage: $currentImage,
fullPreview: $fullPreview),
alignment: .bottom
)
.gesture(DragGesture().updating($draggingOffset, body: { (value, outValue, _) in
if viewModel.imageScale == 0 {
outValue = value.translation
viewModel.onChangeDragGesture(value: draggingOffset)
}}).onEnded({ (value) in
if viewModel.imageScale == 0 {
viewModel.onEnd(value: value)
}
}))
.transition(.offset(y: UIScreen.main.bounds.size.height + 100))
}
}
struct ImageSwipeViewImageSelection: View {
#ObservedObject var viewModel: ImageChatViewModel
#Binding var currentImage: UUID
#Binding var fullPreview: Bool
var body: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 15) {
ForEach(viewModel.images) { image in
Image(image.imageName)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 60)
.cornerRadius(12)
.id(image.id)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(image.id == currentImage ? Color.white : Color.clear, lineWidth: 2)
}
.onTapGesture {
currentImage = image.id
}
}
}
.padding()
}
.frame(height: 80)
.background(BlurView(style: .systemUltraThinMaterialDark).ignoresSafeArea(edges: .bottom))
// While current post changing center current image in scrollview
.onAppear(perform: {
proxy.scrollTo(currentImage, anchor: .bottom)
})
.onChange(of: currentImage) { _ in
viewModel.imageScale = 1
withAnimation {
proxy.scrollTo(currentImage, anchor: .bottom)
}
}
}
.offset(y: fullPreview ? 150 : 0)
}
}
struct PinchAndPanImage: View {
let image: UIImage
#Binding var fullPreview: Bool
// Stuff for Pinch and Pan
#State var imageScale: CGFloat = 1
#State var imageCurrentScale: CGFloat = 0
#State var imagePanOffset: CGSize = .zero
#State var currentImagePanOffset: CGSize = .zero
var usedImageScale: CGFloat {
max(1, min(imageScale + imageCurrentScale, 10))
}
var usedImagePan: CGSize {
let width = imagePanOffset.width + currentImagePanOffset.width
let height = imagePanOffset.height + currentImagePanOffset.height
return CGSize(width: width, height: height)
}
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(0)
.offset(usedImagePan)
.scaleEffect(usedImageScale > 1 ? usedImageScale : 1)
.gesture(
// Magnifying Gesture
MagnificationGesture()
.onChanged({ value in
imageCurrentScale = value - 1
})
.onEnded({ value in
imageCurrentScale = 0
imageScale = imageScale + value - 1
withAnimation(.easeInOut) {
if imageScale > 5 {
imageScale = 5
}
}
})
)
.simultaneousGesture(createPanGesture())
.onTapGesture(count: 2) {
withAnimation {
imageScale = 1
imagePanOffset = .zero
}
}
.onTapGesture(count: 1) {
withAnimation {
fullPreview.toggle()
}
}
}
private func createPanGesture() -> _EndedGesture<_ChangedGesture<DragGesture>>? {
let gesture = DragGesture()
.onChanged { value in
let width = value.translation.width / usedImageScale
let height = value.translation.height / usedImageScale
currentImagePanOffset = CGSize(width: width, height: height)
}
.onEnded { value in
currentImagePanOffset = .zero
let scaledWidth = value.translation.width / usedImageScale
let scaledHeight = value.translation.height / usedImageScale
let width = imagePanOffset.width + scaledWidth
let height = imagePanOffset.height + scaledHeight
imagePanOffset = CGSize(width: width, height: height)
}
return imageScale > 1 ? gesture : nil
}
}
Tried several way to make it bubble effect from bottom to top moving.
I able to move it from bottom to top. but I can not make it like bubble effect.
#State private var bouncing = true
var body: some View {
Image("bubble").resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 40)
.frame(maxHeight: .infinity, alignment: bouncing ? .bottom : .top)
.animation(Animation.easeInOut(duration: 5.0).repeatForever(autoreverses: false))
.onAppear {
self.bouncing.toggle()
}
}
Here is simple bubble animation what I looking for.
import SwiftUI
struct MyParentView: View {
#State var replay: Bool = false
var body: some View {
ZStack{
Color.blue.opacity(0.8)
BubbleEffectView(replay: $replay)
VStack{
Spacer()
Button(action: {
replay.toggle()
}, label: {Text("replay")}).foregroundColor(.red)
}
}
}
}
struct BubbleEffectView: View {
#StateObject var viewModel: BubbleEffectViewModel = BubbleEffectViewModel()
#Binding var replay: Bool
var body: some View {
GeometryReader{ geo in
ZStack{
//Show bubble views for each bubble
ForEach(viewModel.bubbles){bubble in
BubbleView(bubble: bubble)
}
}.onChange(of: replay, perform: { _ in
viewModel.addBubbles(frameSize: geo.size)
})
.onAppear(){
//Set the initial position from frame size
viewModel.viewBottom = geo.size.height
viewModel.addBubbles(frameSize: geo.size)
}
}
}
}
class BubbleEffectViewModel: ObservableObject{
#Published var viewBottom: CGFloat = CGFloat.zero
#Published var bubbles: [BubbleViewModel] = []
private var timer: Timer?
private var timerCount: Int = 0
#Published var bubbleCount: Int = 50
func addBubbles(frameSize: CGSize){
let lifetime: TimeInterval = 2
//Start timer
timerCount = 0
if timer != nil{
timer?.invalidate()
}
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
let bubble = BubbleViewModel(height: 10, width: 10, x: frameSize.width/2, y: self.viewBottom, color: .white, lifetime: lifetime)
//Add to array
self.bubbles.append(bubble)
//Get rid if the bubble at the end of its lifetime
Timer.scheduledTimer(withTimeInterval: bubble.lifetime, repeats: false, block: {_ in
self.bubbles.removeAll(where: {
$0.id == bubble.id
})
})
if self.timerCount >= self.bubbleCount {
//Stop when the bubbles will get cut off by screen
timer.invalidate()
self.timer = nil
}else{
self.timerCount += 1
}
}
}
}
struct BubbleView: View {
//If you want to change the bubble's variables you need to observe it
#ObservedObject var bubble: BubbleViewModel
#State var opacity: Double = 0
var body: some View {
Circle()
.foregroundColor(bubble.color)
.opacity(opacity)
.frame(width: bubble.width, height: bubble.height)
.position(x: bubble.x, y: bubble.y)
.onAppear {
withAnimation(.linear(duration: bubble.lifetime)){
//Go up
self.bubble.y = -bubble.height
//Go sideways
self.bubble.x += bubble.xFinalValue()
//Change size
let width = bubble.yFinalValue()
self.bubble.width = width
self.bubble.height = width
}
//Change the opacity faded to full to faded
//It is separate because it is half the duration
DispatchQueue.main.asyncAfter(deadline: .now()) {
withAnimation(.linear(duration: bubble.lifetime/2).repeatForever()) {
self.opacity = 1
}
}
DispatchQueue.main.asyncAfter(deadline: .now()) {
withAnimation(Animation.linear(duration: bubble.lifetime/4).repeatForever()) {
//Go sideways
//bubble.x += bubble.xFinalValue()
}
}
}
}
}
class BubbleViewModel: Identifiable, ObservableObject{
let id: UUID = UUID()
#Published var x: CGFloat
#Published var y: CGFloat
#Published var color: Color
#Published var width: CGFloat
#Published var height: CGFloat
#Published var lifetime: TimeInterval = 0
init(height: CGFloat, width: CGFloat, x: CGFloat, y: CGFloat, color: Color, lifetime: TimeInterval){
self.height = height
self.width = width
self.color = color
self.x = x
self.y = y
self.lifetime = lifetime
}
func xFinalValue() -> CGFloat {
return CGFloat.random(in:-width*CGFloat(lifetime*2.5)...width*CGFloat(lifetime*2.5))
}
func yFinalValue() -> CGFloat {
return CGFloat.random(in:0...width*CGFloat(lifetime*2.5))
}
}
struct MyParentView_Previews: PreviewProvider {
static var previews: some View {
MyParentView()
}
}
I have a movable VStack which carry a Picker. When I want chose deferent option from Picker I cannot, because SwiftUI thinks I want use DragGesture, therefor my Picker is lockdown! My DragGesture has minimumDistance: 0 but it does not solve issue when I change this value also, from other hand I like to have minimumDistance: 0 so it is not even an option for me to solving issue with increasing minimumDistance, so I need help to find a way, thanks.
struct ContentView: View {
var body: some View {
StyleView()
}
}
struct StyleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
#State private var styleIndex: Int = 0
let styles: [String] = ["a", "b", "c"]
var body: some View {
VStack {
Picker(selection: $styleIndex, label: Text("Style")) {
ForEach(styles.indices, id:\.self) { index in
Text(styles[index].description)
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
Text("selected style: " + styles[styleIndex])
}
.padding()
.background(Color.red)
.cornerRadius(10)
.padding()
.position(x: location.width + translation.width + 200, y: location.height + translation.height + 100)
.gesture(DragGesture(minimumDistance: 0)
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
})
}
}
DragGesture triggers when the user presses down on a view and move at least a certain distance away. So, this creates a picker with a drag gesture that triggers when the user moves it at least 10 points (it should be greater than 0 otherwise how can it know about the tap and the drag)
struct StyleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
#State private var styleIndex: Int = 0
let styles: [String] = ["a", "b", "c"]
var body: some View {
VStack {
Picker(selection: $styleIndex, label: Text("Style")) {
ForEach(styles.indices, id:\.self) { index in
Text(styles[index].description)
}
}.gesture(DragAndTapGesture(count: styles.count, selected: $styleIndex).horizontal)
.pickerStyle(SegmentedPickerStyle())
.padding()
Text("selected style: " + styles[styleIndex])
}
.padding()
.background(Color.red)
.cornerRadius(10)
.padding()
.position(x: location.width + translation.width + 200, y: location.height + translation.height + 100)
.gesture(DragGesture(minimumDistance: 1)
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
})
}
}
struct DragAndTapGesture {
var count: Int
#Binding var selected: Int
init(count: Int, selected: Binding<Int>) {
self.count = count
self._selected = selected
}
var horizontal: some Gesture {
DragGesture().onEnded { value in
if -value.predictedEndTranslation.width > UIScreen.main.bounds.width / 2, self.selected < self.count - 1 {
self.selected += 1
}
if value.predictedEndTranslation.width > UIScreen.main.bounds.width / 2, self.selected > 0 {
self.selected -= 1
}
}
}
}