How to customize a NSPopUpButton and its NSMenu? - swift

I want to style a NSPopUpButton with my own colors. I've gotten pretty much everything else to work except for the caps at the top and bottom of the menu and I can't get the NSPopUpButton to show an image. Here are a few screenshots of the problem:
Why is the drawn background bigger on my custom view compared to the system NSPopUpButton?
Here is an image of the caps problem:
I can't figure out where those caps are drawn and how I can change their color to match the menu items?
View controller
import Cocoa
let textColor = NSColor(calibratedWhite: 0.9, alpha: 1)
let surfacePrimaryColor = NSColor(calibratedWhite: 0.1, alpha: 1)
let surfaceSecondaryColor = NSColor(calibratedWhite: 0.3, alpha: 1)
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.translatesAutoresizingMaskIntoConstraints = false
let stackView = NSStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let cell = PopUpButtonCell()
cell.imagePosition = .imageLeading
let icon = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)
cell.image = icon
print("cell.image: \(cell.image)")
let popUpButton = NSPopUpButton()
popUpButton.cell = cell
for title in (Array(1...100).map { "Folder \($0)" }) {
let menuItem = NSMenuItem()
menuItem.title = title
let menuItemView = MenuItemView()
menuItemView.translatesAutoresizingMaskIntoConstraints = false
menuItemView.onAction {
cell.title = title
menuItem.menu?.cancelTracking()
}
menuItem.view = menuItemView
let titleLabel = NSTextField(string: title)
titleLabel.drawsBackground = false
titleLabel.isBezeled = false
titleLabel.isSelectable = false
titleLabel.isEditable = false
titleLabel.maximumNumberOfLines = 1
titleLabel.textColor = textColor
let deleteButton = Button(systemSymbolName: "xmark")
deleteButton.font = NSFont.systemFont(ofSize: 14)
deleteButton.isBordered = false
deleteButton.contentTintColor = textColor
deleteButton.onAction {
popUpButton.removeItem(withTitle: title)
}
let menuItemStackView = NSStackView()
menuItemView.addSubview(menuItemStackView)
menuItemStackView.orientation = .horizontal
menuItemStackView.edgeInsets = NSEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
menuItemStackView.translatesAutoresizingMaskIntoConstraints = false
menuItemStackView.leadingAnchor.constraint(equalTo: menuItemView.leadingAnchor).isActive = true
menuItemStackView.trailingAnchor.constraint(equalTo: menuItemView.trailingAnchor).isActive = true
menuItemStackView.topAnchor.constraint(equalTo: menuItemView.topAnchor).isActive = true
menuItemStackView.bottomAnchor.constraint(equalTo: menuItemView.bottomAnchor).isActive = true
menuItemStackView.addView(titleLabel, in: .leading)
menuItemStackView.addView(deleteButton, in: .trailing)
popUpButton.menu?.addItem(menuItem)
}
let popUpButton2 = NSPopUpButton()
popUpButton2.addItems(withTitles: Array(1...100).map { "File \($0)" })
stackView.addArrangedSubview(popUpButton)
stackView.addArrangedSubview(popUpButton2)
}
}
Custom button with onAction closure
import AppKit
typealias Listener = () -> Void
class Button: NSButton {
private var listener: Listener?
init(systemSymbolName: String) {
super.init(frame: .zero)
image = NSImage(systemSymbolName: systemSymbolName, accessibilityDescription: nil)
target = self
action = #selector(actionPerformed(_:))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func actionPerformed(_ sender: AnyObject) {
listener?()
}
func onAction(_ closure: #escaping Listener) {
listener = closure
}
}
Custom popup button cell
import Cocoa
class PopUpButtonCell: NSPopUpButtonCell {
override var controlView: NSView? {
didSet {
controlView?.wantsLayer = true
controlView?.layer?.backgroundColor = surfaceSecondaryColor.cgColor
controlView?.layer?.cornerRadius = 4
}
}
// Prevent system background drawing
override func drawBezel(withFrame frame: NSRect, in controlView: NSView) {
}
override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
let attributedTitle = NSMutableAttributedString(attributedString: title)
let range = NSMakeRange(0, attributedTitle.length)
attributedTitle.addAttributes([NSAttributedString.Key.foregroundColor : textColor], range: range)
return super.drawTitle(attributedTitle, withFrame: frame, in: controlView)
}
}
Why is image nil after setting it on the NSPopUpButton?
How can I change the color of the menu caps?

