UIWebView in UIScrollView with autolayout - ios

I am having lots of trouble with this setup. So basically I am displaying some labels with variable height then a button and at the end of the view i need a WebView to display some HTML formatted text.
I resize the web view height constraint when the content is loaded as follows.
func webViewDidFinishLoad(webView: UIWebView) {
nContentViewHeight.constant = webView.scrollView.contentSize.height
}
When the view is first loaded, this works perfectly. The scrolling is good and all the web view content is visible.
But when I rotate the device I don't know how to properly resize the web view. I tried loading the content again in
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation)
so that webViewDidFinishLoad would trigger again and resize the web view. But that doesn't work at all because the contentSize of the scrollview inside the web view doesn't change.
Not knowing why I attempted very ugly solution and that is this:
func fitWebAndScrollView(){
let newRect = CGRectMake(nContentWebView.frame.minX, nContentWebView.frame.minY, self.view.frame.width, 10)
let newWebView = UIWebView(frame: newRect)
newWebView.delegate = self
newWebView.tag = -12
newWebView.scrollView.scrollEnabled = false
self.view.addSubview(newWebView)
newWebView.loadHTMLString(contentHtml, baseURL: nil)
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
fitWebAndScrollView()
}
func webViewDidFinishLoad(webView: UIWebView) {
nContentViewHeight.constant = webView.scrollView.contentSize.height
if webView.tag == -12 {
webView.removeFromSuperview()
nContentViewHeight.constant *= 1.1 // longer the content is more of it is clipped
}
}
And this sort of works, but in some instance the bottom of the WebView content is clipped as if the inner scrollview content size is calculated incorrectly.
Has anybody dealt with this before? I always assumed that this sort of thing wasn't an extra special use case.
Thank you for your ideas.

Aright so I managed to solve it this way
func fitWebAndScrollView(){
if let strH = nContentWebView.stringByEvaluatingJavaScriptFromString("document.getElementById(\"cnt\").offsetHeight;"),
numH = NSNumberFormatter().numberFromString(strH){
nContentViewHeight.constant = CGFloat(numH)+20
}
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
fitWebAndScrollView()
}
func webViewDidFinishLoad(webView: UIWebView) {
fitWebAndScrollView()
}
The problem was actually with the HTML code I was using as a wrapper.
let htmlWrapperString = "<html><head>\n<style type=\"text/css\">body {font-family: \"%#\"; font-size: %#;-webkit-text-size-adjust: none;}</style></head><body><div id=\"cnt\">%#</div></body></html>"
So before I didn't have that wrapping div in the body and when I queried the height of body tag it always returned the whole view port size and not just the text height that was in body.
Now it works reliably.

