How to let TextView be able to scroll horizontally - ios

I know the TextView is embedded in a ScrollView. And if there is a fairly long String(Which contains no "\n")
The TextView will automatically do the line-wrap, according to the width of the TextView.
If TextView's height is short, then we are able to scroll it vertically.
How do you disable the auto line-wrap? Such that, if there are no "\n" encounters, it does not line wrap. Rather, it lets the user scroll horizontally to view the text.
How can I implement this?

I figure out how to do this with many helps of you guys :D, thanks, and here we go!
1. So, firstly, We need a longlonglong String, right?
let displayStr = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 "
2. Assume we have a UIScrollView which is linked by #IBOutlet or got by calling the .viewWithTag(xxx) .We will let it be named as scroll :
3. It's time to get the size of our string, here is a key function we'll use it:
Oh! I almost forget to define what kind of Font( this's a crucial parameter ) we will use, and What is the max-size of our string's size
let maxSize = CGSizeMake(9999, 9999)
let font = UIFont(name: "Menlo", size: 16)!
//key function is coming!!!
let strSize = (displayStr as NSString).boundingRectWithSize(maxSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName : font], context: nil)
4. OK, now we can put the string into a textView, you need to programmatically create a new UITextView, so that we can define the frame which is identical to the string's size(Oh, maybe a little bigger :D) :
let frame = CGRectMake(0, 0, size.width+50, size.height+10)
let textView = UITextView(frame: frame)
textView.editable = false
textView.scrollEnabled = false//let textView becomes unScrollable
textView.font = font
textView.text = displayStr
5. Back to our scroll, we will set its contentSize:
scroll.contentSize = CGSizeMake(size.width, size.height)
6. Finally, addSubview:
scroll.addSubview(textView)
You can see, textView is embed in a scrollView, which allow it to
scroll with 2 directions.
B.T.W. My implement is just a demo for
static String. if you want user to use a textView which will not line
wrap if he doesn't input any "\n", you may need dynamically calculate
the string size. :D
[I hope this will help]

[myTextView setContentSize:CGSizeMake(width, myTextView.frame.size.height)];
The width of the content extends past the width of the textView's frame or else it won't scroll.
Turn off all the scroll options on the UITextView, then embed it in another UIScrollView. Reference:- DualScrollTextView

#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var textView: UITextView!
var yourText: String = "Whatever your text is going to be."
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.scrollView.layoutIfNeeded()
self.scrollView.contentSize = self.textView.bounds.size
}
override func viewDidLoad() {
super.viewDidLoad()
textView.text = yourText
textView.sizeThatFits(CGSizeMake(textView.frame.size.width, 80)) //height to equal the actual size of your text view.
}
I think this should plug and play, but I know for sure that textView.sizeThatFits works. You have to make sure that you constrain your text view to it's parent scrollview for it to work. (Leading, Trailing, Top, Bottom and the Height.)

The accepted answer didn't work with AutoLayout so I'll share my approach:
1. Add a UIScrollView with a UITextView inside it and pin all the edges for both of them
2. Add width and height constraints for your UITextView (doesn't matter what you set them to)
3. Create IBOutlets for the UITextView, the height constraint, and the width constraint
4. Uncheck 'Scrolling Enabled' on the TextView
5. When you update the text, calculate the bounding size of the text and update the height and width constraints with the bounding size
#IBOutlet weak var textView: UITextView!
#IBOutlet weak var textViewHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var textViewWidthConstraint: NSLayoutConstraint!
let displayText = "Your text here."
override func viewDidLoad() {
super.viewDidLoad()
let maxSize = CGSize(width: 10000, height: 30000)
let textRect = displayText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font : self.textView.font!], context: nil)
self.textView.text = displayText
self.textViewHeightConstraint.constant = textRect.height
self.textViewWidthConstraint.constant = textRect.width
}
UPDATE: I discovered that this approach uses a lot of memory for large amounts of text (I was seeing over 1GB used). Here's how I reduced the memory impact from 1GB to 100MB:
1. Set the edge constraints just like step 1 above.
2. Add a width constraint to the TextView for the scrollable content width (I used 2000 but you can adjust it to your liking, the wider you go the more memory you'll use though).
3. Add a constraint to make the TextView and ScrollView have equal heights
That's it! The scrollable width will be constant and once you set the text for the TextView the scrollable height will automatically adjust.
Note: Some caveats of this approach:
The scrollable width will always be the width you set but you can use a hybrid approach with the first solution if you want to make it sized based on the text
You can't see the vertical scroll bar (unless you're scrolled all the way to the right) but it's possible to get it back by adjusting the scroll bar inset of the TextView.
Reference: https://www.ralfebert.de/ios-examples/auto-layout/uiscrollview-storyboard/

