I am trying to set a collectionView with round images I get using KingFisher (image caching library).
I am not sure what I should set with round corner, the imageView or the cell itself. Even if the cell is round the image seems to fill the square.
So far I am using this code (If I dont set it after I change the image its square):
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
...
if let path = user.profilePictureURL {
if let url = URL(string: path) {
cell.profilePictureImageView.kf.setImage(with: url)
cell.profilePictureImageView.layer.cornerRadius = cell.profilePictureImageView.frame.width/2.0
}
}
And the ImageView :
class ProfilePicture : UIImageView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
func commonInit(){
self.layer.masksToBounds = false
self.layer.cornerRadius = self.layer.frame.width/2.0
self.clipsToBounds = true
}
}
But the first time it downloads the images , they are like this (cell background is blue)
You can use like this:
extension UIImageView {
func setRounded() {
let radius = CGRectGetWidth(self.frame) / 2
self.layer.cornerRadius = radius
self.layer.masksToBounds = true
}
}
then call the method as below:
imageView.setRounded()
You can get a circle by adjusting height (in portrait) instead of width:
self.layer.cornerRadius = self.layer.frame.height/2.0
you'll have to adjust your contentMode.scaleAspect if you are unhappy with the results. By adjusting on the long edge you can get a circle but you won't see the entire image.
You can get a circle in the image with a extension of UIView
extension UIImage {
var circleMask: UIImage {
let square = size.width < size.height ? CGSize(width: size.width, height: size.width) : CGSize(width: size.height, height: size.height)
let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
imageView.contentMode = UIView.ContentMode.scaleAspectFill
imageView.image = self
imageView.layer.cornerRadius = square.width/2
//imageView.layer.borderColor = UIColor.white.cgColor
//imageView.layer.borderWidth = 5
imageView.layer.masksToBounds = true
UIGraphicsBeginImageContext(imageView.bounds.size)
imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result!
}}
Related
I'm trying to use a PDF vector image as the contents of a CALayer, but when it is scaled above it's initial size of 15x13, it looks very blurry. I have 'Preserve Vector Data' turned on in my asset catalog for the image in question. Here is the code for my view, which draws an outer circle on one layer, and uses a second layer to display an image of a checkmark in the center of the view if the isComplete property is set to true.
#IBDesignable
public class GoalCheckView: UIView {
// MARK: - Public properties
#IBInspectable public var isComplete: Bool = false {
didSet {
setNeedsLayout()
}
}
// MARK: - Private properties
private lazy var checkImage: UIImage? = {
let bundle = Bundle(for: type(of: self))
return UIImage(named: "check_event_carblog_confirm", in: bundle, compatibleWith: nil)
}()
private var checkImageSize: CGSize {
let widthRatio: CGFloat = 15 / 24 // Size of image is 15x13 when circle is 24x24
let heightRatio: CGFloat = 13 / 24
return CGSize(width: bounds.width * widthRatio, height: bounds.height * heightRatio)
}
private let circleLayer = CAShapeLayer()
private let checkLayer = CALayer()
private let lineWidth: CGFloat = 1
// MARK: - View lifecycle
public override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public override func layoutSubviews() {
super.layoutSubviews()
// Layout circle
let path = UIBezierPath(ovalIn: bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2))
circleLayer.path = path.cgPath
// Layout check
checkLayer.frame = CGRect(
origin: CGPoint(x: bounds.midX - checkImageSize.width / 2, y: bounds.midY - checkImageSize.height / 2),
size: checkImageSize
)
checkLayer.opacity = isComplete ? 1 : 0
}
// MARK: - Private methods
private func setupView() {
// Setup circle layer
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = nil
circleLayer.strokeColor = UIColor(named: "goal_empty", in: bundle, compatibleWith: nil)?.cgColor
layer.addSublayer(circleLayer)
// Setup check layer
checkLayer.contentsScale = UIScreen.main.scale
checkLayer.contentsGravity = .resizeAspect
checkLayer.contents = checkImage?.cgImage
layer.addSublayer(checkLayer)
}
}
This code results in the following display if I set the size of the view to 240x240:
I was able to create a workaround for this. I can check the expected size of my image in layoutSubviews, and if it does not match the size of the UIImage I can use a UIGraphicsImageRenderer to create a new image that is scaled to the correct size. I created an extension of UIImage to facilitate this:
extension UIImage {
internal func imageScaled(toSize scaledSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: scaledSize)
let newImage = renderer.image { [unowned self] _ in
self.draw(in: CGRect(origin: .zero, size: scaledSize))
}
return newImage
}
}
Now, my updated layoutSubviews method looks like this:
public override func layoutSubviews() {
super.layoutSubviews()
// Layout circle
let path = UIBezierPath(ovalIn: bounds.insetBy(dx: lineWidth.mid, dy: lineWidth.mid))
circleLayer.path = path.cgPath
// Layout check
if let checkImage = checkImage, checkImage.size != checkImageSize {
checkLayer.contents = checkImage.imageScaled(toSize: checkImageSize).cgImage
}
let checkOrigin = CGPoint(x: bounds.midX - checkImageSize.midW, y: bounds.midY - checkImageSize.midH)
checkLayer.frame = CGRect(origin: checkOrigin, size: checkImageSize)
}
This results in a nice crisp image:
I'm building an iOS application in which I have to implement a custom UISlider. The problem is that the built-in UISlider doesn't support gradient track. Another issue is my UI style guide shows that the current tracking value rectangle should be a gradient of two colors as shown in the image
How can I build a customized version of the UISlider? I have thought of either subclassing the existing one or by building a UIControl subclass.
I'm using xcode 9.4 and swift 4.2
Thanks in advance!
I ended up solving the problem by setting the gradient layer as an image for the slider minimum track image as following:
#IBDesignable
class GradientSlider: UISlider {
#IBInspectable var thickness: CGFloat = 20 {
didSet {
setup()
}
}
#IBInspectable var sliderThumbImage: UIImage? {
didSet {
setup()
}
}
func setup() {
let minTrackStartColor = Palette.SelectiveYellow
let minTrackEndColor = Palette.BitterLemon
let maxTrackColor = Palette.Firefly
do {
self.setMinimumTrackImage(try self.gradientImage(
size: self.trackRect(forBounds: self.bounds).size,
colorSet: [minTrackStartColor.cgColor, minTrackEndColor.cgColor]),
for: .normal)
self.setMaximumTrackImage(try self.gradientImage(
size: self.trackRect(forBounds: self.bounds).size,
colorSet: [maxTrackColor.cgColor, maxTrackColor.cgColor]),
for: .normal)
self.setThumbImage(sliderThumbImage, for: .normal)
} catch {
self.minimumTrackTintColor = minTrackStartColor
self.maximumTrackTintColor = maxTrackColor
}
}
func gradientImage(size: CGSize, colorSet: [CGColor]) throws -> UIImage? {
let tgl = CAGradientLayer()
tgl.frame = CGRect.init(x:0, y:0, width:size.width, height: size.height)
tgl.cornerRadius = tgl.frame.height / 2
tgl.masksToBounds = false
tgl.colors = colorSet
tgl.startPoint = CGPoint.init(x:0.0, y:0.5)
tgl.endPoint = CGPoint.init(x:1.0, y:0.5)
UIGraphicsBeginImageContextWithOptions(size, tgl.isOpaque, 0.0);
guard let context = UIGraphicsGetCurrentContext() else { return nil }
tgl.render(in: context)
let image =
UIGraphicsGetImageFromCurrentImageContext()?.resizableImage(withCapInsets:
UIEdgeInsets.init(top: 0, left: size.height, bottom: 0, right: size.height))
UIGraphicsEndImageContext()
return image!
}
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(
x: bounds.origin.x,
y: bounds.origin.y,
width: bounds.width,
height: thickness
)
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
}
I have an app where users can place shapes onto an image. The image is in a UIImageView set to AspectFit. The UIImageView is in a UIScrollView to allow for zooming and panning. The shapes are UIViews added to the UIImageView. Everything is working fine except when the Device Orientation changes. Since the image is set to AspectFit and the orientation changes so does the aspect ratio. Therefore, my shapes don't maintain the same position on the image.
Before rotation:
After rotation:
ViewController Code:
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = markupUIImage;
scrollView.maximumZoomScale=5;
scrollView.minimumZoomScale=0.5;
scrollView.bounces=true;
scrollView.bouncesZoom=true;
scrollView.contentSize=CGSize(width: imageView.frame.size.width, height: imageView.frame.size.height);
scrollView.showsHorizontalScrollIndicator=true;
scrollView.showsVerticalScrollIndicator=true;
scrollView.delegate=self;
}
func viewForZooming(in scrollView: UIScrollView) -> UIView?
{
return self.imageView
}
Class for Shapes:
class ShapeUIView: UIView {
override public class var layerClass: Swift.AnyClass {
get {
return CAShapeLayer.self;
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeView()
}
private func initializeView(){
let shapeLayer = (layer as! CAShapeLayer);
shapeLayer.opacity = 1.0
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineWidth = 2.0
shapeLayer.strokeColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).cgColor
shapeLayer.fillColor = UIColor(red: 0, green: 128, blue: 0, alpha: 0.3).cgColor
self.isUserInteractionEnabled = true;
self.alpha = 1.0;
self.isHidden = false;
}
public func setPath(_ path: UIBezierPath) {
(layer as! CAShapeLayer).path = path.cgPath;
}
}
How do I get the shape to stay in the same location on the image after rotation?
I was able to figure out my own question. Posting here in case someone else comes across this.
What I did was set the UIImageView.contentMode = .center and set the UIImageView size to the same height and width of the image. Then I let the UIScrollView do its thing and scale the image. Therefore, I didn't have both the UIImageView scaling the image because of the AspectFit and the UIScrollView scaling the image.
I put the following in my UIViewController:
override func viewDidAppear(_ animated: Bool) {
//Initial Scale
imageView.bounds.size.width = (imageView.image?.size.width)!;
imageView.bounds.size.height = (imageView.image?.size.height)!;
imageView.frame = CGRect(x: 0, y: 0, width: imageView.bounds.size.width, height: imageView.bounds.size.height)
let widthScale: CGFloat = scrollView.width / imageView.image!.size.width;
let heightScale: CGFloat = scrollView.height / imageView.image!.size.height;
let initialZoom = min(widthScale, heightScale);
scrollView.setZoomScale(initialZoom, animated: false)
}
Now when my shapes are draw on the image and the device is rotated they stay in the correct place.
I'm trying to create a custom UIImageView class that accomplishes two things, it makes the image view round and add a shadow to it.
This is what i have:
RoundImage.swift
class RoundImage: UIImageView {
override func awakeFromNib() {
}
func setImageAndShadow(image: UIImage) {
self.image = image
self.superview?.layoutIfNeeded()
print("Image size \(self.frame.size)")
self.clipsToBounds = true
layer.masksToBounds = true
layer.cornerRadius = self.frame.height / 2
layer.shadowOffset = CGSize(width: 0, height: 3.0)
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.7
layer.shadowRadius = 4
}
}
ViewController.Swift
Class ViewController : UIViewController {
#IBOutlet weak var userImageView: RoundImage!
override func viewDidLoad() {
self.userImageView.setImageAndShadow(image: User.sharedInstance.profileImage!)
}
}
As a result of this a get a round image in the app, but there's no shadow in it.
Thanks in advance for any help.
Swift 3 You can use CAShapeLayer:
func setImageAndShadow(image: UIImage, myView : UIView) {
self.image = image
self.superview?.layoutIfNeeded()
print("Image size \(self.frame.size)")
self.clipsToBounds = true
layer.masksToBounds = true
layer.cornerRadius = self.frame.height / 2
let Shape = CAShapeLayer()
let myPath = UIBezierPath(ovalIn: self.frame)
Shape.shadowPath = myPath.cgPath
Shape.shadowColor = UIColor.black.cgColor
Shape.shadowOffset = CGSize(width: 0, height: 3)
Shape.shadowRadius = 7
Shape.shadowOpacity = 0.7
myView.layer.insertSublayer(Shape, at: 0)
}
and in VC
let image = UIImage(named: "IMG_4040.jpg")
self.userImageView.setImageAndShadow(image: image!, myView : view)
You don't see the shadow because it is cropped with cornerRadius. In order to make it visible you should create a UIView with UIImageView inside, apply cornerRadius to UIImageView and shadow to it's container (superview)
I am implementing like a calendar layout with some modification which is shown in the screenshot. To achieve this I have used UICollectionView. The problem is, I have to draw a screen width continuous line(green line in screenshot). The green line should cover the whole width, I know its not showing over the circular cell due to half of the cornerRadius and a vertical line only after the first cell(10 am). Where i have to add the shapelayer, so that it ll seems like a continuous line. Here is the code which I have tried so far.
KKViewController.m
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! KKBookCollectionViewCell
self.bookingCollectionView.backgroundColor = UIColor.whiteColor()
let rectangularRowIndex:NSInteger = indexPath.row % 5
if(rectangularRowIndex == 0 )
{
cell.userInteractionEnabled = false
cell.layer.cornerRadius = 0
cell.timeSlotLabel.text = "10am"
cell.backgroundColor = UIColor.whiteColor()
cell.layer.borderWidth = 0
cell.layer.borderColor = UIColor.clearColor().CGColor
}
else
{
cell.userInteractionEnabled = true
cell.layer.cornerRadius = cell.frame.size.width/2
cell.timeSlotLabel.text = ""
//cell.backgroundColor = UIColor.lightGrayColor()
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor.grayColor().CGColor
if cell.selected == true
{
cell.backgroundColor = UIColor.greenColor()
}
else
{
cell.backgroundColor = UIColor.lightGrayColor()
}
}
return cell
}
KKCollectionCell.m
var borderWidth:CGFloat!
var borderPath:UIBezierPath!
override func awakeFromNib() {
super.awakeFromNib()
drawHorizontalLine()
dottedLine(with: borderPath, and: borderWidth)
drawVerticalLine()
dottedLine(with: borderPath, and: borderWidth)
func drawVerticalLine()
{
borderPath = UIBezierPath()
borderPath.moveToPoint(CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y))
//borderPath.addLineToPoint(CGPointMake(self.frame.size.width - 5, self.frame.origin.y + self.frame.size.height - 50))
borderPath.addLineToPoint(CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y + self.frame.size.height))
borderWidth = 2.0
print("border path is %f, %f:\(borderPath)")
}
func drawHorizontalLine()
{
borderPath = UIBezierPath()
borderPath.moveToPoint(CGPointMake(0, self.frame.origin.y))
borderPath.addLineToPoint(CGPointMake(self.frame.size.width + 10, self.frame.origin.y))
borderWidth = 2.0
print("border path is %f, %f:\(borderPath)")
}
func dottedLine (with path:UIBezierPath, and borderWidth:CGFloat)
{
let shapeLayer = CAShapeLayer()
shapeLayer.strokeStart = 0.0
shapeLayer.strokeColor = UIColor.greenColor().CGColor
shapeLayer.lineWidth = borderWidth
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [1,2]
shapeLayer.path = path.CGPath
self.layer.addSublayer(shapeLayer)
}
You can add a new view inside the collection view cell and set the corner radius for that new view. Also you have to reduce the spacing between the cells. Then the line will look like what you expected.
I know it's an old question but it was never properly answered, i was looking for such behaivior and achieved it with the help of another answer.
To achieve that you need to subclass the UICollectionViewFlowLayout (maybe a simple layout also would do but i didn't try).
class HorizontalLineFlowLayout: UICollectionViewFlowLayout {
var insets: CGFloat = 10.0
static let lineWidth: CGFloat = 10.0
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var attributesCopy: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
attributesCopy += [attribute]
let indexPath = attribute.indexPath
if collectionView!.numberOfItems(inSection: indexPath.section) == 0 { continue }
let contains = attributes.contains { layoutAttribute in
layoutAttribute.indexPath == indexPath && layoutAttribute.representedElementKind == HorizontalLineDecorationView.kind
}
if !contains {
let horizontalAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: HorizontalLineDecorationView.kind, with: indexPath)
let width = indexPath.item == collectionView!.numberOfItems(inSection: indexPath.section) - 1 ?
attribute.frame.width + insets :
attribute.frame.width + insets * 1.5
let frame = CGRect(x: attribute.frame.minX, y: attribute.frame.minY, width: width, height: 0.5)
horizontalAttribute.frame = frame
attributesCopy += [horizontalAttribute]
}
}
return attributesCopy
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
the func layoutAttributesForElements is called each time, and it makes the lines for the cells you specify and for the frames you specify.
you would also need the decoration view which looks like that:
class HorizontalLineDecorationView: UICollectionReusableView {
static let kind = "HorizontalLineDecorationView"
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .gray
alpha = 0.2
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
in general its just a view that goes behind the cell, respectively to its indexPath, so the lines are not all along the screen but those are some lines gathered togather which look like a full line, you can adjust its width and height, play with that.
notice that frame that is set for an attribute in the layout, that is the frame of the decoration view, and is defined with respect to the cell (attribute).
dont forget to register that decoration view and also to make the layout and pass it to the collecitonview like this:
let layout = HorizontalLineFlowLayout()
layout.register(HorizontalLineDecorationView.self, forDecorationViewOfKind: HorizontalLineDecorationView.kind)
layout.minimumInteritemSpacing = 10.0
layout.minimumLineSpacing = 10.0
layout.sectionInset = .zero
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(DateCell.self, forCellWithReuseIdentifier: "month")
collectionView.delegate = monthDelegate
collectionView.dataSource = monthDelegate
collectionView.backgroundColor = .clear
the end result is