UITextView not displaying keyboard inside CollectionViewCell - ios

I am experiencing some difficulty in getting a UITextView to display a keyboard when tapped by the User. I am testing my app on a live device and not the simulator, so it is not the common "keyboard in simulator" issue a lot of developers experience.
I believe it might have something to do with the structure of the app.
I have a TableViewController with a cell that contains a CollectionViewController, and the UITextView is inside a CollectionViewCell.
I had this working in the past, but my fix does not appear to be working anymore. (I'm updating this app for the 1st time in 4 years), So I have stripped out all the code relating to other components in the cell and am now working with just this code (In order of "what should be called first):
TableViewCell for statuses:
This bit of code is inside tableView(cellForRowAtIndexPath)
case 5:
let cell = tableView.dequeueReusableCell(withIdentifier: "status") as! StatusTableViewCell
cell.collectionView.event = self.event
cell.collectionView.doLoad()
cell.collectionView.rootView = self
return cell
StatusTableViewCell
This contains the CollectionView which should display a list of "Statuses" (imagine if Facebook was horizontal)
class StatusTableViewCell: UITableViewCell {
var collectionView: StatusView
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
collectionView = StatusView(frame: .zero, collectionViewLayout: layout)
super.init(style: style, reuseIdentifier: reuseIdentifier)
doSetup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func doSetup(){
//backgroundColor = .black
addSubview(collectionView)
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0": collectionView]))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[v0]|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0": collectionView]))
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
StatusView
This is the bit of code that calls the NewStatusViewCell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("=====INDEX===== ", indexPath.row - 1)
print("number of statuses: ", statuses.count)
if(indexPath.row == 0){
print("creating newStatus")
let cell = dequeueReusableCell(withReuseIdentifier: "NewStatusCell", for: indexPath) as! NewStatusViewCell
// cell.event = self.event
return cell
}
NewStatusViewCell
This should always be called as the very first cell, and let's users post new statuses
class NewStatusViewCell: UICollectionViewCell, UITextViewDelegate {
let status = UITextView()
var event: Event? = nil
let placeholder = UILabel(frame: .zero)
let post = UIButton(type: .custom)
let tutorialView = UIView(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
status.frame = .zero
status.translatesAutoresizingMaskIntoConstraints = false
addSubview(status)
status.topAnchor.constraint(equalTo: topAnchor, constant: 10).isActive = true
status.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
status.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -50).isActive = true
status.rightAnchor.constraint(equalTo: rightAnchor, constant: -5).isActive = true
status.layer.borderWidth = 1.0
status.layer.borderColor = UIColor.lightGray.cgColor
status.isEditable = true
}
The original code included using this class as a delegate, but I have commented out all the relevant delegate code to try to get the textView to run with only apple's code, however before I did this I added a print() statement to the textViewShouldBeginEditing() function. This statement never appeared in my logs indicating it was never called
I know I've just layed out ALOT of code, but if anyone can see where I've gone wrong I would really appreciate any help you can offer.
As always please don't hesitate to ask for more information.
Thanks :)
EDIT
This is what the app looks like just now, the white box underneath "invitees" is the CollectionViewCell and the box inside that is the UITextView

Very common cause is adding subViews to the cell:
addSubview(status)
instead of to the cell's content view:
contentView.addSubview(status)
This applies to both UITableViewCell and UICollectionViewCell
Worth noting: for auto-layout, subViews should also be constrained to the contentView and not to the cell itself.

Related

How to detect long press on a UICollectionViewCell and and perform an action a the cell

