Cannot see Buttons in UIScrollView - swift

I was making a list in the form of scrollview in swift where the view consists of various types such as labels, button etc.
However when i added the button to the subview, they were not displayed although all other labels etc were displayed. I also tried messing around in the constraints and anchors.
On the other hand when i added the same button to self.view.addsubview instead of scrollview.addsubview, they were displayed just not scrolling since not a part of the scrollview anymore.
I even removed the label to make sure that the buttons were not being overlapped(didn't work either)
I also tried to see the code in "code debug hierarchy " (3D mode), i couldn't see the button there either even though i had added it
Below is my code with an example of label, scrollview and button. It be great if anyone could provide any insights.....thanks either way....
................scrollview..........................
var editInfoView : UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentSize.height = 700
view.backgroundColor = tableBackGroundColor
view.frame = CGRect(x: 0, y: 220, width: 375, height: 400)
return view
}()
.......................label...................
vehicleNumberLabel.translatesAutoresizingMaskIntoConstraints = false
vehicleNumberLabel.textColor = .white
vehicleNumberLabel.text = "Vehicle Number"
vehicleNumberLabel.textAlignment = .left
editInfoView.addSubview(vehicleNumberLabel)
vehicleNumberLabel.leftAnchor.constraint(equalTo: editInfoView.leftAnchor).isActive = true
vehicleNumberLabel.topAnchor.constraint(equalTo: editInfoView.topAnchor, constant: 100).isActive = true
vehicleNumberLabel.widthAnchor.constraint(equalToConstant: 160).isActive = true
vehicleNumberLabel.heightAnchor.constraint(equalToConstant: 20).isActive = true
.....................button................................
vehicleNumberButton.translatesAutoresizingMaskIntoConstraints = false
vehicleNumberButton.setTitleColor(tableTextColor, for: .normal)
vehicleNumberButton.setTitle("Vehicle Number", for: .normal)
vehicleNumberButton.tintColor = tableTextColor
vehicleNumberButton.backgroundColor = tableTextColor
editInfoView.addSubview(vehicleNumberButton)
vehicleNumberButton.rightAnchor.constraint(equalTo: editInfoView.rightAnchor).isActive = true
vehicleNumberButton.topAnchor.constraint(equalTo: editInfoView.topAnchor, constant: 400).isActive = true
vehicleNumberButton.widthAnchor.constraint(equalToConstant: 600).isActive = true
vehicleNumberButton.heightAnchor.constraint(equalToConstant: 255).isActive = true

