Problem with offset and buttons in SwiftUI? - ios

I have a group of buttons that i show with a clockwise rotation, but I cannot click them properly:
I think there is a problem with the offset but I don't know how to solve it, any suggestions?
This is the code:
struct CategoryView: View {
// Circle Radius
#State private var radius: Double = 150
let circleSize: Double = 350
// Degree of circle
#State private var degree = -90.0
let cards = ["John", "Mark", "Alex", "Kevin", "Jimmy"]
var body: some View {
ZStack {
let anglePerCount = Double.pi * 2.0 / Double(cards.count)
ForEach(0..<cards.count, id: \.self) { index in
let angle = Double(index) * anglePerCount
let xOffset = CGFloat(radius * cos(angle))
let yOffset = CGFloat(radius * sin(angle))
Button {
} label: {
Text(cards[index])
.font(.title)
.fontWeight(.bold)
.rotationEffect(Angle(radians: angle + Double.pi/2))
.offset(x: xOffset, y: yOffset)
}
}
}
.rotationEffect(Angle(degrees: degree))
.onAppear() {
radius = circleSize/2 - 47 // 47 is for padding
}
}
}

This is a simple mistake, that all SwiftUI devs have made countless times. You're modifying the label, but not the actual button itself. Simply move the modifier to the Button.
Button (action: {}, label: {
Text(cards[index])
.font(.title)
.fontWeight(.bold)
})
.rotationEffect(Angle(radians: angle + Double.pi/2))
.offset(x: xOffset, y: yOffset)
In SwiftUI nearly everything is a View and can be treated as such. A button, also a view, can have most of the same modifiers that a Text can have. In your question, you made a change to the inner view of the button, and not the actual button itself. That's why when you were clicking in the middle, it appeared to not be positioned right, but in fact it did exactly what you told it too. That is an important thing to remember because you can actually push, pull, and offset things outside of the view which makes for some interesting layouts; Layouts such as side-menus, or modals.

Related

SwiftUI animation: move up/down on press, spring on release - how to do the "move up" part

How can I get the blue circles to first move away from the green circle before getting back to it?
The animation should be:
press and hold:
the green circle scales down
the blue circles, while scaling down as well, first move "up" (away from their resting position, as if pushed away by the pressure applied) and then down (to touch the green circle again, as if they were pulled back by some gravitational force)
release
everything springs back into place
(bonus) ideally, the blue circles are ejected as the green circle springs up, and they fall back down in their resting position (next to the surface)
I got everything working except the blue circles moving up part.
This is the current animation:
And its playground.
import SwiftUI
import PlaygroundSupport
struct DotsView: View {
var diameter: CGFloat = 200
var size: CGFloat = 25
var isPressed: Bool = false
var body: some View {
ZStack {
ForEach(0...5, id: \.self) { i in
Circle()
.fill(Color.blue)
.frame(width: size, height: size)
.offset(x: 0, y: -(diameter)/2 - size/2)
.rotationEffect(.degrees(CGFloat(i * 60)))
}
}
.frame(width: diameter, height: diameter)
.animation(.none)
.scaleEffect(isPressed ? 0.8 : 1)
.animation(
isPressed ? .easeOut(duration: 0.2) : .interactiveSpring(response: 0.35, dampingFraction: 0.2),
value: isPressed
)
.background(
Circle()
.fill(Color.green)
.scaleEffect(isPressed ? 0.8 : 1)
.animation(isPressed ? .none : .interactiveSpring(response: 0.35, dampingFraction: 0.2), value: isPressed)
)
}
}
struct ContentView: View {
#State private var isPressed: Bool = false
var body: some View {
DotsView(
diameter: 200,
isPressed: isPressed
)
.frame(width: 500, height: 500)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
isPressed = true
}
.onEnded { _ in
isPressed = false
}
)
}
}
let view = ContentView()
PlaygroundPage.current.setLiveView(view)
Thanks
Actually all is needed is to replace linear scaleEffect with custom geometry effect that gives needed scale curve (initially growing then falling).
Here is a demo of possible approach (tested with Xcode 13.4 / iOS 15.5)
Main part:
struct JumpyEffect: GeometryEffect {
let offset: Double
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let trans = (value + offset * (pow(5, value - 1/pow(value, 5))))
let transform = CGAffineTransform(translationX: size.width * 0.5, y: size.height * 0.5)
.scaledBy(x: trans, y: trans)
.translatedBy(x: -size.width * 0.5, y: -size.height * 0.5)
return ProjectionTransform(transform)
}
}
and usage
.modifier(JumpyEffect(offset: isPressed ? 0.3 : 0, value: isPressed ? 0.8 : 1))
Complete code on GitHub

Adjust Drag Gesture on Rotated View

