Swift: UIStackView Overlapping all Views instead of Stacking them Vertically - ios

I am trying to create a statistics page for my app that will have various charts that are created dynamically depending on the type of data the user has. To do this, I am stacking multiple ViewControllers according to this tutorial: https://swiftwithmajid.com/2019/02/27/building-complex-screens-with-child-viewcontrollers/
I am running into an issue where the ViewController's View is added to the main StackView as an arrangedSubView, but instead of it stacking the views vertically and allowing me to scroll through them all, it just stacks them on top of each other in the z-direction.
Here is the StackViewController Code:
class StackViewController: UIViewController {
private let scrollView = UIScrollView()
private let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.addSubview(stackView)
setupConstraints()
stackView.axis = .vertical
}
private func setupConstraints() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
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.contentLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
])
}
}
extension StackViewController {
func add(_ child: UIViewController) {
addChild(child)
stackView.addArrangedSubview(child.view)
print(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()
}
}
Here is where I create each View Controller and add it to the StackViewController.
For now, I run a loop and add copies of a single view controller over and over:
class PrototypeViewController: StackViewController {
override func viewDidLoad() {
super.viewDidLoad()
for _ in 0...10 {
setupUI()
}
}
private func setupUI() {
let storyboard = UIStoryboard(name: "ConsistencyGraph", bundle: .main)
let consistencyGraphVC = storyboard.instantiateViewController(identifier: "ConsistencyGraphVC") as! ConsistencyGraphVC
add(consistencyGraphVC)
consistencyGraphVC.setupUI(name: "sessionName", consistencyPercentage: 30, ballsHit: 10)
}
}
Here is the View Controller Code:
class ConsistencyGraphVC: UIViewController {
#IBOutlet weak var mainView: UIView!
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var ballsHitLabel: UILabel!
#IBOutlet weak var pieChartView: PieChartView!
override func viewDidLoad() {
super.viewDidLoad()
}
open func setupUI(name: String, consistencyPercentage: Double, ballsHit: Int) {
displayName(name:name)
drawPieChart(consistencyPercentage: consistencyPercentage)
displayBallsHit(ballsHit: ballsHit)
}
private func displayName(name: String) {
let prefix = "Consistency: "
let title = prefix + name
titleLabel.text = title
}
private func displayBallsHit(ballsHit: Int) {
ballsHitLabel.text = String(ballsHit)
}
private func drawPieChart(consistencyPercentage: Double) {
let maxPercent:Double = 100
let remainingPercent = maxPercent - consistencyPercentage
let dataEntry = [PieChartDataEntry(value: consistencyPercentage, data: String(consistencyPercentage)), PieChartDataEntry(value: remainingPercent, data: nil)]
let dataSet = PieChartDataSet(entries: dataEntry)
let chartData = PieChartData(dataSet: dataSet)
let color1 = randomColor()
let color2 = randomColor()
dataSet.colors = [color1, color2]
pieChartView.data = chartData
}
private func randomColor() -> UIColor {
let red = Double(arc4random_uniform(256))
let green = Double(arc4random_uniform(256))
let blue = Double(arc4random_uniform(256))
let color = UIColor(red: CGFloat(red/255), green: CGFloat(green/255), blue: CGFloat(blue/255), alpha: 1)
return color
}
}
At first, I thought it was because the View Controller sizes might be ambiguous. I then hardcoded the width and height of each View Controller, but still no luck.
Any and all help is much appreciated! I'm at a loss as to how this is even possible.
Thank you in advance!

I have finally found the solution!
I needed to add:
child.view.heightAnchor.constraint(equalToConstant: child.view.frame.size.height).isActive = true
to the StackViewController here:
func add(_ child: UIViewController) {
addChild(child)
child.view.heightAnchor.constraint(equalToConstant: child.view.frame.size.height).isActive = true
stackView.addArrangedSubview(child.view)
child.didMove(toParent: self)
}
I am not sure why this is the case. I had already hard coded the height of the view, but the stackView also wanted me to constrain it before adding the view.
I hope this helps someone in the future! I was beating my head against a wall for ages...

You forgot some required constraints for your StackView inside ScrollView.
You only have trailing, top, and bottom which are not enough.
The correct one:
stackView.leadingAnchor
stackView.trailingAnchor
stackView.topAnchor
stackView.bottomAnchor
stackView.widthAnchor

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

Layout not working when container view controller dynamically changes child view controller

I have a MainViewController that contains a ContainerViewController.
The ContainerViewController starts out showing childViewControllerA, and dynamically switches it out to childViewControllerB when a button in the childViewControllerA is clicked:
func showNextViewContoller() {
let childViewControllerB = ChildViewControllerB()
container.addViewController(childViewControllerB)
container.children.first?.remove() // Remove childViewControllerA
}
Here's a diagram:
The second view controller (ViewControllerB) has an image view that I'd like to show in the center. So I assigned it the following constraints:
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6),
])
The problem I'm running into is the imageView is not centered vertically: It's lower than it should be.
When I run the app so that ContainerVeiwController shows childViewControllerB first, then it works as intended. The issue occurs only when childViewControllerB is switched in dynamically after childViewControllerA:
To help debug, I added the following code to all three ViewControllers:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("MainViewController bounds = \(self.view.bounds)")
}
And this gave an interesting print out (running this on an iPhone 13 mini simulator):
MainViewController bounds = (0.0, 0.0, 375.0, 812.0) //iPhone 13 mini screen is 375 x 812
ChildViewControllerA bounds = (0.0, 0.0, 375.0,738.0). // 812 - 50 safety margin - 24 titlebar = 738.
Now, the switch happens after the button was clicked and childViewControllerB is added:
ChildViewControllerB bounds = (0.0, 0.0, 375.0, 812.0)
It seems like ChildViewControllerB is assuming a full screen size and ignoring the bounds of it's parent view controller (ContainerViewController). So, the imageView's hightAnchor is based on the full screen height, causing it to appear off center.
So, I changed the constraints on the imageView to:
NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: super.view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: super.view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: super.view.heightAnchor, multiplier: 0.6),
])
Next, I tried to force a layout update by adding any of these lines after the switch happens in showNextViewController() function above:
container.children.first?.view.layoutSubviews()
//and
container.children.first?.view.setNeedsLayout()
None of them worked.
How do I get ChildViewControllerB to respect the bounds of ContainerViewController?
If it helps, the imageView only needs to be in the center initially. It'll eventually have a pan, pinch and rotate gesture attached, so the user can move it anywhere they want.
Edit 01:
This is how I'm adding and removing a child view controller:
extension UIViewController {
func addViewController(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
guard parent != nil else { return }
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
Edit 02:
On recommendation by a few commentators, I updated the addViewController() function:
func addViewController(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
child.view.topAnchor.constraint(equalTo: self.view.topAnchor),
child.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
child.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
])
child.didMove(toParent: self)
}
This didn't seem to work, I got errors saying "Unable to simultaneously satisfy constraints." Unfortunately I have very little knowledge on how to decipher the error messages...
Edit 03: Simplified Project:
Here's a simplified project. There are four files plus AppDelegate (I'm not using a storyboard):
MainViewController
ViewControllerA
ViewControllerB
Utilities
AppDelegate
MainViewController:
import UIKit
class MainViewController: UIViewController {
let titleBarView = UIView(frame: .zero)
let container = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
setup()
layout()
}
func setup() {
titleBarView.backgroundColor = .gray
view.addSubview(titleBarView)
addViewController(container)
showViewControllerA()
}
func layout() {
titleBarView.translatesAutoresizingMaskIntoConstraints = false
container.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
titleBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
titleBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
titleBarView.heightAnchor.constraint(equalToConstant: 24),
container.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor, constant: 0),
container.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
container.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
container.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
func showViewControllerA() {
let viewControllerA = ViewControllerA()
viewControllerA.delegate = self
container.children.first?.remove()
container.addViewController(viewControllerA)
}
func showViewControllerB() {
let viewControllerB = ViewControllerB()
container.children.first?.remove()
container.addViewController(viewControllerB)
}
}
extension MainViewController: ViewControllerADelegate {
func nextViewController() {
showViewControllerB()
}
}
ViewController A:
protocol ViewControllerADelegate: AnyObject {
func nextViewController()
}
class ViewControllerA: UIViewController {
let nextButton = UIButton()
weak var delegate: ViewControllerADelegate?
override func viewDidLoad() {
super.viewDidLoad()
setup()
layout()
view.backgroundColor = .gray
}
func setup() {
nextButton.setTitle("next", for: .normal)
nextButton.addTarget(self, action: #selector(nextButtonPressed), for: .primaryActionTriggered)
view.addSubview(nextButton)
}
func layout() {
nextButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
nextButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
#objc func nextButtonPressed() {
delegate?.nextViewController()
}
}
ViewController B:
import UIKit
class ViewControllerB: UIViewController {
let imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
setup()
layout()
}
func setup() {
view.addSubview(imageView)
blankImage()
}
func layout() {
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.layer.magnificationFilter = CALayerContentsFilter.nearest;
NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: super.view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: super.view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: super.view.heightAnchor, multiplier: 0.6),
])
view.layoutSubviews()
}
func blankImage() {
let ciImage = CIImage(cgImage: createBlankCGImage(width: 32, height: 64)!)
imageView.image = cIImageToUIImage(ciimage: ciImage, context: CIContext())
}
}
Utilities:
import Foundation
import UIKit
func createBlankCGImage(width: Int, height: Int) -> CGImage? {
let bounds = CGRect(x: 0, y:0, width: width, height: height)
let intWidth = Int(ceil(bounds.width))
let intHeight = Int(ceil(bounds.height))
let bitmapContext = CGContext(data: nil,
width: intWidth, height: intHeight,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
if let cgContext = bitmapContext {
cgContext.saveGState()
let r = CGFloat.random(in: 0...1)
let g = CGFloat.random(in: 0...1)
let b = CGFloat.random(in: 0...1)
cgContext.setFillColor(red: r, green: g, blue: b, alpha: 1)
cgContext.fill(bounds)
cgContext.restoreGState()
return cgContext.makeImage()
}
return nil
}
func cIImageToUIImage(ciimage: CIImage, context: CIContext) -> UIImage? {
if let cgimg = context.createCGImage(ciimage, from: ciimage.extent) {
return UIImage(cgImage: cgimg)
}
return nil
}
extension UIViewController {
func addViewController(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
guard parent != nil else { return }
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
AppDelegate:
import UIKit
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = .white
window?.makeKeyAndVisible()
window?.rootViewController = MainViewController()
return true
}
}
Asteroid is on the right track, but couple other issues...
You are not giving the views of the child controllers any constraints, so they load at their "native" size.
Changing your addViewController(...) func as advised by Asteroid solves the A and B missing constraints, but...
You are calling that same func for your container controller and adding constraints to its view in layout(), so you end up with conflicting constraints.
One solution would be to change your addViewController func to this:
func addViewController(_ child: UIViewController, constrainToSuperview: Bool = true) {
addChild(child)
view.addSubview(child.view)
if constrainToSuperview {
child.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
child.view.topAnchor.constraint(equalTo: view.topAnchor),
child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
child.didMove(toParent: self)
}
then in setup():
func setup() {
titleBarView.backgroundColor = .red
view.addSubview(titleBarView)
// change this
//addViewController(container)
// to this
addViewController(container, constrainToSuperview: false)
showViewControllerA()
}
while leaving your other "add view controller" calls like this:
container.addViewController(viewControllerA)
container.addViewController(viewControllerB)
The other thing that may throw you off is the extraneous super. in your image view constraints:
NSLayoutConstraint.activate([
// change this
//imageView.centerYAnchor.constraint(equalTo: super.view.centerYAnchor),
//imageView.centerXAnchor.constraint(equalTo: super.view.centerXAnchor),
//imageView.heightAnchor.constraint(equalTo: super.view.heightAnchor, multiplier: 0.6),
// to this
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6),
])
Update as following:
func addViewController(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
child.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
child.view.topAnchor.constraint(equalTo: view.topAnchor),
child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)])
}
In MainViewViewController update the layout() function:
func layout() {
titleBarView.translatesAutoresizingMaskIntoConstraints = false
container.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
titleBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
titleBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
titleBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
titleBarView.heightAnchor.constraint(equalToConstant: 24),
//container.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor, constant: 0),
//container.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
//container.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
//container.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}

How can you make this view in iOS Swift?

please click here to see which view I have to make
I have to make a map view which is similar to apple maps, and it has description view on top of the map view. The user can slide the description view left and right to see the route description in order.
I have no idea what these are called or how these are made. I really want to google but have no idea which keyword I have to use.
Please Help!
It is pagerview, you can use this library FSPagerView. Or, you can do it yourself by using scrollview or collectionview. Implement the scrollViewDidScroll to detect what is the current page and show the correct view
To make that view shown in picture, all you need to do is
take a UICollectionView and put it on your mapview. Provide UICollectionView a constraints at top, leading and trailing. Also you need yo provide height constraint of about 120-150.
in your ViewController drag and make an outlet of this UICollectionView.
Make your UIViewController follow UICollectionViewDelegate and UICollectionViewDataSource.
create a UICollectionViewCell with a xib file.
register that cell in your UIViewController.
create a datamodel which has properties that you need in order to show the direction data.
create an array in that UIViewController.
use that array as a data source of UICollectionView.
Make sense?
As others have said, UIPageViewController works well:
class ViewController: UIViewController, UIPageViewControllerDataSource {
var mapView: MKMapView!
var data: [String] = {
var data = [String]()
for _ in 0...5 {
data.append("덕원 아파트에서 출발합니다.")
}
return data
}()
override func viewDidLoad() {
super.viewDidLoad()
mapView = MKMapView()
self.view.addSubview(mapView)
mapView.translatesAutoresizingMaskIntoConstraints = false
// pageVC setup
let cardVC = CardViewController(data: data[0])
let pageVC = PageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
pageVC.setViewControllers([cardVC], direction: .forward, animated: false, completion: nil)
pageVC.dataSource = self
// container view for containment
let containerView = UIView()
view.addSubview(containerView)
containerView.backgroundColor = .black
// containment
addChild(pageVC)
containerView.addSubview(pageVC.view)
pageVC.didMove(toParent: self)
containerView.translatesAutoresizingMaskIntoConstraints = false
pageVC.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// mapView
mapView.widthAnchor.constraint(equalTo: view.widthAnchor),
mapView.heightAnchor.constraint(equalTo: view.heightAnchor),
// container view
containerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
// pageVC
pageVC.view.widthAnchor.constraint(equalTo: containerView.widthAnchor),
pageVC.view.heightAnchor.constraint(equalTo: containerView.heightAnchor)
])
let pageControl = UIPageControl.appearance()
pageControl.pageIndicatorTintColor = UIColor.gray.withAlphaComponent(0.6)
pageControl.currentPageIndicatorTintColor = .white
pageControl.backgroundColor = .clear
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let info = (viewController as! CardViewController).label.text, var index = data.firstIndex(of: info) else { return nil }
index += 1
if index > data.count {
return nil
}
return CardViewController(data: data[index])
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let info = (viewController as! CardViewController).label.text, var index = data.firstIndex(of: info) else { return nil }
index -= 1
if index <= 0 {
return nil
}
return CardViewController(data: data[index])
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return data.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
let pageVC = pageViewController.viewControllers![0] as! CardViewController
let t = pageVC.label.text!
return data.firstIndex(of: t)!
}
}
class PageViewController: UIPageViewController {
}
class CardViewController: UIViewController {
var label: UILabel!
var containerView: UIView!
var iconView: IconView!
init(data: String) {
self.label = UILabel()
self.label.text = data
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let v = UIView()
v.backgroundColor = .black
view = v
}
override func viewDidLoad() {
super.viewDidLoad()
containerView = UIView()
view.addSubview(containerView)
// container view
containerView.addSubview(label)
containerView.translatesAutoresizingMaskIntoConstraints = false
// label
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .black
label.textColor = .white
label.numberOfLines = 0
// icon view
iconView = IconView()
containerView.addSubview(iconView)
iconView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// containerView
containerView.heightAnchor.constraint(equalTo: view.heightAnchor),
containerView.widthAnchor.constraint(equalTo: view.widthAnchor),
// icon view
iconView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 0.4),
iconView.heightAnchor.constraint(equalTo: containerView.heightAnchor),
iconView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
// label
label.leadingAnchor.constraint(equalTo: iconView.trailingAnchor),
label.heightAnchor.constraint(equalTo: containerView.heightAnchor),
label.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10),
])
}
}
class IconView: UIView {
override func draw(_ rect: CGRect) {
let circle = UIBezierPath(arcCenter: self.center, radius: 20, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
UIColor.orange.setFill()
circle.fill()
}
}

How to access cell members (for animation) from scrollViewDidScroll method in UICollectionViewController?

How do I access members (an UIImage or UITextView) added to the view in a UICollectionViewCell from the scrollViewDidScroll method in the UICollectionViewController?
I would like to animate i.e. move the image and text at "different speed" while scrolling vertically to the next cell.
I understand that this can be done within the scrollViewDidScroll method but I don't know how to access the members.
the ViewController:
class OnboardingViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
**code here which I just can't figure out....**
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
collectionView?.register(PageCell.self, forCellWithReuseIdentifier: "cellId")
collectionView?.isPagingEnabled = true
collectionView.showsHorizontalScrollIndicator = false
// this method "creates" the UIPageControll and assigns
setupPageControl()
}
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.currentPage = 0
pageControl.numberOfPages = data.count <--- data provided by a model from a plist - works perfectly
pageControl.currentPageIndicatorTintColor = .black
pageControl.pageIndicatorTintColor = .gray
pageControl.translatesAutoresizingMaskIntoConstraints = false
return pageControl
}()
private func setupPageControl() {
view.addSubview(pageControl)
NSLayoutConstraint.activate([
onboardingPageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
onboardingPageControl.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
onboardingPageControl.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
])
}
all the other override methods are implemented in an extension and working fine i.e.
numberOfItemsInSection section: Int) -> Int {
return data.count
}
This is the PageCell:
class PageCell: UICollectionViewCell {
var myPage: MyModel? {
didSet {
guard let unwrappedPage = myPage else { return }
// the image in question:
myImage.image = UIImage(named: unwrappedPage.imageName)
myImage.translatesAutoresizingMaskIntoConstraints = false
myImage.contentMode = .scaleAspectFit
// the text in question
let attributedText = NSMutableAttributedString(string: unwrappedPage.title, attributes: [:])
attributedText.append(NSAttributedString(string: "\n\(unwrappedPage.description)", attributes: [:]))
myText.attributedText = attributedText
myText.translatesAutoresizingMaskIntoConstraints = false
myText.textColor = .black
myText.textAlignment = .center
myText.isEditable = false
myText.isScrollEnabled = false
myText.isSelectable = false
}
let myImage: UIImageView = {
let imageView = UIImageView()
return imageView
}()
let myText: UITextView = {
let textView = UITextView()
return textView
}()
fileprivate func setup() {
addSubview(myImage)
NSLayoutConstraint.activate([
myImage.safeAreaLayoutGuide.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 60),
myImage.centerXAnchor.constraint(equalTo: centerXAnchor),
myImage.leadingAnchor.constraint(equalTo: leadingAnchor),
myImage.trailingAnchor.constraint(equalTo: trailingAnchor),
myImage.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.4)
])
addSubview(myText)
NSLayoutConstraint.activate([
myText.topAnchor.constraint(equalTo: myImage.bottomAnchor, constant: 16),
myText.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
myText.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16)
])
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You can achieve this by getting current visible cells of your collectionView, and it would be great if you access them in scrollViewDidEndDecelerating intead of scrollViewDidScroll.
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
for cell in collectionView.visibleCells {
//cell.imageView
//cell.txtView
// you can access both imageView and txtView here
let indexPath = collectionView.indexPath(for: cell)
print(indexPath) // this will give you indexPath as well to differentiate
}
}

