inputAccessoryView sizing problem on iPhones without physical home button - swift

inputAccessoryView's background view is falling under its own textField and profile picture imageView.
It works fine on regular screen iPhones, but on new iPhones with notches it looks like this:
Here's how it looks animated when keyboard appears: Transition animation on becomeFirstResponder()
Here's my tableView in which I'm trying to add accessoryView:
import UIKit
import SDWebImage
class CommentsTableViewController: UITableViewController {
let viewModel = CommentsViewModel()
let postID: String
let postCaption: String
let postDate: Date
let postAuthor: ZoogramUser
var keyboardAccessoryView: CommentAccessoryView = {
let commentAccessoryView = CommentAccessoryView()
return commentAccessoryView
}()
init(post: UserPost) {
self.postID = post.postID
self.postCaption = post.caption
self.postDate = post.postedDate
self.postAuthor = post.author
super.init(style: .grouped)
self.tableView.register(PostCommentsTableViewCell.self, forCellReuseIdentifier: PostCommentsTableViewCell.identifier)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Comments"
keyboardAccessoryView.delegate = self
configureKeyboardAccessoryView()
viewModel.getComments(for: self.postID) {
self.tableView.reloadData()
}
tableView.backgroundColor = .systemBackground
tableView.keyboardDismissMode = .interactive
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100
tableView.allowsSelection = false
tableView.separatorStyle = .none
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
override var inputAccessoryView: UIView? {
keyboardAccessoryView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
keyboardAccessoryView.backgroundColor = .systemOrange
return keyboardAccessoryView
}
override var canBecomeFirstResponder: Bool {
return true
}
func configureKeyboardAccessoryView() {
guard let photoURL = AuthenticationManager.shared.getCurrentUserProfilePhotoURL() else {
return
}
keyboardAccessoryView.userProfilePicture.sd_setImage(with: photoURL)
}
}
And here's code for my CommentAccessoryView which I use to override inputAccessoryView:
import UIKit
protocol CommentAccessoryViewProtocol {
func postButtonTapped(commentText: String)
}
class CommentAccessoryView: UIView {
var delegate: CommentAccessoryViewProtocol?
var userProfilePicture: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.backgroundColor = .secondarySystemBackground
imageView.contentMode = .scaleAspectFill
return imageView
}()
var commentTextField: AccessoryViewTextField = {
let textField = AccessoryViewTextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.backgroundColor = .systemBackground
textField.placeholder = "Enter comment"
textField.clipsToBounds = true
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.placeholderText.cgColor
return textField
}()
var postButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 30).isActive = true
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
button.clipsToBounds = true
button.layer.cornerRadius = 30/2
button.setImage(UIImage(systemName: "arrow.up.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35)), for: .normal)
button.tintColor = .systemBlue
button.addTarget(self, action: #selector(didTapPostButton), for: .touchUpInside)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupConstraints()
backgroundColor = .systemBackground
commentTextField.rightView = postButton
commentTextField.rightViewMode = .always
autoresizingMask = .flexibleHeight
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
setViewCornerRadius()
}
func setViewCornerRadius() {
userProfilePicture.layer.cornerRadius = userProfilePicture.frame.height / 2
commentTextField.layer.cornerRadius = commentTextField.frame.height / 2
}
func setupConstraints() {
self.addSubviews(userProfilePicture, commentTextField)
NSLayoutConstraint.activate([
userProfilePicture.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10),
userProfilePicture.centerYAnchor.constraint(equalTo: self.safeAreaLayoutGuide.centerYAnchor),
userProfilePicture.widthAnchor.constraint(equalToConstant: 40),
userProfilePicture.heightAnchor.constraint(equalToConstant: 40),
commentTextField.leadingAnchor.constraint(equalTo: userProfilePicture.trailingAnchor, constant: 10),
commentTextField.centerYAnchor.constraint(equalTo: userProfilePicture.centerYAnchor),
commentTextField.heightAnchor.constraint(equalToConstant: 40),
commentTextField.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
])
}
override var intrinsicContentSize: CGSize {
return CGSize.zero
}
#objc func didTapPostButton() {
guard let text = commentTextField.text else {
return
}
commentTextField.resignFirstResponder()
delegate?.postButtonTapped(commentText: text)
}
}
I've spent days trying to google a fix for that but nothing helps.
There were posts saying they were able to fix something similar by setting customView's bottom constraint to a safe area with the following method:
override func didMoveToWindow() {
if #available(iOS 11.0, *) {
if let window = window {
let bottomAnchor = bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1.0)
bottomAnchor.isActive = true
}
}
}
But when I use it, AutoLayout starts complaining.
UPDATE: I did what HangarRash recommended, changed CommentAccessoryView from UIView to UIInputView and centering profileImageView and textField to view itself and not to safe area. Now it's a little bit better, but seems to ignore safe area, inputAccessoryView should be above Home indicator but lies beneath it instead. Looking at last cell in TableView and Scroll indicator, it seems like TableView also isn't aware of inputAccessoryView and goes under it.

