Swift: Overlay NxN grid of buttons of aspect fit image - ios

Tried searching for a while but couldn't find a way to do this.
I currently have a uiimage that is aspect fit. I'm trying to overlay a NxN grid of buttons over the image.
My thought process was to find the boundaries of the image in the imageview, and then overlay the buttons based off those boundaries. I usually use storyboards but am taking a stab at this programmatically as I couldn't figure out the aspect fit part in storyboards.
I found this code for finding the boundaries of my image after its been fit and did some print statements to be sure that it is finding the correct boundaries which I believe it is.
extension UIImageView {
var contentClippingRect: CGRect {
guard let image = image else { return bounds }
guard contentMode == .scaleAspectFit else { return bounds }
guard image.size.width > 0 && image.size.height > 0 else { return bounds }
let scale: CGFloat
if image.size.width > image.size.height {
scale = bounds.width / image.size.width
} else {
scale = bounds.height / image.size.height
}
let size = CGSize(width: image.size.width * scale, height: image.size.height * scale)
let x = (bounds.width - size.width) / 2.0
let y = (bounds.height - size.height) / 2.0
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
}
I'm currently trying to initialize the stackview by passing the image.contentRectClipping frame to it. My understanding would be that this would create a frame based off the x,y,width and height and I would be good to go. However it places the stackview in the top left of the imageview and not the actual image. Any guidance would be greatly appreciated.

Related

How to get x and y position of UIImage in UIImageView?

I want to get original x and y position of UIImage when we set it in UIImageView with scaleAspectFill.
As we know in scaleAspectFill, some of the portion is clipped. So as per my requirement I want to get x and y value (it may be - value I don't know.).
Here is the original image from gallery
Now I am setting this above image to my app view.
So as above situation, I want to get it's hidden x, y position of image which are clipped.
Can any one tell how to get it?
Use following extension
extension UIImageView {
var imageRect: CGRect {
guard let imageSize = self.image?.size else { return self.frame }
let scale = UIScreen.main.scale
let imageWidth = (imageSize.width / scale).rounded()
let frameWidth = self.frame.width.rounded()
let imageHeight = (imageSize.height / scale).rounded()
let frameHeight = self.frame.height.rounded()
let ratio = max(frameWidth / imageWidth, frameHeight / imageHeight)
let newSize = CGSize(width: imageWidth * ratio, height: imageHeight * ratio)
let newOrigin = CGPoint(x: self.center.x - (newSize.width / 2), y: self.center.y - (newSize.height / 2))
return CGRect(origin: newOrigin, size: newSize)
}
}
Usage
let rect = imageView.imageRect
print(rect)
UI Test
let testView = UIView(frame: rect)
testView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
imageView.superview?.addSubview(testView)
Use below extension to find out accurate details of Image in ImageView.
extension UIImageView {
var contentRect: CGRect {
guard let image = image else { return bounds }
guard contentMode == .scaleAspectFit else { return bounds }
guard image.size.width > 0 && image.size.height > 0 else { return bounds }
let scale: CGFloat
if image.size.width > image.size.height {
scale = bounds.width / image.size.width
} else {
scale = bounds.height / image.size.height
}
let size = CGSize(width: image.size.width * scale, height: image.size.height * scale)
let x = (bounds.width - size.width) / 2.0
let y = (bounds.height - size.height) / 2.0
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
}
How to test
let rect = imgTest.contentRect
print("Image rect:", rect)
Reference: https://www.hackingwithswift.com/example-code/uikit/how-to-find-an-aspect-fit-images-size-inside-an-image-view
If you want to show image like it shows in gallery then you can use contraints
"H:|[v0]|" and "V:|[v0]|" and in imageview use .aspectFit
And if you want the image size you can use imageView.image!.size and calculate the amount of image which is getting cut. In aspectFill the width is matched to screenwidth and accordingly the height gets increased. So I guess you can find how how much amount of image is getting cut.
Try this Library ImageCoordinateSpace
I am not sure if it works for you or not, but it has a feature to convert CGPoint from image coordinates to any view coordinates and vice versa.

How to position UI elements on background image relative to the image

I had I previuosly question here How to make full screen background image inside ScrollView and keep aspect ratio and get very good aswer from "ekscrypto", thank you again.
I have buttons with text on the image (1, 2, 3, 4, 5 etc.) previously i used hard coded X and Y coordinates to position UI elemnts (buttons) on the background image, this solution worked on all iPhone devices, not on iPads.
I changed my code according to the solution I received from "ekscrypto", and now of course this solution not work on any device.
On the image there is a road, and I need to arrange these buttons on this road. How can I properly position this buttons relative to the image, regardless of the device and image scale?
P.S. Ekscrypto also provided solution for the UI element positioning, but I don't understand how it works.
Here's how I currently attempt to create the buttons:
let imageOne = UIImage(named: "level1") as UIImage?
let levelOne = UIButton(type: UIButtonType.system)
levelOne.frame = CGRect.init(x: 10, y: 10, width: 100, height: 45)
levelOne.setImage(imageOne, for: .normal)
scrollView.addSubview(levelOne)
But the iPhone and iPad button positions and sizes should be different. How can I have them placed properly relative to the image?
Thank you so much ekscrypto, and sorry for delay with answer. Your code is working, and solves my problem, but there is a small problems.
Had to change this line let button = UIButton(type: .system) to .custom, or instead of background image you get button that is filled with blue color.
Button with background image is too big, specially on iPhone 5, changed let backgroundDesignHeight: CGFloat = 330.0 to 730 to make it smaller
All buttons are in same place on iPhone and iPads, except «plus devices» there is a small offset to the bottom(down) button should be slightly higher
On some devices background image on button are little bit blurry, this happened after I changed backgroundDesignHeight to 730
You can solve this a few ways:
Compute the final x/y position manually based on current screen dimension and "design" dimension
Manually create aspect-ratio based constraints and let iOS compute the final position for you
Assuming that your device/app orientation is constant (always landscape or always portrait) and is always fullscreen, it may be easier to compute it by hand.
First, you will need a helper function to resize your image:
private func scaledImage(named imageName: String, scale: CGFloat) -> UIImage? {
guard let image = UIImage(named: imageName) else { return nil }
let targetSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
return resizeImage(image: image, targetSize: targetSize)
}
// Adapted from https://stackoverflow.com/questions/31314412/how-to-resize-image-in-swift
private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage? {
let size = image.size
let widthRatio = targetSize.width / size.width
let heightRatio = targetSize.height / size.height
// Figure out what our orientation is, and use that to form the rectangle
var newSize: CGSize
if(widthRatio > heightRatio) {
newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
} else {
newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
}
// This is the rect that we've calculated out and this is what is actually used below
let rect = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)
// Actually do the resizing to the rect using the ImageContext stuff
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
image.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
Next, we will create a function to compute the location of the button, and the size of the button based on the image size:
private func positionScaledButton(x designX: CGFloat, y designY: CGFloat, imageName: String) -> UIButton {
// First, compute our "designScale"
let screenHeight = UIScreen.main.bounds.size.height
let backgroundDesignHeight: CGFloat = 330.0 // ** see below
let designScale = screenHeight / backgroundDesignHeight
// Create button
let button = UIButton(type: .custom)
// Get image to use, and scale it as required
guard let image = scaledImage(named: imageName, scale: designScale) else { return button }
button.frame = CGRect(x: designX * designScale, y: designY * designScale, width: image.size.width, height: image.size.height)
button.setImage(image, for: .normal)
scrollView.addSubview(button)
return button
}
** The value "330.0" in the code above refers to the height of the background image in the scroller, from which the x/y coordinates of the button were measured.
Assuming button's top-left corner should be at x: 10, y: 10, for image "levelOne":
let levelOneButton = positionScaledButton(x: 10, y: 10, imageName: "imageOne")
// To do: addTarget event handler
Use relative, rather than absolute points.
So, rather than saying button 1 is at (30, 150) in points, use the fraction of the screen size instead so it's at (0.0369, 0.4) - then use those fractions to create your auto layout constraints.

How to resize a UIImage without antialiasing?

I am developing an iOS board game. I am trying to give the board a kind of "texture".
What I did was I created this very small image (really small, be sure to look carefully):
And I passed this image to the UIColor.init(patternImage:) initializer to create a UIColor that is this image. I used this UIColor to fill some square UIBezierPaths, and the result looks like this:
All copies of that image lines up perfectly and they form many diagonal straight lines. So far so good.
Now on the iPad, the squares that I draw will be larger, and the borders of those squares will be larger too. I have successfully calculated what the stroke width and size of the squares should be, so that is not a problem.
However, since the squares are larger on an iPad, there will be more diagonal lines per square. I do not want that. I need to resize the very small image to a bigger one, and that the size depends on the stroke width of the squares. Specifically, the width of the resized image should be twice as much as the stroke width.
I wrote this extension to resize the image, adapted from this post:
extension UIImage {
func resized(toWidth newWidth: CGFloat) -> UIImage {
let scale = newWidth / size.width
let newHeight = size.height * scale
UIGraphicsBeginImageContextWithOptions(CGSize(width: newWidth, height: newHeight), false, 0)
self.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}
And called it like this:
// this is the code I used to draw a single square
let path = UIBezierPath(rect: CGRect(origin: point(for: Position(x, y)), size: CGSize(width: squareLength, height: squareLength)))
UIColor.black.setStroke()
path.lineWidth = strokeWidth
// this is the line that's important!
UIColor(patternImage: #imageLiteral(resourceName:
"texture").resized(toWidth: strokeWidth * 2)).setFill()
path.fill()
path.stroke()
Now the game board looks like this on an iPhone:
You might need to zoom in the webpage a bit to see what I mean. The board now looks extremely ugly. You can see the "borders" of each copy of the image. I don't want this. On an iPad though, the board looks fine. I suspect that this only happens when I downsize the image.
I figured that this might be due to the antialiasing that happens when I use the extension. I found this post and this post about removing antialiasing, but the former seems to be doing this in a image view while I am doing this in the draw(_:) method of my custom GameBoardView. The latter's solution seems to be exactly the same as what I am using.
How can I resize without antialiasing? Or on a higher level of abstraction, How can I make my board look pretty?
class Ruled: UIView {
override func draw(_ rect: CGRect) {
let T: CGFloat = 15 // desired thickness of lines
let G: CGFloat = 30 // desired gap between lines
let W = rect.size.width
let H = rect.size.height
guard let c = UIGraphicsGetCurrentContext() else { return }
c.setStrokeColor(UIColor.orange.cgColor)
c.setLineWidth(T)
var p = -(W > H ? W : H) - T
while p <= W {
c.move( to: CGPoint(x: p-T, y: -T) )
c.addLine( to: CGPoint(x: p+T+H, y: T+H) )
c.strokePath()
p += G + T + T
}
}
}
Enjoy.
Note that you would, obviously, clip that view.
If you want to have a number of them on the screen or in a pattern, just do that.
To clip to a given rectangle:
The class above simply draws it the "size of the UIView".
However, often, you want to draw a number of the "boxes" actually within the view, at different coordinates. (A good example is for a calendar).
Furthermore, this example explicitly draws "both stripes" rather than drawing one stripe over the background color:
func simpleStripes(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) {
let stripeWidth: CGFloat = 20.0 // whatever you want
let m = stripeWidth / 2.0
guard let c = UIGraphicsGetCurrentContext() else { return }
c.setLineWidth(stripeWidth)
let r = CGRect(x: x, y: y, width: width, height: height)
let longerSide = width > height ? width : height
c.saveGState()
c.clip(to: r)
var p = x - longerSide
while p <= x + width {
c.setStrokeColor(pale blue)
c.move( to: CGPoint(x: p-m, y: y-m) )
c.addLine( to: CGPoint(x: p+m+height, y: y+m+height) )
c.strokePath()
p += stripeWidth
c.setStrokeColor(pale gray)
c.move( to: CGPoint(x: p-m, y: y-m) )
c.addLine( to: CGPoint(x: p+m+height, y: y+m+height) )
c.strokePath()
p += stripeWidth
}
c.restoreGState()
}
extension UIImage {
func ResizeImage(targetSize: CGSize) -> UIImage
{
let size = self.size
let widthRatio = targetSize.width / self.size.width
let heightRatio = targetSize.height / self.size.height
// Figure out what our orientation is, and use that to form the rectangle
var newSize: CGSize
if(widthRatio > heightRatio) {
newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
} else {
newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
}
// This is the rect that we've calculated out and this is what is actually used below
let rect = CGRect(x: 0, y: 0, width: newSize.width,height: newSize.height)
// Actually do the resizing to the rect using the ImageContext stuff
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
self.draw(in: rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
}

Get the position of imageview dynamically

I need to get the dynamic x,y position of the image view.
I have tried this
self.imgView.bounds.origin.y
and
self.imgView.frame.origin.x
I am getting 0.0 all the time.
How can i get this?
Thanks
The bounds.origin of any view should be (0,0) at all times since it is the coordinate system of the view itself (relative to its origin)
The frame.origin is in the coordinate system of the its superview, so if it's 0,0, then that might be that its origin is at its superview's origin.
Maybe you want to know the origin with respect to the screen? If so,
let posInWindow = v.convert(v.bounds.origin, to: nil)
Please use below code to get rect in UIImageVIew
This code in swift3 And your ImageViewContentMode should be Aspect fit
func calculateRectOfImageInImageView(imageView: UIImageView) -> CGRect {
let imageViewSize = imageView.frame.size
let imgSize = imageView.image?.size
guard let imageSize = imgSize, imgSize != nil else {
return CGRect.zero
}
let scaleWidth = imageViewSize.width / imageSize.width
let scaleHeight = imageViewSize.height / imageSize.height
let aspect = fmin(scaleWidth, scaleHeight)
var imageRect = CGRect(x: 0, y: 0, width: imageSize.width * aspect, height: imageSize.height * aspect)
// Center image
imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2
imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2
// Add imageView offset
imageRect.origin.x += imageView.frame.origin.x
imageRect.origin.y += imageView.frame.origin.y
return imageRect
}
Bounds will always return an x and y of 0, you want to use frame. In your example these values may be correct? Try adding your imageView in the centre of the view and logging the:
self.imgView.frame.origin.x
self.imgView.frame.origin.y

Image doesn't fit to screen

I have an image on screen:
image = SKSpriteNode(imageNamed: "image.png")
image.name = "leftside1"
image.position = CGPoint(x: 0, y: 0)
image.zPosition = 3
self.addChild(image)
Image has w250xh1920 - when I show screen, image not fit to screen, its bigger up and down, but I have set aspectFit in previous scene:
nextScene!.scaleMode = .aspectFit
Try aspectFill doesn't solve my problem. Any advice? Thanks!
Try setting the image size to be the size of the scene like this.
if let size = nextScene?.size {
image.size = size
}
Here is an example of how you can dynamically calculate the width.
if let size = nextScene?.size {
// Calculate ratio
let ratio = size.width / size.height
// Multiple image width by ratio
let width = image.size.width * ratio
// Create a new CGSize
let newSize = CGSize(width: width, height: size.height)
image.size = newSize
}

Resources