Why is image nil after setting it on the NSPopUpButton?
See setImage:
This method has no effect. The image displayed in a pop up button is taken from the selected menu item (in the case of a pop up menu) or from the first menu item (in the case of a pull-down menu).

Related

Abnormality when drawing a view inside of a stack view

Exploring stackviews I've ran into a problem of incorrect representation if views inside of it. So, to make a long story short...
I've made a custom checkbox:
class CheckBox: UIView, CheckBoxProtocol {
required init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
self.layer.borderWidth = 5
self.layer.borderColor = color.cgColor
self.addSubview(checkmark)
checkmark.tintColor = color
let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
self.addGestureRecognizer(gesture)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var isChecked = true
lazy var checkmark: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height))
imageView.isHidden = false
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "checkmark")
return imageView
}()
#objc func toggle() {
self.isChecked.toggle()
self.checkmark.isHidden = !self.isChecked
}
In the Controller, when I add this view to the subviews it looks fairly normal and works as it should work (check-uncheck)
However when I add checkbox to the stackview it looses its visible frame and its functionality (does not check-uncheck) - you can see it on the screenshot
screenshot
Here is the code from the ViewController:
class SettingsViewController: UIViewController {
override func loadView() {
super.loadView()
self.view.backgroundColor = .white
self.view.addSubview(stackView)
}
override func viewDidLoad() {
super.viewDidLoad()
}
lazy var stackView: UIStackView = {
let stackView = UIStackView(frame: CGRect(x: 150, y: 150, width: 0, height: 0))
stackView.axis = .horizontal
stackView.spacing = 50
stackView.alignment = .fill
stackView.distribution = .fillEqually
[redCheckbox,
greenCheckbox,
blackCheckbox,
greyCheckbox,
brownCheckbox,
yellowCheckbox,
purpleCheckbox,
orangeCheckbox].forEach {stackView.addArrangedSubview($0)}
return stackView
}()
private let frame = CGRect(x: 0, y: 0, width: 30, height: 30)
lazy var redCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.red)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var greenCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.green)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var blackCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.black)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var greyCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.grey)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var brownCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.brown)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var yellowCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.yellow)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var purpleCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.purple)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var orangeCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.orange)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
It's because we're working with the lazy property and its life cycle can be a little different. Let's set constraints after the view has loaded. What I would suggest to do:
For each checkbox, change the frame to zero:
lazy var orangeCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.orange)
let checkbox = CheckBox(frame: .zero, color: colorFactory)
checkbox.translatesAutoresizingMaskIntoConstraints = false
return checkbox
}()
Do the same to the stackView:
lazy var stackView: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 5
stackView.alignment = .fill
stackView.distribution = .fillEqually
[redCheckbox,
greenCheckbox,
blackCheckbox,
greyCheckbox,
brownCheckbox,
yellowCheckbox,
purpleCheckbox,
orangeCheckbox].forEach {stackView.addArrangedSubview($0)}
return stackView
}()
Add some constraints on viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(stackView)
stackView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor, constant: -100).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 40).isActive = true
[redCheckbox,
greenCheckbox,
blackCheckbox,
greyCheckbox,
brownCheckbox,
yellowCheckbox,
purpleCheckbox,
orangeCheckbox].forEach {
$0.heightAnchor.constraint(equalToConstant: 30).isActive = true
$0.widthAnchor.constraint(equalToConstant: 30).isActive = true
}
}
What you can do to the image inside the checkBox to work fine:
translatesAutoresizingMaskIntoConstraints = false
lazy var checkmark: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.isHidden = false
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "checkmark")
return imageView
}()
on your required init:
required init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
self.layer.borderWidth = 5
self.layer.borderColor = color.cgColor
self.addSubview(checkmark)
checkmark.tintColor = color
checkmark.topAnchor.constraint(equalTo: topAnchor).isActive = true
checkmark.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
checkmark.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
checkmark.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
self.addGestureRecognizer(gesture)
setNeedsDisplay()
}
To offer some additional info...
You can save yourself a lot of duplicate coding.
Take a look at this...
First, slight modifications to your Checkbox class:
class CheckBox: UIView {//, CheckBoxProtocol {
var checkmark: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "checkmark")
return imageView
}()
var isChecked = true {
didSet {
checkmark.isHidden = !isChecked
}
}
required init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
commonInit(color)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit(.white)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit(.white)
}
func commonInit(_ color: UIColor) {
self.layer.borderWidth = 5
self.layer.borderColor = color.cgColor
self.addSubview(checkmark)
checkmark.tintColor = color
checkmark.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// constrain image view to all 4 sides
checkmark.topAnchor.constraint(equalTo: topAnchor),
checkmark.leadingAnchor.constraint(equalTo: leadingAnchor),
checkmark.trailingAnchor.constraint(equalTo: trailingAnchor),
checkmark.bottomAnchor.constraint(equalTo: bottomAnchor),
])
let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
self.addGestureRecognizer(gesture)
}
#objc func toggle() {
self.isChecked.toggle()
}
}
We've used auto-layout to keep the image view the same size as the view itself.
And, by implementing the var isChecked block we have a more "automated" way of setting the image view's hidden state.
In addition, we can now get the "state" of the checkbox in the controller like this:
if thisCheckBox.isChecked {
// do something
}
Now the view controller... we'll define an array of colors, loop through them to create the CheckBox objects, and add them to the stack view:
class SettingsViewController: UIViewController {
var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
// desired spacing
stackView.spacing = 12
stackView.alignment = .fill
stackView.distribution = .fillEqually
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// stack view Top constraint
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 150.0),
// centered horizontally
stackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// explicit Height
stackView.heightAnchor.constraint(equalToConstant: 30.0),
])
let colors: [UIColor] = [
.red, .green, .black, .gray,
.brown, .yellow, .purple, .orange,
]
// loop through the colors, creating a new
// CheckBox object for each color
// and add it to the stack view
colors.forEach { c in
let checkbox = CheckBox(frame: .zero, color: c)
// 1:1 aspect ratio
checkbox.widthAnchor.constraint(equalTo: checkbox.heightAnchor).isActive = true
stackView.addArrangedSubview(checkbox)
}
}
}
As you can see, we've eliminated the need for all of the individual
lazy var redCheckbox: CheckBox = { ...
lazy var greenCheckbox: CheckBox = { ...
// etc
code blocks.

How to colour radio buttons?

You can see that Apple has coloured the radio buttons. I would like to do the same. I can't seems to find the option to change the colour in Interface Builder on storyboard.
As for doing it, programmatically, I tried enabling layer .wantsLayer = true and then tried to set the colour by .layer?.borderColour = NSColor.systemBlue.cgColor and tried .layer?.backgroundColor = NSColor.systemRed.cgColor and other similar properties but no avail.
Likewise, how do you add the colour rectangles on NSMenuItem on NSPopUpButton?
The following code demonstrates a group of custom radio buttons for MacOS made by subclassing NSButton. It may be run in an Xcode swift project by copy/pasting into a newly added file called ‘main.swift’ and deleting the original AppDelegate.
import Cocoa
class CustomButton: NSButton {
var circleColor: NSColor!
override func draw(_ rect: NSRect) {
let circle = NSBezierPath(ovalIn: bounds)
switch(self.tag) {
case 0:
circleColor = NSColor.red
case 1:
circleColor = NSColor.green
case 2:
circleColor = NSColor.yellow
case 3:
circleColor = NSColor.orange
default:
break
}
circleColor.set()
circle.fill()
if(self.state) == .on {
let dotRect = NSInsetRect(bounds, 18.0, 18.0);
let dot = NSBezierPath (ovalIn:dotRect)
let dotColor = NSColor.black
dotColor.set()
dot.fill()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
#objc func radioGrpAction(_ sender:NSButton) {
print("You selected: id = \(sender.tag)")
}
func buildMenu() {
let mainMenu = NSMenu()
NSApp.mainMenu = mainMenu
// **** App menu **** //
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenuItem.submenu = appMenu
appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q")
}
func buildWnd() {
let _wndW : CGFloat = 300
let _wndH : CGFloat = 200
window = NSWindow(contentRect:NSMakeRect(0,0,_wndW,_wndH),styleMask:[.titled, .closable, .miniaturizable], backing:.buffered, defer:false)
window.center()
window.title = "Radio Button Group"
window.makeKeyAndOrderFront(window)
// === Radio Grp Box === //
let grpBox = NSBox(frame: NSMakeRect( 50,_wndH - 100, 150, 60))
grpBox.title = "Radio Group"
window.contentView!.addSubview (grpBox)
// === Radio Horizontal Grid === //
let _btnW : CGFloat = 24
let _btnH : CGFloat = 24
let _left : CGFloat = 10 // left margin first button
let _YOffset : CGFloat = 5 // 0,0 at left, bottom of group box
let _spacing : CGFloat = 5 // spacing between buttons
for x in stride(from:0, through:3, by:1) {
let _XOffset = _left + CGFloat(x)*(_btnW + _spacing)
let btn = CustomButton(frame:NSMakeRect(_XOffset, _YOffset, _btnW, _btnH))
btn.setButtonType(.radio)
btn.tag = x
if(x == 0){btn.state = .on}
btn.action = #selector(self.radioGrpAction(_:))
grpBox.contentView!.addSubview(btn)
}
// === Quit btn === //
let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
quitBtn.bezelStyle = .circular
quitBtn.title = "Q"
quitBtn.action = #selector(NSApplication.terminate)
window.contentView!.addSubview(quitBtn)
}
func applicationDidFinishLaunching(_ notification: Notification) {
buildMenu()
buildWnd()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
let appDelegate = AppDelegate()
// ***** main.swift ***** //
let app = NSApplication.shared
app.setActivationPolicy(.regular)
app.delegate = appDelegate
app.activate(ignoringOtherApps:true)
app.run()
Ok the same but for AppKit:
import Cocoa
import AppKit
extension NSView {
func centerX(inView view: NSView, constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: constant).isActive = true
}
func centerY(inView view: NSView, constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: constant).isActive = true
}
func setDimensions(height: CGFloat, width: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
widthAnchor.constraint(equalToConstant: width).isActive = true
}
}
class CustomRadioButton: NSView {
private let containerSize: CGFloat = 60.0
private let selectorSize: CGFloat = 20.0
var containerColor: NSColor = .blue
var selectorColor: NSColor = .red
var selected: Bool = true {
didSet {
selectorView.isHidden = !selected
}
}
private lazy var containerView: NSView = {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = containerColor.cgColor
return view
}()
private lazy var selectorView: NSView = {
let view = NSView()
view.wantsLayer = true
view.layer?.backgroundColor = selectorColor.cgColor
return view
}()
override init(frame: CGRect) {
super.init(frame: CGRect(x: 0, y: 0, width: containerSize, height: containerSize))
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureUI() {
addSubview(containerView)
containerView.setDimensions(height: containerSize, width: containerSize)
containerView.layer?.cornerRadius = containerSize / 2
containerView.centerX(inView: self)
containerView.centerY(inView: self)
addSubview(selectorView)
selectorView.setDimensions(height: selectorSize, width: selectorSize)
selectorView.layer?.cornerRadius = selectorSize / 2
selectorView.centerY(inView: containerView)
selectorView.centerX(inView: containerView)
}
}
let selector = CustomRadioButton()
selector.selected = false

How dynamic cell height increase using card view in swift

How to resolve the cell height and based on the text dynamic text
My code:
class NotificationTableViewCell: UITableViewCell {
var notification:Notifications? {
didSet {
guard let notificationList = notification else {return}
if let title = notificationList.title{
titleLabel.text = title
}
if let message = notificationList.Message {
descriptionLabel.text = " \(message) "
}
}
}
let ContentView = UIViewFactory()
.build()
//MARK: - cell title Label
let titleLabel = CustomLabel(text: "")
.changeNumberOfLines(lines: 1)
.buildUI()
//MARK: - cell description Label
let descriptionLabel = CustomLabel(text: "")
.changeTextAlignment(.left)
.changeLineBreakMode(mode: .byWordWrapping)
.changeFont(12)
.buildUI()
//MARK: - Time Label
let timeLabel = CustomLabel(text: "")
.buildUI()
//MARK: - IDPal Logo
let logo : UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "idpal")
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
logo.contentMode = .scaleAspectFit
selectionStyle = .none
setUp()
}
//MARK: - SetUpViews
func setUp() {
addSubview(logo)
addSubview(titleLabel)
addSubview(descriptionLabel)
setUpConstraints()
}
override func layoutSubviews() {
super.layoutSubviews()
let cornerRadius: CGFloat = 10
contentView.clipsToBounds = true
contentView.layer.masksToBounds = false
contentView.layer.shadowColor = UIColor(red: 0.74, green: 0.74, blue: 0.74, alpha: 0.50).cgColor
contentView.layer.cornerRadius = cornerRadius
contentView.backgroundColor = UIColor(red: 1.00, green: 1.00, blue: 1.00, alpha: 1)
contentView.layer.borderWidth = 1.0
contentView.layer.borderColor = UIColor.lightGray.cgColor
contentView.alpha = 1
contentView.frame = contentView.frame.inset(by: UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 0))
//set the values for top,left,bottom,right margins
// let margins = UIEdgeInsets(top: 3, left: 0, bottom: 10, right: 0)
// contentView.frame = contentView.frame.inset(by: margins)
}
func setUpConstraints() {
// containerView.topAnchor.constraint(equalTo:topAnchor).isActive = true
// containerView.leftAnchor.constraint(equalTo:leftAnchor).isActive = true
// containerView.rightAnchor.constraint(equalTo:rightAnchor).isActive = true
// containerView.bottomAnchor.constraint(equalTo:bottomAnchor).isActive = true
//MARK: - IDPal Logo constraints
logo.topAnchor.constraint(equalTo:topAnchor,constant:10).isActive = true
logo.leftAnchor.constraint(equalTo:leftAnchor,constant:14).isActive = true
logo.widthAnchor.constraint(equalToConstant:30).isActive = true
logo.heightAnchor.constraint(equalToConstant:40).isActive = true
//MARK: - titleLabel constraints
titleLabel.topAnchor.constraint(equalTo:logo.topAnchor,constant:0).isActive = true
titleLabel.leftAnchor.constraint(equalTo:logo.rightAnchor,constant:10).isActive = true
titleLabel.rightAnchor.constraint(equalTo:rightAnchor).isActive = true
//MARK: - descriptionLabel constraints
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
descriptionLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor).isActive = true
descriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor,constant:10).isActive = true
descriptionLabel.rightAnchor.constraint(equalTo: titleLabel.rightAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Hear my code can't able to configure if found any suggestion drop here please.
adding the border with it's to be space between contendview but not working for me.
I would like to give shadow effect like card similar to the image in my iOS app
thanks

Create UIButtons with dynamic font size but all share same font size in UIStackView

I am using UIStackView and adding three buttons to it. I want it so that the button with the most text (B1) will be auto resized to fit the width and the other buttons will share the same font size as B1.
#IBOutlet weak var stackView: UIStackView!
var btnTitles = [String]()
btnTitles.append("Practice Exams")
btnTitles.append("Test Taking Tips")
btnTitles.append("About")
createButtons(buttonTitles: btnTitles)
var min = CGFloat(Int.max) // keep track of min font
func createButtons(buttonTitles: [String]) {
var Buttons = [UIButton]()
for title in buttonTitles {
let button = makeButtonWithText(text: title)
// set the font to dynamically size
button.titleLabel!.numberOfLines = 1
button.titleLabel!.adjustsFontSizeToFitWidth = true
button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
button.contentEdgeInsets = UIEdgeInsetsMake(5, 10, 5, 10); // set margins
if (button.titleLabel?.font.pointSize)! < min {
min = (button.titleLabel?.font.pointSize)! // to get the minimum font size of any of the buttons
}
stackView.addArrangedSubview(button)
Buttons.append(button)
}
}
func makeButtonWithText(text:String) -> UIButton {
var myButton = UIButton(type: UIButtonType.system)
//Set a frame for the button. Ignored in AutoLayout/ Stack Views
myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
// background color - light blue
myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)
//State dependent properties title and title color
myButton.setTitle(text, for: UIControlState.normal)
myButton.setTitleColor(UIColor.white, for: UIControlState.normal)
// set the font to dynamically size
myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
myButton.contentHorizontalAlignment = .center // align center
return myButton
}
I wanted to find the minimum font size and then set all the buttons to the minimum in viewDidAppear button the font prints as 70 for all of them even though they clearly appear different sizes (see image)
override func viewDidAppear(_ animated: Bool) {
print("viewDidAppear")
let button = stackView.arrangedSubviews[0] as! UIButton
print(button.titleLabel?.font.pointSize)
let button1 = stackView.arrangedSubviews[1] as! UIButton
print(button1.titleLabel?.font.pointSize)
let button2 = stackView.arrangedSubviews[2] as! UIButton
print(button2.titleLabel?.font.pointSize)
}
image
You can try playing around with this func to return the scaled-font-size of a label:
func actualFontSize(for aLabel: UILabel) -> CGFloat {
// label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
guard let str = aLabel.text,
aLabel.minimumScaleFactor > 0.0,
aLabel.adjustsFontSizeToFitWidth
else { return aLabel.font.pointSize }
let attributes = [NSAttributedString.Key.font : aLabel.font]
let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])
let context = NSStringDrawingContext()
context.minimumScaleFactor = aLabel.minimumScaleFactor
_ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)
return aLabel.font.pointSize * context.actualScaleFactor
}
On viewDidAppear() you would loop through the buttons, getting the smallest actual font size, then set the font size for each button to that value.
It will take some experimentation... For one thing, I've noticed in the past that font-sizes can get rounded - so setting a label's font point size to 20.123456789 won't necessarily give you that exact point size. Also, since this changes the actual font size assigned to the labels, you'll need to do some resetting if you change the button title dynamically. Probably also need to account for button frame changes (such as with device rotation, etc).
But... here is a quick test that you can run to see the approach:
class TestViewController: UIViewController {
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.alignment = .center
v.distribution = .fillEqually
v.spacing = 8
return v
}()
var btnTitles = [String]()
var theButtons = [UIButton]()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fixButtonFonts()
}
func setupUI() -> Void {
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40),
])
btnTitles.append("Practice Exams")
btnTitles.append("Test Taking Tips")
btnTitles.append("About")
createButtons(buttonTitles: btnTitles)
}
func fixButtonFonts() -> Void {
var minActual = CGFloat(70)
// get the smallest actual font size
theButtons.forEach { btn in
if let lbl = btn.titleLabel {
let act = actualFontSize(for: lbl)
// for debugging
//print("actual font size: \(act)")
minActual = Swift.min(minActual, act)
}
}
// set font size for each button
theButtons.forEach { btn in
if let lbl = btn.titleLabel {
lbl.font = lbl.font.withSize(minActual)
}
}
}
func createButtons(buttonTitles: [String]) {
for title in buttonTitles {
let button = makeButtonWithText(text: title)
// set the font to dynamically size
button.titleLabel!.numberOfLines = 1
button.titleLabel!.adjustsFontSizeToFitWidth = true
// .minimumScaleFactor is required
button.titleLabel!.minimumScaleFactor = 0.05
button.titleLabel!.baselineAdjustment = .alignCenters // I think it keeps it centered vertically
button.contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10); // set margins
stackView.addArrangedSubview(button)
theButtons.append(button)
}
}
func makeButtonWithText(text:String) -> UIButton {
let myButton = UIButton(type: UIButton.ButtonType.system)
//Set a frame for the button. Ignored in AutoLayout/ Stack Views
myButton.frame = CGRect(x: 30, y: 30, width: 150, height: 100)
// background color - light blue
myButton.backgroundColor = UIColor(red: 0.255, green: 0.561, blue: 0.847, alpha: 1)
//State dependent properties title and title color
myButton.setTitle(text, for: UIControl.State.normal)
myButton.setTitleColor(UIColor.white, for: UIControl.State.normal)
// set the font to dynamically size
myButton.titleLabel!.font = myButton.titleLabel!.font.withSize(70)
myButton.contentHorizontalAlignment = .center // align center
return myButton
}
func actualFontSize(for aLabel: UILabel) -> CGFloat {
// label must have text, must have .minimumScaleFactor and must have .adjustsFontSizeToFitWidth == true
guard let str = aLabel.text,
aLabel.minimumScaleFactor > 0.0,
aLabel.adjustsFontSizeToFitWidth
else { return aLabel.font.pointSize }
let attributes = [NSAttributedString.Key.font : aLabel.font]
let attStr = NSMutableAttributedString(string:str, attributes:attributes as [NSAttributedString.Key : Any])
let context = NSStringDrawingContext()
context.minimumScaleFactor = aLabel.minimumScaleFactor
_ = attStr.boundingRect(with: aLabel.bounds.size, options: .usesLineFragmentOrigin, context: context)
return aLabel.font.pointSize * context.actualScaleFactor
}
}
Result:

