How can I transfer DragGesture to separate function in SwiftUI? - swiftui

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)
}
}

Related

SwiftUI: How to center a rectangle relative to another view that has a custom anchor scaling effect?

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)
}
}
}

Picker selection and translation conflict in SwiftUI

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
}
}
}
}

How can I have 2 Gestures in SwiftUI?

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)
}
}

Recreating a masked blur effect in SwiftUI

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

How to draw a line in SwiftUI with the mouse?

I am trying to do something like this, but I do not know where to begin.
I have some code so far which "works" sort of..
here is my full code (macOS)
struct ContentView: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
var body: some View {
ZStack {
Path { path in
path.move(to: CGPoint(x: 20, y: 100))
path.addLine(to: CGPoint(x: currentPosition.width, y: currentPosition.height))
}
.trimmedPath(from: 0, to: 1)
.strokedPath(StrokeStyle(lineWidth: 9, lineCap: .square, lineJoin: .round))
.foregroundColor(.red)
Circle()
.frame(width: 7, height: 7)
.foregroundColor(Color.blue)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
print(self.newPosition.width)
self.newPosition = self.currentPosition
}
)
Color.clear
}
}
}
But my blue circle is far away from the rectangle.
Any help please?
You are simply missing the position of the Circle.
struct ContentView: View {
#State private var startPoint: CGPoint = .zero
#State private var endPoint: CGPoint = CGPoint(x: 100, y: 100)
var body: some View {
ZStack {
Path { (path) in
path.move(to: startPoint)
path.addLine(to: endPoint)
}
.strokedPath(StrokeStyle(lineWidth: 9, lineCap: .square, lineJoin: .round))
.foregroundColor(.red)
//Circle 1
Circle()
.frame(width: 16, height: 16)
.position(startPoint)
.foregroundColor(.blue)
.gesture(DragGesture()
.onChanged { (value) in
self.startPoint = CGPoint(x: value.location.x, y: value.location.y)
})
//Circle 2
Circle()
.frame(width: 16, height: 16)
.position(endPoint)
.foregroundColor(.green)
.gesture(DragGesture()
.onChanged { (value) in
self.endPoint = CGPoint(x: value.location.x, y: value.location.y)
})
Color.clear
}
}
}