How to remove and addArrangedSubvew without view blinking? - ios

I have some view that consists from one stackview. Every n second I should update this view with data from server. When the new data come, I clean stackView (with method removeFromSuperView of its children) and add arrangedSubviews again to update UI. Sometimes, server sends the same data as old data. But doing this update operation, my view is kinda shuddering. Its kinda dragging and shuddering every time I clean and add views to my stackview. Of course, I can only update my UI if oldData != newData. But this competition is difficult and hard to find correctly. So, how to update stackview with new data without shuddering and blinking?
Here is my code:
func configure(_ items: [Item]) {
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
items.forEach {
let someView = SomeView()
someView.configure($0)
stackView.addArrangedSubview(someView)
}
}

Instead of removing / re-adding views to the stack view...
only add new views if there are more Items than current arranged subviews
set the data - configure() - each arranged subview
hide existing arranged subviews if there are more than needed
For example:
func configure(_ items: [Item]) {
// if we have fewer arranged subviews than items
// add new ones
while stackView.arrangedSubviews.count < items.count {
stackView.addArrangedSubview(SomeView())
}
// hide any existing arranged subviews if we have too many
for i in 0..<stackView.arrangedSubviews.count {
stackView.arrangedSubviews[i].isHidden = i >= items.count
}
// update the existing arranged subviews with the new data
for (thisItem, thisView) in zip(items, stackView.arrangedSubviews) {
// unwrap the arranged subview
guard let v = thisView as? SomeView else {
continue
}
v.configure(thisItem)
}
}
Here's a complete example - the number of "items" and their data will change every 1.5 seconds:
Sample struct Item with two strings:
struct Item {
var title: String = ""
var desc: String = ""
}
Sample "SomeView" class with two labels:
class SomeView: UIView {
let titleLabel = UILabel()
let descLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .systemYellow
titleLabel.font = .boldSystemFont(ofSize: 18.0)
descLabel.font = .italicSystemFont(ofSize: 15.0)
[titleLabel, descLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white // so we can see the label frames at run-time
addSubview(v)
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
descLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4.0),
descLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),
descLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
descLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
])
}
func configure(_ item: Item) -> Void {
titleLabel.text = item.title
descLabel.text = item.desc
}
}
Sample "NeoView" class with the stack view:
class NeoView: UIView {
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
])
}
func configure(_ items: [Item]) {
// if we have fewer arranged subviews than items
// add new ones
while stackView.arrangedSubviews.count < items.count {
stackView.addArrangedSubview(SomeView())
}
// hide any existing arranged subviews if we have too many
for i in 0..<stackView.arrangedSubviews.count {
stackView.arrangedSubviews[i].isHidden = i >= items.count
}
// update the existing arranged subviews with the new data
for (thisItem, thisView) in zip(items, stackView.arrangedSubviews) {
// unwrap the arranged subview
guard let v = thisView as? SomeView else {
continue
}
v.configure(thisItem)
}
}
}
Example controller class:
class NeoTestViewController: UIViewController {
let neoView: NeoView = {
let v = NeoView()
v.backgroundColor = .systemTeal
return v
}()
var items: [[Item]] = []
var idx: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
neoView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(neoView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
neoView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
neoView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
neoView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// no bottom constraint
])
items = sampleData()
var idx: Int = 0
self.neoView.configure(self.items[idx % self.items.count])
// update neoView with a new set of items every 1.5 seconds
Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { timer in
idx += 1
self.neoView.configure(self.items[idx % self.items.count])
}
}
func sampleData() -> [[Item]] {
// we'll create 8 sets of [Item] arrays
// each with a different number of Items
let numItems: [Int] = [
4, 2, 6, 3, 7, 8, 3, 5,
]
var items: [[Item]] = []
var i: Int = 1
numItems.forEach { n in
var theseItems: [Item] = []
for j in 1...n {
let thisItem = Item(title: "Set \(i) item \(j) title.", desc: "Set \(i) item \(j) description.")
theseItems.append(thisItem)
}
theseItems[0].title = "Set \(i) has \(n) items."
items.append(theseItems)
i += 1
}
return items
}
}

Related

Typewriter Effect UILabel width issue

