How to change borders of UIPageControl dots? - ios

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)

Related

Removing Shadow Underneath Semi-Transparent UIView

I’ve been trying to add a drop shadow to a semi transparent UIView but the drop shadow is showing up underneath the view. Basically anywhere inside the outline of the view, I don't want to see any shadows. The location icon has no styling.
// Basic Shadow
self.myView.layer.shadowColor = UIColor.black.cgColor
self.myView.layer.shadowOpacity = 0.3
self.myView.layer.shadowOffset = CGSize(width: 0, height: 3)
self.myView.layer.shadowRadius = 0
The easiest way to do this is to use a custom UIView subclass with two CAShapeLayers...
For the "shadow" layer path, use a rounded-rect UIBezierPath that is slightly taller than the view, so it extends below the bottom.
Here's a quick example...
Custom View Class
class CustomView: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.7) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.73, green: 0.84, blue: 0.96, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var offset: CGFloat = 10 { didSet { setNeedsLayout() } }
private let shadowLayer = CAShapeLayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// rounded-rect path for "shadow" border
r.size.height += offset
let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
shadowLayer.path = spth.cgPath
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.lineWidth = borderWidth
shadowLayer.strokeColor = shadowColor.cgColor
}
}
Example Controller Class
class CustomViewTestVC: UIViewController {
let gradView = BasicGradientView()
let customView = CustomView()
// let's add a label between the gradient view and the custom view
// so we can confirm it's translucent
let testLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .systemBlue
v.font = .systemFont(ofSize: 34.0, weight: .bold)
v.text = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque." // Tap anywhere to toggle this label's visibility."
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[gradView, testLabel, customView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
gradView.widthAnchor.constraint(equalToConstant: 312.0),
gradView.heightAnchor.constraint(equalTo: gradView.widthAnchor, multiplier: 1.0),
gradView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabel.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -4.0),
testLabel.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: 0.0),
testLabel.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
testLabel.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),
customView.widthAnchor.constraint(equalTo: gradView.widthAnchor, constant: -90.0),
customView.heightAnchor.constraint(equalTo: gradView.heightAnchor, constant: -90.0),
customView.centerXAnchor.constraint(equalTo: gradView.centerXAnchor),
customView.centerYAnchor.constraint(equalTo: gradView.centerYAnchor),
])
gradView.endPoint = CGPoint(x: 1.0, y: 1.0)
gradView.colors = [
.red, .yellow, .cyan,
]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testLabel.isHidden.toggle()
}
}
Basic Gradient View
class BasicGradientView: UIView {
public var colors: [UIColor] = [.white, .black] { didSet { setNeedsLayout() } }
public var startPoint: CGPoint = CGPoint(x: 0.0, y: 0.0) { didSet { setNeedsLayout() } }
public var endPoint: CGPoint = CGPoint(x: 1.0, y: 0.0) { didSet { setNeedsLayout() } }
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
private var gLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
}
override func layoutSubviews() {
super.layoutSubviews()
gLayer.colors = colors.compactMap( {$0.cgColor })
gLayer.startPoint = startPoint
gLayer.endPoint = endPoint
}
}
This is the output -- tap anywhere to toggle the UILabel visibility:
Then add your imageView on top (or as a subview of the custom view):
Edit - to answer comment
We can get a shadow to show only on the outside by:
replacing the "fake-shadow shape layer" with a CALayer
using the bezier path as the layer's .shadowPath
creating a bezier path with a "hole" cut in it
use that path as a CAShapeLayer path
and then masking the shadow layer with that CAShapeLayer
Like this:
Here are updates to the above code as examples. Both classes are very similar, with the same custom properties that can be changed from their defaults. I've also added a UIImageView as a subview, to produce this output:
as before, tapping anywhere will toggle the UILabel visibility:
CustomViewA Class
class CustomViewA: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black.withAlphaComponent(0.3) { didSet { setNeedsLayout() } }
public var shadowOpacity: Float = 0.3
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 8.0) { didSet { setNeedsLayout() } }
// shadowRadius is not used, but this allows us to treat both CustomViewA and CustomViewB the same
public var shadowRadius: CGFloat = 0 { didSet { setNeedsLayout() } }
public var image: UIImage? {
didSet {
imageView.image = image
}
}
private let imageView = UIImageView()
private let shadowLayer = CAShapeLayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
var r = bounds
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// rounded-rect path for "shadow" border
r.size.height += shadowOffset.height
let spth = UIBezierPath(roundedRect: r, cornerRadius: cornerRadius)
shadowLayer.path = spth.cgPath
shadowLayer.fillColor = UIColor.clear.cgColor
shadowLayer.lineWidth = borderWidth
shadowLayer.strokeColor = shadowColor.cgColor
}
}
CustomViewB Class
class CustomViewB: UIView {
public var translucentColor: UIColor = .white.withAlphaComponent(0.5) { didSet { setNeedsLayout() } }
public var borderColor: UIColor = .init(red: 0.739, green: 0.828, blue: 0.922, alpha: 1.0) { didSet { setNeedsLayout() } }
public var borderWidth: CGFloat = 4 { didSet { setNeedsLayout() } }
public var cornerRadius: CGFloat = 20 { didSet { setNeedsLayout() } }
public var shadowColor: UIColor = .black { didSet { setNeedsLayout() } }
public var shadowOpacity: Float = 0.7
public var shadowOffset: CGSize = CGSize(width: 0.0, height: 10.0) { didSet { setNeedsLayout() } }
public var shadowRadius: CGFloat = 6 { didSet { setNeedsLayout() } }
public var image: UIImage? {
didSet {
imageView.image = image
}
}
private let imageView = UIImageView()
private let shadowLayer = CALayer()
private let topLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
backgroundColor = .clear
layer.addSublayer(shadowLayer)
layer.addSublayer(topLayer)
// add a square (1:1) image view, 1/2 the width of self
// centered horizontally and vertically
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
override func layoutSubviews() {
super.layoutSubviews()
// rounded-rect path for visible border
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
// translucent rounded-rect bordered properties
topLayer.path = pth.cgPath
topLayer.fillColor = translucentColor.cgColor
topLayer.lineWidth = borderWidth
topLayer.strokeColor = borderColor.cgColor
// we're going to mask the shadow layer with a "cutout" of the rounded rect
// the shadow is going to spread outside the bounds,
// so the "outer" path needs to be larger
// we'll make it plenty large enough
let bpth = UIBezierPath(rect: bounds.insetBy(dx: -bounds.width, dy: -bounds.height))
bpth.append(pth)
bpth.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.fillRule = .evenOdd
maskLayer.path = bpth.cgPath
shadowLayer.mask = maskLayer
shadowLayer.shadowPath = pth.cgPath
shadowLayer.shadowOpacity = shadowOpacity
shadowLayer.shadowColor = shadowColor.cgColor
shadowLayer.shadowRadius = shadowRadius
shadowLayer.shadowOffset = shadowOffset
}
}
Example Controller Class - uses the BasicGradientView class above
class CustomViewTestVC: UIViewController {
let gradViewA = BasicGradientView()
let gradViewB = BasicGradientView()
let customViewA = CustomViewA()
let customViewB = CustomViewB()
// let's add a label between the gradient view and the custom view
// so we can confirm it's translucent
let testLabelA: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.textAlignment = .center
v.textColor = .systemRed
v.font = .systemFont(ofSize: 32.0, weight: .regular)
return v
}()
let testLabelB: UILabel = {
let v = UILabel()
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[gradViewA, gradViewB, testLabelA, testLabelB, customViewA, customViewB].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
gradViewA.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
gradViewA.widthAnchor.constraint(equalToConstant: 300.0),
gradViewA.heightAnchor.constraint(equalTo: gradViewA.widthAnchor, multiplier: 1.0),
gradViewA.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabelA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -4.0),
testLabelA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: 0.0),
testLabelA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
testLabelA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),
customViewA.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: -84.0),
customViewA.heightAnchor.constraint(equalTo: gradViewA.heightAnchor, constant: -84.0),
customViewA.centerXAnchor.constraint(equalTo: gradViewA.centerXAnchor),
customViewA.centerYAnchor.constraint(equalTo: gradViewA.centerYAnchor),
gradViewB.topAnchor.constraint(equalTo: gradViewA.bottomAnchor, constant: 8.0),
gradViewB.widthAnchor.constraint(equalTo: gradViewA.widthAnchor, constant: 0.0),
gradViewB.heightAnchor.constraint(equalTo: gradViewB.widthAnchor, multiplier: 1.0),
gradViewB.centerXAnchor.constraint(equalTo: g.centerXAnchor),
testLabelB.widthAnchor.constraint(equalTo: testLabelA.widthAnchor, constant: -0.0),
testLabelB.heightAnchor.constraint(equalTo: testLabelA.heightAnchor, constant: 0.0),
testLabelB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
testLabelB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
customViewB.widthAnchor.constraint(equalTo: customViewA.widthAnchor, constant: 0.0),
customViewB.heightAnchor.constraint(equalTo: customViewA.heightAnchor, constant: 0.0),
customViewB.centerXAnchor.constraint(equalTo: gradViewB.centerXAnchor),
customViewB.centerYAnchor.constraint(equalTo: gradViewB.centerYAnchor),
])
// let's setup the gradient views the same
gradViewA.colors = [
.init(red: 0.242, green: 0.591, blue: 0.959, alpha: 1.0),
.init(red: 0.113, green: 0.472, blue: 0.866, alpha: 1.0)
]
gradViewA.endPoint = CGPoint(x: 1.0, y: 1.0)
gradViewB.colors = gradViewA.colors
gradViewB.endPoint = gradViewA.endPoint
// let's give the two test labels the same properties
testLabelB.numberOfLines = testLabelA.numberOfLines
testLabelB.textAlignment = testLabelA.textAlignment
testLabelB.textColor = testLabelA.textColor
testLabelB.font = testLabelA.font
let s = "This is a test to confirm that the view and the \"shadow\" are both translucent while the border is opaque."
testLabelA.text = "CustomViewA\n" + s
testLabelB.text = "CustomViewB\n" + s
// set the .image property of both custom views
if let img = UIImage(named: "marker") {
customViewA.image = img
customViewB.image = img
} else {
if let img = UIImage(systemName: "mappin.and.ellipse")?.withTintColor(.white, renderingMode: .alwaysOriginal) {
customViewA.image = img
customViewB.image = img
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
testLabelA.isHidden.toggle()
testLabelB.isHidden.toggle()
}
}

