Use UIImageView or UIView for drawing in swift - ios

I have a custom cell for my UITableView. If the user chose a picture for an item cell will show a picture:
if not, I will show first two characters of the item name:
I'm using following code from paint code to make rectangle:
public class CircleStyleKit : NSObject {
//// Drawing Methods
public dynamic class func drawCanvas1(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 38, height: 38), resizing: ResizingBehavior = .aspectFit, circleLable: String = "DR", circleSize: CGSize = CGSize(width: 38, height: 38), textSize: CGFloat = 17) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()!
//// Resize to Target Frame
context.saveGState()
let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 38, height: 38), target: targetFrame)
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 38, y: resizedFrame.height / 38)
} // It's too long
I showed part of code because it's too long.
Then I used a UIView to draw this rectangle:
import UIKit
class CirclyStyleView: UIView {
override func draw(_ rect: CGRect) {
CircleStyleKit.drawCanvas1()
}
}
}
There are two problems:
First, if I use UIImageView in my storyboard I just can draw an image
and I don't know if I can draw a rectangle with my code or not. However, I
checked it doesn't work.
Second if I use UIView, I can draw a rectangle, but if I want to draw an image I should use UIColor(patternImage: UIImage(named: "picName")!), but I can't change this image frame the way I did for image view. For example, make it circle as I've shown in the picture.
The question is, can I use UIImageView and is there any way to make my rectangle in UIImageView
Or can I use UIView and is there any way to set my custom image. I mean change image size and frame.