I want to have a UILabel in the middle of the screen that starts from the left of the screen. The UILabel will get updated every second from a Timer to simulate a typewriter effect. So to display "Album releases", the UILabel will show this series of text:
A
Al
Alb
Albu
Album
Album
Album r
Album re
Album rel
Album rele
Album relea
Album releas
Album release
Album releases
The label has a black background. So I want to make sure the width fits the text. But if the text is too long then the width should go up to the screen width and then breaks into a second line. I tried using numberOfLines = 0 and sizeToFit to achieve that. But the issue is that for some reason, one letter is being written on a line. I cannot manage to fix that. Here's some code:
private let taglinesLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.textColor = .white
label.backgroundColor = .black
label.numberOfLines = 0
return label
}()
override func layoutSubviews() {
...
taglinesLabel.frame = CGRect(x: 0, y: 200, width: 0, height: 0)
...
}
public func showText() {
...
startTimer()
...
}
func startTimer () {
guard timer == nil else { return }
timer = Timer.scheduledTimer(
timeInterval: TimeInterval(0.1),
target : self,
selector : #selector(onTimer),
userInfo : nil,
repeats : true)
}
#objc private func onTimer() {
let tagline = taglines[taglineIndex]
if(taglineCharacterIndex < tagline.count) {
let substring = tagline.prefix(taglineCharacterIndex)
print(substring)
taglinesLabel.text = String(substring)
taglinesLabel.sizeToFit()
taglineCharacterIndex += 1
}
}
How to constraint the UILabel to expand its width and not go to the next line like shown in the picture?
Couple things...
First, use auto-layout and constraints instead of .sizeToFit().
Second, let's replace the UILabel with a non-editable, non-scrolling UITextView because:
It gives us a visually nice "padding" around the text
It keeps the text at the top (if we have set a height for the view)
It avoids a weird "line jumping" when the text wraps
So, start with an example controller that puts an instance of our custom TypingView 40-points from the Top and from each side. We'll also give it four sample "taglines" and we'll start the timer in viewDidAppear:
class ViewController: UIViewController {
let testView = TypingView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// no bottom or height constraint
// we let the TypingView set its own height
])
testView.taglines = [
"First sample string.",
"This is the Second sample tagline.",
"This tagline will be long enough that it will wrap onto at least two lines.",
"Here is the final tagline.",
]
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
testView.startTimer()
}
}
Now, our custom UIView subclass:
class TypingView: UIView {
public var taglines: [String] = []
private var taglineIndex: Int = 0
private var timer: Timer!
private var taglineCharacterIndex: Int = 0
private let taglinesLabel: UITextView = {
let v = UITextView()
v.font = .systemFont(ofSize: 17.0, weight: .regular)
v.textColor = .white
v.backgroundColor = .black
v.isScrollEnabled = false
v.isEditable = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
backgroundColor = .black
taglinesLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(taglinesLabel)
NSLayoutConstraint.activate([
taglinesLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
taglinesLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
taglinesLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
taglinesLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
])
}
func startTimer () {
guard timer == nil else { return }
timer = Timer.scheduledTimer(
timeInterval: TimeInterval(0.1),
target : self,
selector : #selector(onTimer),
userInfo : nil,
repeats : true)
}
#objc private func onTimer() {
guard taglineIndex < taglines.count else {
timer.invalidate()
timer = nil
return
}
let tagline = taglines[taglineIndex]
if taglineCharacterIndex < tagline.count + 1 {
let substring = tagline.prefix(taglineCharacterIndex)
taglinesLabel.text = String(substring)
taglineCharacterIndex += 1
} else if taglineCharacterIndex < tagline.count + 6 {
// this will provide a half-second "pause" before going to the next tagline
taglineCharacterIndex += 1
} else {
taglineIndex += 1
taglineCharacterIndex = 0
}
}
}
It will look like this (adding 1 character every 1/10th second):
and with enough text to wrap:
Edit - in response to comment...
To get the view to expand horizontally as the text gets longer, change the trailing constraint (in ViewController) from:
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
to:
testView.trailingAnchor.constraint(lessThanOrEqualTo: g.trailingAnchor, constant: -40.0),
Now it will grow wider with each character, but only until it reaches 40-points from the right side.
You could also replace the trailing constraint with a width constraint if that would better suit your needs.
For example:
testView.widthAnchor.constraint(lessThanOrEqualToConstant: 240.0),
would limit its width to 240-points.

How can implement two vertical button in swipe to delete in ios?

