Dynamicly insert Views into custom cell with Dynamic Height - ios

Hello Guys i spend few days figuring out how to solve my problem and i am not much skilled in swift i decided ask some profesionals
My Problem:
DATA: array list of events(apointment, task, etc...)
number of events its not always same thats why i have to insert as many views as events in array and height of each cell is always different
custom cell created with xib file
I am inserting views inside cell ( creating column of apointments and tasks) but i have a problem when scrolling everything start to look really bad. Can someone help me and told my why it look like broken lego when scrolling ?
I want to make something like this
I tried add label to left side of my column of views and it did not work. cell height was small and content did not appear because was hidden under next row. Cell height was only height of that one lable. It ignores constrains of last view and only notice constrain of that label
ViewController
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var data = [PLCalendarDay]()
var tableView : UITableView!
let days = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]
override func viewDidLoad() {
super.viewDidLoad()
for (var i = 0; i<5; i++) {
var boleen = true
if i > 2 {boleen = false}
let calendar = NSCalendar.currentCalendar()
let day = calendar.dateByAddingUnit(.Day, value: i, toDate: NSDate(), options: [])
print("robim pole")
self.data.append(PLCalendarDay(day: day!, withEnd: boleen))
}
tableView = UITableView()
tableView.registerNib(UINib(nibName: "PLCalendarCell", bundle: nil), forCellReuseIdentifier: "PLCalendarCellid")
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 200
tableView.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height)
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .None
self.view.addSubview(tableView!)
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(animated: Bool) {
// self.tableView.reloadSections(NSIndexSet(indexesInRange: NSMakeRange(0, self.tableView.numberOfSections)), withRowAnimation: .None)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("davam cell number")
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("davam cell")
let cell = tableView.dequeueReusableCellWithIdentifier("PLCalendarCellid", forIndexPath: indexPath) as! PLCalendarCell
cell.setupCell(data[indexPath.row].events)
//cell.selectionStyle = .None
// cell.day.text = data[indexPath.row].date.dateStringWithFormat("dd")
// let day = data[indexPath.row].date.dateStringWithFormat("dd-MM-yyyy")
// cell.dayWord.text = days[getDayOfWeek(day)!-1]
print("som awake1 1 1 ")
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// cell selected code here
}
func getDayOfWeek(today:String)->Int? {
let formatter = NSDateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
if let todayDate = formatter.dateFromString(today) {
let myCalendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let myComponents = myCalendar.components(.Weekday, fromDate: todayDate)
let weekDay = myComponents.weekday
return weekDay
} else {
return nil
}
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 200
}
}
cell file
class PLCalendarCell: UITableViewCell {
#IBOutlet weak var day: UILabel!
#IBOutlet weak var dayWord: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
print("som awake")
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupCell (events: [PLCalendarEvent]){
let cellWidht = self.contentView.frame.width
var positionY:CGFloat = 10.0
var lastView: UIView? = nil
for event in events {
if event.end != nil {
let view = PLCalendarCellView(frame: CGRectMake(70, positionY, cellWidht, 50.0), time: true)
view.title.text = event.desc
view.time.text = "\(event.start.dateStringWithFormat("dd-MM-yyyy")) - \(event.end!.dateStringWithFormat("dd-MM-yyyy"))"
view.backgroundColor = UIColor.greenColor()
view.layer.cornerRadius = 4
self.addSubview(view)
if lastView == nil {
let constrain = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1, constant: 10)
self.addConstraint(constrain)
} else {
let constrain = NSLayoutConstraint(item: lastView!, attribute: NSLayoutAttribute.BottomMargin, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 10)
self.addConstraint(constrain)
}
lastView = view
positionY += 60.0
}
else {
let view = PLCalendarCellView(frame: CGRectMake(70, positionY, cellWidht, 30.0), time: false)
view.title.text = event.desc
view.backgroundColor = UIColor.greenColor()
view.layer.cornerRadius = 4
self.addSubview(view)
if lastView == nil {
let constrain = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.TopMargin, multiplier: 1, constant: 10)
self.addConstraint(constrain)
} else {
let constrain = NSLayoutConstraint(item: lastView!, attribute: NSLayoutAttribute.BottomMargin, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 10)
self.addConstraint(constrain)
}
lastView = view
positionY += 40.0
}
}
// eventHolderView.frame = CGRectMake(0, 0, cellWidht, positionY)
// let constrain = NSLayoutConstraint(item: self.contentView, attribute: NSLayoutAttribute.BottomMargin, relatedBy: NSLayoutRelation.Equal, toItem: lastView, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 10)
// self.addConstraint(constrain)
let constrain = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.BottomMargin, relatedBy: NSLayoutRelation.Equal, toItem: lastView, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 10)
self.addConstraint(constrain)
}
}
Callendar day
class PLCalendarDay: NSObject {
let date: NSDate!
var events = [PLCalendarEvent]()
init(day: NSDate, withEnd: Bool) {
self.date = NSCalendar.currentCalendar().startOfDayForDate(day)
if withEnd {
for(var i=0; i<5;i++){
events.append(PLCalendarEvent(description: "Only one day", startDate: NSCalendar.currentCalendar().startOfDayForDate(date)))
}
} else {
for(var i=0; i<5;i++){
events.append(PLCalendarEvent(description: "Only one day", startDate: NSCalendar.currentCalendar().startOfDayForDate(date), endDate: date))
}
}
}
}
Callendar event
class PLCalendarEvent : NSObject{
let desc: String
let start: NSDate
var end: NSDate? = nil
init(description: String, startDate: NSDate) {
self.desc = description
self.start = startDate
}
init(description: String, startDate: NSDate, endDate: NSDate) {
self.desc = description
self.start = startDate
self.end = endDate
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
i really cant achieve any good result i will be really thankful for help

I resolve the problem. All i needed was proper constraints but not only vertical but even horizontal.

Related

Show activity indicator in UICollectionView Footer

How to show and hide activity indicator in collection-view footer. I need to add a activity indicator in footer area of collectionview.
Sample for you:
import UIKit
open class PagingTableView: UITableView {
private var loadingView: UIView!
private var indicator: UIActivityIndicatorView!
internal var page: Int = 0
internal var previousItemCount: Int = 0
open var currentPage: Int {
get {
return page
}
}
open weak var pagingDelegate: PagingTableViewDelegate? {
didSet {
pagingDelegate?.paginate(self, to: page)
}
}
open var isLoading: Bool = false {
didSet {
isLoading ? showLoading() : hideLoading()
}
}
open func reset() {
page = 0
previousItemCount = 0
pagingDelegate?.paginate(self, to: page)
}
private func paginate(_ tableView: PagingTableView, forIndexAt indexPath: IndexPath) {
let itemCount = tableView.dataSource?.tableView(tableView, numberOfRowsInSection: indexPath.section) ?? 0
guard indexPath.row == itemCount - 1 else { return }
guard previousItemCount != itemCount else { return }
page += 1
previousItemCount = itemCount
pagingDelegate?.paginate(self, to: page)
}
private func showLoading() {
if loadingView == nil {
createLoadingView()
}
tableFooterView = loadingView
}
private func hideLoading() {
reloadData()
pagingDelegate?.didPaginate?(self, to: page)
tableFooterView = nil
}
private func createLoadingView() {
loadingView = UIView(frame: CGRect(x: 0, y: 0, width: frame.width, height: 50))
indicator = UIActivityIndicatorView()
indicator.color = UIColor.lightGray
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.startAnimating()
loadingView.addSubview(indicator)
centerIndicator()
tableFooterView = loadingView
}
private func centerIndicator() {
let xCenterConstraint = NSLayoutConstraint(
item: loadingView, attribute: .centerX, relatedBy: .equal,
toItem: indicator, attribute: .centerX, multiplier: 1, constant: 0
)
loadingView.addConstraint(xCenterConstraint)
let yCenterConstraint = NSLayoutConstraint(
item: loadingView, attribute: .centerY, relatedBy: .equal,
toItem: indicator, attribute: .centerY, multiplier: 1, constant: 0
)
loadingView.addConstraint(yCenterConstraint)
}
override open func dequeueReusableCell(withIdentifier identifier: String, for indexPath: IndexPath) -> UITableViewCell {
paginate(self, forIndexAt: indexPath)
return super.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
}
}

Dynamic cell height with SDWebImage

I have a table view and table view cells with an image view on them. I want them to have a fixed width but with a dynamic height (depending on the image coming in from the server).
I am using SDWebImage to download and set the image, but the table view cells are turning out very weird.
I didn't forget to:
postTableView.estimatedRowHeight = UITableViewAutomaticDimension
postTableView.rowHeight = UITableViewAutomaticDimension
cellForRow method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! TableViewCell
let post = postArray[indexPath.row]
cell.setupCell(with: post)
return cell
}
Table view cell class:
class TableViewCell: UITableViewCell {
#IBOutlet weak var postTitle: UILabel!
#IBOutlet weak var postSource: UILabel!
#IBOutlet weak var postChart: UIImageView!
internal var aspectConstraint: NSLayoutConstraint? {
didSet {
if oldValue != nil {
postChart.removeConstraint(oldValue!)
}
if aspectConstraint != nil {
postChart.addConstraint(aspectConstraint!)
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
selectionStyle = UITableViewCellSelectionStyle.none
}
override func prepareForReuse() {
super.prepareForReuse()
postChart.image = nil
aspectConstraint = nil
}
func setupCell(with post: Post) {
postTitle.text = post.title
postSource.text = post.source
let tempImageView = UIImageView()
tempImageView.sd_setImage(with: URL(string: post.chartURL!), placeholderImage: UIImage(named: "placeholder.png")) { (image, error, cache, url) in
if let image = image {
self.setCustomImage(image: image)
}
}
}
func setCustomImage(image: UIImage) {
let aspect = image.size.width / image.size.height
let constraint = NSLayoutConstraint(item: postChart, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: postChart, attribute: NSLayoutAttribute.height, multiplier: aspect, constant: 0.0)
constraint.priority = UILayoutPriority(rawValue: 999)
aspectConstraint = constraint
postChart.image = image
setNeedsLayout()
}
}
You just need to tell the whole tableView to refresh its view. Use this code in the view controller holding the tableView:
func refreshTableView() {
self.tableView.beginUpdates()
self.tableView.setNeedsDisplay()
self.tableView.endUpdates()
}
Using a delegate pattern tell the viewController to refresh the tableView, so something like:
func setCustomImage(image: UIImage) {
let aspect = image.size.width / image.size.height
let constraint = NSLayoutConstraint(item: postChart, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: postChart, attribute: NSLayoutAttribute.height, multiplier: aspect, constant: 0.0)
constraint.priority = UILayoutPriority(rawValue: 999)
aspectConstraint = constraint
postChart.image = image
// call this to refresh table
delegate.refreshTableView()
}
Post class:
class Post {
var title : String?
var source : String?
var chartURL : String?
var postChart: UIImage?
var category : String?
var rank : CGFloat?
var imageSize: CGSize?
init(data: NSDictionary) {
title = data["title"] as? String
source = data["source"]as? String
chartURL = data["chartURL"] as? String
postChart = data["postChart"] as? UIImage
category = data["category"] as? String
rank = data["rank"] as? CGFloat
}
}

While typing on keyboard button tap doesn't get recognized

I've got a problem with my iOS application. I want to implement something like a MessagingViewController where you got a UITextView and Button (sendButton) below an UITableView. If a tap the textView, the keyboard appears, the View goes up.. so far so good.
But, if I am entering random text and tab/press the send button (independent of what should happen now) while typing or if the time gap between typing and Button tab is to small, the tap doesn't get recognized. If you try iMessage or WhatsApp this isn't a problem.
I don't know what to do, I also tried CocoaPods like SlackViewController or InputAccessoryView but it is always the same problem. While typing, the button tap doesn't get recognized. I tried it with the normal IBAction of a UIButton and UITapGestureRecognizer.
I hope somebody can help me, this problem makes the UX horrible.
Thanks a lot!!!
Edit: Here's an example where the Button is in an InputAccessoryView.
import UIKit
class MessagingViewController: UIViewController, UITableViewDataSource {
var messages: [String] = ["12", "32"]
var accessory: UIView!
var cancelButton: UIButton!
var charactersLeftLabel: UILabel!
var sendButton: UIButton!
#IBOutlet var tableView: UITableView!
#IBOutlet var messagesTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.register(UINib(nibName: TableViewCell.className, bundle:nil),
forCellReuseIdentifier: TableViewCell.className)
addAccessoryView()
NotificationCenter.default.addObserver(self, selector: #selector(MessagingViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(MessagingViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell
cell?.titleLabel.text = "Sender: \(indexPath.row): "
cell?.detailLabel.text = messages[indexPath.row]
return cell!
}
func addAccessoryView() {
let frame = CGRect(x:0, y:0, width: self.view.frame.width,height: 45)
accessory = UIView(frame: frame)
accessory.backgroundColor = UIColor.lightGray
accessory.alpha = 0.6
accessory.translatesAutoresizingMaskIntoConstraints = false
self.messagesTextView.inputAccessoryView = accessory
sendButton = UIButton(type: .custom)
sendButton.setTitleColor(UIColor.red, for: .normal)
sendButton.setTitle("Send", for: .normal)
sendButton.setTitleColor(UIColor.white, for: .disabled)
sendButton.addTarget(self, action: #selector(MessagingViewController.sendButtonTapped(_:)), for: .touchUpInside)
sendButton.showsTouchWhenHighlighted = true
sendButton.isEnabled = true
sendButton.translatesAutoresizingMaskIntoConstraints = false
accessory.addSubview(sendButton)
let sendTrailingConstraint = NSLayoutConstraint(item: sendButton, attribute: .trailing, relatedBy: .equal, toItem: accessory, attribute: .trailing, multiplier: 1.0, constant: -20)
accessory.addConstraint(sendTrailingConstraint)
let sendCenterYConstraint = NSLayoutConstraint(item: sendButton, attribute: .centerY, relatedBy: .equal, toItem: accessory, attribute: .centerY, multiplier: 1.0, constant: 0)
accessory.addConstraint(sendCenterYConstraint)
}
func sendButtonTapped(_ sender: UIButton) {
messages.append(messagesTextView.text)
messagesTextView.text.removeAll()
tableView.reloadData()
tableView.scrollToRow(at: IndexPath(row: messages.count - 1, section: 0), at: .bottom, animated: true)
}
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y == 0{
self.view.frame.origin.y -= keyboardSize.height
}
}
}
func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y != 0{
self.view.frame.origin.y += keyboardSize.height
}
}
}
}
I just tested on my project, it's works without any problem, add your code to see what you did. (i can't comment so i put this as an answer)

