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)
}
}
Related
The rectangle should always be centered in ContainerView no matter what scale offset or anchor point innerContainerView has.
What offset is needed to place the rectangle in the center of the ContainerView?
let innerContainerSize = CGSize(width: 100, height: 100)
struct innerContainerView: View {
#Binding var ia: [si]
var body: some View {
ZStack() {
ForEach(ia) { i in
Rectangle()
.fill(.green)
.scaleEffect(i.scale)
.offset(x: i.frame.origin.x, y: i.frame.origin.y)
.frame(width: 500 * 0.7, height: 500 * 0.7)
}
}
}
}
struct ContainerView: View {
#Binding var ia: [si]
#Binding var fscale: CGFloat
#Binding var foffset: CGSize
var body: some View {
innerContainerView(ia: $ia)
.frame(width: innerContainerSize.width, height: innerContainerSize.height)
.background(Color.yellow)
.scaleEffect(fscale, anchor: .init(x: (250 - foffset.width) / 500, y: (250 - foffset.height) / 500))
.offset(foffset)
}
}
struct ContentView: View {
#State var ia = [si]()
#State var fscale: CGFloat = 1
#State var foffset: CGSize = .zero
var body: some View {
VStack(alignment: .center) {
ContainerView(ia: $ia, fscale: $fscale, foffset: $foffset)
.frame(width: 500, height: 500)
.background(Color.blue)
.clipped()
.offset(x: 50)
}
}
}
you can use .alignmentGuide() with GeometryReader.
Possible example :
struct ContentView: View {
let color = [Color.red, .green, .yellow, .blue, .orange, .gray]
let number = [5,4,3,2,1]
var body: some View {
GeometryReader { proxy in
ZStack {
Rectangle()
.fill(.red)
.scaleEffect(CGSize(width: 3, height: 2))
.frame(width: 100,height: 100)
.border(Color.blue, width: 4)
Rectangle()
.frame(width: 250,height: 100)
.offset(x: 50, y: 75)
.alignmentGuide(HorizontalAlignment.center) { viewDimension in
viewDimension[HorizontalAlignment.center] + 50 // offset of x
}
.alignmentGuide(VerticalAlignment.center, computeValue: { viewDimension in
viewDimension[VerticalAlignment.center] + 75 // offset of y
})
.border(Color.gray, width: 4)
.position(x: proxy.size.width/2 - 50, y: proxy.size.height/2 - 75)
// adding this once is enough
Rectangle()
.fill(Color.yellow)
.frame(width: 50,height: 30)
.offset(x: 100, y: 150)
.alignmentGuide(HorizontalAlignment.center) { viewDimension in
viewDimension[HorizontalAlignment.center] + 100 // offset of x
}
.alignmentGuide(VerticalAlignment.center, computeValue: { viewDimension in
viewDimension[VerticalAlignment.center] + 150 // offset of y
})
.border(Color.gray, width: 4)
}
.border(.gray)
}
}
}
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 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
}
}
}
}
I have simple View which has 2 DragGesture, first one updating and second one onEnded, I want make my body cleaner and i want transfer those functions how we can do that? I think those are kind of clouser functions.
struct ContentView: 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)
.gesture(
DragGesture()
// transfer to updatingFunction
.updating($translation) { value, state, _ in
state = value.translation
}
// transfer to onEndedFunction
.onEnded { value in onEndedFunction(value: value)}
)
}
func updatingFunction() {
}
func onEndedFunction(value: DragGesture.Value) {
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
}
}
You can use inout for passing state.
struct ContentViewGesture: 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)
.gesture(
DragGesture()
// transfer to updatingFunction
.updating($translation) { value, state, _ in
updatingFunction(value: value, state: &state) //<<-- Here
}
// transfer to onEndedFunction
.onEnded { value in onEndedFunction(value: value)}
)
}
func updatingFunction(value: DragGesture.Value, state: inout CGSize) { //<<-- Here
state = value.translation
}
func onEndedFunction(value: DragGesture.Value) {
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
}
}
It does not look much reasonable, instead I would recommend to separate entire gesture (if you look for code simplification, readability, etc.)
Here is possible variant:
struct ContentView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
private var dragGesture: some Gesture {
DragGesture()
.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)
}
}
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)
.gesture(dragGesture)
}
}
I've created this masked blur effect (code below), it runs in SwiftUI, but uses UIViewRepresentable for the masking, is it possible to re-create the same effect, but just in pure SwiftUI?
Here's the current code, if you run it, use your finger to drag on the screen, this moves the mask to reveal underneath.
import SwiftUI
import UIKit
struct TestView: View {
#State var position: CGPoint = .zero
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
self.position = value.location
}
}
var body: some View {
ZStack {
Circle()
.fill(Color.green)
.frame(height: 200)
Circle()
.fill(Color.pink)
.frame(height: 200)
.offset(x: 50, y: 100)
Circle()
.fill(Color.orange)
.frame(height: 100)
.offset(x: -50, y: 00)
BlurView(style: .light, position: $position)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.gesture(
simpleDrag
)
}
}
struct BlurView: UIViewRepresentable {
let style: UIBlurEffect.Style
#Binding var position: CGPoint
func makeUIView(context: UIViewRepresentableContext<BlurView>) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: style)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(blurView, at: 0)
NSLayoutConstraint.activate([
blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
])
let clipPath = UIBezierPath(rect: UIScreen.main.bounds)
let circlePath = UIBezierPath(ovalIn: CGRect(x: 100, y: 0, width: 200, height: 200))
clipPath.append(circlePath)
let layer = CAShapeLayer()
layer.path = clipPath.cgPath
layer.fillRule = .evenOdd
view.layer.mask = layer
view.layer.masksToBounds = true
return view
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<BlurView>) {
let clipPath = UIBezierPath(rect: UIScreen.main.bounds)
let circlePath = UIBezierPath(ovalIn: CGRect(x: position.x, y: position.y, width: 200, height: 200))
clipPath.append(circlePath)
let layer = CAShapeLayer()
layer.path = clipPath.cgPath
layer.fillRule = .evenOdd
uiView.layer.mask = layer
uiView.layer.masksToBounds = true
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
I think I nearly have a solution, I can use a viewmodifer to render the result twice on top of each other with a ZStack, I can blur one view, and use a mask to knock a hole in it.
import SwiftUI
struct TestView2: View {
#State var position: CGPoint = .zero
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
self.position = value.location
}
}
var body: some View {
ZStack {
Circle()
.fill(Color.green)
.frame(height: 200)
Circle()
.fill(Color.pink)
.frame(height: 200)
.offset(x: 50, y: 100)
Circle()
.fill(Color.orange)
.frame(height: 100)
.offset(x: -50, y: 00)
}
.maskedBlur(position: $position)
.gesture(
simpleDrag
)
}
}
struct MaskedBlur: ViewModifier {
#Binding var position: CGPoint
/// Render the content twice
func body(content: Content) -> some View {
ZStack {
content
content
.blur(radius: 10)
.mask(
Hole(position: $position)
.fill(style: FillStyle(eoFill: true))
.frame(maxWidth: .infinity, maxHeight: .infinity)
)
}
}
}
extension View {
func maskedBlur(position: Binding<CGPoint>) -> some View {
self.modifier(MaskedBlur(position: position))
}
}
struct Hole: Shape {
#Binding var position: CGPoint
func path(in rect: CGRect) -> Path {
var path = Path()
path.addRect(UIScreen.main.bounds)
path.addEllipse(in: CGRect(x: position.x, y: position.y, width: 200, height: 200))
return path
}
}
#if DEBUG
struct TestView2_Previews: PreviewProvider {
static var previews: some View {
TestView2()
}
}
#endif