Related

Table View Design overlapping

I am new to swift .I want to display the records with image view in table view cell . I have defined the property with leadingAnchor , trailingAnchor, widthAnchor, heightAnchor with content view . But when I run the app it overlapping the view .
Here is the code in cell .
import UIKit
class PeopleCell: UITableViewCell {
static let identifier = "PeopleCell"
let containerView:UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true // this will make sure its children do not go out of the boundary
return view
}()
let profileImageView:UIImageView = {
let img = UIImageView()
img.contentMode = .scaleAspectFill // image will never be strecthed vertially or horizontally
img.translatesAutoresizingMaskIntoConstraints = false // enable autolayout
img.layer.cornerRadius = 35
img.clipsToBounds = true
return img
}()
let firstnameTitleLabel:UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 20)
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let lastnameTitleLabel:UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 14)
label.textColor = .white
label.backgroundColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1)
label.layer.cornerRadius = 5
label.clipsToBounds = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.contentView.addSubview(profileImageView)
containerView.addSubview(firstnameTitleLabel)
containerView.addSubview(lastnameTitleLabel)
self.contentView.addSubview(containerView)
profileImageView.centerYAnchor.constraint(equalTo:self.contentView.centerYAnchor).isActive = true
profileImageView.leadingAnchor.constraint(equalTo:self.contentView.leadingAnchor, constant:10).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant:70).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant:70).isActive = true
containerView.centerYAnchor.constraint(equalTo:self.contentView.centerYAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo:self.profileImageView.trailingAnchor, constant:10).isActive = true
containerView.trailingAnchor.constraint(equalTo:self.contentView.trailingAnchor, constant:-10).isActive = true
containerView.heightAnchor.constraint(equalToConstant:40).isActive = true
firstnameTitleLabel.topAnchor.constraint(equalTo:self.containerView.topAnchor).isActive = true
firstnameTitleLabel.leadingAnchor.constraint(equalTo:self.containerView.leadingAnchor).isActive = true
firstnameTitleLabel.trailingAnchor.constraint(equalTo:self.containerView.trailingAnchor).isActive = true
lastnameTitleLabel.topAnchor.constraint(equalTo:self.firstnameTitleLabel.bottomAnchor).isActive = true
lastnameTitleLabel.leadingAnchor.constraint(equalTo:self.containerView.leadingAnchor).isActive = true
lastnameTitleLabel.topAnchor.constraint(equalTo:self.firstnameTitleLabel.bottomAnchor).isActive = true
lastnameTitleLabel.leadingAnchor.constraint(equalTo:self.containerView.leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(firstName: String, lastName: String) {
firstnameTitleLabel.text = "Firstname :\(firstName)"
lastnameTitleLabel.text = "Lastname : \(lastName)"
}
func configureImageCell(row: Int, viewModel: ViewModel) {
profileImageView.image = nil
viewModel
.downloadImage(row: row) { [weak self] data in
let image = UIImage(data: data)
self?.profileImageView.image = image
}
}
}
Here is the view controller code .
import UIKit
import Combine
class PeopleViewController: UIViewController {
var coordinator: PeopleBaseCoordinator?
init(coordinator: PeopleBaseCoordinator) {
super.init(nibName: nil, bundle: nil)
self.coordinator = coordinator
title = "People"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private let viewModel = ViewModel()
private var subscribers = Set<AnyCancellable>()
var activityIndicator = UIActivityIndicatorView(style: .medium)
private lazy var tableView: UITableView = {
let tableview = UITableView()
tableview.translatesAutoresizingMaskIntoConstraints = false
tableview.dataSource = self
tableview.prefetchDataSource = self
tableview.showsVerticalScrollIndicator = false
tableview.register(PeopleCell.self, forCellReuseIdentifier: PeopleCell.identifier)
return tableview
}()
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator.startAnimating()
setUpUI()
setUpBinding()
self.activityIndicator.stopAnimating()
// Do any additional setup after loading the view.
}
private func setUpUI() {
view.backgroundColor = .white
title = "People List "
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo:view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo:view.safeAreaLayoutGuide.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo:view.safeAreaLayoutGuide.rightAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo:view.safeAreaLayoutGuide.bottomAnchor).isActive = true
// Creating constrain for Indecator
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(activityIndicator)
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
private func setUpBinding() {
viewModel
.$peoples
.receive(on : RunLoop.main)
.sink { [weak self ] _ in
self?.tableView.reloadData()
}
.store(in: &subscribers)
viewModel.getPeople()
}
}
extension PeopleViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.peoples.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: PeopleCell.identifier, for: indexPath) as? PeopleCell
else { return UITableViewCell() }
let row = indexPath.row
let people = viewModel.peoples[row]
cell.configureCell(firstName: people.firstName, lastName: people.lastName)
cell.configureImageCell(row: row, viewModel: viewModel)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
extension PeopleViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
viewModel.getPeople()
}
}
Here is the result .
This is rather tricky because it seems your constraints are fine, assuming that your tableview height is 100, but the screenshot tableview cells seem a little shorter than 100. Let's assume the cell height is 100 correct.
I suggest you try configuring the imageView (and other views) in override func layoutSubViews(), which is a function that renders whenever the contentView's bound change. It should also be noted that better practice is where the imageSize is relative to the cell/contentView's frame instead of hardcoded values.
So it should look like
import UIKit
class PeopleCell: UITableViewCell {
let profileImageView:UIImageView = {
let img = UIImageView()
return img
}()
override func layoutSubviews() {
super.layoutSubviews()
profileImageView.translatesAutoresizingMaskIntoConstraints = false profileImageView.centerYAnchor.constraint(equalTo:self.contentView.centerYAnchor).isActive = true
profileImageView.leadingAnchor.constraint(equalTo:self.contentView.leadingAnchor, constant:10).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant:self.frame.width * 0.7).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant:self.frame.width * 0.7).isActive = true
//You may want to try with other type of contentMode such as aspectFit, etc
profileImageView.contentMode = .scaleAspectFill
profileImageView.layer.cornerRadius = self.frame.width / 2
profileImageView.clipsToBounds = true
}
//If above doesn't work, you may want to look into the imageConfiguration function you made and ensure that contentMode is applied properly.
func configureImageCell(row: Int, viewModel: ViewModel) {
profileImageView.image = nil
viewModel
.downloadImage(row: row) { [weak self] data in
let image = UIImage(data: data)
self?.profileImageView.image = image
self?.profileImageView.contentMode = .scaleAspectFill
}
}
If all of the above code doesn't work, try to find the profileImageView size values by using breakpoints or ViewHierarchy within Xcode. To check the height of image or cell itself, they should be sufficient for you to find clues to resolve the issue.
All the best.

