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.
Related
I need to change alpha value of the button in another class. But the problem is button created as "lazy var" so I can not change that value.
lazy var middleButton: UIButton = {
let button = UIButton(type: .custom)
button.frame.size = CGSize(width: 56, height: 56)
button.layer.cornerRadius = button.frame.width / 2
button.layer.masksToBounds = true
button.backgroundColor = .white
button.setImage(UIImage(named: "iconBasket"), for: .normal)
button.contentMode = .center
button.imageView?.contentMode = .scaleAspectFit
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
button.addTarget(self, action: #selector(btnBasketClicked))
addSubview(button)
return button
}()
I want this button's alpha as 0.2 when view is scrolling. Here is the code
extension ProductListView: UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0{
// alpha = 0.2
MyTabBar.shared.scrollDown()
}
else{
// alpha = 1.0
MyTabBar.shared.scrollUp()
}
}
}
func scrollDown(){
middleButton.alpha = 0.2
}
I've tried lots of way but doesn't work. Calling "addSubView()" function in "layoutSubviews()" solve my problem but this causes another problem which my function "basketButtonClicked()" are not called. I used Delegate pattern and here it is.
protocol BasketButtonDelegate: class {
func basketButtonClicked()
}
#objc private func btnBasketClicked(_ sender: UIButton!){
delegate?.basketButtonClicked()
}
override func layoutSubviews() {
super.layoutSubviews()
addSubview(middleButton)
}
When I call "addSubView" function in "layoutSubviews()", "basketbuttonClicked" never called.
extension TabCoordinator : BasketButtonDelegate
{
func basketButtonClicked(){
log.debug("basketButtonClicked")
let coor = CartCoordinator(navigationController, hidesBar: true)
childCoordinators.append(coor)
coor.start()
}
}
(I assigned delegate so the problem is not about it.)
A bit complicated but I hope we can figure it out.
You need to add protocol to your MyTabBar class . It should be like this
class MyTabBar {
static var shared = MyTabBar()
weak var delegate : abc?
func scrollDown(){
delegate?.xyz()
}
}
protocol abc : AnyObject {
func xyz()
}
And in your class
class btnView : UIView , abc{
lazy var middleButton: UIButton = {
let button = UIButton(type: .custom)
button.frame.size = CGSize(width: 56, height: 56)
button.layer.cornerRadius = button.frame.width / 2
button.layer.masksToBounds = true
button.backgroundColor = .red
button.contentMode = .center
button.imageView?.contentMode = .scaleAspectFit
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
button.addTarget(self, action: #selector(btnBasketClicked), for: .touchUpInside)
return button
}()
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
#objc func btnBasketClicked(){
print("Im here")
}
func addMiddleButton(){
self.addSubview(middleButton)
}
func alphaaa(){
self.middleButton.alpha = 0.2
}
func xyz() {
self.alphaaa()
}
}
Last , in your ProductListView create and instance of your view , or if you add with autolayout just call 2. function in your viewDidLoad
var viewwww = btnView(frame: CGRect(x: 10, y: 10, width: 100, height: 100))
viewwww.addMiddleButton() // call to add btn to custom view
and extension
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0{
// alpha = 0.2
MyTabBar.shared.delegate = viewwww
MyTabBar.shared.scrollDown()
}
else{
// alpha = 1.0
MyTabBar.shared.scrollUp()
}
}
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
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 SegmentedControl with 2 lines using:
// AppDelegate
UILabel.appearanceWhenContainedInInstancesOfClasses([UISegmentedControl.self]).numberOfLines = 0
The problem is the line fonts are the same exact size. I need to change the titleTextAttributes for each line so that the second line is smaller then the first line.
I know I can use this for both lines:
segmentedControl.setTitleTextAttributes([NSAttributedStringKey.font : UIFont.systemFont(ofSize: 17))
How can I do this?
// The SegmentedControl
let segmentedControl: UISegmentedControl = {
let segmentedControl = UISegmentedControl(items: ["Pizza\n123.1K", "Turkey Burgers\n456.2M", "Gingerale\n789.3B"])
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
segmentedControl.tintColor = UIColor.orange
segmentedControl.backgroundColor = .white
segmentedControl.isHighlighted = true
segmentedControl.addTarget(self, action: #selector(selectedIndex(_:)), for: .valueChanged)
return segmentedControl
}()
You'll want to create a custom control by subclassing UIControl. Here's a quick example:
CustomSegmentedControl.swift
import UIKit
import CoreImage
public class CustomSegmentedControl: UIControl {
public var borderWidth: CGFloat = 1.0
public var selectedSegementIndex = 0 {
didSet {
self.styleButtons()
}
}
public var numberOfSegments: Int {
return self.segments.count
}
private var buttons: [UIButton] = []
private var stackView = UIStackView(frame: CGRect.zero)
private var stackBackground = UIView(frame: CGRect.zero)
private var segments: [NSAttributedString] = [] {
didSet {
for subview in self.stackView.arrangedSubviews {
subview.removeFromSuperview()
}
self.buttons = []
for i in 0..<segments.count {
let segment = segments[i]
self.createAndAddSegmentButton(title: segment)
}
self.styleButtons()
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
private func setup() {
self.addSubview(stackBackground)
self.stackBackground.constrainToBounds(of: self)
self.addSubview(stackView)
self.stackView.constrainToBounds(of: self)
self.stackView.axis = .horizontal
self.stackView.distribution = .fillEqually
self.stackView.spacing = borderWidth
self.layer.cornerRadius = 5.0
self.layer.borderWidth = borderWidth
self.clipsToBounds = true
self.stackBackground.backgroundColor = tintColor
}
private func createAndAddSegmentButton(title: NSAttributedString) {
let button = createSegmentButton(title: title)
self.buttons.append(button)
self.stackView.addArrangedSubview(button)
}
private func createSegmentButton(title: NSAttributedString) -> UIButton {
let button = UIButton(frame: CGRect.zero)
button.titleLabel?.numberOfLines = 0
button.titleLabel?.textAlignment = .center
button.setAttributedTitle(title, for: .normal)
button.addTarget(self, action: #selector(self.actSelected(button:)), for: .touchUpInside)
return button
}
override public var tintColor: UIColor! {
willSet {
self.layer.borderColor = newValue.cgColor
self.stackBackground.backgroundColor = newValue
}
}
public func setSegments(_ segments: [NSAttributedString]) {
self.segments = segments
}
#objc private func actSelected(button: UIButton) {
guard let index = self.buttons.index(of: button) else {
print("invalid selection should never happen, would want to handle better than this")
return
}
self.selectedSegementIndex = index
self.sendActions(for: .valueChanged)
}
private func styleButtons() {
for i in 0..<self.buttons.count {
let button = self.buttons[i]
if i == selectedSegementIndex {
button.backgroundColor = self.tintColor
button.titleLabel?.textColor = self.backgroundColor ?? .white
} else {
button.backgroundColor = self.backgroundColor
button.titleLabel?.textColor = self.tintColor
}
}
}
}
extension UIView {
func constrainToBounds(of view: UIView) {
self.translatesAutoresizingMaskIntoConstraints = false
let attrs: [NSLayoutAttribute] = [.leading, .top, .trailing, .bottom]
let constraints = attrs.map { (attr) -> NSLayoutConstraint in
return NSLayoutConstraint(item: self,
attribute: attr,
relatedBy: .equal,
toItem: view,
attribute: attr,
multiplier: 1.0,
constant: 0)
}
NSLayoutConstraint.activate(constraints)
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var customSegment: CustomSegmentedControl!
private var segments: [NSAttributedString] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.customSegment.backgroundColor = .white
self.customSegment.tintColor = .orange
let pizza = createText(title: "Pizza", subTitle: "123K")
let turkey = createText(title: "Turkey Burgers", subTitle: "456.2M")
let gingerAle = createText(title: "Gingerale", subTitle: "789.3B")
self.segments = [pizza, turkey, gingerAle]
self.customSegment.setSegments(self.segments)
self.customSegment.addTarget(self, action: #selector(self.segmentSelectionChanged(control:)), for: .valueChanged)
}
#objc private func segmentSelectionChanged(control: CustomSegmentedControl) {
let segment = self.segments[control.selectedSegementIndex]
print("selected segment = \(segment.string)")
}
func createText(title: String, subTitle: String) -> NSAttributedString {
let titleStr = NSMutableAttributedString(string: "\(title)\n", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16)])
let subStr = NSAttributedString(string: subTitle, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 10)])
titleStr.append(subStr)
return titleStr
}
}
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)