Swift Calculate Content Offset of item moving across visible screen - ios

I am re-creating the iPhone App Switcher page where the app's tab view size is based off the location of it on the visual screen (bigger on the right and smaller on the left). I I have an array of views within a scroll view. I want to set the size of each view (tab) based on the location / content offset of the view as it scrolls horizontally across the visible screen x-axis.
Here's my code in scrollViewDidScroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
tabViews.forEach { (tabView) in // tabViews: [UIView]
let screenWidth = UIScreen.main.bounds.width
// This should be a value between 0 and 1
var screenOffsetX = tabView.convert(CGPoint(x: tabView.frame.minX, y: 0),to: view).x
let maxValue: CGFloat = screenWidth / 8 // Max value is 1 + 1/8 scale size
if screenOffsetX > maxValue { // Set max scale
screenOffsetX = min(screenOffsetX, maxValue)
}
let minValue: CGFloat = 0
if screenOffsetX < minValue { // Set min scale
screenOffsetX = max(screenOffsetX, minValue)
}
let scaleAmount: CGFloat = 1 + (screenOffsetX / screenWidth)
let scaleTransform = CGAffineTransform(scaleX: scaleAmount, y: scaleAmount)
tabView.transform = scaleTransform
}
}
I think the math is off. I don't think the abcd.convert(point: ) is returning the correct content offset on the visual screen. Here's an image of the scroll view with views (tabs):
Each view should be slightly bigger on the right side of the screen and smaller on the left.
Any ideas?

tabView.convert won't return values between 0...1 for sure - it just translates a point to a different view coordinates.
Also it should be scrollView.convert, not tabView.convert.
Those checks can be simplified:
if screenOffsetX > maxValue { // Set max scale
screenOffsetX = min(screenOffsetX, maxValue)
}
There is no point of doing min after you already confirmed that maxValue is smaller. You either do:
if screenOffsetX > maxValue { // Set max scale
screenOffsetX = maxValue
}
or
screenOffsetX = min(screenOffsetX, maxValue)
I am not sure what is the usage in your code for maxValue and minValue, but if you want to scale things based on their position, I would suggest doing this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
tabViews.forEach { tabView in // tabViews: [UIView]
let screenWidth = UIScreen.main.bounds.width
var screenOffsetX = scrollView.convert(CGPoint(x: tabView.frame.minX, y: 0), to: view).x
var screenOffsetPercentage = screenOffsetX / screenWidth
let minValue: CGFloat = 0.5
let maxValue: CGFloat = 1
let scaleAmount = minValue + (maxValue - minValue) * screenOffsetPercentage
let scaleTransform = CGAffineTransform(scaleX: scaleAmount, y: scaleAmount)
tabView.transform = scaleTransform
}
}
You will have to adapt it to your idea, but as a start it scales from 1.0 scale at right side, to 0.5 scale at the left side.
Results looks like: https://media.giphy.com/media/m5KSgIInJmGjEHLZXb/giphy.gif

Related

Spritekit Camera Node scale but pin the bottom of the scene

