I have 4 UIButtons in a 2x2 grid, and I want them to move themselves into one column with 4 rows when I toggle a SegmentedControl.
What's the best way of going about this? Do I need to change the current constraints, or is there another way?
Thanks.
You can use UICollectionViewController.
On tap of UISegmentControl you can change the size of cell under sizeForItemAtIndexPath() method based on condition.
Sample Code:
class YourViewController : UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if (listViewType == "kTileView") {
let size = collectionView.frame.width / 2
return CGSize(width: size, height: 50)
}
else {
let size = collectionView.frame.width
return CGSize(width: size, height: 50)
}
}
}
I guess you want something like this:
One way to rearrange four subviews from a column to a 2x2 grid is using stack views. Make a vertical stack view for 2-element column of the 2x2 grid, and put those two column stack views in a horizontal stack view. Then, to turn the grid into a single column, you set the axis of the horizontal stack view to vertical.
Here's my storyboard:
And here's my view controller:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
updateViews()
}
#IBOutlet var segmentedControl: UISegmentedControl!
#IBOutlet var rearrangingStackView: UIStackView!
#IBAction func segmentedControlValueChanged(_ sender: Any) {
UIView.animate(withDuration: 0.25) {
self.updateViews()
self.view.layoutIfNeeded()
}
}
private func updateViews() {
if segmentedControl.selectedSegmentIndex == 0 {
rearrangingStackView.axis = .vertical
} else {
rearrangingStackView.axis = .horizontal
}
}
}
Related
I am trying to implement an application that allows a user to swipe horizontally between collection view cells while also being able to scroll vertically to see the entire content of a particular cell.
I want all the collection view cells to have a table view embedded within them.
The problem I am running into as of now is that my horizontal scrolling works as I have set the collection view flow layout to horizontal. I know that collection view flow layouts can only support one direction. Because of this, I tried to implement the following solution.
As of now within my project, I have a view controller with a scroll view inside. Embedded within the scroll view, is a collection view. This collection view has its own header implemented via dequeueReusableSupplementaryView.
I know that constraints can often be an issue preventing vertical scrolling to take place so here is a picture of my constraints:
Additionally, here is some code that I have used to implement this system:
Determining the scroll view content size
override func viewDidLayoutSubviews() {
super.viewWillLayoutSubviews()
scrollView.contentSize.height = collectionView.frame.size.height
scrollView.contentSize.width = self.view.frame.size.width
}
Setting up the collection view
func setupCollectionView() {
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(UINib(nibName: "TableViewHolderCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "tableViewHolderCollectionViewCell")
collectionView.register(UINib(nibName: "CustomCollectionViewHeaderView", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "CustomCollectionViewHeaderView")
collectionView.delaysContentTouches = true
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.bounces = false
collectionView.isPagingEnabled = true
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.sectionHeadersPinToVisibleBounds = true
layout.sectionInset = UIEdgeInsets(top: 0, left: -self.view.frame.size.width, bottom: 0, right: 0)
collectionView.setCollectionViewLayout(layout, animated: false)
}
Setting up the collection view data
extension TasksAndScheduleViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 8
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: self.view.frame.size.width, height: self.view.frame.size.height)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "tableViewHolderCollectionViewCell", for: indexPath) as! TableViewHolderCollectionViewCell
cell.backgroundColor = colorArray[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 343)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
//setting up header view
return headerView
}
}
After trying to debug, I realized that the collection view vertical scroll might have been overriding the vertical scroll of the scroll view I had added to my view controller. In order to solve this, I created a custom class (as seen below) which my collection view implemented. To my knowledge, this was successful at disabling the vertical scroll for the collection view but it was not successful in enabling the other scroll view's vertical scroll.
class CollectionViewVerticalScroll: UICollectionView {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let direction = panGestureRecognizer.direction(in: self)
if direction.contains(.Down) || direction.contains(.Up) {
return false
}
return true
}
}
My desired goal is to have the ability to swipe horizontally between these collection view cells that have table views while also being able to vertically scroll the collection view cell and the contents of the table view embedded inside of it. Ideally, the vertical scroll should allow me to scroll the table view cells while also moving the entire view upwards. The closest example to what I am trying to implement that I could find online is twitter's search page. The only difference is that my application has a collection header view and no navigation bar. I have attached a picture below:
I would appreciate any help. Please do let me know if you have any questions or if something doesn't make sense to you.
I am testing out some code and it looks like in iOS13 the UICollectionViewFlowLayout is not receiving the changes in the collectionView frame.
Below is a sample code, in which I simply change the height of the collectionView based on the amount I scroll inthe tableview below the collectionView:
ViewController
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.collectionViewLayout.invalidateLayout()
let totalScroll = scrollView.contentSize.height - scrollView.bounds.size.height
let offset = (scrollView.contentOffset.y)
let percentage = offset / totalScroll
var frame = collectionView.frame
frame.size.height = 40 - (40 * percentage)
collectionView.frame = frame
}
CustomCollectionViewFlowLayout: UICollectionViewFlowLayout
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
print(collectionView?.frame)
return attributes
}
The print statement inside the CustomCollectionViewFlowLayout in iOS 12 and below prints out the changes in collectionView.frame correctly i.e. the height actually changes. But in iOS 13, it isn't being reflected at all.
Help anybody?
First of all,
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.collectionViewLayout.invalidateLayout()
...
}
looks suspicious. Are you sure that your layout should be invalidated inside the scrollViewDidScroll function?
There is a proper place where a layout should be invalidated while scrolling:
final class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// proper place to invalidate layout while scrolling
}
}
If you invalidate your layout inside the layout class you will be able to calculate the desired layout container height properly. And then you can inform your collection view that its height should be changed. For example, with the help of some delegate:
protocol CustomCollectionViewFlowLayoutSizeDelegate: class {
func newSizeAvailableFor(layout: CustomCollectionViewFlowLayout, progress: CGFloat)
}
final class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
weak var sizeDelegate: CustomCollectionViewFlowLayoutSizeDelegate?
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// proper place to invalidate layout while scrolling
}
}
final class CustomViewController: CustomCollectionViewFlowLayoutSizeDelegate {
func newSizeAvailableFor(layout: CustomCollectionViewFlowLayout, progress: CGFloat) {
// change collectionView frame
}
}
Conclusion:
Your layout is trying to listen to its container height, but its container height is calculated based on your layout. You can easily drop the container dependency for height calculation or just provide the layout with initial container height. In the end the layout will be responsible for all the calculations.
I have a custom UICollectionViewCell that has a UIImage and a UIButton.
The frame for the extended cell is 110 x 60. By default it will be 60x60.
When the app loads, I'd like for the cell to start at 60x60 and only show the image. When the cell is tapped, the cell will update to the 110x60 frame and reveal the UIButton that is beside the image.
Currently, my app does load and the cells are 60x60, but due to my auto-layout setup the image is squished and the button is full size. If I tap on the cell, it does update it's frame and it looks great.
The goal is to only see the image first and then see the button after the cell has updated its frame.
I would also like to be able to tap on the cell again and resize it back to 60x60, hiding the button and only showing the image.
Here is what I am currently trying:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.performBatchUpdates(nil, completion: nil)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
switch collectionView.indexPathsForSelectedItems?.first {
case .some(indexPath):
return CGSize(width: 110.0, height: 60.0)
default:
return CGSize(width: 60.0, height: 60.0)
}
}
Per request, my CollectionViewCell Class code:
class myCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var myImageView: UIImageView!
#IBOutlet weak var myButton: UIButton!
var myCellDelegate : myCollectionViewCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = 30
self.layer.masksToBounds = true
myImageView.layer.cornerRadius = myImageView.frame.width / 2
myImageView.layer.masksToBounds = true
}
// MARK: - Actions
#IBAction func myButtonTapped(_ sender: Any) {
self.myCellDelegate?.actionClicked(self)
}
}
To note, there's not much there so not sure if it'll help any. I'm just adjusting the cornerRadius for the cell and my image and then creating a delegate for the action to the button.
I think much depends by your constraints in the nib file.
Option 1
Interface builder > select your ImageView > Right Panel > Size inspector > Play with the "content Hugging Priority" and "content Compression Resistance"
In particular the Horizontal compression resistance of the imageView has to be greater than compression resistance of the button.
The system chooses the view to stretch based of the priorities indicated for these two parameters.
Option 2
Top
+-------------+--------+
| | |
| | |
Left| (Image) |(Button)|Right
| | |
| | |
+-------------+--------+
Bottom
<------------->
Width
Left, Top, Right, Bottom ---> constraint to the cell contentView
Width ---> set to a fixed 60
(Remember to enable clipsToBounds)
When the cell enlarges your button will appear.
You can eventually add an animation.
I'm trying to setup a UICollectionViewLayout programmatically. I'm using a UICollectionView also without using storyboards, as well as settings its constraints.
I've followed Ray Wenderlich tutorial on the subject with some changes to adapt the code to Swift 3 (as AZCoder2 did https://github.com/AZCoder2/Pinterest).
Since all these examples uses storyboards, I've also introduced some changes to create the UICollectionView and its UICollectionViewLayout:
collectionViewLayout = PinterestLayout()
collectionViewLayout.delegate = self
collectionView = UICollectionView.init(frame: .zero, collectionViewLayout: collectionViewLayout)
The result: I can't see anything. If I change the UICollectionViewLayout with the one that Apple provides (UICollectionViewFlowLayout), at least I can see the cells with their content. If I implement some changes and use the storyboard, everything works great but it's not the way I want to accomplish this. The whole view is made programmatically and the collection view is a part of it.
What am I missing? Is it something to do with the way I instantiate the UICollectionViewLayout? Do I have to register something (for example, as I need to register the reusable cell)?
How about you just create a variable that creates your flow layout for you like this
var flowLayout: UICollectionViewFlowLayout {
let _flowLayout = UICollectionViewFlowLayout()
// edit properties here
_flowLayout.itemSize = CGSize(width: 98, height: 134)
_flowLayout.sectionInset = UIEdgeInsetsMake(0, 5, 0, 5)
_flowLayout.scrollDirection = UICollectionViewScrollDirection.horizontal
_flowLayout.minimumInteritemSpacing = 0.0
// edit properties here
return _flowLayout
}
And then you can set it by calling the variable.
self.collectionView.collectionViewLayout = flowLayout // after initializing it another way
// or
UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
I think this will work.
override init(collectionViewLayout layout: UICollectionViewLayout) {
super.init(collectionViewLayout: layout)
collectionView?.collectionViewLayout = YourCollectionViewLayout()
}
It's possible to do what I was trying to do. Basically, follow the tutorials I suggested in my own question and setup the collection view and its view layout as follow:
collectionViewLayout = PinterestLayout()
collectionViewLayout.delegate = self
collectionView = DynamicCollectionView.init(frame: .zero, collectionViewLayout: collectionViewLayout)
Note that I'm using DynamicCollectionView (instead of UICollectionView). This class is not provided by Apple: I've made my own using the code provided in this post.
Remember that this approach is when you're creating a view programmatically, using constraints. (May be it has another cases of use)
I resolved this issue taking these steps:
1 - Changing UICollectionViewController to a UIViewController with a collectionView inside.
2 - On ViewDidLoad I set the delegates and add the view, as usual. Moreover, I instantiate the ViewLayout and use it to instantiate the CollectionView
var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
navigationController!.isToolbarHidden = true
let layout = MosaicViewLayout()
layout.delegate = self
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
guard let collectionView = collectionView else {
return
}
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: "CharacterCell")
setupCollectionConstraints()
}
3 - On the ViewLayout the protocol stays like this:
protocol MosaicViewLayoutDelegate:class {
func collectionView(_ collectionView: UICollectionView,
heightForItemAtIndexPath indexPath: IndexPath) -> CGFloat
}
Don't forget to add the instance inside the class
weak var delegate: MosaicViewLayoutDelegate?
4 - Add the delegate extension to you ViewController
extension HomeViewImpl: MosaicViewLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForItemAtIndexPath indexPath: IndexPath) -> CGFloat {
let random = arc4random_uniform(4) + 1
return CGFloat(random * 100)
}
}
enter code here
I have a UIImageView inside a UICollectionView Cell.
I wanted there to be 2 cells per column in the uicollectionview so I used this code....
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let padding: CGFloat = 25
let collectionCellSize = collectionView.frame.size.width - padding
return CGSize(width: collectionCellSize/2, height: collectionCellSize/2)
}
For the image view I wanted it to be round, and this code usually works elsewhere..
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
I have tried putting that in the cellForItemAt, with no luck
Now inside the CollectionView Cell Class I added it like this
override func layoutSubviews() {
super.layoutSubviews()
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
}
The image looks like a deflated football.
Is the padding code messing up the rounded image view code?
You need to add self.accountImageView.layoutIfNeeded().
And make sure height and width of your imageview is equal
override func layoutSubviews()
{
super.layoutSubviews()
self.accountImageView.layoutIfNeeded() // add this
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
}
If you are using constraint then try this -
override func layoutSubviews() {
super.layoutSubviews()
self.accountImageView.layoutIfNeeded() // Add this line
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
}