I am trying to implement swipe to delete feature with two options in tableview, one is to delete and another one is to Update.The things I want is these options should be vertical rather than horizontal.I have checked so many question but nothing find.
Thanks in advance for support.
.
As I mentioned in the comments, here is one approach:
add your buttons to the cell
add a "container" view to the cell
constrain the container view so it overlays / covers the buttons
add a Pan gesture recognizer to the container view so you can drag it left / right
as you drag it left, it will "reveal" the buttons underneath
You lose all of the built-in swipe functionality, but this is one approach that might give you the design you're going for.
First, an example of creating a "drag view":
class DragTestViewController: UIViewController {
let backgroundView = UIView()
let containerView = UIView()
// leading and trailing constraints for the drag view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(60.0)
private let origTrailing = CGFloat(-60.0)
private var currentLeading = CGFloat(60.0)
private var currentTrailing = CGFloat(-60.0)
override func viewDidLoad() {
super.viewDidLoad()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .cyan
backgroundView.clipsToBounds = true
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.backgroundColor = .red
// add a label to the container view
let exampleLabel = UILabel()
exampleLabel.translatesAutoresizingMaskIntoConstraints = false
exampleLabel.text = "Drag Me"
exampleLabel.textColor = .yellow
containerView.addSubview(exampleLabel)
backgroundView.addSubview(containerView)
view.addSubview(backgroundView)
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor, constant: origTrailing)
NSLayoutConstraint.activate([
// constrain backgroundView top to top + 80
backgroundView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80.0),
// constrain backgroundView leading / trailing to leading / trailing with 40-pt "padding"
backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
// constrain height to 100
backgroundView.heightAnchor.constraint(equalToConstant: 100.0),
// constrain containerView top / bottom to backgroundView top / bottom with 8-pt padding
containerView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: 8.0),
containerView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -8.0),
// activate leading / trailing constraints
leadingConstraint,
trailingConstraint,
// constrain the example label centered in the container view
exampleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
exampleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
])
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the containerView - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
// update current vars
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
default:
break
}
}
}
That code will produce this:
A red view with a centered label, inside a cyan view. You can drag the red "container" view left and right.
Add a view controller to a new project and assign its Custom Class to DragTestViewController from the above code. There are no #IBOutlet or #IBAction connections, so you should be able to run it as-is. See if you can drag the red view.
Using that as a starting point, we can get this:
with this code:
// simple rounded-corner shadowed view
class ShadowRoundedView: UIView {
let shadowLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.layer.addSublayer(shadowLayer)
clipsToBounds = false
backgroundColor = .clear
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.black.cgColor
shadowLayer.shadowOffset = CGSize(width: 0.0, height: 1.0)
shadowLayer.shadowRadius = 4.0
shadowLayer.shadowOpacity = 0.6
shadowLayer.shouldRasterize = true
shadowLayer.rasterizationScale = UIScreen.main.scale
}
override func layoutSubviews() {
super.layoutSubviews()
let pth = UIBezierPath(roundedRect: bounds, cornerRadius: 16.0)
shadowLayer.path = pth.cgPath
}
}
// simple rounded button
class RoundedButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height * 0.5
}
}
class DragRevealCell: UITableViewCell {
// callback closure for button taps
var callback: ((Int) -> ())?
// this will hold the "visible" labels, and will initially cover the buttons
let containerView: ShadowRoundedView = {
let v = ShadowRoundedView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// this will hold the buttons
let buttonsView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.clipsToBounds = true
return v
}()
// a "delete" button
let deleteButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Delete", for: [])
v.setTitleColor(.blue, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .white
return v
}()
// an "update" button
let updateButton: RoundedButton = {
let v = RoundedButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Update", for: [])
v.setTitleColor(.white, for: [])
v.setTitleColor(.lightGray, for: .highlighted)
v.backgroundColor = .blue
return v
}()
// single label for this example cell
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
return v
}()
// leading and trailing constraints for the container view
private var leadingConstraint: NSLayoutConstraint!
private var trailingConstraint: NSLayoutConstraint!
private let origLeading = CGFloat(8.0)
private let origTrailing = CGFloat(-8.0)
private var currentLeading = CGFloat(0.0)
private var currentTrailing = CGFloat(0.0)
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// cell background color
backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// add buttons to buttons container view
buttonsView.addSubview(deleteButton)
buttonsView.addSubview(updateButton)
// add label to container view -- this is where you would add all your labels, stack views, image views, etc.
containerView.addSubview(myLabel)
// add buttons view first
addSubview(buttonsView)
// add container view second - this will "overlay" it on top of the buttons view
addSubview(containerView)
// containerView leading / trailing constraints - these will be updated as we drag
leadingConstraint = containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: origLeading)
trailingConstraint = containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: origTrailing)
// needed to avoid layout warnings
let bottomConstraint = containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0)
bottomConstraint.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
leadingConstraint,
trailingConstraint,
bottomConstraint,
myLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0),
myLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20.0),
myLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20.0),
myLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -8.0),
myLabel.heightAnchor.constraint(equalToConstant: 120.0),
buttonsView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),
buttonsView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
deleteButton.topAnchor.constraint(equalTo: buttonsView.topAnchor, constant: 0.0),
deleteButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
deleteButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.bottomAnchor.constraint(equalTo: buttonsView.bottomAnchor, constant: 0.0),
updateButton.leadingAnchor.constraint(equalTo: buttonsView.leadingAnchor, constant: 8.0),
updateButton.trailingAnchor.constraint(equalTo: buttonsView.trailingAnchor, constant: -8.0),
updateButton.topAnchor.constraint(equalTo: deleteButton.bottomAnchor, constant: 12.0),
updateButton.heightAnchor.constraint(equalTo: deleteButton.heightAnchor),
updateButton.widthAnchor.constraint(equalTo: deleteButton.widthAnchor),
deleteButton.widthAnchor.constraint(equalToConstant: 120.0),
deleteButton.heightAnchor.constraint(equalToConstant: 40.0),
])
// delete button border
deleteButton.layer.borderColor = UIColor.blue.cgColor
deleteButton.layer.borderWidth = 1.0
// targets for button taps
deleteButton.addTarget(self, action: #selector(self.deleteTapped(_:)), for: .touchUpInside)
updateButton.addTarget(self, action: #selector(self.updateTapped(_:)), for: .touchUpInside)
// pan gesture recognizer
let p = UIPanGestureRecognizer(target: self, action: #selector(self.drag(_:)))
containerView.addGestureRecognizer(p)
}
#objc func drag(_ g: UIPanGestureRecognizer) -> Void {
// when we get a Pan on the container view - a "drag" ...
guard let sv = g.view?.superview else {
return
}
let translation = g.translation(in: sv)
switch g.state {
case .began:
currentLeading = leadingConstraint.constant
currentTrailing = trailingConstraint.constant
case .changed:
// only track left-right dragging
// don't allow drag-to-the-right
if currentLeading + translation.x <= origLeading {
leadingConstraint.constant = currentLeading + translation.x
trailingConstraint.constant = currentTrailing + translation.x
}
default:
// if the drag-left did not fully reveal the buttons, animate the container view back in place
if containerView.frame.maxX > buttonsView.frame.minX {
self.leadingConstraint.constant = self.origLeading
self.trailingConstraint.constant = self.origTrailing
UIView.animate(withDuration: 0.3, animations: {
self.layoutIfNeeded()
}, completion: { _ in
//self.dragX = 0.0
})
}
}
}
#objc func deleteTapped(_ sender: Any?) -> Void {
callback?(0)
}
#objc func updateTapped(_ sender: Any?) -> Void {
callback?(1)
}
}
class DragRevealTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(DragRevealCell.self, forCellReuseIdentifier: "DragRevealCell")
tableView.separatorStyle = .none
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "DragRevealCell", for: indexPath) as! DragRevealCell
c.myLabel.text = "Row \(indexPath.row)" + "\n" + "This is where you would populate the cell's labels, image views, any other UI elements, etc."
c.selectionStyle = .none
c.callback = { value in
if value == 0 {
print("Delete action")
} else {
print("Update action")
}
}
return c
}
}
Add a UITableViewController the project and assign its Custom Class to DragRevealTableViewController from the above code. Again, there are no #IBOutlet or #IBAction connections, so you should be able to run it as-is.
NOTE: This is example code only, and should not be considered "production ready"!!! It is only partially implemented and will likely need quite a bit more work. But, it may give you a good starting point.

Cannot move UILabel position programatically