Related

Layout for different iPhone screens

I'm building my first real-world app after going through some tutorials and I've come across a layout issue. It is quite simple to adjust UI layout to different screen size classes, but I haven't found any information on how to adjust layout within same size class.
For example, I have a label whose Top Space constraint is set to 40 pt form top of view. It looks neat on a large iPhone 8 Plus screen:
But on a smaller iPhone SE screen (which is confusingly of same size class) this constraint of 40 pt pushes the label halfway through to the center, leaving reasonably less useful space below it:
So I was wondering if there's a way to set different constraints for different iPhones: say, 40 pt for iPhone 8 Plus, 30 pt for iPhone 8 and 20 pt for iPhone SE. Same goes about positioning other views below the label: I want them more compact vertical-wise on a small iPhone screen, and having more space between them on large screen. I know this last part can be solved with a stack view, but it's not always convenient to use.
UPD. Here is a full layout of the view on 8 Plus screen:
It has 3 fixed constraints:
1. From 'Title' label to top of the view - 50 pt
2. From 'Percent' label to bottom of 'Title' label - 60 pt
3. From 'Details' label to bottom of the view - 80 pt.
I've used text autoshrink in all labels + height of each label is proportional to view's height. This made layout a bit more flexible, but still there's a noticible issue on small SE screen:
As you can see, 'Details' is squeezed to 'Percent' label. At this point it would be great to move 'Percent' label higher up and closer to 'Title', but unlike heights constraints cannot be set in proportion (not in IB at least) to Superview height.
One of the options I see is to put a blank view between top and mid labels, making its height proportional and setting 'Percent' label top constraint at 0 to this blank view. Not sure though using such a "crutch" is a good practice.
You may get your most satisfactory results by using a single UILabel and setting the Attributed Text, instead of trying to get multiple labels and font sizes to cooperate.
Try this:
Create a new View Controller
add a normal UILabel
set constraints to 85% of width and 80% of height, and centered both ways
connect the label to an IBOutlet
then:
class ScalingViewController: UIViewController {
#IBOutlet weak var theLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let titleFont = UIFont.systemFont(ofSize: 80.0, weight: UIFontWeightThin)
let pctFont = UIFont.systemFont(ofSize: 100.0, weight: UIFontWeightThin)
let paraFont = UIFont.systemFont(ofSize: 30.0, weight: UIFontWeightLight)
// for blank lines between Title and Percent and between Percent and Body
let blankLineFont = UIFont.systemFont(ofSize: 36.0, weight: UIFontWeightLight)
let sTitle = "Title"
let sPct = "78%"
let sBody = "A detailed text explaining the meaning of percentage above and what a person should do to make it lower or higher."
// create the Attributed String by combining Title, Percent and Body, plus blank lines
let attText = NSMutableAttributedString()
attText.append(NSMutableAttributedString(string: sTitle, attributes: [NSFontAttributeName: titleFont]))
attText.append(NSMutableAttributedString(string: "\n\n", attributes: [NSFontAttributeName: blankLineFont]))
attText.append(NSMutableAttributedString(string: sPct, attributes: [NSFontAttributeName: pctFont]))
attText.append(NSMutableAttributedString(string: "\n\n", attributes: [NSFontAttributeName: blankLineFont]))
attText.append(NSMutableAttributedString(string: sBody, attributes: [NSFontAttributeName: paraFont]))
// these properties can be set in Interface Builder... or set them here to make sure.
theLabel.textAlignment = .center
theLabel.numberOfLines = 0
theLabel.adjustsFontSizeToFitWidth = true
theLabel.minimumScaleFactor = 0.05
// set the label content
theLabel.attributedText = attText
}
}
This gives me these results for 7+, 6s and SE:
And, just for demonstration's sake, how it looks with additional text in the "body" paragraph:

