I'm trying to position a button at a fixed position inside a UIImageView (AspectFit) which itself is inside a UIScrollView. This worked perfectly on my first try when the UIScrollView and the UIImageView containers both covered the whole screen, the button was pinned to a certain location of the image and stayed at position during zooming. You can see the result in the below image.
As you can obviously see there are white borders above and below the image (related to aspect fit) therefore I had to do some calculation to get the margin from top to calculate the "real" y position of the red square. My code looks like this:
let originalImageSize: CGSize = CGSize(width: image.size.width, height: image.size.height)
let aspectRatio = image.size.width/image.size.height;
let requiredHeight = self.view.bounds.size.width / aspectRatio;
let screenHeight = self.view.bounds.height;
let marginTop = (screenHeight - requiredHeight) / 2;
let renderedImageSize: CGSize = CGSize(width: self.view.bounds.width, height: requiredHeight)
let x:Double = 0
let y:Double = 0
let button = UIButton()
button.frame = CGRect(x: Double(renderedImageSize.width/originalImageSize.width) * x,
y: Double(renderedImageSize.height/originalImageSize.height) * y + Double(marginTop),
width: 10, height: 10)
button.backgroundColor = UIColor.red
imageView.addSubview(button)
As you see I calculated the "marginTop" to get the real y position. The square is perfectly located on x: 0 and y:0 (relative to the image). So far so good, this example worked perfectly.
Now I created a new view which contains a navigation bar and tab bar. The scrollView is in between and no longer covers the whole screen but only the area between my navigation and my tab bar. The imageView has the same size like my scrollView. Pretty much the same as in the example above. Now I tried to set my button a specific location again, but this time there is an offset on the y axis of exactly 6 pixels and I have no idea what I'm doing wrong. And to make it even worse when testing it on other devices the offset on the y axis is even bigger than 6 pixels, while the first example works perfectly accross all devices I tested. You can see the result with the "wrong" y-axis value here.
I changed my code to the following, based on the fact that sizes should be calculated according to the "new" scrollView size.
let originalImageSize: CGSize = CGSize(width: image.size.width, height: image.size.height)
let aspectRatio = image.size.width/image.size.height;
let requiredHeight = scrollView.bounds.size.width / aspectRatio;
let screenHeight = scrollView.bounds.height;
let marginTop = (screenHeight - requiredHeight) / 2;
let renderedImageSize: CGSize = CGSize(width: scrollView.bounds.width, height: requiredHeight)
let x:Double = 0
let y:Double = 0
let button = UIButton()
button.frame = CGRect(x: Double(renderedImageSize.width/originalImageSize.width) * x,
y: Double(renderedImageSize.height/originalImageSize.height) * y + Double(marginTop),
width: 10, height: 10)
button.backgroundColor = UIColor.red
imageView.addSubview(button)
A quick workaround would be something likes this, but it is hacky as hell and doesn't work for other device sizes and I obviously want to learn how to do it the right way:
[...] y: Double(renderedImageSize.height/originalImageSize.height) * y + Double(marginTop) - 6, [...]
I've been sitting on this for hours now and still don't have any idea why the y-axis is off even though the calculation of the top margin should be right and why the y axis offset is even bigger now on different devices. I'm thankful for any advice as I'm pretty new to iOS developing and I guess I'm missunderstanding something related to calculating correct sizes.
Turns out, that the solution to my problem is rather simple. I first started wondering when I saw that scrollView.bounds.size.height
returned a bigger value than the actual screen height on devices smaller than the iPhone X, which seemed pretty strange. Then I tried to figure out how to find the "real" size of my scrollView on different devices, because it is obviously not really bigger than the whole screen according to the simulator visual result.
So instead of doing the above quoted calculation inside viewDidLoad() doing all my calculation in viewDidLayoutSubviews() instead and this solved the whole problem.
The only thing I'm still wondering about is, why there was an offset even on the iPhone X as it was my default template in Xcode.
Related
Im trying to remove the top part of an image by cropping, but the result is unexpected.
The code used:
extension UIImage {
class func removeStatusbarFromScreenshot(_ screenshot:UIImage) -> UIImage {
let statusBarHeight = 44.0
let newHeight = screenshot.size.height - statusBarHeight
let newSize = CGSize(width: screenshot.size.width, height: newHeight)
let newOrigin = CGPoint(x: 0, y: statusBarHeight)
let imageRef:CGImage = screenshot.cgImage!.cropping(to: CGRect(origin: newOrigin, size: newSize))!
let cropped:UIImage = UIImage(cgImage:imageRef)
return cropped
}
}
My logic is that I need to make the image smaller in heigh by 44px and move the origin y by 44px, but it ends up only creating an image much smaller of the top left corner.
The only way that I get it to work as expected is by multiplying the width by 2 and height by 2.5 in newSize, but that also double the size of the image produced..
Which anyways doesnt make much sense.. can someone help make it work without using magic values?
There are two main problems with what you're doing:
A UIImage has a scale (usually tied to resolution of your device's screen), but a CGImage does not.
Different devices have different "status bar" heights. In general, what you want to cut off from the top is not the status bar but the safe area. The top of the safe area is where your content starts.
Because of this:
You are wrong to talk about 44 px. There are no pixels here. Pixels are physical atomic illuminations on your screen. In code, there are points. Points are independent of the scale (and the scale is the multiplier between points and pixels).
You are wrong to talk about the number 44 itself as if it were hard-coded. You should get the top of the safe area instead.
By crossing into the CGImage world without taking scale into account, you lose the scale information, because CGImage knows nothing of scale.
By crossing back into the UIImage world without taking scale into account, you end up with a UIImage with a resolution of 1, which may not be the resolution of the original UIImage.
The simplest solution is not to do any of what you are doing. First, get the height of the safe area; call it h. Then just draw the snapshot image into a graphics image context that is the same scale as your image (which, if you play your cards right, it will be automatically), but is h points shorter than the height of your image — and draw it with its y origin at -h, thus cutting off the safe area. Extract the resulting image and you're all set.
Example! This code comes a view controller. First, I'll take a screenshot of my own device's current screen (this view controller's view) as my app runs:
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
let screenshot = renderer.image { context in
view.layer.render(in: context.cgContext)
}
Now, I'll cut the safe area off the top of that screenshot:
let h = view.safeAreaInsets.top
let size = screenshot.size
let r = UIGraphicsImageRenderer(
size: .init(width: size.width, height: size.height - h)
)
let result = r.image { _ in
screenshot.draw(at: .init(x: 0, y: -h))
}
Experimentation will confirm that this works perfectly on every device, regardless of whether it has a bezel and regardless of its screen resolution: the top of the resulting image, result, is the top of your actual content.
I have a label in GameScene, there is no auto layout to use here, how I can use CGpoint to make sure this label stay at the same position on different screens.
Below is the code where I put my score label
scoreLabel?.position = CGPoint(x: -size.width / 2 + 120, y: size.height / 2 - 150)
let scoreLabel = //insert functionality of scoreLabel here
scoreLabel?.position = CGPoint(x: size.width + //insert dimention,
y: size.height + //insert dimension)
self.addChild(scoreLabel)
seeing your previous comment, I would suggest you use the following functions, which change the dimensions when they are run on different screen sizes:
AspectFill
AspectFit
ResizeFill
Here is a link that could help you on how to use these functions: Sprite Kit Scene Editor GameScene.sks scene width and height
Hope this helps!
Here is what I put in my View Controller:
var h, w = 1000
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
w = view.frame.width / (view.frame.height / 1000)
let scene = GameScene(size: CGSize(width: w, height: h))
scene.scaleMode = .aspectFill
view.presentScene(scene)
}
}
}
It's handy to know that I make the height of any device 1000. It's much easier to make it universal.
Then I place everything percentage-wise on the screen.
label.position.x = w/2
label.position.y = h/2
label2.position.x = w/2
label2.position.y = h/2 - 50
These lines of code will place 2 labels in the center of the screen. Even if you rotate your device, and even if you use different sized devices. That - 50 I used will always stay the same.
My main takeaway: I create global states of the relative size of the screen.
Helpful tip: I always make my scene's anchor point to be .zero. That way, the min width and height are always 0, and the max height is always 1000.
Edit
This is what I want visualised (ignore the ugly red line, it just indicates the movement of the UIView):
I want to have a UIView that is initialised in the middle of the screen. After that, I want to give it a push upwards and the gravity pulls it down till it is off the screen. My old question works with a UIPushBehaviour, UIDynamicBehaviour and a UIGravityBehaviour (see below). Matt pointed out a UIPushBehaviour is maybe not the right choice, since it not work out well across every screen size available on iOS.
I can do this with a UIView.animate function, but it is really static and does not look natural. With the UIPushBehaviour, UIDynamicBehaviour and UIGravityBehaviour, it looks really nice but the UIPushBehaviour's magnitude can not be calculated across every screen size to give the same ending point of the UIView's x and y position.
Question
How can I initialise a UIView in the middle of the screen, 'pull up' that UIView (with some change in the x position) and let the gravity (or something else) pulls it down until it is off the screen? It is important that the change in the x and y position will be the same on every screen size.
Below is my old question
I have a UIPushBehaviour with instantaneous as mode in which I push some UIViews around. The greater the screen size, the less it pushes.
I also have a UIDynamicItemBehavior with resistance set to 1, I think this is one the main reasons it is different in each screen size (correct me if I am wrong).
I want a function that will push the UIView to the same ending point, with the same speed, duration and ending point regardless of the screen size.
I tried to make a relative magnitude without any luck:
For the iPhone 5S, let's say a magnitude of 0.5 would touch a UIView from the middle to the top. I wanted to calculate the magnitude across all devices like this:
let y = 0.5 / 520 // 5S screen height
magnitude = self.view.frame.height * y
For the iPhone 8, it has a very different output and is not working. When reading the docs, I thought I would understand it. I thought 1 magnitude represents 100 pixels, but it is clearly not that case.
Is there any way I can calculate a magnitude to, for example, move a UIView from the middle to the right?
I made a project here. There is a black UIView that get's pushed to the edges on an iPhone 5, but not on the iPhone 8.
Solution
You need to scale the push amount relative to the size of the screen so your view always ends in the same place. To do this, adjusting the UIPushBehavior's pushDirection vector works quite well. In this case, I set the push direction to be proportional to the bounds of the view, and scaled it down by a constant factor.
let push = UIPushBehavior(items: [pushView], mode: .instantaneous)
let pushFactor: CGFloat = 0.01
push.pushDirection = CGVector(dx: -view.bounds.width * pushFactor, dy: -view.bounds.height * pushFactor)
animator.addBehavior(push)
You may need to adjust some constants to get the exact animation you want. The constants you can adjust are:
Gravity magnitude (currently 0.3)
Push factor (currently 0.01)
Depending on your needs, you may need to scale the gravity magnitude proportional to the size of the screen as well.
Note: These constants will need to change based on the size of your animated view, since UIKit Dynamics treats the size of the view as its mass. If your view needs to be dynamically sized, you will need to scale your constants according to the size of the animated view.
Edit regarding comments on the original question:
Views of varying sizes: Like I mentioned in my note above, you'll need to apply an additional factor to account for the "mass" of the views. Something like view.frame.height * view.frame.width * someConstant should work well.
iPad screen size: Currently the pushFactor is applied to both the dx and dy components of the vector. Because iPads have a different aspect ratio, you'll need to split this into two constants, maybe xPushFactor and yPushFactor, which can account for the differences in aspect ratio.
Examples
iPhone 8
iPhone SE
Full Playground Source Code
Copy and paste this code into a Swift playground to see it in action. I've included the sizes of various iPhone screens, so just uncomment the size you want to easily test the animation on different device sizes. Most of the interesting/relevant code is in viewDidAppear.
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let pushView = UIView()
var animator: UIDynamicAnimator!
override func viewDidLoad() {
super.viewDidLoad()
view.frame = CGRect(x: 0, y: 0, width: 568, height: 320) // iPhone SE
// view.frame = CGRect(x: 0, y: 0, width: 667, height: 375) // iPhone 8
// view.frame = CGRect(x: 0, y: 0, width: 736, height: 414) // iPhone 8+
// view.frame = CGRect(x: 0, y: 0, width: 812, height: 375) // iPhone X
view.backgroundColor = .white
let pushViewSize = CGSize(width: 200, height: 150)
pushView.frame = CGRect(x: view.bounds.midX - pushViewSize.width / 2, y: view.bounds.midY - pushViewSize.height / 2, width: pushViewSize.width, height: pushViewSize.height)
pushView.backgroundColor = .red
view.addSubview(pushView)
animator = UIDynamicAnimator(referenceView: self.view)
let dynamic = UIDynamicItemBehavior()
dynamic.resistance = 1
animator.addBehavior(dynamic)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let gravity = UIGravityBehavior(items: [pushView])
gravity.magnitude = 0.3
animator.addBehavior(gravity)
let push = UIPushBehavior(items: [pushView], mode: .instantaneous)
let pushFactor: CGFloat = 0.01
push.pushDirection = CGVector(dx: -view.bounds.width * pushFactor, dy: -view.bounds.height * pushFactor)
animator.addBehavior(push)
}
}
let vc = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = vc.view
I have a UIScrollView which contains a UIImage. On top of that is a box that the user can move the image, so that that portion is cropped.
This screenshot explains it better:
So they can scroll the image around until the portion they want is inside that box.
I then want to be able to crop the scrollView/UIImage to exactly that size and store the cropped image.
It shouldn't be very hard but I've spent ages trying screenshots, UIGraphicsContext, etc. and cant seem to get anything to work.
Thanks for the help.
I finally figured out how to get it to work. Here is the code:
func croppedImage() -> UIImage {
let cropSize = CGSize(width: 280, height: 280)
let scale = (imageView.image?.size.height)! / imageView.frame.height
let cropSizeScaled = CGSize(width: cropSize.width * scale, height: cropSize.height * scale)
if #available(iOS 10.0, *) {
let r = UIGraphicsImageRenderer(size: cropSizeScaled)
let x = -scrollView.contentOffset.x * scale
let y = -scrollView.contentOffset.y * scale
return r.image { _ in
imageView.image!.draw(at: CGPoint(x: x, y: y))
}
} else {
return UIImage()
}
}
So it first calculates the scale of the imageView and the actual image.
Then it creates a CGSize of that crop box as shown in the photo. However, the width and height must be scaled by the scale factor. (e.g. 280 * 6.5)
You must check if the phone is running iOS 10.0 for UIGraphicsImageRender - if not, it won't work.
Initialise this with the crop box size.
The image must then be offset, and this is calculated by getting the scrollView's content offset, negating it, and multiplying by the scale factor.
Then return the image drawn at that point!
I'm trying to draw on top of an image in a CALayer and am having trouble with where the drawing shows up on different size displays.
func drawLayer(){
let circleLayer = CAShapeLayer()
let radius: CGFloat = 30
let x = Thermo.frame.origin.x
let y = Thermo.frame.origin.y
let XX = Thermo.frame.width
let YY = Thermo.frame.height
print("X: \(x) Y: \(y) Width: \(XX) Height: \(YY)")
circleLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 2.0 * radius, height: 2.0 * radius) , cornerRadius: radius).CGPath
circleLayer.fillColor = UIColor.redColor().CGColor
circleLayer.shadowOffset = CGSizeMake(0, 3)
circleLayer.shadowRadius = 5.0
circleLayer.shadowColor = UIColor.blackColor().CGColor
circleLayer.shadowOpacity = 0.8
circleLayer.frame = CGRectMake(0, 410, 0, 192);
self.Thermo.layer.addSublayer(circleLayer)
circleLayer.setNeedsDisplay()
}
That draws a circle, in the correct place ... for an iPhone 6s. But when the enclosing UIImageView component is scaled for a smaller device, well, to clearly doesn't. I added the print() to see what the image size, and position was and ... well, it's exactly the same on every device I run it on X: 192.0 Y: 8.0 Width: 216.0 Height: 584.0 but clearly it's being scaled by the constraints in the AuoLayout manager.
So, my question is how can I figure out the proper radio and position for different screen sizes if I can't use the enclosing View's size and position since that seems to never change?
Here is the image I am starting with, in a UIImageView, and trying to draw over.
Im of course trying to color it in based on data from an external device. Any suggestions/sample code most appreciated!
CALayer and its subclasses incl. CAShapeLayer have a property
var contentsScale: CGFloat
From class reference :
For layers you create and manage yourself, you must set the value of this property yourself based on the resolution of the screen and the content you are providing. Core Animation uses the value you specify as a cue to determine how to render your content.
So what you need to do is set the scale on the layer and you get the scale of the device from UIDevice class
circleLayer.scale = UIScreen.mainScreen().scale