NSGridView difficulties - swift

I'm using an NSGridView in the settings panel for my macOS app. I set it up like this:
class GeneralViewController: RootViewController {
private var gridView: NSGridView?
override func loadView() {
super.loadView()
let restaurantLabel = NSTextField()
restaurantLabel.stringValue = "Restaurant"
let restaurantButton = NSPopUpButton()
restaurantButton.addItems(withTitles: ["A", "B", "C"])
let updatesLabel = NSTextField()
updatesLabel.stringValue = "Updates"
updatesLabel.isEditable = false
updatesLabel.isBordered = false
updatesLabel.isBezeled = false
let updatesButton = NSButton(checkboxWithTitle: "Automatically pull in updates from WordPress", target: nil, action: nil)
let empty = NSGridCell.emptyContentView
gridView = NSGridView(views: [
[restaurantLabel, restaurantButton],
[updatesLabel, updatesButton]
])
gridView?.wantsLayer = true
gridView?.layer?.backgroundColor = NSColor.red.cgColor
gridView?.column(at: 0).xPlacement = .trailing
gridView?.rowAlignment = .firstBaseline
gridView?.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 600), for: .horizontal)
gridView?.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 600), for: .vertical)
view.addSubview(gridView!)
}
override func viewDidLayout() {
super.viewDidLayout()
gridView?.frame = NSRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
}
}
Now, when I run this, the NSGridView fills up the whole view, but the checkbox is really off. As you can see in the image.
Also, I'd love for the content to not fill the whole cell, making it all look a tad more centered.
How can I solve this?

If you change the row alignment to .lastBaseline it will fix your issue of the vertical alignment of the NSButton and the NSTextField. If you want more spacing (I'm guessing that's what you mean with "I'd love for the content to not fill the whole cell"), you can use the columnSpacing and rowSpacing properties of the NSGridView. If you then also add instances of NSGridCell.emptyContentView around your "real" content, you should be able to achieve the following effect.
This is new code after the changes I suggested:
class GeneralViewController: RootViewController {
private var gridView: NSGridView?
override func loadView() {
super.loadView()
let restaurantLabel = NSTextField()
restaurantLabel.stringValue = "Restaurant"
let restaurantButton = NSPopUpButton()
restaurantButton.addItems(withTitles: ["A", "B", "C"])
let updatesLabel = NSTextField()
updatesLabel.stringValue = "Updates"
updatesLabel.isEditable = false
updatesLabel.isBordered = false
updatesLabel.isBezeled = false
let updatesButton = NSButton(checkboxWithTitle: "Automatically pull in updates from WordPress", target: nil, action: nil)
let empty = NSGridCell.emptyContentView
gridView = NSGridView(views: [
[empty],
[empty, restaurantLabel, restaurantButton, empty],
[empty, updatesLabel, updatesButton, empty],
[empty],
[empty]
])
gridView?.wantsLayer = true
gridView?.layer?.backgroundColor = NSColor.red.cgColor
gridView?.column(at: 0).xPlacement = .trailing
gridView?.rowAlignment = .lastBaseline
gridView?.columnSpacing = 20
gridView?.rowSpacing = 20
gridView?.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 600), for: .horizontal)
gridView?.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 600), for: .vertical)
view.addSubview(gridView!)
}
override func viewDidLayout() {
super.viewDidLayout()
gridView?.frame = NSRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
}
}

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.

I want to make an if statement that each image would be full screen when the user is tap