UIInputViewController NSAutoresizingMaskLayoutConstraint issue

I keep trying to implement input accessory VC in my app and I faced with the following issue. When I'm trying to modify the height of the root view of my custom UIInputViewController it's working well despite the one problem. The problem is that in the logs I see the following:
[LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
<NSAutoresizingMaskLayoutConstraint:0x174490cc0 UIInputView:0x10a80bb80.(null) == 46.5>,
<NSAutoresizingMaskLayoutConstraint:0x17448cd50 UIInputView:0x10a80bb80.height == 76>,
<NSLayoutConstraint:0x17429b710 UIInputView:0x10a80bb80.top == UIInputSetHostView:0x10402e940.top>
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x17429b710 UIInputView:0x10a80bb80.top == UIInputSetHostView:0x10402e940.top>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
Code of my custom UIInputViewController:
import UIKit
import RxSwift
import RxCocoa
#objc protocol InputViewControllerDelegate: NSObjectProtocol {
func answerTextViewDidChange(_ textView: UITextView)
}
class InputViewController: UIInputViewController {
fileprivate var closeButton: UIButton?
private var separatorView: UIView?
private var tipLabel: UILabel?
private var answerTextView: ConstrainedTextView?
private var buttonHeightConstraint: NSLayoutConstraint?
private var separatorHeightConstraint: NSLayoutConstraint?
private var answerTextViewBottomConstraint: NSLayoutConstraint?
weak var delegate: InputViewControllerDelegate?
private let junk = DisposeBag()
private var appropriateMaxLines: Int {
let isPortrait = UIDevice.current.orientation.isPortrait
return isPortrait ? 5 : 3
}
var answerText: String {
get {
return answerTextView?.text ?? ""
}
set {
answerTextView?.text = newValue
}
}
var isAnswerTextViewFirstResponder: Bool {
get {
return answerTextView?.isFirstResponder ?? false
}
set {
_ = newValue ? answerTextView?.becomeFirstResponder() : answerTextView?.resignFirstResponder()
}
}
// MARK: - Life Cycle
deinit {
NotificationCenter.default.removeObserver(self)
}
override func loadView() {
super.loadView()
let notifName = Notification.Name.UIDeviceOrientationDidChange
NotificationCenter.default.addObserver(self,
selector: #selector(rotated),
name: notifName,
object: nil)
configureView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
answerTextView?.maxLines = appropriateMaxLines
setCloseButtonDisabledIfNeeded()
}
// MARK: - Layout
private func configureView() {
view.backgroundColor = RGB(0xF6F6F6)
view.frame = CGRect(x: 0, y: 0, width: screenWidth, height: 70)
view.autoresizingMask = [.flexibleWidth]
// Separator
separatorView = UIView()
separatorView?.backgroundColor = UIColor.lightGray
separatorView?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(separatorView!)
AutoLayoutEqualizeSuper(separatorView, .left, 0)
AutoLayoutEqualizeSuper(separatorView, .right, 0)
AutoLayoutEqualizeSuper(separatorView, .top, 0)
separatorHeightConstraint = AutoLayoutSetAttribute(separatorView, .height, 1)
// Close Button
closeButton = UIButton(type: .system)
closeButton?.setTitle("Hide", for: .normal)
closeButton?.titleLabel?.font = UIFont.systemFont(ofSize: 17)
closeButton?.translatesAutoresizingMaskIntoConstraints = false
closeButton?.addTarget(self, action: #selector(dismissKeyboard), for: .touchUpInside)
view.addSubview(closeButton!)
AutoLayoutSetAttribute(closeButton, .width, 70)
buttonHeightConstraint = AutoLayoutSetAttribute(closeButton, .height, 35)
AutoLayoutEqualizeSuper(closeButton, .right, -5)
view.addConstraint(NSLayoutConstraint(item: closeButton!, attribute: .top, relatedBy: .equal, toItem: separatorView, attribute: .bottom, multiplier: 1, constant: 0))
// Tip Label
tipLabel = UILabel()
tipLabel?.textColor = UIColor.darkGray
tipLabel?.text = "Your answer:"
tipLabel?.font = UIFont.systemFont(ofSize: 17)
tipLabel?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tipLabel!)
AutoLayoutEqualizeSuper(tipLabel, .left, 5)
AutoLayoutSetAttribute(tipLabel, .height, 35)
view.addConstraint(NSLayoutConstraint(item: tipLabel!, attribute: .right, relatedBy: .equal, toItem: closeButton, attribute: .left, multiplier: 1, constant: 0))
// Text View
answerTextView = ConstrainedTextView()
answerTextView?.backgroundColor = UIColor.white
answerTextView?.delegate = self
answerTextView?.scrollsToTop = false
answerTextView?.showsVerticalScrollIndicator = false
answerTextView?.font = REG_FONT(15)
answerTextView?.translatesAutoresizingMaskIntoConstraints = false
answerTextView?.layer.masksToBounds = true
answerTextView?.layer.cornerRadius = 7
answerTextView?.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.7).cgColor
answerTextView?.layer.borderWidth = 1
view.addSubview(answerTextView!)
AutoLayoutEqualizeSuper(answerTextView, .left, 5)
AutoLayoutEqualizeSuper(answerTextView, .right, -5)
answerTextViewBottomConstraint = AutoLayoutEqualizeSuper(answerTextView, .bottom, -5)
answerTextView?
.rx
.observe(CGRect.self, "bounds")
.distinctUntilChanged {
$0?.height == $1?.height
}
.subscribe(onNext: { [unowned self] newBounds in
if var newHeight = newBounds?.height,
let separatorHeight = self.separatorHeightConstraint?.constant,
let buttonHeight = self.buttonHeightConstraint?.constant,
let bottomSpace = self.answerTextViewBottomConstraint?.constant {
newHeight = newHeight < 35 ? 35 : newHeight
let generalHeight = newHeight + separatorHeight + buttonHeight + abs(bottomSpace)
var frame = self.view.frame
frame.size.height = generalHeight
self.view.frame = frame
}
})
.addDisposableTo(junk)
}
// MARK: - Other methods
fileprivate func setCloseButtonDisabledIfNeeded() {
closeButton?.isEnabled = answerTextView?.isFirstResponder ?? false
}
func rotated() {
answerTextView?.maxLines = appropriateMaxLines
}
}
// MARK: - UITextViewDelegate Protocol Conformance
extension InputViewController: UITextViewDelegate {
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
textView.inputAccessoryView = view
return true
}
func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
textView.inputAccessoryView = nil
return true
}
func textViewDidBeginEditing(_ textView: UITextView) {
setCloseButtonDisabledIfNeeded()
}
func textViewDidEndEditing(_ textView: UITextView) {
setCloseButtonDisabledIfNeeded()
}
func textViewDidChange(_ textView: UITextView) {
delegate?.answerTextViewDidChange(textView)
}
}