I have a list of images displaying in a UICollectionViewCell. Now I want a way to display an overlay on the image when user long press on the cell. I have been able to place a long press gesture on the cell but unfortunately how to perform the overlay on the cell is where I'm struggling to achieve.
In my cellForItemAt I have this
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reUseMyCellID, for: indexPath) as! MyCollectionCell
cell.gestureRecognizers?.removeAll()
cell.tag = indexPath.row
let directFullPreviewer = UILongPressGestureRecognizer(target: MyCollectionCell(), action: #selector(MyCollectionCell().directFullPreviewLongPressAction))
cell.addGestureRecognizer(directFullPreviewer)
I have this function for the action on LongPressGestureRecognizer in my MyCollectionCell
class MyCollectionCell: UICollectionViewCell {
weak var textLabel: UILabel!
let movieImage: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.clipsToBounds = true
image.contentMode = .scaleAspectFill
image.layer.cornerRadius = 10
// image.image = UIImage(named: "105")
return image
}()
let movieOverlay: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .black.withAlphaComponent(0.7)
view.alpha = 0
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(movieImage)
movieImage.addSubview(btnRate)
movieImage.addSubview(movieOverlay)
NSLayoutConstraint.activate([
movieImage.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
movieImage.topAnchor.constraint(equalTo: contentView.topAnchor),
movieImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
movieImage.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
movieOverlay.leadingAnchor.constraint(equalTo: movieImage.leadingAnchor),
movieOverlay.topAnchor.constraint(equalTo: movieImage.topAnchor),
movieOverlay.trailingAnchor.constraint(equalTo: movieImage.trailingAnchor),
movieOverlay.bottomAnchor.constraint(equalTo: movieImage.bottomAnchor)
])
}
override func prepareForReuse() {
super.prepareForReuse()
movieImage.image = nil
}
func configure(with urlString: String){
movieImage.sd_setImage(with: URL(string: urlString), placeholderImage: UIImage(named: "ImagePlaceholder"))
}
#objc func directFullPreviewLongPressAction(g: UILongPressGestureRecognizer)
{
if g.state == UIGestureRecognizer.State.began
{
movieOverlay.alpha = 1
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Your current code has many issues when creating the long press gesture.
You are setting the target to a new cell instance that is immediately thrown away. Set the target as cell, not MyCollectionCell().
You are also using the wrong syntax for the selector. Don't attempt to create a new instance of a cell. Just pass the name of the method #selector(MyCollectionCell.directFullPreviewLongPressAction).
Having said all of that, there is no reason this code should be in the cellForItemAt method. You should be creating the long press gesture inside the cell class.
Remove these three lines from cellForItemAt:
cell.gestureRecognizers?.removeAll()
let directFullPreviewer = UILongPressGestureRecognizer(target: MyCollectionCell(), action: #selector(MyCollectionCell().directFullPreviewLongPressAction))
cell.addGestureRecognizer(directFullPreviewer)
Then add the following lines inside the init of MyCollectionCell:
let directFullPreviewer = UILongPressGestureRecognizer(target: self, action: #selector(directFullPreviewLongPressAction))
addGestureRecognizer(directFullPreviewer)
Now the cell is fully responsible for setting up and handling the long press gesture.
Unrelated to your question, you should know that the line:
cell.tag = indexPath.row
should be:
cell.tag = indexPath.item
row is used for UITableView. item is used for UICollectionView.
But besides that, you really should avoid such code. If your collection view allows cells to be inserted, deleted, and/or reordered, then a cell's tag will no longer represent the item you set.

Expandable custom UITableViewCell for iOS 11+

I realize that many people have asked this question in various forms and the answers are all over the page, so let me summarize my specific situation in hopes of getting more specific answers. First of all, I'm building for iOS 11+ and have a relatively recent version of XCode (11+). Maybe not the latest, but recent enough.
Basically, I need a self-sizing tableview where the cells may expand and collapse at runtime when the user interacts with them. In viewDidLoad I set the rowHeight to UITableView.automaticDimension and estimatedRowHeight to some number that's bigger than the canned value of 44. But the cell is not expanding like it should, even though I seem to have tried every bit of advice in the book.
If that matters, I have a custom class for the table cell but no .XIB file for it - the UI is defined directly in the prototype. I've tried a number of other variations, but it feels like the easiest is making a UIStackView the only direct child of the prototype (the "revenue" features so to speak would all be inside it. In my case, they include a label and another tableview - I nest 3 levels deep - but that's probably beside the point) and constraining all 4 of it's edges to the parent. I've tried that, and I've tinkered with the distribution in the stack view (Fill, Fill Evenly, Fill Proportionately), but none of it seems to work. What can I do to make the cells expand properly?
In case anyone's wondering, I used to override heightForRowAt but now I don't because it's not easy to predict the height at runtime and I'm hoping the process could be automated.
Start with the basics...
Here is a vertical UIStackView with two labels:
The red outline shows the frame of the stack view.
If we tap the button, it will set bottomLabel.isHidden = true:
Notice that in addition to being hidden, the stack view removes the space it was occupying.
Now, we can do that with a stack view in a table view cell to get expand/collapse functionality.
We'll start with every-other row expanded:
Now we tap the "Collapse" button for row 1 and we get:
Not quite what we want. We successfully "collapsed" the cell content, but the table view doesn't know anything about it.
So, we can add a closure... when we tap the button, the code in the cell will show/hide the bottom label AND it will use the closure to tell the table view what happened. Our cellForRowAt func looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! ExpColCell
c.setData("Top \(indexPath.row)", str2: "Bottom \(indexPath.row)\n2\n3\n4\n5", isCollapsed: isCollapsedArray[indexPath.row])
c.didChangeHeight = { [weak self] isCollapsed in
guard let self = self else { return }
// update our data source
self.isCollapsedArray[indexPath.row] = isCollapsed
// tell the tableView to re-run its layout
self.tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
and we get:
Here's a complete example:
Simple "dashed outline view"
class DashedOutlineView: UIView {
#IBInspectable var dashColor: UIColor = .red
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.lineDashPattern = [8,8]
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.strokeColor = dashColor.cgColor
shapeLayer.path = UIBezierPath(rect: bounds).cgPath
}
}
The cell class
class ExpColCell: UITableViewCell {
public var didChangeHeight: ((Bool) -> ())?
private let stack = UIStackView()
private let topLabel = UILabel()
private let botLabel = UILabel()
private let toggleButton = UIButton()
private let outlineView = DashedOutlineView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// button properties
toggleButton.translatesAutoresizingMaskIntoConstraints = false
toggleButton.backgroundColor = .systemBlue
toggleButton.setTitleColor(.white, for: .normal)
toggleButton.setTitleColor(.gray, for: .highlighted)
toggleButton.setTitle("Collapse", for: [])
// label properties
topLabel.text = "Top Label"
botLabel.text = "Bottom Label"
topLabel.font = .systemFont(ofSize: 32.0)
botLabel.font = .italicSystemFont(ofSize: 24.0)
topLabel.backgroundColor = .green
botLabel.backgroundColor = .systemTeal
botLabel.numberOfLines = 0
// outline view properties
outlineView.translatesAutoresizingMaskIntoConstraints = false
// stack view properties
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.spacing = 8
// add the labels
stack.addArrangedSubview(topLabel)
stack.addArrangedSubview(botLabel)
// add outlineView, stack view and button to contentView
contentView.addSubview(outlineView)
contentView.addSubview(stack)
contentView.addSubview(toggleButton)
// we'll use the margin guide
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
outlineView.topAnchor.constraint(equalTo: stack.topAnchor),
outlineView.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
outlineView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
outlineView.bottomAnchor.constraint(equalTo: stack.bottomAnchor),
toggleButton.topAnchor.constraint(equalTo: g.topAnchor),
toggleButton.trailingAnchor.constraint(equalTo: g.trailingAnchor),
toggleButton.leadingAnchor.constraint(equalTo: stack.trailingAnchor, constant: 16.0),
toggleButton.widthAnchor.constraint(equalToConstant: 92.0),
])
// we set the bottomAnchor constraint like this to avoid intermediary auto-layout warnings
let c = stack.bottomAnchor.constraint(equalTo: g.bottomAnchor)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// set label Hugging and Compression to prevent them from squeezing/stretching
topLabel.setContentHuggingPriority(.required, for: .vertical)
topLabel.setContentCompressionResistancePriority(.required, for: .vertical)
botLabel.setContentHuggingPriority(.required, for: .vertical)
botLabel.setContentCompressionResistancePriority(.required, for: .vertical)
contentView.clipsToBounds = true
toggleButton.addTarget(self, action: #selector(toggleButtonTapped), for: .touchUpInside)
}
func setData(_ str1: String, str2: String, isCollapsed: Bool) -> Void {
topLabel.text = str1
botLabel.text = str2
botLabel.isHidden = isCollapsed
updateButtonTitle()
}
func updateButtonTitle() -> Void {
let t = botLabel.isHidden ? "Expand" : "Collapse"
toggleButton.setTitle(t, for: [])
}
#objc func toggleButtonTapped() -> Void {
botLabel.isHidden.toggle()
updateButtonTitle()
// comment / un-comment this line to see the difference
didChangeHeight?(botLabel.isHidden)
}
}
and a table view controller to demonstrate
class ExpColTableViewController: UITableViewController {
var isCollapsedArray: [Bool] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(ExpColCell.self, forCellReuseIdentifier: "c")
// 16 "rows" start with every-other row collapsed
for i in 0..<15 {
isCollapsedArray.append(i % 2 == 0)
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isCollapsedArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! ExpColCell
c.setData("Top \(indexPath.row)", str2: "Bottom \(indexPath.row)\n2\n3\n4\n5", isCollapsed: isCollapsedArray[indexPath.row])
c.didChangeHeight = { [weak self] isCollapsed in
guard let self = self else { return }
// update our data source
self.isCollapsedArray[indexPath.row] = isCollapsed
// tell the tableView to re-run its layout
self.tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
}

Layout constraint not updating for UITableViewCell

The effect I'm tying to achieve is making it feel like the UITableViewCell "widens" when it is selected. I do this by adding a subview (let's call it visibleView) to the UITableViewCell's content view, and then I adjust mainView when the cell is selected.
However, visibleView's size doesn't change upon selection. Code below:
class feedTableCell: UITableViewCell {
var visibleCell: UIView!
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
self.visibleCell = UIView()
self.visibleCell.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.visibleCell)
visibleCell.widthAnchor.constraint(equalToConstant: 150).isActive = true
visibleCell.heightAnchor.constraint(equalToConstant: 100).isActive = true
visibleCell.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
visibleCell.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor).isActive = true
visibleCell.layer.cornerRadius = 15
visibleCell.backgroundColor = .white
}
override func setSelected(_ selected: Bool, animated: Bool) {
if selected {
self.visibleCell.widthAnchor.constraint(equalToConstant: 300).isActive = true
self.contentView.layoutIfNeeded()
} else {
self.visibleCell.widthAnchor.constraint(equalToConstant: 150).isActive = true
self.contentView.layoutIfNeeded()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
However, if I replace layout constraints with frames and instead update visibleCell by adjusting its frame, everything works fine.
You have to remove the previous width constraint before you add a new one. They are not exchanged. Hold a reference to the constraint to be able to remove it before you add the new constraint.

Self-sizing cells with UICollectionViewCompositionalLayout

I have a view controller that displays a collection view with self-sizing cells. The collection view has one section that scrolls horizontally. It looks like this:
Problem
The collection view behaves unexpectedly when the view controller is presented using the default pageSheet style on iOS 13+.
When pulling upward on the sheet, cells may appear to resize like the cell labeled "Rectify" below:
When pulling upward on the sheet, the content may shift horizontally. Sometimes, cells may disappear too:
Question
Is there a way to fix this behavior while still using UICollectionViewCompositionalLayout and the pageSheet presentation style?
Code Summary
The code is pretty straightforward. Just 3 classes, which can be dropped into the ViewController.swift file using the Single View App project template in Xcode.
A UICollectionViewCell class called Cell. The cell has a UILabel and overrides sizeThatFits(_:).
A UIViewController called ViewController used only to present BugViewController.
BugViewController, which configures the data source and presents the collection view. This is where the problem occurs.
Code
import UIKit
// MARK: - Cell -
final class Cell: UICollectionViewCell {
static let reuseIdentifier = "Cell"
lazy var label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.frame.size = contentView.bounds.size
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
contentView.backgroundColor = .tertiarySystemFill
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
.init(width: label.sizeThatFits(size).width + 32, height: 32)
}
}
// MARK: - ViewController -
final class ViewController: UIViewController {
private let button: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Tap Me!".uppercased(), for: .normal)
button.addTarget(self, action: #selector(presentBugViewController), for: .touchUpInside)
button.sizeToFit()
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
button.center = view.center
}
#objc func presentBugViewController() {
present(BugViewController(), animated: true)
}
}
// MARK: - BugViewController -
final class BugViewController: UIViewController {
private let models = [
"Better Call Saul",
"Mad Men",
"Rectify",
"Tiger King: Murder, Mayhem, and Madness",
"Master of None",
"BoJack Horseman"
]
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.contentInset.top = 44
collectionView.backgroundColor = .white
return collectionView
}()
private lazy var dataSource = UICollectionViewDiffableDataSource<Int, String>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { fatalError() }
cell.label.text = itemIdentifier
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
snapshot.appendSections([0])
snapshot.appendItems(models)
dataSource.apply(snapshot)
}
private func createCollectionViewLayout() -> UICollectionViewCompositionalLayout {
let layoutSize = NSCollectionLayoutSize.init(
widthDimension: .estimated(200),
heightDimension: .absolute(32)
)
let section = NSCollectionLayoutSection(group:
.horizontal(
layoutSize: layoutSize,
subitems: [.init(layoutSize: layoutSize)]
)
)
section.interGroupSpacing = 8
section.orthogonalScrollingBehavior = .continuous
return .init(section: section)
}
}
Notes
The collection view in my app actually has many sections and scrolls vertically. That is why I'm using a vertically scrolling collection view and a section with orthogonalScrollingBehavior in the example code.
Failed Attempts
I've tried using Auto Layout constraints instead of sizeThatFits(_:).
I've tried not using UICollectionViewDiffableDataSource.
Workarounds
Modifying the cell with a child scroll view and passing in an array of strings (as opposed to one at a time) does avoid this problem. But, it's a dirty hack that I'd like to avoid if possible.

