Autolayout aspect ratio constraints - ios

I want to set a subview constraint as follows. If the interface is landscape (view.bounds.width > view.bounds.height),set aspect ratio of subview to be 4:3. In portrait mode, it should be 3:4. While I can always change the constraints programmatically on auto rotation, wondering if there is a clever way of designing constraints that is only one time.

You'll have to listen for the UIDevice.orientationDidChangeNotification notification like in this example:
class ViewController3: UIViewController {
let blueSubview: UIView = {
let view = UIView()
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var blueSubviewHeight = NSLayoutConstraint()
var blueSubviewWidth = NSLayoutConstraint()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(refreshConstraints), name: UIDevice.orientationDidChangeNotification, object: nil)
refreshConstraints()
setUI()
}
fileprivate func setUI() {
view.backgroundColor = .white
view.addSubview(blueSubview)
[blueSubview.leftAnchor.constraint(equalTo: view.leftAnchor),
blueSubview.topAnchor.constraint(equalTo: view.topAnchor),
blueSubviewWidth,
blueSubviewHeight].forEach({ $0.isActive = true })
}
#objc func refreshConstraints() {
NSLayoutConstraint.deactivate([blueSubviewWidth, blueSubviewHeight])
if view.frame.width > view.frame.height {
blueSubviewWidth = blueSubview.widthAnchor.constraint(equalToConstant: view.frame.height * (4/3))
blueSubviewHeight = blueSubview.heightAnchor.constraint(equalToConstant: view.frame.height)
}
else {
blueSubviewWidth = blueSubview.widthAnchor.constraint(equalToConstant: view.frame.width)
blueSubviewHeight = blueSubview.heightAnchor.constraint(equalToConstant: view.frame.width * (4/3))
}
NSLayoutConstraint.activate([blueSubviewWidth, blueSubviewHeight])
}
}

Related

How to use UIScrollView to scroll to the top of the first view?

I currently have a UIScrollView with a UIImageView (top image) positioned below the UINavigationBar. However, I want to position the UIImageView at the very top of the screen (bottom image). Is there a way to implement this?
What I've tried so far: I added a UIScrollView extension (source) that is supposed to scroll down to the view parameter provided, but it hasn't worked for me.
extension UIScrollView {
// Scroll to a specific view so that it's top is at the top our scrollview
func scrollToView(view:UIView, animated: Bool) {
if let origin = view.superview {
// Get the Y position of your child view
let childStartPoint = origin.convert(view.frame.origin, to: self)
// Scroll to a rectangle starting at the Y of your subview, with a height of the scrollview
self.scrollRectToVisible(CGRect(x:0, y:childStartPoint.y,width: 1,height: self.frame.height), animated: animated)
}
}
// Bonus: Scroll to top
func scrollToTop(animated: Bool) {
let topOffset = CGPoint(x: 0, y: -contentInset.top)
setContentOffset(topOffset, animated: animated)
}
// Bonus: Scroll to bottom
func scrollToBottom() {
let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
if(bottomOffset.y > 0) {
setContentOffset(bottomOffset, animated: true)
}
}
}
class MealDetailsVC: UIViewController {
private var mealInfo: MealInfo
init(mealInfo: MealInfo) {
self.mealInfo = mealInfo
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
scrollView.scrollToView(view: iv, animated: false) // used extension from above
}
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
return scrollView
}()
lazy var iv: UIImageView = {
let iv = UIImageView()
iv.image = Image.defaultMealImage!
iv.contentMode = .scaleAspectFill
return iv
}()
}
extension MealDetailsVC {
func setupViews() {
addBackButton()
addSubviews()
autoLayoutViews()
constrainSubviews()
}
fileprivate func addBackButton() {
...
}
#objc func goBack(sender: UIBarButtonItem) {
...
}
fileprivate func addSubviews() {
view.addSubview(scrollView)
scrollView.addSubview(iv)
}
fileprivate func autoLayoutViews() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
iv.translatesAutoresizingMaskIntoConstraints = false
}
fileprivate func constrainSubviews() {
NSLayoutConstraint.activate([
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
NSLayoutConstraint.activate([
iv.topAnchor.constraint(equalTo: scrollView.topAnchor),
iv.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
iv.heightAnchor.constraint(equalTo: iv.widthAnchor, multiplier: 0.6)
])
}
}
This may help.
scrollView.contentInsetAdjustmentBehavior = .never
For more information,
This property specifies how the safe area insets are used to modify the content area of the scroll view. The default value of this property is UIScrollViewContentInsetAdjustmentAutomatic.
Don't set a height anchor and add a imageView centerXAnchor equal to ScrollView centerXAnchor constraint. set imageView.contentMode to .scaleAspectFit

How to fetch the width and height of a programatically added view in Swift/ iOS?

I have added a UIView and a UIImageView programatically, but I need its width and height.
Is it possible to fetch it, if yes, please share how to fetch it.
Code:
let View1: UIView = {
let viewView = UIView()
viewView.translatesAutoresizingMaskIntoConstraints = false
viewView.contentMode = .scaleAspectFit
print(UserDefaults.standard.bool(forKey: "backgroundColourSelected"))
if UserDefaults.standard.bool(forKey: "backgroundColourSelected") {
viewView.backgroundColor = self.viewColor
}else {
viewView.backgroundColor = .white
}
viewView.clipsToBounds = true
return viewView
}()
self.canvasView.addSubview(View1)
View1.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor, constant: 0).isActive = true
View1.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor, constant: 0).isActive = true
View1.widthAnchor.constraint(equalTo: canvasView.widthAnchor, constant: 0).isActive = true
View1.heightAnchor.constraint(equalTo: canvasView.widthAnchor, multiplier: aspectRatio).isActive = true
You need to know the life cycle of view controller.
To get height of any view use viewDidLayoutSubviews.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(view1.frame.size)
}
size property will print both final height and width.
use override func viewWillLayoutSubviews() for ViewController and override func layoutSubviews() for UIView.
Example : - for UIViewControllers
override func viewWillLayoutSubviews() {
print(yourView.frame.height)
}
for UIViews
override func layoutSubviews() {
print(yourView.frame.height)
print(yourView.frame.width)
}
Since your view is inside the function just move it out
class YourController / YourView {
private let imgView: UIImageView = {
let iv = UIImageView()
// do whatever you want with your image using iv. eg. iv.contentMode = .scaleAspectFit or iv.image = UIImage(named: "custom_img")
return iv
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(imgView)
//set constraints for your
}
// then use layoutSubViews(for views) / viewWillLayoutSubViews (for controller) and calculate the height
}