call label postion and size from outside of class (Swift4)

What I am trying to do is assign the position and size of a label from outside a class. Then within 2 separate classes call the label to add text to it. This would save time a lot of time if this would work.
let backbutton = UILabel!
backbutton.translatesAutoresizingMaskIntoConstraints = false
backbutton.leftAnchor.constraint(equalTo: _, constant: 20).isActive = true
backbutton.topAnchor.constraint(equalTo: _, constant: 125).isActive = true
backbutton.widthAnchor.constraint(equalToConstant: 50).isActive = true
backbutton.heightAnchor.constraint(equalToConstant: 50).isActive = true
class nineViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
backbutton.text = String("red")
}
}
class two: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
backbutton.text = String("two")
}
}
Create a Utilities class separately to use the functions that are inside it globally.
Utilities:
class Utilities: NSObject
{
class func createLabel(on view: UIView, horizontalAnchors hAnchors: (leading: CGFloat, leadingView: UIView, trailing: CGFloat, trailingView: UIView), verticalAnchors vAnchors: (top: CGFloat, topView: UIView, bottom: CGFloat, bottomView: UIView)) -> UILabel {
let label = UILabel()
view.addSubview(label)
label.backgroundColor = UIColor.red
label.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: hAnchors.leadingView.leadingAnchor, constant: hAnchors.leading).isActive = true
label.trailingAnchor.constraint(equalTo: hAnchors.trailingView.trailingAnchor, constant: -hAnchors.trailing).isActive = true
label.topAnchor.constraint(equalTo: vAnchors.topView.topAnchor, constant: vAnchors.top).isActive = true
label.bottomAnchor.constraint(equalTo: vAnchors.bottomView.topAnchor, constant: -vAnchors.bottom).isActive = true
return label
}
class func createLabel(on view: UIView, positionAnchors pAnchors: (leading: CGFloat, leadingView: UIView, top: CGFloat, topView: UIView), size: (width: CGFloat, height: CGFloat)) -> UILabel {
let label = UILabel()
view.addSubview(label)
label.backgroundColor = UIColor.red
label.translatesAutoresizingMaskIntoConstraints = false
label.leadingAnchor.constraint(equalTo: pAnchors.leadingView.leadingAnchor, constant: pAnchors.leading).isActive = true
label.topAnchor.constraint(equalTo: pAnchors.topView.topAnchor, constant: pAnchors.top).isActive = true
label.widthAnchor.constraint(equalToConstant: size.width).isActive = true
label.heightAnchor.constraint(equalToConstant: size.height).isActive = true
return label
}
}
In ViewController:
#IBOutlet weak var autoLayedoutLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let originY: CGFloat = 50
let spacing: CGFloat = 16
let width: CGFloat = 300
let height: CGFloat = 50
let label = Utilities.createLabel(on: view, positionAnchors: (spacing, view, originY, view), size: (width, height))
label.text = "Label with Position Anchors & Size"
label.backgroundColor = UIColor.red
let label2 = Utilities.createLabel(on: view, horizontalAnchors: (spacing, view, spacing, view), verticalAnchors: (spacing + height, label, spacing, autoLayedoutLabel))
label2.text = "Label with Horizontal & Vertical Anchors"
label2.backgroundColor = UIColor.green
}
You can have different variable for buttonText and set his position and size in his setter like
var buttonText:String {
didSet{
backButton.text = buttonText
setFontAndPosition()
}
}
and in viewController just set the value
override func viewDidLoad() {
super.viewDidLoad()
buttonText = "red"
}
I found it's feasible to directly use global UILable. If you don't need to manage too many labels, this is the simplest way.
A TabBarcontroller is used for testing here.
let backbutton = UILabel()
class MyTabBarController : UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setViewControllers([SettingViewController(), NineViewController(), TwoViewController()], animated: false)
}
}
class SettingViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "setting", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
self.view.addSubview(backbutton)
backbutton.text = "cool"
backbutton.translatesAutoresizingMaskIntoConstraints = false
backbutton.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 20).isActive = true
backbutton.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 125).isActive = true
backbutton.widthAnchor.constraint(equalToConstant: 50).isActive = true
backbutton.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
class NineViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "nine", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
backbutton.text = String("red")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
backbutton.text = String("red-Appear")
}
}
class TwoViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "two", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
backbutton.text = String("two")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
backbutton.text = String("two-Appear")
}
}
If you prefer defining the label inside one class. You may define the global UILabel as this:
weak var backbutton: UILabel!
class SettingViewController: UIViewController {
let mybutton = UILabel()
backbutton = mybutton
// continue
}
You don't need to change any other codes.
Now is the second part of the story. If you wanna setup a global UILabel outside any view, is that possible. Without constraints it's very simple like this:
let backbutton: UILabel! = {
let button = UILabel()
button.text = "test"
button.frame = CGRect.init(x: 200, y: 200, width: 50, height: 50)
return button
}()
The setting View changes like this :
class SettingViewController: UIViewController {
override var tabBarItem: UITabBarItem!{
get {
return UITabBarItem.init(title: "setting", image: nil, tag: 0)
}
set{
super.tabBarItem = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
self.view.addSubview(backbutton)
}
}
It's clear there is only one line in the SettingVC. But if you need to use constraints, what should we do? Everything else is fine, but the position of UILabel constraints depends on the superView of UILabel. So an extension can be used here to make things easier.
let specialLabelTag = 1001
let backbutton: UILabel! = {
let button = UILabel()
button.tag = specialLabelTag
button.text = "test" // for test purpose
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 50).isActive = true
button.heightAnchor.constraint(equalToConstant: 50).isActive = true
return button
}()
extension UILabel{
override open func didMoveToSuperview() {
superview?.didMoveToSuperview()
if(tag == specialLabelTag){
leftAnchor.constraint(equalTo: superview!.leftAnchor, constant: 20).isActive = true
topAnchor.constraint(equalTo: superview!.topAnchor, constant: 125).isActive = true
}
}
The tag used in extension is to identify the global UILabel in order not to affect other UILabels. Only position constraints are needed in the extension. SettingUP vc is as same as before.
Now you can build a label without any view class. But you have to add them somewhere and modify the text as you like. Hope this is the answer to the question.
BTW, you can subclass the UILabel to MyUILabel with above code and then make it global (just put outside any class). It would be much easier because you don't need to use specialLabelTag.
let backbutton = MyUILabel()

Resources