I have a camera node that is scaled at 1. When I run the game, I want it to scale it down (i.e. zoom out) but keep the "floor" at the bottom. How would I go about pinning the camera node to the bottom of the scene and effectively zooming "up" (difficult to explain). So the bottom of the scene stays at the bottom but the rest zooms out.
I have had a go with SKConstraints but not having any luck (I'm quite new at SpriteKit)
func setConstraints(with scene: SKScene, and frame: CGRect, to node: SKNode?) {
let scaledSize = CGSize(width: scene.size.width * xScale, height: scene.size.height * yScale)
let boardContentRect = frame
let xInset = min((scaledSize.width / 2), boardContentRect.width / 2)
let yInset = min((scaledSize.height / 2), boardContentRect.height / 2)
let insetContentRect = boardContentRect.insetBy(dx: xInset, dy: yInset)
let xRange = SKRange(lowerLimit: insetContentRect.minX, upperLimit: insetContentRect.maxX)
let yRange = SKRange(lowerLimit: insetContentRect.minY, upperLimit: insetContentRect.maxY)
let levelEdgeConstraint = SKConstraint.positionX(xRange, y: yRange)
if let node = node {
let zeroRange = SKRange(constantValue: 0.0)
let positionConstraint = SKConstraint.distance(zeroRange, to: node)
constraints = [positionConstraint, levelEdgeConstraint]
} else {
constraints = [levelEdgeConstraint]
}
}
then calling the function with:
gameCamera.setConstraints(with: self, and: scene!.frame, to: nil)
(This was code from a tutorial I was following) The "setConstraints" function is an extension of SKCameraNode
I'm not sure this will give me the correct output, but when I run the code to scale, it just zooms from the middle and shows the surrounding area of the scene .sks file.
gameCamera.run(SKAction.scale(to: 0.2, duration: 100))
This is the code to scale the gameCamera
EDIT: Answer below is nearly what I was looking for, this is my updated answer:
let scaleTo = 0.2
let duration = 100
let scaleTop = SKAction.customAction(withDuration:duration){
(node, elapsedTime) in
let newScale = 1 - ((elapsedTime/duration) * (1-scaleTo))
let currentScaleY = node.yScale
let currentHeight = node.scene!.size.height * currentScaleY
let newHeight = node.scene!.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.setScale(newScale)
node.position.y += yOffset
}
You cannot use a constraint because your scale size is dynamic.
Instead you need to move your camera position to give the illusion it is only scaling in 3 directions.
To do this, I would recommend creating a custom action.
let scaleTo = 2.0
let duration = 1.0
let currentNodeScale = 0.0
let scaleTop = SKCustomAction(withDuration:duration){
(node, elapsedTime) in
if elapsedTime == 0 {currentNodeScale = node.scale}
let newScale = currentNodeScale - ((elapsedTime/duration) * (currentNodeScale-scaleTo))
let currentYScale = node.yScale
let currentHeight = node.scene.size.height * currentYScale
let newHeight = node.scene.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.scale(to:newScale)
node.position.y += yOffset
}
What this is doing is comparing the new height of your camera with the old height, and moving it 1/2 the distance.
So if your current height is 1, this means your camera sees [-1/2 to 1/2] on the y axis. If you new scale height is 2, then your camera sees [-1 to 1] on the y axis. We need to move the camera up so that the camera sees [-1/2 to 3/2], meaning we need to add 1/2. So we do 2 - 1, which is 1, then go 1/2 that distance. This makes our yOffset 1/2, which you add to the camera.

Swift: How to get Height and width of a rotated image.

I am rotating a square image View whose width is 160 but after rotation width become 190 same as height.
Rotating it by this function
func transformUsingRecognizer(_ recognizer: UIGestureRecognizer, transform: CGAffineTransform) -> CGAffineTransform {
if let rotateRecognizer = recognizer as? UIRotationGestureRecognizer {
return transform.rotated(by: rotateRecognizer.rotation)
}
return transform
}
tried these two codes to get correct width
imgDraggingImage.bounds.size.width
imgDraggingImage.frame.height
but no success.
Image before rotation width 160
Image after rotation, no change in width but now returning width 190
Seems width value depends on rotation as on any different angle it returns different width value. But I need correct value which in this case is 160. please help
extension CGAffineTransform {
var angle: CGFloat { return atan2(-self.c, self.a) }
var angleInDegrees: CGFloat { return self.angle * 180 / .pi }
var scaleX: CGFloat {
let angle = self.angle
return self.a * cos(angle) - self.c * sin(angle)
}
var scaleY: CGFloat {
let angle = self.angle
return self.d * cos(angle) + self.b * sin(angle)
}
}
The above extension solve the problem.
self.imgDraggingImage.transform.scaleX
self.imgDraggingImage.transform.scaleY
the above code scaleX gave change in Width of image (scale) even if rotated too and scaleY provide the % change in image height.
where imgDraggingImage.bounds.size.width gave constant image size even if image scaled.
while imgDraggingImage.frame.width was creating issue when image was rotated.
Depends, do you want the size in Pixels or in Points:
let heightInPoints = image.size.height
let heightInPixels = heightInPoints * image.scale
let widthInPoints = image.size.width
let widthInPixels = widthInPoints * image.scale

how to prevent view to exceed its parent bounds on Pinch Gesture - swift

I assigned the pinch gesture to uiview:
myView.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: "handlePinch:"))
This function will scale the uiview:
func handlePinch(recognizer : UIPinchGestureRecognizer) {
if let view = recognizer.view {
view.transform = CGAffineTransformScale(view.transform,
recognizer.scale, recognizer.scale)
recognizer.scale = 1
}
}
but the scale is not limited within the parent view. and when i try to scale it down it has also no limit, it scales till it disappears.
The grey view is the parent
My Question is
How can it be prevented from exceeding the parent frame on scale up and disappearing on scale down?
You could use something like this:
if let view = recognizer.view, parent = recognizer.view.superview {
// this will only let it scale to half size
let minimumThreshold: CGFloat = 0.5
var scale: CGFloat = recognizer.scale
// assuming your view is square, which based on your example it is
let newSize = view.frame.height * scale
// prevents the view from growing larger than the smallest dimension of the parent view
let allowableSize = min(parent.frame.height, parent.frame.width)
let maximumScale: CGFloat = allowableSize/view.frame.height
// change scale if it breaks either bound
if scale < minimumThreshold {
print("size is too small")
scale = minimumThreshold
}
if newSize > allowableSize {
print("size is too large")
scale = maximumScale
}
// apply the transform
view.transform = CGAffineTransformMakeScale(scale, scale)
}
The key here is to decide on a lower bound based on preference, and an upper bound based on the ratio of your view's initial size to the parent's size. By doing so, the view cannot be shrunk smaller than desired, as assumedly the user should not be able to scale down to a point where they cannot resize due to difficulty pinching. In addition, the view should not be allowed to be scaled infinitely, thus overflowing its bounds. This check does just that, as it allows only a range of sizes you dictate to be set by the user's interactions.
I wrote a function to test if the pinch is out of parent view's boundary.
/// test if an origin frame scale would be within the boundary of parent view
func scaleWithinBoundary(originFrame:CGRect, parent:UIView, scale:CGFloat) -> Bool {
let height = originFrame.height * scale
let width = originFrame.width * scale
let leftX = originFrame.midX - width/2, rightX = originFrame.midX + width/2
let topY = originFrame.midY - height/2, bottomY = originFrame.midY + height/2
return leftX >= 0 && topY >= 0 && leftX >= parent.frame.minX && rightX <= parent.frame.maxX
&& topY >= parent.frame.minY && bottomY <= parent.frame.maxY
}

