Related
I have a simple iOS App, containing a Scroll View (scroll horizontally, but mainly as described here: Is it possible for UIStackView to scroll?). By default the Horizontal Stack is empty (spacing = Standard, Distribution is Fill, but also Equal Spacing does not improve this), and I add buttons programatically to it (for example purpose I add a random length string, to have various sizes), using:
filterStackView.translatesAutoresizingMaskIntoConstraints = false
let newButtonConfiguration = UIButton.Configuration.filled()
let newButton = UIButton(configuration: newButtonConfiguration)
newButton.translatesAutoresizingMaskIntoConstraints = false
newButton.setTitle("\(randomString(length: Int.random(in: 5..<25)))", for: .normal)
newButton.setImage(UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified)), for: .normal)
newButton.addTarget(self, action: #selector(newButtonPressedAction), for: .touchUpInside)
filterStackView.addArrangedSubview(newButton)
I already tried adding to the button:
newButton.sizeToFit()
newButton.layoutIfNeeded()
or to the View Stack:
filterStackView.sizeToFit()
filterStackView.layoutIfNeeded()
Without any visible change. The weird things happening are:
After adding another one, sizes start changing in a funny way (if buttons are added from Story Board they really respect the width of the text - and Autolayout works fine):
And after playing with adding and removing them for a while:
And in the debug console I see constraints failing badly:
2022-11-01 14:05:48.450435+0100 TestAppSideScroll[20626:640959] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x600001b58c80 'UISV-canvas-connection' H:[UIButton:0x122408660'N6xoAsl']-(0)-| (active, names: '|':UIStackView:0x1505080c0 )>",
"<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>"
)
Will attempt to recover by breaking constraint
<_UISystemBaselineConstraint:0x600001b586e0 'UISV-spacing' H:[UIButton:0x1223095d0'oHSryjPsjrZA']-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x122408660'N6xoAsl'] (active)>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
Which actually makes no sense, since the conflict seems to be the spacing constraint with the connection to the canvas constraint? But that I would expect to happen for every single button I add, if that would be the general problem - however there are no real constraints, except the spacing and the snapping of the scroll view.
Any help would be appreciated.
Yeah... something really weird going on there.
My guess would be that UIKit has some sort of algorithm that is trying to apply "readability" to the buttons. Clearly, though, that's not the goal here.
After some quick testing, one way to get around this is to add the button as a subview to a clear "container" view, and then add that container view to the stack view.
Some sample code:
class SampleViewController: UIViewController {
let filterStackViewA: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let filterStackViewB: UIStackView = {
let v = UIStackView()
v.spacing = 8
v.distribution = .fill
v.alignment = .center
return v
}()
let scrollViewA: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemYellow
return v
}()
let scrollViewB: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .systemOrange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
let addBtn = UIButton()
addBtn.setTitle("Add Button", for: [])
addBtn.setTitleColor(.white, for: .normal)
addBtn.setTitleColor(.lightGray, for: .highlighted)
addBtn.backgroundColor = .systemGreen
addBtn.layer.cornerRadius = 8
let labelA: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added to \"Container\" views..."
return v
}()
let labelB: UILabel = {
let v = UILabel()
v.textColor = .white
v.font = .systemFont(ofSize: 15.0, weight: .light)
v.text = "Buttons added directly to stack view..."
return v
}()
[addBtn, scrollViewA, scrollViewB, filterStackViewA, filterStackViewB, labelA, labelB].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(addBtn)
view.addSubview(labelA)
view.addSubview(scrollViewA)
scrollViewA.addSubview(filterStackViewA)
view.addSubview(labelB)
view.addSubview(scrollViewB)
scrollViewB.addSubview(filterStackViewB)
let g = view.safeAreaLayoutGuide
let cgA = scrollViewA.contentLayoutGuide
let fgA = scrollViewA.frameLayoutGuide
let cgB = scrollViewB.contentLayoutGuide
let fgB = scrollViewB.frameLayoutGuide
NSLayoutConstraint.activate([
addBtn.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
addBtn.widthAnchor.constraint(equalToConstant: 200.0),
addBtn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
labelA.topAnchor.constraint(equalTo: addBtn.bottomAnchor, constant: 40.0),
labelA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.topAnchor.constraint(equalTo: labelA.bottomAnchor, constant: 8.0),
scrollViewA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewA.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewA.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewA.topAnchor.constraint(equalTo: cgA.topAnchor, constant: 4.0),
filterStackViewA.leadingAnchor.constraint(equalTo: cgA.leadingAnchor, constant: 8.0),
filterStackViewA.trailingAnchor.constraint(equalTo: cgA.trailingAnchor, constant: -8.0),
filterStackViewA.bottomAnchor.constraint(equalTo: cgA.bottomAnchor, constant: -4.0),
filterStackViewA.heightAnchor.constraint(equalTo: fgA.heightAnchor, constant: -8.0),
labelB.topAnchor.constraint(equalTo: filterStackViewA.bottomAnchor, constant: 20.0),
labelB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
labelB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.topAnchor.constraint(equalTo: labelB.bottomAnchor, constant: 8.0),
scrollViewB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollViewB.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollViewB.heightAnchor.constraint(equalToConstant: 60.0),
filterStackViewB.topAnchor.constraint(equalTo: cgB.topAnchor, constant: 4.0),
filterStackViewB.leadingAnchor.constraint(equalTo: cgB.leadingAnchor, constant: 8.0),
filterStackViewB.trailingAnchor.constraint(equalTo: cgB.trailingAnchor, constant: -8.0),
filterStackViewB.bottomAnchor.constraint(equalTo: cgB.bottomAnchor, constant: -4.0),
filterStackViewB.heightAnchor.constraint(equalTo: fgB.heightAnchor, constant: -8.0),
])
addBtn.addTarget(self, action: #selector(addButton), for: .touchUpInside)
}
#objc func addButton() {
// unwrap optional system image
guard let img = UIImage(systemName: "xmark.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .unspecified))
else { return }
var newButtonConfiguration = UIButton.Configuration.filled()
newButtonConfiguration.title = randomString(length: Int.random(in: 5..<15))
newButtonConfiguration.image = img
// let's add a new button to a "container" view
let newButtonA = UIButton(configuration: newButtonConfiguration)
newButtonA.translatesAutoresizingMaskIntoConstraints = false
let v = UIView()
v.addSubview(newButtonA)
NSLayoutConstraint.activate([
newButtonA.topAnchor.constraint(equalTo: v.topAnchor),
newButtonA.leadingAnchor.constraint(equalTo: v.leadingAnchor),
newButtonA.trailingAnchor.constraint(equalTo: v.trailingAnchor),
newButtonA.bottomAnchor.constraint(equalTo: v.bottomAnchor),
])
// add the container view to the stack view
filterStackViewA.addArrangedSubview(v)
// let's add a new button directly to the stack view
let newButtonB = UIButton(configuration: newButtonConfiguration)
filterStackViewB.addArrangedSubview(newButtonB)
newButtonA.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
newButtonB.addTarget(self, action: #selector(removeMe(_:)), for: .touchUpInside)
// let's make sure the new button is visible
// note: this is just for example...
// if we rapidly tap and add buttons, this can easily "miss" the last button
// so don't expect this to be "production" code
DispatchQueue.main.async {
let sz = self.scrollViewA.contentSize
let rA = CGRect(x: sz.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewA.scrollRectToVisible(rA, animated: true)
let szB = self.scrollViewB.contentSize
let rB = CGRect(x: szB.width - 1.0, y: 0.0, width: 1.0, height: 1.0)
self.scrollViewB.scrollRectToVisible(rB, animated: true)
}
}
#objc func removeMe(_ sender: UIButton) {
// get a reference to the tapped button's superview
guard let sv = sender.superview else { return }
var v: UIView!
if sv is UIStackView {
// we tapped a button added directly to the stack view
v = sender
} else {
// we tapped a button that's in a "container" view
v = sv
}
guard let st = v.superview as? UIStackView,
let idx = st.arrangedSubviews.firstIndex(of: v),
filterStackViewA.arrangedSubviews.count > idx,
filterStackViewB.arrangedSubviews.count > idx
else {
print("something's not setup right, so return")
return
}
let bA = filterStackViewA.arrangedSubviews[idx]
let bB = filterStackViewB.arrangedSubviews[idx]
// let's animate the buttons away
bA.isHidden = true
bB.isHidden = true
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
}, completion: { _ in
bA.removeFromSuperview()
bB.removeFromSuperview()
})
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
It will look like this when running:
Each tap of "Add Button" will add a new button to each scrolling-stackView, with the top set using buttons in container views. Tapping any of those buttons will remove it from its stack view, along with its "twin" from the other one.
A quick question, for what should've been an easy implementation.
Im trying to implement a UISearchcontroller and the UIsearchbar property when trying to customize it and set constraints behaves properly appears perfect but the minute i click on the search bar it resets its constraints to nil(guessing based on visual debugger).
Before clicking
and here is the second image which shows what happens when clicked
After clicking
Ive been trying to do this for a day now.
Context: My Main VC is a collection view and another button.
below is the search view specific code, I tried isolating the issue in a playground file and noticed issue starts when i add constraints to it.
var searchController:UISearchController!
private func setupSearchView(){
let viewController = UISearchController(searchResultsController: nil)
viewController.delegate = self
let bar = viewController.searchBar
bar.delegate = self
bar.searchBarStyle = .minimal
bar.translatesAutoresizingMaskIntoConstraints=false
bar.searchTextField.layer.cornerRadius = 20
bar.searchTextField.textColor = .darkGray
bar.searchTextField.backgroundColor = .white
bar.showsCancelButton = false
bar.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
bar.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
bar.layer.shadowOpacity = 1.0
bar.layer.shadowRadius = 0.0
bar.layer.masksToBounds = false
guard let customFont = UIFont(name: "Poppins-SemiBold", size: 14.0) else {
fatalError("""
Failed to load the "CustomFont-Light" font.
Make sure the font file is included in the project and the font name is spelled correctly.
"""
)}
bar.searchTextField.font=customFont
self.searchController = viewController
self.view.addSubview(bar)
bar.isHidden = true
}
func setupContstraints() {
//NSLayoutConstrainst
let searchBar:UISearchBar=searchController!.searchBar
NSLayoutConstraint.activate([
searchButton!.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30),
searchButton!.topAnchor.constraint(equalTo: view.topAnchor, constant: 30),
searchButton!.widthAnchor.constraint(equalToConstant: 50),
searchButton!.heightAnchor.constraint(equalToConstant: 50),
//search bar
searchBar.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
searchBar.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 30),
searchBar.widthAnchor.constraint(equalToConstant: 170.0),
searchBar.heightAnchor.constraint(equalToConstant: 50.0)]
)
Update:
When i gave the search bar fixed width and height(not ideal for different device size) it now appears with the width and height but doesn't obey the top anchor constraint.
See current image
also updated the snippet with current constraints
Give a fixed width and height value for your "SearchBar" object. It will probably be fixed.
It may take up as much width as the cursor when clicked.
Comment out or delete the 2 lines below while giving a fixed height and width for the searchBar.
searchBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 50),
searchBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -100)
Rid of the UISearchController and use a UISearchBar instead.
let searchBar = UISearchBar()
let clearButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
setupSearchView()
}
private func setupSearchView(){
searchBar.searchBarStyle = .minimal
searchBar.translatesAutoresizingMaskIntoConstraints=false
searchBar.searchTextField.layer.cornerRadius = 20
searchBar.searchTextField.textColor = .darkGray
searchBar.searchTextField.backgroundColor = .white
searchBar.showsCancelButton = false
searchBar.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
searchBar.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
searchBar.layer.shadowOpacity = 1.0
searchBar.layer.shadowRadius = 0.0
searchBar.layer.masksToBounds = false
// guard let customFont = UIFont(name: "Poppins-SemiBold", size: 14.0) else {
// fatalError("""
// Failed to load the "CustomFont-Light" font.
// Make sure the font file is included in the project and the font name is spelled correctly.
// """
// )}
// bar.searchTextField.font=customFont
// set your search clear button
clearButton.setImage(UIImage(systemName: "multiply.circle.fill"), for: .normal)
clearButton.tintColor = .red
let searchStack = UIStackView(arrangedSubviews: [searchBar, clearButton])
searchStack.axis = .horizontal
searchStack.distribution = .fill
searchStack.alignment = .fill
searchStack.spacing = 16
searchStack.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(searchStack)
// bar.isHidden = true
NSLayoutConstraint.activate([
clearButton.heightAnchor.constraint(equalTo: clearButton.widthAnchor),
searchStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 30),
searchStack.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 30),
searchStack.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -30),
searchStack.heightAnchor.constraint(equalToConstant: 50)
])
}
I have a UI with 3 main parts: header, keypad and a button.
Size of header and button are fixed, the remaining area should be occupied by keypad.
Like this:
The keypad part is build up by a nested UIStackView (vertical UIStackView parent, with 4 horizontal UIStackView children), 3 buttons will be added to each of the horizontal UIStackView.
Everything is fine except I would like the buttons to have the same height as the horizontal UIStackView, so that it should be easier for user to click on it.
I have tried
rowStackView.alignment = .fill
or
for button in numberButtons
{
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalTo: button.superview!.heightAnchor).isActive = true
}
However, the numberPadStackView will be squeezed like following. How should I fix that? Thanks.
These are my codes for now:
headerStackView.axis = .vertical
headerStackView.alignment = .center
headerStackView.setContentHuggingPriority(.defaultHigh, for: .vertical)
headerStackView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
view.addSubview(headerStackView)
numberPadStackView.axis = .vertical
numberPadStackView.distribution = .fillEqually
numberPadStackView.alignment = .center
view.addSubview(numberPadStackView)
initNumpad()
view.addSubview(requestLabel)
initNumPad()
private func initNumpad()
{
var rowStackView = UIStackView()
numberButtons.removeAll()
for i in 0 ..< 11
{
if i % 3 == 0
{
rowStackView = UIStackView()
rowStackView.axis = .horizontal
rowStackView.distribution = .fillEqually
rowStackView.alignment = .center
rowStackView.setContentHuggingPriority(.defaultLow, for: .vertical)
numberPadRowStackView.append(rowStackView)
numberPadStackView.addArrangedSubview(rowStackView)
}
let button = UIButton()
switch i
{
case 0 ..< 9:
button.setTitle("\(i + 1)", for: .normal)
case 9:
button.setTitle(".", for: .normal)
case 10:
button.setTitle("0", for: .normal)
default:
return
}
button.titleLabel?.textAlignment = .center
button.setContentHuggingPriority(.defaultLow, for: .vertical)
button.backgroundColor = UIColor.random()
numberButtons.append(button)
rowStackView.addArrangedSubview(button)
}
numberPadDeleteImageView.backgroundColor = UIColor.random()
rowStackView.addArrangedSubview(numberPadDeleteImageView)
}
Layout
headerStackView.translatesAutoresizingMaskIntoConstraints = false
numberPadStackView.translatesAutoresizingMaskIntoConstraints = false
requestLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
headerStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 24),
headerStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Padding),
headerStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Padding),
numberPadStackView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 43),
numberPadStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Padding),
numberPadStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Padding),
requestLabel.topAnchor.constraint(equalTo: numberPadStackView.bottomAnchor, constant: 21),
requestLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 28),
requestLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -28),
requestLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -5),
requestLabel.heightAnchor.constraint(equalToConstant: 40),
])
for rowStackView in numberPadRowStackView
{
rowStackView.translatesAutoresizingMaskIntoConstraints = false
rowStackView.widthAnchor.constraint(equalTo: numberPadStackView.widthAnchor).isActive = true
}
Couple notes...
Setting Content Hugging Priority on a UIStackView is generally not going to give you the expected results. That's because the stack view is arranging its subviews (based on the stack view's Alignment and Distribution properties). The Content Hugging Priority of the stack view's arranged subviews will be the controlling factor.
As is obvious, if I lay out 4 labels, vertically constrained top-to-bottom, each having the same Content Hugging priority (such as the default 250), I'll get something like this in Storyboard (note the Red layout-problem indicator):
and at runtime it may look like this:
Auto-layout is going to respect the (intrinsic) Height for Labels 1, 2 and 4, and then stretch Label 3 to complete the layout.
If I embed the top two and bottom two labels each in vertical stack views...
Storyboard will look like this (again, note the Red layout-problem indicator):
and we get the same thing at run-time:
Even if I set the Content Hugging Priority of the top stack view to 1000, it won't make a difference -- because auto-layout is using the arranged subviews to decide what to do.
So, for your layout, divide your screen into *three layout elements:
the "header" section
the "numberPad" section
the "process" section
and then tell auto-layout you want the Header and Process elements to maintain their heights, and allow the numberPad to stretch.
Since UI elements default to Hugging Priority of 250, probably the easiest way to manage that is reduce the Hugging Priority of the numberPad buttons.
Here is some example code. I'm not sure how you're laying out your "headerStackView" as it doesn't really look like it would lend itself to a stack view... so, I laid it out as a header UIView:
class WithNumpadViewController: UIViewController {
// three "parts" to our layout
let headerView = UIView()
let numberPadStackView = UIStackView()
let requestLabel = UILabel()
// data labels to be filled-in
let currencyLabel = UILabel()
let currValueLabel = UILabel()
let balanceLabel = UILabel()
// however you're using this
var numberButtons: [UIButton] = []
let Padding: CGFloat = 16
override func viewDidLoad() {
super.viewDidLoad()
if let vc = self.navigationController?.viewControllers.first {
vc.navigationItem.title = "Wallet"
}
self.navigationController?.navigationBar.barTintColor = .black
self.navigationController?.navigationBar.tintColor = .white
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
title = "Withdraw"
// add "three parts" to view
[headerView, numberPadStackView, requestLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain headerView to Top / Leading / Trailing (safe-area)
// let its content determine its height
headerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
headerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
headerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
// constrain numberPad 40-pts from Bottom headerView
numberPadStackView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 40),
// Leading / Trailing with Padding
numberPadStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: Padding),
numberPadStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -Padding),
// constrain requestLabel 21-pts from Bottom of numberPad
requestLabel.topAnchor.constraint(equalTo: numberPadStackView.bottomAnchor, constant: 21),
// Leading / Trailing with 28-pts padding
requestLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 28),
requestLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -28),
// Bottom 5-pts from Bottom (safe-area)
requestLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -5),
// explicit Height of 40-pts
requestLabel.heightAnchor.constraint(equalToConstant: 40),
])
// setup contents of headerView
initHeader()
// setup contents of numberPad
initNumpad()
// properties for requestLabel
requestLabel.backgroundColor = .black
requestLabel.textColor = .white
requestLabel.textAlignment = .center
requestLabel.text = "Process"
// fill-in data labels
currencyLabel.text = "HKD"
currValueLabel.text = "0"
balanceLabel.text = "Balance: HKD 2 (Available)"
// maybe add number pad button actions here?
numberButtons.forEach { b in
b.addTarget(self, action: #selector(self.numberPadButtonTapped(_:)), for: .touchUpInside)
}
}
#objc func numberPadButtonTapped(_ btn: UIButton) -> Void {
let t = btn.currentTitle ?? "Delete"
print("Tapped:", t)
// do what you want based on which button was tapped
}
private func initHeader()
{
// not clear how you're setting up your "header"
// so I'll guess at it
// view properties
headerView.backgroundColor = .black
headerView.clipsToBounds = true
headerView.layer.cornerRadius = 24
headerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
// static text label
let transferToLabel = UILabel()
transferToLabel.text = "Transfer to..."
transferToLabel.font = UIFont.systemFont(ofSize: 14.0, weight: .regular)
// PayPal button
let payPalButton = UIButton()
payPalButton.setTitle("PayPal", for: [])
payPalButton.backgroundColor = .white
payPalButton.setTitleColor(.blue, for: [])
// data label fonts
currencyLabel.font = UIFont.systemFont(ofSize: 28.0, weight: .bold)
currValueLabel.font = currencyLabel.font
balanceLabel.font = UIFont.systemFont(ofSize: 11.0, weight: .regular)
// label text color
[transferToLabel, currencyLabel, currValueLabel, balanceLabel].forEach {
$0.textColor = .white
}
// horizontal stack to hold currencyLabel, currValueLabel
let currValStack = UIStackView()
currValStack.axis = .horizontal
currValStack.spacing = 12
// vertical stack to hold currValStack, balanceLabel
let innerVStack = UIStackView()
innerVStack.axis = .vertical
innerVStack.alignment = .center
innerVStack.spacing = 2
// add labels to Horizontal stack
currValStack.addArrangedSubview(currencyLabel)
currValStack.addArrangedSubview(currValueLabel)
// add horizontal stack and balanceLabel to vertical stack
innerVStack.addArrangedSubview(currValStack)
innerVStack.addArrangedSubview(balanceLabel)
// view to hold vertical stack (so we can center it vertically)
let innerView = UIView()
// add vertical stack to innerView
innerView.addSubview(innerVStack)
// add elements to headerView
headerView.addSubview(transferToLabel)
headerView.addSubview(payPalButton)
headerView.addSubview(innerView)
// we'll be applying constraints
[headerView, transferToLabel, payPalButton, currencyLabel, currValueLabel, balanceLabel,
innerView, innerVStack].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
// "Transfer to..." label Top: Padding, leading: Padding
transferToLabel.topAnchor.constraint(equalTo: headerView.topAnchor, constant: Padding),
transferToLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
// payPal button Top: Padding to transfer label Bottom
// Leading / Trailing to Leading / Trailing with Padding
payPalButton.topAnchor.constraint(equalTo: transferToLabel.bottomAnchor, constant: Padding),
payPalButton.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
payPalButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -Padding),
// payPalButton explicit height
payPalButton.heightAnchor.constraint(equalToConstant: 50.0),
// innerView Top: 0 to payPal button Bottom
// Leading / Trailing to Leading / Trailing with Padding
// Bottom: 0
innerView.topAnchor.constraint(equalTo: payPalButton.bottomAnchor, constant: 0.0),
innerView.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: Padding),
innerView.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -Padding),
innerView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 0.0),
// innerVStack Top / Bottom to innerView Top / Bottom with 24-pts padding
// centerX
innerVStack.topAnchor.constraint(equalTo: innerView.topAnchor, constant: 32.0),
innerVStack.bottomAnchor.constraint(equalTo: innerView.bottomAnchor, constant: -32.0),
innerVStack.centerXAnchor.constraint(equalTo: innerView.centerXAnchor, constant: 0.0),
])
}
private func initNumpad()
{
numberButtons.removeAll()
// numberPad stack properties
numberPadStackView.axis = .vertical
numberPadStackView.alignment = .fill
numberPadStackView.distribution = .fillEqually
numberPadStackView.spacing = 4
// a little more logical way to manage button layout
let buttonLabels: [[String]] = [
["1", "2", "3"],
["4", "5", "6"],
["7", "8", "9"],
[".", "0", "<"],
]
// adjust as desired
let btnFontSize: CGFloat = 28
buttonLabels.forEach { thisRowLabels in
// create a "row" stack view
let rowStack = UIStackView()
rowStack.axis = .horizontal
rowStack.alignment = .fill
rowStack.distribution = .fillEqually
// same horizontal spacing as "number pad" stack's vertical spacing
rowStack.spacing = numberPadStackView.spacing
// for each number string
thisRowLabels.forEach { s in
// create button
let btn = UIButton()
if s == "<" {
// if it's the "delete button"
// set image here
let iconCfg = UIImage.SymbolConfiguration(pointSize: btnFontSize, weight: .bold, scale: .large)
if let normIcon = UIImage(systemName: "delete.left", withConfiguration: iconCfg)?.withTintColor(.black, renderingMode: .alwaysOriginal),
let highIcon = UIImage(systemName: "delete.left", withConfiguration: iconCfg)?.withTintColor(.lightGray, renderingMode: .alwaysOriginal)
{
btn.setImage(normIcon, for: .normal)
btn.setImage(highIcon, for: .highlighted)
}
} else {
// set number pad button title
btn.setTitle(s, for: [])
}
// number pad button properties
btn.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
btn.setTitleColor(.black, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.titleLabel?.font = UIFont.systemFont(ofSize: btnFontSize, weight: .bold)
// let's give 'em a rounded-corner border
btn.layer.borderColor = UIColor.blue.cgColor
btn.layer.borderWidth = 1
btn.layer.cornerRadius = 8
// allow buttons to stretch vertically!!!
btn.setContentHuggingPriority(UILayoutPriority(rawValue: 249), for: .vertical)
// add button to this row stack
rowStack.addArrangedSubview(btn)
// add button to numberButtons array
numberButtons.append(btn)
}
// add this rowStack to the number pad stack
numberPadStackView.addArrangedSubview(rowStack)
}
}
}
The result, on iPhone 8:
iPhone 11:
and iPhone 11 Pro Max:
I have a custom tableView Cell, I can't figure out why my UITextView won't show up.
This is my UITextView:
fileprivate let notificationBody: UITextView = {
let text = UITextView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
text.translatesAutoresizingMaskIntoConstraints = false
text.backgroundColor = UIColor.appWhite
text.font = UIFont.preferredFont(forTextStyle: .headline)
text.isScrollEnabled = true
text.isEditable = false
text.text = "hjfhsdjfdsfhjksdfhksdjhkfsdjhfshjdfahsdjfdsjklfjsdkfjksdlfjkdsfjklsdlkjfsdjklfsdjklfsdjklfjkldsfjklsdfkjldskjfsdlkjfsdklfjlkdsjflksdajflkdsjflksdjflksdajflksdajflsdkjflsdkjflsdkjflsdkjflsdjflsdjflsdjfsdlkjfslkd"
text.textAlignment = NSTextAlignment.natural
return text
}()
this is how I set it:
fileprivate func setupNotificationBody(){
addSubview(notificationBody)
NSLayoutConstraint.activate([
notificationBody.topAnchor.constraint(equalTo: notificationTitle.bottomAnchor, constant: 5),
notificationBody.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5),
notificationBody.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5),
notificationBody.bottomAnchor.constraint(equalTo: self.dismissNotificationButton.topAnchor, constant: -5)
])
}
And ofcourse I called setupNotificationBody in Init.
I also have in this cell another button and a uilabel which show up correctly.
It looks like you need your cell to have dynamic height. In this case, you should set the property isScrollEnabled in your UITextView to false, so the UITextView will be able to calculate it's own height.
Also, remember to set the cell height to UITableView.automaticDimension in your UITableViewDelegate.
I am creating everything programmatically and I am having issues linking multiple buttons to react to a selected custom action.
the blue button is created programmatically and I use tags to keep track which one is pressed. When the blue button is selected, an action menu pops up which can link actions to the button by pressing the add icon.
You can select the action you want by clicking on "select" then press and drag from the "o" button to create the connector.
I store in a dictionary the selected action and which buttons are connected to it
The issue is how I can link the "o" button, "select" button and "name" label from the same row to each other when they are created programmatically? I am not using tableview to create the actions. Would that be easier to use?
This create the action row
// MARK: - ACTION Input
func createAction()
{
let actionTabContainer = UIView()
actionTabContainer.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
actionTabContainer.translatesAutoresizingMaskIntoConstraints = false
actionTabContainer.backgroundColor = UIColor.darkGray
actionTabContainer.layer.borderWidth = 2
actionTabContainer.layer.borderColor = UIColor(red: 29/255.0, green: 30/255.0, blue: 33/255.0, alpha: 1.0).cgColor
actionScrollViewContainer.addSubview(actionTabContainer)
actionTabContainer.widthAnchor.constraint(equalToConstant: actionScrollViewContainer.frame.width).isActive = true
actionTabContainer.heightAnchor.constraint(equalToConstant: 50).isActive = true
actionTabContainer.leftAnchor.constraint(equalTo: actionScrollViewContainer.leftAnchor, constant: 10).isActive = true
actionTabContainer.topAnchor.constraint(equalTo: actionScrollViewContainer.topAnchor, constant: 2 + constantAdd).isActive = true
constantAdd = constantAdd + 50
let connectorBtn = UIButton()
connectorBtn.createRectangleButton(buttonPositionX: 0, buttonPositionY: 0, buttonWidth: 0, buttonHeight: 0, buttonTitle: "O", buttonTag: 400)
connectorBtn.translatesAutoresizingMaskIntoConstraints = false
connectorBtn.backgroundColor = UIColor.gray
actionTabContainer.addSubview(connectorBtn)
connectorBtn.widthAnchor.constraint(equalToConstant: 30).isActive = true
connectorBtn.heightAnchor.constraint(equalToConstant: 30).isActive = true
connectorBtn.leftAnchor.constraint(equalTo: actionTabContainer.leftAnchor, constant: 10).isActive = true
connectorBtn.centerYAnchor.constraint(equalTo: actionTabContainer.centerYAnchor, constant: 0).isActive = true
connectorBtn.addTarget(self, action: #selector(addConnector(sender:)), for: .touchUpInside)
addPanReconiser(view: connectorBtn)
let chooseActionButton = UIButton()
chooseActionButton.createRectangleButton(buttonPositionX: 0, buttonPositionY: 0, buttonWidth: 0, buttonHeight: 0, buttonTitle: "select", buttonTag: 700)
chooseActionButton.translatesAutoresizingMaskIntoConstraints = false
chooseActionButton.backgroundColor = UIColor.gray
chooseActionButton.layer.cornerRadius = 0
actionTabContainer.addSubview(chooseActionButton)
chooseActionButton.widthAnchor.constraint(equalToConstant: 110).isActive = true
chooseActionButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
chooseActionButton.leftAnchor.constraint(equalTo: connectorBtn.rightAnchor, constant: 10).isActive = true
chooseActionButton.centerYAnchor.constraint(equalTo: connectorBtn.centerYAnchor, constant: 0).isActive = true
chooseActionButton.addTarget(self, action: #selector(addConnector(sender:)), for: .touchUpInside)
let actionMarkerConnectedLabel = UILabel()
actionMarkerConnectedLabel.createLabel(labelPositionX: 0, labelPositionY: 0, labelWidth: 0, labelHeight: 0, labelTitle: "name")
actionMarkerConnectedLabel.backgroundColor = UIColor.gray
actionMarkerConnectedLabel.translatesAutoresizingMaskIntoConstraints = false
actionMarkerConnectedLabel.textAlignment = .center
connectorBtn.addSubview(actionMarkerConnectedLabel)
actionMarkerConnectedLabel.widthAnchor.constraint(equalToConstant: 100).isActive = true
actionMarkerConnectedLabel.heightAnchor.constraint(equalToConstant: 32).isActive = true
actionMarkerConnectedLabel.leftAnchor.constraint(equalTo: chooseActionButton.rightAnchor, constant: 10).isActive = true
actionMarkerConnectedLabel.centerYAnchor.constraint(equalTo: connectorBtn.centerYAnchor, constant: 0).isActive = true
}
I have my own extensions to create rectangles and other shapes which might look confusing.
Thanks for any recommendations
I think it will better if using table view and control them by indexPath. If not, you can create a variable to keep "Count" then when create new :
count += 1
button1.tag = count
button2.tag = count