Custom resuable UIActivityIndicatorView Class programmatically

I'm new in making views programmatically.
I'm trying to make UIActivityIndicatorView class to make it reusable for me.
This is the class I made:
class ActivityIndicator: UIActivityIndicatorView {
let indicator = UIActivityIndicatorView()
let indicatorContainer = UIView()
func setupIndicatorView() {
indicatorContainer.isHidden = false
indicator.isHidden = false
indicator.style = .large
indicator.color = .white
indicator.startAnimating()
indicator.hidesWhenStopped = true
indicator.translatesAutoresizingMaskIntoConstraints = false
indicatorContainer.backgroundColor = .darkGray
indicatorContainer.alpha = 0.7
indicatorContainer.layer.cornerRadius = 8.0
indicatorContainer.translatesAutoresizingMaskIntoConstraints = false
addSubview(indicatorContainer)
indicatorContainer.addSubview(indicator)
func setupIndicatorContainerConstraints() {
NSLayoutConstraint.activate([
indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor),
indicatorContainer.widthAnchor.constraint(equalToConstant: frame.width / 5),
indicatorContainer.heightAnchor.constraint(equalToConstant: frame.width / 5)
])
}
func setupIndicatorViewConstraints() {
NSLayoutConstraint.activate([
indicator.centerXAnchor.constraint(equalTo: indicatorContainer.centerXAnchor),
indicator.centerYAnchor.constraint(equalTo: indicatorContainer.centerYAnchor)
])
}
setupIndicatorContainerConstraints()
setupIndicatorViewConstraints()
}
func hideIndicatorView() {
indicatorContainer.isHidden = true
indicator.stopAnimating()
indicator.isHidden = true
indicatorContainer.removeFromSuperview()
indicator.removeFromSuperview()
}
}
When I'm trying to make an instance from this class, it doesn't work in any other controller. Like this:
class SignInViewController: UIViewController {
let indicator = ActivityIndicator()
lazy var mainView: SignInView = {
let view = SignInView(delegate: self, frame: self.view.frame)
view.backgroundColor = .white
return view
}()
override func loadView() {
super.loadView()
view = mainView
}
func loginButtonTapped() {
indicator.setupIndicatorView()
}
}
I searched a lot to understand how to make it work but I haven't found a way.
You don't add it to vc's view
indicator.setupIndicatorView()
so consider
setupIndicatorView(_ view:UIView) {
.....
.....
// add it here
addSubview(indicatorContainer)
indicatorContainer.addSubview(indicator)
view.addSubview(self)
}

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!

How to update constraints of a view when other view is hidden using Snapkit?

There are three views from top to bottom: redView, yellowView, blueView.
I want to hide yellowView and change the constraint of blueView in order to let blueView below redView.
The picture is below:
this picture is original
this picture is what I want
The code is below:
private lazy var redView: UIView = {
let redView = UIView()
redView.backgroundColor = UIColor.red
return redView
}()
private lazy var yellowView: UIView = {
let yellowView = UIView()
yellowView.backgroundColor = UIColor.yellow
return yellowView
}()
private lazy var blueView: UIView = {
let blueView = UIView()
blueView.backgroundColor = UIColor.blue
return blueView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(redView)
view.addSubview(yellowView)
view.addSubview(blueView)
redView.snp.makeConstraints { (make) in
make.top.left.right.equalTo(view)
make.height.equalTo(40)
}
yellowView.snp.makeConstraints { (make) in
make.top.equalTo(redView.snp.bottom)
make.left.right.height.equalTo(redView)
}
blueView.snp.makeConstraints { (make) in
make.top.equalTo(yellowView.snp.bottom)
make.left.right.height.equalTo(yellowView)
}
}
if yellowView.isHidden == true {
//how is the code?
} else {
//how is the code?
}
You need set height constraint for yellow view and take IBOutlet for its height constraint of yellowView and modify its constant as per hide/show .
if yellowView.isHidden == true {
//how is the code?
ibHeightOutletOfYellow.constant = 0; // hide here
} else {
//how is the code?
ibHeightOutletOfYellow.constant = 50; // as per your needed
}

Resources