UITextView with Adjustable Font Size

I tried to make an editable uitextview with centered text that can have a maximum of 2 lines and that automatically adjusts its font size in order to fit its text within a fixed width and height.
My solution: Type some text in a UITextView, automatically copy that text and paste it in a uilabel that itself automatically adjusts its font size perfectly, and then retrieve the newly adjusted font size of the uilabel and set that size on the UITextView text.
I have spent about a month on this and repeatedly failed. I can't find a way to make it work. My attempted textview below glitches some letters out and hides large portions of out-of-bounds text instead of resizing everything. Please help me stack overflow Gods.
My attempt:
import UIKit
class TextViewViewController: UIViewController{
private let editableUITextView: UITextView = {
let tv = UITextView()
tv.font = UIFont.systemFont(ofSize: 20)
tv.text = "Delete red text, and type here"
tv.backgroundColor = .clear
tv.textAlignment = .center
tv.textContainer.maximumNumberOfLines = 2
tv.textContainer.lineBreakMode = .byWordWrapping
tv.textColor = .red
return tv
}()
private let correctTextSizeLabel: UILabel = {
let tv = UILabel()
tv.font = UIFont.systemFont(ofSize: 20)
tv.backgroundColor = .clear
tv.text = "This is properly resized"
tv.adjustsFontSizeToFitWidth = true
tv.lineBreakMode = .byTruncatingTail
tv.numberOfLines = 2
tv.textAlignment = .center
tv.textColor = .green
return tv
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(correctTextSizeLabel)
view.addSubview(editableUITextView)
editableUITextView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
editableUITextView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
editableUITextView.heightAnchor.constraint(equalToConstant: 150).isActive = true
editableUITextView.widthAnchor.constraint(equalToConstant: 150).isActive = true
editableUITextView.translatesAutoresizingMaskIntoConstraints = false
editableUITextView.delegate = self
correctTextSizeLabel.leftAnchor.constraint(equalTo: editableUITextView.leftAnchor).isActive = true
correctTextSizeLabel.rightAnchor.constraint(equalTo: editableUITextView.rightAnchor).isActive = true
correctTextSizeLabel.topAnchor.constraint(equalTo: editableUITextView.topAnchor).isActive = true
correctTextSizeLabel.bottomAnchor.constraint(equalTo: editableUITextView.bottomAnchor).isActive = true
correctTextSizeLabel.translatesAutoresizingMaskIntoConstraints = false
editableUITextView.isScrollEnabled = false
}
func getApproximateAdjustedFontSizeOfLabel(label: UILabel) -> CGFloat {
if label.adjustsFontSizeToFitWidth == true {
var currentFont: UIFont = label.font
let originalFontSize = currentFont.pointSize
var currentSize: CGSize = (label.text! as NSString).size(withAttributes: [NSAttributedString.Key.font: currentFont])
while currentSize.width > label.frame.size.width * 2 && currentFont.pointSize > (originalFontSize * label.minimumScaleFactor) {
currentFont = currentFont.withSize(currentFont.pointSize - 1)
currentSize = (label.text! as NSString).size(withAttributes: [NSAttributedString.Key.font: currentFont])
}
return currentFont.pointSize
} else {
return label.font.pointSize
}
}
}
//MARK: - UITextViewDelegate
extension TextViewViewController : UITextViewDelegate {
private func textViewShouldBeginEditing(_ textView: UITextView) {
textView.becomeFirstResponder()
}
func textViewDidBeginEditing(_ textView: UITextView) {
}
func textViewDidEndEditing(_ textView: UITextView) {
}
func textViewDidChange(_ textView: UITextView) {
textView.becomeFirstResponder()
self.correctTextSizeLabel.text = textView.text
self.correctTextSizeLabel.isHidden = false
let estimatedTextSize = self.getApproximateAdjustedFontSizeOfLabel(label: self.correctTextSizeLabel)
print("estimatedTextSize: ",estimatedTextSize)
self.editableUITextView.font = UIFont.systemFont(ofSize: estimatedTextSize)
}
}
UITextField's have the option to automatically adjust font size to fit a fixed width but they only allow 1 line of text, I need it to have a maximum of 2. UILabel's solve this problem perfectly but they aren't editable.
After some searching, this looks like it will be very difficult to get working as desired.
This doesn't directly answer your question, but it may be an option:
Here's the example code:
class TestInputViewController: UIViewController {
let testLabel: InputLabel = InputLabel()
override func viewDidLoad() {
super.viewDidLoad()
let instructionLabel = UILabel()
instructionLabel.textAlignment = .center
instructionLabel.text = "Tap yellow label to edit..."
let centeringFrameView = UIView()
// label properties
let fnt: UIFont = .systemFont(ofSize: 32.0)
testLabel.isUserInteractionEnabled = true
testLabel.font = fnt
testLabel.adjustsFontSizeToFitWidth = true
testLabel.minimumScaleFactor = 0.25
testLabel.numberOfLines = 2
testLabel.setContentHuggingPriority(.required, for: .vertical)
let minLabelHeight = ceil(fnt.lineHeight)
// so we can see the frames
centeringFrameView.backgroundColor = .red
testLabel.backgroundColor = .yellow
[centeringFrameView, instructionLabel, testLabel].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
view.addSubview(instructionLabel)
view.addSubview(centeringFrameView)
centeringFrameView.addSubview(testLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// instruction label centered at top
instructionLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
instructionLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// centeringFrameView 20-pts from instructionLabel bottom
centeringFrameView.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 20.0),
// Leading / Trailing with 20-pts "padding"
centeringFrameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
centeringFrameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// test label centered vertically in centeringFrameView
testLabel.centerYAnchor.constraint(equalTo: centeringFrameView.centerYAnchor, constant: 0.0),
// Leading / Trailing with 20-pts "padding"
testLabel.leadingAnchor.constraint(equalTo: centeringFrameView.leadingAnchor, constant: 20.0),
testLabel.trailingAnchor.constraint(equalTo: centeringFrameView.trailingAnchor, constant: -20.0),
// height will be zero if label has no text,
// so give it a min height of one line
testLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: minLabelHeight),
// centeringFrameView height = 3 * minLabelHeight
centeringFrameView.heightAnchor.constraint(equalToConstant: minLabelHeight * 3.0)
])
// to handle user input
testLabel.editCallBack = { [weak self] str in
guard let self = self else { return }
self.testLabel.text = str
}
testLabel.doneCallBack = { [weak self] in
guard let self = self else { return }
// do something when user taps done / enter
}
let t = UITapGestureRecognizer(target: self, action: #selector(self.labelTapped(_:)))
testLabel.addGestureRecognizer(t)
}
#objc func labelTapped(_ g: UITapGestureRecognizer) -> Void {
testLabel.becomeFirstResponder()
testLabel.inputContainerView.theTextView.text = testLabel.text
testLabel.inputContainerView.theTextView.becomeFirstResponder()
}
}
class InputLabel: UILabel {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
override var canBecomeFirstResponder: Bool {
return true
}
override var canResignFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
get { return inputContainerView }
}
lazy var inputContainerView: CustomInputAccessoryView = {
let customInputAccessoryView = CustomInputAccessoryView(frame: .zero)
customInputAccessoryView.backgroundColor = .blue
customInputAccessoryView.editCallBack = { [weak self] str in
guard let self = self else { return }
self.editCallBack?(str)
}
customInputAccessoryView.doneCallBack = { [weak self] in
guard let self = self else { return }
self.resignFirstResponder()
}
return customInputAccessoryView
}()
}
class CustomInputAccessoryView: UIView, UITextViewDelegate {
var editCallBack: ((String) -> ())?
var doneCallBack: (() -> ())?
let theTextView: UITextView = {
let tv = UITextView()
tv.isScrollEnabled = false
tv.font = .systemFont(ofSize: 16)
tv.autocorrectionType = .no
tv.returnKeyType = .done
return tv
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(theTextView)
theTextView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// constraint text view with 8-pts "padding" on all 4 sides
theTextView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
theTextView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
theTextView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
theTextView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
])
theTextView.delegate = self
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if (text == "\n") {
textView.resignFirstResponder()
doneCallBack?()
}
return true
}
func textViewDidChange(_ textView: UITextView) {
editCallBack?(textView.text ?? "")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var intrinsicContentSize: CGSize {
return .zero
}
}