Although I cannot determine the root cause of your issue with the code and explanation provided I suspect the frame of your UIScrollView() is zero after viewDidAppear(_:) adding subviews to a CGRect.zero can cause some strange behavior with the layout engine. When we create constraints programmatically we are creating a combination of inequalities, equalities, and priorities to restrict the view to a particular frame. If a the value of these constraint equations is incorrect it changes how your relating views appear. Its good practice to avoid the use of leftAnchor and rightAnchor as well, because views may flip direction based on language (writing direction) and user settings.
ViewController.swift
import UIKit
class ViewController: UIViewController {
var editInfoScrollView : UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.isUserInteractionEnabled = true
view.alwaysBounceVertical = true
view.isScrollEnabled = true
view.contentSize.height = 700
view.backgroundColor = UIColor.red.withAlphaComponent(0.3)
// Does nothing because `translatesAutoresizingMaskIntoConstraints = false`
// Instead, set the content size after activating constraints in viewDidAppear
//view.frame = CGRect(x: 0, y: 220, width: 375, height: 400)
return view
}()
var vehicleNumberLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = UIColor.black
label.text = "Vehicle Number"
label.textAlignment = .left
return label
}()
lazy var vehicleNumberButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.tag = 1
button.setTitleColor(UIColor.black, for: .normal)
button.setTitle("Go to Vehicle", for: .normal)
button.tintColor = UIColor.white
button.backgroundColor = UIColor.clear
button.layer.cornerRadius = 30 // about half of button.frame.height
button.layer.borderColor = UIColor.black.cgColor
button.layer.borderWidth = 2.0
button.layer.masksToBounds = true
button.addTarget(self, action: #selector(handelButtons(_:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
self.setupSubviews()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.editInfoScrollView.contentSize = CGSize(width: self.view.frame.width, height: 700.0)
}
func setupSubviews() {
self.view.addSubview(editInfoScrollView)
editInfoScrollView.addSubview(vehicleNumberLabel)
editInfoScrollView.addSubview(vehicleNumberButton)
let spacing: CGFloat = 12.0
let constraints:[NSLayoutConstraint] = [
editInfoScrollView.widthAnchor.constraint(equalTo: self.view.widthAnchor),
editInfoScrollView.heightAnchor.constraint(equalToConstant: 400.0),
editInfoScrollView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
editInfoScrollView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 220.0),
vehicleNumberLabel.leadingAnchor.constraint(equalTo: editInfoScrollView.leadingAnchor, constant: spacing),
vehicleNumberLabel.trailingAnchor.constraint(equalTo: editInfoScrollView.trailingAnchor, constant: -spacing),
vehicleNumberLabel.centerXAnchor.constraint(equalTo: editInfoScrollView.centerXAnchor, constant: -50),
vehicleNumberLabel.heightAnchor.constraint(equalToConstant: 75.0),
vehicleNumberButton.widthAnchor.constraint(equalTo: editInfoScrollView.widthAnchor, multiplier: 0.66),
vehicleNumberButton.heightAnchor.constraint(equalToConstant: 65.0),
vehicleNumberButton.topAnchor.constraint(equalTo: vehicleNumberLabel.bottomAnchor, constant: spacing),
vehicleNumberButton.centerXAnchor.constraint(equalTo: editInfoScrollView.centerXAnchor),
]
NSLayoutConstraint.activate(constraints)
}
#objc func handelButtons(_ sender: UIButton) {
switch sender.tag {
case 0:
print("Default button tag")
case 1:
print("vehicleNumberButton was tapped")
default:
print("Nothing here yet")
}
}
}

Related

What is wrong with this StackView?

