Observing Value for TableView - ios

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?

Related

UITableview content size gets wrong while using estimated height

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)
}
}
}

How to respond to programmatic changes of a TextView text

The TextView delegate is set:
textView.delegate = self //self being a UITextViewDelegate
but the delegate method doesn't get called when the text is set programmatically
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
}
How to respond to text changes without going reactive?
This is how UITextField (and most of UIKit controls) behave- doesn't trigger event when set programatically. It makes sense- lets you avoid recurring, infinite calls.
If you really want to be notify when text is changed programatically, you have to subclass UITextField and override text property (probably attributedText also). Then in didSet block call delegate method.
Don't forget that UITextField inherits from UIControl- I would also call sendActions(for:) to make target-action mechanism fire.
I just tried KVO on UITextView,
self.textView1.text = "You are working, but I will change you in 5 seconds"
Add your observer
self.textView1.addObserver(self, forKeyPath: "text", options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
Trigger text change programmatically, just an example do it the way you want.
DispatchQueue.main.asyncAfter(deadline:.now() + 5) {
self.textView1.text = "Changed after some time"
}
Override the KVO method.
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object == self.textView1{
//do your thing here...
}
}
FYI from Apple docs below
Note: Although the classes of the UIKit framework generally do not
support KVO, you can still implement it in the custom objects of your
application, including custom views.
https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/KVO.html
declare your text view variable with #objc dynamic
declare and hold a variable with type NSKeyValueObservation
use function observe(_:changeHandler:) bind your text view's text property, hold the return value with variable declared in step 2
observe changes in changeHandler
example:
#objc dynamic private var textView: UITextView!
private var observation: NSKeyValueObservation?
func bind() {
observation = observe(\.textView.text, options: [.old, .new]) { object, change in
print(object, change)
}
}
You have misspelled the delegate with textView with _textView
func textViewDidChange(textView: UITextView) { //Handle the text changes here
print(textView.text); //the textView parameter is the textView where text was changed
}
Put this in viewDidLoad
textView!.delegate = self

Keep track of UIView position during translation animation

I know this has been asked before but all the answers were provided in Objective C and I'm looking for a Swift solution. If I've missed an existing Swift solution, please let me know and I'll close this question.
This is how I'm animating view:
UIView.animate(withDuration: 10.0, animations: { () in
let translateTransform = CGAffineTransform.init(translationX: 0.0, y: Constants.screenHeight)
self.icon.transform = translateTransform
})
What I'd like to do is keep track of current frame position throughout the animation. Do I need to take a different approach to achieve that?
You may want to try this to get a view's frame during an animation:
let currentFrame = myView.layer.presentation()!.frame
That will get you the frame at the time the code runs so if you wanted a record of the frames throughout the animation you may want to use a Timer (previously NSTimer).
In this example the optional is force unwrapped so if you're not sure if it's nil or not you may want to use an if-let statement.
Hope this helps and let me know if you have any other problems.
UIView.frame is observable. Try to use KVO for the frame keyPath:
view.addObserver(self, forKeyPath:"frame", options:.new, context:nil)
override func observeValue(forKeyPath keyPath: String, ofObject object: Any, change: [String: id], context: UnsafeMutableRawPointer) {
print("value of \(keyPath) changed: \(change[NSKeyValueChangeNewKey])")
}
KVO doesn't work for classes that don't extend NSObject.
In order to use key-value observing with a Swift class you should inherit from NSObject.
Add the dynamic modifier to any property you want to observe.
More on dynamic: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html#//apple_ref/doc/uid/TP40014216-CH4-ID57
class YourClass: NSObject {
dynamic var view: UIView = ...
}
Full article on KVO:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-XID_8

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