Button action in CollectionView cell swift

I have a button in my CollectionViewCell and I added an action to the button but the action does not get triggered. I am currently using the cell independently.
class BalanceCell: UICollectionViewCell {
lazy var toggleBtn: ToggleButton = {
let view = ToggleButton()
view.isUserInteractionEnabled = true
view.addTarget(self, action: #selector(onTapped), for: .touchUpInside)
return view
}()
lazy var titleLabel: LabelX = {
let view = LabelX()
view.text = "My Available Balance"
view.font = .systemFont(ofSize: 16, weight: .regular)
return view
}()
lazy var amountLabel: TextFieldX = {
let view = TextFieldX()
view.isSecureTextEntry = true
view.text = "N000,000.00"
view.textAlignment = .center
view.font = .systemFont(ofSize: 26, weight: .medium)
return view
}()
private lazy var stackView: StackViewX = {
let view = StackViewX(arrangedSubviews: [titleLabel, amountLabel])
view.alignment = .center
view.axis = .vertical
view.distribution = .fillProportionally
view.spacing = 4
return view
}()
lazy var bgImageView: ImageViewX = {
let view = ImageViewX()
view.backgroundColor = R.color.roundEdgeButtonBgColor()
return view
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func layoutSubviews() {
super.layoutSubviews()
setupViewHierarchy()
setupConstraints()
setupProperties()
translatesAutoresizingMaskIntoConstraints = false
}
func setupViewHierarchy() {
addSubview(bgImageView)
bgImageView.addSubviews([stackView, toggleBtn])
}
func setupConstraints() {
bgImageView.fillToSuperview()
stackView.centerInSuperview()
toggleBtn.anchor(verticalY: bgImageView.centerYAnchor, horizontalX: nil, paddingX: 0, paddingY: 0, width: 24, height: 24, enableInsets: false)
toggleBtn.rightAnchor.constraint(equalTo: stackView.leftAnchor, constant: -18).isActive = true
}
func setupProperties() {
bgImageView.backgroundColor = .red
bgImageView.layer.cornerRadius = 15
layer.cornerRadius = 15
}
func setup(with title: String, amount: String, bgImageViewColor: UIColor) {
titleLabel.text = title
amountLabel.text = amount
bgImageView.backgroundColor = bgImageViewColor
}
#objc private func onTapped() {
self.setSecureMode(self.toggleBtn.isSecure)
}
private func setSecureMode(_ secure: Bool) {
amountLabel.isSecureTextEntry = secure
let tempText = amountLabel.text
amountLabel.text = tempText
}
}
This is the way I use it.
lazy var balacnceView: BalanceCell = {
let view = BalanceCell()
return view
}()
ANy help as to why the Togelbutton is not getting called is appritiatedswift
The default value of isUserInteractionEnabled property is true in UIButton. You do not need to set this property to true.
However, the default value of isUserInteractionEnabled property is false in UIImageView. If you want to enable action in UIImageView (or in subviews of UIImageView), then set isUserInteractionEnabled to true.
In your code, you have put all subviews inside bgImageView. Just add this line of code:
bgImageView.isUserInteractionEnabled = true

UITapGestureRecognizer for UILabel inside of a StackView

I have a Stack View with two labels, one of which once tapped, suppose to lead to another view.
Here the code of my UIView subclass where labels and a StackView are setup:
import UIKit
import SnapKit
class WelcomeView: UIView {
weak var coordinator: MainCoordinator?
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "backGroundImage")
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
return imageView
}()
private let questionLabel: UILabel = {
let questionLabel = UILabel()
questionLabel.text = "Don't have an Account?"
questionLabel.font = UIFont(name: "Avenir Next Regular", size: 17)
questionLabel.textColor = .black
return questionLabel
}()
private let signUpLabel: UILabel = {
let signUpLabel = UILabel()
let tap = UITapGestureRecognizer(target: self, action: #selector(ViewController.signUpTapped(tapGesture:)))
signUpLabel.text = "Sign Up"
signUpLabel.font = UIFont(name: "Avenir Next Regular", size: 17)
signUpLabel.textColor = .black
signUpLabel.highlightedTextColor = .link
signUpLabel.isHighlighted = true
signUpLabel.isUserInteractionEnabled = true
signUpLabel.addGestureRecognizer(tap)
return signUpLabel
}()
lazy var signUpstackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [questionLabel, signUpLabel])
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fill
stackView.spacing = 8
stackView.isUserInteractionEnabled = true
return stackView
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSubViews()
}
func addSubViews() {
self.backgroundColor = .white
self.addSubview(imageView)
self.addSubview(btnSignIn)
self.addSubview(signUpstackView)
setConstraints()
}
func setConstraints() {
imageView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
btnSignIn.snp.makeConstraints { (make) in
make.height.equalTo(60)
make.bottomMargin.equalTo(-50)
make.leftMargin.equalTo(28)
make.rightMargin.equalTo(-28)
}
signUpstackView.snp.makeConstraints { (make) in
make.height.equalTo(24)
make.centerX.equalTo(self)
make.top.equalTo(btnSignIn).offset(70)
}
}
}
I added UITapGestureRecognizer in signUpLabel.
And here is the code from my ViewController containing my IBAction function signUpTapped which is specified in UITapGestureRecognizer:
class ViewController: UIViewController {
var welcomeView = WelcomeView()
override func loadView() {
view = welcomeView
}
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func signUpTapped(tapGesture:UITapGestureRecognizer) {
print("Tapped")
}
}
For some reason nothing is happened when I try to click on my SignUp Label. Is this an issue because my UILabel is inside of a StackView?
You need
Sol 1
let tap = UITapGestureRecognizer(target: self, action: #selector(self.signUpTapped(tapGesture:)))
then inside the view class
weak var delegate:ViewController?
#objc func signUpTapped(tapGesture:UITapGestureRecognizer) {
print("Tapped")
delegate?.tapped()
}
var welcomeView = WelcomeView()
override func loadView() {
welcomeView.delegate = self
view = welcomeView
}
func tapped(){}
Sol 2
weak var delegate:ViewController?
init(frame: CGRect,delegate:ViewController) {
super.init(frame: frame)
self.delegate = delegate
addSubViews()
}
With
let tap = delegate(target:delegate!, action: #selector(delegate!.signUpTapped(tapGesture:)))
and inside the vc
#objc func signUpTapped(tapGesture:UITapGestureRecognizer) {
print("Tapped")
}

