UITableViewCell width not expanding on rotation to landscape - swift

I'm having trouble resizing the content of a UITableViewCell when the device rotates to landscape (and therefore view width increases).
For context, this is part of a universal split view app and is only occurring on iPhone 8 in the simulator (which doesn't support split view). Later devices which do support the split view have no issue.
In my UITableViewController, translatesAutoresizingMaskIntoConstraints = false, and 'Follow Readable Width' is unchecked in IB. I have also added:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
self.tableView.reloadData()
}
The tableView also has a custom UITableViewCell to which I've added:
override func layoutSubviews() {
uniqueIDLeading.constant = (self.contentView.frame.width * 0.4)
layoutIfNeeded()
}
No errors or warnings in the console. Any other ideas?

Eventually fixed with the following in custom UITableViewCell class.
override func layoutSubviews() {
super.layoutSubviews()
autoresizingMask = .flexibleWidth
layoutIfNeeded()
}

I have the similar issue today, but I am not using a custom cell. I solved it with the following code.
Swift:
self.tableView.autoresizingMask = .flexibleWidth
Objective-C:
self.tableView.autoresizingMask=UIViewAutoresizingFlexibleWidth;

Related

how to adjust layout of tableview cells having collectionView on screen rotation on all screen sizes?

I have CollectionView embedded in TableView cells to show images posted by a user. Im targeting this app on every device from iPhone 8 to Mac and I do have screen rotation on for iPhones and iPads. This is my first time developing app for all platforms & dealing with screen rotation & after searching online I came up with many solutions for handling layout on screen rotation but none of them worked for TableView cells that contain CollectionView and are repeated.
Here I would like to know the best solutions among these for different situations and how to use them to handle CollectionView inside TableView cells.
The first solution I implemented is to handle everything by overriding viewWillLayoutSubviews() in my main View Controller and wrote something like this:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let index = IndexPath(row: 0, section: 0)
guard let cell = tblHome.cellForRow(at: index) as? PriorityTaskTableViewCell else {
return
}
guard let flowLayout = cell.cvPriorityTask.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
flowLayout.invalidateLayout()
cell.layoutIfNeeded()
cell.heightCvPriorityTask.constant = cell.height + 40
}
I had to add cell.layoutIfNeeded() & cell.heightCvPriorityTask.constant = cell.height + 40 to update cell height after rotation. The above approach worked fine for me on every device but only for one TableView cell as you can see I can access CollectionView present in 1st row of TableView only. This approach did not help me deal with situation where I have multiple rows with CollectionView like in case of a social media feed.
Then I decided to deal with rotation in TableView Cell Subclass and came up with following code inside PriorityTaskTableViewCell:
weak var weakParent: HomeViewController?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
cvPriorityTask.contentInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
cvPriorityTask.delegate = self
cvPriorityTask.dataSource = self
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard
let previousTraitCollection = previousTraitCollection,
self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass ||
self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass
else {
return
}
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
weakParent?.tblHome.updateConstraints()
DispatchQueue.main.async {
self.cvPriorityTask?.reloadData()
self.weakParent?.tblHome.updateConstraints()
}
}
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
viewWillTransition(to: size, with: coordinator)
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
weakParent?.tblHome.updateConstraints()
coordinator.animate(alongsideTransition: { context in
}, completion: { context in
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
self.weakParent?.tblHome.updateConstraints()
})
}
And this is how I'm setting up CollectionView Layout:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var width = 0.0
if traitCollection.horizontalSizeClass == .compact {
width = (weakParent?.view.frame.size.width ?? 200) * 0.88
}
else {
width = (weakParent?.view.frame.size.width ?? 200) * 0.425
}
if traitCollection.userInterfaceIdiom == .mac {
height = width * 0.75
}
else {
height = width * 1.13
}
return CGSize(width: width, height: height)
}
This is again another modified code and it seems to work perfectly for small devices like iPhone 8. But does not have any impact on larger displays on rotation. Maybe it works only for traitCollection.horizontalSizeClass == .compact If I could get this working I would have solved my issue for dealing my repeating CollectionView inside multiple rows but I have no idea why it doesn't work on iPhone 13 Pro Max or iPads.
After trying for hours I removed code and just called cvPriorityTask.collectionViewLayout.invalidateLayout() in layoutSubviews() function and it worked too for all cells so I removed all above code from Cell subclass and left with this only:
override func layoutSubviews() {
super.layoutSubviews()
cvPriorityTask.collectionViewLayout.invalidateLayout()
}
This alone made sure collectionView embedded in TableView cells were changing layout as expected but they started cutting TableView cell as main TableView wasn't being updated so I modified my code to somehow refresh TableView too and came up with something like this:
override func layoutSubviews() {
super.layoutSubviews()
cvPriorityTask.collectionViewLayout.invalidateLayout()
layoutIfNeeded()
heightCvPriorityTask.constant = height + 40
weakParent?.tblHome.rowHeight = UITableView.automaticDimension
weakParent?.tblHome.updateConstraints()
}
It did not work at all. If somehow I could update layout of the main table too from the cell subclass.
So im confused which should be right approach. The first one seemed to be most common one I find on internet but I don't know how to handle multiple cells containing CollectionViews with that approach.
Second one works only for iPhone 8 and I have no idea why. And I don't think reloading collectionView is the right approach but it works for small screens so I wouldn't mind if somehow works for large screens.
The third one seems totally wrong to me but actually works on perfectly for every device. However only cell layout is adjusted and I tried to update layout of whole tableView from Cell Subclass but no luck.
So how to deal with layout issues for CollectionView which is embedded in TableView cells? Am I on right track or there is another better way to deal with this situtaion?

