I have a custom UIView and I add it to my ViewController like this:
let myCustomView = Bundle.main.loadNibNamed("MyCustomView", owner: nil, options: nil) as! MyCustomView
myCustomView.layer.cornerRadius = 10
myCustomView.layer.masksToBounds = true
I round the corners of the view. But I am wondering, is there a way to move this logic of rounding the corners inside the MyCustomView class?
As you use IB, you may find it more convenient to make an extension of UIView
extension UIView {
#IBInspectable var borderColor: UIColor? {
set {
layer.borderColor = newValue?.cgColor
}
get {
if let color = layer.borderColor {
return UIColor(cgColor:color)
} else {
return nil
}
}
}
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
clipsToBounds = newValue > 0
}
get {
return layer.cornerRadius
}
}
}
Then you can set those values from Attributes inspector.
Yes - If you're loading a nib with a custom view, that nib is very likely referring to another class. If that's the case, you can move the logic inside the class itself.
That said, I really like Lawliet's suggestion of making a UIView extension with IBInspectable properties. The downside to that approach is that every single view now has these properties, which creates a certain overhead and potential for clashes.
You can do something like this in your UIView subclass:
class RoundedView: UIView {
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
}
}
Also if you want to pass a custom value instead of '10' in the cornerRadius property, you can try to implement a convenience init by looking here:
Override Init method of UIView in Swift
Related
I'm trying to build a UIView that lays out other views using their intrinsic width, left to right, and then wraps if necessary. UICollectionView seems overly complicated for what I need. I also tried using the FlexLayout wrapper for Yoga, and couldn't seem to get the right incantation. It seems like it should be much easier than this -- what's the simple solution?
I ended up writing my own UIView to solve the problem. I doubt if it's very robust, but it solves my immediate needs. Please, comment if you have an improvements to suggest.
import UIKit
class FlowLayout: UIView {
private var hgapValue: CGFloat = 0.0
private var vgapValue: CGFloat = 0.0
var hgap: CGFloat {
set(newValue) {
hgapValue = (newValue < 0) ? -newValue : newValue
}
get {
return hgapValue
}
}
var vgap: CGFloat {
set(newValue) {
vgapValue = (newValue < 0) ? -newValue : newValue
}
get {
return vgapValue
}
}
//initWithFrame to init view from code
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
override func didAddSubview(_ subview: UIView) {
refreshLayout()
}
//common func to init our view
private func setupView() {
// backgroundColor = .red
}
public func refreshLayout() {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
for subview in subviews {
subview.frame.origin.x = x
subview.frame.origin.y = y
x += subview.frame.width + hgapValue
if x + subview.frame.width >= (subview.superview?.frame.width)! {
x = 0.0
y += subview.frame.height + vgapValue
}
}
setNeedsDisplay()
}
}
In interface builder, using UIStackView to achieve a simple alternative.
You can also turn on auto layout to make it naturally.
I've been setting the backgroundColor of buttons by doing self.layer.backgroundColor = someColor.
However, this doesn't seem to work with a custom class? I have this general class that I use:
class DarkButton: BaseButton {
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = 5
self.setTitleColor(UIColor.black, for: .normal)
self.setTitleColor(UIColor.lightGray, for: .highlighted)
self.layer.backgroundColor = UIColor(red:0.77, green:0.77,
blue:0.77, alpha: 1.0).cgColor
self.clipsToBounds = false
}
}
the self.layer.backgroundColor works just fine. Now if I extend it, like so:
class SuperCoolButton: DarkButton {
required init() {
super.init()
self.setUp()
}
required init(spacing: Spacing) {
super.init(spacing: spacing)
self.setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setUp()
}
func setUp() {
self.generateImage()
self.changeBGColor()
}
func generateImage() {
let image = UIImage(named: "logoSmall") as UIImage?
self.setImage(image, for: .normal)
self.imageEdgeInsets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
self.imageView?.contentMode = .scaleAspectFit
}
func changeBGColor() {
self.layer.backgroundColor = UIColor.red.cgColor
}
}
the backgroundColor stays that of the DarkButton backgroundColor, however setting the image does indeed work >_>
This is happening because layoutSubviews() implemented in the super class(DarkButton) gets called after setUp method of DarkButton. You need to override layoutSubviews in your class SuperCoolButton and call setUp there instead of at its init method.
override func layoutSubviews() {
super.layoutSubviews()
setUp()
}
EDIT:
I think you should move the code which you have written inside [layoutSubviews][1] of DarkButton class.
layoutSubviews method gets called multiple times and only the code related to the layout of the view should be there.
Subclasses can override this method as needed to perform more precise
layout of their subviews. You should override this method only if the
autoresizing and constraint-based behaviors of the subviews do not
offer the behavior you want.
The ideal place to change layer's corner radius and setTitleColor or background color is either at init of the custom view or at awakeFromNib:(only, if the view is always going to be designed in nib).
Because you want to change the backgroud color of button at some later time. You can simply call your changeBgColor method on SuperCoolButton. Earlier it was not working, because layoutSubviews must be setting the background color back to the default.
Do not call your setUp() method in init instead of this call it in layoutSubviews
class SuperCoolButton: DarkButton {
required init() {
super.init()
//self.setUp()
}
required init(spacing: Spacing) {
super.init(spacing: spacing)
//self.setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//self.setUp()
}
func setUp() {
self.generateImage()
self.changeBGColor()
}
func generateImage() {
let image = UIImage(named: "logoSmall") as UIImage?
self.setImage(image, for: .normal)
self.imageEdgeInsets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
self.imageView?.contentMode = .scaleAspectFit
}
func changeBGColor() {
self.layer.backgroundColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
setUp()
}
}
I have a collectionView cell that should either display an image or an icon that is generated as a custom UIView (lets say IconView).
Currently, I implemented this by adding an UIImageView and an IconView as subviews to a container view.
When an image is provided, the image property of UIImageView is simply updated. When a new IconView is provided it is currently always added as a subview to the container view. Therefore, before adding, it is first checked whether an IconView has already been added, and if so it is removed.
Although this implementation works, it is not very elegant and seems not efficient since it results in scrolling issues when the number of rows increase.
Would there be a better (more efficient) way to implement this for a single CollectionViewCell?
class CustomCell: UICollectionViewCell {
internal var image: UIImage? {
didSet {
self.imageView.image = image!
}
}
internal var iconView: IconView? {
didSet {
if !(self.iconContainerView.subviews.flatMap{ $0 as? IconView}.isEmpty) {
self.iconView!.removeFromSuperview()
}
self.iconView!.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.iconView!)
self.image = nil
}
}
fileprivate var imageView: UIImageView!
fileprivate var iconContainerView: UIView!
fileprivate var layoutConstraints = [NSLayoutConstraint]()
override init(frame: CGRect) {
super.init(frame: frame)
// ContainerView
self.iconContainerView = UIView()
self.iconContainerView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.iconContainerView)
// ImageView
self.imageView = UIImageView()
self.imageView.translatesAutoresizingMaskIntoConstraints = false
self.iconContainerView.addSubview(self.imageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.iconContainerView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true
self.iconContainerView.widthAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.heightAnchor.constraint(equalToConstant: 60).isActive = true
self.iconContainerView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
// Deactivate non-reusable constraints
_ = self.layoutConstraints.map { $0.isActive = false }
self.layoutConstraints = [NSLayoutConstraint]()
if let iconView = self.iconView {
self.imageView.isHidden = true
self.layoutConstraints.append(iconView.centerYAnchor.constraint(equalTo: self.iconContainerView.centerYAnchor))
self.layoutConstraints.append(iconView.centerXAnchor.constraint(equalTo: self.iconContainerView.centerXAnchor))
self.layoutConstraints.append(iconView.heightAnchor.constraint(equalToConstant: 40))
self.layoutConstraints.append(iconView.widthAnchor.constraint(equalToConstant: 40))
} else {
self.imageView.isHidden = false
self.iconView?.isHidden = true
self.layoutConstraints.append(self.imageView.leadingAnchor.constraint(equalTo: self.iconContainerView.leadingAnchor))
self.layoutConstraints.append(self.imageView.trailingAnchor.constraint(equalTo: self.iconContainerView.trailingAnchor))
self.layoutConstraints.append(self.imageView.topAnchor.constraint(equalTo: self.iconContainerView.topAnchor))
self.layoutConstraints.append(self.imageView.bottomAnchor.constraint(equalTo: self.iconContainerView.bottomAnchor))
}
_ = self.layoutConstraints.map {$0.isActive = true}
}
}
Don't ad and remove the IconView when setting. Add both in the same spot and change the isHidden, alpha, or opacity or bringSubviewToFront. This is much less main thread intensive.
I want to create a Circle image view for my profile avatar. I have tried this:-
class CircleImageView: UIImageView {
override func draw(_ rect: CGRect) {
// Drawing code
layer.masksToBounds = true
layer.cornerRadius = min(rect.width/2 , rect.height/2)
clipsToBounds = true
}
}
But its not working.
An extension will be great to set corner or do round image:
extension UIImageView {
func setRadius(radius: CGFloat? = nil) {
self.layer.cornerRadius = radius ?? self.frame.width / 2;
self.layer.masksToBounds = true;
}
}
Use:
imgview.setRadius(radius: 10)
imgview.setRadius() //default frame.width/2
Draw function is for drawing not changing layer.
Use layoutSubviews
override func layoutSubviews() {
super.layoutSubviews()
layer.masksToBounds = true
layer.cornerRadius = min(self.frame.width/2 , self.frame.height/2)
clipsToBounds = true
}
You are adding the code in the wrong place, drawRect: is not really the right method to do such a functionality for editing the layer, you can achive this by:
Editing the layer when init(frame:) the imageView (also, adding the same functionality in init(coder:) because it should work for both approaches: programmatically and via storyboard):
class CircleImageView: UIImageView {
override init(frame: CGRect) {
super.init(frame: frame)
setupCircleLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupCircleLayer()
}
private func setupCircleLayer() {
layer.masksToBounds = true
layer.cornerRadius = min(frame.width/2 , frame.height/2)
clipsToBounds = true
}
}
Or as #Mohammadalijf suggested in his answer by overriding layoutSubviews() method:
class CircleImageView: UIImageView {
override func layoutSubviews() {
super.layoutSubviews()
layer.masksToBounds = true
layer.cornerRadius = min(frame.width/2 , frame.height/2)
clipsToBounds = true
backgroundColor = UIColor.black
}
}
It does the desired fucntionality that you are asking for, but note that:
Subclasses can override this method as needed to perform more precise
layout of their subviews. You should override this method only if the
autoresizing and constraint-based behaviors of the subviews do not
offer the behavior you want. You can use your implementation to set
the frame rectangles of your subviews directly.
i.e, it is related to updating the layout of the view, check the documentation for more information; That's why I prefer to do it in the init methods.
I'm having a problem with the graphOrigin property in my UIView subclass. When I defined graphOrigin as a computed variable, it convert's the superview's center point to this view's center point and displays the graph in the center of the screen. This does not happen when the variable isn't computed. See code and screenshot for the working case:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint {
return convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes in the view
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
AxesDrawer is a class that draws axes in the current view, here is the method signature for drawAxesInRect:
drawAxesInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
And here is the code and screenshot for the case that doesn't work:
class GraphX_YCoordinateView: UIView {
var graphOrigin: CGPoint! {
didSet {
setNeedsDisplay()
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
#IBInspectable var scale: CGFloat = 50 {
didSet {
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
// Draw X-Y axes
let axesDrawer = AxesDrawer(contentScaleFactor: contentScaleFactor)
axesDrawer.drawAxesInRect(bounds, origin: graphOrigin, pointsPerUnit: scale)
}
}
So literally all I changed was initializing the graphOrigin property in place and computing it in the initializer. I didn't touch the StoryBoard at all while editing this code.
I tried initializing the variable inline:
var graphOrigin = convertPoint(center, fromView: superview)
But this wasn't allowed because the implicit self is not initialized when properties are computed.
Can anyone explain why the superview's center seems to change location depending on how the variable is initialized?
This function
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
graphOrigin = convertPoint(center, fromView: superview)
}
means that you are loading view from xib, and at this time the view dimension is 600:600 (look at your xib). So that your graphOrigin = 300:300. This is why you see the second picture.
To fix that problem, you should compute the graphOrigin after the view finish layout in viewDidLayout.