here is the code that I check if they images are tapped but only the 3 image is apply full screen. As you can see I set the image is tapped to statusImageView to call the function of zoomImage So I want fixed for all images.
func imageTapped() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageZoom(tapGestureRecognizer:)))
if detailsImage[0].tag == 0 {
detailsImage[0].isUserInteractionEnabled = true
detailsImage[0].addGestureRecognizer(tapGestureRecognizer)
self.statusImageView = detailsImage[0]
}
let tapGestureRecognizer1 = UITapGestureRecognizer(target: self, action: #selector(imageZoom(tapGestureRecognizer:)))
detailsImage[1].isUserInteractionEnabled = true
detailsImage[1].addGestureRecognizer(tapGestureRecognizer1)
self.statusImageView = detailsImage[1]
let tapGestureRecognizer2 = UITapGestureRecognizer(target: self, action: #selector(imageZoom(tapGestureRecognizer:)))
detailsImage[2].isUserInteractionEnabled = true
detailsImage[2].addGestureRecognizer(tapGestureRecognizer2)
self.statusImageView = detailsImage[2]
}
let blackBackgroundColor = UIView()
let tappedImage = UIImageView()
var statusImageView: UIImageView?
let navigationBarView = UIView()
#objc func imageZoom(tapGestureRecognizer: UITapGestureRecognizer) {
annimationImage(detailsImage: statusImageView!)
}
And then I call the annihilationImage function which is takes the statusImageView as input
func annimationImage(detailsImage: UIImageView) {
if let startingFrame = statusImageView?.superview?.convert(statusImageView!.frame, to: nil) {
statusImageView!.alpha = 0
blackBackgroundColor.frame = self.view.frame
blackBackgroundColor.backgroundColor = UIColor.black
blackBackgroundColor.alpha = 0
view.addSubview(blackBackgroundColor)
navigationBarView.frame = CGRect(x: 0, y: 0, width: 1000, height: 100)
navigationBarView.backgroundColor = UIColor.black
navigationBarView.alpha = 0
if let keyWindow = UIApplication.shared.keyWindow {
keyWindow.addSubview(navigationBarView)
}
let tappedImage = UIImageView()
tappedImage.backgroundColor = .gray
tappedImage.frame = startingFrame
tappedImage.contentMode = .scaleToFill
tappedImage.isUserInteractionEnabled = true
tappedImage.image = statusImageView?.image
tappedImage.clipsToBounds = true
view.addSubview(tappedImage)
UIView.animate(withDuration: 0.75, animations: {
tappedImage.frame = CGRect(x: 0, y: y, width: self.view.frame.size.width, height: 300)
})
}
}
I want to make an if statement that while check which image is tapped
You can use tapGestureRecognizer.view for getting a view that is assigned a tap gesture. And by this, you can also get the view tag (your view is corresponding to the image view in your code)
#objc func imageZoom(tapGestureRecognizer: UITapGestureRecognizer) {
let senderView = tapGestureRecognizer.view // Here you get a view that is associated with the gesture
let tag = senderView?.tag // Here you get a tag that is assigned to all image
// Add your condition here by tag
annimationImage(detailsImage: statusImageView!)
}

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 do I prevent one UIView from being hidden by another UIView?

