UIScrollView's contentOffset gets truncated when adding small number - ios

I have swift 2.0 ios app with storyboard. I have a tableViewController and in each row I have scrollView (with user scrolling disabled). Inside this scrollView I have an image, which fits scroll's size and it's a bit taller. Thing is I want to achieve "floating" effect on this image, when tableView is scrolled.
Here's a code:
override func scrollViewDidScroll(scrollView: UIScrollView) {
let cells = tableView.visibleCells
let offsetY = scrollView.contentOffset.y
var diff:CGFloat = (offsetY - lastY) / 10
for cell in cells {
let indexPath = tableView.indexPathForCell(cell)!
let scroll = cell.viewWithTag(102) as! UIScrollView
let oldY = scroll.contentOffset.y
let newY = oldY + diff
let height = scroll.contentSize.height
if(newY >= 0 && newY + cellHeight < height){
scroll.contentOffset = CGPointMake(scroll.contentOffset.x, newY)
}
if(indexPath.row == 0){
print("\(oldY) + \(diff) = \(newY)")
}
}
lastY = offsetY
}
What I'm doing here is calculating difference of tableView scroll position (variable "diff"), get all visible cells and scroll scrollView's by that difference (divided by 10 and in opposite direction). When I scroll fast it's somehow working, but when I scroll very slowly nothing is moving.
When I'm slowly scrolling I'm getting output like this (last print):
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
15.5 + 0.05 = 15.55
When diff is too small scrollView is somehow truncating (or rounding) a result.
When I'm scrolling faster:
23.0 + 0.25 = 23.25
23.5 + 0.25 = 23.75
24.0 + 0.15 = 24.15
24.0 + 0.25 = 24.25
24.5 + 0.25 = 24.75
25.0 + 0.25 = 25.25
25.5 + 0.25 = 25.75
Here you can see it's suming up, but some results does not match (sum of line x should be first number in the next line).
It's not an issue with CGFloat as simple example shows:
var sum:CGFloat = 0
for(var i = 0; i < 10; i++){
let number:CGFloat = 0.05
sum += number
print(sum)
}
Results in:
0.05
0.1
0.15
0.2
0.25
0.3
0.35
0.4
0.45
0.5
How can I fix it?
Maybe I should use a different approach to get similar effect?

UIScrollView.contentOffset is rounded to 0.5 on retina displays (0.33 on iPhone 6+) when set.
I suggest creating custom UITableViewCell subclass (if you don't have it yet) and create custom variable inside it:
class MyCell: UITableViewCell {
var unroundedContentOffsetY: CGFloat {
didSet {
scroll.contentOffset = CGPoint(x: scroll.contentOffset.x, unroundedContentOffsetY)
}
}
}
Then make calculations on cell.unroundedContentOffsetY variable instead of cell.scroll.contentOffset

Apparently one can also override the contentOffset property in the custom subclass like this:
var unrounded: CGPoint?
override var contentOffset: CGPoint {
get {
return unrounded ?? .zero
}
set {
unrounded = newValue
super.contentOffset = newValue
}
}
This has the advantage that it also works when calling setContentOffset(_:animated:), not only when assigning to contentOffset.

Related

Swift Calculate Content Offset of item moving across visible screen

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

Calculations of the 80% of screen and 20% in UICollectionView

I write function that return size of the cell for the UICollectionView.
fileprivate func getCellSize(with row: Int) -> CGSize {
let percentage: CGFloat = row == 0 ? 0.8 : 0.2
let width = self.collectionView?.frame.width
let height = self.collectionView?.frame.height
let expectedWidth = width! * percentage
let expectedHeight = height! * 0.5
return CGSize(width: expectedWidth, height: expectedHeight)
}
This function is working fine but it has small issue that is connected with rounding it seams to me. Because layout that I receive is not fully covered with cells as it is expected.
The result of the function for the iPhone 6 emulator is following:
0: ROW = (533.60000000000002,187.5)
1: ROW = (133.40000000000001,187.5)
Actual result:
Expected result:
How about
if let frameWidth = self.collectionView?.frame.width {
let row0Width = Int(Double(frameWidth) * 0.8)
let otherWidth = frameWidth - row0Width
let expectedWidth = row == 0 ? row0Width : otherWidth
// ...
}
to avoid rounding issues?

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

Making Sure Nodes are visible

So I have this sprite kit game, which is coded in swift 2. The game includes these colored circles (Green, Red, Purple, Yellow, Blue) that fall down the screen, starting from the same height, but starting at different widths. When the circles hit the bottom of the screen, the respectable method is called. The problem I am having is, the random x position can sometimes cut half of the circle off because it is on the very side of the screen. How can I prevent the circles from clipping the side of the screen? Here are the methods that are called when the circles hit the bottom of the screen.
func changeGreen(){
Green.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Green.position.x = CGFloat(PositionX)
}
func changeRed(){
Red.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Red.position.x = CGFloat(PositionX)
}
func changeBlue() {
Blue.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Blue.position.x = CGFloat(PositionX)
}
func changeYellow() {
Yellow.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Yellow.position.x = CGFloat(PositionX)
}
func changePurple() {
Purple.position.y = frame.size.height * 0.9
let PositionX = arc4random_uniform(UInt32(self.frame.width))
Purple.position.x = CGFloat(PositionX)
}
Assuming position.x is the center of the circle, I think something like this might work:
let maxX = 350 // this is your frame width
let radius = 50 // radius of the circle
var positionX = Int(arc4random_uniform(UInt32(maxX - radius))) // max position of x is taken care of here
let pointsFromLeft = positionX - radius
if pointsFromLeft < 0 {
positionX -= pointsFromLeft // move to the right if necessary
}
You have to offset the radius of the circle from both ends of frame so that the circles never get clipped.
let radius = 20 // radius of your circle
let positionX = radius + arc4random_uniform(UInt32(self.frame.width - 2 * radius))

Resources