How can I add a horizontal stack of buttons above a collection view in Swift? - ios

I am trying to add three buttons above a collection view that pulls either the videos, photos, or all content from the photo library based on what button a user selects.
I managed to create the collection view and the buttons separately but when I try to combine them in a vertical stack view I get an error where I declare the UIStackView.
I am trying to combine a horizontal stack view (the buttons) into a vertical stack view (the button stack view on top and the collection view below).
I think the error is related to how I am declaring the buttonStack but everything I have tried has failed.
I assumed that two stacked views would be the best way to accomplish my goal but I am open to other/better suggestions. Regardless I would like to know why this is not working for me.
Code line and error message:
let stackView = UIStackView(arrangedSubviews: [buttonsStack, collectionView!])
Type of expression is ambiguous without more context
class TestVideoItemCell: UICollectionViewCell {
var stackView = UIStackView()
var vid = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
vid.contentMode = .scaleAspectFill
vid.clipsToBounds = true
self.addSubview(vid)
}
override func layoutSubviews() {
super.layoutSubviews()
vid.frame = self.bounds
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TestVideoViewVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate {
var myCollectionView: UICollectionView!
var videoArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
grabVideos()
}
//MARK: CollectionView
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return videoArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! VideoItemCell
cell.vid.image = videoArray[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width
return CGSize(width: width/4 - 1, height: width/4 - 1)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myCollectionView.collectionViewLayout.invalidateLayout()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
//MARK: grab videos
func grabVideos(){
videoArray = []
DispatchQueue.global(qos: .background).async {
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
requestOptions.deliveryMode = .highQualityFormat
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions)
print(fetchResult)
print(fetchResult.count)
if fetchResult.count > 0 {
for i in 0..<fetchResult.count{
imgManager.requestImage(for: fetchResult.object(at: i) as PHAsset, targetSize: CGSize(width:500, height: 500),contentMode: .aspectFill, options: requestOptions, resultHandler: { (image, error) in
self.videoArray.append(image!)
})
}
} else {
print("No videos found.")
}
DispatchQueue.main.async {
self.myCollectionView.reloadData()
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: SetUp Methods
func buttonsStack() {
let buttonChoices = ButtonStackView()
buttonChoices.setupButtonsStackView()
}
func setupCollection() {
let layout = UICollectionViewFlowLayout()
myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
myCollectionView.delegate = self
myCollectionView.dataSource = self
myCollectionView.register(VideoItemCell.self, forCellWithReuseIdentifier: "videoCell")
myCollectionView.backgroundColor = UIColor.white
self.view.addSubview(myCollectionView)
myCollectionView.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.RawValue(UInt8(UIView.AutoresizingMask.flexibleWidth.rawValue) | UInt8(UIView.AutoresizingMask.flexibleHeight.rawValue)))
}
func setupStack() {
let stackView = UIStackView(arrangedSubviews: [buttonsStack, collectionView!])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fillEqually
stackView.axis = .horizontal
stackView.spacing = 8
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
}
}
UPDATE
Below is an image example of the general concept of the UI I am trying to create.

Try taking things step-by-step...
Start with this code (assigned to a plain UIViewController as the root controller of a UINavigationController):
class TestVideoViewVC: UIViewController {
var myCollectionView: UICollectionView!
var videoArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
// set main view background color to a nice medium blue
view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
// vertical stack view for the full screen (safe area)
let mainStack = UIStackView()
mainStack.axis = .vertical
mainStack.spacing = 8
mainStack.translatesAutoresizingMaskIntoConstraints = false
// add it to the view
view.addSubview(mainStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: g.topAnchor),
mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
}
}
If you run the app as-is, you'll see this:
The stack view is there, but we haven't added any subviews to it.
So, let's add two views (labels)... the top one at 50-pts in height:
class TestVideoViewVC: UIViewController {
var myCollectionView: UICollectionView!
var videoArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
// set main view background color to a nice medium blue
view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
// vertical stack view for the full screen (safe area)
let mainStack = UIStackView()
mainStack.axis = .vertical
mainStack.spacing = 8
mainStack.translatesAutoresizingMaskIntoConstraints = false
// add it to the view
view.addSubview(mainStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: g.topAnchor),
mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
// add two arranged subviews, so we can see the layout
let v1 = UILabel()
v1.textAlignment = .center
v1.text = "Buttons will go here..."
v1.backgroundColor = .green
let v2 = UILabel()
v2.textAlignment = .center
v2.text = "Collection view will go here..."
v2.backgroundColor = .yellow
// let's give the top view a height of 50-pts
v1.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
mainStack.addArrangedSubview(v1)
mainStack.addArrangedSubview(v2)
}
}
Run that code, and we get:
Now, let's replace v1 (the "top" label) with a horizontal stack view with 3 red, 50-pt tall buttons:
class TestVideoViewVC: UIViewController {
var myCollectionView: UICollectionView!
var videoArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
// set main view background color to a nice medium blue
view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
// vertical stack view for the full screen (safe area)
let mainStack = UIStackView()
mainStack.axis = .vertical
mainStack.spacing = 8
mainStack.translatesAutoresizingMaskIntoConstraints = false
// add it to the view
view.addSubview(mainStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: g.topAnchor),
mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
// create a horizontal stack view
let buttonsStack = UIStackView()
buttonsStack.axis = .horizontal
buttonsStack.spacing = 8
buttonsStack.distribution = .fillEqually
// create and add 3 50-pt height buttons to the stack view
["Videos", "Photos", "All"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.gray, for: .highlighted)
b.backgroundColor = .red
buttonsStack.addArrangedSubview(b)
b.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
}
// add the buttons stack view to the main stack view
mainStack.addArrangedSubview(buttonsStack)
// create a label (this will be our collection view)
let v2 = UILabel()
v2.textAlignment = .center
v2.text = "Collection view will go here..."
v2.backgroundColor = .yellow
// add the label to the main stack view
mainStack.addArrangedSubview(v2)
}
}
Run that code, and we get:
Now we can replace v2 with our collection view:
class TestVideoViewVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate {
var myCollectionView: UICollectionView!
var videoArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
// set main view background color to a nice medium blue
view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
// vertical stack view for the full screen (safe area)
let mainStack = UIStackView()
mainStack.axis = .vertical
mainStack.spacing = 8
mainStack.translatesAutoresizingMaskIntoConstraints = false
// add it to the view
view.addSubview(mainStack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
mainStack.topAnchor.constraint(equalTo: g.topAnchor),
mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
// create a horizontal stack view
let buttonsStack = UIStackView()
buttonsStack.axis = .horizontal
buttonsStack.spacing = 8
buttonsStack.distribution = .fillEqually
// create and add 3 50-pt height buttons to the stack view
["Videos", "Photos", "All"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.gray, for: .highlighted)
b.backgroundColor = .red
buttonsStack.addArrangedSubview(b)
b.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
}
// add the buttons stack view to the main stack view
mainStack.addArrangedSubview(buttonsStack)
// setup the collection view
setupCollection()
// add it to the main stack view
mainStack.addArrangedSubview(myCollectionView)
// start the async call to get the assets
grabVideos()
}
func setupCollection() {
let layout = UICollectionViewFlowLayout()
myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
myCollectionView.delegate = self
myCollectionView.dataSource = self
myCollectionView.register(VideoItemCell.self, forCellWithReuseIdentifier: "videoCell")
myCollectionView.backgroundColor = UIColor.white
}
//MARK: CollectionView
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return videoArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! VideoItemCell
cell.vid.image = videoArray[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width
return CGSize(width: width/4 - 1, height: width/4 - 1)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myCollectionView.collectionViewLayout.invalidateLayout()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 1.0
}
//MARK: grab videos
func grabVideos(){
videoArray = []
DispatchQueue.global(qos: .background).async {
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
requestOptions.deliveryMode = .highQualityFormat
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
//let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions)
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
print(fetchResult)
print(fetchResult.count)
if fetchResult.count > 0 {
for i in 0..<fetchResult.count{
imgManager.requestImage(for: fetchResult.object(at: i) as PHAsset, targetSize: CGSize(width:500, height: 500),contentMode: .aspectFill, options: requestOptions, resultHandler: { (image, error) in
self.videoArray.append(image!)
})
}
} else {
print("No videos found.")
}
DispatchQueue.main.async {
self.myCollectionView.reloadData()
}
}
}
}
class VideoItemCell: UICollectionViewCell {
var stackView = UIStackView()
var vid = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
vid.contentMode = .scaleAspectFill
vid.clipsToBounds = true
self.addSubview(vid)
}
override func layoutSubviews() {
super.layoutSubviews()
vid.frame = self.bounds
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And we should have this (I just have the default photos, no videos, so I changed grabVideos() to get photos):

You might be better off using AutoLayout instead of a vertical UIStackView.
So you should put the Button stack view on the top of the ViewController, and you might want to give it a fixed height. Then, instead of setting the frame of collectionView, set up its constraints, so that its edges stick to the viewController's view, except for top constraint, which should stick to the button stack's bottom constraint.
Side note, interface builder is not necessarily better than creating views programmatically. Personally, I would suggest you stick with the programmatic way.

Well, I will suggest to go ahead programatically. But, I didn't under stood your code a lot, but what I will suggest you that you can take a UIStackView and place it below safe area and then there will be the UICollectionView.
private let stackV: UIStackView = {
let stackV = UIStackView()
stackV.axis = .horizontal
stackV.distribution = .horizontal
stackV.distribution = .equalSpacing
return stackV
}()
Then, just create the buttons:
private let collectionB: [UIButtons] = {
let buttonA = UIButton()
let buttonB = UIButton()
let buttonC = UIButton()
return [buttonA, buttonB, buttonC]
}()
Them you can add it as an arranged subview to the stackV:
collectionB.forEach { stackV.addArrangedSubview($0) }
This will do it, you have to give a fixed heigh to the stackV and that is it then you can add the collectionV at the bottom and place your constraints accordingly.
I have created views programatically for a long time now and what you are trying to achieve is totally achievable, if I got to look at the UI then things would have been better, what I didn't understood is that why have you added a stackView in your UICollectionViewCell, may be it can be handled in a smoother way.
P.S.: I haven't typed this code on Xcode it may throw some error.

Related

How can I change the constraints so that it was like on the second screenshot?

Please can you help me to change constraints in code to take an elements like in the second screen.
enter image description here
enter image description here
class DetailsHomeViewController: UIViewController {
var images:[String] = ["label","label","label"]
let MainImageView: UIImageView = {
let theImageView = UIImageView()
theImageView.image = UIImage(named: "label.png")
theImageView.translatesAutoresizingMaskIntoConstraints = false
return theImageView
}()
let someImageView: UIImageView = {
let theImageView = UIImageView()
theImageView.backgroundColor = .white
theImageView.translatesAutoresizingMaskIntoConstraints = false
theImageView.isUserInteractionEnabled = true
return theImageView
}()
lazy var collectionView:UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.dataSource = self
cv.delegate = self
cv.register(ImageCell.self, forCellWithReuseIdentifier: ImageCell.identifier)
cv.translatesAutoresizingMaskIntoConstraints = false
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(MainImageView)
view.addSubview(someImageView)
someImageView.addSubview(collectionView)
someImageViewConstraints()
MainImageViewConstraints()
view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
setupViews()
mysetupViews()
}
private func setupViews() {
someImageViewConstraints()
}
func someImageViewConstraints() {
someImageView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width/2)
someImageView.center = view.center
let itemHeight = 120
let padding = 25
let width = (itemHeight * 3) + padding
collectionView.frame = CGRect(x: Int(view.center.x) - (width / 2),
y: Int(someImageView.frame.height) - (itemHeight + padding),
width: width, height: itemHeight)
}
private func mysetupViews() {
createCustomNavigationBar()
let RightButton = createCustomButton(
imageName: "square.and.arrow.up",
selector: #selector(RightButtonTapped)
)
let customTitleView = createCustomTitleView(
detailsName: "Label"
)
navigationItem.rightBarButtonItems = [RightButton]
navigationItem.titleView = customTitleView
}
#objc private func RightButtonTapped() {
print("RightButtonTapped")
}
func MainImageViewConstraints() {
MainImageView.translatesAutoresizingMaskIntoConstraints = false
} }
extension DetailsHomeViewController:UICollectionViewDataSource , UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageCell.identifier, for: indexPath) as! ImageCell
cell.someImageView.image = UIImage(named: images[indexPath.row])
return cell
}
} extension DetailsHomeViewController:UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.height, height: collectionView.frame.height)
}
}
class ImageCell:UICollectionViewCell {
static let identifier = "ImageCell"
override var isSelected: Bool {
didSet {
self.someImageView.layer.borderColor = isSelected ? UIColor.green.cgColor : UIColor.clear.cgColor
self.someImageView.layer.borderWidth = 5
}
}
let someImageView: UIImageView = {
let theImageView = UIImageView()
theImageView.clipsToBounds = true
return theImageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 8
self.clipsToBounds = true
addSubview(someImageView)
someImageView.frame = self.bounds
}
override func layoutSubviews() {
super.layoutSubviews()
//someImageView.frame = self.frame
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
You really need to go through a bunch of good auto-layout tutorials, but here is a quick general idea...
We'll constrain the "main image view" 12-points from the top, 40-points from each side, and give it a 5:3 ratio.
Then we'll add a UIStackView (with 12-point spacing) below it, constrained 12-points from the bottom of the main image view, 75% of the width, centered horizontally.
Each image view that we add to the stack view will be constrained to a 1:2 ratio (square).
Here's sample code to do that:
class DetailsHomeViewController: UIViewController {
var images:[String] = ["label", "label", "label"]
let mainImageView: UIImageView = {
let theImageView = UIImageView()
theImageView.contentMode = .scaleAspectFill
theImageView.clipsToBounds = true
theImageView.layer.cornerRadius = 24.0
return theImageView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
setupViews()
mysetupViews()
}
private func setupViews() {
// a stack view to hold the "thumbnail" image views
let stackView = UIStackView()
stackView.spacing = 12
mainImageView.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainImageView)
view.addSubview(stackView)
// respect the safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// main image view - let's go with
// Top with 12-points "padding"
mainImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
// Leading and Trailing with 40-points "padding"
mainImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
mainImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// height equal to width x 3/5ths (5:3 aspect ratio)
mainImageView.heightAnchor.constraint(equalTo: mainImageView.widthAnchor, multiplier: 3.0 / 5.0),
// stack view
// Top 12-points from main image view Bottom
stackView.topAnchor.constraint(equalTo: mainImageView.bottomAnchor, constant: 12.0),
// width equal to 75% of the main image view width
stackView.widthAnchor.constraint(equalTo: mainImageView.widthAnchor, multiplier: 0.75),
// centered horizontally
stackView.centerXAnchor.constraint(equalTo: mainImageView.centerXAnchor),
])
// make sure we can load the main image
if let img = UIImage(named: images[0]) {
mainImageView.image = img
}
// now we'll add 3 image views to the stack view
images.forEach { imgName in
let thumbView = UIImageView()
thumbView.contentMode = .scaleAspectFill
thumbView.clipsToBounds = true
thumbView.layer.cornerRadius = 16.0
// we want them to be square
thumbView.heightAnchor.constraint(equalTo: thumbView.widthAnchor).isActive = true
// make sure we can load the image
if let img = UIImage(named: imgName) {
thumbView.image = img
}
stackView.addArrangedSubview(thumbView)
}
}
private func mysetupViews() {
createCustomNavigationBar()
let RightButton = createCustomButton(
imageName: "square.and.arrow.up",
selector: #selector(RightButtonTapped)
)
let customTitleView = createCustomTitleView(
detailsName: "Label"
)
navigationItem.rightBarButtonItems = [RightButton]
navigationItem.titleView = customTitleView
}
#objc private func RightButtonTapped() {
print("RightButtonTapped")
}
}