Setting UITextView height to a variable number of lines

I have a a UITextView populated with a paragraph of text with variable height at runtime.
At runtime, how do I adjust the height of UITextView to the height of a variable number of lines (not necessarily equal to the number lines in the paragraph of text)?
For example:
Set UITextView to the height of 2 lines of text for some cases of text, then
Set UITextView to the height of 5 lines of text for other cases of text (with a scroll-bar to scroll for the other lines as is default behavior)
The analogue in CSS would be to set the view height to 2em if the text is "small", or to 5em if the text is "large", or in Android setting maxLines on a TextView at runtime.
you can use autolayout. first you create a height constraint for your UITextView. then you outlet this constraint to your controller. then you can manipulate height from controller anytime you like.
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
heightConstraint.constant = 200 // or something else
}

Using UIStackView with a UITextView with dynamic height

I just found out about UIStackView, and I'm trying to see if it can finally make it easier to lay out expanding content.
Specifically, I want to do the following:
|-- UITextView ------------|
| some dynamic text here, | I want the text view's height to change
| it could be short, or it | depending on the height of the text
| could be tall | (set later, in code)
|-- UIView ----------------|
|xxxxxxxxxxxxxxxxxxxxxxxxxx| I want this view to expand to fill the
|xxxxxxxxxxxxxxxxxxxxxxxxxx| vertical space.
|xxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxx| It should start right below the
|xxxxxxxxxxxxxxxxxxxxxxxxxx| bottom of the UITextView
|xxxxxxxxxxxxxxxxxxxxxxxxxx|
|--------------------------|
I think I know how to do this with regular constraints. Is it possible to do with UIStackView? I might be understanding the purpose of UIStackView, but I would love to be able to create layouts like this more easily.
When I put both of these into a UIStackView, Interface builder says "Needs constraints for: Y position or height" for both views.
Programmatically it looks like this:
#IBOutlet private var stackView: UIStackView!
{
didSet {
stackView.addArrangedSubview(titleTextView)
stackView.addArrangedSubview(UIView())
}
}
lazy var titleTextView: UITextView =
{
let textView = UITextView()
textView.isScrollEnabled = false
return textView
}()
You don’t need anything else for the UITextView to expand automatically. This shouldn’t complain about ambiguities.
You can add a stackview then inside that add a textview, then set scrollable false, it will automaticaly handle your problem
Calculate the text height in textview add a height constraint on uitextview say 20 and when you calculate the height just change the height constraint.Give the below view constraint of top 0 from textview and you issue will be resolved.
Calculate height using this function
let height = string.boundingRectWithSize(CGSizeMake(CGFloat.max,textviewwidth), options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: font!], context: nil).size.height
Try using an UILabel with lines = 0 instead of an UITextView.

Dynamic height UITableViewCell

