UITableview content size gets wrong while using estimated height - ios

I am getting empty space while I am using the Table view content size. I referred this like https://forums.developer.apple.com/thread/81895. But it did not work for me. When I call to get the table content size , it gives wrong size for example, if table view content size was 100 means then it gives me 140 or 80 like...etc. So, If anyone knows how to handle OR how to get correct table view content size ??

use the below code once
override func viewDidLoad() {
super.viewDidLoad()
yourTableView.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 = yourTableView.contentSize.height
print(contentHeight)
}
}
}

Related

Find out the moment UIView entered (scrolled into) the screen

UIView is a view of a MyViewController A
A is initialized and its view is added as a subView of a scroll view,
which is on B's view like this:
View Controller B
B View
Scroll View
Image View
Container View
View Controller A view
A's view is initialized and added as a subview in B's viewDidLoad
method
I have no control of B, can't use methods of UIScrollViewDelegate
A's view is initially off screen, we can't see it
Need to somehow be notified that the A's view has been "scrolled into" the screen.
Since you have little control your best solution might actually be KVO. I like to avoid it though. You can create a solution with no access to anything at all. The following will take the first superview that is scroll view and try to hook to it's content offset and display when it is visible on scroll view and when it is not:
class MyView: UIView {
private weak var scrollView: UIScrollView?
private var observationContext = 0
private(set) var isCurrentlyVisible: Bool = false {
didSet {
if(oldValue != isCurrentlyVisible) {
print(isCurrentlyVisible ? "Visible" : "Not visisble")
}
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.findParentScrollView()
self.attachScrollViewObserving()
}
}
private func findParentScrollView() {
var scrollView: UIView? = self.superview
while scrollView != nil && !(scrollView is UIScrollView) {
scrollView = scrollView?.superview
}
self.scrollView = scrollView as? UIScrollView
}
private func attachScrollViewObserving() {
guard let scrollView = self.scrollView else { return }
scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [], context: &observationContext)
refreshIsVisibleInScrollView()
}
private func refreshIsVisibleInScrollView() {
guard let scrollView = self.scrollView else {
isCurrentlyVisible = false
return
}
let frameInScrollView = self.convert(bounds, to: scrollView)
let visibleFrame = CGRect(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y, width: scrollView.bounds.width, height: scrollView.bounds.height)
isCurrentlyVisible = visibleFrame.intersects(frameInScrollView) // OR visibleFrame.contains(frameInScrollView)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if &observationContext == context {
refreshIsVisibleInScrollView()
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
I hope you can imagine that a lot of things in here are dangerous but can be avoided if you have at least some access to your views:
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
Here the view waits for a second to ensure that it actually is already added into view hierarchy. A better approach would be to do this in view controller like:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myView.scrollView = self.parentScrollView
myView.attachScrollViewObserving()
}
The findParentScrollView is also dangerous. If you can find get it directly somehow or assign it when you create the view controller A you would improve stability. The problem naturally is that you may have scroll view within scroll view in some cases (note that table view and collection view are scroll views as well).
The rest should be quite straight forward I hope. But note that this is not yet a bulletproof implementation. Other properties may effect visibility of your view like resizing or repositioning of your own view within its parent; changing content insets; changing size of scroll view itself; transformations... It might be an overkill trying to handle them all so just use what you believe may change.

Observing Value for TableView

I'm in the process of creating a tableview which changes dynamically in height when the number of cells increases or decreases. In order to complete this, I added an observer to my tableview:
tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
I receive this with the following method:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
tableView.frame = CGRect(x: tableView.frame.origin.x, y: tableView.frame.origin.y, width: tableView.frame.size.width, height: tableView.contentSize.height)
tableView.reloadData()
}
However, when I run this I get an error:
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffeed414ff8)
I am sure these lines are causing the error and have already tried to call super.observeValue(...)
Any help would be greatly appreciated.
The way that Key Value Observing works in Objective C is that EVERY observed change for EVERY object you are observing goes through the same observeValue function. Its entirely possible that your observe function is getting called before the tableView is ever loaded from the storyboard and thats why you are crashing.
You are supposed to check at least 2 things before you touch the tableView:
is the object parameter === to your tableView (ie is this the object you are looking for)
is the keyPath == to the keyPath that you want to observe on this object?

Couldn't get the correct height of the wkWebView?