Designable Button does not show images from assets folder

I customized a button and I wanted to make it designable in the storyboard.
If I set the background to an image which is stored in the assets folder, the storyboard doesn't show anything. I still get a transparent button. Any ideas how i can fix this?
import UIKit
#IBDesignable class AnswerButton: UIButton {
private static let BTN_NORMAL_IMAGE_NAME = "ButtonNormal"
private static let BTN_ANSWERED_IMAGE_NAME = "ButtonAnswered"
enum AnswerState : Int {
case normal = 0
case answered
case correctAnswered
case wrongAnswered
}
var answerState: AnswerState = .normal {
didSet {
if answerState == .normal {
self.answerLabel.textColor = UIColor.white
self.setBackgroundImage(UIImage(named: AnswerButton.BTN_NORMAL_IMAGE_NAME), for: .normal)
} else if answerState == .answered {
self.answerLabel.textColor = UIColor.black
self.setBackgroundImage(UIImage(named: AnswerButton.BTN_ANSWERED_IMAGE_NAME), for: .normal)
} else if answerState == .correctAnswered {
self.answerLabel.textColor = UIColor.blue
self.setBackgroundImage(UIImage(named: AnswerButton.BTN_ANSWERED_IMAGE_NAME), for: .normal)
}
}
}
#IBInspectable var myAnswerState: Int {
get {
return self.answerState.rawValue
}
set (value) {
self.setAnswerState(AnswerState(rawValue: value) ?? .normal)
}
}
#IBInspectable var letter: String = "" {
didSet {
self.letterLabel.text = String(letter.first!) + ":"
}
}
#IBInspectable var answer: String = "" {
didSet {
self.answerLabel.text = answer
}
}
public let letterLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.textAlignment = .left
label.textColor = UIColor.orange
label.font = UIFont(name: "Arial Rounded MT Bold", size: 24)
return label
}()
public let answerLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.textAlignment = .left
label.textColor = UIColor.white
label.font = UIFont(name: "Arial Rounded MT Bold", size: 22)
return label
}()
override func layoutSubviews() {
super.layoutSubviews()
self.letterLabel.frame = CGRect(x: 40, y: 0, width: 30, height: self.frame.height)
self.answerLabel.frame = CGRect(x: 80, y: 0, width: 310, height: self.frame.height)
}
private func setAnswerState(_ state: AnswerState) {
self.answerState = state
}
private func setLetter(_ letter: String) {
self.letter = letter
}
private func setAnswer(_ answer: String) {
self.answer = answer
}
init(letter: String, selectionState: AnswerState, answer: String) {
super.init(frame: CGRect(x: 0, y: 0, width: 420, height: 47))
self.sharedInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.sharedInit()
}
#objc
func touchUpInside(sender: UIButton) {
self.setAnswerState(.answered)
}
func sharedInit() {
self.adjustsImageWhenHighlighted = false
self.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
self.setLetter(letter)
self.setAnswer(answer)
self.setAnswerState(.normal)
self.addSubview(self.letterLabel)
self.addSubview(self.answerLabel)
}
override func prepareForInterfaceBuilder() {
self.sharedInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.sharedInit()
}
}
an image with the name ButtonBackground lives in the assets folder.
You have a few issues in your code...
First, to get images to load at design-time / #IBDesignable, you need to tell Interface Builder where to get them:
let dynamicBundle = Bundle(for: AnswerButton.self)
let img = UIImage(named: AnswerButton.BTN_NORMAL_IMAGE_NAME, in: dynamicBundle, compatibleWith: nil)
Unless you're doing something out-of-the-ordinary with your bundle structures, you can use that both at design-time and run-time. That is, you don't conditional code.
Next, in your #IBInspectable var letter / didSet:
#IBInspectable var letter: String = "" {
didSet {
self.letterLabel.text = String(letter.first!) + ":"
}
}
make sure your string is not empty or it will crash:
#IBInspectable var letter: String = "" {
didSet {
if !letter.isEmpty {
self.letterLabel.text = String(letter.first!) + ":"
}
}
}
Next, your sharedInit() func will be called when you change an #IBInspectable value, so don't call this:
self.setAnswerState(.normal)
or your button will never reflect any other value.
And, the way you're setting frames for your labels looks problematic -- if the button width is less than 390-pts, the answerLabel won't fit.
Lastly, I'd suggest loading your two images at init, instead of re-loading every time the state changes.
Here's an update to your class:
#IBDesignable class AnswerButton: UIButton {
private static let BTN_NORMAL_IMAGE_NAME = "ButtonNormal"
private static let BTN_ANSWERED_IMAGE_NAME = "ButtonAnswered"
private var normalImage: UIImage!
private var answeredImage: UIImage!
enum AnswerState : Int {
case normal
case answered
case correctAnswered
case wrongAnswered
}
var answerState: AnswerState = .normal {
didSet {
switch answerState {
case .answered:
self.answerLabel.textColor = UIColor.black
self.setBackgroundImage(answeredImage, for: .normal)
case .correctAnswered:
self.answerLabel.textColor = UIColor.blue
self.setBackgroundImage(answeredImage, for: .normal)
case .wrongAnswered:
self.answerLabel.textColor = UIColor.red
self.setBackgroundImage(answeredImage, for: .normal)
default:
self.answerLabel.textColor = UIColor.white
self.setBackgroundImage(normalImage, for: .normal)
}
}
}
#IBInspectable var myAnswerState: Int {
get {
return self.answerState.rawValue
}
set (value) {
if let t: AnswerState = AnswerState(rawValue: value) {
self.setAnswerState(t)
} else {
self.setAnswerState(.normal)
}
}
}
#IBInspectable var letter: String = "" {
didSet {
if !letter.isEmpty {
self.letterLabel.text = String(letter.first!) + ":"
}
}
}
#IBInspectable var answer: String = "" {
didSet {
self.answerLabel.text = answer
}
}
public let letterLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.textAlignment = .left
label.textColor = UIColor.orange
label.font = UIFont(name: "Arial Rounded MT Bold", size: 24)
label.backgroundColor = UIColor.red.withAlphaComponent(0.25)
return label
}()
public let answerLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.textAlignment = .left
label.textColor = UIColor.white
label.font = UIFont(name: "Arial Rounded MT Bold", size: 22)
label.backgroundColor = UIColor.green.withAlphaComponent(0.25)
return label
}()
override func layoutSubviews() {
super.layoutSubviews()
self.letterLabel.frame = CGRect(x: 40, y: 0, width: 30, height: self.frame.height)
self.answerLabel.frame = CGRect(x: 80, y: 0, width: 310, height: self.frame.height)
}
private func setAnswerState(_ state: AnswerState) {
self.answerState = state
}
private func setLetter(_ letter: String) {
self.letter = letter
}
private func setAnswer(_ answer: String) {
self.answer = answer
}
init(letter: String, selectionState: AnswerState, answer: String) {
super.init(frame: CGRect(x: 0, y: 0, width: 420, height: 47))
setAnswer(answer)
self.sharedInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.sharedInit()
}
#objc
func touchUpInside(sender: UIButton) {
self.setAnswerState(.answered)
}
func sharedInit() {
self.adjustsImageWhenHighlighted = false
self.addTarget(self, action: #selector(touchUpInside), for: .touchUpInside)
self.setLetter(letter)
self.setAnswer(answer)
// don't call this here!
//self.setAnswerState(.normal)
self.addSubview(self.letterLabel)
self.addSubview(self.answerLabel)
// load normal / answered images once
let dynamicBundle = Bundle(for: AnswerButton.self)
if let img = UIImage(named: AnswerButton.BTN_NORMAL_IMAGE_NAME, in: dynamicBundle, compatibleWith: nil) {
self.normalImage = img
}
if let img = UIImage(named: AnswerButton.BTN_ANSWERED_IMAGE_NAME, in: dynamicBundle, compatibleWith: nil) {
self.answeredImage = img
}
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
self.sharedInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.sharedInit()
}
}