How do I add a button at every x degrees? [duplicate]

I have an array of buttons and when I append them to a view I want the to be positioned around a image view which is in the center. Based on how many objects there are in the array, I want them to be evenly spaced around the whole circle. Below is my attempt to do so. What am I doing wrong and how should I fix it? There is more than one button behind the moose.
var userbutton = [UIButton]()
var upimage = [UIImage]()
var locationpic = [AnyObject]()
func locationsSet(){
for (index, users) in upimage.enumerate() {
let userbutton = UIButton()
userbutton.addTarget(self, action: "buttonAction:", forControlEvents: .TouchUpInside)
userbutton.frame = CGRectMake(100, 100, 50, 50)
userbutton.layer.cornerRadius = userbutton.frame.size.width/2
userbutton.clipsToBounds = true
userbutton.setImage(users, forState: .Normal)
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * (radius + 40)
let pointy = (centery) + (sin(radians)) * (radius + 40)
userbutton.center.x = pointx
userbutton.center.y = pointy
self.userbutton.append(userbutton)
self.view.addSubview(userbutton)
print("x\(pointx)")
print("y\(pointy)")
}
}
How I would do this:
Create an extension to UIView to get the diagonal and radius. These are handy because we want our "satellites" to have predictable placing even when the "planet" isn't square.
extension UIView {
var diagonal : CGFloat {
return sqrt(pow(self.frame.width, 2) + pow(self.frame.height, 2))
}
var radius : CGFloat {
return diagonal / 2
}
}
This will return a point based on an angle and a distance from an origin.
It uses dreadful trigonometry.
func getPoint(fromPoint point: CGPoint, atDistance distance: CGFloat, withAngleRadians angle:CGFloat) -> CGPoint {
let x = point.x
let y = point.y
let dx = (distance * cos(angle))
let dy = (distance * sin(angle))
return CGPoint(x: (dx + x), y: (dy + y))
}
Now the real function. Generate a bunch of points in a circle pattern. I used a running sum for the angle instead of multiplying each time by the index. This just returns the centre points for the views.
func encirclePoint(point : CGPoint, distance:CGFloat, inParts parts: Int) -> [CGPoint] {
let angle = 2 * CGFloat(M_PI) / CGFloat(parts) // critical part, you need radians for trigonometry
var runningAngle : CGFloat = -(CGFloat(M_PI) / 2) // start at the top
var points : [CGPoint] = []
for _ in 0..<parts {
let circlePoint = getPoint(fromPoint: point, atDistance: distance, withAngleRadians: runningAngle)
points.append(circlePoint)
runningAngle += angle
}
return points
}
Now you can create a simple function that takes a view, a margin and an array of "satellite" views. It will set their centre and add them to the superview of the view we used to input. It makes sense not to add them to the view itself since they might not be placed inside it.
func encircleView(view : UIView, withSubViews subViews : [UIView], withMargin margin : CGFloat) {
guard !(subViews.isEmpty) else { // if there are no subviews : abort
return
}
let distance = view.radius + margin
let points = encirclePoint(view.center, distance: distance, inParts: subViews.count)
guard subViews.count == points.count, let uberView = view.superview else { // if the count is not the same or there is no superview: abort
return
}
for (point, subView) in zip(points, subViews) { subView.center = point }
}
Notice how I did nothing except for the centre calculations in these functions. Styling them goes in another function. This makes it super easy to maintain and debug.
I might even let the last function just return the subviews with updated frames and add them later.
Or negative margin :)
Gist
A full circle is 2 * pi radians. You need to divide that by the number of items you have and multiply that by the index of the item you are currently processing. Use trig to find the location on the circle:
for (index, users) in upimage.enumerate() {
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
......
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * radius
let pointy = centery + sin(radians) * radius
......
}