I have read lots of articles about how to get height of the webView. Most of them use KVO to observe the contentSize. I also did that.
1) create wkWebView and forbid webView's scrollView
lazy var webView: WKWebView = WKWebView()
webView.scrollView.bounces = false
webView.scrollView.isScrollEnabled = false
webView.snp.makeConstraints({ (make) in
make.left.right.equalTo(view)
make.top.equalTo(headerView.snp.bottom)
make.height.equalTo(AppDefaults.screenH - 64 - 44)
make.bottom.equalTo(containView)
})
2) add observer
webView.scrollView.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 scroll = object as? UIScrollView {
if scroll.contentSize.height > 0 {
updateWebViewConstraints(scroll.contentSize.height)
}
}
}
}
func updateWebViewConstraints(_ height: CGFloat) {
webView.snp.remakeConstraints { make in
make.left.right.equalTo(view)
make.top.equalTo(headerView.snp.bottom)
make.height.equalTo(height)
make.bottom.equalTo(containView).offset(-44)
}
}
3) remove observer
deinit {
webView.scrollView.removeObserver(self, forKeyPath: "contentSize")
}
When I run the app, sometimes I can get the correct height of the webView which was show right. Like below images
But sometimes ,the height of the webView over the correct height , it cause webView have part of empty space. We can see the below images, it get the wrong contentSize,
And when I tap the nextButton to load the new content, it also keep the previous height. That's not good. Just like if the contentSize's height over the new content's height. It will not change.
In order to compare wkWebView, so I have tried UIWebView, it was correct. So, I don't know how to solve that problem. Please give me some suggestions. Thanks
If the contentSize of the page you're rendering is less than the bounds of the WKWebView, the contentSize will be at least the same as the bounds, which results in a blank space.
The only way you could get around this was to set the bounds of your WKWebView to something less than the size of the content. Once the page renders you will know the size of the WKWebView.scrollView's contentSize and you can size it using your logic above.
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
self.webViewHeightConstraint?.constant = 0
self.view.layoutIfNeeded()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webViewHeightConstraint?.constant = webView.scrollView.contentSize.height
self.view.layoutIfNeeded()
}
}
I faced the same issue. Based on runmad's solution, setting the height to zero before load and setting the correct height afterward works.

How to get UIView's frame update while applying UIKit Dynamics' gravity to it?

I'm experiencing UIKit Dynamics right now and here is what I would like to do :
"Drop" multiple UIView instances from the top of the screen to the bottom using gravity => OK
Trigger an event when each view's distance from the bottom is like 100px. => NOT OK :(
Here is what I tried :
Add on observer on the "frame" property for each view => I don't know why but observeValueForKeyPath is never called (see code below)...
Add a transparent collision boundary to each view and listen to UICollisionBehaviorDelegate => This kinda works but it is not the behavior I want. Because of course the view is "stopped" by the boundary and I want it to go down
This is my test code :
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.animator = UIDynamicAnimator(referenceView: view)
self.gravity = UIGravityBehavior()
self.animator.addBehavior(gravity)
let timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: Selector("createView"), userInfo: nil, repeats: true)
}
func createView() {
var newView = UIView(frame: CGRectMake((UIScreen.mainScreen().bounds.size.width/2) - 20, -40, 40, 40))
self.view.addSubview(newView)
self.gravity.addItem(newView)
newView.addObserver(self, forKeyPath: "frame", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "frame" {
if let newView = object as? UIView {
println(newView.frame.origin.y)
if (newView.frame.origin.y > UIScreen.mainScreen().bounds.size.height - 100) {
println("Trigger event !")
}
}
}
}
With this code, the views will get down correctly but no event will be triggered when it the views are 100px far from the bottom
I would greatly appreciate any help
A way to do it is by adding an UICollisionBehavior, creating a view position 100px above the bottom and define an action which would be called when your items collide with this.
I can't create a sample for you now, but http://www.raywenderlich.com/50197/uikit-dynamics-tutorial will be a great help, look for a section called "Invisible boundaries and collisions". It's objective-C, though, but that shouldn't be a problem as this is pretty easy.
I've been trying to make this work too, really there should be a delegate callback from UIDynamicAnimator after it updates frames.
I've managed to solve it like this:
class FrameReportingView: UIView {
override var center: CGPoint {
didSet{
print("center: \(center)")
}
}
}
I'm using a UIPushBehavior and the center is set each frame. You can implement a delegate on FrameReportingView to create a callback after the center is changed.

Is it possible to add observer to tableView.contentOffset?

I need to track tableView.contentOffset.y Is it possible to add observer to tableView.contentOffset?
I think this is impossible because contentOffset doesn't inherit NSObject class.
Is any other solution?
UITableView is a UIScrollView subclass so you can use the UIScrollViewDelegate method scrollViewDidScroll: to be notified when the view scrolled. Check the contentOffset of the scrollView in that method
contentOffset is a key path, so you can also observe its changes using KVO
[self.tableView addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
Swift 5
tableContentObserver = table.observe(\UITableView.contentOffset, options: .new) { [weak self] table, change in
self?.navigationItem.rightBarButtonItem?.title = "\(change.newValue)"
}
Swift 3
Add an observer for the contentOffset key path using Key-Value Observing (KVO):
tableView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [.old, .new], context: nil)
And handle notifications for changes:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(UIScrollView.contentOffset) {
// Your code
}
}

Resources