collectionView Animation on SwipUP?

I want to animate a collectionView on swipe on the basis of content offset. it's like a page controller but shows only one swipe. I have attached a video because I know the explanation is not good.
Link
you can download the video from there.
There can be several ways to do what you want, here is one way:
I am setting up a UIImageView along with an overlay UIView which is pinned to the main view's leading, trailing, top and bottom anchors.
My aim is to swipe up a collection view like your example and then change the color of the overlay view based on the cell tapped in the collection view.
Here are some vars and the initial set up:
class SwipeCollectionView: UIViewController
{
private var collectionView: UICollectionView!
private let imageView = UIImageView()
private let overlayView = UIView()
// Collection view data source
private let colors: [UIColor] = [.red,
.systemBlue,
.orange,
.systemTeal,
.purple,
.systemYellow]
// Store the collection view's bottom anchor which is used
// to animate the collection view
private var cvBottomAnchor: NSLayoutConstraint?
// Use this flag to disable swipe actions when carousel is showing
private var isCarouselShowing = false
// Use this to check if we are swiping up
private var previousSwipeLocation: CGPoint?
// Random width and height, change as you wish
private let cellWidth: CGFloat = 100
private let collectionViewHeight: CGFloat = 185
private let reuseIdentifier = "cell"
override func viewDidLoad()
{
super.viewDidLoad()
configureNavigationBar()
configureOverlayView()
configureCollectionView()
}
private func configureNavigationBar()
{
title = "Swipe CV"
let appearance = UINavigationBarAppearance()
// Color of the nav bar background
appearance.backgroundColor = .white // primary black for you
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
}
Now once some basic set up is done, here is how the image and the overlay view was set up. Nothing fancy happens here but pay attention to the gesture recognizer added to the overlay view
private func configureOverlayView()
{
imageView.image = UIImage(named: "dog")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
overlayView.backgroundColor = colors.first!.withAlphaComponent(0.5)
overlayView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(imageView)
view.addSubview(overlayView)
// Auto layout pinning the image view and overlay view
// to the main container view
view.addConstraints([
imageView.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
imageView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
imageView.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
imageView.bottomAnchor
.constraint(equalTo: view.bottomAnchor),
overlayView.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
overlayView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
overlayView.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
overlayView.bottomAnchor
.constraint(equalTo: view.bottomAnchor)
])
// We will observe a swipe gesture to check if it is a swipe
// upwards and then react accordingly
let swipeGesture = UIPanGestureRecognizer(target: self,
action: #selector(didSwipe(_:)))
overlayView.addGestureRecognizer(swipeGesture)
}
Now once that is done, we need to create a horizontal scrolling uicollectionview that is positioned off screen which can be animated in when we swipe up, here is how this is done
private func configureCollectionView()
{
collectionView = UICollectionView(frame: .zero,
collectionViewLayout: createLayout())
collectionView.backgroundColor = .clear
collectionView.register(UICollectionViewCell.self,
forCellWithReuseIdentifier: reuseIdentifier)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.showsHorizontalScrollIndicator = false
// Add some padding for the content on the left
collectionView.contentInset = UIEdgeInsets(top: 0,
left: 15,
bottom: 0,
right: 0)
collectionView.dataSource = self
collectionView.delegate = self
overlayView.addSubview(collectionView)
// Collection View should start below the screen
// We need to persist with this constraint so we can change it later
let bottomAnchor = overlayView.safeAreaLayoutGuide.bottomAnchor
cvBottomAnchor
= collectionView.bottomAnchor.constraint(equalTo: bottomAnchor,
constant: collectionViewHeight)
// Collection View starts as hidden and will be animated in swipe up
collectionView.alpha = 0.0
// Add collection view constraints
overlayView.addConstraints([
collectionView.leadingAnchor.constraint(equalTo: overlayView.leadingAnchor),
cvBottomAnchor!,
collectionView.trailingAnchor.constraint(equalTo: overlayView.trailingAnchor),
collectionView.heightAnchor.constraint(equalToConstant: collectionViewHeight)
])
}
private func createLayout() -> UICollectionViewFlowLayout
{
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: cellWidth, height: collectionViewHeight)
layout.minimumInteritemSpacing = 20
return layout
}
The code up to this point should give you something like this:
You still do not see the collection view and for that, implement the didSwipe action of the pan gesture
#objc
private func didSwipe(_ gesture: UIGestureRecognizer)
{
if !isCarouselShowing
{
let currentSwipeLocation = gesture.location(in: view)
if gesture.state == .began
{
// record the swipe location when we start the pan gesture
previousSwipeLocation = currentSwipeLocation
}
// On swipe continuation, verify the swipe is in the upward direction
if gesture.state == .changed,
let previousSwipeLocation = previousSwipeLocation,
currentSwipeLocation.y < previousSwipeLocation.y
{
isCarouselShowing = true
revealCollectionView()
}
}
}
// Animate the y position of the collection view and the alpha
private func revealCollectionView()
{
// We need to set the top constraint (y position)
// to be somewhere above the screen plus some padding
cvBottomAnchor?.constant = 0 - 75
UIView.animate(withDuration: 0.25) { [weak self] in
// animate change in constraints
self?.overlayView.layoutIfNeeded()
// reveal the collection view
self?.collectionView.alpha = 1.0
} completion: { (finished) in
// do something
}
}
Finally, just for completeness, here is the collection view data source and delegate:
extension SwipeCollectionView: UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int
{
return colors.count
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell
= collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier,
for: indexPath)
cell.layer.cornerRadius = 20
cell.clipsToBounds = true
cell.contentView.backgroundColor = colors[indexPath.item]
return cell
}
}
extension SwipeCollectionView: UICollectionViewDelegate
{
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath)
{
overlayView.backgroundColor
= colors[indexPath.item].withAlphaComponent(0.5)
}
}
Now running this should give you this experience when you swipe up:
I believe this should be enough to get you started
Update
The full source code of the example is available on a gist here