I have cells that consist of a title, date and a number of hashtags.
Here's the storyboard's screenshot:
Custom cell in storyboard
I've set the following in my ViewDidLoad:
tableView.rowHeight = UITableViewAutomaticDimension
But haven't given any estimatedHeight for the tableView which I'll explain why.
Here's my customCell:
#IBOutlet weak var title: UILabel!
#IBOutlet weak var date: UILabel!
#IBOutlet weak var hashtagsView: UIView!
var item: Item? {
didSet {
configureCell()
}
}
func configureCell() {
if item = item {
title.text = item!.title
date.text = item!.date
// The part where I calculate the sizes of hashtags to fit them in hashtagsView
let totalWidth = CGRectGetWidth(hashtagsView.frame)
print("TotalWidth: \(totalWidth)")
.
.
.
.
print("Content: \(self.contentView.frame)")
print("HashtagsView: \(self.hashtagsView.frame)")
}
}
Here's the results:
With tableView.estimatedHeight = 150
TotalWidth: 240.0
Content: (0.0, 0.0, 240.0, 119.666666666667)
HashtagsView: (0.0, 0.0, 240.0, 128.0)
Without estimatedHeight
TotalWidth: 240.0
Content: (0.0, 0.0, 414.0, 43.6666666666667)
HashtagsView: (0.0, 0.0, 240.0, 128.0)
Recap
When there is an estimatedHeight, cell's contentView doesn't print a correct width, but displays the cell's contents well like nothing's wrong (except for the hashtagsView).
When there's not an estimatedHeight, cell's contentView does actually print the correct width, which lets me calculate the hashtags frames, but the cells display with their default 44 height.
Detailed Info
Since I don't know how many hashtags there is, I'm trying to use the blank UIView to add the UIButtons programmatically. And for calculation purposes of the hashtags' buttons, I need to have the "width" of the cell's contentView, or the hashtagsView's, but when I set tableView.estimatedHeight, cell's width will be some arbitrary number (e.g. 240 in 6s Plus Simulator). And I just can't get the hashtagsView's width, even though I have no auto-layout issues.
And when I don't give tableView.estimatedHeight an estimation, I get the following:
Custom cells without estimatedHeight
Updated - An update asked by #EarlGray in the comments
The hashtags are actually UIButtons I add them to the hashtagsView dynamically. I need to stretch the hashtagsView's height so it'll fit more than one line of hashtags.
I think I'll either need to subclass UIView and override layoutSubviews() to achieve fit vertical layout or add constraints to each subview (UIButtons) programmatically.
Doing what #VinodVishwanath said, setting estimatedRowHeight combined with explicit heights and vertical space constraints gives me this:
Which unables me to get the width of the hashtagsView. During the calculations of the UIBUttons' frames, I need the cell.contentView's width, but somehow, setting the estimatedRowHeight gives me the following coordinates for the cell.contentView.frame
Content: (0.0, 0.0, 240.0, 119.666666666667)
Which is incorrect, because it has to give 414, and that's why my hashtags start from half of the screen.
Commenting out tableView.estimatedRowHeight gives me the correct coordinates:
Content: (0.0, 0.0, 414.0, 43.6666666666667)
But messes my tableView like so:
Update #2 - Here's my constraints for the cell.contentView
ContentView's constraints
Update #3 - A breakpoint on my configureCell method
HashtagsView's superview returns nil!!
I don't get it, my UIView IBOutlet is connected, I double checked.
All of the contentView.subviews have incorrect frames. So does the superview-less hashtagsView.
But when I remove estimatedRowHeight, it suddenly considers hashtagsView as a subview of cell's contentView. Except for contentView, it's subviews frames' still return incorrect and negative values.
When you don't provide the estimated row-height, the cell height defaults to 44, and that's why you're getting a content height of 43.667.
You need to provide the estimated row height, which will be the height when there are no tags in the field provided. Then all that's needed to set the correct height dynamically is the right set of autolayout constraints to provide the contentView's intrinsic size.
This will happen when you set a vertical space constraint between all the subviews of contentView, including a topSpaceToSuperview and bottomSpaceToSuperview to enable the AutoLayout engine to calculate the intrinsic content size.
Edit
I have analysed your constraints from the screenshot, and here are the relevant constraints you have added to calculate the cell height:
Title.top = topMargin + 8
bottomMargin = Share Button.bottom
Link.centerY = Share Button.centerY
Hashtags View.top = Title.bottom
Date.top = Hashtags View.bottom + 15
Now here's a visual representation of these constraints:
TopMargin
|
Title
|
Hashtags View
|
Date
!!! broken link !!!
Link — Share Button
|
BottomMargin
The broken link prevents the Layout Engine from calculating the cell height. You need to fix the broken link in the vertical layout, for instance, by setting Date.centerY = shareButton.centerY, or by setting Date.top = HashtagsView.bottom.

How to easily collapse vertical space around label when its text is nil?

