I'm trying to create a UIButton with rounded corners, a background image and a shadow. Before adding the shadow, everything works fine.
But after adding shadow values, the shadow doesn't show up. Obviously due to clipsToBounds property value being set to true. If I remove that, it looks like this.
Since I need the corner radius as well, I cannot have the clipsToBounds be false.
This is my code.
class CustomButton: UIButton {
var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
clipsToBounds = true
}
}
var shadowRadius: CGFloat {
get {
return layer.shadowRadius
}
set {
layer.shadowRadius = newValue
}
}
var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
var shadowOffset: CGSize {
get {
return layer.shadowOffset
}
set {
layer.shadowOffset = newValue
}
}
var shadowColor: UIColor? {
get {
if let color = layer.shadowColor {
return UIColor(cgColor: color)
}
return nil
}
set {
if let color = newValue {
layer.shadowColor = color.cgColor
} else {
layer.shadowColor = nil
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
private lazy var button: CustomButton = {
let button = CustomButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setBackgroundImage(UIImage(named: "Rectangle"), for: .normal)
button.setTitleColor(.white, for: .normal)
button.setTitle("Sign Up", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
button.cornerRadius = 20
button.shadowColor = .systemGreen
button.shadowRadius = 10
button.shadowOpacity = 1
button.shadowOffset = CGSize(width: 0, height: 0)
return button
}()
Is there a workaround to have both the shadow and the corner radius?
Demo project
You can do it via adding shadow and background image with different layer.
First, if you don't need the properties, remove all and modify your CustomButton implementation just like below (modify as your need):
class CustomButton: UIButton {
private let cornerRadius: CGFloat = 20
private var imageLayer: CALayer!
private var shadowLayer: CALayer!
override func draw(_ rect: CGRect) {
addShadowsLayers(rect)
}
private func addShadowsLayers(_ rect: CGRect) {
// Add Image
if self.imageLayer == nil {
let imageLayer = CALayer()
imageLayer.frame = rect
imageLayer.contents = UIImage(named: "Rectangle")?.cgImage
imageLayer.cornerRadius = cornerRadius
imageLayer.masksToBounds = true
layer.insertSublayer(imageLayer, at: 0)
self.imageLayer = imageLayer
}
// Set the shadow
if self.shadowLayer == nil {
let shadowLayer = CALayer()
shadowLayer.masksToBounds = false
shadowLayer.shadowColor = UIColor.systemGreen.cgColor
shadowLayer.shadowOffset = .zero
shadowLayer.shadowOpacity = 1
shadowLayer.shadowRadius = 10
shadowLayer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
layer.insertSublayer(shadowLayer, at: 0)
self.shadowLayer = shadowLayer
}
}
}
And initialize your button like below:
private lazy var button: CustomButton = {
let button = CustomButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.white, for: .normal)
button.setTitle("Sign Up", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold)
return button
}()
You need to use two separate views for shadow and image. I can't find any solution to set image, shadow, and corner radius using the same button layer.
Make button's corner radius(clipsToBounds=true) rounded and set the image on it.
Take a shadow view under the button with proper shadow radius and offset.
You can add view.layer.masksToBounds = false
This will disable clipping for sublayer
https://developer.apple.com/documentation/quartzcore/calayer/1410896-maskstobounds
Related
This seems to be a tricky problem I cannot find the solution for.
I need a UIButton with:
rounded corners,
background color for state,
drop shadow.
I can achieve the the corner radius and background color for state subclassing a UIButton and setting the parameters:
// The button is set to "type: Custom" in the interface builder.
// 1. rounded corners
self.layer.cornerRadius = 10.0
// 2. Color for state
self.backgroundColor = UIColor.orange // for state .normal
self.setBackgroundColor(color: UIColor.red, forState: .highlighted)
// 3. Add the shadow
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowOpacity = 0.3
self.layer.shadowRadius = 6.0
self.layer.masksToBounds = false
// I'm also using an extension to be able to set the background color
extension UIButton {
func setBackgroundColor(color: UIColor, forState: UIControl.State) {
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
if let context = UIGraphicsGetCurrentContext() {
context.setFillColor(color.cgColor)
context.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
let colorImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.setBackgroundImage(colorImage, for: forState)
}
}
}
The problem:
Once the button is rendered, seems fine, has the desired background color, rounded corners and the shadow. But once is pressed the setBackgroundImage(image, for: forState) launches and gets rid of the radius, so the button is displayed as a square with the desired color and shadow.
Is there a way to preserve the radius when the button is pressed (.highlighted)?
I did try the solutions from this post (for example) but none consider the setBackgroundImage(image, for: forState). I cannot find anything that works...
If you just want to change the background color for .normal and .highlighted - that is, you don't need a background image - you can override var isHighlighted to handle the color changes.
Here is an example UIButton subclass. It is marked #IBDesignable and has normalBackground and highlightBackground colors marked as #IBInspectable so you can set them in Storyboard / Interface Builder:
#IBDesignable
class HighlightButton: UIButton {
#IBInspectable
var normalBackground: UIColor = .clear {
didSet {
backgroundColor = self.normalBackground
}
}
#IBInspectable
var highlightBackground: UIColor = .clear
override open var isHighlighted: Bool {
didSet {
backgroundColor = isHighlighted ? highlightBackground : normalBackground
}
}
func setBackgroundColor(_ c: UIColor, forState: UIControl.State) -> Void {
if forState == UIControl.State.normal {
normalBackground = c
} else if forState == UIControl.State.highlighted {
highlightBackground = c
} else {
// implement other states as desired
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// 1. rounded corners
self.layer.cornerRadius = 10.0
// 2. Default Colors for state
self.setBackgroundColor(.orange, forState: .normal)
self.setBackgroundColor(.red, forState: .highlighted)
// 3. Add the shadow
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 3)
self.layer.shadowOpacity = 0.3
self.layer.shadowRadius = 6.0
self.layer.masksToBounds = false
}
}
It would be awesome if anyone would guide me to the right solution here. It's my second question of the day over here, and I am kind hesitant to ask it over here but I am learning totally new stuff(like creating your own segmented control etc) and have literally no idea where to even begin with this error. I have tried to debug as much as possible with my limited knowledge.
So here's my problem in the summary form.
I have two views. Inner view's width is dependent on the outer view's width. So If I update my outer View's width Constraint(using constraint IBoutlet), outer view's width changes but the inner views width remains the sameas the old one. I did layoutIfNeeded() on the outer view after I change the width constraint of the outer view but nothing happens.
In detail:
I have a segmented control(outer view) which selector's width(inner view) is dependent on the segmented control's total width. As I said above, the width of the selector remains the same after I change the the total width of the segmented control. Selector's width is dependent on the segmented control's width.
Here's the image illustrating my problem.
As you can see my selector's width is not getting updated. It should be half of the total width of the new segmented control.
How I updated my segmented control's width?
Basically I took the width constraint of the segmented control as an IBOUtlet in my VC then I increased it's width according to the screen size. But the selector's width remain same.
HERE'S THE CODE that I used to changed my width of my segmented control
DispatchQueue.main.async {
self.segmentedWidthControl.constant = UIScreen.main.bounds.width/2
self.segmentedControl.layoutIfNeeded()
//self.segmentedControl.updateConstraints() // this doesn't work either
}
For the custom segmented control, I followed tutorial from the youtube.
Here's the code
#IBDesignable
class SegmentedControl: UIControl{
var buttons = [UIButton]()
var selector: UIView!
var selectSegmentIndex = 0
#IBInspectable
var borderWidth: CGFloat = 0{
didSet{
layer.borderWidth = borderWidth
}
}
#IBInspectable
var borderColor: UIColor = .clear {
didSet{
layer.borderColor = borderColor.cgColor
}
}
override func draw(_ rect: CGRect) {
layer.cornerRadius = frame.height/2
}
#IBInspectable
var commaSeperatedButtonTitles: String = ""{
didSet{
updateView()
}
}
#IBInspectable
var selectorColor: UIColor = .white{
didSet{
updateView()
}
}
#IBInspectable
var selectorTextColor: UIColor = .white{
didSet{
updateView()
}
}
#IBInspectable
var TextColor: UIColor = .lightGray {
didSet{
updateView()
}
}
func updateView(){
buttons.removeAll()
subviews.forEach { $0.removeFromSuperview()}
let buttonTitles = commaSeperatedButtonTitles.components(separatedBy: ",")
for buttonTitle in buttonTitles{
let button = UIButton(type: .system)
button.setTitle(buttonTitle, for: .normal)
button.setTitleColor(TextColor, for: .normal)
button.addTarget(self, action: #selector(buttonTapped(button: )), for: .touchUpInside )
buttons.append(button)
}
buttons[0].setTitleColor(selectorTextColor, for: .normal)
let selectorWidth = frame.width/CGFloat(buttonTitles.count)
selector = UIView(frame: CGRect(x: 0, y: 0, width: selectorWidth, height: frame.height))
selector.backgroundColor = selectorColor
selector.translatesAutoresizingMaskIntoConstraints = false
selector.layer.cornerRadius = frame.height/2
addSubview(selector)
let sv = UIStackView(arrangedSubviews: buttons)
sv.axis = .horizontal
sv.alignment = .fill
sv.translatesAutoresizingMaskIntoConstraints = false
sv.distribution = .fillEqually
addSubview(sv)
sv.topAnchor.constraint(equalTo: topAnchor).isActive = true
sv.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
sv.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
sv.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
}
#objc func buttonTapped(button:UIButton){
for (buttonIndex,btn) in buttons.enumerated(){
btn.setTitleColor(TextColor, for: .normal)
if(btn == button){
selectSegmentIndex = buttonIndex
let selectorStartPosition = frame.width/CGFloat(buttons.count) * CGFloat(buttonIndex)
UIView.animate(withDuration: 0.3) {
self.selector.frame.origin.x = selectorStartPosition
}
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
sendActions(for: .valueChanged)
}
}
Here's the GITHUB link if you want to run the app. https://github.com/Rikenm/Auto-Counter-iOS
And lastly thank you for the help.
I'd highly recommend using constraints and auto-layout, rather than explicitly setting frames.
Here is your custom class, with really only a few changes. I've commented everything I did:
#IBDesignable
class SegmentedControl: UIControl{
var buttons = [UIButton]()
var selector: UIView!
var selectSegmentIndex = 0
// leading constraint for selector view
var selectorLeadingConstraint: NSLayoutConstraint!
#IBInspectable
var borderWidth: CGFloat = 0{
didSet{
layer.borderWidth = borderWidth
}
}
#IBInspectable
var borderColor: UIColor = .clear {
didSet{
layer.borderColor = borderColor.cgColor
}
}
override func draw(_ rect: CGRect) {
layer.cornerRadius = frame.height/2
}
#IBInspectable
var commaSeperatedButtonTitles: String = ""{
didSet{
updateView()
}
}
#IBInspectable
var selectorColor: UIColor = .white{
didSet{
updateView()
}
}
#IBInspectable
var selectorTextColor: UIColor = .white{
didSet{
updateView()
}
}
#IBInspectable
var TextColor: UIColor = .lightGray {
didSet{
updateView()
}
}
// this will update the control in IB
// when constraints are changed
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateView()
}
// this will keep the selector corners "round"
override func layoutSubviews() {
super.layoutSubviews()
selector.layer.cornerRadius = selector.frame.height / 2.0
}
func updateView(){
buttons.removeAll()
subviews.forEach { $0.removeFromSuperview()}
let buttonTitles = commaSeperatedButtonTitles.components(separatedBy: ",")
for buttonTitle in buttonTitles{
let button = UIButton(type: .system)
button.setTitle(buttonTitle, for: .normal)
button.setTitleColor(TextColor, for: .normal)
button.addTarget(self, action: #selector(buttonTapped(button: )), for: .touchUpInside )
buttons.append(button)
}
buttons[0].setTitleColor(selectorTextColor, for: .normal)
// not needed
//let selectorWidth = frame.width/CGFloat(buttonTitles.count)
// we're going to use auto-layout, so no need to set a frame
//selector = UIView(frame: CGRect(x: 0, y: 0, width: selectorWidth, height: frame.height))
selector = UIView(frame: CGRect.zero)
selector.backgroundColor = selectorColor
selector.translatesAutoresizingMaskIntoConstraints = false
selector.layer.cornerRadius = frame.height/2
addSubview(selector)
// constrain selector top to self.top
selector.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
// constrain selector height to self.height
selector.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
// constrain selector width to self.width
// with multiplier of 1 / number of buttons
let m = 1.0 / CGFloat(buttons.count)
selector.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: m).isActive = true
// instantiate leading constraint for selector, and
// keep a reference in var selectorLeadingConstraint
selectorLeadingConstraint = selector.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0.0)
// make it active
selectorLeadingConstraint.isActive = true
let sv = UIStackView(arrangedSubviews: buttons)
sv.axis = .horizontal
sv.alignment = .fill
// sv.distribution = .fillProportionally
sv.translatesAutoresizingMaskIntoConstraints = false
sv.distribution = .fillEqually
addSubview(sv)
sv.topAnchor.constraint(equalTo: topAnchor).isActive = true
sv.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
sv.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
sv.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
}
#objc func buttonTapped(button:UIButton){
for (buttonIndex,btn) in buttons.enumerated(){
btn.setTitleColor(TextColor, for: .normal)
if(btn == button){
selectSegmentIndex = buttonIndex
let selectorStartPosition = frame.width/CGFloat(buttons.count) * CGFloat(buttonIndex)
// update selector's leading constraint, instead of explicit frame
//self.selector.frame.origin.x = selectorStartPosition
self.selectorLeadingConstraint.constant = selectorStartPosition
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
sendActions(for: .valueChanged)
}
}
Edit:
Here is another option. Instead of using a UIStackView, use constraints to layout the buttons. The "selector" view will now be a sibling of the buttons, so you can use a centerX constraint instead of calculating the .leadingAnchor constraint.
The big benefit is that now you can change the size of the custom segmented control after it has been displayed, and the "selector" size and position will update automatically. For example, if you set the width of the control to 50% of the width of the screen (or its superview), and then you rotate the device so the control gets wider or narrower.
#IBDesignable
class SegmentedControl: UIControl{
var buttons = [UIButton]()
var selector: UIView!
// centerX constraint for selector view
var selectorCenterXConstraint: NSLayoutConstraint!
#IBInspectable
var borderWidth: CGFloat = 0{
didSet{
layer.borderWidth = borderWidth
}
}
#IBInspectable
var borderColor: UIColor = .clear {
didSet{
layer.borderColor = borderColor.cgColor
}
}
override func draw(_ rect: CGRect) {
layer.cornerRadius = frame.height/2
}
#IBInspectable
var commaSeperatedButtonTitles: String = ""{
didSet{
updateView()
}
}
#IBInspectable
var selectorColor: UIColor = .white{
didSet{
updateView()
}
}
#IBInspectable
var selectorTextColor: UIColor = .white{
didSet{
updateView()
}
}
#IBInspectable
var TextColor: UIColor = .lightGray {
didSet{
updateView()
}
}
// this will update the control in IB
// when constraints are changed
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateView()
}
// this will keep the selector corners "round"
override func layoutSubviews() {
super.layoutSubviews()
selector.layer.cornerRadius = selector.frame.height / 2.0
}
func updateView(){
buttons.removeAll()
subviews.forEach { $0.removeFromSuperview()}
// deactivate centerX constraint if its been initialized
if selectorCenterXConstraint != nil {
selectorCenterXConstraint.isActive = false
}
// add the selector view first
selector = UIView(frame: CGRect.zero)
selector.backgroundColor = selectorColor
selector.translatesAutoresizingMaskIntoConstraints = false
selector.layer.cornerRadius = frame.height/2
addSubview(selector)
let buttonTitles = commaSeperatedButtonTitles.components(separatedBy: ",")
for buttonTitle in buttonTitles{
let button = UIButton(type: .system)
button.setTitle(buttonTitle, for: .normal)
button.setTitleColor(TextColor, for: .normal)
button.addTarget(self, action: #selector(buttonTapped(button: )), for: .touchUpInside )
buttons.append(button)
}
buttons[0].setTitleColor(selectorTextColor, for: .normal)
// add each button and set top and height constraints
buttons.forEach {
self.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
$0.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
}
// constrain first button's leading to self.leading
// constrain last button's trailing to self.trailing
NSLayoutConstraint.activate([
buttons[0].leadingAnchor.constraint(equalTo: self.leadingAnchor),
buttons[buttons.count - 1].trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
// constrain each button's width to the first button's width
for i in 1..<buttons.count {
buttons[i].leadingAnchor.constraint(equalTo: buttons[i - 1].trailingAnchor).isActive = true
buttons[i].widthAnchor.constraint(equalTo: buttons[0].widthAnchor).isActive = true
}
// constrain selector top, height and width to first button's top, height and width
selector.topAnchor.constraint(equalTo: buttons[0].topAnchor).isActive = true
selector.heightAnchor.constraint(equalTo: buttons[0].heightAnchor).isActive = true
selector.widthAnchor.constraint(equalTo: buttons[0].widthAnchor).isActive = true
// constrain selector's centerX to first button's centerX
selectorCenterXConstraint = selector.centerXAnchor.constraint(equalTo: buttons[0].centerXAnchor)
selectorCenterXConstraint.isActive = true
}
#objc func buttonTapped(button:UIButton){
buttons.forEach { btn in
btn.setTitleColor(TextColor, for: .normal)
if (btn == button) {
// deactivate selector's current centerX constraint
self.selectorCenterXConstraint.isActive = false
// constrain selector's centerX to selected button's centerX
self.selectorCenterXConstraint = self.selector.centerXAnchor.constraint(equalTo: btn.centerXAnchor)
// re-activate selector's centerX constraint
self.selectorCenterXConstraint.isActive = true
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
sendActions(for: .valueChanged)
}
}
One tip for posting questions here --- read How to create a Minimal, Complete, and Verifiable Example
The easies solution would be to override layoutSubviews and update the width of the selector view.
override func layoutSubviews() {
super.layoutSubviews()
selector.frame.width = frame.width / CGFloat(buttons.count)
}
But you could also have antoayout do it automatically by constraining the selector view to have the same width as the segmented controller with a multiplier of 1/buttons.count
I have created a custom segmented control using this video (https://www.youtube.com/watch?v=xGdRCUrSu94&t=2502s) . I want to be able to change the string values displayed inside the custom segmented control (commaSeperatedButtonTitles) during runtime from 1,2,3,4 to 5,6,7,8 but for some reason the view is not updating the values. The values seem to be able to be changed in viewDidLoad but not inside action events which is what I need.
import UIKit
#IBDesignable
class CustomSegmentedControl: UIControl {
var buttons = [UIButton]()
var selector: UIView!
var selectedSegmentIndex = 0
#IBInspectable
var borderWidth: CGFloat = 0 {
didSet{
layer.borderWidth = borderWidth
}
}
#IBInspectable
var borderColor: UIColor = UIColor.gray {
didSet{
layer.borderColor = borderColor.cgColor
}
}
#IBInspectable
var commaSeperatedButtonTitles: String = "" {
didSet{
}
}
#IBInspectable
var textColor: UIColor = .lightGray {
didSet{
}
}
#IBInspectable
var selectorColor: UIColor = .blue {
didSet{
}
}
#IBInspectable
var selectorTextColor: UIColor = .white {
didSet{
}
}
func updateView(){
buttons.removeAll()
subviews.forEach { (view) in
view.removeFromSuperview()
}
var buttonTitles = commaSeperatedButtonTitles.components(separatedBy: ",")
for buttonTitle in buttonTitles {
let button = UIButton(type: .system)
button.setTitle(buttonTitle, for: .normal)
button.setTitleColor(textColor, for: .normal)
button.addTarget(self, action: #selector(buttonTapped(button:)), for: .touchUpInside)
buttons.append(button)
}
buttons[0].setTitleColor(selectorTextColor, for: .normal)
let selectorWidth = (frame.width / CGFloat(buttonTitles.count))
selector = UIView(frame: CGRect(x: 0, y: 0, width: selectorWidth, height: frame.height))
selector.layer.cornerRadius = (frame.height / 2)
selector.backgroundColor = selectorColor
addSubview(selector)
let sv = UIStackView(arrangedSubviews: buttons)
sv.axis = .horizontal
sv.alignment = .fill
sv.distribution = .fillEqually
addSubview(sv)
sv.translatesAutoresizingMaskIntoConstraints = false
sv.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
sv.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
sv.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
sv.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
// Drawing code
layer.cornerRadius = (frame.height / 2)
//updateView()
}
override func layoutSubviews() {
updateView()
}
func buttonTapped(button: UIButton){
for (buttonIndex, btn) in buttons.enumerated() {
btn.setTitleColor(textColor, for: .normal)
if btn == button{
selectedSegmentIndex = buttonIndex
let selectorStartPosition = (frame.width / CGFloat(buttons.count) * CGFloat(buttonIndex))
UIView.animate(withDuration: 0.3, animations: {
self.selector.frame.origin.x = selectorStartPosition
})
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
sendActions(for: .valueChanged)
}
}
Code in View Controller inside action event:
customSegment.commaSeperatedButtonTitles = "5,6,7,8"
It appears to me that updateView() method is not being called after you set the commaSeperatedButtonTitles property. Try calling it after the property is set:
#IBInspectable
var commaSeperatedButtonTitles: String = "" {
didSet {
self.updateView()
}
}
Another point worth mentioning: it's probably unnecessary to call updateView() every time on layoutSubviews() as it recreates all the buttons for your custom segmented control.
Changing the colours is pretty straightforward, but is it possible to change the border of all unselected dots?
Ex:
dot.layer.borderWidth = 0.5
dot.layer.borderColor = UIColor.blackColor()
Yes This can be done..
Actually its pretty simple.
For iOS 14 Apple has introduced a great customization, where you can set custom images and even set background
let pageControl = UIPageControl()
pageControl.numberOfPages = 5
pageControl.backgroundStyle = .prominent
pageControl.preferredIndicatorImage = UIImage(systemName: "bookmark.fill")
pageControl.setIndicatorImage(UIImage(systemName: "heart.fill"), forPage: 0)
For prior to iOS 14:-
The Pagecontrol is composed of many Subviews which you can access. self.pageControl.subviews returns you [UIView] i.e array of UIView's.
After you get a single view you can add border to it , change its borderColor, change its border width, transform the dot size like scaling it.. All those properties that a UIView has can be used.
for index in 0..<array.count{ // your array.count
let viewDot = weakSelf?.pageControl.subviews[index]
viewDot?.layer.borderWidth = 0.5
viewDot?.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
if (index == indexPath.row){ // indexPath is the current indexPath of your selected cell or view in the collectionView i.e which needs to be highlighted
viewDot?.backgroundColor = UIColor.black
viewDot?.layer.borderColor = UIColor.black.cgColor
}
else{
viewDot?.backgroundColor = UIColor.white
viewDot?.layer.borderColor = UIColor.black.cgColor
}
}
and it looks like this
And remember you do not need to set weakSelf?.pageControl.currentPage = indexPath.row.Do let me know in case of any problem.. Hope this solves your problem.
All the best
iOS 14 allows setting indicator image with SFSymbol here's my subclassing of UIPageControl
class BorderedPageControl: UIPageControl {
var selectionColor: UIColor = .black
override var currentPage: Int {
didSet {
updateBorderColor()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
currentPageIndicatorTintColor = selectionColor
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func updateBorderColor() {
if #available(iOS 14.0, *) {
let smallConfiguration = UIImage.SymbolConfiguration(pointSize: 8.0, weight: .bold)
let circleFill = UIImage(systemName: "circle.fill", withConfiguration: smallConfiguration)
let circle = UIImage(systemName: "circle", withConfiguration: smallConfiguration)
for index in 0..<numberOfPages {
if index == currentPage {
setIndicatorImage(circleFill, forPage: index)
} else {
setIndicatorImage(circle, forPage: index)
}
}
pageIndicatorTintColor = selectionColor
} else {
subviews.enumerated().forEach { index, subview in
if index != currentPage {
subview.layer.borderColor = selectionColor.cgColor
subview.layer.borderWidth = 1
} else {
subview.layer.borderWidth = 0
}
}
}
}
}
Extension for set pagecontrol indicator border / Swift 3
extension UIImage {
class func outlinedEllipse(size: CGSize, color: UIColor, lineWidth: CGFloat = 1.0) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
return nil
}
context.setStrokeColor(color.cgColor)
context.setLineWidth(lineWidth)
let rect = CGRect(origin: .zero, size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5)
context.addEllipse(in: rect)
context.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
USE:
let image = UIImage.outlinedEllipse(size: CGSize(width: 7.0, height: 7.0), color: .lightGray)
self.pageControl.pageIndicatorTintColor = UIColor.init(patternImage: image!)
self.pageControl.currentPageIndicatorTintColor = .lightGray
If anybody wants to CustomUIPageControl, then might need this
#IBDesignable
class CustomPageControl: UIView {
var dotsView = [RoundButton]()
var currentIndex = 0
#IBInspectable var circleColor: UIColor = UIColor.orange {
didSet {
updateView()
}
}
#IBInspectable var circleBackgroundColor: UIColor = UIColor.clear {
didSet {
updateView()
}
}
#IBInspectable var numberOfDots: Int = 7 {
didSet {
updateView()
}
}
#IBInspectable var borderWidthSize: CGFloat = 1 {
didSet {
updateView()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func updateView() -> Void {
for v in self.subviews{
v.removeFromSuperview()
}
dotsView.removeAll()
let stackView = UIStackView()
stackView.axis = NSLayoutConstraint.Axis.horizontal
stackView.distribution = UIStackView.Distribution.fillEqually
stackView.alignment = UIStackView.Alignment.center
stackView.spacing = 10
stackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(stackView)
//Constraints
stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
NSLayoutConstraint.activate([
stackView.heightAnchor.constraint(equalToConstant: 20.0)
])
stackView.removeFullyAllArrangedSubviews()
for i in 0..<numberOfDots {
let button:RoundButton = RoundButton(frame: CGRect.zero)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.heightAnchor.constraint(equalToConstant: 10.0),
button.widthAnchor.constraint(equalToConstant: 10.0),
])
button.tag = i
button.layer.borderWidth = 1
// button.backgroundColor = circleBackgroundColor
// button.layer.borderWidth = borderWidthSize
// button.layer.borderColor = circleColor.cgColor
button.addTarget(self, action:#selector(self.buttonClicked), for: .touchUpInside)
stackView.addArrangedSubview(button)
dotsView.append(button)
}
}
func updateCurrentDots(borderColor : UIColor, backColor : UIColor, index : Int){
for button in dotsView{
if button == dotsView[index]{
button.backgroundColor = backColor
button.layer.borderColor = borderColor.cgColor
}else{
button.backgroundColor = .clear
button.layer.borderColor = borderColor.cgColor
}
}
}
#objc func buttonClicked() {
print("Button Clicked")
}
class RoundButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
}
override func layoutSubviews() {
self.layer.cornerRadius = self.frame.size.width / 2
}
}
extension UIStackView {
func removeFully(view: UIView) {
removeArrangedSubview(view)
view.removeFromSuperview()
}
func removeFullyAllArrangedSubviews() {
arrangedSubviews.forEach { (view) in
removeFully(view: view)
}
}
}
You can use either Programmatically or Stoaryboard
To update the current dots calls this function
self.pageControl.updateCurrentDots(borderColor: .white, backColor: .white, index:1)
I want to show a picture into imageView like the image contact (in a circle) But when I try to show this, the imageView rescale his size and this doesn't show correctly in a circle.
image.layer.borderWidth=1.0
image.layer.masksToBounds = false
image.layer.borderColor = UIColor.whiteColor().CGColor
image.layer.cornerRadius = image.frame.size.height/2
image.clipsToBounds = true
I want to show like this:
But I get this:
How can do the image resize to UIImageView size to show as a circle?
Thanks!
This is solution which I have used in my app:
var image: UIImage = UIImage(named: "imageName")
imageView.layer.borderWidth = 1.0
imageView.layer.masksToBounds = false
imageView.layer.borderColor = UIColor.whiteColor().CGColor
imageView.layer.cornerRadius = image.frame.size.width/2
imageView.clipsToBounds = true
Swift 4.0
let image = UIImage(named: "imageName")
imageView.layer.borderWidth = 1.0
imageView.layer.masksToBounds = false
imageView.layer.borderColor = UIColor.white.cgColor
imageView.layer.cornerRadius = image.frame.size.width / 2
imageView.clipsToBounds = true
What frame size are you using for image? I can get a perfect circle if I set the frame to be a square.
let image = UIImageView(frame: CGRectMake(0, 0, 100, 100))
Fast and Simple solution.
How to mask UIImage to Circle without cropping with Swift.
extension UIImageView {
public func maskCircle(anyImage: UIImage) {
self.contentMode = UIViewContentMode.ScaleAspectFill
self.layer.cornerRadius = self.frame.height / 2
self.layer.masksToBounds = false
self.clipsToBounds = true
// make square(* must to make circle),
// resize(reduce the kilobyte) and
// fix rotation.
self.image = anyImage
}
}
How to call:
let anyAvatarImage:UIImage = UIImage(named: "avatar")!
avatarImageView.maskCircle(anyAvatarImage)
Try this it's work for me ,
set imageView width and height same .
Swift
imageview?.layer.cornerRadius = (imageview?.frame.size.width ?? 0.0) / 2
imageview?.clipsToBounds = true
imageview?.layer.borderWidth = 3.0
imageview?.layer.borderColor = UIColor.white.cgColor
Screenshot
Objective C
self.imageView.layer.cornerRadius = self.imageView.frame.size.width / 2;
self.imageView.clipsToBounds = YES;
self.imageView.layer.borderWidth = 3.0f;
self.imageView.layer.borderColor = [UIColor whiteColor].CGColor;
Hope this will help to some one .
Create your custom circle UIImageView and create the circle under the layoutSubviews helps if you use Autolayout.
/*
+-------------+
| |
| |
| |
| |
| |
| |
+-------------+
The IMAGE MUST BE SQUARE
*/
class roundImageView: UIImageView {
override init(frame: CGRect) {
// 1. setup any properties here
// 2. call super.init(frame:)
super.init(frame: frame)
// 3. Setup view from .xib file
}
required init?(coder aDecoder: NSCoder) {
// 1. setup any properties here
// 2. call super.init(coder:)
super.init(coder: aDecoder)
// 3. Setup view from .xib file
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.borderWidth = 1
self.layer.masksToBounds = false
self.layer.borderColor = UIColor.white.cgColor
self.layer.cornerRadius = self.frame.size.width/2
self.clipsToBounds = true
}
}
I would suggest making your image file a perfect square to begin with. This can be done in almost any photo editing program. After that, this should work within viewDidLoad. Credit to this video
image.layer.cornerRadius = image.frame.size.width/2
image.clipsToBounds = true
That is all you need....
profilepic = UIImageView(frame: CGRectMake(0, 0, self.view.bounds.width * 0.19 , self.view.bounds.height * 0.1))
profilepic.layer.borderWidth = 1
profilepic.layer.masksToBounds = false
profilepic.layer.borderColor = UIColor.blackColor().CGColor
profilepic.layer.cornerRadius = profilepic.frame.height/2
profilepic.clipsToBounds = true
this extension really works for me (including in swift 4+)
extension UIImageView {
func roundedImage() {
self.layer.cornerRadius = (self.frame.size.width) / 2;
self.clipsToBounds = true
self.layer.borderWidth = 3.0
self.layer.borderColor = UIColor.white.cgColor
}
}
Then simply call it as
imageView.roundedImage()
If your using a UIViewController here's how do do it using Anchors. The key is to set the imageView's layer.cornerRadius in viewWillLayoutSubviews like so:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
imageView.layer.cornerRadius = imageView.frame.size.width / 2
}
Also make sure the heightAnchor and widthAnchor are the same size. They are both 100 in my example below
Code:
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.borderWidth = 1.0
imageView.clipsToBounds = true
imageView.layer.borderColor = UIColor.white.cgColor
return imageView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(imageView)
imageView.heightAnchor.constraint(equalToConstant: 100).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 100).isActive = true
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50).isActive = true
imageView.image = UIImage(named: "pizzaImage")
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
imageView.layer.cornerRadius = imageView.frame.size.width / 2
}
If your using a CollectionView Cell set the imageView's layer.cornerRadius in layoutSubviews():
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(imageView)
imageView.heightAnchor.constraint(equalToConstant: 100).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 100).isActive = true
imageView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 50).isActive = true
imageView.image = UIImage(named: "pizzaImage")
}
override func layoutSubviews() {
super.layoutSubviews() // call super.layoutSubviews()
imageView.layer.cornerRadius = imageView.frame.size.width / 2
}
reviewerImage.layer.cornerRadius = reviewerImage.frame.size.width / 2;
reviewerImage.clipsToBounds = true
what i found out is that your width and height of image view must return an even number when divided by 2 to get a perfect circle e.g
let image = UIImageView(frame: CGRectMake(0, 0, 120, 120))
it should not be something like
let image = UIImageView(frame: CGRectMake(0, 0, 130, 130))
I had a similar result (more of an oval than a circle). It turned out that the constraints I set on the UIImageView forced it into an oval instead of a circle. After fixing that, the above solutions worked.
try this.
swift code
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
perform(#selector(self.setCircleForImage(_:)), with: pickedImage, afterDelay: 0)
}
#objc func setCircleForImage(_ imageView : UIImageView){
imageView.layer.cornerRadius = pickedImage.frame.size.width/2
imageView.clipsToBounds = true
}
I fixed it doing modifying the view:
Go to your Main.storyboard
Click on your image
View -> Mode -> Aspect Fill
It works perfectly
This work perfectly for me.
The order of lines is important
func circularImage(photoImageView: UIImageView?)
{
photoImageView!.layer.frame = CGRectInset(photoImageView!.layer.frame, 0, 0)
photoImageView!.layer.borderColor = UIColor.grayColor().CGColor
photoImageView!.layer.cornerRadius = photoImageView!.frame.height/2
photoImageView!.layer.masksToBounds = false
photoImageView!.clipsToBounds = true
photoImageView!.layer.borderWidth = 0.5
photoImageView!.contentMode = UIViewContentMode.ScaleAspectFill
}
How to use:
#IBOutlet weak var photoImageView: UIImageView!
...
...
circularImage(photoImageView)
This also works for me. For perfect circle result, use the same size for width and height. like image.frame = CGRect(0,0,200, 200)
For non perfect circle, width and height should not be equal like this codes below.
image.frame = CGRect(0,0,200, 160)
image.layer.borderColor = UIColor.whiteColor().CGColor
image.layer.cornerRadius = image.frame.size.height/2
image.layer.masksToBounds = false
image.layer.clipsToBounds = true
image.contentMode = .scaleAspectFill
Use this code to make image round
self.layoutIfNeeded()
self.imageView.layer.cornerRadius =
self.imageView.frame.width/2
self.imageView.layer.masksToBounds = true
You can add this file extension to your project & Don't forget to make your image Square "Width = Height" and you can grantee it by giving the image width and Aspect Ratio (1:1)
import Foundation
import UIKit
extension UIView {
#IBInspectable
var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable
var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable
var borderColor: UIColor? {
get {
let color = UIColor(cgColor: layer.borderColor!)
return color
}
set {
layer.borderColor = newValue?.cgColor
}
}
}
Then you will write this line in the cell or view controller or wherever you use your image:
imageViewCountryImage.cornerRadius = imageViewCountryImage.frame.height / 2
and you will find your image very super circular
// MARK: ImageView extension to make rounded
#IBDesignable extension UIImageView {
#IBInspectable var masksToBounds: Bool {
set {
layer.masksToBounds = newValue
}
get {
return layer.masksToBounds
}
}
#IBInspectable var borderWidth: CGFloat {
set {
layer.borderWidth = newValue
}
get {
return layer.borderWidth
}
}
#IBInspectable var cornerRadius: CGFloat {
set {
layer.cornerRadius = newValue
}
get {
return layer.cornerRadius
}
}
#IBInspectable var borderColor: UIColor? {
set {
guard let uiColor = newValue else { return }
layer.borderColor = uiColor.cgColor
}
get {
guard let color = layer.borderColor else { return nil }
return UIColor(cgColor: color)
}
}
}
You need to make sure the height and width should be the same as your image/view.
Like an image with 100 widths and 100 height sizes (100 X 100). If the sizes are different then the circle does not look like a circle.
You can add this extension to your code
import Foundation
import UIKit
extension UIView {
#IBInspectable
var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
#IBInspectable
var borderWidth: CGFloat {
get {
return layer.borderWidth
}
set {
layer.borderWidth = newValue
}
}
#IBInspectable
var borderColor: UIColor? {
get {
let color = UIColor(cgColor: layer.borderColor!)
return color
}
set {
layer.borderColor = newValue?.cgColor
}
}
}
Just add this extension
Extension:
extension UIImageView {
func circleImageView() {
layer.borderColor = UIColor.white.cgColor
layer.borderWidth = 2
contentMode = .scaleAspectFill
layer.cornerRadius = self.frame.height / 2
layer.masksToBounds = false
clipsToBounds = true
}
}
Controller:
self.imageView?.circleImageView()
One more thing, in order to make the image circle we've to set both width and height equal to each other.
Make sure that your height and width of your UIImageView is equal, or else it will look elliptical.
I have solved this problem with using these codes
private let profileAvatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.clipsToBounds = true
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = imageView.frame.width/2
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = UIImage(systemName: "person")
imageView.backgroundColor = .black
imageView.layer.borderWidth = 2.0
imageView.layer.borderColor = UIColor.black.cgColor
return imageView
}()