constrain a label inside of a UIView which is inside of a expandable collectionViewCell - ios

I have an expandable/collapsible cell in Xcode, and I am trying to do what is said in the title. When I expand the cell, the text is centred inside of the cell and I'm not sure why because I have the label constrained inside of the UIView. I will leave code below,
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(SubView)
SubView.anchors(top: topAnchor, topPad: 0, bottom: nil, bottomPad: 0, left: leftAnchor, leftPad: 0, right: rightAnchor, rightPad: 0, height: 80, width: self.bounds.width - 20)
SubView.addSubview(textField)
textField.anchor(top: SubView.topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor, padding: .init(top: 0, left: 0, bottom: 0, right: 0))
textField.centerYAnchor.constraint(equalTo: SubView.centerYAnchor).isActive = true
textField.centerXAnchor.constraint(equalTo: SubView.centerXAnchor).isActive = true
textField.translatesAutoresizingMaskIntoConstraints = false
}
this is how I create the anchors and addSubview.
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "profCell", for: indexPath) as! profCell
if indexPath.row == 0 {
cell.textField.text = "Skills & Preferences"
} else if indexPath.row == 1 {
cell.textField.text = "Bio"
} else if indexPath.row == 2 {
cell.textField.text = "Reviews"
}
return cell
}
and this is how I try to create the label text. I will leave the two images that show what I am asking.
this is how I want the cell to look even if it is expanded.
when it is expanded, the label moves to the center of the cell.
thanks for all of the help!

You need to understand a bit about Auto-Layout Constraints. If you are using Leading, Trailing, Top & Bottom constraints, you are not required to add Center X & Center Y anchors. But even if you add those extraneous constraints you need to be very careful so that they don't conflict with each other. For this purpose, you need to make some of them Low priority constraints while leaving some as High priority constraints or make them greater/equal or less/equal instead of explicit equal. This is a topic that I can't make completely understandable here in SO.
Secondly, you have pinned the leading, bottom & trailing constraints to the contentView of the UITableViewCell itself which is wrong according to your expectation. You either
change it to constraint the textField with respect to SubView's anchors here:
textField.anchor(top: SubView.topAnchor, leading: SubView.leadingAnchor,
bottom: SubView.bottomAnchor, trailing: SubView.trailingAnchor,
padding: .init(top: 0, left: 0, bottom: 0, right: 0))
and delete those textField.centerXAnchor & textField.centerYAnchor entirely.
Or
delete the line of setting the leading, trailing, top & bottom constraints entirely keeping only the center constraints.
Note: Use camelCased identifier for properties.

Related

Swift: Modify UIStackView in collectionViewCell after model is set

I have a UIStackView inside a collectionViewCell that I would like to modify depending on the Tweet object. If the Tweet object contains a retweet object, I would like to include the retweetedTextView to the stackView.
My current implementation doesn't render the stackView on screen at all. Code:
class TweetCell: UICollectionViewCell {
//Initialize UI elements
var tweet: Tweet? {
didSet {
if let tweet = tweet {
if let _ = tweet.retweetedStatus {
bottomHStack = VStack(subviews: [
reactionHStack,
isRetweetedTextView,
tweetTextTextView,
timestampLabel
], spacing: 8, layoutMargins: UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing))
} else {
bottomHStack = VStack(subviews: [
reactionHStack,
tweetTextTextView,
timestampLabel
], spacing: 8, layoutMargins: UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing))
}
}
}
}
var bottomHStack: VStack!
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
//Other stackviews
bottomHStack = VStack(subviews: [
reactionHStack,
isRetweetedTextView,
tweetTextTextView,
timestampLabel
], spacing: 8, layoutMargins: UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing))
//Overall stack
let mainStack = VStack(subviews: [
userHStack,
mediaCollectionView,
bottomHStack,
separatorLine
])
addSubview(mainStack)
mainStack.topAnchor.constraint(equalTo: topAnchor).isActive = true
mainStack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
mainStack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
I have considered alternative ways to achieve the "removal of isRetweetedTextView", but doesn't seem to be optimal, for example setting the heightConstraint to zero. Although it does appear to remove the isRetweetedTextView, the spacing in the stackView is still present.
I would like to completely remove the view from the stack.
Note that I am also implementing a similar feature to tweetTextTextView, ie if the tweetText is an empty string, I would like to remove it from the stackView as well. Creating different stackviews to cater for the varying combinations would be impractical.
There's a default behavior in UIStackView that it automatically removes a view and rearrange the stack if that view's isHidden property is true.
You could create one stack view with all possible views on it, then simply hide or show the views based on your needs. There is no need to initialize multiple stack views for the different possible arrangements of their underlying views.
For example, you could initialize one stack view with reactionHStack, isRetweetedTextView, tweetTextTextView, and timestampLabel, then make isRetweetedTextView.isHidden = true if there is no retweet.

layoutMarginsGuide producing unwanted leading and trailing margins

I'm creating constraints programmatically on my views. When I try to use the layoutMarginsGuide anchors, the top and bottom anchors work as expected, but the leading and trailing anchors create margins even if the insets are set at 0. What is creating these unwanted margins and how can I set them correctly?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemRed
view.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
let childView = UIView(frame: .zero)
childView.backgroundColor = .systemIndigo
childView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(childView)
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
childView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
childView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
childView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
])
}
So that the side contraints do not contain spaces and are completely glued to the sides, you have to remove the layoutMarginsGuide, it would be like:
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
childView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
childView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
childView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
Unlike other views, the system manages the margins of a view controller's root view. By default, it enforces minimum left and right margins of either 16 or 20 points depending on the view width. The top and bottom margins are by default zero.
So if you want less margin of root view then system minimum. you have to make false to viewRespectsSystemMinimumLayoutMargins
viewRespectsSystemMinimumLayoutMargins = false
view.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)

