in an attempt to up my "getting away from massive view controllers" I am trying some stuff with view controller containment.
Here's what I want:
have a base class with a scroll view and an embedded stack view
inherit from that class with an implementation of my desired scene
being able to add child view controllers as needed to my implementation
I expect something like this:
Here's the code I am trying to achieve this with:
import UIKit
/**
Base class
*/
class PrototypeStackViewController: UIViewController {
private let scrollView: UIScrollView = {
let s = UIScrollView()
s.translatesAutoresizingMaskIntoConstraints = false
s.contentMode = .scaleToFill
return s
}()
private let stackView: UIStackView = {
let s = UIStackView()
s.translatesAutoresizingMaskIntoConstraints = false
s.axis = .vertical
s.alignment = .fill
s.distribution = .fill
s.contentMode = .scaleToFill
return s
}()
override func loadView() {
view = UIView()
view.backgroundColor = .systemGray
view.addSubview(scrollView)
scrollView.addSubview(stackView)
let nstraint = stackView.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor)
stackViewHeightConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
stackViewHeightConstraint
])
}
override func viewDidLoad() {
super.viewDidLoad()
}
func add(child: UIViewController) {
addChild(child)
stackView.addArrangedSubview(child.view)
child.didMove(toParent: self)
}
func remove(child: UIViewController) {
guard child.parent != nil else { return }
child.willMove(toParent: nil)
stackView.removeArrangedSubview(child.view)
child.view.removeFromSuperview()
child.removeFromParent()
}
}
/**
Implementation of my scene
*/
class PrototypeContainmentViewController: PrototypeStackViewController {
lazy var topViewController: PrototypeSubViewController = {
let t = PrototypeSubViewController()
t.view.backgroundColor = .systemRed
t.label.text = "Top View Controller"
return t
}()
lazy var centerViewController: PrototypeSubViewController = {
let t = PrototypeSubViewController()
t.view.backgroundColor = .systemGreen
t.label.text = "Center View Controller"
return t
}()
lazy var bottomViewController: PrototypeSubViewController = {
let t = PrototypeSubViewController()
t.view.backgroundColor = .systemBlue
t.label.text = "Bottom View Controller"
return t
}()
override func loadView() {
super.loadView()
add(child: topViewController)
add(child: centerViewController)
add(child: bottomViewController)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
/**
Sample View Controller
*/
class PrototypeSubViewController: UIViewController {
lazy var label: UILabel = {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
return l
}()
override func loadView() {
view = UIView()
view.backgroundColor = .systemRed
view.addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
Here's what I get:
If you look close, you can even see the "Bottom View Controller" label – but on top the green center view controller.
I am missing something here in my love-hate-relation with auto layout, that much is for sure...
You are missing the width constraint that ensures that the stack view fills the width of the scroll view:
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
Also I would set your label constraints in this way:
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
label.top.constraint(equalTo: view.topAnchor),
label.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
Had to make some tweaks to make it work.
First of all, to span over the whole width, I was missing stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), which Francesco Deliro correctly told me in the other answer.
Also, turns out I needed to change my stackViews distribution to .fillProportionally.
The code I was able to get what I showed on the question's picture is now this:
/**
Base Class
*/
class PrototypeStackViewController: UIViewController {
private let scrollView: UIScrollView = {
let s = UIScrollView()
s.translatesAutoresizingMaskIntoConstraints = false
s.contentMode = .scaleToFill
return s
}()
private let stackView: UIStackView = {
let s = UIStackView()
s.translatesAutoresizingMaskIntoConstraints = false
s.axis = .vertical
s.alignment = .fill
s.distribution = .fillProportionally //CHANGED to this
s.contentMode = .scaleToFill
return s
}()
override func loadView() {
view = UIView()
view.backgroundColor = .systemGray
view.addSubview(scrollView)
scrollView.addSubview(stackView)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
//CHANGED set of constraints here, seems as though width and heigt anchors are needed
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
stackView.heightAnchor.constraint(equalTo: scrollView.heightAnchor),
stackView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
])
}
override func viewDidLoad() {
super.viewDidLoad()
}
func add(child: UIViewController) {
addChild(child)
stackView.addArrangedSubview(child.view)
child.didMove(toParent: self)
}
func remove(child: UIViewController) {
guard child.parent != nil else { return }
child.willMove(toParent: nil)
stackView.removeArrangedSubview(child.view)
child.view.removeFromSuperview()
child.removeFromParent()
}
}
class PrototypeContainmentViewController: PrototypeStackViewController {
lazy var topViewController: PrototypeSubViewController = {
let t = PrototypeSubViewController()
t.view.backgroundColor = .systemRed
t.label.text = "Top View Controller"
return t
}()
lazy var centerViewController: PrototypeSubViewController = {
let t = PrototypeSubViewController()
t.view.backgroundColor = .systemGreen
t.label.text = "Center View Controller"
return t
}()
lazy var bottomViewController: PrototypeSubViewController = {
let t = PrototypeSubViewController()
t.view.backgroundColor = .systemBlue
t.label.text = "Bottom View Controller"
return t
}()
override func loadView() {
super.loadView()
add(child: topViewController)
add(child: centerViewController)
add(child: bottomViewController)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
class PrototypeSubViewController: UIViewController {
lazy var label: UILabel = {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
l.textAlignment = .center
return l
}()
override func loadView() {
view = UIView()
view.backgroundColor = .systemRed
view.addSubview(label)
NSLayoutConstraint.activate([
//CHANGED to this set of constraints
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
Hope that this answer will help future me and potentially others as well :-)
Related
I have a UIStackView initially set with 4 buttons. If I need to later swap out the last button with a new button or back to the initial button, how can I do that?
lazy var stackView: UIStackView = {
let sv = UIStackView()
sv.axis = .horizontal
sv.distribution = .fillEqually
sv.alignment = .fill
return sv
}()
// ...
var bt4: UIButton!
var bt5: UIButton!
// viewDidLoad
func configureStackView() {
view.addSubview(stackView)
stackView.addArrangedSubview(bt1)
stackView.addArrangedSubview(bt2)
stackView.addArrangedSubview(bt3)
stackView.addArrangedSubview(bt4)
// place stackView at bottom of scene
}
func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
if val {
// if true replace bt4 in stackView with bt5
} else {
// if false replace bt5 in stackView with bt4
}
}
You can do this quite easily, without the need to keep a reference to bt4 and bt5:
func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
// if true replace bt4 in stackView with bt5
// must have 5 buttons in the stack view
guard stackView.arrangedSubviews.count == 5 else { return }
stackView.arrangedSubviews[3].isHidden = val
stackView.arrangedSubviews[4].isHidden = !val
}
If you really want to keep a separate reference to the buttons, and add-to/remove-from the stack view, your can do this:
func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
// if true replace bt4 in stackView with bt5
let btnToShow: UIButton = val ? bt5 : bt4
// we only want to replace the button if it's not already there
guard let lastButton = stackView.arrangedSubviews.last as? UIButton,
lastButton != btnToShow
else { return }
lastButton.removeFromSuperview()
stackView.addArrangedSubview(btnToShow)
}
Here are complete examples...
First, using .isHidden approach:
class StackViewController: UIViewController {
lazy var stackView: UIStackView = {
let sv = UIStackView()
sv.axis = .horizontal
sv.distribution = .fillEqually
sv.alignment = .fill
sv.spacing = 12
return sv
}()
override func viewDidLoad() {
super.viewDidLoad()
configureStackView()
}
func configureStackView() {
for i in 1...5 {
let b = UIButton()
b.setTitle("\(i)", for: [])
b.backgroundColor = .red
stackView.addArrangedSubview(b)
}
// place stackView at bottom of scene
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
])
// add a couple "set val" buttons
let btnSV: UIStackView = {
let sv = UIStackView()
sv.axis = .horizontal
sv.distribution = .fillEqually
sv.alignment = .fill
sv.spacing = 12
return sv
}()
["True", "False"].forEach { t in
let b = UIButton()
b.setTitle(t, for: [])
b.backgroundColor = .blue
b.addTarget(self, action: #selector(setTrueFalse(_:)), for: .touchUpInside)
btnSV.addArrangedSubview(b)
}
btnSV.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btnSV)
NSLayoutConstraint.activate([
btnSV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
btnSV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
btnSV.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// start with button "5" hidden
swapLastButtonInStackViewWithNewButton(false)
}
#objc func setTrueFalse(_ sender: UIButton) {
guard let t = sender.currentTitle else { return }
swapLastButtonInStackViewWithNewButton(t == "True")
}
func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
// if true replace bt4 in stackView with bt5
// must have 5 buttons in the stack view
guard stackView.arrangedSubviews.count == 5 else { return }
stackView.arrangedSubviews[3].isHidden = val
stackView.arrangedSubviews[4].isHidden = !val
}
}
or, using a reference to bt4 and bt5 and adding/removing them:
class StackViewController: UIViewController {
lazy var stackView: UIStackView = {
let sv = UIStackView()
sv.axis = .horizontal
sv.distribution = .fillEqually
sv.alignment = .fill
sv.spacing = 12
return sv
}()
var bt4: UIButton!
var bt5: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
configureStackView()
}
func configureStackView() {
for i in 1...5 {
let b = UIButton()
b.setTitle("\(i)", for: [])
b.backgroundColor = .red
stackView.addArrangedSubview(b)
}
// place stackView at bottom of scene
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
])
// add a couple "set val" buttons
let btnSV: UIStackView = {
let sv = UIStackView()
sv.axis = .horizontal
sv.distribution = .fillEqually
sv.alignment = .fill
sv.spacing = 12
return sv
}()
["True", "False"].forEach { t in
let b = UIButton()
b.setTitle(t, for: [])
b.backgroundColor = .blue
b.addTarget(self, action: #selector(setTrueFalse(_:)), for: .touchUpInside)
btnSV.addArrangedSubview(b)
}
btnSV.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btnSV)
NSLayoutConstraint.activate([
btnSV.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
btnSV.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
btnSV.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this would go at the end of configureStackView(), but
// we'll put it here to keep the changes obvious
// references to btn4 and btn5
guard stackView.arrangedSubviews.count == 5,
let b4 = stackView.arrangedSubviews[3] as? UIButton,
let b5 = stackView.arrangedSubviews[4] as? UIButton
else {
fatalError("Bad setup - stackView does not have 5 buttons!")
}
bt4 = b4
bt5 = b5
// start with button "5" hidden
swapLastButtonInStackViewWithNewButton(false)
}
#objc func setTrueFalse(_ sender: UIButton) {
guard let t = sender.currentTitle else { return }
swapLastButtonInStackViewWithNewButton(t == "True")
}
func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
// if true replace bt4 in stackView with bt5
let btnToShow: UIButton = val ? bt5 : bt4
// we only want to replace the button if it's not already there
guard let lastButton = stackView.arrangedSubviews.last as? UIButton,
lastButton != btnToShow
else { return }
lastButton.removeFromSuperview()
stackView.addArrangedSubview(btnToShow)
}
}
Edit
The above code might seem a little overly complicated -- but I think that's more related to all of the setup and "extra" checks.
As a more straight-forward answer...
As long as you have setup your stack view and have valid references to bt4 and bt5, all you need to do is this:
func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
// if true replace bt4 in stackView with bt5
if val {
bt4.removeFromSuperview()
stackView.addArrangedSubview(bt5)
} else {
bt5.removeFromSuperview()
stackView.addArrangedSubview(bt4)
}
}
That will avoid the animation issues.
You can store the arrange of stack subviews like this:
lazy var stackViewArrangedSubviews = stackView.arrangedSubviews {
didSet {
setStackViewSubviews(with: stackViewArrangedSubviews)
}
}
and then
func setStackViewSubviews(with subviews: [UIView]) {
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
subviews.forEach { stackView.addArrangedSubview($0) }
}
and finally implement the swap function like this:
func swapLastButtonInStackViewWithNewButton(_ val: Bool) {
if val {
stackViewArrangedSubviews[3] = bt5
// if true replace bt4 in stackView with bt5
} else {
stackViewArrangedSubviews[3] = bt4
// if false replace bt5 in stackView with bt4
}
}
This is not perfect, You can improve the code by your need.
UIStackView automatically removes a view when this view is hidden. So basically all you have to do is to properly set the isHidden boolean of button 4 and button 5.
class ViewController: UIViewController {
#IBOutlet private weak var button4: UIButton!
#IBOutlet private weak var button5: UIButton!
private var showButton5 = false {
didSet {
button5.isHidden = !showButton5
button4.isHidden = showButton5
}
}
override func viewDidLoad() {
super.viewDidLoad()
showButton5 = false
}
#IBAction private func toggle() {
showButton5.toggle()
}
}
As suggested by some experts like #DonMag , i design the view in a separate controller, while trying to do so , every thing works, but when i use a navigation controller and there is a navigation bar at top, if i try and design in separate controller and then add its view to another controller the safeAreaLayoutGuide does not work and the view is attached to top of screen ignoring the safearea
SOLUTION as per #Mohammad Azam, DonMag solutions works as well , thanks
import UIKit
class NotesDesign: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func commonInit(){
let notesTitle = UITextField()
let notesContent = UITextView()
let font = UIFont(name: "CourierNewPS-ItalicMT", size: 20)
let fontM = UIFontMetrics(forTextStyle: .body)
notesTitle.font = fontM.scaledFont(for: font!)
notesContent.font = fontM.scaledFont(for: font!)
notesTitle.widthAnchor.constraint(greaterThanOrEqualToConstant:250).isActive = true
notesTitle.heightAnchor.constraint(equalToConstant: 30).isActive = true
notesTitle.borderStyle = .line
notesContent.widthAnchor.constraint(greaterThanOrEqualToConstant:250).isActive = true
notesContent.heightAnchor.constraint(greaterThanOrEqualToConstant: 100).isActive = true
notesContent.layer.borderWidth = 1
let notesStack = UIStackView()
notesStack.axis = .vertical
notesStack.spacing = 20
notesStack.alignment = .top
notesStack.distribution = .fill
notesStack.addArrangedSubview(notesTitle)
notesStack.addArrangedSubview(notesContent)
// Do any additional setup after loading the view.
notesStack.translatesAutoresizingMaskIntoConstraints = false
addSubview(notesStack)
notesStack.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
notesStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
notesStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10).isActive = true
notesStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -40).isActive = true
}
}
And where i call it
import UIKit
class AddNotesViewController: UIViewController {
var design = NotesDesign()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(design)
design.translatesAutoresizingMaskIntoConstraints = false
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(saveData))
}
#objc func saveData() {
}
}
And this is what i get
If you are taking a view from its ViewController and adding it as a subview to another view, you need to constrain it just like you would with any other added subview:
class AddNotesViewController: UIViewController {
var design = NotesViewDesign()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(design.view)
// a view loaded from a UIViewController has .translatesAutoresizingMaskIntoConstraints = true
design.view.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain the loaded view
design.view.topAnchor.constraint(equalTo: g.topAnchor),
design.view.leadingAnchor.constraint(equalTo: g.leadingAnchor),
design.view.trailingAnchor.constraint(equalTo: g.trailingAnchor),
design.view.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(saveData))
}
#objc func saveData() {
}
}
I created a ViewController, and I want to add in my ViewController a container view, here I set the container view inside my ViewController:
var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
func setUpViews() {
view.addSubview(containerView)
containerView.heightAnchor.constraint(equalToConstant: 300).isActive = true
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
}
Here I set my instance of SecondViewController in the containerView:
override func viewDidLoad() {
super.viewDidLoad()
setUpViews()
let secondViewController = SecondViewController()
secondViewController.willMove(toParent: self)
containerView.addSubview(secondViewController.view)
self.addChild(secondViewController)
secondViewController.didMove(toParent: self)
}
In my SecondViewController, I declared label and a view, I set the label in the center of the view:
let label: UILabel = {
let label = UILabel()
label.text = "Hello!"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(myView)
myView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
myView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
myView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
view.addSubview(label)
label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
That's what I see in my app, but I aspected to see a label in the center of the gray view.
It doesn't work like I aspected and I don't understand why.
You need to set the frame and/or constraints on the loaded view:
override func viewDidLoad() {
super.viewDidLoad()
setUpViews()
let secondViewController = SecondViewController()
secondViewController.willMove(toParent: self)
containerView.addSubview(secondViewController.view)
// set the frame
secondViewController.view.frame = containerView.bounds
// enable auto-sizing (for example, if the device is rotated)
secondViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.addChild(secondViewController)
secondViewController.didMove(toParent: self)
}
My code below is a basic scrollview using page control which uses basic colors on each screen. I want to display an imageview over the uiview which displays the basic colors. I want to call var pics from the asset file in Xcode.
var colors:[UIColor] = [UIColor.red, UIColor.blue, UIColor.green, UIColor.yellow]
var pics = ["a","b","c","d"]
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(scrollView)
for index in 0..<4 {
frame.origin.x = self.scrollView.frame.size.width * CGFloat(index)
frame.size = self.scrollView.frame.size
let subView = UIView(frame: frame)
subView.backgroundColor = colors[index]
self.scrollView .addSubview(subView)
}
func configurePageControl() {
self.pageControl.numberOfPages = colors.count
}
Here I suppose that you have 4 images attached in your project named 0,1,2,3 png , try this
import UIKit
class ViewController: UIViewController , UIScrollViewDelegate {
let scrollView = UIScrollView()
let pageCon = UIPageControl()
override func viewDidLoad() {
super.viewDidLoad()
let viewsCount = 4
var prevView = self.view!
scrollView.delegate = self
scrollView.isPagingEnabled = true
pageCon.numberOfPages = viewsCount
pageCon.currentPage = 0
pageCon.tintColor = .green
pageCon.currentPageIndicatorTintColor = .orange
pageCon.backgroundColor = .blue
pageCon.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.insertSubview(pageCon, aboveSubview: scrollView)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant:20),
scrollView.heightAnchor.constraint(equalToConstant: 400),
pageCon.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pageCon.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant:-20),
])
for i in 0..<viewsCount {
let imageV = UIImageView()
imageV.image = UIImage(named: "\(i).png")
imageV.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(imageV)
if prevView == self.view {
imageV.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
}
else {
imageV.leadingAnchor.constraint(equalTo: prevView.trailingAnchor).isActive = true
}
NSLayoutConstraint.activate([
imageV.topAnchor.constraint(equalTo: scrollView.topAnchor),
imageV.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
imageV.widthAnchor.constraint(equalToConstant: self.view.frame.width),
imageV.heightAnchor.constraint(equalToConstant: 400)
])
if i == viewsCount - 1 {
imageV.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
}
prevView = imageV
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
pageCon.currentPage = Int(scrollView.contentOffset.x / self.view.frame.width)
}
}
I followed this programmatic scrollview-pageview tutorial I keep getting a crash inside the Scrollview Delegate's ScrollViewDidScroll(...)
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1030b0d98)
The crash appears on this line: pageControl.currentPage = Int(round(pageFraction))
Why does this crash keeps happening
ViewController Class:
class ViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.backgroundColor = UIColor.white
return scrollView
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .equalSpacing
return stackView
}()
let pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.currentPage = 0
pageControl.tintColor = UIColor.white
pageControl.pageIndicatorTintColor = UIColor.gray
pageControl.currentPageIndicatorTintColor = UIColor.red
pageControl.addTarget(self, action: #selector(pageControlTapped(sender:)), for: .valueChanged)
return pageControl
}()
var views = [UIView]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
scrollView.delegate = self
configureViewsForViewArray()
setScrollViewAnchors()
setStackViewAnchors()
setPageViewsInsideStackView()
setPageControlAnchors()
}
func configureViewsForViewArray(){
let pageView1 = PageView(headerText: "Header 1", paragraphText: "Bla bla bla", backgroundColor: .red)
views.append(pageView1)
let pageView2 = PageView(headerText: "Header 2", paragraphText: "Bla bla bla", backgroundColor: .orange)
views.append(pageView2)
let pageView3 = PageView(headerText: "Header 3", paragraphText: "Bla bla bla", backgroundColor: .blue)
views.append(pageView3)
let pageView4 = PageView(headerText: "Header 4", paragraphText: "Bla bla bla", backgroundColor: .green)
views.append(pageView4)
}
func setScrollViewAnchors(){
view.addSubview(scrollView)
if #available(iOS 11.0, *) {
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
} else {
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
}
if #available(iOS 11.0, *) {
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
} else {
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
}
if #available(iOS 11.0, *) {
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
} else {
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
}
if #available(iOS 11.0, *) {
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
} else {
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
}
}
func setStackViewAnchors(){
scrollView.addSubview(stackView)
stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
}
func setPageViewsInsideStackView(){
for eachView in self.views{
eachView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(eachView)
eachView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
eachView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
}
}
func setPageControlAnchors(){
view.addSubview(pageControl)
pageControl.numberOfPages = views.count
if #available(iOS 11.0, *) {
pageControl.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
} else {
// Fallback on earlier versions
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
if #available(iOS 11.0, *) {
pageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
} else {
// Fallback on earlier versions
pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20).isActive = true
}
}
#objc func pageControlTapped(sender: UIPageControl) {
let pageWidth = scrollView.bounds.width
let offset = sender.currentPage * Int(pageWidth)
UIView.animate(withDuration: 0.33, animations: { [weak self] in
self?.scrollView.contentOffset.x = CGFloat(offset)
})
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageWidth = scrollView.bounds.width
let pageFraction = scrollView.contentOffset.x/pageWidth
//I get a crash here?
pageControl.currentPage = Int(round(pageFraction))
}
}
// this is the model for the views that get added inside the [UIView] array above
PageView:
import Foundation
import UIKit
class PageView: UIView {
// Private so that it can only be modified from within the class
private var headerTextField = UITextField()
// When this property is set it will update the headerTextField text
var headerText: String = "" {
didSet {
headerTextField.text = headerText
}
}
// Private so that you can only change from within the class
private var paragraphTextView = UITextView()
// When this property is set it will update the paragraphTextView text
var paragraphText: String = "" {
didSet {
paragraphTextView.text = paragraphText
}
}
// Designated Init method
init(headerText: String, paragraphText: String, backgroundColor: UIColor) {
super.init(frame: .zero)
setup()
self.headerTextField.text = headerText
self.paragraphTextView.text = paragraphText
self.backgroundColor = backgroundColor
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
// Basic text and view setup
headerTextField.isUserInteractionEnabled = false
headerTextField.textColor = .black
headerTextField.textAlignment = .center
headerTextField.sizeToFit()
paragraphTextView.isUserInteractionEnabled = false
paragraphTextView.textColor = .black
paragraphTextView.textAlignment = .center
paragraphTextView.sizeToFit()
paragraphTextView.isScrollEnabled = false
paragraphTextView.backgroundColor = .clear
// Configuring the textfield/view for autoLayout
headerTextField.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(headerTextField)
paragraphTextView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(paragraphTextView)
// Creating and activating the constraints
NSLayoutConstraint.activate([
headerTextField.centerXAnchor.constraint(equalTo: self.centerXAnchor),
headerTextField.centerYAnchor.constraint(equalTo: self.centerYAnchor),
paragraphTextView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
paragraphTextView.topAnchor.constraint(equalTo: headerTextField.bottomAnchor, constant: 20),
paragraphTextView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: (2/3))
])
}
}