I have a file for the View section of app, where i have all the labels and images that i intent to use, this is what i have in my DetailViewTableCell class, which inherits from UIView.
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
}
Now i move to my DetailViewController class, here i try and add the label, the label is added but it appears always at top left corner at 0,0 coordinate, when i try and add constraints for position, i always get error, now i can try
detailMain.detailName.frame.origin.x = 30
but i get error:
Constraint items must each be a view or layout guide.
In any case i do not wish to use this approach but more something like this
NSLayoutConstraint(item: detailMain.detailName, attribute: .leading, relatedBy: .equal, toItem: detailMain.detailName.superview, attribute: .leading, multiplier: 1, constant: 20).isActive = true
but i get the same above error, my over all code is this:
self.view.addSubview(detailMain.detailName)
detailMain.detailName.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailName.heightAnchor.constraint(equalToConstant: 25).isActive = true
detailMain.detailName.widthAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
detailMain.detailName.font = UIFont(name: "Rubik-Medium", size: 30)
detailMain.detailName.backgroundColor = UIColor.white
detailMain.detailName.textColor = UIColor.black
Which works perfectly fine but the moment i try and constraints, the error come up, this is how the app shows up with out constraints and name at top most left corner
////////UPDATE
So here is my new DetailViewTableCell,
import UIKit
class DetailViewTableCell: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and this 2 lines is what i add to my Detail view controller in viewDidLoad
let v = DetailViewTableCell()
detailTableView.tableHeaderView = v
Also i add this function
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = detailTableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
detailTableView.tableHeaderView = headerView
detailTableView.layoutIfNeeded()
}
}
then in my viewForHeaderInSection inbuilt function i add this
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView()
tableView.rowHeight = 80
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
headerView.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.translatesAutoresizingMaskIntoConstraints = false
detailMain.detailMainImage.heightAnchor.constraint(equalToConstant: 400).isActive = true
detailMain.detailMainImage.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
detailMain.detailMainImage.image = UIImage(named: restaurant.image)
detailMain.detailMainImage.contentMode = .scaleAspectFill
detailMain.detailMainImage.clipsToBounds = true
headerView.addSubview(detailMain.detailMainImage)
//Add the name
detailMain.detailName.text = restaurant.name
headerView.addSubview(detailMain.detailName)
return headerView
}
but still same position , is there any thing i add to add or remove from my code
You didn't show where you *want the labels, but this should get you going...
In your "header view" class:
add your elements: detailMainImage, detailName, etc...
set their properties and constraints as desired
You can get auto-layout to use the constraints you've setup in the header view to automatically determine its height:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
So, here's a complete example:
class TestHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// instantiate the header view
let v = DetailTableHeaderView()
// set it as the tableHeaderView
tableView.tableHeaderView = v
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// this is needed to allow the header view's content
// to determine its height
guard let headerView = tableView.tableHeaderView else {
return
}
let size = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
if headerView.frame.size.height != size.height {
headerView.frame.size.height = size.height
tableView.tableHeaderView = headerView
tableView.layoutIfNeeded()
}
}
}
class DetailTableHeaderView: UIView {
var detailMainImage: UIImageView = UIImageView()
var detailName: UILabel = UILabel()
var detailType: UILabel = UILabel()
var detailHeart: UIImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
backgroundColor = .white
// give the heart image view a background color so we can see its frame
detailHeart.backgroundColor = .red
if let img = UIImage(named: "teacup") {
detailMainImage.image = img
}
// give labels some text so we can see them
detailType.text = "Detail Type"
detailName.text = "Detail Name"
// setup fonts for labels as desired
//detailName.font = UIFont(name: "Rubik-Medium", size: 30)
// I don't have "Rubik" so this is with the system font
detailName.font = UIFont.systemFont(ofSize: 30, weight: .bold)
detailName.backgroundColor = UIColor.white
detailName.textColor = UIColor.black
detailType.textColor = .white
[detailMainImage, detailName, detailType, detailHeart].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
addSubview($0)
}
// give main image a height
// set its Priority to 999 to prevent layout constraint conflict warnings
let mainImageHeightAnchor = detailMainImage.heightAnchor.constraint(equalToConstant: 300.0)
mainImageHeightAnchor.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
// constrain main image to all 4 sides
detailMainImage.topAnchor.constraint(equalTo: topAnchor),
detailMainImage.leadingAnchor.constraint(equalTo: leadingAnchor),
detailMainImage.trailingAnchor.constraint(equalTo: trailingAnchor),
detailMainImage.bottomAnchor.constraint(equalTo: bottomAnchor),
// activate the height contraint
mainImageHeightAnchor,
// constrain detailType label
// 30-pts from Leading
// 12-pts from Top
detailType.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailType.topAnchor.constraint(equalTo: topAnchor, constant: 12.0),
// constrain detailName label
// 30-pts from Leading
// 12-pts from Bottom
detailName.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 30.0),
detailName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12.0),
// constrain detailHeart image
// 12-pts from Trailing
// 12-pts from Bottom
// width: 24 height: equal to width (1:1 square)
detailHeart.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
detailHeart.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
detailHeart.widthAnchor.constraint(equalToConstant: 24),
detailHeart.heightAnchor.constraint(equalTo: detailHeart.widthAnchor),
])
}
}
For this example, I just set the background color of the "heart" image view to red, and I clipped the teacup out of your image:
And this is the result:
Edit - to use the custom view as a Section header view...
Use the same DetailTableHeaderView class from above, but change the table view controller as follows:
class TestSectionHeaderTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.sectionHeaderHeight = UITableView.automaticDimension
tableView.estimatedSectionHeaderHeight = 300
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
let v = DetailTableHeaderView()
// for example implementation...
if let img = UIImage(named: "teacup") {
v.detailMainImage.image = img
}
v.detailName.text = "Testing the Name"
// for your implementation...
//if let img = UIImage(named: restaurant.image) {
// v.detailMainImage.image = img
//}
//v.detailName.text = restaurant.name
return v
}
return nil;
}
}

How to Implement UITextView inside UITableView inside UITableView Swift

