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
}
}
}
}
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()
}
}
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
}
}
i am trying to add swipe inside list cell , swipe to show more options such as Delete, archive etc .
The swipe is working just fine , but the List ( vertical scroll ) is no longer scrolling up down .
Cell Bite :
import SwiftUI
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
//Swipe to custom options ,by "Jack" this option not yet available in SwiftUI
let drag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged {
if (self.offset.width > 0 ){ return }
self.offset.width = $0.translation.width
}.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -100, height: 0)
} else {
self.offset = .zero
}
}
ZStack{
Rectangle().foregroundColor(.blue).offset(x: offset.width, y: offset.height)
.gesture(drag)
.animation(.easeIn, value: offset)
Text("test").foregroundColor(.white)
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct cl_task_Previews: PreviewProvider {
static var previews: some View {
cl_task().previewLayout(.sizeThatFits)
}
}
List main view :
struct Item {
let uuid = UUID()
let value: String
}
struct w_tasks: View {
#State private var items = [Item]()
var body: some View {
ZStack {
List(self.items, id: \.uuid) {item in
cl_task()
}
.simultaneousGesture(DragGesture().onChanged({ value in
//Scrolled
}))
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.items.append(Item(value: "Item"))
}, label: {
Text("+")
.font(.system(size: 50))
.frame(width: 77, height: 70)
.foregroundColor(Color.white)
.padding(.bottom, 7)
})
.background(Color(hex : "#216D94"))
.cornerRadius(38.5)
.padding()
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}
}
}
}
}
struct w_tasks_Previews: PreviewProvider {
static var previews: some View {
w_tasks()
}
}
I've posted my question after spending hours solving this issue as i am new to SwiftUI , any advice how to solve it ?
The solution is to give different distance for swipe, like below
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
// give 25 distance makes vertical scroll enabled !!
let drag = DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged {
Tested with Xcode 12.4 / iOS 14.4
I have a CircleView() which is movable! I want minimumDistance for DragGesture be Zero for this View and in the other hand I defined another Gesture called onTapGesture, it is working but not in the way I wanted! because of onTapGesture the minimumDistance became 10 and with dragging you can see that SwiftUI think minimumDistance is 10, how can I have both Gestures working fine with minimumDistance = 0 ?
my goal: I want have an onTapGesture and a DragGesture with minimumDistance = 0
import SwiftUI
struct ContentView: View {
var body: some View {
CircleView()
}
}
struct CircleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
var body: some View {
Circle()
.fill()
.frame(width: 100, height: 100, alignment: .center)
.position(x: location.width + translation.width + 100, y: location.height + translation.height + 100)
.onTapGesture { print("onTapGesture!") } // << Until here: minimumDistance: 10
.gesture(DragGesture(minimumDistance: 0) // << After here: minimumDistance: 0 But also it does not Help! SwiftUI thinks that minimumDistance is 10!
.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)
})
}
}
One option is to compose a combination of two simultaneous gestures, like this:
import SwiftUI
struct ContentView: View {
var body: some View {
CircleView()
}
}
struct CircleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
var body: some View {
let tapDrag = 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)
}
.simultaneously(with: TapGesture()
.onEnded{ print("onTapGesture!") } )
Circle()
.fill()
.frame(width: 100, height: 100, alignment: .center)
.position(x: location.width + translation.width + 100, y: location.height + translation.height + 100)
.gesture(tapDrag)
}
}
I want to drag across rectangles in a grid and change their color. This code is almost working, but this is not always the right rectangle that reacts: it behaves rather randomly. Any hints?
import SwiftUI
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color] = Array(repeating: Color.gray, count: 4)
#State private var rect = [CGRect]()
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect.insert(geo.frame(in: .global), at: rect.endIndex)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
If I correctly understood your goal, here is fixed variant (with small modifications to avoid repeated hardcoding).
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color]
#State private var rect: [CGRect]
init() {
_colors = State(initialValue: Array(repeating: .gray, count: data.count))
_rect = State(initialValue: Array(repeating: .zero, count: data.count))
}
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect[item] = geo.frame(in: .global)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}