Suppose I have three labels that are laid out below each other in a column. The uppermost label's top edge is pinned to the superview's top edge. All following labels' top edges are pinned to the preceding label's bottom edge. The leading and trailing edges of all labels are pinned to the leading and trailing edge of the superview. Here's what it looks like in Interface Builder (I added a blue background on every label to visualize its extent).
In the simulator the result looks like this.
All labels are connected to outlets in a view controller.
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
#IBOutlet weak var label3: UILabel!
When I set the text of label2 to nil
label2.text = nil
the label itself collapses.
However, the top and bottom spaces around the label do not collapse. This is evident by the fact that there is no blue background on the middle label in the last screenshot. As a result, the space between label1 and label3 is double the space of the layout in the first screenshot.
My question is - on iOS8 - what is the easiest way to collapse either the middle label's top or bottom space so that the two remaining labels still use the vertical spacing defined in the original layout? To be clear, this is the result I want to achieve.
Options I've found so far:
Bottom/Top Spacing Constraint Outlet
Define an outlet for the middle label's top or bottom spacing constraint.
#IBOutlet weak var spacingConstraint: NSLayoutConstraint!
Store the constraint's initial constant into a variable (e.g. in awakeFromNib or viewDidLoad).
private var initialSpacing: CGFloat!
override func viewDidLoad() {
initialSpacing = spacingConstraint.constant
...
Set the constraint's constant to zero whenever the text is set to nil or back to its initial value when the text is not nil.
spacingConstraint.constant = label2.text == nil ? 0 : initialSpacing
This approach feels a bit clumsy since it requires two additional variables.
Height Constraint Outlet
Set the vertical spacing around the middle label to zero and increase its height by the same amount. Define an outlet for the height constraint and proceed as above, setting the height to zero when the text is nil and back to it's initial value when the height is not nil.
This is still as clumsy as the previous approach. In addition, you have to hardcode the spacing and cannot use the built-in default spacings (blank fields in Interface builder).
UIStackView
This is not an option since UIStackView is only available on iOS 9 and above.
I'm using this UIView category for this purpose.
It extends UIView by adding two more property named fd_collapsed and fd_collapsibleConstraints using objective-c runtime framework. You simply drag constraints that you want to be disabled when fd_collapsed property set to YES. Behind the scene, it captures the initial value of these constraints, then set to zero whenever fd_collapsed is YES. Reset to initial values when fd_collapsed is NO.
There is also another property called fd_autocollapsed
Not every view needs to add a width or height constraint, views like UILabel, UIImageView have their Intrinsic content size when they have content in it. For these views, we provide a Auto collapse property, when its content is gone, selected constraints will collapse automatically.
This property automatically sets fd_collapsed property to YES whenever specified view has content to display.
It's really simple to use. It's kinda shame that there is no builtin solution like that.
Your solutions are good enough for me and I'd do Bottom/Top Spacing Constraint Outlet solution but since you want something different. You can use this third party: https://github.com/orta/ORStackView It has iOS7+ support and do exactly what you need.
This is low-key a pain all perfectionist devs learn about when trying to stack a bunch of labels. Solutions can get too verbose, annoying to folow, and really annoying to implement (ie. keeping a reference to the top constraint... gets annoying once you do it multiple times, or just change the order of the labels)
Hopefully my code below puts an end to this:
class MyLabel: UILabel {
var topPadding: CGFloat = 0
override func drawText(in rect: CGRect) {
var newRect = rect
newRect.origin.y += topPadding/2
super.drawText(in: newRect)
}
override var intrinsicContentSize: CGSize {
var newIntrisicSize = super.intrinsicContentSize
guard newIntrisicSize != .zero else {
return .zero
}
newIntrisicSize.height += topPadding
return newIntrisicSize
}
}
Usage:
let label = MyLabel()
label.topPadding = 10
// then use autolayout to stack your labels with 0 offset
Granted, its only for top padding, but that should be the only thing you need to layout your labels properly. It works great with or without autolayout. Also its a big plus not needing to do any extra mental gymnastics just to do something so simple. Enjoy!

Resources