how to implement UITextView inside UITableView inside UITableView ??
(1) If you type text in 'UITextView', the height of 'UITableViewCell2' is automatically increased.
(2) When the height of 'UITableViewCell2' is increased, the height of 'UITableViewCell' is automatically increased accordingly.
I have implemented the case of (1) but not (2).
How should I implement it?
Nesting table views may not be the ideal solution for this, but your UITableViewCell would need to estimate and measure the whole height of the embedded UITableView, and propagate changes up to the parent table.
You might want to give this a try...
It using a single-section table view. Each cell contains a UIStackView that arranges the (variable) UITextViews.
No #IBOutlet or #IBAction or prototype cell connections... just assign a standard UIViewController custom class to TableTextViewsViewController:
//
// TableTextViewsViewController.swift
// Created by Don Mag on 3/10/20.
//
import UIKit
class TextViewsCell: UITableViewCell, UITextViewDelegate {
let frameView: UIView = {
let v = UIView()
v.backgroundColor = .clear
v.layer.borderColor = UIColor(red: 0.0, green: 0.5, blue: 0.0, alpha: 1.0).cgColor
v.layer.borderWidth = 1
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let stackViewPadding: CGFloat = 8.0
var textViewCosure: ((Int, String)->())?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let g = contentView.layoutMarginsGuide
contentView.addSubview(frameView)
frameView.addSubview(stackView)
// bottom constraint needs to be less than 1000 (required) to avoid auot-layout warnings
let frameViewBottomConstrait = frameView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
frameViewBottomConstrait.priority = UILayoutPriority(rawValue: 999)
NSLayoutConstraint.activate([
frameView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
frameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
frameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
frameViewBottomConstrait,
stackView.topAnchor.constraint(equalTo: frameView.topAnchor, constant: stackViewPadding),
stackView.leadingAnchor.constraint(equalTo: frameView.leadingAnchor, constant: stackViewPadding),
stackView.trailingAnchor.constraint(equalTo: frameView.trailingAnchor, constant: -stackViewPadding),
stackView.bottomAnchor.constraint(equalTo: frameView.bottomAnchor, constant: -stackViewPadding),
])
}
override func prepareForReuse() {
super.prepareForReuse()
stackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
}
func fillData(_ strings: [String]) -> Void {
strings.forEach {
let v = UITextView()
v.font = UIFont.systemFont(ofSize: 16.0)
v.isScrollEnabled = false
// hugging and compression resistance set to required for cell expansion animation
v.setContentHuggingPriority(.required, for: .vertical)
v.setContentCompressionResistancePriority(.required, for: .vertical)
v.text = $0
// frame the text view
v.layer.borderColor = UIColor.blue.cgColor
v.layer.borderWidth = 1
v.delegate = self
stackView.addArrangedSubview(v)
}
}
func textViewDidChange(_ textView: UITextView) {
guard let idx = stackView.arrangedSubviews.firstIndex(of: textView) else {
fatalError("Shouldn't happen, but couldn't find the textView index")
}
textViewCosure?(idx, textView.text)
}
}
class TableTextViewsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let topLabel: UILabel = {
let v = UILabel()
v.text = "Top Label"
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let tableView: UITableView = {
let v = UITableView()
v.layer.borderColor = UIColor.red.cgColor
v.layer.borderWidth = 1
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var myData: [[String]] = [[String]]()
var textViewsInRows: [Int] = [
3, 4, 2, 6, 1, 4, 3,
]
override func viewDidLoad() {
super.viewDidLoad()
// generate some dummy data
var i = 1
textViewsInRows.forEach {
var s: [String] = [String]()
for j in 1...$0 {
s.append("Table Row: \(i) TextView \(j)")
}
myData.append(s)
i += 1
}
view.addSubview(topLabel)
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
topLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
topLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
topLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
tableView.topAnchor.constraint(equalTo: topLabel.bottomAnchor, constant: 8.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
])
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
tableView.keyboardDismissMode = .onDrag
tableView.register(TextViewsCell.self, forCellReuseIdentifier: "TextViewsCell")
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TextViewsCell", for: indexPath) as! TextViewsCell
cell.fillData(myData[indexPath.row])
cell.textViewCosure = { [weak self] idx, str in
// update our data
self?.myData[indexPath.row][idx] = str
// update table view cell height
self?.tableView.beginUpdates()
self?.tableView.endUpdates()
}
return cell
}
}
Result - red border is the tableView, green border is each cell's contentView, blue border is each textView:

Adding more tableViews as subview on my main scrollView depending on a UISwitch state works only after application relaunches