I'm creating a custom, reusable segmented controller using UIViews and I'm having a problem with overlapping views. It currently looks like this:
You can see that the blue selector is under the buttons but I want it to sit at the bottom and be four pixels high. To do this, I have:
let numberOfButtons = CGFloat(buttonTitles.count)
let selectorWidth = frame.width / numberOfButtons
let selectorYPosition = frame.height - 3 <--- This cause it to be hidden behind the button
selector = UIView(frame: CGRect(x: 0, y: selectorYPosition, width: selectorWidth, height: 4))
selector.layer.cornerRadius = 0
selector.backgroundColor = selectorColor
addSubview(selector)
bringSubviewToFront(selector) <--- I thought this would work but it does nothing
which results in the selector UIView being hidden behind the segment UIView (I have the Y position set to - 3 so you can see how it's being covered up. I actually want it to be - 4, but that makes it disappear entirely):
I thought using bringSubviewToFront() would bring it in front of the segment UIView but it doesn't seem to do anything. I've looked through Apple View Programming Guide and lots of SO threads but can't find an answer.
Can anybody help me see what I'm missing?
Full code:
class CustomSegmentedControl: UIControl {
var buttons = [UIButton]()
var selector: UIView!
var selectedButtonIndex = 0
var borderWidth: CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
var borderColor: UIColor = UIColor.black {
didSet {
layer.borderColor = borderColor.cgColor
}
}
var separatorBorderColor: UIColor = UIColor.lightGray {
didSet {
}
}
var commaSeparatedTitles: String = "" {
didSet {
updateView()
}
}
var textColor: UIColor = .lightGray {
didSet {
updateView()
}
}
var selectorColor: UIColor = .blue {
didSet {
updateView()
}
}
var selectorTextColor: UIColor = .black {
didSet {
updateView()
}
}
func updateView() {
buttons.removeAll()
subviews.forEach { $0.removeFromSuperview() }
// create buttons
let buttonTitles = commaSeparatedTitles.components(separatedBy: ",")
for buttonTitle in buttonTitles {
let button = UIButton(type: .system)
button.setTitle(buttonTitle, for: .normal)
button.setTitleColor(textColor, for: .normal)
button.backgroundColor = UIColor.white
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
buttons.append(button)
}
// make first button selected
buttons[0].setTitleColor(selectorTextColor, for: .normal)
let numberOfButtons = CGFloat(buttonTitles.count)
let selectorWidth = frame.width / numberOfButtons
let selectorYPosition = frame.height - 3
selector = UIView(frame: CGRect(x: 0, y: selectorYPosition, width: selectorWidth, height: 4))
selector.layer.cornerRadius = 0
selector.backgroundColor = selectorColor
addSubview(selector)
bringSubviewToFront(selector)
let stackView = UIStackView(arrangedSubviews: buttons)
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fillEqually
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
stackView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
}
#objc func buttonTapped(button: UIButton) {
for (buttonIndex, btn) in buttons.enumerated() {
btn.setTitleColor(textColor, for: .normal)
if btn == button {
let numberOfButtons = CGFloat(buttons.count)
let selectorStartPosition = frame.width / numberOfButtons * CGFloat(buttonIndex)
UIView.animate(withDuration: 0.3, animations: { self.selector.frame.origin.x = selectorStartPosition })
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
sendActions(for: .valueChanged)
}
}
You are covering up your selector with the stackView.
You need to do:
bringSubviewToFront(selector)
after you have added all of the views. Move that line to the bottom of updateView().

Button becomes inactive in programmatic dynamic stackView (Swift)

I'm trying to implement a programmatic version of the Dynamic Stack View in Apple's Auto Layout Cookbook. An "Add Item" button is supposed to add new views to a vertical stackView, including a delete button to remove each view. My programmatic code works fine for 1 touch of the "Add Item" button, but then that button becomes inactive. As a result, I can only add 1 item to the stackView. If I used the delete button, the "Add Item" becomes active again. I've included an animated gif to illustrate.
I'm posting both my Programmatic code (which has the problem) and below that the original storyboard-based code (which works fine). I've tried putting a debug breakpoint at the addEntry func, but that didn't help. -Thanks
Programmatic Code ("Add Item" button only works once):
import UIKit
class CodeDynamStackVC: UIViewController {
// MARK: Properties
var scrollView = UIScrollView()
var stackView = UIStackView()
var button = UIButton()
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
setupInitialVertStackView()
}
//setup initial button inside vertical stackView
func setupInitialVertStackView() {
// make inital "Add Item" button
button = UIButton(type: .system)
button.setTitle("Add Item", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.addTarget(self, action: #selector(addEntry), for: .touchUpInside)
//enclose button in a vertical stackView
stackView.addArrangedSubview(button)
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .equalSpacing
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let viewsDictionary = ["v0":stackView]
let stackView_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
let stackView_V = NSLayoutConstraint.constraints(withVisualFormat: "V:|-20-[v0(25)]|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary)
view.addConstraints(stackView_H)
view.addConstraints(stackView_V)
}
// MARK: Interface Builder actions
func addEntry() {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stackView entry to place within the parent vertical stackView
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fill
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
Storyboard-based Code ("Add Item" button always works)
import UIKit
class DynamStackVC: UIViewController {
// MARK: Properties
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var stackView: UIStackView!
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview.
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
// MARK: Interface Builder actions
#IBAction func addEntry(_: AnyObject) {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stack view entry to place within the parent `stackView`.
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fillProportionally
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
I figured it out by putting background colors on all buttons and labels within the dynamic stackView. As you can see in the new animated gif, the cyan color of the "Add Item" button disappears after the first button press. To confirm, I doubled the original height of the button (i.e., from (25) to (50) at the left in the gif), which then allowed for two button pressed before it no longer worked and the cyan background disappeared. This taught me a lot about how the dynamic stackView works, and I hope it will help someone else.