Adding a separator line under custom tableview cell in UITableViewCell

I am trying to add a separator to my uitableview cell. i tried this.
let separator = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 8.0))
cell.contentView.addSubview(separator)
But this adds the separator view on top of the cell, i need it to the bottom.
I also tried this way.
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(view)
view.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor).isActive = true
view.heightAnchor.constraint(equalToConstant: 8.0).isActive = true
view.topAnchor.constraint(equalTo: cell.contentView.bottomAnchor).isActive = true
But this gives me no common ancestor error. and i don't want to use storyboard. i need it because i am using same cell at different places, somewhere i need the separator somewhere not. what should i do?
Change constraint for this
view.topAnchor.constraint(equalTo: cell.contentView.bottomAnchor).isActive = true
to
view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor).isActive = true
its better to create the line inside init of the custom cell and make it a property
let view = UIView()
then mange its state from cellForRowAt
view.isHidden = true/false
Here is a better way to use separators:
First enable separators in your UITableView by:
myTableView.separatorStyle = .singleLine
Then at your cellForRowAt function:
// Create your cell
// if you want to show the separator then
cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
// if you want to hide the separator then
self.separatorInset = UIEdgeInsets(top: 0, left: UIScreen.main.bounds.width, bottom: 0, right: 0)
This would work for cells in the same UITableView as well. Because by adding a left inset of screen width then it won't show on screen and if you set it to 0 it'll be displyed from left edge to right edge of the screen.
Also you can change the color or the insets of the separator by using other properties without using storyboards or xibs.
you can just try below line under viewDidLoad() that include the tableview
override func viewDidLoad() {
tableView.separatorStyle = .singleLine
}

Align UIImage vertically center

I am trying to align an image vertically central using Swift. I understand you do this by using constraints, however I've been unable to get this to work.
func getLogo() {
let logo = UIImage(named: "LogoWhite")
let logoView = UIImageView(image: logo)
logoView.contentMode = .scaleAspectFit
self.addSubview(logoView)
}
If you don't want to use constraints (I personally do not like them) you can check for container center and put your UIImageView there.
Example:
containerView -> the view that contains your logo
logo -> the view you want vertically centered
logo.center.y = containerView.center.y
If the containerView is the screen, then
let screen = UIScreen.main.bounds
let height = screen.height
logo.center.y = screen.height / 2
I would suggest using extensions on UIView to make auto layout much easier. This seems like a lot of code to begin with for such a simple task but these convenience functions will make things a lot quicker in the long run for example:
public func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero){
translatesAutoresizingMaskIntoConstraints = false
//Set top, left, bottom and right constraints
if let top = top {
topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true
}
if let leading = leading {
leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom).isActive = true
}
if let trailing = trailing {
trailingAnchor.constraint(equalTo: trailing, constant: -padding.right).isActive = true
}
//Set size contraints
if size.width != 0 {
widthAnchor.constraint(equalToConstant: size.width).isActive = true
}
if size.height != 0 {
heightAnchor.constraint(equalToConstant: size.height).isActive = true
}
}
Then you could simply call:
someUIView.anchor(anchor(top: topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: nil, padding: .init(top: 16, left: 16, bottom: 16, right: 16), size: .init(width: 80, height: 80))
Then to answer your question directly you could then add more extensions to do some stuff easily:
public func anchorCenterXToSuperview(constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
if let anchor = superview?.centerXAnchor {
centerXAnchor.constraint(equalTo: anchor, constant: constant).isActive = true
}
}
Finally you could then simply call anchorCenterXToSuperview() on any UIView to centre any object.
Don't forget to make sure you've added your view to a view hierarchy before attempting to layout your views otherwise you'll get errors.

How do I attach a UI View below the elementKindSectionHeader of a Collection View?

I have created a Collection View and have made good use of the UICollectionView.elementKindSectionHeader by registering a Header class. See my code below:
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionViewLayout()
setupCollectionView()
setupMenuBar()
}
var headerView: HeaderView?
fileprivate func setupCollectionViewLayout() {
if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
layout.sectionInset = .init(top: padding, left: padding, bottom: padding, right: padding)
}
}
fileprivate func setupCollectionView() {
self.collectionView.backgroundColor = .white
//For iPhone X's make sure it doesn't cover the swipe bar at bottom
self.collectionView.contentInsetAdjustmentBehavior = .never
// Register cell classes
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
self.collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId)
self.collectionView.contentInset = UIEdgeInsets(top: 60, left: 0, bottom: 0, right: 0)
self.collectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 60, left: 0, bottom: 0, right: 0)
}
which gives me a nice Header like:
But now I want a menu bar beneath the header, but above the main section of the collection view. I have created a MenuBar with the correct dimensions but I can't think what to constrain it to? See my code below to add a Menu Bar currently:
let menuBar: MenuBar = {
let mb = MenuBar()
return mb
}()
fileprivate func setupMenuBar() {
view.addSubview(menuBar)
view.addConstraintWithFormat(format: "H:|[v0]|", views: menuBar)
view.addConstraintWithFormat(format: "V:|-400-[v0(50)]|", views: menuBar)
}
..but this literally just pins the bar 400px down the entire view and does not move if you scroll up or down
Does anybody have an idea for this?
if you want this menu bar to move with the UITableView scrolling, it should be placed inside the header, below the "main header".
This approach abstraction is considering your header + menu bar as just one header.

Resources