I have an image on a view.
added rotation to rotate the view
added drag gesture to pan the image
drag gesture works fine when image is not rotated
once the view is rotated to certain angle the drag gesture gets disturbed, since view is rotated.
So, how to adjust the dragOffset and position based on the angle of rotation?
Code:
struct ImageView: View {
#State private var dragOffset: CGSize = .zero
#State private var position: CGSize = .zero
#State private var currentRotation: Angle = .zero
#GestureState private var twistAngle: Angle = .zero
public var body: some View {
let rotationGesture = RotationGesture(minimumAngleDelta: .degrees(10))
.updating($twistAngle, body: { (value, state, _) in
state = value
})
.onEnded{ self.currentRotation += $0 }
let dragGesture = DragGesture()
.onChanged({ (value) in
self.dragOffset = value.translation
})
.onEnded({ (value) in
self.position.width += value.translation.width
self.position.height += value.translation.height
self.dragOffset = .zero
})
let gestures = rotationGesture
.simultaneously(with: dragGesture)
Image.placeholder320x192
.offset(x: dragOffset.width + position.width, y: dragOffset.height + position.height)
.rotationEffect(currentRotation + twistAngle)
.gesture(gestures, including: .gesture)
}
}
The order of the modifiers matter. You currently have the offset before the rotation - therefore you are applying the offset then rotating. This makes the offset appear at an angle. Instead, you want to rotate and then offset.
Change:
Image.placeholder320x192
.offset(x: dragOffset.width + position.width, y: dragOffset.height + position.height)
.rotationEffect(currentRotation + twistAngle)
.gesture(gestures, including: .gesture)
To this:
Image.placeholder320x192
.rotationEffect(currentRotation + twistAngle)
.offset(x: dragOffset.width + position.width, y: dragOffset.height + position.height)
.gesture(gestures, including: .gesture)

Using MagnificationGesture; how can I zoom in to where a user's fingers actually “pinch”?

I am facing the same problem as below with SwiftUI.
Using PinchGesture; how can I zoom in to where a user's fingers actually "pinch"?
I think I can solve this problem by giving anchor parameter of scaleEffect the center coordinate of the two fingers.
But I don't know how to get the center.
My Code:
import SwiftUI
struct MagnificationGestureView: View {
#State var scale: CGFloat = 1.0
#State var lastScaleValue: CGFloat = 1.0
var body: some View {
Image(
systemName: "photo"
)
.resizable()
.frame(
width: 100,
height: 100
)
.scaleEffect(
self.scale
//, anchor: <---- give: the two fingers center position
)
.gesture(
magnification
)
}
var magnification: some Gesture {
// Can I get the center of the pinch?
MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.scale = self.scale * delta
}.onEnded { _ in
self.lastScaleValue = 1.0
}
}
}
Reference:
https://stackoverflow.com/a/58468234/1979953

Is there any way to place buttons according to a central point in swift?