UITableView with variable cell height: Working in IB but not programmatically

TL;DR
My programmatically created table view cells are not resizing according to the intrinsic content height of their custom views, even though I am using UITableViewAutomaticDimension and setting both the top and bottom constraints.
The problem probably lies in my implementation of the UITableViewCell subclass. See the code below under Doesn't work programmatically > Code > MyCustomCell.swift.
Goal
I'm trying to make a suggestion bar for a custom Mongolian keyboard. Mongolian is written vertically. In Android it looks like this:
Progress
I've learned that I should use a UITableView with variable cell heights, which is available starting with iOS 8. This requires using auto layout and telling the table view to use automatic dimensions for the cell heights.
Some things I've had to learn along the way are represented in my recent SO questions and answers:
How to make a custom table view cell
Getting variable height to work with in a table view with a standard UILabel
Getting intrinsic content size to work for a custom view
Using a programmatically created UITableViewCell
Set constraints programmatically
So I have come to the point where I have the vertical labels that support intrinsic content size. These labels go in my custom table view cells. And as described in the next section, they work when I do it in the storyboard, but not when I create everything programmatically.
Works in IB
In order to isolate the problem I created two basic projects: one for where I use the storyboard and one where I do everything programmatically. The storyboard project works. As can be seen in the following image, each table view cell resizes to match the height of custom vertical label.
In IB
I set constraints to pin the top and bottom as well as centering the label.
Code
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
import UIKit
class MyCustomCell: UITableViewCell {
#IBOutlet weak var myCellLabel: UIMongolSingleLineLabel!
}
Doesn't work programmatically
Since I want the suggestion bar to be a part of the final keyboard, I need to be able to create it programmatically. However, when I try to recreate the above example project programmatically, it isn't working. I get the following result.
The cell heights are not resizing and the custom vertical labels are overlapping each other.
I also get the following error:
Warning once only: Detected a case where constraints ambiguously
suggest a height of zero for a tableview cell's content view. We're
considering the collapse unintentional and using standard height
instead.
This error has been brought up before multiple times on Stack Overflow:
iOS8 - constraints ambiguously suggest a height of zero
Detected a case where constraints ambiguously suggest a height of zero
custom UITableviewcell height not set correctly
ios 8 (UITableViewCell) : Constraints ambiguously suggest a height of zero for a tableview cell's content view
However, the problem for most of those people is that they were not setting both a top and bottom pin constraint. I am, or at least I think I am, as is shown in my code below.
Code
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
var tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
// Suggestion bar
tableView.frame = CGRect(x: 0, y: 20, width: view.bounds.width, height: view.bounds.height)
tableView.registerClass(MyCustomCell.self, forCellReuseIdentifier: cellReuseIdentifier)
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
view.addSubview(tableView)
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
I think the problem is probably in here since this is the main difference from the IB project.
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
Supplemental Code
I'll also include the code for the custom vertical label that I used in both projects above, but since the IB project works, I don't think the main problem is here.
import UIKit
#IBDesignable
class UIMongolSingleLineLabel: UIView {
private let textLayer = LabelTextLayer()
var useMirroredFont = false
// MARK: Primary input value
#IBInspectable var text: String = "A" {
didSet {
textLayer.displayString = text
updateTextLayerFrame()
}
}
#IBInspectable var fontSize: CGFloat = 17 {
didSet {
updateTextLayerFrame()
}
}
#IBInspectable var centerText: Bool = true {
didSet {
updateTextLayerFrame()
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
// Text layer
textLayer.backgroundColor = UIColor.yellowColor().CGColor
textLayer.useMirroredFont = useMirroredFont
textLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(textLayer)
}
override func intrinsicContentSize() -> CGSize {
return textLayer.frame.size
}
func updateTextLayerFrame() {
let myAttribute = [ NSFontAttributeName: UIFont.systemFontOfSize(fontSize) ]
let attrString = NSMutableAttributedString(string: textLayer.displayString, attributes: myAttribute )
let size = dimensionsForAttributedString(attrString)
// This is the frame for the soon-to-be rotated layer
var x: CGFloat = 0
var y: CGFloat = 0
if layer.bounds.width > size.height {
x = (layer.bounds.width - size.height) / 2
}
if centerText {
y = (layer.bounds.height - size.width) / 2
}
textLayer.frame = CGRect(x: x, y: y, width: size.height, height: size.width)
textLayer.string = attrString
invalidateIntrinsicContentSize()
}
func dimensionsForAttributedString(attrString: NSAttributedString) -> CGSize {
var ascent: CGFloat = 0
var descent: CGFloat = 0
var width: CGFloat = 0
let line: CTLineRef = CTLineCreateWithAttributedString(attrString)
width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, nil))
// make width an even integer for better graphics rendering
width = ceil(width)
if Int(width)%2 == 1 {
width += 1.0
}
return CGSize(width: width, height: ceil(ascent+descent))
}
}
// MARK: - Key Text Layer Class
class LabelTextLayer: CATextLayer {
// set this to false if not using a mirrored font
var useMirroredFont = true
var displayString = ""
override func drawInContext(ctx: CGContext) {
// A frame is passed in, in which the frame size is already rotated at the center but the content is not.
CGContextSaveGState(ctx)
if useMirroredFont {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextScaleCTM(ctx, 1.0, -1.0)
} else {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextTranslateCTM(ctx, 0, -self.bounds.width)
}
super.drawInContext(ctx)
CGContextRestoreGState(ctx)
}
}
Update
The entire code for the project is all here, so if anyone is interested enough to try it out, just make a new project and cut and paste the code above into the following three files:
ViewController.swift
MyCustomCell.swift
UIMongolSingleLineLabel.swift
The error is pretty trivial:
Instead of
self.addSubview(myCellLabel)
use
self.contentView.addSubview(myCellLabel)
Also, I would replace
// pin top
NSLayoutConstraint(...).active = true
// pin bottom
NSLayoutConstraint(...).active = true
// center horizontal
NSLayoutConstraint(...).active = true
with
let topConstraint = NSLayoutConstraint(...)
let bottomConstraint = NSLayoutConstraint(...)
let centerConstraint = NSLayoutConstraint(...)
self.contentView.addConstraints([topConstraint, bottomConstraint, centerConstraint])
which is more explicit (you have to specify the constraint owner) and thus safer.
The problem is that when calling active = true on a constraint, the layout system has to decide to which view it should add the constraints. In your case, because the first common ancestor of contentView and myCellLabel is your UITableViewCell, they were added to your UITableViewCell, so they were not actually constraining the contentView (constraints were between siblings not between superview-subview).
Your code actually triggered a console warning:
Warning once only: Detected a case where constraints ambiguously suggest a height of zero for a tableview cell's content view. We're considering the collapse unintentional and using standard height instead.
Which made me to look immediately at the way the constraints are created for your label.
I have tested your code and found the issue was in setting constraints please use below code part for setting constants in your "MyCustomCell.swift" file setup() function
let topConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0)
let centerConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1, constant: 0)
self.addConstraints([centerConstraint, topConstraint, bottomConstraint])
Also set clips to bound property to your cell lable in "viewcontroller.swift"
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
cell.myCellLabel.clipsToBounds=true
return cell
}
For your ease I have uploaded my sample code on GitHub Dynamic Height Sample
Output is looking like this now
The problem seems to come from the vertical constraints in the cell
By putting them relative to self instead of self.contentView in MyCustomCell you can fix your problem
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
the full class would be:
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
The thing you are missing is this function:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return heightValue
}
Im not so sure what you should do exactly, but by the fact that you know your labels you should be able to return an exact height value for each cell in this method
I think you are missing to set constraints for tableView with superview. And try to increase estimated row height also.

Resources