The goal: Have a scroll view that displays an array of uiimageviews (photos) that you can horizontally scroll through them
How I understand to do this: Make the frame (CGRect) of each uiimageview the height and width of the scroll view, the y value to 0 on each, and set the first imgViews x value to 0. For every imgView after that, add the width of the scrollview to the x value. In theory, this would line the imgViews (Photos) up next to each other horizontally and not allow for any vertical scrolling or zooming, purely a horizontal photo viewer.
The storyboard setup: I am creating my scrollview in a xib file (It’s a custom uiCollectionViewCell), with these constraints:
— Top space to cell (0)
— Trailing space to cell (0)
— Leading space to cell (0)
— Height of 400
— Bottom space to a view (0)
—— (See Below for img)
Laying out the UIImgViews:
func layoutScrollView() {
for (index, img) in currentImages.enumerate() {
let imgView = UIImageView(frame: CGRect(x: CGFloat(index) * scrollView.bounds.width, y: CGFloat(0), width: scrollView.bounds.width, height: scrollView.bounds.height))
imgView.contentMode = .ScaleAspectFill
imgView.image = img
scrollView.addSubview(imgView)
scrollView.contentSize = CGSize(width: imgView.frame.width * CGFloat(index), height: scrollView.bounds.height)
scrollView.setNeedsLayout()
}
}
My suspicion: I suspect the issue is stemming from the auto layout constraints i’ve specified, but (considering Im asking a SO question) not sure
If there is a better way to do this (really the correct way) please let me know! I have been trying to wrap my head around this for a few days now.
I appreciate all responses! Thanks for reading
EDIT #1
I tried paulvs approach of setting setNeedsLayout & layoutIfNeeded before the "for" loop, and still no luck. Here is (out of three images selected) the second photo displaying. It seems that both the first and second photos are way longer than the content view and that would move the middle view over (Squished).
Your code looks fine except for a few details (that may be causing the problem):
Add:
view.setNeedsLayout()
view.layoutIfNeeded()
before accessing the scrollView's frame (a good place would be before the for-loop).
This is because when using Autolayout, if you access a view's frame before the layout engine has performed a pass, you will get incorrect frames sizes/positions.
Remove these lines from inside the for-loop:
scrollView.contentSize = CGSize(width: imgView.frame.width * CGFloat(index), height: scrollView.bounds.height)
scrollView.setNeedsLayout()
and place this line after (outside) the for loop:
scrollView.contentSize = CGSize(width: imgView.frame.width * CGFloat(currentImages.count), height: scrollView.bounds.height)
Related
This question is about supporting a variable-height, custom text view using constraints and the view's intrinsicContentSize for autolayout. Before you click that 'duplicate' button, hear me out.
I have a custom text view (from scratch, inherits from NSView). It supports many of the usual NSTextView features, the most relevant here being multiple lines and laying out its content based on width available. The app it's built for loads a couple of these text views into each row of a table view. The issue is that the height doesn't get set properly according to its intrinsicContentSize.
I created a sample project that simplifies the problem. It uses a "pseudo" text view with a fixed number and size of characters used to determine width/height required. In the sample, there is a table view of one column whose cell view has only one subview, a PseudoTextView. The text view is pinned to the edges of its cell view with a little padding. How can I get the system to recognize that the text view should abide by the constraints that define the width while allowing the text view to grow in height, wrapped tightly by the cell view? Here's the text view code:
class PseudoTextView: NSView {
#IBInspectable var characterCount: Int = 0
#IBInspectable var characterWidth: CGFloat = 5
#IBInspectable var characterHeight: CGFloat = 8
#IBInspectable var background: NSColor = .blue {
didSet {
layer?.backgroundColor = background.cgColor
}
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
wantsLayer = true
layer?.backgroundColor = background.cgColor
}
override var intrinsicContentSize: NSSize {
let requiredWidth = characterWidth * CGFloat(characterCount)
let lineCount = (requiredWidth / frame.width).rounded(.up)
let usedHeight = lineCount * characterHeight
let charactersPerLine = (frame.width / characterWidth).rounded(.down)
let usedWidth = charactersPerLine * characterWidth
return NSSize(width: usedWidth, height: usedHeight)
}
This version returns the appropriate size based on the frame of the view. This obviously doesn't work because it's accessed during the updateConstraints phase of layout when the frame hasn't been set. I've also tried using NSView.noIntrinsicMetric for the width, but this will drive the text view to zero width and the height never recovers. There are an enormous number of other attempts I've made, but I won't bore you with them all.
NSTextField does something different (assuming 'Editable' is off, 'Wrap' is on). It's intrinsicContentSize reports the full width of the text on a single line (even if it's much longer than the width available), but it is somehow resized to the correct width. Once resized, the intrinsicContentWidth then still reports the full single-line width, but the height is adjusted to account for multiple lines. There is some magic somewhere I haven't been able to divine.
I've read every line of related documentation. If there's a blog post on the topic, I've probably read it. If there's a question on SO on the topic, I've probably read it. If you wrote a book on the topic, I've probably bought it. All of these sources tease at the problem I'm having, but none of them answer the question of how to handle this particular situation. Desperate.
Update:
After reading an old blog post by Jonathon Mah (http://devetc.org/code/2014/07/07/auto-layout-and-views-that-wrap.html) I created an example that uses his approach. Here's another project that mimics his technique and works correctly. This is the top portion of the app. It's a fixed container view that's adjusted with a slider. The patchwork are the pseudo characters of the custom view whose background is the pink color.
However, when inserted into a self-sizing table view, the custom view correctly matches the width of its cell, but the cell is not adjusted to respect the intrinsic height. If you change the custom view's bottom constraint to be optional (say, with a >= relation) the custom view does shrink to the correct height, but the cell view remains fixed. How do I convince the cell view to shrink its height to respect the intrinsicContentSize.height of its subview?
I have a solution for your problem, although it may not be optimal, since I do not have too much experience with macos specifics. So, first of all, let's define that the table view should use automatic row heights:
override func awakeFromNib() {
super.awakeFromNib()
tableView.usesAutomaticRowHeights = true
}
In your second sample the tableView outlet was not not connected to TableViewController, but it probably should be, so do not forget to connect it.
Now, in your WrappingCellView, you override layout(), but the value that you set for preferredMaxLayoutWidth is incorrect. I think it should be the width of the superview:
override func layout() {
// 16 is used for the sake of example
wrappingView.preferredMaxLayoutWidth = (superview?.frame.width ?? 16) - 16
super.layout()
}
Next part is the one I am not sure about:
func tableViewColumnDidResize(_ notification: Notification) {
tableView.reloadData()
}
There should be a better API to recalculate row heights. I hope you or someone else can suggest something :)
These three adjustments result in proper recalculation of the cell height:
I've spent several hours googling and in the debugger and I cannot figure out why my parent view in this xib is changing at runtime.
I have created a simple Xib:
In the container I set the width and height constraints (I tried setting constraints in the top parent but I can't seem to be able to):
At runtime I load the Xib programatically and I add it to a view. However after I add it to the view and set the position, the frame of the parent is smaller and the position is wrong.
Here I am explicitly set the x to 16, and the y to 400. When I look at it in the inspector debug tool however, I get different results than I want because the parent frame has changed and the Container position is wrong as a result. I turned the inspector to the side so you can see the parent (in blue) and the child container (in white) and how the parent is smaller than the child:
The details for the parent (the root xib view 'Item Detail Size Widget') are as follows. Notice the height is now 32 instead of 76:
The details for the top level child (Container) are as follows:
So the constraints I set for the container are being honored but the parent is resizing (I assumed since I couldn't set constraints it would use the frame I set).
I tried turning off and on translatesAutoResizingMaskIntoConstraints and a few other things but I can't seem to get the parent to be the exact same size as the Container.
Do you have any suggestions as to why the root Item Detail Size Widget will not match the size of the Container and is changing at run time?
Update
For reference here is the code where I add the widget:
let sizer: ItemDetailSizeWidget = .fromNib()
sizer.x = 16
sizer.y = 400
contentView.addSubview(sizer)
I have UIView extensions that set x and y as follows:
var x: CGFloat {
set { self.frame = CGRect(x: newValue,
y: self.y,
width: self.width,
height: self.height)
}
get { return self.frame.origin.x }
}
var y: CGFloat {
set { self.frame = CGRect(x: self.x,
y: newValue,
width: self.width,
height: self.height)
}
get { return self.frame.origin.y }
}
And here is the fromNib extension
class func fromNib<T: UIView>() -> T {
return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
}
Xcode always uses the autoresizing mask for a top-level view in a xib. You can see that your first screen shot: it has the autoresizing control shown.
Your top-level view's autoresizingMask is set to flexible width and height.
You have not set any width or height constraints between your top-level view and the “Container” subview.
You also have this code:
contentView.addSubview(sizer)
I suspect (since you mention contentView) that you're adding sizer to the view hierarchy of a table view cell or a collection view cell. When the cell is first created, it might not yet be at its final size. The collection view (or table view) might resize the cell after you return it from collectionView:cellForRowAtIndexPath:.
Since your sizer view has translatesAutoresizingMaskIntoConstraints set to true when it's loaded from the nib, and since its autoresizingMask is [.flexibleWidth, .flexibleHeight], this means that it will grow or shrink when its superview grows or shrinks.
To fix, try these steps:
Change the autoresizing mask to this:
Constrain the width of “Item Size Detail Widget” to equal the width of “Container”, and constrain the heights to equal also:
I noticed that this code doesn't quite work as expected on iOS 11, because the "adjustedContentInset" property value changes as the "navigationBar" shrinks during a scroll:
CGFloat contentInsetTop=[scrollView contentInset].top;
if (#available(iOS 11.0, *))
{
contentInsetTop=[scrollView adjustedContentInset].top;
}
////
[scrollView setContentOffset:CGPointMake(0, -contentInsetTop) animated:YES];
... For example, this might start out as 140, then reduce to 88 beyond a minimal scroll offset. This means if you call this, it doesn't actually scroll all the way to the top.
Aside from preserving the original offset in memory from when the UIScrollView loads, is there a way to recover this value later to ensure that it does indeed scroll to top consistently, no matter the "adjustedContentInset"?
Currently, there is indeed no way to do this with iOS 11, I have heard. The only way to do so is to capture the initial value and store it for the life of the navigation/view controller.
I will update my answer accordingly if I hear otherwise, but it will be broken in the base iOS 11 release forever unfortunately.
I had this same problem with a Large Title in iOS 11, and the following code worked for me.
The following code first scrolls the offset a reasonable size above where you want to be. The value -204.666666666667 was the tallest value from setting the Accessibility > Larger Text > Larger Accessibility Sizes to the highest. I'm sure this doesn't cover other possibilities, but it is working for me so far. -CGFloat.greatestFiniteMagnitude is otherwise too problematic.
tableView.setContentOffset(CGPoint(x: 0.0, y: -204.666666666667), animated: false)
This will now give you back the right adjusted content size. To avoid being scrolled too far higher, i.e. leaving white space, just use the value as follows.
var contentOffset = CGPoint.zero // Just setting a variable we can change as needed below, as per iOS version.
if #available(iOS 11, *) {
contentOffset = CGPoint(x: 0.0, y: -tableView.adjustedContentInset.top)
} else {
contentOffset = CGPoint(x: 0.0, y: -tableView.contentInset.top)
}
tableView.setContentOffset(contentOffset, animated: false)
In summary, set the offset higher first (-204.666666666667 in my case, or just -300 or whatever), then that readjusts the adjustedContentInset.top to include the Large Title, scroll bar, etc., then you can now set the content offset as needed.
I have a tableView with a tableHeaderView that is assigned through a nib file. This tableHeaderView contains a TextView (orange background) that is sized with a height constraint using sizeThatFits.
let textViewSize = titleView.sizeThatFits(CGSize(width: titleView.frame.size.width, height: CGFloat(MAXFLOAT)))
textViewHeightConstraint.constant = textViewSize.height
The constraints of this header view are setup to properly drive it’s height.
When the tableView sizes the frame for the tableHeaderView i am getting very strange results from the headerView.systemLayoutSizeFitting when scrolling in the UITextView is no enabled.
if let headerView = tableView.tableHeaderView {
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
if headerView.frame.height != height {
headerView.frame = frame
tableView.tableHeaderView = headerView
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
}}
If scrolling in the textView is enabled the tableHeaderView scales as expected, but the textView is cutting off the top line.
If scrolling for the textView is disabled (desired setting) then the height for this headerView is calculated to be larger than the size required. The length of text seems to multiply this effect.
I should also add that the TextView is being assigned attributed text with various paragraph and line spacing options.
Any suggestions as to what could be going on are much appreciated.
I have resolved my issue. I am still not sure why the above problems were occurring, but when I applied the header view in a storyboard (vs loading it in via a nib file and assigning it to tableView.tableHeaderView at runtime) all of my autolayout issues disappeared.
Still would like to know why it was mis-behaving and if there is a way to get my table header views to work through a xib files
i have a view inside a viewController, i wanted to start the smaller view outside the viewController in the left, and animate it to the centre when i press a button. so i made it like this:
override func viewDidLayoutSubviews() {
smallView.center = CGPointMake(smallView.center.x - 400, smallView.center.y)
}
And it works perfectly!, the problem is i have a text view inside that smaller view, and every time i start editing it it jumps outside of the main viewController right where it was, and i have to press the button again to bring it inside.
How to fix this?
PS: i tried positioning it to the centre when i start editing the text view like this:
func textViewDidBeginEditing(textView: UITextView) {
smallView.center = CGPointMake(smallView.center.x + 400, smallView.center.y)
}
But it doesn't work. and the method is connected to the textView properly(delegate)
PS2: i also have imagePickerController inside my viewController.
OK, as you're using Auto Layout.
The first rule of Auto Layout (you will see this in any Auto Layout book) is that you absolutely cannot touch the frame or center of a view directly.
// don't do these
myView.frame = CGRectMake(0, 0, 100, 100);
// ever
myView.center = CGPointMake(50, 50);
You can get the frame and center but you can never set the frame or center.
In order to move stuff around using Auto Layout you need to update the constraints.
For instance if I set up a view called myView and want it to grow and shrink in height I would do something like...
Set the top constraint to the superview at 0.
Set the left constraint to the superview at 0.
Set the right constraint to the superview at 0.
Set the height constraint to 50 (for example) and save it in a property called heightConstraint.
Now to animate the change in height I do this...
self.heightConstraint.constant = 100;
[UIView animateWithDuration:1.0
animations:^ {
[self.view layoutIfNeeded];
}];
This will then animate the height from 50 (where it was when I set it) to 100.
This is the same for ANY animation of views using Auto Layout. If (for instance) I had saved the left constraint I could change that and it would increase and decrease the gap from the left edge of the superview to myView.
There are several really good tutorials about AutoLayout on the Ray Wenderlich site. I'd suggest you take a look at them.
Whatever you do, I'd strongly suggest not just disabling Auto Layout. If you don't know how to use Auto Layout then you will very quickly fall behind with iOS 8 and the new device sizes.