Is there a way to give outer border in every section of uicollctionview? - swift

Try to add outer border of every section in a collection view.
If i'm using cell.layer.border, it will also create an inner border. Is there a simple way to create outer border only for every section in collection view?
Try to created red border like image below

As Matt pointed out in the comments and the articles pointed out, you would need to make use of a DecorationView.
You can read up on this here
So to do this, you would have to follow these steps:
Create a custom UICollectionReusableView which would serve as your decoration view
Subclass UICollectionViewFlowLayout to create a custom layout
Override layoutAttributesForDecorationView and layoutAttributesForElements to figure out the frame of each section and place the decoration view in the section frame
Use the custom flow layout as the layout of your collection view
Here is that in code
Create the Decoration view, which is just a regular view with a border
class SectionBackgroundView : UICollectionReusableView {
static let DecorationViewKind = "SectionBackgroundIdentifier"
override init(frame: CGRect) {
super.init(frame: frame)
// Customize the settings to what you want
backgroundColor = .clear
layer.borderWidth = 5.0
layer.borderColor = UIColor.blue.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Create a custom flow layout
class BorderedFlowLayout: UICollectionViewFlowLayout {
override init() {
super.init()
// Register your decoration view for the layout
register(SectionBackgroundView.self,
forDecorationViewOfKind: SectionBackgroundView.DecorationViewKind)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForDecorationView(ofKind elementKind: String,
at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if elementKind == SectionBackgroundView.DecorationViewKind {
guard let collectionView = collectionView else { return nil }
// Initialize a UICollectionViewLayoutAttributes for a DecorationView
let decorationAttributes
= UICollectionViewLayoutAttributes(forDecorationViewOfKind: SectionBackgroundView.DecorationViewKind,
with:indexPath)
// Set it behind other views
decorationAttributes.zIndex = 2
let numberOfItemsInSection
= collectionView.numberOfItems(inSection: indexPath.section)
// Get the first and last item in the section
let firstItem = layoutAttributesForItem(at: IndexPath(item: 0, section: indexPath.section))
let lastItem = layoutAttributesForItem(at: IndexPath(item: (numberOfItemsInSection - 1),
section: indexPath.section))
// The difference between the maxY of the last item and
// the the minY of the first item is the height of the section
let height = lastItem!.frame.maxY - firstItem!.frame.minY
// Set the frame of the decoration view for the section
decorationAttributes.frame = CGRect(x: 0,
y: firstItem!.frame.minY,
width: collectionView.bounds.width,
height: height)
return decorationAttributes
}
return nil
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Get all the UICollectionViewLayoutAttributes for the current view port
var attributes = super.layoutAttributesForElements(in: rect)
// Filter to get all the different sections
let sectionAttributes
= attributes?.filter { $0.indexPath.item == 0 } ?? []
// Loop through the different sections
for sectionAttribute in sectionAttributes {
// Create decoration attributes for the current section
if let decorationAttributes
= self.layoutAttributesForDecorationView(ofKind: SectionBackgroundView.DecorationViewKind,
at: sectionAttribute.indexPath) {
// Add the decoration attributes for a section if it is in the current viewport
if rect.intersects(decorationAttributes.frame) {
attributes?.append(decorationAttributes)
}
}
}
return attributes
}
}
Make use of the custom layout in your view controller
private func configureCollectionView() {
collectionView = UICollectionView(frame: CGRect.zero,
collectionViewLayout: createLayout())
collectionView.backgroundColor = .white
collectionView.register(UICollectionViewCell.self,
forCellWithReuseIdentifier: "cell")
// You can ignore the header and footer views as you probably already did this
collectionView.register(HeaderFooterView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: HeaderFooterView.identifier)
collectionView.register(HeaderFooterView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
withReuseIdentifier: HeaderFooterView.identifier)
collectionView.dataSource = self
collectionView.delegate = self
view.addSubview(collectionView)
}
private func createLayout() -> UICollectionViewFlowLayout {
let flowLayout = BorderedFlowLayout()
flowLayout.minimumLineSpacing = 10
flowLayout.minimumInteritemSpacing = 10
flowLayout.scrollDirection = .vertical
flowLayout.sectionInset = UIEdgeInsets(top: 10,
left: horizontalPadding,
bottom: 10,
right: horizontalPadding)
return flowLayout
}
Doing all of this should give you what you want
I have only posted the most important snippets. If for some reason you can't follow along, here is the full code to recreate the example

Related

swift tvOS vertical scroll Grid

I am new to tvOS development and I would like to create a Grid with 4 items per row. The collection should vertically scroll. My challenge is that I'm not sure what to do with the information provided by Apple here in this doc as there is no example.
In iOS we would use something like:
class GridFlowLayout: UICollectionViewFlowLayout {
// here you can define the height of each cell
let itemHeight: CGFloat = 160
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
/**
Sets up the layout for the collectionView. 1pt distance between each cell and 1pt distance between each row plus use a vertical layout
*/
func setupLayout() {
minimumInteritemSpacing = 1
minimumLineSpacing = 1
scrollDirection = .vertical
}
var itemWidth: CGFloat {
return collectionView!.frame.width / 4 - 1
}
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
}
}
Then assign it to the collectionView layout
collectionView.collectionViewLayout = GridFlowLayout()
Im not sure what to do for tvOS how should I adapt the above code to work for tvOS using programmatic code.

How to create this view with constraints programtically

I feel this is pretty simple to accomplish but I can't seem to figure it out. I'm fairly new to not using the storyboard and trying to learn how to set my constraints programatically for my views. I created the view that I want easily in storyboard but can't seem to get it programatically.
I have my view controller has my parent view, and then I call a container view. I imagine in the container view is where I setup my constraints but I can't get the height of my view to stay the same every-time I change to a different device
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var clariView = ClariContainerView()
view.addSubview(clariView)
}
}
This my view controller and then my ClariContainerView looks like this:
class ClariContainerView: UIView {
lazy var clariQuestionView: UIView = {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 0))
containerView.backgroundColor = .blue
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
public func setupView() {
addSubview(clariQuestionView)
setupLayout()
}
public func setupLayout() {
NSLayoutConstraint.activate([
clariQuestionView.heightAnchor.constraint(equalToConstant: 169)
])
}
}
What I'm trying to recreate is this:
I need the height of the blue view to always be 169.
Here is how you would do that:
First, you don't need to define a frame for your containerView since the translatesAutoresizingMaskIntoConstraints = falsestatement is specifying that you'll be using auto-layout and therefore the frame will be ignored:
lazy var clariQuestionView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .blue
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
And here is how you would define your constraints. You need to set height, but also need to pin the view to the bottom, the leading, and the trailing edges of self.view:
public func setupLayout() {
NSLayoutConstraint.activate([
clariQuestionView.heightAnchor.constraint(equalToConstant: 169),
clariQuestionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
clariQuestionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
clariQuestionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
}
For such a basic layout you don't really need to add heightAnchor. Here is a simple way to achieve desired behavior + bonus — a code snippet to adjust height according to the device's safeAreaInsets.
class ClariContainerView: UIView {
lazy var clariQuestionView: UIView = {
let desiredContainerHeigh = 169
// If you want, you can use commented code to adjust height according to the device's safe area.
// This might be needed if you want to keep the same height over safe area on all devices.
// let safeAreaAdjustment = UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets.bottom ?? 0
let containerView = UIView(frame: CGRect(x: 0, y: UIScreen.main.bounds.height - 169, width: UIScreen.main.bounds.width, height: 169))
containerView.backgroundColor = .blue
containerView.translatesAutoresizingMaskIntoConstraints = true
return containerView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
public func setupView() {
addSubview(clariQuestionView)
}
}

UICollectionView ScrollTo Crashing

I am attempting to have my UICollectionView in my custom UIView, be able to scroll to a page when a button is pressed in the main VC. Unfortunately everytime I call the method to do it in the MainVC, it crashes, # to scrollToItem .
here is my MainVC.
let mainView = MainViewsHome()
In the ViewWillAppear:
/** Setting up the bottom half **/
mainView.frame = CGRect(x:self.view.frame.width * 0, y:self.view.frame.height / 6.2, width:self.view.frame.width,height:self.view.frame.height / 1.1925)
mainView.backgroundColor = UIColor.clear
self.view.addSubview(mainView)
&& My custom UIView
class MainViewsHome: UIView, UIGestureRecognizerDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var cellId = "Cell"
var collectionView : UICollectionView!
override init(frame: CGRect) {
super.init(frame: CGRect(x:0, y:0, width:UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 1.3))
/**Creation of the View **/
let flowLayout : UICollectionViewFlowLayout = UICollectionViewFlowLayout()
flowLayout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
flowLayout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: CGRect(x:self.frame.width * 0,y:self.frame.height * 0,width:self.frame.width,height: self.frame.height), collectionViewLayout: flowLayout)
collectionView.register(uploadGenreSelectionCVC.self, forCellWithReuseIdentifier: cellId)
collectionView.isPagingEnabled = true
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = UIColor.purple
self.addSubview(collectionView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveToPage() {
print("we are moving to page")
self.collectionView.scrollToItem(at: 2 as IndexPath, at: UICollectionViewScrollPosition.right, animated: true)
}
&& Then in my MainVC I am calling: mainView.moveToPage() Where am I going wrong with my understanding?
Problem is with 2 as IndexPath, IndexPath is not Integer so you need to make the object of IndexPath using its init init(item:section:). and after that scroll collectionView to specific item this way.
func moveToPage() {
let indexPath = IndexPath(item: 2, section: 0) //Change section from 0 to other if you are having multiple sections
self.collectionView.scrollToItem(at: indexPath, at: .right, animated: true)
}
Note: CollectionView's items start at 0 index so if you want to scroll to 2nd item then make indexPath using IndexPath(item: 1, section: 0) because 2nd will scroll at 3rd cell of collectionView.

Nothing Occurring with ScrollToItem in UICollectionView

I have two collection views within a ViewController. One Collection view represents the tab bar at the top, the other represents pages on the bottom half of the screen. I have the second UICollectionView (on the bottom half) within a custom UIView (loading from another class). My objective is to have when I click on the tab bar, to move the bottom collectionView accordingly, with regards to the scrollToIndex. My issue is that when I click on the tab bar at the top, and call the function that is in my custom UIView, nothing occurs. Do I need to share information through a delegate/protocol? What am I doing incorrectly?
In my MainVC I have:
/** Setting up the top header **/
let topBar = UIView()
/** Setting up the connection to the Bottom Collection View **/
let mainView = MainViewsHome()
override func viewWillAppear(_ animated: Bool) {
self.view.layoutIfNeeded()
/**Setting up the header that the buttons lay on **/
topBar.frame = CGRect(x:self.view.frame.width * 0, y:self.view.frame.height * 0, width:self.view.frame.width,height:self.view.frame.height / 6.2)
topBar.backgroundColor = UIColor.red
self.view.addSubview(topBar)
/** Setting up the buttons in the header**/
setUpHeaderBarButtons()
/** Setting up the bottom half **/
mainView.frame = CGRect(x:self.view.frame.width * 0, y:self.view.frame.height / 6.2, width:self.view.frame.width,height:self.view.frame.height / 1.1925)
mainView.backgroundColor = UIColor.clear
self.view.addSubview(mainView)
mainView.delegate = self
}
& Then setting up the collectionView for the MainVC (the bar buttons)
func setUpHeaderBarButtons() {
let flowLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: CGRect(x: topBar.frame.width * 0, y:topBar.frame.height / 1.75, width: topBar.frame.width, height: topBar.frame.height / 3.6), collectionViewLayout: flowLayout)
collectionView.register(SelectionCVC.self, forCellWithReuseIdentifier: cellId)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = UIColor.clear
topBar.addSubview(collectionView)
}
&& Then the didSelect for the MainVC (The bar buttons)
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("selecting cell")
mainView.moveToPage()
}
&& Then my custom UIVIew with the bottom collection view that I wish to trigger a scrollToIndex
class MainViewsHome: UIView, UIGestureRecognizerDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var cellId = "Cell"
var collectionView2 : UICollectionView!
override init(frame: CGRect) {
super.init(frame: CGRect(x:0, y:0, width:UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 1.27))
/**Creation of the View **/
let flowLayout2 : UICollectionViewFlowLayout = UICollectionViewFlowLayout()
flowLayout2.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
flowLayout2.scrollDirection = .horizontal
let collectionView2 = UICollectionView(frame: CGRect(x:self.frame.width * 0,y:self.frame.height * 0,width:self.frame.width,height: self.frame.height), collectionViewLayout: flowLayout2)
collectionView2.register(SelectionCVC.self, forCellWithReuseIdentifier: cellId)
collectionView2.isPagingEnabled = true
collectionView2.delegate = self
collectionView2.dataSource = self
collectionView2.backgroundColor = UIColor.purple
self.addSubview(collectionView2)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func moveToPage() {
// print("we are moving to page")
let indexPath = IndexPath(item: 2, section: 0) //Change section from 0 to other if you are having multiple sections
self.collectionView2?.scrollToItem(at: indexPath, at: [], animated: true)
}
}
where am i going wrong?
In the init of MainViewsHome instead of initializing instance property collectionView2 you are initializing completely new collectionView so you are not having reference in collectionView2.
It should be
self.collectionView2 = UICollectionView(frame: CGRect(x:self.frame.width * 0,y:self.frame.height * 0,width:self.frame.width,height: self.frame.height), collectionViewLayout: flowLayout2)
Instead of
let collectionView2 = UICollectionView(frame: CGRect(x:self.frame.width * 0,y:self.frame.height * 0,width:self.frame.width,height: self.frame.height), collectionViewLayout: flowLayout2)

Can you get a UITableView's intrinsic content size to update based on the number of rows shown if scrolling is disabled?

We have a portion of our UI which is a small list of labels with color swatches next to them. The design I'm taking over has six of these hard-coded in the layout even though the actual data is dynamic, meaning if we only need to show three, we have to explicitly hide three, which also throws off the balance of the page. Making matters worse is each one of those 'items' is actually made up of several sub-views so a screen with six hard-coded items has eighteen IBOutlets.
What I'm trying to do is to instead use a UITableView to represent this small portion of the screen, and since it won't scroll, I was wondering if you can use AutoLayout to configure the intrinsic content height of the UITableView to be based on the number of rows.
Currently I have a test page with a UITableView vertically constrained to the center, but without a height constraint because I am hoping to have the table's intrinsic content size reflect the visible rows. I have also disabled scrolling on the table. When I reload the table, I call updateConstraints. But the table still does not resize.
Note: We can't use a UIStackView (which would have been perfect for this) because we have to target iOS8 and that wasn't introduced until iOS9, hence this solution.
Has anyone been able to do something similar to our needs?
Ok, so unlike UITextView, it doesn't look like UITableView ever returns an intrinsic size based on the visible rows. But that's not that big a deal to implement via a subclass, especially if there's a single section, no headers or footers, and the rows are of a fixed height.
class AutoSizingUiTableView : UITableView
{
override func intrinsicContentSize() -> CGSize
{
let requiredHeight = rowCount * rowHeight
return CGSize(width: UIView.noIntrinsicMetric, height: CGFloat(requiredHeight))
}
}
I'll leave it up to the reader to figure out how to get their own rowCount. The same if you have variable heights, multiple sections, etc. You just need more logic.
By doing this, it works great with AutoLayout. I just wish it handled this automatically.
// Define this puppy:
class AutoTableView: UITableView {
override func layoutSubviews() {
super.layoutSubviews()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
get {
var height:CGFloat = 0;
for s in 0..<self.numberOfSections {
let nRowsSection = self.numberOfRows(inSection: s)
for r in 0..<nRowsSection {
height += self.rectForRow(at: IndexPath(row: r, section: s)).size.height;
}
}
return CGSize(width: UIView.noIntrinsicMetric, height: height)
}
set {
}
}
}
and make it your class in IB.
obs: this is if your class is only cells and shit. if it has header, footer or some other thign, dunno. it'll not work. for my purposes it works
peace
This can be done, please see below for a very simple (and rough - rotation does not work properly!) example, which allows you to update the size of the table view by entering a number in the text field and resetting with a button.
import UIKit
class ViewController: UIViewController {
var tableViewController : FlexibleTableViewController!
var textView : UITextView!
var button : UIButton!
var count : Int! {
didSet {
self.refreshDataSource()
}
}
var dataSource : [Int]!
let rowHeight : CGFloat = 50
override func viewDidLoad() {
super.viewDidLoad()
// Configure
self.tableViewController = FlexibleTableViewController(style: UITableViewStyle.plain)
self.count = 10
self.tableViewController.tableView.backgroundColor = UIColor.red
self.textView = UITextView()
self.textView.textAlignment = NSTextAlignment.center
self.textView.textColor = UIColor.white
self.textView.backgroundColor = UIColor.blue
self.button = UIButton()
self.button.setTitle("Reset", for: UIControlState.normal)
self.button.setTitleColor(UIColor.white, for: UIControlState.normal)
self.button.backgroundColor = UIColor.red
self.button.addTarget(self, action: #selector(self.updateTable), for: UIControlEvents.touchUpInside)
self.layoutFrames()
// Assemble
self.view.addSubview(self.tableViewController.tableView)
self.view.addSubview(self.textView)
self.view.addSubview(self.button)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func refreshDataSource() -> Void {
if let _ = self.dataSource {
if !self.dataSource.isEmpty {
self.dataSource.removeAll()
}
}
else
{
self.dataSource = [Int]()
}
for count in 0..<self.count {
self.dataSource.append(count)
}
self.tableViewController.dataSource = self.dataSource
self.tableViewController.tableView.reloadData()
if let _ = self.view {
self.layoutFrames()
self.view.setNeedsDisplay()
}
}
func updateTable() -> Void {
guard let _ = self.textView.text else { return }
guard let validNumber = Int(self.textView.text!) else { return }
self.count = validNumber
}
func layoutFrames() -> Void {
if self.tableViewController.tableView != nil {
self.tableViewController.tableView.frame = CGRect(origin: CGPoint(x: self.view.frame.width / 2 - 100, y: 100), size: CGSize(width: 200, height: CGFloat(self.dataSource.count) * self.rowHeight))
NSLog("\(self.tableViewController.tableView.frame)")
}
if self.textView != nil {
self.textView.frame = CGRect(origin: CGPoint(x: 50, y: 100), size: CGSize(width: 100, height: 100))
}
if self.button != nil {
self.button.frame = CGRect(origin: CGPoint(x: 50, y: 150), size: CGSize(width: 100, height: 100))
}
}
}
class FlexibleTableViewController : UITableViewController {
var dataSource : [Int]!
override init(style: UITableViewStyle) {
super.init(style: style)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell()
cell.frame = CGRect(origin: CGPoint(x: 10, y: 5), size: CGSize(width: 180, height : 40))
cell.backgroundColor = UIColor.green
return cell
}
}
Whether it is a good idea or not, is, as has been pointed out, another question! Hope that helps!
Version from no_ripcord accounting for header and footer height
final // until proven otherwise
class IntrinsicallySizedTableView: UITableView {
override func layoutSubviews() {
super.layoutSubviews()
self.invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
guard let dataSource = self.dataSource else {
return super.intrinsicContentSize
}
var height: CGFloat = (tableHeaderView?.intrinsicContentSize.height ?? 0)
+ contentInset.top + contentInset.bottom
if let footer = tableFooterView {
height += footer.intrinsicContentSize.height
}
let nsections = dataSource.numberOfSections?(in: self) ?? self.numberOfSections
for section in 0..<nsections {
let sectionheader = rectForHeader(inSection: section)
height += sectionheader.height
let sectionfooter = rectForFooter(inSection: section)
height += sectionfooter.height
let nRowsSection = self.numberOfRows(inSection: section)
for row in 0..<nRowsSection {
height += self.rectForRow(at: IndexPath(row: row, section: section)).size.height
}
}
return CGSize(width: UIView.noIntrinsicMetric, height: height)
}
}