UPDATED BELOW!
I have a UI structure with a horizontal scrollView nesting 5 tableViews – each representing a day of the week. I have added a UISwitch to add weekend to the week, so when the user switches it on, two more tableview-subviews are added to the scrollView. So far so good, but the switch change only takes effect, when I relaunch the application. Looks like ViewDidLoad() makes it happen, but nothing else. I added a Bool variable called isWeekOn. Its state is managed from viewDidLoad:
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
dayTableViews = fiveOrSevenDayTableViews()
where fiveOrSevenTableViews() is a closure returning the array of tableviews with the proper count and dayTableViews is my local array variable.
lazy var fiveOrSevenDayTableViews: () -> [DayTableView] = {
if self.isWeekendOn == false {
return [self.mondayTableView, self.tuesdayTableview, self.wednesdayTableview, self.thursdayTableView, self.fridayTableView]
} else {
return [self.mondayTableView, self.tuesdayTableview, self.wednesdayTableview, self.thursdayTableView, self.fridayTableView, self.saturdayTableView,self.sundayTableView]
}
}
I added a didSet property observer to isWeekendOn and that also calls setupViews(), where the number of tableviews is also decided by calling fiveOrSevenTableViews closure .
var isWeekendOn: Bool = false {
didSet {
print("LessonVC IsWeekendon: ",isWeekendOn)
dayTableViews = fiveOrSevenDayTableViews()
setupViews()
print("didset daytableviews", fiveOrSevenDayTableViews().count)
}
}
Where my setupViews() looks like:
func setupViews() {
setupScrollView()
let numberOfTableViews = CGFloat(dayTableViews.count)
let stackView = UIStackView(arrangedSubviews: fiveOrSevenDayTableViews())
print("setupViews stacview subviews count", stackView.arrangedSubviews.count)
stackView.axis = .horizontal
stackView.distribution = .fillEqually
scrollView.addSubview(stackView)
setupStackViewConstraints(stackView, numberOfTableViews)
}
And setupScrollView():
private func setupScrollView() {
let numberOfTableViews = CGFloat(dayTableViews.count)
print("setupScrollview dableviews", numberOfTableViews)
scrollView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height:0)
scrollView.contentSize = CGSize(width: view.frame.width * numberOfTableViews, height: 0)
view.addSubview(scrollView)
setupScrollviewConstraints()
}
All the print statements are called properly, so I am wondering, why the changes actually do not take effect real time, and instead working only relaunch.
What I tried:
As #maniponken suggested, i made a function which looks like:
func readdStackView(_ stackView: UIStackView) { stackView.removeFromSuperview()
setupViews() }
than I call this within the isWeekendOn didSet observer. Didn't work out unfortunately.
UPDATE:
Actually when I put anything in my isWeekendon didSet observer, doesn't work! For example changing my navigationBar backgroundColor...etc Everything is reflecting on console though, in the print statements! Those functions also take effect at relaunch only.I have no idea what I am doing wrong.
UPDATE2:
Removing the tables works without problem with a local UIButton! My Problem is the following though: I have a settings view controller, which has a switch for setting 5 or 7 table views. Realtime update does not work with that switch, only with le local button, triggering an #objc func. I still need that settings panel for the user though!
Try this, it's not a stackview but it works for adding (and removing) tableviews to a ViewController.
This method is not using Storyboards
In your viewcontroller, containing the tableview
import Foundation
import UIKit
class SevenTableviews: UIViewController, UITableViewDelegate, UITableViewDataSource {
let tableView1: UITableView = {
let tv = UITableView()
tv.backgroundColor = .white
tv.separatorStyle = .none
return tv
}()
let tableView2: UITableView = {
let tv = UITableView()
tv.backgroundColor = .white
tv.separatorStyle = .none
return tv
}()
let tableSwitch: UISwitch = {
let switchBtn = UISwitch()
switchBtn.addTarget(self, action: #selector(switchTables), for: .touchUpInside)
return switchBtn
}()
var isTableTwoShowing = false
let reuseIdentifier = "DaysCell"
var days = ["monday", "tuesday", "wednesday", "thursday", "friday"]
var weekendDays = ["saturday", "sunday"]
override func viewDidLoad() {
super.viewDidLoad()
setupTableview()
}
func setupTableview() {
tableView1.dataSource = self
tableView1.delegate = self
tableView1.register(DaysTableviewCell.self, forCellReuseIdentifier: reuseIdentifier)
view.addSubview(tableView1)
tableView1.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.leftAnchor, bottom: view.centerYAnchor, right: view.rightAnchor)
if isTableTwoShowing == true {
tableView2.dataSource = self
tableView2.delegate = self
tableView2.register(DaysTableviewCell.self, forCellReuseIdentifier: reuseIdentifier)
view.addSubview(tableView2)
tableView2.anchor(top: view.centerYAnchor, left: view.leftAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, right: view.rightAnchor)
}
view.addSubview(tableSwitch)
tableSwitch.anchor(bottom: view.safeAreaLayoutGuide.bottomAnchor, right: view.rightAnchor, paddingBottom: 24, paddingRight: 12)
}
#objc func switchTables() {
if tableSwitch.isOn {
isTableTwoShowing = true
setupTableview()
} else {
isTableTwoShowing = false
tableView2.removeFromSuperview()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == tableView1 {
return days.count
} else if tableView == tableView2 {
return weekendDays.count
} else {
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier) as! DaysTableviewCell
if tableView == tableView1 {
cell.dateLabel.text = days[indexPath.row]
return cell
} else {
cell.dateLabel.text = weekendDays[indexPath.row]
return cell
}
}
}
in your tableviewCell-class:
import Foundation
import UIKit
class DaysTableviewCell: UITableViewCell {
let identifier = "DaysCell"
let cellContainer: UIView = {
let view = UIView()
view.backgroundColor = .white
view.backgroundColor = Colors.boxBack
view.setCellShadow()
return view
}()
let dateLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 20)
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
func setupViews() {
selectionStyle = .none
addSubview(cellContainer)
cellContainer.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 4, paddingLeft: 8, paddingBottom: 4, paddingRight: 8, height: 35)
cellContainer.addSubview(dateLabel)
dateLabel.anchor(top: cellContainer.topAnchor, left: cellContainer.leftAnchor, bottom: cellContainer.bottomAnchor, right: cellContainer.rightAnchor, paddingTop: 4, paddingLeft: 8, paddingBottom: 4, paddingRight: 8)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I am using the same cell class for both tableviews but you can decide yourself how you want to do this.
Also, my constraints are set with an extension i found from a tutorial once:
extension UIView {
func anchor(top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, paddingTop: CGFloat? = 0, paddingLeft: CGFloat? = 0, paddingBottom: CGFloat? = 0, paddingRight: CGFloat? = 0, width: CGFloat? = nil, height: CGFloat? = nil) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop!).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddingLeft!).isActive = true
}
if let bottom = bottom {
if let paddingBottom = paddingBottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
}
if let right = right {
if let paddingRight = paddingRight {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
}
if let width = width {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if let height = height {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
}
Hope this helps
Couple notes:
You don't need to re-create / re-add your stack view ever time the switch gets changed. Add it in viewDidLoad() and then add / remove the "DayTableViews"
Use constraints for your stack view inside your scroll view, instead of calculating .contentSize.
Probably want to use an array of your "Day Tables" rather than having individual mondayTableView, tuesdayTableView, etc... vars.
Here's an example you can work from. I used a simple UIView with a centered label as a simulated "DayTableView" - should be pretty clear. Everything is via code - no #IBOutlet or #IBAction - so to test this, create a new project, add this code, and assign the startup view controller to AddToScrollViewController:
//
// AddToScrollViewController.swift
//
// Created by Don Mag on 11/15/19.
//
import UIKit
class DayTableView: UIView {
// simple UIView with a centered label
// this is just simulatig a UITableView
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .yellow
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class AddToScrollViewController: UIViewController {
let theSwitch: UISwitch = {
let v = UISwitch()
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .orange
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
v.spacing = 16
return v
}()
let mondayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Monday"
return v
}()
let tuesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Tuesday"
return v
}()
let wednesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Wednesday"
return v
}()
let thursdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Thursday"
return v
}()
let fridayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Friday"
return v
}()
let saturdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Saturday"
return v
}()
let sundayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Sunday"
return v
}()
var dayTableViews: [DayTableView] = [DayTableView]()
lazy var fiveOrSevenDayTableViews: () -> [DayTableView] = {
if self.isWeekendOn == false {
return [self.mondayTableView, self.tuesdayTableView, self.wednesdayTableView, self.thursdayTableView, self.fridayTableView]
} else {
return [self.mondayTableView, self.tuesdayTableView, self.wednesdayTableView, self.thursdayTableView, self.fridayTableView, self.saturdayTableView,self.sundayTableView]
}
}
var isWeekendOn: Bool = false {
didSet {
print("LessonVC IsWeekendon: ",isWeekendOn)
dayTableViews = fiveOrSevenDayTableViews()
setupViews()
print("didset daytableviews", fiveOrSevenDayTableViews().count)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// for each of these views...
[theSwitch, scrollView, stackView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
}
// for each of these views...
[mondayTableView, tuesdayTableView, wednesdayTableView, thursdayTableView, fridayTableView, saturdayTableView, sundayTableView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
// constrain widths to 160 (change to desired table view widths)
$0.widthAnchor.constraint(equalToConstant: 160.0).isActive = true
// give them a background color so we can see them
$0.backgroundColor = .systemBlue
}
// add the (empty) stack view to the scroll view
scrollView.addSubview(stackView)
// add the switch to the view
view.addSubview(theSwitch)
// add the scroll view to the view
view.addSubview(scrollView)
// use safe area for view elements
let g = view.safeAreaLayoutGuide
// we need to constrain the scroll view contents (the stack view, in this case)
// to the contentLayoutGuide so auto-layout can handle the content sizing
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// put switch in top-left corner
theSwitch.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
theSwitch.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
// constrain scroll view 12-pts below the switch
// and leading / trailing / bottom at Zero
scrollView.topAnchor.constraint(equalTo: theSwitch.bottomAnchor, constant: 12.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain the stack view to the scroll view's contentLayoutGuide
// with 8-pts padding on each side (easier to see the framing)
stackView.topAnchor.constraint(equalTo: sg.topAnchor, constant: 8.0),
stackView.bottomAnchor.constraint(equalTo: sg.bottomAnchor, constant: -8.0),
stackView.leadingAnchor.constraint(equalTo: sg.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: sg.trailingAnchor, constant: -8.0),
// constrain height of stack view to height of scroll view frame,
// minus 16-pts (for 8-pt padding)
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, constant: -16),
])
// add a target for the switch
theSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
// set based on saved state in UserDefaults
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
}
#objc func switchChanged(_ sender: Any) {
// switch was tapped (toggled on/off)
if let v = sender as? UISwitch {
// update state in UserDefaults
UserDefaults.standard.set(v.isOn, forKey: "switchState")
// update the UI
isWeekendOn = v.isOn
}
}
func setupViews() {
// first, remove any existing table views
stackView.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
// get the array of 5 or 7 table views
let a = fiveOrSevenDayTableViews()
// add the table views to the stack view
a.forEach {
stackView.addArrangedSubview($0)
}
print("setupViews stacview subviews count", stackView.arrangedSubviews.count)
}
}
Scrolled to the right with the "weekend switch" off:
Scrolled to the right immediately after turning the "weekend switch" on:
Edit
Here is a slightly different (bit more efficient) approach. Instead of adding / removing table views, simply show / hide the Saturday and Sunday tables. The stack view will automatically handle the scroll view's content size.
Full updated example:
//
// AddToScrollViewController.swift
//
// Created by Don Mag on 11/15/19.
//
import UIKit
class DayTableView: UIView {
// simple UIView with a centered label
// this is just simulatig a UITableView
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.backgroundColor = .yellow
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}
class AddToScrollViewController: UIViewController {
let theSwitch: UISwitch = {
let v = UISwitch()
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.backgroundColor = .orange
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.distribution = .fillEqually
v.spacing = 16
return v
}()
let mondayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Monday"
return v
}()
let tuesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Tuesday"
return v
}()
let wednesdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Wednesday"
return v
}()
let thursdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Thursday"
return v
}()
let fridayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Friday"
return v
}()
let saturdayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Saturday"
return v
}()
let sundayTableView: DayTableView = {
let v = DayTableView()
v.theLabel.text = "Sunday"
return v
}()
var isWeekendOn: Bool = false {
didSet {
print("LessonVC IsWeekendon: ",isWeekendOn)
setupViews()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// for each of these views...
[theSwitch, scrollView, stackView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
}
// for each of these views...
[mondayTableView, tuesdayTableView, wednesdayTableView, thursdayTableView, fridayTableView, saturdayTableView, sundayTableView].forEach {
// we're going to use auto-layout
$0.translatesAutoresizingMaskIntoConstraints = false
// constrain widths to 160 (change to desired table view widths)
$0.widthAnchor.constraint(equalToConstant: 160.0).isActive = true
// give them a background color so we can see them
$0.backgroundColor = .systemBlue
// add them to the stack view
stackView.addArrangedSubview($0)
}
// add the stack view to the scroll view
scrollView.addSubview(stackView)
// add the switch to the view
view.addSubview(theSwitch)
// add the scroll view to the view
view.addSubview(scrollView)
// use safe area for view elements
let g = view.safeAreaLayoutGuide
// we need to constrain the scroll view contents (the stack view, in this case)
// to the contentLayoutGuide so auto-layout can handle the content sizing
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// put switch in top-left corner
theSwitch.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
theSwitch.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
// constrain scroll view 12-pts below the switch
// and leading / trailing / bottom at Zero
scrollView.topAnchor.constraint(equalTo: theSwitch.bottomAnchor, constant: 12.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain the stack view to the scroll view's contentLayoutGuide
// with 8-pts padding on each side (easier to see the framing)
stackView.topAnchor.constraint(equalTo: sg.topAnchor, constant: 8.0),
stackView.bottomAnchor.constraint(equalTo: sg.bottomAnchor, constant: -8.0),
stackView.leadingAnchor.constraint(equalTo: sg.leadingAnchor, constant: 8.0),
stackView.trailingAnchor.constraint(equalTo: sg.trailingAnchor, constant: -8.0),
// constrain height of stack view to height of scroll view frame,
// minus 16-pts (for 8-pt padding)
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, constant: -16),
])
// add a target for the switch
theSwitch.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
// set based on saved state in UserDefaults
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
// update the switch UI
theSwitch.isOn = isWeekendOn
}
#objc func switchChanged(_ sender: Any) {
// switch was tapped (toggled on/off)
if let v = sender as? UISwitch {
// update state in UserDefaults
UserDefaults.standard.set(v.isOn, forKey: "switchState")
// update the UI
isWeekendOn = v.isOn
}
}
func setupViews() {
// show or hide Sat and Sun table views
saturdayTableView.isHidden = !isWeekendOn
sundayTableView.isHidden = !isWeekendOn
}
}
Why don’t you make a horizontal collection view instead of the normal scroll view. It would be easier to call reloadData whenever you want to add or delete a cell (and of course each cell is a tableView)
Finally I have solved the problem.
UPDATE: The main reason I had to set up NotificationCenter for this matter, is that I used UITabBarController to add SettingsVC to my app instead of presenting it modally. Details below.
//Skip this part for answer
My main problem – as it turned out – was that my UISwitch was on a separate vc, called SettingsViewController.
This switch supposed to do the tableview-adding and removing on my main vc. I tried with delegate protocols, targeting shared instance of settingsVC, nothing worked, but adding a local button for this – which is definitely not what I wanted.
Then I read about NotificationCenter!
I remembered it from Apples App Development For Swift book,I read last year, but forgot since.
// So the Anwswer
After I set my constraints correctly based on the great hint of #DonMag, I set up NotificationCenter for my SettingsViewController, posting to my Main VC.
class SettingsViewController: UITableViewController {
private let reuseID = "reuseId"
lazy var switchButton: UISwitch = {
let sw = UISwitch()
sw.addTarget(self, action: #selector(switchPressed), for: .valueChanged)
sw.onTintColor = AdaptiveColors.navigationBarColor
return sw
}()
static let switchNotification = Notification.Name("SettingsController.switchNotification")
var isOn = Bool() {
didSet {
NotificationCenter.default.post(name:SettingsViewController.switchNotification, object: nil)
}
}
#objc func switchPressed(_ sender: UISwitch) {
UserDefaults.standard.set(sender.isOn, forKey: "switchState")
self.isOn = sender.isOn
}
then in the mainVC:
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
view.backgroundColor = .white
isWeekendOn = UserDefaults.standard.bool(forKey: "switchState")
// The Solution:
NotificationCenter.default.addObserver(self, selector: #selector(handleRefresh), name: SettingsViewController.switchNotification, object: nil)
dayTableViews = fiveOrSevenDayTableViews()
print("daytableviews count ", dayTableViews.count)
scrollView.delegate = self
editButtonItem.title = LocalizedString.edit
navigationItem.title = localizedDays[currentPage]
setupNavigationBar()
setupButtons()
setupTableViews()
setupViews()
isWeekendOn == true ? setupCurrentDayViewFor_7days() : setupCurrentDayViewFor_5days()
}
then here in mainVC's #objc func handleRefresh() { } i am handling the removal or addition!
UPDATE:
in SettingsVC:
static let switchOnNotification = Notification.Name("SettingsController.switchOnNotification")
static let switchOffNotification = Notification.Name("SettingsController.switchOffNotification")
var isOn = Bool() {
didSet {
}
willSet {
if newValue == true {
NotificationCenter.default.post(name:SettingsViewController.switchOnNotification, object: nil)
} else if newValue == false {
NotificationCenter.default.post(name:SettingsViewController.switchOffNotification, object: nil)
}
}
}
in viewDidLoad in mainVC:
NotificationCenter.default.addObserver(self, selector: #selector(handleAddWeekendTableViews), name: SettingsViewController.switchOnNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleRemoveWeekendTableViews), name: SettingsViewController.switchOffNotification, object: nil)
#objc func handleAddWeekendTableViews() {
[saturdayTableView, sundayTableView].forEach {
stackView.addArrangedSubview($0)
dayTableViews.append($0)
}
}
#objc func handleRemoveWeekendTableViews() {
manageCurrentPage()
dayTableViews.removeLast(2)
[saturdayTableView, sundayTableView].forEach {
$0.removeFromSuperview()
}
}
This one is actually working!

Resources