UIImageView not resizing as circle and UILabel not resizing within StackView and Custom Collection Cell

I am trying to resize my UIImageView as a circle, however; every time I try to resize the UIImageView, which is inside a StackView along with the UILabel, I keep on ending up with a more rectangular shape. Can someone show me where I am going wrong I have been stuck on this for days? Below is my code, and what it's trying to do is add the image with the label at the bottom, and the image is supposed to be round, and this is supposed to be for my collection view controller.
custom collection view cell
import UIKit
class CarerCollectionViewCell: UICollectionViewCell {
static let identifier = "CarerCollectionViewCell"
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.frame = CGRect(x: 0, y: 0, width: 20, height: 20);
//imageView.center = imageView.superview!.center;
imageView.contentMode = .scaleAspectFill
imageView.layer.borderWidth = 4
imageView.layer.masksToBounds = false
imageView.layer.borderColor = UIColor.orange.cgColor
imageView.layer.cornerRadius = imageView.frame.height / 2
return imageView
}()
private let carerNamelabel: UILabel = {
let carerNamelabel = UILabel()
carerNamelabel.layer.masksToBounds = false
carerNamelabel.font = .systemFont(ofSize: 12)
carerNamelabel.textAlignment = .center
carerNamelabel.layer.frame = CGRect(x: 0, y: 0, width: 50, height: 50);
return carerNamelabel
}()
private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.layer.masksToBounds = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.backgroundColor = .systemOrange
stackView.distribution = .fillProportionally
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override init(frame: CGRect) {
super.init(frame: frame)
configureContentView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func configureContentView() {
imageView.clipsToBounds = true
stackView.clipsToBounds = true
carerNamelabel.clipsToBounds = true
contentView.addSubview(stackView)
configureStackView()
}
private func configureStackView() {
allContraints()
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(carerNamelabel)
}
private func allContraints() {
setStackViewConstraint()
}
private func setStackViewConstraint() {
stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor).isActive = true
stackView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true
stackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
}
public func configureImage(with imageName: String, andImageName labelName: String) {
imageView.image = UIImage(named: imageName)
carerNamelabel.text = labelName
}
override func layoutSubviews() {
super.layoutSubviews()
stackView.frame = contentView.bounds
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
carerNamelabel.text = nil
}
}
Below here is my code for the CollectionViewControler
custom collection view
import UIKit
class CarerViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.itemSize = CGSize(width: 120, height: 120)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(CarerCollectionViewCell.self, forCellWithReuseIdentifier: CarerCollectionViewCell.identifier)
collectionView.showsVerticalScrollIndicator = false
collectionView.backgroundColor = .clear
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
// Layout constraints for `collectionView`
NSLayoutConstraint.activate([
collectionView.widthAnchor.constraint(equalTo: view.widthAnchor),
collectionView.heightAnchor.constraint(lessThanOrEqualTo: view.heightAnchor, constant: 600),
collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
])
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CarerCollectionViewCell.identifier, for: indexPath) as! CarerCollectionViewCell
cell.configureImage(with: "m7opt04g_ms-dhoni-afp_625x300_06_July_20", andImageName: "IMAGE NO. 1")
return cell
}
}
Can somebody show or point to me what I am doing wrong, thank you
This is what I am trying to achieve
UIStackView could be tricky sometimes, you can embed your UIImageView in a UIView, and move your layout code to viewWillLayoutSubviews(), this also implys for the UILabel embed that also inside a UIView, so the containers UIViews will have a static frame for the UIStackViewto be layout correctly and whats inside them will only affect itself.