How to set top left and right corner radius with desired drop shadow in UITabbar?

I've spent almost a couple of hours to figure it out. However, it did not happen and finally, I had to come here. Two things are required to be achieved:
Firstly I'd like to have a spontaneous corner radius at the top (which is basically TopRight & TopLeft) of UITabbar.
Secondly, I'd like to have a shadow above those corner radius(shown in below image).
Please have a look at below image
Let me know if anything further required from my side, I'll surely provide that.
Any help will be appreciated.
Edit 1
One more little question arose here along, suppose, Even if, However, we were able to accomplish this, Would Apple review team accept the application?
I'm being little nervous and curious about it.
Q : One more little question arose here along, suppose, Even if, However, we were able to accomplish this, Would Apple review team accept the application?
A: Yes They are accept your app I have Add This Kind Of TabBar.
Create Custom TabBar
HomeTabController
import UIKit
class HomeTabController: UITabBarController
{
var viewCustomeTab : CustomeTabView!
var lastSender : UIButton!
//MARK:- ViewController Methods
override func viewDidLoad()
{
super.viewDidLoad()
UITabBar.appearance().shadowImage = UIImage()
allocateTabItems()
}
//MARK:- Prepare Methods
// Allocate shop controller with tab bar
func allocateTabItems()
{
let vc1 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Avc") as? Avc
let item1 = UINavigationController(rootViewController: vc1!)
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.viewControllers = [item1]
createTabBar()
}
func createTabBar()
{
viewCustomeTab = CustomeTabView.instanceFromNib()
viewCustomeTab.translatesAutoresizingMaskIntoConstraints = false
viewCustomeTab.call()
self.view.addSubview(viewCustomeTab)
if #available(iOS 11, *)
{
let guide = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([guide.bottomAnchor.constraint(equalToSystemSpacingBelow: viewCustomeTab.bottomAnchor, multiplier: 0), viewCustomeTab.leadingAnchor.constraint(equalToSystemSpacingAfter: guide.leadingAnchor, multiplier: 0), guide.trailingAnchor.constraint(equalToSystemSpacingAfter: viewCustomeTab.trailingAnchor, multiplier: 0), viewCustomeTab.heightAnchor.constraint(equalToConstant: 70) ])
}
else
{
let standardSpacing: CGFloat = 0
NSLayoutConstraint.activate([viewCustomeTab.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor, constant: standardSpacing), bottomLayoutGuide.topAnchor.constraint(equalTo: viewCustomeTab.bottomAnchor, constant: standardSpacing)
])
}
viewCustomeTab.btnTab1.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab2.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab3.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab4.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab5.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
//self.view.layoutIfNeeded()
viewCustomeTab.layoutIfNeeded()
viewCustomeTab.btnTab1.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab2.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab3.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab4.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab5.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab1.isSelected = true
}
//MARK:- Button Click Actions
//Manage Tab From Here
func setSelect(sender:UIButton)
{
viewCustomeTab.btnTab1.isSelected = false
viewCustomeTab.btnTab2.isSelected = false
viewCustomeTab.btnTab3.isSelected = false
viewCustomeTab.btnTab4.isSelected = false
viewCustomeTab.btnTab5.isSelected = false
sender.isSelected = true
}
#objc func buttonTabClickAction(sender:UIButton)
{
//self.selectedIndex = sender.tag
if sender.tag == 0
{
let vc1 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Bvc") as? Bvc
let item1 = UINavigationController(rootViewController: vc1!)
item1.navigationBar.isHidden = false
self.viewControllers = [item1]
setSelect(sender: viewCustomeTab.btnTab1)
return
}
if sender.tag == 1
{
let vc2 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Cvc") as? Cvc
let item2 = UINavigationController(rootViewController: vc2!)
item2.navigationBar.isHidden = false
item2.navigationBar.isTranslucent = false
self.viewControllers = [item2]
setSelect(sender: viewCustomeTab.btnTab2)
return
}
if sender.tag == 2
{
let vc3 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Dvc") as? Dvc
let item3 = UINavigationController(rootViewController: vc3!)
item3.navigationBar.isHidden = false
item3.navigationBar.isTranslucent = false
self.viewControllers = [item3]
setSelect(sender: viewCustomeTab.btnTab3)
return
}
if sender.tag == 3
{
}
if sender.tag == 4
{
}
}
}
Create Custom View For Shadow Effect and For + Button.
import UIKit
class CustomeTabView: UIView
{
#IBOutlet weak var btnTab5: UIButton!
#IBOutlet weak var btnTab4: UIButton!
#IBOutlet weak var btnTab3: UIButton!
#IBOutlet weak var btnTab2: UIButton!
#IBOutlet weak var btnTab1: UIButton!
#IBOutlet weak var vRadius: UIView!
class func instanceFromNib() -> CustomeTabView
{
return UINib(nibName: "CustomeTabView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! CustomeTabView
}
private var shadowLayer: CAShapeLayer!
override func layoutSubviews()
{
super.layoutSubviews()
let shadowSize : CGFloat = 2.0
let shadowPath = UIBezierPath(roundedRect: CGRect(x: -shadowSize / 2, y: -shadowSize / 2, width: self.vRadius.frame.size.width, height: self.vRadius.frame.size.height), cornerRadius : 20)
self.vRadius.layer.masksToBounds = false
self.vRadius.layer.shadowColor = UIColor.black.cgColor
self.vRadius.layer.shadowOffset = CGSize.zero//(width: self.vRadius.frame.size.width, height: self.vRadius.frame.size.height)
self.vRadius.layer.shadowOpacity = 0.5
self.vRadius.layer.shadowPath = shadowPath.cgPath
self.vRadius.layer.cornerRadius = 20
}
OpenImg
Swift 4.2
You can achieve this with some custom view with a custom tab bar controller. You can customize the colors and shadows by editing only the custom views.
Custom Tab Bar Controller
import UIKit
class MainTabBarController: UITabBarController{
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
tabBar.backgroundImage = UIImage.from(color: .clear)
tabBar.shadowImage = UIImage()
let tabbarBackgroundView = RoundShadowView(frame: tabBar.frame)
tabbarBackgroundView.cornerRadius = 25
tabbarBackgroundView.backgroundColor = .white
tabbarBackgroundView.frame = tabBar.frame
view.addSubview(tabbarBackgroundView)
let fillerView = UIView()
fillerView.frame = tabBar.frame
fillerView.roundCorners([.topLeft, .topRight], radius: 25)
fillerView.backgroundColor = .white
view.addSubview(fillerView)
view.bringSubviewToFront(tabBar)
}
Rounded Shadow View
import UIKit
class RoundShadowView: UIView {
let containerView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
layoutView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layoutView() {
// set the shadow of the view's layer
layer.backgroundColor = UIColor.clear.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: -8.0)
layer.shadowOpacity = 0.12
layer.shadowRadius = 10.0
containerView.layer.cornerRadius = cornerRadius
containerView.layer.masksToBounds = true
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
// pin the containerView to the edges to the view
containerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
}
UIImage extension
import UIKit
extension UIImage {
static func from(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor)
context!.fill(rect)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
}
To add any radius or shape you can use a UIBezierPath. The example that I put has left and right corners with a radius and you can use more designable personalizations if you want.
#IBDesignable class TabBarWithCorners: UITabBar {
#IBInspectable var color: UIColor?
#IBInspectable var radii: CGFLoat = 15.0
private var shapeLayer: CALayer?
override func draw(_ rect: CGRect) {
addShape()
}
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.gray.withAlphaComponent(0.1).cgColor
shapeLayer.fillColor = color?.cgColor ?? UIColor.white.cgColor
shapeLayer.lineWidth = 1
if let oldShapeLayer = self.shapeLayer {
layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
private func createPath() -> CGPath {
let path = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: radii, height: 0.0))
return path.cgPath
}
}
Swift 5.3.1, XCode 11+, iOS 14
For using in storyboards:
import UIKit
class CustomTabBar: UITabBar {
override func awakeFromNib() {
super.awakeFromNib()
layer.masksToBounds = true
layer.cornerRadius = 20
layer.maskedCorners = [.layerMinXMinYCorner,.layerMaxXMinYCorner]
}
}
Subclassing UITabBarController then overried viewWillLayoutSubviews()
and add this code .
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.tabBar.layer.masksToBounds = true
self.tabBar.layer.cornerRadius = 12 // whatever you want
self.tabBar.layer.maskedCorners = [.layerMinXMinYCorner,.layerMaxXMinYCorner] // only the top right and left corners
}
This will be the result