Use a single UIImageView and create a method on your model that returns a UIImage given it's circumstances. When an image must be drawn, draw into an "image context".
I'm not a swift author, so in almost-swift...
func imageForThisItem(item : AnyObject) -> UIImage {
// not really "AnyObject", use the object type in your data source array
// even better, make this a method on that model object
if (/* item has an image, like the sailboat */) {
return UIImage(named:"SailboatImage") // or however you got the sailboat image
} else {
// initialize "size" to be the same size as the sailboat images
UIGraphicsBeginImageContextWithOptions(size, false, 0)
// do your circle and text drawing here within the rect (0,0,size.width,size.height)
UIColor.greenColor().setFill()
let bezier = UIBezierPath(rect : rect)
bezier.fill()
let initials = item.initials() // however you get this
// even better, if this is a model method, then self.initials()
let attributes = // an attribute dictionary, see link below
initials.drawInRect(rect, withAttributes: attributes)
// or use CGOffsetRect to provide a little margin
var image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
Here's an example of text attributes in swift.
Another great reason to make this a method on your model is that you should probably cache the result of this method so you don't have to compute it each time. The lazy initializer pattern is perfect for this.
lazy var imageForThisItem: [UIImage] = {
// code as above, using "self" rather than item
}()

Related

Disable anti-aliasing in UIKit

I've got a pixel art game app that uses UIKit for its menus and SpriteKit for the gameplay scene. The pixels are getting blurred due to anti-aliasing.
With the sprites I can turn off the anti-aliasing using...
node.texture?.filteringMode = .nearest
but in UIKit I can't find a way to turn off the anti-aliasing in the UIImageView's.
I saw this post but there's no example and the answer wasn't accepted. Not sure how to turn it off using CGContextSetShouldAntialias, or where to call it.
Based on the example I found here, tried using this subclass but it didn't seem to make a difference; according to my breakpoints the method is never called:
class NonAliasingView: UIImageView {
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// fill background with black color
ctx.addRect(bounds)
ctx.setFillColor(UIColor.black.cgColor)
ctx.fillPath()
if let img = image {
let pixelSize = CGSize(width: img.size.width * layer.contentsScale, height: img.size.height * layer.contentsScale)
UIGraphicsBeginImageContextWithOptions(pixelSize, true, 1)
guard let imgCtx = UIGraphicsGetCurrentContext() else { return }
imgCtx.setShouldAntialias(false)
img.draw(in: CGRect(x: 0, y: 0, width: pixelSize.width, height: pixelSize.height))
guard let cgImg = imgCtx.makeImage() else { return }
ctx.scaleBy(x: 1, y: -1)
ctx.translateBy(x: 0, y: -bounds.height)
ctx.draw(cgImg, in: CGRect(x: (bounds.width - img.size.width) / 2, y: (bounds.height - img.size.height) / 2, width: img.size.width, height: img.size.height))
}
}
}
Here's the code from my view controller where I tried to implement the subclass (modeImage is an IBOutlet to a UIImageView):
// modeImage.image = gameMode.displayImage
let modeImg = NonAliasingView()
modeImg.image = gameMode.displayImage
modeImage = modeImg
If I try to use UIGraphicsGetCurrentContext in the view controller it is nil and never passes the guard statement.
I've confirmed view.layer.allowsEdgeAntialiasing defaults to false. I don't need anti-aliasing at all, so if there's a way to turn off anti-aliasing app wide, or in the whole view controller, I'd be happy to use it.
How do you disable anti-aliasing with a UIImageView in UIKit?
UPDATE
Added imgCtx.setShouldAntialias(false) to method but still not working.
To remove all antialiasing on your image view and just use nearest-neighbor filtering, set the magnificationFilter and minificationFilter of the image view's layer to CALayerContentsFilter.nearest, as in:
yourImageView.layer.magnificationFilter = .nearest
yourImageView.layer.minificationFilter = .nearest

Change the size of UIImageView in a UITableViewCell

I am a newbie in Swift And Xcode.
Is there any clean way to change UIImageView Size placed inside a UITableViewCell?
This is my TableView, I want to align left label correctly
Replace
cell.imageView!.image = image
with
cell.imageView?.image = image.scaleImage(toSize: CGSize(width: 40, height: 40))
Add this UIImageView extension to the project.
extension UIImage {
func scaleImage(toSize newSize: CGSize) -> UIImage? {
var newImage: UIImage?
let newRect = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height).integral
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
if let context = UIGraphicsGetCurrentContext(), let cgImage = self.cgImage {
context.interpolationQuality = .high
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: newSize.height)
context.concatenate(flipVertical)
context.draw(cgImage, in: newRect)
if let img = context.makeImage() {
newImage = UIImage(cgImage: img)
}
UIGraphicsEndImageContext()
}
return newImage
}
}
Reason for the error in your code is UITableViewCell is assigning the various size for UIImageView based on the image size inside it. In addition to I've adjusted the image to specific size so that it can fit to imageView content in UITableViewCell.
Note:
Please don't post the screenshots of the code as it does not help to others to copy it from the question and which attracts -ve voting as well. However, you can post the screenshots for XIBs, Storyboards and for Simulation errors.
You could use a stack view or a view with proportianal constraint to the relavite view in your cell xib. In this way the portion of the flag will be always the same.
Otherwise, if you want to do it quickly, you can contraint the leading of the label to the cell view and not with the flag. It's a litte bit dirtier but it should work.
Fix the width of the UIImageView and set its contentMode to .scaleAspectFit.
myImageView.safeAreaLayoutGuide.widthAnchor.constraint(equalToConstant: 50).isActive = true
myImageView.contentMode = .scaleAspectFit
myImageView.clipsToBounds = true
However, if you are using the storyboard, you can set these properties there itself.

How can I change the phase of UIColor patternImage in Swift?