How do I place the objects in the array around the center image?

I have an array of buttons and when I append them to a view I want the to be positioned around a image view which is in the center. Based on how many objects there are in the array, I want them to be evenly spaced around the whole circle. Below is my attempt to do so. What am I doing wrong and how should I fix it? There is more than one button behind the moose.
var userbutton = [UIButton]()
var upimage = [UIImage]()
var locationpic = [AnyObject]()
func locationsSet(){
for (index, users) in upimage.enumerate() {
let userbutton = UIButton()
userbutton.addTarget(self, action: "buttonAction:", forControlEvents: .TouchUpInside)
userbutton.frame = CGRectMake(100, 100, 50, 50)
userbutton.layer.cornerRadius = userbutton.frame.size.width/2
userbutton.clipsToBounds = true
userbutton.setImage(users, forState: .Normal)
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * (radius + 40)
let pointy = (centery) + (sin(radians)) * (radius + 40)
userbutton.center.x = pointx
userbutton.center.y = pointy
self.userbutton.append(userbutton)
self.view.addSubview(userbutton)
print("x\(pointx)")
print("y\(pointy)")
}
}
How I would do this:
Create an extension to UIView to get the diagonal and radius. These are handy because we want our "satellites" to have predictable placing even when the "planet" isn't square.
extension UIView {
var diagonal : CGFloat {
return sqrt(pow(self.frame.width, 2) + pow(self.frame.height, 2))
}
var radius : CGFloat {
return diagonal / 2
}
}
This will return a point based on an angle and a distance from an origin.
It uses dreadful trigonometry.
func getPoint(fromPoint point: CGPoint, atDistance distance: CGFloat, withAngleRadians angle:CGFloat) -> CGPoint {
let x = point.x
let y = point.y
let dx = (distance * cos(angle))
let dy = (distance * sin(angle))
return CGPoint(x: (dx + x), y: (dy + y))
}
Now the real function. Generate a bunch of points in a circle pattern. I used a running sum for the angle instead of multiplying each time by the index. This just returns the centre points for the views.
func encirclePoint(point : CGPoint, distance:CGFloat, inParts parts: Int) -> [CGPoint] {
let angle = 2 * CGFloat(M_PI) / CGFloat(parts) // critical part, you need radians for trigonometry
var runningAngle : CGFloat = -(CGFloat(M_PI) / 2) // start at the top
var points : [CGPoint] = []
for _ in 0..<parts {
let circlePoint = getPoint(fromPoint: point, atDistance: distance, withAngleRadians: runningAngle)
points.append(circlePoint)
runningAngle += angle
}
return points
}
Now you can create a simple function that takes a view, a margin and an array of "satellite" views. It will set their centre and add them to the superview of the view we used to input. It makes sense not to add them to the view itself since they might not be placed inside it.
func encircleView(view : UIView, withSubViews subViews : [UIView], withMargin margin : CGFloat) {
guard !(subViews.isEmpty) else { // if there are no subviews : abort
return
}
let distance = view.radius + margin
let points = encirclePoint(view.center, distance: distance, inParts: subViews.count)
guard subViews.count == points.count, let uberView = view.superview else { // if the count is not the same or there is no superview: abort
return
}
for (point, subView) in zip(points, subViews) { subView.center = point }
}
Notice how I did nothing except for the centre calculations in these functions. Styling them goes in another function. This makes it super easy to maintain and debug.
I might even let the last function just return the subviews with updated frames and add them later.
Or negative margin :)
Gist
A full circle is 2 * pi radians. You need to divide that by the number of items you have and multiply that by the index of the item you are currently processing. Use trig to find the location on the circle:
for (index, users) in upimage.enumerate() {
let radians = CGFloat(M_PI) * 2.0 / CGFloat(upimage.count) * CGFloat(index)
......
let centerx = self.view.bounds.width / 2.0
let radius = currentuserpic.frame.size.width / 2.0
let centery = self.view.bounds.height / 2.0
let pointx = centerx + cos(radians) * radius
let pointy = centery + sin(radians) * radius
......
}

Resources