I am trying to build an interface with the IB under Xcode with some Containers View.
I will try to do my best to explain the problem:
I have a main scene which contains two controllers. The first one, at the top of the scene, contains a "Last Post View", which retrieves the last post from a Wordpress website and displays the cover image, the post's date and the title.
The second one contains a Collection View which leads to other views.
Functionally and independently, everything seems to work fine. The problem is that I can not figure how to make work this "stack" with autolayout and fit on portrait and landscape modes and different devices.
Here is my Storyboard
The Home Controller's constraints
The Last Post View's constraints
The Collection View's constraints
..and finally, what I get
After hours of searching and attempts, I found that the Scroll View, contained in my Home Controller, must have only one direct child. But I don't know how to put the different constraints. Plus, I always get the message: Scrollable content size is ambiguous for "Scroll View".
Another problem that I have, is when I am in landscape mode, I can't scroll the "whole view". At best, I can scroll the Collection View only (when I can display it) but not the entire screen.
(Maybe it can help if I said that I am using Swift 2)
Does anyone have a suggestion? It will be much appreciated!
Many thanks!
EDIT 1
I tried to apply the Xingou's solution and I think I am quite close the goal but I obviously miss something.
Here is my HomeViewController
class HomeViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var containerViewHeightConstrait: NSLayoutConstraint!
#IBOutlet weak var lastPostContainerView: UIView!
#IBOutlet weak var scrollContainerView: UIView!
#IBOutlet weak var mainCollectionContainerView: UIView!
/*************************/
/***** VIEW DID LOAD *****/
/*************************/
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
self.lastPostContainerView.setNeedsDisplay()
self.mainCollectionContainerView.setNeedsDisplay()
self.containerViewHeightConstrait.constant = UIScreen.mainScreen().bounds.height - 64
//END viewDidLoad
}
...
/********************************/
/***** SET CONTAINER HEIGHT *****/
/********************************/
func needSetContainerHeight( height: CGFloat ) {
self.containerViewHeightConstrait.constant = height + lastPostContainerView.bounds.height + 200
self.scrollView.contentSize.height = height + lastPostContainerView.bounds.height + 200
print( "View Height Constrait \( self.containerViewHeightConstrait.constant )" )
print( "Scroll View Height \( self.scrollView.contentSize.height )" )
//END needSetContainerHeight
}
...
...and my MainCollectionController
class MainCollectionViewController: UICollectionViewController {
/*************************/
/***** VIEW DID LOAD *****/
/*************************/
override func viewDidLoad() {
super.viewDidLoad()
collectionView!.autoresizingMask = [ .FlexibleWidth ]
//END viewDidLoad
}
/****************************************/
/****************************************/
/***************************/
/***** VIEW DID APPEAR *****/
/***************************/
override func viewDidAppear( animated: Bool ) {
super.viewDidAppear( animated )
( self.parentViewController as! HomeViewController ).needSetContainerHeight( self.collectionView!.contentSize.height )
//END viewDidAppear
}
...
If I well understood what was proposed, here is how constraints should look like :
(Main) View
Scroll View
Subviews Container
Last Post Container
Collection Container
List of all constraints
... and what I get
Portrait
Landscape
I made a few tests with different extra constraints and I found out that I had to tell the Scroll View to fill its whole parent to display something on the screen (otherwise, I just get the red background).
Another thing is if I add a constraint for the space between Last Post Container and Collection Container, things are "well" positioned but I cannot click on the collection's cells anymore. But, if I don't add this constraint, things are more messy (for example, the collection view overlaps the post view), but I am able to click on the cells.
Unfortunately, the screen seems to be cropped and there are some differences when I rotate the screen. But I think I have to recompute the heights when the device is rotated (am I right?). The margin at the top of screen still here, but not always: it depends in which mode I started the app and how many rotation I do.
Another thing I forgot to mention, is the last post is asynchronously retrieved. So, the cover image, which has a variable size, is displayed after the main screen is displayed. I moved this in the AppDelegate - didFinishLaunchingWithOptions and stocked the post in the Notification Center but unfortunately, there's still some delay.
Any suggestions? :)
SOLUTION
Like Xingou said, it is far easier to use the header section of the collection view (Accessories / Section Header).
you need to solve the following thing:
in you home sense, viewdidload , add the following code:
self.automaticallyAdjustsScrollViewInsets = false
this can make the red area between navagationbar and you picture disappear.
fixed the Scrollable content size is ambiguous for "Scroll View":
this is because the scrollview did not know the content size it would show。you need reset the constrain of the view( the two container view's super view ),i will call it scrollcontainerview:
scrollcontainerview.leading = scrollview.leading
scrollcontainerview.trailing = scrollview.trailing
scrollcontainerview.top = scrollview.top
scrollcontainerview.bottom = scrollview.bottom
scrollcontainerview.width = self.view.width
scrollcontainerview.height = 603 //we will chang this value through code
now the "the Scrollable content size is ambiguous for "Scroll View"" error should be disappeared.
change the scrollcontainerview.height in code :
in you home scene, drag the scrollcontainerview.height constraint into you view controller,and chang it to fit the screen:
#IBOutlet weak var contianerViewHeightConstrait:
NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false
//because we set the height at const 603,but height is different on different device,so we need chang it to the correct value
contianerViewHeightConstrait.constant = UIScreen.mainScreen().bounds.height - 64
}
and now you can see the two container view fill the screen。
need scroll the whole content, not only the collection view:
to solve this problem ,you need assign a correct height to the scrollview.contentsize.height .
in you collection view controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
(self.parentViewController as! HomeScenceViewController).needSetContainerHeight(self.collectionView.contentSize.height)
}
then add a method in HomeScenceViewController
// set the containerview and scrollview height
func needSetContainerHeight(height:CGFloat){
//the 200 is your first container view's height
contianerViewHeightConstrait.constant = height + 200
scrollview.contentSize.height = height + 200
}
and now you should can scroll the whole content
Related
To help in following this question, I've put up a GitHub repository:
https://github.com/mattneub/SelfSizingCells/tree/master
The goal is to get self-sizing cells in a table view, based on a custom view that draws its own text rather than a UILabel. I can do it, but it involves a weird layout kludge and I don't understand why it is needed. Something seems to be wrong with the timing, but then I don't understand why the same problem doesn't occur for a UILabel.
To demonstrate, I've divided the example into three scenes.
Scene 1: UILabel
In the first scene, each cell contains a UILabel pinned to all four sides of the content view. We ask for self-sizing cells and we get them. Looks great.
Scene 2: StringDrawer
In the second scene, the UILabel has been replaced by a custom view called StringDrawer that draws its own text. It is pinned to all four sides of the content view, just like the label was. We ask for self-sizing cells, but how will we get them?
To solve the problem, I've given StringDrawer an intrinsicContentSize based on the string it is displaying. Basically, we measure the string and return the resulting size. In particular, the height will be the minimal height that this view needs to have in order to display the string in full at this view's current width, and the cell is to be sized to that.
class StringDrawer: UIView {
#NSCopying var attributedText = NSAttributedString() {
didSet {
self.setNeedsDisplay()
self.invalidateIntrinsicContentSize()
}
}
override func draw(_ rect: CGRect) {
self.attributedText.draw(with: rect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], context: nil)
}
override var intrinsicContentSize: CGSize {
let measuredSize = self.attributedText.boundingRect(
with: CGSize(width:self.bounds.width, height:10000),
options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin],
context: nil).size
return CGSize(width: UIView.noIntrinsicMetric, height: measuredSize.height.rounded(.up) + 5)
}
}
But something's wrong. In this scene, some of the initial cells have some extra white space at the bottom. Moreover, if you scroll those cells out of view and then back into view, they look correct. And all the other cells look fine. That proves that what I'm doing is correct, so why isn't it working for the initial cells?
Well, I've done some heavy logging, and I've discovered that at the time intrinsicContentSize is called initially for the visible cells, the StringDrawer does not yet correctly know its own final width, the width that it will have after autolayout. We are being called too soon. The width we are using is too narrow, so the height we are returning is too tall.
Scene 3: StringDrawer with workaround
In the third scene, I've added a workaround for the problem we discovered in the second scene. It works great! But it's horribly kludgy. Basically, in the view controller, I wait until the view hierarchy has been assembled, and then I force the table view to do another round of layout by calling beginUpdates and endUpdates.
var didInitialLayout = false
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if !didInitialLayout {
didInitialLayout = true
UIView.performWithoutAnimation {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
}
The Mystery
Okay, so here are my questions:
(1) Is there a better, less kludgy workaround?
(2) Why do we need this workaround at all? In particular, why do we have this problem with my StringDrawer but not with a UILabel? Clearly, a UIlabel does know its own width early enough for it to give its own content size correctly on the first pass when it is interrogated by the layout system. Why is my StringDrawer different from that? Why does it need this extra layout pass?
I am able to present the PDFThumnailView of my pdf document. The problem is it is skipping pages. It shows pages 1,3,5 etc...and not displaying the pages in between. My code is below.
#IBOutlet weak var pdfView: PDFView!
#IBOutlet weak var pdfThumbnailView: PDFThumbnailView!
func setupThumbnailView() {
pdfThumbnailView.pdfView = pdfView
pdfThumbnailView.thumbnailSize = CGSize.init(width: thumbnailDimension, height: thumbnailDimension)
pdfThumbnailView.backgroundColor = sidebarBackgroundColor
}
I might be wrong but I think this is the way that PDFThumbnailView works. It fits as many thumbnails as it can within the View width by not displaying some intermediate thumbnails. It does show them if you touch over the thumbnails. Make the width bigger or the thumbnails smaller and it shows more.
You have to make pdfThumbnailView bigger. However then it can be too big to fit on the screen, so let’s put it in a scroll view.
First make change the constraints on the pdfThumbnailView to make it wide enough to accomodate all the pages.
NSLayoutConstraint.activate([
pdfThumbnailView.heightAnchor.constraint(equalToConstant: CGFloat(thumbnailSize)),
pdfThumbnailView.widthAnchor.constraint(equalToConstant: CGFloat(pdfDocument.pageCount*thumbnailSize))
])
Next create the scroll view and add the thumbnail view as its only subview.
var pdfThumbnailScrollView = UIScrollView()
pdfThumbnailScrollView.translatesAutoresizingMaskIntoConstraints = false
pdfThumbnailScrollView.addSubview(pdfThumbnailView)
Finally add some constraints so the scroll view and the thumbnail view know how to lay themselves out. The scroll view only has one subview, so let's constrain it to fit.
NSLayoutConstraint.activate([
pdfThumbnailView.leadingAnchor.constraint(equalTo: pdfThumbnailScrollView.leadingAnchor),
pdfThumbnailView.trailingAnchor.constraint(equalTo: pdfThumbnailScrollView.trailingAnchor),
pdfThumbnailView.topAnchor.constraint(equalTo: pdfThumbnailScrollView.topAnchor),
pdfThumbnailView.bottomAnchor.constraint(equalTo: pdfThumbnailScrollView.bottomAnchor)
])
You may follow this tutorial more details.
I have tableView cell with different content(views, labels, imageViews) in one cell. But in something cells content can be not full. How can i use resizing cells without removing and adding always constraints? Thanks.
One of possible solutions for this problem:
Add constraints for hidden state with priority 1000
Add extra constraints for resized state with lower priority (ex 750)
Save constraints that is ONLY for hidden state into IBOutlet collection
Save constraints that is ONLY for resized state into another IBOutlet collection
Code:
#IBOutlet var hiddenConstraints: [NSLayoutConstraint] = []
#IBOutlet var visibleConstraints: [NSLayoutConstraint] = []
func hide(_ hide: Bool) {
for hiddenConstraint in self.hiddenConstraints {
hiddenConstraint.isActive = hide
}
for visibleConstraint in self.visibleConstraints {
visibleConstraint.isActive = !hide
}
self.layoutIfNeeded()
}
There is faster solution:
Move content that can be hidden into container view
Set height constraint for container view
Change from code height constraint constant to 0 if hidden or to proper height if visible
Code:
#IBOutlet var heightConstraint: NSLayoutConstraint!
func hide(_ hide: Bool) {
self. heightConstraint.constant = hide ? 0 : 150 //Estimated height
self.layoutIfNeeded()
}
This is not a good approach, as it will lead to constraint crashes at runtime. So I prefer to use first one.
Also you will need to update your cell from table to move other cells up or down.
Ray Wenderlich has a fantastic tutorial on dynamic sizing of table cells that can be found here:
https://www.raywenderlich.com/129059/self-sizing-table-view-cells
TL;DR You need to make sure your cell's content is pinned on all four sides to the cell's content view, as well as setting as high priority vertical hugging, greater than or equal to height constraint on your label.
I want to create a spreadsheet style table with stackviews. However as you can see in the image, columns are not aligned due to previous items being wider. How do ı make sure they are aligned ?
This is how I create stacks:
class MyTableFullScreenViewController: UIViewController {
var json: JSON?
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var verticalStack: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<json!["Rows"].arrayValue.count{
let horizontalStack = UIStackView()
horizontalStack.axis = .Horizontal
horizontalStack.spacing = 16
horizontalStack.distribution = .Fill
for j in 0..<json!["Rows"][i]["Column"].arrayValue.count{
let innerJson = json!["Rows"][i]["Column"][j]
switch innerJson["InputType"].intValue{
case 1:
Inputs.Input1(innerJson, parent: horizontalStack, isRepeatable: true)
case 11:
Inputs.Input11(innerJson, parent: horizontalStack, isRepeatable: true)
case 15:
Inputs.Input15(innerJson, parent: horizontalStack, isRepeatable: true)
default:
break
}
}
verticalStack.addArrangedSubview(horizontalStack)
}
}
}
This is how storyboard is done:
You should be using a UICollectionView as opposed to UIStackView for this... Think of UIStackView as a wrapper around AutoLayout simplifying the laying out of a collection of views within a predefined space as opposed a scrollable container that's able to handle the layout of a virtually infinite number of views.
If you're creating a spreadsheet, you'll likely want the ability to scroll both horizontally and vertically, but you'd only be able to scroll vertically based on the sample code you posted (unless you 'hack' UIStackView but at that point you might as well use a UICollectionView).
What I'd recommend is to create a UITableView containing a horizontally scrolling UICollectionView in each cell. Alternatively, depending on your needs, you could also nest the UICollectionViews within a vertically scrolling UICollectionView.
Containing UICollectionViews/UITableViews within other UICollectionViews/UITableViews is actually very performant and frequently used in scenarios like the one you described.
I have a label in a View Controller scene inside a root view.
I set the constraint of the height to 30 using the Pins button right below.
Everything looks pretty in the storyboard.
But when I want to get the size of the element inside my code I always get the value 21. I tried refLabel.bounds.size.height and refLabel.frame.height. I call this in the override func viewDidLoad(){
super.viewDidLoad() function of the corresponding class.
Has this something to do with the intrinsic content size?
Try this code
refLabel.layoutIfNeeded()
let height = refLabel.frame.size.height
print("right height is \(height)")