First of all, if the code is really as shown, you are assigning the width of the content to the height constant. This would definitely cause the issue. (Apparently it is not as shown; please show actual code instead of typing in new "similar" code into the question…)
Second, it is not a very reliable way to detect the content height by only doing it in webViewDidFinishLoad. On some sites it may take a long time before this is actually called but the content may still be usable, and of course any action by the user in the browser may change it, and many websites append more content to the end of the page on the fly (e.g., as the user scrolls down).
Also, I hope you realize that scrollView.contentSize is the size of the content itself (not the visible part), and its height won't change on rotation unless the width is changed in a way that makes the content layout change.
Overall I think you may be trying to do something that UIWebView is simply not suited for. You should only autolayout the size of the webview itself, not have it inside an external UIScrollView (if I read the title correctly and that's what you are doing). Its inner scrollView will then handle the content scrolling for you automatically, instead of conflicting with your external scrollView. You can set yourself up as the delegate of the inner scrollview if you need to react to events from it, etc.
To resize the UIWebView itself on rotation, set up constraints to bind the distance from each of its edges to the surrounding views (or use the simpler autoresizingMask). There should be no need to do anything in didRotateFromInterfaceOrientation (and if you do need manual layout, do it in viewWillLayoutSubviews and/or viewDidLayoutSubviews).

Related

A mystery about iOS autolayout with table views and self-sizing table view cells

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?

How to dynamically size UIScrollView?

I'm adding content to the UIScrollView dynamically. The content size of the scroll view is increasing. How can I change the content size to fit the dynamically added content? See the code I'm using that is not working
extension UIScrollView {
func updateContentView() {
contentSize.height = subviews.sorted(by: { $0.frame.maxY < $1.frame.maxY }).last?.frame.maxY ?? contentSize.height
contentSize.height += 300
}
}
refer this question
I hope the above solves your problem but I'd prefer using a UITableView or a UICollectionView in place of this because everything is handled so smoothly in them. You just have to give the number of rows(return a variable which changes dynamically). That's it, your contentSize is adjusted internally.
But again if you have a different requirement please go with the reference link!

Prevent UITextView from offsetting its text container

I am tying to modify the height of a UITextView dynamically (up to a max height) while the user enters text. I am experiencing a very strange behavior when there are an even number of lines in the text view.
I am using autolayout and the text view has a height constraint. I respond to calls to the text view's delegate (textViewDidChange(_:)), where I calculate and adjust the height constraint based on the contentSize.
Here is the code:
func textViewDidChange(_ textView: UITextView) {
let newHeight = textView.contentSize.height
let newConstraintConst = max(MinTextViewHeight, min(MaxTextViewHeight, newHeight))
self.textViewHeightConstraint.constant = newConstraintConst
}
This works well, it resizes the frame up to MaxTextViewHeight and then the text view can scroll. However, when there are an even number of lines in the text view, the text view adds a kind of offset to the bottom of its NSTextContainer, causing the top line to be cut off:
However, when there are odd lines the NSTextContainer is no longer offset:
At first I thought it was somehow being controlled by the text view's textContainerInset but that is only used to pad the space inside the NSTextContainer, as setting it to .zero removes the space inside but does not affect the offset (and incidentally makes it even worse, as the top line almost completely disappears):
I have looked through the UITextView class reference and I don't see any property that would let me manipulate or even get the value of this offset.
As a workaround I am increasing the text container's top inset and removing the bottom inset:
textView.textContainerInset = UIEdgeInsetsMake(10, 0, 0, 0)
This works so far, but I arrived at a value of 10 by trial-and-error, and so far I've only tested it on a single device.
I am not interested in more hacky workarounds that require fragile, fixed values; I am trying to understand how this offset is being set and a proper way to fix it. I'm hoping that someone can provide some insight, thanks!
Just a speculation, but I think the problem is that the text view assumes that the height of itself does not change while calling textViewDidChange, so it scrolls when it thinks it has to, regardless of you changing its frame.
Not sure if you think my solution is too hacky, but this will stop it from scrolling when you don't want it. I simply pin the content offset to the top as long as the wanted content size is smaller than your max size.
Just add this:
func scrollViewDidScroll(scrollView: UIScrollView) {
if textView.contentSize.height <= MaxTextViewHeight && textView.contentOffset.y > 0.0 {
textView.contentOffset.y = 0.0;
}
}

Disable scrolling in a UITextView while avoiding both bouncing to top and invisibility of text at the present location for ios

I've tried a lot of things but couldn't come up with a solution. Any words of thought can help me evaluate on this. I made a full view DrawBoard class which is inherited from a UITextView class.When the switch on the view controller is on the user can type and scroll, when it is off the user can draw on the Drawboard.
#IBAction func changeSwitch(sender: UISwitch) {
if sender.on{
drawBoard.setNeedsDisplay()
drawBoard.scrollEnabled = true
drawBoard.editable = true
drawBoard.selectable = true
drawBoard.switchBool = false
}else if !sender.on {
drawBoard.switchBool=true
let a:CGPoint = drawBoard.contentOffset
drawBoard.scrollEnabled = false
drawBoard.setContentOffset(a, animated: false)
drawBoard.editable = false
drawBoard.selectable=false
}
}
It updates the scrolling but scrollEnabled= false just saves the text that is in the first page range of the textview so that it scrolls to the top automatically and disable the scrolling there.Then when I do the setContentOffset the drawable view of the background is visible and it draws on the right place of the textview. However the text that should be on top of it is not visible. This only happens when the switch button is set to off while I am out of the first page's range.How do I also make the text at that range visible?
Sorry if it's a really easy question I'm new to programming and got stuck for a considerable amount of time for this.Thank you.
You have to update the content size of the UIScrollView.
Like this:
scrollView.contentSize = CGSize(width: 100, height: 100)
"Content Size" determines the size of scrollable content, if it's not large enough to cover new views, you won't be able to see it in your scroll view.

iOS - Adaptive layout (Size classes) Fetch Data as early as possible, one time and place a UIView in viewcontrollers views center

Im using Auto layout with Size classes enabled.
I want to fetch data as soon as possible and it works fine doing this in:
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
}
The problem is that the frame is not yet calculated and gives a frame of i.e 600x600 which is the canvas size in storyboard. This means that if I want to place a custom loader in the center by using:
self.loader.center = self.view.center
This will make the loader be place someplace far away from the center.
I can fix the loader placement by calling fetchData in:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
fetchData()
}
But this makes it fetch more than only one time.
So my question is: When should I fetchData(), I want it to happen as early as possible. Only one time, and I want to loader to be centered.
Thanks
Edit: Disabling Auto Layout or Size classes is not an option.
Use this,
self.loader.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/2);
SWIFT:
self.loader.center = CGPoint(x: UIScreen.mainScreen().bounds.width/2, y: UIScreen.mainScreen().bounds.height/2)
or you can try fetching your data from viewWillLayoutSubviews method
Answering my own question:
Use:
self.view.setNeedsLayout()
to force the view to draw itself and setup the constraints.

Resources