UIScrollView not showing up in the view

I am implementing a UIScrollView in a CollectionViewCell. I have a custom view which the scroll view should display, hence I am performing the following program in the CollectionViewCell. I have created everything programmatically and below is my code :
struct ShotsCollections {
let title: String?
}
class ShotsMainView: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
containerScrollView.contentSize.width = frame.width * CGFloat(shotsData.count)
shotsData = [ShotsCollections.init(title: "squad"), ShotsCollections.init(title: "genral")]
var i = 0
for data in shotsData {
let customview = ShotsMediaView(frame: CGRect(x: containerScrollView.frame.width * CGFloat(i), y: 0, width: containerScrollView.frame.width, height: containerScrollView.frame.height))
containerScrollView.addSubview(customview)
i += 1
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var shotsData = [ShotsCollections]()
var containerScrollView: UIScrollView = {
let instance = UIScrollView()
instance.isScrollEnabled = true
instance.bounces = true
instance.backgroundColor = blueColor
return instance
}()
private func setupViews() { //These are constraints by using TinyConstraints
addSubview(containerScrollView)
containerScrollView.topToSuperview()
containerScrollView.bottomToSuperview()
containerScrollView.rightToSuperview()
containerScrollView.leftToSuperview()
}
}
Now the issue is, while the scrollview is displayed, the content in it is not. I on printing the contentSize and frame of the scrollview, it displays 0. But if I check the Debug View Hierarchy, scrollview containes 2 views with specific frames.
I am not sure whats going wrongs. Any help is appreciated.
When you are adding customView in your containerScrollView, you are not setting up the constraints between customView and containerScrollView.
Add those constraints and you will be able to see your customViews given that your customView has some height. Also, when you add more view, you would need to remove the bottom constraint of the last added view and create a bottom constraint to the containerScrollView with the latest added view.
I created a sample app for your use case. I am pasting the code and the resultant screen shot below. Hope this is the functionality you are looking for. I suggest you paste this in a new project and tweak the code until you are satisfied. I have added comments to make it clear.
ViewController
import UIKit
class ViewController: UIViewController {
// Initialize dummy data array with numbers 0 to 9
var data: [Int] = Array(0..<10)
override func loadView() {
super.loadView()
// Add collection view programmatically
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(ShotsMainView.self, forCellWithReuseIdentifier: ShotsMainView.identifier)
self.view.addSubview(collectionView)
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: collectionView.topAnchor),
self.view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
self.view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
self.view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
])
collectionView.delegate = self
collectionView.dataSource = self
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = UIColor.white
self.view.addSubview(collectionView)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = UIColor.white
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ShotsMainView.identifier, for: indexPath) as! ShotsMainView
return cell
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// The cell dimensions are set from here
return CGSize(width: collectionView.frame.size.width, height: 100.0)
}
}
ShotsMainView
This is the collection view cell
import UIKit
class ShotsMainView: UICollectionViewCell {
static var identifier = "Cell"
weak var textLabel: UILabel!
override init(frame: CGRect) {
// Initialize with zero frame
super.init(frame: frame)
// Add the scrollview and the corresponding constraints
let containerScrollView = UIScrollView(frame: .zero)
containerScrollView.isScrollEnabled = true
containerScrollView.bounces = true
containerScrollView.backgroundColor = UIColor.blue
containerScrollView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(containerScrollView)
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: containerScrollView.topAnchor),
self.bottomAnchor.constraint(equalTo: containerScrollView.bottomAnchor),
self.leadingAnchor.constraint(equalTo: containerScrollView.leadingAnchor),
self.trailingAnchor.constraint(equalTo: containerScrollView.trailingAnchor)
])
// Add the stack view that will hold the individual items that
// in each row that need to be scrolled horrizontally
let stackView = UIStackView(frame: .zero)
stackView.distribution = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
containerScrollView.addSubview(stackView)
stackView.backgroundColor = UIColor.magenta
NSLayoutConstraint.activate([
containerScrollView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
containerScrollView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
containerScrollView.topAnchor.constraint(equalTo: stackView.topAnchor),
containerScrollView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor)
])
// Add individual items (Labels in this case).
for i in 0..<10 {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(label)
label.text = "\(i)"
label.font = UIFont(name: "System", size: 20.0)
label.textColor = UIColor.white
label.backgroundColor = UIColor.purple
label.layer.masksToBounds = false
label.layer.borderColor = UIColor.white.cgColor
label.layer.borderWidth = 1.0
label.textAlignment = .center
NSLayoutConstraint.activate([
label.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1.0, constant: 0.0),
label.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.2, constant: 0.0)
])
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Screenshot