I'm using the very convenient UIColor(patternImage:) to create some CAShapeLayers with tiled patterns in an iOS 10 app with Xcode 8.2. Tiling always starts at the origin of the view, which can be inconvenient if you want it to start somewhere else. To illustrate, here's a screenshot from the simulator (code below):
The CAShapeLayer on the left starts at (0,0), so everything is fine. The one on the right is at (110,50), so it's split in the middle. Here's the code:
let firstBox = CAShapeLayer()
firstBox.fillColor = UIColor(patternImage: UIImage(named: "test-image")!).cgColor
view.layer.addSublayer(firstBox)
firstBox.path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100)).cgPath
let secondBox = CAShapeLayer()
secondBox.fillColor = UIColor(patternImage: UIImage(named: "test-image")!).cgColor
view.layer.addSublayer(secondBox)
secondBox.path = UIBezierPath(rect: CGRect(x: 110, y: 50, width: 100, height: 100)).cgPath
I want to adjust the phase of the pattern for the right CAShapeLayer so that both tiles show a full face. Apple's documentation for UIColor(patternImage:) helpfully refers to a function for this purpose:
To change the phase, make the color the current color and then use the
setPatternPhase(_:) function to change the phase.
Sounds simple! But I'm having a hard time implementing it. I'm not really sure what "make the color the current color" means. I tried getting the current context and calling setPatternPhase on it, both before and after assigning the fill color to the layer:
UIGraphicsGetCurrentContext()?.setPatternPhase(CGSize(width: 25, height: 25))
No noticeable effect. I tried subclassing the containing UIView and setting the phase in its drawRect: method, as suggested in this answer. But drawRect: doesn't exist in Swift, so I tried both draw(_ rect:) and draw(_ layer:, in:). Both functions get called, but there's no noticeable effect.
class PatternView: UIView {
override func draw(_ rect: CGRect) {
UIGraphicsGetCurrentContext()?.setPatternPhase(CGSize(width: 25, height: 25))
super.draw(rect)
}
override func draw(_ layer: CALayer, in ctx: CGContext) {
ctx.setPatternPhase(CGSize(width: 25, height: 25))
super.draw(layer, in: ctx)
}
}
At Dave Weston's suggestion, I used UIImage's .set() to set the current stroke and fill for the current context before calling setPatternPhase. Unfortunately the output is unaffected. Here's the code I tried:
let secondBoxColor = UIColor(patternImage: UIImage(named: "test-image")!)
secondBoxColor.set()
UIGraphicsGetCurrentContext()?.setPatternPhase(CGSize(width: 50, height: 50))
let secondBox = CAShapeLayer()
secondBox.fillColor = secondBoxColor.cgColor
view.layer.addSublayer(secondBox)
secondBox.path = UIBezierPath(rect: CGRect(x: 110, y: 50, width: 100, height: 100)).cgPath
How can I shift the phase of the pattern that gets drawn into a CAShapeLayer?
To make your pattern the current color, you should call the set() instance method on the UIColor instance that contains your pattern. This configures the color as the current stroke and fill color for the current context.
Then, according to Apple's docs, setPatternPhase should work.
I haven't been able to solve this problem, but I thought I'd share the workaround I'm using in case it's useful to anyone.
As far as I can tell, CAShapeLayer does its rendering in a secret, hidden place, and ignores the normal display() and draw() functions that CALayerDelegates are supposed to use. As a result, you never have access to the CGContext it's using to render, so there's no way to call setPatternPhase().
My specific use case for setPatternPhase() was to have the pattern line up with the top-left of the CAShapeLayer it's drawn in, so I found an alternate way to do that. It does not allow you to set an arbitrary phase.
What I did instead is create a new CALayer subclass called CAPatternLayerthat takes a UIImage to tile and a CGPath to fill. It delegates to a CALayerDelegate class called CAPatternLayerDelegate, which provides a draw(layer: in ctx:) function. When a draw is requested, the delegate creates a temporary UIImageView, fills it with the tiled image, then renders it to the CALayer's context.
A neat side-effect of this is that you can use a UIImage with cap insets, which allows 9-slice scaling with the center slice tiled.
Here's the code for PatternLayer and PatternLayerDelegate:
class CAPatternLayer: CALayer {
var image: UIImage?
var path: CGPath? {
didSet {
if let path = self.path {
self.frame = path.boundingBoxOfPath
// shift the path to 0,0 since you built position into the frame
var translation = CGAffineTransform(translationX: -path.boundingBoxOfPath.origin.x, y: -path.boundingBoxOfPath.origin.y)
let shiftedPath = path.copy(using: &translation)
// use the shifted version
self.path = shiftedPath
self.maskLayer.path = shiftedPath
}
}
}
let maskLayer: CAShapeLayer = CAShapeLayer()
override init() {
super.init()
self.delegate = CAPatternLayerDelegate.sharedInstance
self.setNeedsDisplay()
self.mask = self.maskLayer
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
convenience init(path: CGPath, image: UIImage) {
self.init()
defer {
self.image = image
self.path = path
}
}
}
class CAPatternLayerDelegate: NSObject, CALayerDelegate {
static let sharedInstance = CAPatternLayerDelegate()
func draw(_ layer: CALayer, in ctx: CGContext) {
// cast layer to a CAPatternLayer so you can access properties
if let layer = layer as? CAPatternLayer, let image = layer.image, let path = layer.path {
// create a UIImageView to display the image, then render it to the context
let imageView = UIImageView()
// if a path bounding box was set, use it, otherwise draw over the whole layer
imageView.bounds = path.boundingBoxOfPath
imageView.image = image
imageView.layer.render(in: ctx)
}
}
}
And here's an example in use:
// create a path to fill
let myMaskPath = UIBezierPath(rect: CGRect(x: 50, y: 25, width: 200, height: 100))
// pull the image and set it up as a resizeableImage with cap insets
let patternImage = UIImage(named: "ground")!.resizableImage(withCapInsets: .init(top: 16, left: 16, bottom: 0, right: 16), resizingMode: .tile)
// create the CAPatternLayer and add it to the view
let myPatternLayer = CAPatternLayer(path: myMaskPath.cgPath, image: patternImage)
view.layer.addSublayer(myPatternLayer)
Output:

How to use a color as a placeholder in UIImage

How can I use color as a placeholder in UIImage? For example I have an image like this:
And I want to change red to other image and receive something like this:
I thought about checking color of pixels and checking whether it's red and finding a frame, but maybe you know any better methods?
You need to take two View, one is UIView with a backgroundcolor of green(let us say), on that UIView you need to put your UIImageView, and you need to set the constraint according to that. Now set the background color of your UIImageView as red, and as per your requirement, check if the UIImageView's image is nil, if yes then you can add an image to your UIImageView.
Here is a function that will create a UIImage from the specified color. You can call this method with .red to generate the placeholder image you want.
static func image(fromColor color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
let renderer = UIGraphicsImageRenderer(bounds: rect)
let img = renderer.image { ctx in
ctx.cgContext.setFillColor(color.cgColor)
ctx.cgContext.fill(rect)
}
return img
}

How To Create in Swift a Circular Profile Picture or Rounded Corner Image with a border which does not leak?

Basing on the source code below:
#IBOutlet var myUIImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
self.makingRoundedImageProfileWithRoundedBorder()
}
private func makingRoundedImageProfileWithRoundedBorder() {
// Making a circular image profile.
// self.myUIImageView.layer.cornerRadius = self.myUIImageView.frame.size.width / 2
// Making a rounded image profile.
self.myUIImageView.layer.cornerRadius = 20.0
self.myUIImageView.clipsToBounds = true
// Adding a border to the image profile
self.myUIImageView.layer.borderWidth = 10.0
self.myUIImageView.layer.borderColor = UIColor.whiteColor().CGColor
}
Indeed I am able to render a circular or rounded UIImageView, but the problem is that if we add the border, the image leaks a bit. It's way worse with a circular UIImageView, it leaks whenever the border is bent, so LEAKS EVERYWHERE! You can find a screenshot of the result below:
Any way to fix that in Swift? Any sample code which answers to this question will be highly appreciated.
Note: as far as possible the solution has to be compatible with iOS 7 and 8+.
First Solution
Basing on the #Jesper Schläger suggestion
"If I may suggest a quick and dirty solution:
Instead of adding a border to the image view, you could just add another white view below the image view. Make the view extend 10 points in either direction and give it a corner radius of 20.0. Give the image view a corner radius of 10.0."
Please find the Swift implementation below:
import UIKit
class ViewController: UIViewController {
#IBOutlet var myUIImageView: UIImageView!
#IBOutlet var myUIViewBackground: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Making a circular UIView: cornerRadius = self.myUIImageView.frame.size.width / 2
// Making a rounded UIView: cornerRadius = 10.0
self.roundingUIView(self.myUIImageView, cornerRadiusParam: 10)
self.roundingUIView(self.myUIViewBackground, cornerRadiusParam: 20)
}
private func roundingUIView(let aView: UIView!, let cornerRadiusParam: CGFloat!) {
aView.clipsToBounds = true
aView.layer.cornerRadius = cornerRadiusParam
}
}
Second Solution
Would be to set a circle mask over a CALayer.
Please find the Objective-C implementation of this second solution below:
CALayer *maskedLayer = [CALayer layer];
[maskedLayer setFrame:CGRectMake(50, 50, 100, 100)];
[maskedLayer setBackgroundColor:[UIColor blackColor].CGColor];
UIBezierPath *maskingPath = [UIBezierPath bezierPath];
[maskingPath addArcWithCenter:maskedLayer.position
radius:40
startAngle:0
endAngle:360
clockwise:TRUE];
CAShapeLayer *maskingLayer = [CAShapeLayer layer];
[maskingLayer setPath:maskingPath.CGPath];
[maskedLayer setMask:maskingLayer];
[self.view.layer addSublayer:maskedLayer];
If you comment out from line UIBezierPath *maskingPath = [UIBezierPath bezierPath]; through [maskedLayer setMask:maskingLayer]; you will see that the layer is a square. However when these lines are not commented the layer is a circle.
Note: I neither tested this second solution nor provided the Swift implementation, so feel free to test it and let me know if it works or not through the comment section below. Also feel free to edit this post adding the Swift implementation of this second solution.
If I may suggest a quick and dirty solution:
Instead of adding a border to the image view, you could just add another white view below the image view. Make the view extend 10 points in either direction and give it a corner radius of 20.0. Give the image view a corner radius of 10.0.
I worked on improving the code but it keeps crashing. I'll work on it, but I appear to have got a (rough) version working:
Edit Updated with a slightly nicer version. I don't like the init:coder method but maybe that can factored out/improved
class RoundedImageView: UIView {
var image: UIImage? {
didSet {
if let image = image {
self.frame = CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
}
}
var cornerRadius: CGFloat?
private class func frameForImage(image: UIImage) -> CGRect {
return CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
override func drawRect(rect: CGRect) {
if let image = self.image {
image.drawInRect(rect)
let cornerRadius = self.cornerRadius ?? rect.size.width/10
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
UIColor.whiteColor().setStroke()
path.lineWidth = cornerRadius
path.stroke()
}
}
}
let image = UIImage(named: "big-teddy-bear.jpg")
let imageView = RoundedImageView()
imageView.image = image
Let me know if that's the sort of thing you're looking for.
A little explanation:
As I'm sure you've found, the "border" that iOS can apply isn't perfect, and shows the corners for some reason. I found a few other solutions but none seemed to work. The reason this is a subclass of UIView, and not UIImageView, is that drawRect: is not called for subclasses of UIImageView. I am not sure about the performance of this code, but it seems good from my (limited) testing
Original code:
class RoundedImageView: UIView {
var image: UIImage? {
didSet {
if let image = image {
self.frame = CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
}
}
private class func frameForImage(image: UIImage) -> CGRect {
return CGRect(x: 0, y: 0, width: image.size.width/image.scale, height: image.size.width/image.scale)
}
override func drawRect(rect: CGRect) {
if let image = self.image {
self.image?.drawInRect(rect)
let path = UIBezierPath(roundedRect: rect, cornerRadius: 50)
UIColor.whiteColor().setStroke()
path.lineWidth = 10
path.stroke()
}
}
}
let image = UIImage(named: "big-teddy-bear.jpg")
let imageView = RoundedImageView()
imageView.image = image
imageView.layer.cornerRadius = 50
imageView.clipsToBounds = true

Resources