what is wrong with my StackView?
This is the code:
class PushUpViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setUpStackView()
}
func setUpStackView() {
// SetUp StackView:
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .fillProportionally
stackView.spacing = 50
view.addSubview(stackView)
// SetUp StackView Constraints:
stackView.pin(to: view)
stackView.setCustomSpacing(50, after: PushUpButton)
stackView.setCustomSpacing(100, after: TimeLabel)
// Set Elements to StackView:
stackView.addArrangedSubview(TimeLabel)
stackView.addArrangedSubview(PushUpButton)
stackView.addArrangedSubview(secondStackView)
// SetUp PushUpButton:
PushUpButton.backgroundColor = .white
PushUpButton.setTitle("\(count)", for: .normal)
PushUpButton.setTitleColor(.systemGray, for: .normal)
PushUpButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 70)
PushUpButton.translatesAutoresizingMaskIntoConstraints = false
PushUpButton.heightAnchor.constraint(equalToConstant: 300).isActive = true
PushUpButton.widthAnchor.constraint(equalToConstant: 150).isActive = true
// SetUp TimeLabel
TimeLabel.textAlignment = .center
TimeLabel.text = "\(counter)"
TimeLabel.textColor = .black
TimeLabel.font = .boldSystemFont(ofSize: 30)
self.view.addSubview(TimeLabel)
TimeLabel.translatesAutoresizingMaskIntoConstraints = false
TimeLabel.widthAnchor.constraint(equalToConstant: 300).isActive = true
TimeLabel.heightAnchor.constraint(equalToConstant: 200).isActive = true
// SetUp SecondStackView
secondStackView.translatesAutoresizingMaskIntoConstraints = false
secondStackView.axis = .horizontal
secondStackView.alignment = .center
secondStackView.distribution = .fillEqually
secondStackView.spacing = 20
// SetUp SecondStackView Constrains
secondStackView.heightAnchor.constraint(equalToConstant: 50).isActive = true
secondStackView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -15).isActive = true
// Set Elements:
secondStackView.addArrangedSubview(breakButton)
secondStackView.addArrangedSubview(stopbutton)
//SetUp BreakButton
breakButton.backgroundColor = .lightGray
breakButton.setTitle("Break", for: .normal)
breakButton.setTitle("Start", for: .selected)
breakButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
breakButton.setTitleColor(.white, for: .normal)
breakButton.layer.cornerRadius = 12
breakButton.layer.borderWidth = 1
breakButton.layer.borderColor = UIColor.white.cgColor
// breakButton.addTarget(self, action: #selector(BreakButtonTapped), for: .touchUpInside)
breakButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
breakButton.widthAnchor.constraint(equalToConstant: 150),
breakButton.heightAnchor.constraint(equalToConstant: 50)
])
// SetUp StopButton:
stopbutton.backgroundColor = .systemRed
stopbutton.setTitle("Stop", for: .normal)
stopbutton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
stopbutton.setTitleColor(.white, for: .normal)
stopbutton.layer.cornerRadius = 12
stopbutton.layer.borderWidth = 1
stopbutton.layer.borderColor = UIColor.white.cgColor
// stopbutton.addTarget(self, action: #selector(stopButtonTapped), for: .touchUpInside)
stopbutton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stopbutton.widthAnchor.constraint(equalToConstant: 150),
stopbutton.heightAnchor.constraint(equalToConstant: 50)
])
}
}
And this is how it look:
But it should looks like this:
This is what comes in the console when I am on the StackView VC:
I have no idea what this means or what I should do to solve this problem
I do not understand StackViews... I watched a lot of yt tutorials but they are all the same and did't help me. My biggest problem is the distribution of the StackView: I don't know where the difference is
First tip: forget using .fillProportionally with stack views. It is almost never used ... and when it is used, it's used for very specific reasons.
Second tip: during development, give your UI elements contrasting background colors to make it easy to see the frames at run-time.
Third tip: Use leadingLowerCase for variable and function names... Use LeadingUpperCase for class names.
Fourth tip: group similar code together - such as setting view properties, setting constraints, etc - and include logical comments to make it easier to follow what your code is doing.
Take a look at this:
class PushUpViewController: UIViewController {
let stackView = UIStackView()
let secondStackView = UIStackView()
let pushUpButton = UIButton()
let breakButton = UIButton()
let stopbutton = UIButton()
let timeLabel = UILabel()
var count: Int = 0
var counter: Float = 0.0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setUpStackView()
}
func setUpStackView() {
// SetUp StackView:
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fill
// SetUp timeLabel
timeLabel.textAlignment = .center
timeLabel.text = "\(counter)"
timeLabel.textColor = .black
timeLabel.font = .boldSystemFont(ofSize: 30)
// SetUp pushUpButton:
pushUpButton.backgroundColor = .white
pushUpButton.setTitle("\(count)", for: .normal)
pushUpButton.setTitleColor(.systemGray, for: .normal)
pushUpButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 70)
// SetUp secondStackView
secondStackView.axis = .horizontal
secondStackView.alignment = .fill
secondStackView.distribution = .fillEqually
secondStackView.spacing = 20
//SetUp breakButton
breakButton.backgroundColor = .lightGray
breakButton.setTitle("Break", for: .normal)
breakButton.setTitle("Start", for: .selected)
breakButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
breakButton.setTitleColor(.white, for: .normal)
breakButton.layer.cornerRadius = 12
breakButton.layer.borderWidth = 1
breakButton.layer.borderColor = UIColor.white.cgColor
// SetUp stopButton:
stopbutton.backgroundColor = .systemRed
stopbutton.setTitle("Stop", for: .normal)
stopbutton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
stopbutton.setTitleColor(.white, for: .normal)
stopbutton.layer.cornerRadius = 12
stopbutton.layer.borderWidth = 1
stopbutton.layer.borderColor = UIColor.white.cgColor
// add buttons to horizontal second stack view
secondStackView.addArrangedSubview(breakButton)
secondStackView.addArrangedSubview(stopbutton)
// if we want the center PushUpButton to be 300 x 150
// and centered vertically
// we need to embed it in a clear view
let holderView = UIView()
// add PushUpButton to holderView
holderView.addSubview(pushUpButton)
// views added as arrangedSubviews of a stack view automatically get
// .translatesAutoresizingMaskIntoConstraints = false
// but, because we're adding the PushUpButton as a subview
// of holderView, we need to set it here
pushUpButton.translatesAutoresizingMaskIntoConstraints = false
// add views to stack view
stackView.addArrangedSubview(timeLabel)
stackView.addArrangedSubview(holderView)
stackView.addArrangedSubview(secondStackView)
// add stackView to view
view.addSubview(stackView)
// SetUp StackView Constraints:
//stackView.pin(to: view)
// respect safe-area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain stackview to full view (safe-area)
// to bottom with Zero extra space
stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
// to top with 20-pts "padding"
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
// and 8-pts "padding" on each side
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
// pushUpButton should be 300x150
pushUpButton.widthAnchor.constraint(equalToConstant: 300.0),
pushUpButton.heightAnchor.constraint(equalToConstant: 150.0),
// pushUpButton should be centered in holderView
pushUpButton.centerXAnchor.constraint(equalTo: holderView.centerXAnchor),
pushUpButton.centerYAnchor.constraint(equalTo: holderView.centerYAnchor),
// bottom buttons should have Height: 50
secondStackView.heightAnchor.constraint(equalToConstant: 50.0),
])
// break and stop button actions
//breakButton.addTarget(self, action: #selector(BreakButtonTapped), for: .touchUpInside)
//stopbutton.addTarget(self, action: #selector(stopButtonTapped), for: .touchUpInside)
// during development, so we can see the layout easily
//holderView.backgroundColor = .yellow
//PushUpButton.backgroundColor = .green
//TimeLabel.backgroundColor = .cyan
}
}
Result on iPhone 11:
on iPhone 8:
and with background colors to help during development:
Additional Tip:
When learning auto layout (particularly stack views), work on your layout in Storyboard / Interface Builder. You can immediately see how it looks and what happens when changing values / properties. You can also change the View as: to immediately see how it looks on different devices / screen sizes. If you want to keep everything in code, once you have your layout looking the way you want, then replicate those constraints and settings in code.