UIButton with background image, rounded corners and a shadow

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

How to change color only a fraction of the total in swift?

I'm doing a QR code scan. I use UIView to change the background color of the screen to translucent color. However, I want to make the background color of the place that scans the QR code transparent. What should I do?
class QRcodeScannerViewController : UIViewController, AVCaptureMetadataOutputObjectsDelegate {
#IBOutlet weak var qrcodeView: UIView!
#IBOutlet weak var header: UINavigationBar!
#IBOutlet weak var flash: UIButton!
#IBOutlet weak var qrcodeScanArea: UIImageView!
var previewLayer: AVCaptureVideoPreviewLayer!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
self.qrcodeView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
view.layer.insertSublayer(previewLayer, at: 0)
view.bringSubviewToFront(flash)
view.bringSubviewToFront(header)
header.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
header.isTranslucent = true
header.backgroundColor = UIColor.clear
header.shadowImage = UIImage()
Current My QRcode ScanView
Here's the view I want:
I don't know which part I reverse, but only the color I want is black and the rest is transparent.
view.addSubview(qrcodeView)
let shape = CGRect(x: 0, y: 0, width: 200, height: 200)
let maskLayer = CAShapeLayer()
maskLayer.path = UIBezierPath(roundedRect: shape, cornerRadius: 0).cgPath
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
qrcodeView.layer.mask = maskLayer
I made and applied the class by referring to the answers below. But it doesn't work for me.
let focusview = FocusView.init(frame: qrcodeView.frame)
focusview.areaBackground = UIColor.black
view.addSubview(focusview)
I did something similar ... actually pretty much the same thing, some time ago.
I've dug out the code I used and have posted it here for your reference
fileprivate let focusSize: CGSize = CGSize(width: 218, height: 150)
fileprivate let focusCornerRadius: CGFloat = 10.0
class FocusView: UIView {
var areaBackground: UIColor? {
set {
maskedFocusView.backgroundColor = newValue
}
get {
return maskedFocusView.backgroundColor
}
}
fileprivate let maskedFocusView: MaskedFocusView = {
return MaskedFocusView()
}()
let focusView: UIView = {
let view = UIView()
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 2
view.layer.cornerRadius = focusCornerRadius
// view.layer.shadowColor = UIColor.white.cgColor
// view.layer.shadowRadius = 5.0
// view.layer.shadowOpacity = 0.9
// view.layer.shadowOffset = CGSize.zero
view.layer.masksToBounds = true
return view
}()
override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
backgroundColor = UIColor.darkGray.withAlphaComponent(0.5)
translatesAutoresizingMaskIntoConstraints = false
addSubview(maskedFocusView)
addSubview(focusView)
setupFocusViewConstraints()
setupMaskedFocusViewConstraints()
}
func setupFocusViewConstraints() {
NSLayoutConstraint.activate(
focusView.centerXAnchor.constraint(equalTo: centerXAnchor),
focusView.centerYAnchor.constraint(equalTo: centerYAnchor)
)
let regularFocusViewConstraints = [
focusView.widthAnchor.constraint(equalToConstant: focusSize.width),
focusView.heightAnchor.constraint(equalToConstant: focusSize.height)
]
NSLayoutConstraint.activate(regularFocusViewConstraints)
}
func setupMaskedFocusViewConstraints() {
NSLayoutConstraint.activate(
maskedFocusView.centerXAnchor.constraint(equalTo: centerXAnchor),
maskedFocusView.centerYAnchor.constraint(equalTo: centerYAnchor),
maskedFocusView.topAnchor.constraint(equalTo: topAnchor),
maskedFocusView.bottomAnchor.constraint(equalTo: bottomAnchor),
maskedFocusView.leadingAnchor.constraint(equalTo: leadingAnchor),
maskedFocusView.trailingAnchor.constraint(equalTo: trailingAnchor)
)
}
}
// MARK: - Masked focus view
fileprivate class MaskedFocusView: UIView {
let maskLayer: CAShapeLayer = CAShapeLayer()
override func awakeFromNib() {
super.awakeFromNib()
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() {
backgroundColor = UIColor.darkGray.withAlphaComponent(0.5)
maskLayer.backgroundColor = UIColor.clear.cgColor
layer.mask = maskLayer
translatesAutoresizingMaskIntoConstraints = false
}
override func layoutSubviews() {
super.layoutSubviews()
let width = bounds.width
let height = bounds.height
let x = (width - focusSize.width) / 2
let y = (height - focusSize.height) / 2
let focusRect = CGRect(x: x, y: y, width: focusSize.width, height: focusSize.height)
let fullRect = CGRect(origin: bounds.origin, size: bounds.size)
let path = CGMutablePath()
path.addPath(UIBezierPath(rect: fullRect).cgPath)
path.addPath(UIBezierPath(roundedRect: focusRect, cornerRadius: focusCornerRadius).reversing().cgPath)
maskLayer.path = path
maskLayer.fillRule = .evenOdd
}
}
// MARK: - Layout constraints extension
extension NSLayoutConstraint {
/// A helper function to activate layout constraints.
static func activate(_ constraints: NSLayoutConstraint? ...) {
for case let constraint in constraints {
guard let constraint = constraint else {
continue
}
(constraint.firstItem as? UIView)?.translatesAutoresizingMaskIntoConstraints = false
constraint.isActive = true
}
}
}
extension Array where Element: NSLayoutConstraint {
func activate() {
forEach {
if !$0.isActive {
$0.isActive = true
}
}
}
func deactivate() {
forEach {
if $0.isActive {
$0.isActive = false
}
}
}
}

IBDesignable properties not updating during runtime

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.

Resources