How can I change my UICollectionView's Flow Layout to a vertical List with Horizontal Scrolling

Basically what I am trying to create is a table with three cells stacked on top of one another. But, if there are more than three cells, I want to be able to swipe left on the Collection View to show more cells. Here is a picture to illustrate.
Right now I have the cells arranged in a list but I cannot seem to change the scroll direction for some reason. - They still scroll vertically
Here is my current code for the Flow Layout:
Note: I'm not going to include the Collection View code that is in my view controller as I do not think it is relevant.
import Foundation
import UIKit
class HorizontalListCollectionViewFlowLayout: UICollectionViewFlowLayout {
let itemHeight: CGFloat = 35
func itemWidth() -> CGFloat {
return collectionView!.frame.width
}
override var itemSize: CGSize {
set {
self.itemSize = CGSize(width: itemWidth(), height: itemHeight)
}
get {
return CGSize(width: itemWidth(), height: itemHeight)
}
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return collectionView!.contentOffset
}
override var scrollDirection: UICollectionViewScrollDirection {
set {
self.scrollDirection = .horizontal
} get {
return self.scrollDirection
}
}
}
If you have your cells sized correctly, Horizontal Flow Layout will do exactly what you want... fill down and across.
Here is a simple example (just set a view controller to this class - no IBOutlets needed):
//
// ThreeRowCViewViewController.swift
//
// Created by Don Mag on 6/20/17.
//
import UIKit
private let reuseIdentifier = "LabelItemCell"
class LabelItemCell: UICollectionViewCell {
// simple CollectionViewCell with a label
#IBOutlet weak var theLabel: UILabel!
let testLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 14)
label.textColor = UIColor.black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addViews()
}
func addViews(){
addSubview(testLabel)
testLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 8.0).isActive = true
testLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: 0.0).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ThreeRowCViewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
// 3 gray colors for the rows
let cellColors = [
UIColor.init(white: 0.9, alpha: 1.0),
UIColor.init(white: 0.8, alpha: 1.0),
UIColor.init(white: 0.7, alpha: 1.0)
]
var theCodeCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
// height we'll use for the rows
let rowHeight = 30
// just picked a random width of 240
let rowWidth = 240
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
// horizontal collection view direction
layout.scrollDirection = .horizontal
// each cell will be the width of the collection view and our pre-defined height
layout.itemSize = CGSize(width: rowWidth - 1, height: rowHeight)
// no item spacing
layout.minimumInteritemSpacing = 0.0
// 1-pt line spacing so we have a visual "edge" (with horizontal layout, the "lines" are vertical blocks of cells
layout.minimumLineSpacing = 1.0
theCodeCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
theCodeCollectionView.dataSource = self
theCodeCollectionView.delegate = self
theCodeCollectionView.register(LabelItemCell.self, forCellWithReuseIdentifier: reuseIdentifier)
theCodeCollectionView.showsVerticalScrollIndicator = false
// set background to orange, just to make it obvious
theCodeCollectionView.backgroundColor = .orange
theCodeCollectionView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(theCodeCollectionView)
// set collection view width x height to rowWidth x (rowHeight * 3)
theCodeCollectionView.widthAnchor.constraint(equalToConstant: CGFloat(rowWidth)).isActive = true
theCodeCollectionView.heightAnchor.constraint(equalToConstant: CGFloat(rowHeight * 3)).isActive = true
// center the collection view
theCodeCollectionView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0).isActive = true
theCodeCollectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0).isActive = true
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 12
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! LabelItemCell
cell.backgroundColor = cellColors[indexPath.row % 3]
cell.testLabel.text = "\(indexPath)"
return cell
}
}
I'll leave the "enable paging" part up to you :)

Resources