func is reseting position of pan gesture

The func addBlackView is adding a black view everytime the func is called. The black view is connected a to uiPangesture the problem is evertyime the func addblackview is called the code is reseting the position of wherever the first black has been moved. You can see what is goin on in the gif below. I just want the 1st black view to not move and stay in the same position if a new black view is Called.
import UIKit
class ViewController: UIViewController {
var image1Width2: NSLayoutConstraint!
var iHieght: NSLayoutConstraint!
var currentView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(currentView)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
button.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
button.widthAnchor.constraint(equalToConstant: 100),
button.heightAnchor.constraint(equalToConstant: 80),
])
button.addTarget(self,action: #selector(addBlackView),for: .touchUpInside)
}
let slider:UISlider = {
let slider = UISlider(frame: .zero)
return slider
}()
private lazy var button: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.setTitleColor(.white, for: .normal)
button.setTitle("add", for: .normal)
return button
}()
let blackView: UIView = {
let view = UIView()
view.backgroundColor = .black
return view
}()
var count = 0
#objc
private func addBlackView() {
let newBlackView = UIView(frame: CGRect(x: 20, y: 20, width: 100, height: 100)) // whatever frame you want
newBlackView.backgroundColor = .orange
self.view.addSubview(newBlackView)
self.currentView = newBlackView
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(moveView(_:)))
newBlackView.addGestureRecognizer(recognizer)
newBlackView.isUserInteractionEnabled = true
image1Width2 = newBlackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.1)
image1Width2.isActive = true
iHieght = newBlackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.1)
iHieght.isActive = true
count += 1
newBlackView.tag = (count)
newBlackView.translatesAutoresizingMaskIntoConstraints = false
newBlackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
newBlackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
#objc private func moveView(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .changed:
let translation = recognizer.translation(in: self.view)
recognizer.view!.center = .init(x: recognizer.view!.center.x + translation.x,
y: recognizer.view!.center.y + translation.y)
recognizer.setTranslation(.zero, in: self.view)
default:
break
}
}
}
They always go back to the centre because you have constrained the black (orange) views to the centre:
newBlackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
newBlackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
You shouldn't even be able to drag any of the views at all, but I guess setting center ignores constraints for some reason. Anyway, when you add a new view, UIKit calls view.setNeedsLayout/layoutIfNeeded somewhere down the line, and this causes all the views to realise "oh wait, I'm supposed to be constrained to the centre!" and snap back. :D
If you want to keep using constraints, try storing the centre X and Y constraints of all the views in an array:
var centerXConstraints: [NSLayoutConstraint] = []
var centerYConstraints: [NSLayoutConstraint] = []
And append to them when you add a new view:
let yConstraint = newBlackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let xConstraint = newBlackView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
xConstraint.isActive = true
yConstraint.isActive = true
centerXConstraints.append(xConstraint)
centerYConstraints.append(yConstraint)
Then, rather than changing the center, change the constant of these constraints:
let centerXConstraint = centerXConstraints[recognizer.view!.tag - 1]
let centerYConstraint = centerYConstraints[recognizer.view!.tag - 1]
centerXConstraint.constant += translation.x
centerYConstraint.constant += translation.y
Alternatively, and this is what I would do, just remove all your constraints, and translateAutoresizingMaskIntoConstraints = true. This way you can freely set your center.

