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
Related
I'm building a drag and drop interaction for an iOS app. I want to enable the user to drag and drop images containing transparent parts.
However, the default preview for the dragged contents is a rectangle with an opaque white background that covers my app's background.
When I create a custom preview by implementing the UIDragInteractionDelegate method
dragInteraction(_:previewForLifting:session:), as in Apple's code sample Adopting Drag and Drop in a Custom View, the transparency of my source image is still not taken into account, meaning my preview image is still displayed in a rectangle with an opaque white background:
func dragInteraction(_ interaction: UIDragInteraction, previewForLifting item: UIDragItem, session: UIDragSession) -> UITargetedDragPreview? {
guard let image = item.localObject as? UIImage else { return nil }
// Scale the preview image view frame to the image's size.
let frame: CGRect
if image.size.width > image.size.height {
let multiplier = imageView.frame.width / image.size.width
frame = CGRect(x: 0, y: 0, width: imageView.frame.width, height: image.size.height * multiplier)
} else {
let multiplier = imageView.frame.height / image.size.height
frame = CGRect(x: 0, y: 0, width: image.size.width * multiplier, height: imageView.frame.height)
}
// Create a new view to display the image as a drag preview.
let previewImageView = UIImageView(image: image)
previewImageView.contentMode = .scaleAspectFit
previewImageView.frame = frame
/*
Provide a custom targeted drag preview that lifts from the center
of imageView. The center is calculated because it needs to be in
the coordinate system of imageView. Using imageView.center returns
a point that is in the coordinate system of imageView's superview,
which is not what is needed here.
*/
let center = CGPoint(x: imageView.bounds.midX, y: imageView.bounds.midY)
let target = UIDragPreviewTarget(container: imageView, center: center)
return UITargetedDragPreview(view: previewImageView, parameters: UIDragPreviewParameters(), target: target)
}
I tried to force the preview not to be opaque, but it did not help:
previewImageView.isOpaque = false
How can I get transparent parts in the lift preview?
Overwrite the backgroundColor in the UIDragPreviewParameters, as it defines the color for the background of a drag item preview.
Set it to UIColor.clear which is a color object whose grayscale and alpha values are both 0.0.
let previewParameters = UIDragPreviewParameters()
previewParameters.backgroundColor = UIColor.clear // transparent background
return UITargetedDragPreview(view: previewImageView,
parameters: previewParameters,
target: target)
You can define a UIBezierPath according to your image and set it to previewParameters.visiblePath
Example (Swift 4.2):
let previewParameters = UIDragPreviewParameters()
previewParameters.visiblePath = UIBezierPath(roundedRect: CGRect(x: yourX, y: yourY, width: yourWidth, height: yourHeight), cornerRadius: yourRadius)
//... Use the created previewParameters
https://www.dropbox.com/s/wlizis5zybsvnfz/File%202017-04-04%2C%201%2052%2024%20PM.jpeg?dl=0
Hello all Swifters,
Could anyone tell me how to set this kind of UI? Is there any half rounded image that they have set?
Or there are two images. One with the mountains in the background and anohter image made half rounded in white background and placed in on top?
Please advise
Draw an ellipse shape using UIBezier path.
Draw a rectangle path exactly similar to imageView which holds your image.
Transform the ellipse path with CGAffineTransform so that it will be in the center of the rect path.
Translate rect path with CGAffineTransform by 0.5 to create intersection between ellipse and the rect.
Mask the image using CAShapeLayer.
Additional: As Rob Mayoff stated in comments you'll probably need to calculate the mask size in viewDidLayoutSubviews. Don't forget to play with it, test different cases (different screen sizes, orientations) and adjust the implementation based on your needs.
Try the following code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
guard let image = imageView.image else {
return
}
let size = image.size
imageView.clipsToBounds = true
imageView.image = image
let curveRadius = size.width * 0.010 // Adjust curve of the image view here
let invertedRadius = 1.0 / curveRadius
let rect = CGRect(x: 0,
y: -40,
width: imageView.bounds.width + size.width * 2 * invertedRadius,
height: imageView.bounds.height)
let ellipsePath = UIBezierPath(ovalIn: rect)
let transform = CGAffineTransform(translationX: -size.width * invertedRadius, y: 0)
ellipsePath.apply(transform)
let rectanglePath = UIBezierPath(rect: imageView.bounds)
rectanglePath.apply(CGAffineTransform(translationX: 0, y: -size.height * 0.5))
ellipsePath.append(rectanglePath)
let maskShapeLayer = CAShapeLayer()
maskShapeLayer.frame = imageView.bounds
maskShapeLayer.path = ellipsePath.cgPath
imageView.layer.mask = maskShapeLayer
}
}
Result:
You can find an answer here:
https://stackoverflow.com/a/34983655/5479510
But generally, I wouldn't recommend using a white image overlay as it may appear distorted or pixelated on different devices. Using a masking UIView would do just great.
Why you could not just create (draw) rounded transparent image and add UIImageView() with UIImage() at the top of the view with required height and below this view add other views. I this this is the easiest way. I would write comment but I cant.
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:
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
}()
if a I have a path for a polyline saved as string,
From google maps sdk: path.encodedPath()
Or I have a series of latlngs points,
Can I draw a thumbnail for that path ?
I don't want to draw it on mapView, I wonder if I can draw it in any other view like imageView or any thing similar.
I created swift 3 code which you can just copy and paste in playground and see the results immediately
the code is here:
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
//All you need is to create a path with that points and create image or layer with that path
//To perpare for this let make some extensions with helper code
//Extension for UIBeziePath to easily create it from points
extension UIBezierPath
{
convenience init(points:[CGPoint])
{
self.init()
//connect every points by line.
//the first point is start point
for (index,aPoint) in points.enumerated()
{
if index == 0 {
self.move(to: aPoint)
}
else {
self.addLine(to: aPoint)
}
}
}
}
//to create image from path you can use this class function
extension UIImage
{
class func imageFrom(path:UIBezierPath,lineColor:UIColor,fillColor:UIColor)->UIImage
{
//create context to draw in use path bounds as context size. assume that path is inzide of rect with start corener at 0,0 coordinate
UIGraphicsBeginImageContextWithOptions(path.bounds.size, false, 0)
print("path bounds \(path.bounds) lineWidth:\(path.lineWidth)")
let context = UIGraphicsGetCurrentContext()
//set fill color
context?.setFillColor(fillColor.cgColor)
//set line coolor
context?.setStrokeColor(lineColor.cgColor)
context?.setLineWidth(path.lineWidth)
//draw a path
context?.addPath(path.cgPath)
context?.drawPath(using: .fillStroke)
//get image from context
let image = UIGraphicsGetImageFromCurrentImageContext()!
//finish context
UIGraphicsEndImageContext()
return image
}
}
//2. To create layer use this extension
extension CAShapeLayer
{
convenience init(path:UIBezierPath, lineColor:UIColor, fillColor:UIColor)
{
self.init()
self.path = path.cgPath
self.strokeColor = lineColor.cgColor
self.fillColor = fillColor.cgColor
self.lineWidth = path.lineWidth
self.opacity = 1
self.frame = path.bounds
}
}
//how to use:
//1. assume you recieved points
let points:[CGPoint] = [CGPoint(x: 0, y: 0),CGPoint(x: 150, y: 50),CGPoint(x: 75, y:140),CGPoint(x: 0, y: 80)]
//2. create path
let path = UIBezierPath(points: points)
//3. you can specify path line width
path.lineWidth = 2
//4. as a joinstyle too
path.lineJoinStyle = .round
//5. a)now you can create image from path with helper function
let image = UIImage.imageFrom(path: path, lineColor: UIColor.purple, fillColor: UIColor.red)
print(image)
//and set it to imageView
let imageView = UIImageView(image: image)
imageView.frame.origin = CGPoint(x: 200, y: 200)
imageView.backgroundColor = UIColor.green
//5. Maybe you will need to specify content mode for imageView
imageView.contentMode = .scaleAspectFit
//5 b.) Or you can create a Layer. Add add it to someone's layer layter
//if you need, you can apply transform to path - this is special way to
//adjust scale, rotation an lots of other cool stuff on layers, paths.
//Create special struct which descripbes transformation
//Identity is a special case which does not make any transformations at all
var transform = CGAffineTransform.identity
//scale it by 0.5 for x and 0.5 for y. if you need to increse scale by
//100 times, just pass 100 for x and y arguments
transform = transform.scaledBy(x: 0.5, y: 0.5)
//no apply transform to path.
path.apply(transform)
let layer = CAShapeLayer(path: path, lineColor: UIColor.blue, fillColor: UIColor.brown)
//6. let see results
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.backgroundColor = UIColor.white
//for imageView
container.addSubview(imageView)
//for CAShapeLayer
container.layer.addSublayer(layer)
//for playGround you can set this to see result there
//Do not forget to select from menu
//View -> Assistant Editor-> Show Assistance Editor
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
//Also I have to mention that the CAShapeLayer solution takes less memory which is critical for really big images
//but the UIImage is easier to use
the brown figure is layer with path scaled by 0.5, the red one is imageView
If you have a series of lat longs, you can know the maximum and minimum lat long, say they are : maxLat, maxLong, minLat, minLong. Please not that the both max values need not belong to the same coordinate. Same for both min values.
You can use this to get a rect :
let rect = CGRect(x: minLng , y: minLat, width: (maxLng - minLng), height: (maxLat - minLat))
Now, all other lat longs are points in this rectangle. You can get every coordinate's corresponding CGPoint value by
let point = CGPoint(x: maxLng - coordinate.longitude, y: maxLat - coordinate.latitude)
Using this rect and the series of CGPoints you created, you can draw a path (by starting with the first point and adding all subsequent points to it) on a view (like the image view you mention in your answer) or create a graphics context just to create a thumbnail of your path and save it as an image.
Refer the drawing and printing guide if you are unfamiliar with drawing in CGContexts.
However, if you mean to place this thumbnail equivalent of path on an image the real challenge is, how would you know the position. A simple trick would be to get map-equivalent min max coordinates of the image on which you mean to superimpose the path thumbnail and use these min max values to create the path and the context. Then you can center the thumbnail on the image.
Your project sounds interesting. Enjoy, and good luck.