this is what I am trying to do:
Is anyone know how can i do it in swift?
I know there are some libraries that do something similar but I am trying to do it myself
You can achieve something like that with UICollectionView and custom UICollectionViewLayout
Some examples:
https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel
https://augmentedcode.io/2019/01/20/circle-shaped-collection-view-layout-on-ios/
https://github.com/robertmryan/CircularCollectionView
SwiftUI
You can position any view anywhere using either offset(x: y) or position(x:y).
offset(x:y:)
Offset this view by the specified horizontal and vertical distances. - https://developer.apple.com
position(x:y:)
Positions the center of this view at the specified coordinates in its
parent’s coordinate space.https://developer.apple.com
For instance:
ZStack {
Text("A")
.background(Color.red)
.position(x: 10, y: 20)
Text("b")
.background(Color.red)
.position(x: 50, y: 30)
Text("c")
.background(Color.red)
.position(x: 100, y: 40)
Text("d")
.background(Color.red)
.position(x: 150, y: 200)
}
Find the exact x and y position yourself
Fore more info, read this articel
https://www.hackingwithswift.com/books/ios-swiftui/absolute-positioning-for-swiftui-views
Sure. Set up constraints with offsets from your center point, and use trig to calculate the offsets. The code might look something like this:
let steps = 16 // The number of buttons you want
let radius = 75.0. // Your desired circle radius, in points
let angleStep = Double.pi * 2.0 / Double(steps)
for index in 0 ..< steps {
let angle = Double(index) * angleStep
let xOffset = CGFloat(radius * cos(angle))
let yOffset = CGFloat(radius * sin(angle))
// add button to superview with center anchored to center of superview
// offset by xOffset and yOffset
}
Edit:
I mostly create my views and controls using storyboards, so this was a good excuse to practice creating them in code. I made a demo project on github that creates a circle of buttons. You can download it here:
https://github.com/DuncanMC/ButtonsInCircle.git
All the logic is in the View controller's source file:
//
// ViewController.swift
// ButtonsInCircle
//
// Created by Duncan Champney on 2/28/21.
//
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var buttonContainerView: UIView!
let buttonCount = 12 //The number of buttons to create
var angleStep: Double = 0 //The change in angle between buttons
var radius: CGFloat = 75.0 // The radius to use (will be updated at runtime based on the size of the container view.)
//A type to hold a layout anchor for a button, it's index, and whether it's a horizontal or veritical anchor
typealias ConstraintTuple = (index: Int, anchor: NSLayoutConstraint, axis: NSLayoutConstraint.Axis)
//An array of the layout anchors for our buttons.
var constraints = [ConstraintTuple]()
//Our layout has changed. Update the array of layout anchors
func updateButtonConstraints() {
for (index, constraint, axis) in self.constraints {
let angle = Double(index) * self.angleStep
let xOffset = self.radius * CGFloat(cos(angle))
let yOffset = self.radius * CGFloat(sin(angle))
if axis == .horizontal {
constraint.constant = xOffset
} else {
constraint.constant = yOffset
}
}
}
override func viewDidLayoutSubviews() {
//Pick a radius that's a little less than 1/2 the shortest side of our bounding rectangle
radius = min(buttonContainerView.bounds.width, buttonContainerView.bounds.height) / 2 - 30
print("Radius = \(radius)")
updateButtonConstraints()
}
func createButtons() {
for index in 0 ..< buttonCount {
//Create a button
let button = UIButton(primaryAction:
//Define the button title, and the action to trigger when it's tapped.
UIAction(title: "Button \(index+1)") { action in
print("Button \(index + 1) tapped")
}
)
button.translatesAutoresizingMaskIntoConstraints = false //Remember to do this for UIViews you create in code
button.layer.borderWidth = 1.0 // Draw a rounded rect around the button so you can see it
button.layer.cornerRadius = 5
button.setTitle("\(index+1)", for: .normal)
button.setTitleColor(.blue, for: .normal)
//Add it to the container view
buttonContainerView.addSubview(button)
button.sizeToFit()
//Create center x & y layout anchors (with no offset to start)
let buttonXAnchor = button.centerXAnchor.constraint(equalTo: buttonContainerView.centerXAnchor, constant: 0)
buttonXAnchor.isActive = true
//Add a tuple for this layout anchor to our array
constraints.append(ConstraintTuple(index: index, anchor: buttonXAnchor, axis: .horizontal))
let buttonYAnchor = button.centerYAnchor.constraint(equalTo: buttonContainerView.centerYAnchor, constant: 0)
buttonYAnchor.isActive = true
//Add a tuple for this layout anchor to our array
constraints.append(ConstraintTuple(index: index, anchor: buttonYAnchor, axis: .vertical))
}
}
override func viewDidLoad() {
super.viewDidLoad()
angleStep = Double.pi * 2.0 / Double(buttonCount)
createButtons()
}
}
It looks like this when you run it:
(On iOS, the starting angle (0°) is "East" in terms of a compass. If you want your first button to start at the top you'd have to add an offset to your starting angle.)

How can I access a ForEach loop's index outside of it in another view?

I hope I worded the title correctly. Basically, I'm making a tinder-like interface that will allow the user to swipe through a set of cards. I've followed along with a tutorial to successfully make the swiping through the cards work. However, I can't for the life of me figure out how to get a button to do the same.
Here is an image of the ContentView ---> https://i.imgur.com/K9zN6Vj.png
Here is the main ContentView Code
import SwiftUI
struct ContentView: View {
let cards = Card.data.shuffled()
#State var isHelp = false
var body: some View {
VStack {
ZStack {
AnimatedBackground()
.ignoresSafeArea()
.blur(radius: 25)
VStack {
//Top Stack
Spacer()
TopStack()
//Card
ZStack {
ForEach(cards, id: \.id) { index in
CardView(card: index)
// .shadow(color: .black, radius: 10)
.padding(8)
}
}
//Bottom Stack
BottomStack()
}
}
}
}
}
Here is the CardView code
struct CardView: View {
#State var card: Card
var body: some View {
ZStack {
Color("myBlue")
Text("\(card.text)")
.foregroundColor(.white)
}
.cornerRadius(20)
// Step 1 - ZStack follows the coordinate of the card model
.offset(x: card.x, y: card.y)
.rotationEffect(.init(degrees: card.degree))
// Step 2 - Gesture recognizer updaets the coordinate calues of the card model
.gesture (
DragGesture()
.onChanged { value in
// user is dragging the view
withAnimation(.default) {
card.x = value.translation.width
card.y = value.translation.height
card.degree = 7 * (value.translation.width > 0 ? 1 : -1)
}
}
.onEnded { value in
// do something when the user stops dragging
withAnimation(.interpolatingSpring(mass: 1.0, stiffness: 50, damping: 8, initialVelocity: 0)) {
switch value.translation.width {
case 0...100:
card.x = 0; card.degree = 0; card.y = 0
case let x where x > 100:
card.x = 500; card.degree = 12
case (-100)...(-1):
card.x = 0; card.degree = 0; card.y = 0;
case let x where x < -100:
card.x = -500; card.degree = -12
default: card.x = 0; card.y = 0
}
}
}
)
}
}
The bottom stack is just buttons. I want the buttons to essentially go forward and backward through the index, but I don't know how to access it because it's in a different view. Again, I'm not entirely sure how the main swiping is working; I followed along with a tutorial so I'm definitely out of my comfort zone. Maybe a button wouldn't work at all with the way the swiping is being achieved? Any help is super appreciated, thank you for taking a look!

Resources