UIView is Being Over Written Every Time Func is Called

My swift codes goal is to place a uiview every time the button is pressed. In my gif you can see every time the blue button is called it is over written. When the code is pressed the gif should have 2 uiviews in it. You can see the transparent uiview of where the first view disappears. Basically all that is wrong with this code is when the addBlackView is called it should add to the views on the screen basically just like a infinite array.
import UIKit
class ViewController: UIViewController {
var image1Width2: NSLayoutConstraint!
var iHieght: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(slider)
slider.translatesAutoresizingMaskIntoConstraints = false
slider.value = 0.5
slider.isUserInteractionEnabled = true
NSLayoutConstraint.activate([
slider.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
slider.leadingAnchor.constraint(equalTo: view.leadingAnchor),
slider.heightAnchor.constraint(equalToConstant: 100),
slider.widthAnchor.constraint(equalTo: view.widthAnchor,multiplier: 1),
])
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
button.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
button.widthAnchor.constraint(equalToConstant: 100),
button.heightAnchor.constraint(equalToConstant: 80),
])
button.addTarget(self,action: #selector(addBlackView),for: .touchUpInside)
slider.addTarget(self, action: #selector(increase), for: .allEvents)
}
let slider:UISlider = {
let slider = UISlider(frame: .zero)
return slider
}()
private lazy var button: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.setTitleColor(.white, for: .normal)
button.setTitle("add", for: .normal)
return button
}()
let blackView: UIView = {
let view = UIView()
view.backgroundColor = .black
return view
}()
#objc
private func addBlackView() {
self.view.addSubview(blackView)
blackView.translatesAutoresizingMaskIntoConstraints = false
blackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
blackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
image1Width2 = blackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.1)
image1Width2.isActive = true
iHieght = blackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.1)
iHieght.isActive = true
view.layoutIfNeeded()
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(moveView(_:)))
blackView.addGestureRecognizer(recognizer)
}
#objc private func moveView(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
print("gesture began")
case .changed:
let translation = recognizer.translation(in: self.view)
recognizer.view!.center = .init(x: recognizer.view!.center.x + translation.x,
y: recognizer.view!.center.y + translation.y)
recognizer.setTranslation(.zero, in: self.view)
default:
break
}
}
#objc func increase() {
image1Width2.constant = CGFloat(slider.value) * view.frame.size.width * 0.10
iHieght.constant = CGFloat(slider.value) * view.frame.size.width * 0.10
}}
The problem is that you're reusing and resetting blackView every time you execute addBlackView, so the changes you've made will be lost (hence why the view goes back in the center after you pressed the button).
You would need to create a complete new view in addBlackView, which would be your 'currentView' that you are manipulating and then add add gesture recognizers to it. Then once you execute addBlackView again, the 'currentView' would be 'validated' (stored in an array or whatever you need to do with it) and then you create another one to manipulate.
Something like this:
private func addBlackView() {
let newBlackView = UIView(frame: CGRect(0, 0, 10, 10)) // whatever frame you want
self.view.addSubview(newBlackView)
self.currentView = newBlackView
}

add 2 image views to a uiscrollview func every time it is called