Swift NSCollectionView flow from bottom to top

I am workin on a messaging client for macOS, written in Swift. I use an NSScrollView with an NSCollectionView as the documentView to present the messages. Currently, I have implemented infinite scrolling, but now the problem is that the collectionView loads the cells starting at the top and works its way down – the default behavior for an NSCollectionView. Instead, I need it to start at the bottom and work its way up – the way that a typical messaging application displays messages.
Solutions I have tried:
Scrolling to the bottom of the collectionView as soon as the view loads or the user selects a different conversation. This solution is visibly janky and truly messes up infinite scrolling.
Overriding the isFlipped variable in the scrollView, the scrollView's contentView, and the collectionView. Doing this has had zero visible effect on any of the views.
Rotating the entire scrollView, collectionView, contentView, or collectionView cells by pi radians. As a desperate measure, I attempted to rotate the entire scrollView and was not able to do that nor rotate any of the collectionView items. I did something along the lines of wantsLayer = true
layer!.setAffineTransform(CGAffineTransform(rotationAngle: .pi))
updateLayer()
setNeedsDisplay(frameRect). This again has no visible affect.
What's the best way to go about getting the NSCollectionView to go from bottom to top? By the way, I am not using Storyboards.
Hi #will i am not sure if this code will help you, because i have used it for iPhone chat application and it works for me. In my case i have simply put collectionView in view controller.
//MARK:- View Controller life cycle method
override func viewDidLoad() {
chatCollectionView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if(keyPath == "contentSize"){
if let newvalue = change?[.newKey] {
let contentHeight: CGFloat = chatCollectionView.contentSize.height
let collectionHeight = chatCollectionView.frame.size.height
if contentHeight < collectionHeight {
var insets: UIEdgeInsets = chatCollectionView.contentInset
insets.top = collectionHeight - contentHeight
chatCollectionView.contentInset = insets
} else {
chatCollectionView.contentInset = .zero
}
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
chatCollectionView.removeObserver(self, forKeyPath: "contentSize")
}

viewDidLayoutSubviews for Custom UITableViewCell

I want to animate a subview of a custom TableViewCell. To perform this animation, the cell needs the width of this subview, which is laid out by an auto-layout-constraint.
However, when I use the animation-function in the cellForRowAtIndex function (mycell.animate()), the width is 0, because the subviews are not laid out yet and the animation will not work.
In a regular view, I would use viewDidLayoutSubviews(), because then the view is laid out, I can get the width and perform the animation. However, what's the equivalent function for a custom UITableViewCell?
I tried the willDisplay delegate function of the TableView, but when I print out the width of the cells subview, it still says 0...
The correct place is inside layoutSubviews:
class MyCell : UITableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
// do your thing
}
}
It will work if you animate your view inside draw function in tableViewCell
override func draw(_ rect: CGRect) {
super.draw(rect)
//Your code here
}

Why my background gradient view is not loading in my customer UINavigationController class