didSet property value only appears in print()

INITIAL GOAL:
Have a view with a list of cells positioned vertically displaying some information. As soon as the user clicks on a cell to show a new view with more information.
THE ROAD SO FAR (curry on my wayward son!):
I created 2 view controllers: ViewController (subclassing UICollectionViewController, UICollectionViewDelegateFlowLayout) and DetailViewController (subclassing UIViewController).
I created a Cell that the ViewController uses to generate the collection view and a DetailView that the DetailViewController uses
I created a struct named Detail as a custom data type which provides storage of data using properties (ex. name, surname, address, etc.)
The struct:
struct Detail: Decodable {
let name: String?
let surname: String?
let address: String?
let description: String?
}
I use the following data for testing (after the testing is done I will get this data from an API call). I placed it inside ViewController:
let details: [Detail] = [Detail(name: "Chris", surname: "Doe", address: "Neverland 31", description: "This is a description about Chris Doe"), Detail(name: "Tony", surname: "Cross", address: "Galaxy Road 1", description: "This is a description about Tony Cross")]
To create the cells using the information above and the method:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
And also:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! Cell
As the method requires us to return a UICollectionViewCell, I first send the associated information to Cell by doing the following:
cell.details = details[indexPath.item]
return cell
Inside the Cell I created the following property using didSet to help me retrieve the information:
var details: Detail? {
didSet {
guard let details = details else { return }
guard let name = details.name else { return }
....
....
}
As you can understand using the information coming from ViewController I dynamically constructed each cell.
All were good at this point.
Then I tried to show a detailed view when clicking on a cell. To do this I followed the same practice inside the method:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let detailView = DetailView()
detailView.details = details[indexPath.item]
let detailViewController = DetailViewController()
detailViewController.modalTransitionStyle = .coverVertical
self.present(detailViewController, animated: true, completion: nil)
}
Again, in the DetailView I use the same approach to get the data associated with the selected item. This way I can have access to the data of the cell the user selects, as shown below:
import UIKit
class DetailView: UIView {
var dismissDetailViewAction: (() -> Void)?
var details: Detail? {
didSet {
// get details
guard let details = details else { return }
guard let name = details.name else { return }
guard let surname = details.surname else { return }
guard let address = details.address else { return }
guard let description = details.description else { return }
// print description and it shows in the console but not in the view
print(description)
let attributedTextDescription = NSMutableAttributedString(string: description, attributes: [NSAttributedStringKey.font: UIFont.FontBook.AvertaRegular.of(size: 20), NSAttributedStringKey.foregroundColor: UIColor.white])
briefDescription.attributedText = attributedTextDescription
briefDescription.textAlignment = .center
briefDescription.textColor = .white
briefDescription.numberOfLines = 0
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not yet been implemented")
}
fileprivate func setupView() {
setupDescriptionText()
setupCloseButton()
}
let briefDescription: UITextView = {
let text = UITextView()
text.textColor = .red
return text
}()
let closeButton: UIButton = {
let button = UIButton(title: "Close", font: UIFont.FontBook.AvertaRegular.of(size: 18), textColor: .white, cornerRadius: 5)
button.backgroundColor = .black
button.addTarget(self, action: #selector(closeDetailView), for: .touchUpInside)
return button
}()
fileprivate func setupDescriptionText() {
self.addSubview(briefDescription)
briefDescription.translatesAutoresizingMaskIntoConstraints = false
briefDescription.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5).isActive = true
briefDescription.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5).isActive = true
briefDescription.topAnchor.constraint(equalTo: self.topAnchor, constant: 10).isActive = true
briefDescription.heightAnchor.constraint(equalToConstant: 300).isActive = true
}
fileprivate func setupCloseButton() {
self.addSubview(closeButton)
closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
closeButton.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
closeButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 40).isActive = true
closeButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -40).isActive = true
closeButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
}
#objc func closeDetailView() {
dismissDetailViewAction?()
}
}
So, what I actually do is to design the static part of the view outside didSet, and what is dynamic part inside didSet. This works with the cells of collectionView.
I use the DetailViewController to display the DetailView and dismiss itself when the user clicks on the "Close" button.
import UIKit
class DetailViewController: UIViewController {
// reference DetailView view
var detailView: DetailView!
override func viewDidLoad() {
super.viewDidLoad()
// setup view elements
setupView()
}
fileprivate func setupView() {
let mainView = DetailView(frame: self.view.frame)
self.detailView = mainView
self.view.addSubview(detailView)
self.homeDetailView.dismissDetailViewAction = dismissDetailView
// pin view
self.detailView.translatesAutoresizingMaskIntoConstraints = false
self.detailView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.detailView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.detailView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.detailView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
fileprivate func dismissDetailView() {
// dismiss current (DetailViewController) controller
self.dismiss(animated: true, completion: nil)
}
}
The reason I did this is that I like to keep my ViewControllers as clean as possible (Massive View Controller, not my thing).
THE PROBLEM
The whole thing is built without any problem, but when I click on a cell to go to the DetailView no information is displayed.
THE WEIRD PART
Inside the DetailView --> didSet, when I use print(name), it works just fine (you see the correct name inside console). But when I try to use that value inside the view it will not be displayed.
And I know that my DetailView is just fine since if I use hardcoded values in it, it works (you see the correct result).
Any advise why this is not working properly?
PS: I am building the whole thing programmatically. No storyboards involved.
Thanks in advance and sorry for the lost post.
As was mentioned, your detailView is not referenced inside detailViewController. Instead, you create another instance of DetailView inside DetailViewController but this one has no Detail in it.
The console message was called from inside your detailView, but inside detailViewController is another instance that did not call this message, because its Detail is set to nil by default.
To be short, to fix that you should simply do the following changes:
import UIKit
class DetailViewController: UIViewController {
var detail: Detail!
private lazy var detailView: DetailView = {
let mainView = DetailView(frame: self.view.frame)
mainView.details = detail
return mainView
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
fileprivate func setupView() {
self.view.addSubview(detailView)
self.homeDetailView.dismissDetailViewAction = dismissDetailView
// pin view
self.detailView.translatesAutoresizingMaskIntoConstraints = false
self.detailView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.detailView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.detailView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.detailView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
fileprivate func dismissDetailView() {
// dismiss current (DetailViewController) controller
self.dismiss(animated: true, completion: nil)
}
}
And inside your collectionView(...) func:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let detailViewController = DetailViewController()
detailViewController.detail = details[indexPath.item]
detailViewController.modalTransitionStyle = .coverVertical
self.present(detailViewController, animated: true, completion: nil)
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let detailView = DetailView()
detailView.details = details[indexPath.item]
let detailViewController = DetailViewController()
detailViewController.modalTransitionStyle = .coverVertical
self.present(detailViewController, animated: true, completion: nil)
}
You make a DetailView here, pass it your details... and then do nothing with it.
Normally DetailView would be a property of the DetailViewController and you'd pass the details to the view controller, which would display it.
What's happening here is that you're creating, configuring and throwing away a DetailView, when you probably should be using the one that DetailViewController owns, or should own.

Resources