My swift code below goal is to add 2 image views every time. Ass you can in the gif below only one image view is being added. I just need to add 2 image views. The image views are lastImage and lastImage2. you can see only lastImage is being shown. It seems I can only add 1 imageview when func didclickadd is called.
import UIKit
class ViewController: UIViewController {
fileprivate var lastImage:UIImageView?
fileprivate var lastImage2:UIImageView?
fileprivate var mainViewBootom:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupVIew()
}
override func viewDidAppear(_ animated: Bool) {
scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
view.layoutIfNeeded()
}
//MARK: Components
let scrollView:UIScrollView = {
let sv = UIScrollView(frame: .zero)
return sv
}()
let mainView:UIView = {
let uv = UIView()
uv.backgroundColor = .white
return uv
}()
let btnAdd:UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Add", for: .normal)
return btn
}()
let textField:UITextField = {
let jake = UITextField()
return jake
}()
//MARK: Setup UI
func setupVIew() {
view.addSubview(scrollView)
view.addSubview(btnAdd)
view.addSubview(textField)
scrollView.translatesAutoresizingMaskIntoConstraints = false
btnAdd.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
btnAdd.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnAdd.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
btnAdd.widthAnchor.constraint(equalToConstant: 100),
btnAdd.heightAnchor.constraint(equalToConstant: 45),
//
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 25),
textField.widthAnchor.constraint(equalToConstant: 100),
textField.heightAnchor.constraint(equalToConstant: 45),
//
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: btnAdd.topAnchor , constant: -12),
])
btnAdd.addTarget(self, action: #selector(didClickedAdd), for: .touchUpInside)
scrollView.addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
mainView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mainView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mainView.topAnchor.constraint(equalTo: scrollView.topAnchor),
])
let imgView = UIImageView(frame: CGRect(x: 0, y: 0, width: 150, height: 100))
imgView.backgroundColor = .red
mainView.addSubview(imgView)
let samsam = UIImageView(frame: CGRect(x: 0, y: 200, width: 40, height: 100))
samsam.backgroundColor = .blue
mainView.addSubview(samsam)
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 150).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 100).isActive = true
samsam.translatesAutoresizingMaskIntoConstraints = false
samsam.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
samsam.topAnchor.constraint(equalTo: imgView.bottomAnchor).isActive = true
samsam.widthAnchor.constraint(equalToConstant: 75).isActive = true
samsam.heightAnchor.constraint(equalToConstant: 100).isActive = true
if lastImage != nil {
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 20).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 12).isActive = true
}
lastImage = samsam
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 12)
mainViewBootom!.isActive = true
}
#objc func didClickedAdd(){
let imgView = UIImageView(frame: CGRect(x: 20, y: 0, width: 30, height: 20))
imgView.backgroundColor = .orange
mainView.addSubview(imgView)
let ss = UIImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 50))
imgView.backgroundColor = .green
mainView.addSubview(ss)
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 60).isActive = true
ss.translatesAutoresizingMaskIntoConstraints = false
ss.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = false
ss.widthAnchor.constraint(equalToConstant: 80).isActive = true
ss.heightAnchor.constraint(equalToConstant: 90).isActive = true
if lastImage != nil {
ss.topAnchor.constraint(equalTo: imgView.topAnchor , constant: 20).isActive = true
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 50).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 10).isActive = true
ss.bottomAnchor.constraint(equalTo: imgView.bottomAnchor , constant: 25).isActive = true
}
lastImage = imgView
lastImage2 = ss
mainView.removeConstraint(mainViewBootom!)
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage2!.bottomAnchor , constant: 40)
mainViewBootom!.isActive = true
view.layoutIfNeeded()
scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
view.layoutIfNeeded()
}
}
Couple notes...
With proper constraint setup, auto-layout handles the UIScrollView content size all by itself. No need to ever set scrollView.contentSize = ...
You have several instances of adding a subview (image view) to your mainView, which is a subview of your scroll view, but then you add constraints from that subview to your controller's view. Make sure you are constraining elements to the proper other elements.
Here's your code, with commented changes:
class BenViewController: UIViewController {
fileprivate var lastImage:UIImageView?
// 1) don't need this
// fileprivate var lastImage2:UIImageView?
fileprivate var mainViewBootom:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupVIew()
}
// 2) don't need this
// override func viewDidAppear(_ animated: Bool) {
// scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
// view.layoutIfNeeded()
// }
//MARK: Components
let scrollView:UIScrollView = {
let sv = UIScrollView(frame: .zero)
return sv
}()
let mainView:UIView = {
let uv = UIView()
uv.backgroundColor = .white
return uv
}()
let btnAdd:UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Add", for: .normal)
return btn
}()
let textField:UITextField = {
let jake = UITextField()
return jake
}()
//MARK: Setup UI
func setupVIew() {
view.addSubview(scrollView)
view.addSubview(btnAdd)
view.addSubview(textField)
scrollView.translatesAutoresizingMaskIntoConstraints = false
btnAdd.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
btnAdd.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnAdd.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
btnAdd.widthAnchor.constraint(equalToConstant: 100),
btnAdd.heightAnchor.constraint(equalToConstant: 45),
//
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 25),
textField.widthAnchor.constraint(equalToConstant: 100),
textField.heightAnchor.constraint(equalToConstant: 45),
//
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: btnAdd.topAnchor , constant: -12),
])
btnAdd.addTarget(self, action: #selector(didClickedAdd), for: .touchUpInside)
scrollView.addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
// 3) change this:
// NSLayoutConstraint.activate([
// mainView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// mainView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// mainView.topAnchor.constraint(equalTo: scrollView.topAnchor),
// ])
//
// to this
NSLayoutConstraint.activate([
mainView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
mainView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
mainView.topAnchor.constraint(equalTo: scrollView.topAnchor),
mainView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
mainView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
])
// end of change 3)
let imgView = UIImageView(frame: CGRect(x: 0, y: 0, width: 150, height: 100))
imgView.backgroundColor = .red
mainView.addSubview(imgView)
let samsam = UIImageView(frame: CGRect(x: 0, y: 200, width: 40, height: 100))
samsam.backgroundColor = .blue
mainView.addSubview(samsam)
imgView.translatesAutoresizingMaskIntoConstraints = false
// 4) change view.centerXAnchor to mainView.centerXAnchor
// imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.centerXAnchor.constraint(equalTo: mainView.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 150).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 100).isActive = true
samsam.translatesAutoresizingMaskIntoConstraints = false
// 5) change view.centerXAnchor to mainView.centerXAnchor
// samsam.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
samsam.centerXAnchor.constraint(equalTo: mainView.centerXAnchor).isActive = true
samsam.topAnchor.constraint(equalTo: imgView.bottomAnchor).isActive = true
samsam.widthAnchor.constraint(equalToConstant: 75).isActive = true
samsam.heightAnchor.constraint(equalToConstant: 100).isActive = true
if lastImage != nil {
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 20).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 12).isActive = true
}
lastImage = samsam
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 12)
mainViewBootom!.isActive = true
}
#objc func didClickedAdd(){
let imgView = UIImageView(frame: CGRect(x: 20, y: 0, width: 30, height: 20))
imgView.backgroundColor = .orange
mainView.addSubview(imgView)
let ss = UIImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 50))
// 6) typo or copy/paste mistake
// imgView.backgroundColor = .green
ss.backgroundColor = .green
mainView.addSubview(ss)
imgView.translatesAutoresizingMaskIntoConstraints = false
// 7) change view.centerXAnchor to mainView.centerXAnchor
// imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.centerXAnchor.constraint(equalTo: mainView.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 60).isActive = true
ss.translatesAutoresizingMaskIntoConstraints = false
// 8) change view.leadingAnchor to mainView.leadingAnchor
// ss.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = false
ss.leadingAnchor.constraint(equalTo: mainView.leadingAnchor).isActive = false
ss.widthAnchor.constraint(equalToConstant: 80).isActive = true
ss.heightAnchor.constraint(equalToConstant: 90).isActive = true
// 9) always need to do this ... but did you mean imgView.bottomAnchor?
ss.topAnchor.constraint(equalTo: imgView.topAnchor , constant: 20).isActive = true
if lastImage != nil {
// 9a) instead of only here
//ss.topAnchor.constraint(equalTo: imgView.topAnchor , constant: 20).isActive = true
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 50).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 10).isActive = true
}
// 10) always need to do this
// deactivate bottom constraint
mainViewBootom?.isActive = false
lastImage = ss
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage!.bottomAnchor, constant: 40)
mainViewBootom?.isActive = true
// 11) don't need any of this
// lastImage = imgView
// lastImage2 = ss
// mainView.removeConstraint(mainViewBootom!)
//
//
// mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage2!.bottomAnchor , constant: 40)
//
//
//
//
// mainViewBootom!.isActive = true
// view.layoutIfNeeded()
//
// scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
// view.layoutIfNeeded()
}
}
Use Xcode’s “view debugger” (the button is circled in red in my screen snapshot below) and you’ll see what’s going on:
Your ss view has no background color. Note, that when you created that view, you accidentally reset the imgView background color a second time rather than setting the ss.backgroundColor.
Fix that and you’ll see your both imgView and ss:
The view debugger is your best friend when trying to diagnose issues like this. Now, obviously, the green view probably isn’t where you intended it, but you should now be able to see it and diagnose that issue very easily.
All of this having been said, a few observations:
You’re making life much harder than you need to. If you just set the constraints for the scroll view and a stack view within that scroll view, you then only need to add an arranged subview. For example:
#objc func didTapButton(_ sender: UIButton) {
stackView.addArrangedSubview(randomView())
stackView.addArrangedSubview(randomView())
}
Note, once the stack view and scroll view have been set up (see below), then you don’t need to mess around with contentSize or constraints for these subviews at all (other than the widthAnchor and heightAnchor). The auto layout engine, combined with the constraints between the stack view and the scroll view, will take care of everything for you.
So, a full working example:
class ViewController: UIViewController {
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 10
return stackView
}()
let button: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Add", for: .normal)
button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
}
// MARK: - Actions
extension ViewController {
#objc func didTapButton(_ sender: UIButton) {
stackView.addArrangedSubview(randomView())
stackView.addArrangedSubview(randomView())
}
}
// MARK: - Private utility methods
private extension ViewController {
func configure() {
view.addSubview(scrollView)
view.addSubview(button)
scrollView.addSubview(stackView)
NSLayoutConstraint.activate([
// define frame of `scrollView`
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: button.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// define frame of `button`
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// define contentSize of `scrollView` based upon size of `stackView`
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
// but define width of `stackView` relative to the _main view_
stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
])
button.setContentHuggingPriority(.required, for: .vertical)
}
func randomView() -> UIView {
let widthRange = view.bounds.width * 0.1 ... view.bounds.width * 0.9
let heightRange = view.bounds.width * 0.1 ... view.bounds.width * 0.25
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: .random(in: widthRange)),
view.heightAnchor.constraint(equalToConstant: .random(in: heightRange))
])
view.backgroundColor = UIColor(red: .random(in: 0.25...1), green: .random(in: 0.25...1), blue: .random(in: 0.25...1), alpha: 1)
return view
}
}
Even better, I’d personally set up the scroll view, stack view, button, and all the associated constraints in Interface Builder, and then that hairy configure method in my example goes away completely. It’s fun to learn how to create views programmatically, but in real-world projects, it’s rarely the most productive way to do it. Do programmatic views where needed (e.g. adding arranged subviews to the stack view on the click of a button), but otherwise, for those views that should be there when you first run the app, Interface Builder is worth considering.
E.g. It dramatically reduces the amount of code above, leaving us simply with:
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var stackView: UIStackView!
#IBAction func didTapButton(_ sender: UIButton) {
stackView.addArrangedSubview(randomView())
stackView.addArrangedSubview(randomView())
}
}
// MARK: - Private utility methods
private extension ViewController {
func randomView() -> UIView { ... }
}
Clearly, it takes a while to get used to designing views and configuring constraints in IB, but it’s worth the effort. It distills our code down the the bare essentials.
In your code, you’re setting frames for these image views and then setting translatesAutoresizingMaskIntoConstraints. There’s absolutely no point in setting the frame in that case, because translatesAutoresizingMaskIntoConstraints says “ignore my frame, use constraints instead.”
I’m assuming you’re doing all of this just to become familiar with scroll views, but it’s worth noting that, especially when adding lots of image views, that the scroll view is an inherently inefficient approach.
For example, let’s say you’ve added 100 image views, but you can see only 8 at a time. Do you really want to hold all 100 image views in memory at the same time? No.
But, UITableView, which is a subclass of UIScrollView, takes care of this. You end up only keeping the currently visible image views in memory. It’s a far better approach.
This is especially true when you start using actual UIImage objects, because they require a lot of memory. We get lulled into a sense of security, looking at reasonably sized PNG/JPG assets, but when they’re loaded into memory, they’re uncompressed and require a disproportionate amount of memory.

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().