I'm trying to set a gradient to the background of my subclassed NavigationController. When I add a colour to the same code it works well but I can't seem to let my gradient show up. I created a subclass of a UIView that returns a CAGradientLayer as its background view.
Here is my subclassed UIView : (Note the colours are weird so I am sure its loading the right Gradient.
#IBDesignable
class GenericBackgrounView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
///The roundness for the corner
#IBInspectable var cornerRadius: CGFloat = 0.0 {
didSet{
setupGradient()
}
}
func setupGradient() {
//let gradientColors = [bgDarkColor.cgColor, bgDarkColor.blended(withFraction: 0.5, of: bgLightColor).cgColor, bgLightColor.cgColor]
let gradientColors = [UIColor.brown.cgColor, UIColor.red.blended(withFraction: 0.5, of: UIColor.cyan).cgColor, UIColor.yellow.cgColor]
gradientLayer.colors = gradientColors
gradientLayer.locations = ESDefault.backgroundGradientColorLocations
setNeedsDisplay()
}
var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override func awakeFromNib() {
super.awakeFromNib()
setupGradient()
}
override func prepareForInterfaceBuilder() {
setupGradient()
}
}
And Here is my UINavigationController :
class GenericNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
let backView = GenericBackgrounView(frame: self.view.frame)
backView.bounds = self.view.bounds
self.view.backgroundColor = UIColor.clear
self.view.addSubview(backView)
self.view.sendSubview(toBack: backView)
// Do any additional setup after loading the view.
}
}
Also note that my GenericBackgroundView works fine when I use it for any views I add in the interface builder.
I have been at this to long. I think I will suggest to Apple to setup some kind of Theming API in both code and Interface Builder... and the ability to add gradients straight into Interface Builder...
Thanks for you help.
Instead of setting it up in awakeFromNib() , try calling it in viewDidLayoutSubviews(). Reason is that in viewDidLayoutSubviews() will have the correct frame of the view , while in awakeFromNib() you wouldn't know the right frame of the view.From Apple Documentation.
Alright, I've tinkered a bit and found some working code. I would still love to understand the reason why this works and not the way I had it before. I hate feeling it works by magic...
here is the working code : (Remember that my gradient is in form of CAGradientLayer and I have made some static variable that has defaults.
import UIKit
class GenericNavigationController: UINavigationController {
let backViewGradient = Default.testGradientCALayer
override func viewDidLoad() {
super.viewDidLoad()
setupBackground()
}
func setupBackground() {
backViewGradient.frame = self.view.frame
self.view.layer.insertSublayer(backViewGradient, at: 0)
}
override func prepareForInterfaceBuilder() {
setupBackground()
}
}
What I'm wondering is how come since all the UIControls that are subclassed from UIView don't all work the same. They should all have a view that is the background and we should all be able to either add a layer or a subview to them and be able to get my previous code to work or my latest code too which does not work with TableViewCells.
I will leave this question open because I would love to know the truth behind this. I don't think I can fully grasp Swift or Xcode if it behaves somewhat magically and inconsistent.

make UIView in UIScrollView stick to the top when scrolled up

So in a UITableView when you have sections the section view sticks to the top until the next section overlaps it and then it replaces it on top. I want to have a similar effect, where basically I have a UIView in my UIScrollView, representing the sections UIView and when it hits the top.. I want it to stay in there and not get carried up. How do I do this? I think this needs to be done in either layoutSubviews or scrollViewDidScroll and do a manipulation on the UIVIew..
To create UIView in UIScrollView stick to the top when scrolled up do:
func createHeaderView(_ headerView: UIView?) {
self.headerView = headerView
headerViewInitialY = self.headerView.frame.origin.y
scrollView.addSubview(self.headerView)
scrollView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let headerFrame = headerView.frame
headerFrame.origin.y = CGFloat(max(headerViewInitialY, scrollView.contentOffset.y))
headerView.frame = headerFrame
}
Swift Solution based on EVYA's response:
var navigationBarOriginalOffset : CGFloat?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationBarOriginalOffset = navigationBar.frame.origin.y
}
func scrollViewDidScroll(scrollView: UIScrollView) {
navigationBar.frame.origin.y = max(navigationBarOriginalOffset!, scrollView.contentOffset.y)
}
If I recall correctly, the 2010 WWDC ScrollView presentation discusses precisely how to keep a view in a fixed position while other elements scroll around it. Watch the video and you should have a clear-cut approach to implement.
It's essentially updating frames based on scrollViewDidScroll callbacks (although memory is a bit hazy on the finer points).
Evya's solution works really well, however if you use Auto Layout, you should do something like this (The Auto Layout syntax is written in Masonry, but you get the idea.):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
//Make the header view sticky to the top.
[self.headerView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.scrollView.mas_top).with.offset(scrollView.contentOffset.y);
make.left.equalTo(self.scrollView.mas_left);
make.right.equalTo(self.scrollView.mas_right);
make.height.equalTo(#(headerViewHeight));
}];
[self.scrollView bringSubviewToFront:self.headerView];
}