Swift - Size of content view does not match with UITableViewCell

I have an iOS app with a TableView, all UI of the app are done programmingly with autolayout using Swift.
All UI are working great until recently, I have to add a new component (custom UIView) inside the UITableViewCell which cell height will be changed when the new component is shown or hidden. The height of the cell is not correct so my views inside the UITableViewCell become a mess.
After checking the Debug View Hierarchy, I found that the height of the UITableViewCell is different than then UITableViewCellContentView.
When the component should display:
Table view cell has correct height (Longer height)
Content View of UITableViewCell is shorter then expected (the height is correct if component is hidden)
When the component should hidden:
Table view cell has correct height (Shorter height)
Content View of UITableViewCell is longer then expected (the height is correct if component is display)
I am not really sure what is the real issue. When I toggle the component to be displayed or not, I do the followings:
// Update the constraints status
var componentIsShown: Bool = .....
xxxConstraints?.isActive = componentIsShown
yyyConstraints?.isActive = !componentIsShown
// Update UI
layoutIfNeeded()
view.setNeedsUpdateConstraints()
tableView.beginUpdates()
tableView.endUpdates()
It seems to me that when I toggle the component to be displayed or not, the UITableViewCell is updated immediately, but content view used previous data to update the height. If this is the issue, how could I update the content view height also?
If this is not the issue, any suggestion to fix it or do further investigation?
Thanks
====================
Updated in 2018-08-29:
Attached are the codes for the issue.
Clicking on the topMainContainerView in MyBaseView (the view with red alpha bg) will toggle the hiddenView display or not.
In ViewController.swift:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
var cell: MyViewCell
if let theCell = tableView.dequeueReusableCell(withIdentifier: "cell") as? MyViewCell
{
cell = theCell
}
else
{
cell = MyViewCell(reuseIdentifier: "cell")
}
cell.setViewCellDelegate(delegate: self)
return cell
}
MyViewCell.swift
class MyViewCell: MyViewParentCell
{
var customView: MyView
required init?(coder aDecoder: NSCoder)
{
return nil
}
override init(reuseIdentifier: String?)
{
customView = MyView()
super.init(reuseIdentifier: reuseIdentifier)
selectionStyle = .none
}
override func initViews()
{
contentView.addSubview(customView)
}
override func initLayout()
{
customView.translatesAutoresizingMaskIntoConstraints = false
customView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
customView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
customView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
customView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
}
override func setViewCellDelegate(delegate: MyViewCellDelegate)
{
super.setViewCellDelegate(delegate: delegate)
customView.delegate = delegate
customView.innerDelegate = self
}
}
MyViewParentCell.swift:
protocol MyViewCellDelegate
{
func reloadTableView()
}
protocol MyViewCellInnerDelegate
{
func viewCellLayoutIfNeeded()
}
class MyViewParentCell: UITableViewCell
{
private var delegate: MyViewCellDelegate?
var innerDelegate: MyViewCellInnerDelegate?
required init?(coder aDecoder: NSCoder)
{
return nil
}
init(reuseIdentifier: String?)
{
super.init(style: UITableViewCellStyle.default, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
initViews()
initLayout()
}
func initViews()
{
}
func initLayout()
{
}
override func layoutSubviews()
{
}
func setViewCellDelegate(delegate: MyViewCellDelegate)
{
self.delegate = delegate
}
}
extension MyViewParentCell: MyViewCellInnerDelegate
{
func viewCellLayoutIfNeeded()
{
print("MyViewParentCell viewCellLayoutIfNeeded")
setNeedsUpdateConstraints()
layoutIfNeeded()
}
}
MyView.swift
class MyView: MyParentView
{
private var mainView: UIView
// Variables
var isViewShow = true
// Constraint
private var mainViewHeightConstraint: NSLayoutConstraint?
private var hiddenViewHeightConstraint: NSLayoutConstraint?
private var hiddenViewPosYHideViewConstraint: NSLayoutConstraint?
private var hiddenViewPosYShowViewConstraint: NSLayoutConstraint?
// Constant:
let viewSize = UIScreen.main.bounds.width
// Init
override init()
{
mainView = UIView(frame: CGRect(x: 0, y: 0, width: viewSize, height: viewSize))
super.init()
}
required init?(coder aDecoder: NSCoder)
{
return nil
}
override func initViews()
{
super.initViews()
topMainContainerView.addSubview(mainView)
}
override func initLayout()
{
super.initLayout()
//
mainView.translatesAutoresizingMaskIntoConstraints = false
mainView.topAnchor.constraint(equalTo: topMainContainerView.topAnchor).isActive = true
mainView.bottomAnchor.constraint(equalTo: topMainContainerView.bottomAnchor).isActive = true
mainView.leadingAnchor.constraint(equalTo: topMainContainerView.leadingAnchor).isActive = true
mainView.widthAnchor.constraint(equalToConstant: viewSize).isActive = true
mainView.heightAnchor.constraint(equalToConstant: viewSize).isActive = true
mainViewHeightConstraint = mainView.heightAnchor.constraint(equalToConstant: viewSize)
mainViewHeightConstraint?.isActive = true
}
override func toggle()
{
isViewShow = !isViewShow
print("toggle: isViewShow is now (\(isViewShow))")
setViewHidden()
}
private func setViewHidden()
{
UIView.animate(withDuration: 0.5) {
if self.isViewShow
{
self.hiddenViewBottomConstraint?.isActive = false
self.hiddenViewTopConstraint?.isActive = true
}
else
{
self.hiddenViewTopConstraint?.isActive = false
self.hiddenViewBottomConstraint?.isActive = true
}
self.layoutIfNeeded()
self.needsUpdateConstraints()
self.innerDelegate?.viewCellLayoutIfNeeded()
self.delegate?.reloadTableView()
}
}
}
MyParentView.swift
class MyParentView: MyBaseView
{
var delegate: MyViewCellDelegate?
var innerDelegate: MyViewCellInnerDelegate?
override init()
{
super.init()
}
required init?(coder aDecoder: NSCoder)
{
return nil
}
override func initViews()
{
super.initViews()
}
override func initLayout()
{
super.initLayout()
translatesAutoresizingMaskIntoConstraints = false
}
}
MyBaseView.swift
class MyBaseView: UIView
{
var topMainContainerView: UIView
var hiddenView: UIView
var bottomActionContainerView: UIView
var bottomSeparator: UIView
var hiddenViewTopConstraint: NSLayoutConstraint?
var hiddenViewBottomConstraint: NSLayoutConstraint?
// Layout constratint
var descriptionWidthConstraint: NSLayoutConstraint?
var moreMainTopAnchorConstraint: NSLayoutConstraint?
var moreMainBottomAnchorConstraint: NSLayoutConstraint?
var separatorTopAnchorToActionBarConstraint: NSLayoutConstraint?
var separatorTopAnchorToPartialCommentConstraint: NSLayoutConstraint?
// Constant
let paddingX: CGFloat = 10
let InnerPaddingY: CGFloat = 9
init()
{
topMainContainerView = UIView()
hiddenView = UIView()
bottomActionContainerView = UIView()
bottomSeparator = UIView()
super.init(frame: .zero)
initViews()
initLayout()
}
required init?(coder aDecoder: NSCoder)
{
return nil
}
func initViews()
{
let borderColor = UIColor.gray
backgroundColor = UIColor(red: 211/255.0, green: 211/255.0, blue: 1, alpha: 1)
topMainContainerView.backgroundColor = UIColor.red.withAlphaComponent(0.7)
let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
topMainContainerView.addGestureRecognizer(gesture)
// Hidden View
hiddenView.backgroundColor = UIColor.yellow
hiddenView.layer.cornerRadius = 50
// Action
bottomActionContainerView.backgroundColor = UIColor.blue
bottomSeparator.backgroundColor = borderColor
// Add hiddenView first, so it will hide behind main view
addSubview(hiddenView)
addSubview(topMainContainerView)
addSubview(bottomActionContainerView)
addSubview(bottomSeparator)
}
func initLayout()
{
// MARK: Main
topMainContainerView.translatesAutoresizingMaskIntoConstraints = false
topMainContainerView.topAnchor.constraint(equalTo: topAnchor, constant: 30).isActive = true
topMainContainerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
topMainContainerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20).isActive = true
topMainContainerView.heightAnchor.constraint(equalToConstant: 150).isActive = true
// Hidden View
hiddenView.translatesAutoresizingMaskIntoConstraints = false
hiddenViewTopConstraint = hiddenView.topAnchor.constraint(equalTo: topMainContainerView.bottomAnchor)
hiddenViewTopConstraint?.isActive = true
hiddenView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
hiddenView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20).isActive = true
hiddenViewBottomConstraint = hiddenView.bottomAnchor.constraint(equalTo: topMainContainerView.bottomAnchor)
hiddenViewBottomConstraint?.isActive = false
hiddenView.heightAnchor.constraint(equalToConstant: 100).isActive = true
// MARK: Bottom
bottomActionContainerView.translatesAutoresizingMaskIntoConstraints = false
bottomActionContainerView.topAnchor.constraint(equalTo: hiddenView.bottomAnchor, constant: InnerPaddingY).isActive = true
bottomActionContainerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
bottomActionContainerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
bottomActionContainerView.heightAnchor.constraint(equalToConstant: 32).isActive = true
// MARK: Separator
bottomSeparator.translatesAutoresizingMaskIntoConstraints = false
separatorTopAnchorToPartialCommentConstraint = bottomSeparator.topAnchor.constraint(equalTo: bottomActionContainerView.bottomAnchor, constant: InnerPaddingY)
separatorTopAnchorToActionBarConstraint = bottomSeparator.topAnchor.constraint(equalTo: bottomActionContainerView.bottomAnchor, constant: InnerPaddingY)
separatorTopAnchorToPartialCommentConstraint?.isActive = false
separatorTopAnchorToActionBarConstraint?.isActive = true
bottomSeparator.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
bottomSeparator.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
bottomSeparator.heightAnchor.constraint(equalToConstant: 10).isActive = true
bottomSeparator.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
self.hiddenViewBottomConstraint?.isActive = false
self.hiddenViewTopConstraint?.isActive = true
}
#objc func toggle()
{
}
}
The layout of contentView is not updating. You should try
cell.contentView.layoutIfNeeded()
Try and share results.
I finally found that I should call super.layoutSubviews() in MyViewParentCell.swift or simply remove the function to fix the issue.
override func layoutSubviews()
{
super.layoutSubviews()
}