How can I change the UICollectionView cell height?
As you can see, the text just stops with "...". I want the size to get bigger when the label I bigger, I want the cell size to go below the label. How can I do this? Hope you understand my question.. Anyone please?
This answer does not change the cell size at all, but it could be a solution to your problem. In your cellForItemAtIndexPath after you set your cell: let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! CollectionViewCell
Try adding this: cell.imageText.adjustsFontSizeToFitWidth = true
This will make the font fit the label size, and the more text the smaller font size. Let me know how this worked for you.
You should make use of a delegate method called collectionView:layout:sizeForItemAtIndexPath from the UICollectionViewDelegateFlowLayout.
Now, the actual implementation will depend on your specific case (is it local content, if it is downloaded over the network, how many custom cells you have, etc.), but here's a copy of a very simple UICollectionViewController which works.
It makes use of a single custom cell of the type MyAwesomeCell which has a single label outlet called myAwesomeLabel.
I have not made any changes in the storyboard concerning Preferred Width, Content Compression Resistance Priority, etc. The only important change I have made is setting the lines property to 0 for the label, which means unlimited amount of lines.
PS, it's been written in Swift 2.
//
// ViewController.swift
// CollectionCell
//
// Created by Stefan Veis Pennerup on 21/09/15.
// Copyright © 2015 Kumuluzz. All rights reserved.
//
import UIKit
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
// MARK: - Constants
struct Constants {
static let ReuseIdentifierMyAwesomeCell = "myAwesomeCell"
}
// MARK: - Models
let myAwesomeCells = [
"Lorem ipsum dolor sit amet.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed massa leo, mollis id tortor at posuere.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque semper vitae mi vel hendrerit. Suspendisse et feugiat mi. Donec quis sollicitudin quam, non porttitor nulla. Phasellus in luctus lorem, sed auctor enim. Suspendisse potenti. Ut maximus pharetra diam, ac laoreet est dignissim eu nullam."
]
var cellSizes: [CGSize]!
// MARK: - Lifecycle methods
override func viewDidLoad() {
super.viewDidLoad()
// Gives the size array an initial value since collectionView:layout:sizeForItemAtIndexPath
// is called before collectionView:cellForItemAtIndexPath
cellSizes = myAwesomeCells.map({ _ in return CGSize(width: 200, height: 50) })
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Now that the collection view has appeared, then all the cells have been initialized
// with their appropiate content. The view should then be reloaded with the newly
// calculated sizes as well.
collectionView?.reloadData()
}
// MARK: - UICollectionViewDataSource
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
NSLog("\(self), collectionView:numberOfItemsInSection")
return myAwesomeCells.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
NSLog("\(self), collectionView:cellForItemAtIndexPath")
// Configures the cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(Constants.ReuseIdentifierMyAwesomeCell, forIndexPath: indexPath) as! MyAwesomeCell
cell.myAwesomeLabel.text = myAwesomeCells[indexPath.item]
// Calculates the height
cell.setNeedsLayout()
cell.layoutIfNeeded()
cellSizes[indexPath.item] = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
// Returns the new cell
return cell
}
// MARK: - UICollectionViewDelegate
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
NSLog("\(self), collectionView:layout:sizeForItemAtIndexPath")
return cellSizes[indexPath.item]
}
}
Try creating a fake Label with the same text, call sizetofit on it and then see the actual size of the label with full text.
Then, once you get the heigth, you can set it into your collectionViewLayout itemSize property.
I believe this way all your collectionview cell will be bigger but maybe it can be a starting point for a better solution...
Related
I've got a simple NSViewRepresentable that wraps an NSTextView.
struct TextView: NSViewRepresentable {
typealias NSViewType = NSTextView
var text: NSAttributedString
func makeNSView(context: Context) -> NSTextView {
let view = NSTextView()
// set background color to show view bounds
view.backgroundColor = NSColor.systemBlue
view.drawsBackground = true
view.isEditable = false
view.isSelectable = false
return view
}
func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.textStorage?.setAttributedString(text)
}
}
I want to center it vertically, so I'm using a VStack with Spacers above and below.
I want to leave left and right margins around it proportional to the window size, so I've wrapped it in a GeometryReader and frame().
struct ContentView: View {
func textView() -> some View {
let text = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod \
tempor incididunt ut labore et dolore magna aliqua.
"""
// Note: real application has more complex text than this, *not* using
// NSAttributedString isn't really an option, at least till SwiftUI has
// proper rich text support.
let size: CGFloat = 18
let font = NSFont(name: "LeagueSpartan-Bold", size: size)
let attrString = NSAttributedString(
string: text,
attributes: [ .foregroundColor: NSColor.systemYellow,
.font: font ?? NSFont.systemFont(ofSize: size) ])
return TextView(text: attrString)
}
var body: some View {
let textView: some View = self.textView()
return ZStack {
// use ZStack to provide window background color
Color(NSColor.systemTeal)
VStack {
Spacer()
GeometryReader { m2 in
textView.frame(width: m2.size.width / 1.618)
}
Spacer()
}
}
}
}
The horizontal frame works fine, but the vertical spacing isn't working at all:
(initial state)
And resizing the window produces shenanigans:
(resize from bottom)
(resize from bottom to top, then down again)
Oh, and the Preview is completely bananas:
If I replace my custom TextView with a SwiftUI native Text, the layout works fine, which suggests that the problem is in TextView. (Note also that the default window size is smaller.)
It seems likely that the size of the NSTextView isn't getting set properly. If I add nsView.sizeToFit() to updateNSView():
func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.textStorage?.setAttributedString(text)
nsView.sizeToFit()
}
this gets me the smaller default window size, and stops the text from bouncing to the bottom of the window when I resize up and down, but the text is still pinned near the top of the window, the preview is still broken, and resizing from the bottom still gets the NSTextView temporarily filling most of the height of the window.
Other things I've tried fiddling with: isVerticallyResizable, setContentCompressionResistancePriority, autoresizingMask and translatesAutoresizingMaskIntoConstraints, invalidateIntrinsicContentSize. None of these seem to make any obvious difference.
It seems like what I want is to update the NSTextView size when the containing SwiftUI views resize, but there doesn't seem to be any obvious way to do that, and I might anyway be wrong.
If you just need to show NSAttributedString, as I understood, then approach based on NSTextField, as shown below, is more appropriate, because NSTextView does not have default internal layout and requires explicit external frame.
Here is modified representable, ContentView does not require changes.
struct TextView: NSViewRepresentable {
typealias NSViewType = NSTextField
var text: NSAttributedString
func makeNSView(context: Context) -> NSTextField {
let view = NSTextField()
// set background color to show view bounds
view.backgroundColor = NSColor.systemBlue
view.drawsBackground = true
view.isEditable = false
view.isSelectable = false
view.lineBreakMode = .byWordWrapping
view.maximumNumberOfLines = 0
view.translatesAutoresizingMaskIntoConstraints = false
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.required, for: .vertical)
return view
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.attributedStringValue = text
}
}
How to achieve self sizing collectionViewCells using RxDataSource?
I've tried setting
flowLayout.estimatedItemSize = CGSize(width: 187, height: 102)
But then app crashes when dataSourceObservable changes.
I've tried setting cell size inside
dataSource.configureCell = { [weak self] (dataSource, collectionView, indexPath, _) in
Which is not a good idea, because cells overlap, and it is because I am not using
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
Now, is possible to layout cell sizes properly using only observables? That is not to call something like
dataSourceVar.value[indexPath].cellSize
Inside collectionView sizeForItemAt?
Faced a similar problem, solved in this way
// swiftlint:disable line_length type_name
final class RxCollectionViewSectionedReloadDataSourceAndDelegate<Section: SectionModelType>: RxCollectionViewSectionedReloadDataSource<Section>, UICollectionViewDelegateFlowLayout {
typealias CellSize = (CollectionViewSectionedDataSource<Section>, UICollectionView, UICollectionViewLayout, IndexPath, Item) -> CGSize
typealias SizeViewInSection = (CollectionViewSectionedDataSource<Section>, UICollectionView, UICollectionViewLayout, Int, Section) -> CGSize
private var cellSize: CellSize
private var headerSectionViewSize: SizeViewInSection?
private var footerSectionViewSize: SizeViewInSection?
init(
configureCell: #escaping ConfigureCell,
configureSupplementaryView: ConfigureSupplementaryView? = nil,
moveItem: #escaping MoveItem = { _, _, _ in () },
canMoveItemAtIndexPath: #escaping CanMoveItemAtIndexPath = { _, _ in false },
cellSize: #escaping CellSize,
headerSectionViewSize: SizeViewInSection? = nil,
footerSectionViewSize: SizeViewInSection? = nil
) {
self.cellSize = cellSize
self.headerSectionViewSize = headerSectionViewSize
self.footerSectionViewSize = footerSectionViewSize
super.init(
configureCell: configureCell,
configureSupplementaryView: configureSupplementaryView,
moveItem: moveItem,
canMoveItemAtIndexPath: canMoveItemAtIndexPath
)
}
override func collectionView(
_ collectionView: UICollectionView,
observedEvent: Event<RxCollectionViewSectionedReloadDataSource<Section>.Element>
) {
collectionView.delegate = self
super.collectionView(collectionView, observedEvent: observedEvent)
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
cellSize(self, collectionView, collectionViewLayout, indexPath, self[indexPath])
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int
) -> CGSize {
headerSectionViewSize?(self, collectionView, collectionViewLayout, section, sectionModels[section]) ?? .zero
}
func collectionView(
_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForFooterInSection section: Int
) -> CGSize {
footerSectionViewSize?(self, collectionView, collectionViewLayout, section, sectionModels[section]) ?? .zero
}
}
// swiftlint:enable line_length type_name
Add collection view to Storyboard. Import RxDataSources as a dependency.
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionLayout: UICollectionViewFlowLayout! {
didSet {
collectionLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
private let section = BehaviorRelay(
value: Section(items: [
Item(title: "Lorem ipsum dolor sit amet, consectetur"),
Item(title: "adipiscing elit, sed do eiusmod tempor"),
Item(title: "incididunt ut labore et dolore magna aliqua"),
Item(title: "Ut enim ad minim veniam"),
Item(title: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
Item(title: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
]
)
)
private lazy var collectionDatasource = {
return RxCollectionViewSectionedReloadDataSource<Section>(
configureCell: { (dataSource, collectionView, indexPath, item) -> UICollectionViewCell in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionCell
cell.titleLabel.text = item.title
cell.layer.borderWidth = 0.5
cell.layer.borderColor = UIColor.lightGray.cgColor
cell.maxWidth = collectionView.bounds.width - 16
return cell
})
}()
override func viewDidLoad() {
super.viewDidLoad()
initCollection()
}
private func initCollection() {
section
.asObservable()
.map({ return [$0] })
.bind(to: collectionView.rx.items(dataSource: collectionDatasource))
.disposed(by: disposeBag)
}
}
Data Model
import Foundation
struct Item {
let title: String
}
Create Section class subclassing SectionModelType
import RxDataSources
struct Section {
var items: [Item]
}
extension Section: SectionModelType {
init(
original: Section,
items: [Item]) {
self = original
self.items = items
}
}
Collection View Cell class
import UIKit
class CollectionCell: UICollectionViewCell {
#IBOutlet weak var titleLabel: UILabel!
// Note: must be strong
#IBOutlet private var maxWidthConstraint: NSLayoutConstraint! {
didSet {
maxWidthConstraint.isActive = false
}
}
var maxWidth: CGFloat? = nil {
didSet {
guard let maxWidth = maxWidth else {
return
}
maxWidthConstraint.isActive = true
maxWidthConstraint.constant = maxWidth
}
}
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
I have created a UICollectionView in which I have one prototype cell. I want 2 cells per row and running it on smaller screen gives me on one cell per row.
The distance between cells gets increased on bigger screen. I am talking only with respect to iPhones.
I think I have to set constrains programmatically by taking screen width and divide it by 2. I know how to take screen width and divide it by 2 but I don't know how to give width and heights to cell programmatically.
UICollectionViewDelegateFlowLayout defines a method for this purpose.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// return a CGSize
}
Example:
class MyViewController : UIViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.delegate = self
self.collectionView.dataSource = self
}
// ...
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: CGFloat(self.view.frame.size.width / 2), height: self.view.frame.size.height)
}
// ...
}
I'm having trouble loading the image in a default PFCollectionViewCell. I'm using the PFQueryCollectionViewController, and has set up the stuff properly - except for the imageFile.
The default PFCollectionViewCell has two fields: the PFImageView and the UILabel.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFCollectionViewCell? {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as? PFCollectionViewCell
if cell == nil {
cell = PFCollectionViewCell()
}
let nom = object as! Nom
cell?.imageView.file = nom.imageFile
println(nom.imageFile) //prints the object id
cell?.imageView.loadInBackground() //nothing actually loads
cell?.textLabel.text = nom.createdBy.name //textlabel is populated properly
return cell
}
I was able to populate the textField
I wasn't able to populate the imageView
What am I doing wrong?
Figured it out: You need to make sure you also put the placeholder image for it to load. You must set the image property of the PFImageView.
They should really document that...
Ok, first time I have ever had to ask for help on here, usually I can search and find my answer but not this time. I have a table that displays a pictures and names. If one of them are selected it goes to another view and the data passes. However, I am trying to get that passed information to display in a table like: Name: (Passed Info), Age: (Passed Info), Gender: (Passed Info) etc. I know the data passes because I can display the info in a label, but I can not figure out how to get it to show in a table. Index issue? String Issue?
This is code that passes the info:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var detailsViewController: DetailsViewController = segue.destinationViewController as DetailsViewController
var horseIndex = appsTableView!.indexPathForSelectedRow()!.row
var selectedHorse = self.horses[horseIndex]
detailsViewController.horse = selectedHorse
This is the code on the controller getting the data where I want the table to display
class DetailsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
#IBOutlet weak var titleLabel: UILabel!
var horse: Herd?
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.horse.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
cell.textLabel?.text = self.horse[indexPath.Row]
return cell
}
}
I get an error on the return self.horse.count stating not a member and an error on the self.horse[indexPath.Row] stating NSIndex does not have a member named row.
I feel like I am not unwrapping it properly or something, but I can not figure it out or find an answer in my searches. If you need more information please let me know and thanks in advance for any help.
CLARIFYING...
You correctly grab self.horses[horseIndex] in the segue method, so you've already done the work to get the 1 specific horse. No need to do it again. Is self.horses tied to the Herd type? Confusing why Herd shows up again in the DetailView - you don't seem to need it.
It sounds like what you actually want at this point is a tabular layout of the details of that single horse ... Correct? Like going from your entire "Contacts" list to the tabular view of a single contact?
That no longer involves the array of multiple horses, so your use of Herd? and herd.count aren't necessary. Use static labels or a static tableView to show the info from the 1 Horse.
WHAT'S YOUR DATA STRUCTURE & WHAT GOES IN DETAIL VIEW?
Presumably what you want to create (if you haven't already) is a Horse Type:
class Horse {
//you could use optional properties here, or require them for a fully initialized Horse.
let name:String?
let gender:String?
let breed:String?
var age:Int?
var restingHeartRate:Int?
init(name:String?, gender:String?, breed:String?, age:Int?, restingHeartRate:Int?) {
//set arguments passed in to respective properties of self...
}
func winTripleCrown() {
println("\(name!) Wins! Suck on that, Sea Biscuit!")
}
}
Ensure your horses array is declared to only take Horse instances, and fill it appropriately:
var horses = [Horse]()
let horse1 = Horse(name:"Bob" gender:"male" breed: "Yessir, I am a real horse" age:42 restingHeartRate: 30)
horses.append(horse1)
println(horses.count) //prints "1"
horses[0].winTripleCrown() //prints: "Bob Wins! Suck on that, Sea Biscuit!"
Use the Horse Type instead of Herd in the DetailViewController:
var horse: Horse? //nil until set in prepareForSegue
Then in the segue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
detailsViewController.horse = selectedHorse
Now, assuming you only put properly created Horse instances into the initial array, and there IS one at the selectedIndex, you're guaranteed that the horse variable in DetailViewController is a Horse instance, and that it's the one selected in the 1st "overall" tableView.
CREATE THE DETAIL VIEW
EASY:
The easy solution at this point is to create a detailView layout with labels, images, etc and hook them up to #properties in the DetailViewController. Then map the Horse properties to the #IBOutlets. Done. No need to mess w/tableViews anymore - just fake it or use a scrollView to make it look like a table.
TABLEVIEW ROUTE
But if you want to use a UITableView, then you'd need the delegate methods like you're using... The difference is you need to look at the # of properties of the single Horse instance - not anything about the overall list of Horses in a Herd or total array.
You COULD hard-code the # of rows if you're certain the # and order of Horse properties will always be consistent:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5 //1 row per property in a `Horse` instance as defined above
}
However, if you want to account for more dynamic results (like an array of wins/losses, or a photo gallery of variable length) you'd need to have enough cells to map all the Horse properties. How can you do that with a custom type that isn't an array or dictionary?
In Swift, you can determine how many properties a Type has by creating a "Mirror" type and using it for introspection, to reflect info back to you like the count property missing above.
let horseMirror = reflect(self.horse)
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.horseMirror.count //should also return "5" but no longer hard-coded
}
And then in cellForRowAtIndexPath: you can switch on the indexPath to assign the various Horse properties to the labels (or images, etc) in the tableView cells. Create custom tableView cell types w/unique identifiers if you want to show different information, or stick w/built-in options:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
//haven't played w/this to see if the optionals are correct. Consider it psuedocode...
switch indexPath.row {
case 1: cell.textLabel.text = self.horse.name?
case 2: cell.textLabel.text = self.horse.gender?
case 3: cell.textLabel.text = self.horse.breed?
case 4: cell.textLabel.text = String(self.horse.age?)
case 5: cell.textLabel.text = String(self.horse.restingHeartRate?)
default: cell.textLabel.text = ""
}
return cell
}