KVO for scroll start & end in UIScrollView - ios

Is there a way to use KVO (key value observing) to detect scrollViewWillBeginDragging and scrollViewDidEndDecelerating in swift?
Edit: I tried
scrollView.addObserver(self, forKeyPath: "dragging", options: NSKeyValueObservingOptions.New, context: nil)
But it's never called. If I observe for instance "contentOffset" in the same way, it's called. Is it KVC compliant?

I'm not sure whether it is sufficient for your question, but while you may not be able to observe isDragging or isDecelerating directly you can track them as your observing contentOffset. To find the start and end of dragging you can simply track when isDragging changes from false to true and vice versa by creating a global Boolean (here draggingInitiated) and including something like this in your observeValue:
if !draggingInitiated && scrollView.isDragging {
draggingInitiated = true
print("Started Dragging")
} else if draggingInitiated && !scrollView.isDragging {
draggingInitiated = false
print("Ended Dragging")
}

Answer based on #Philip De Vries
Swift 5
add observer
scrollView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
observe
// MARK: - Observation
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if let obj = object as? UIScrollView {
if obj == self.scrollView && keyPath == "contentOffset" {
scrollViewDidScroll(scrollView)
if !draggingInitiated && scrollView.isDragging {
draggingInitiated = true
onStartDraggin()
} else if draggingInitiated && !scrollView.isDragging {
draggingInitiated = false
onReleaseDragging()
}
}
}
}
cleanUp
private func removeObsrvers() {
scrollView.removeObserver(self, forKeyPath: "contentOffset")
}

You can add observers using NSNotification center.
In the viewDidLoad of your VC,
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.informBeginDragging), name: "begin", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.informEndDragging), name: "end", object: nil)
}
You can do anything in the following two methods
func informBeginDragging() {
//do something
}
func informEndDragging() {
//do something
}
And these two methods will be triggered when scroll view begin dragging or end dragging
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
NSNotificationCenter.defaultCenter().postNotificationName("end", object: nil)
}
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
NSNotificationCenter.defaultCenter().postNotificationName("begin", object: nil)
}

Related

object communication when object is not kvc compliant in Swift

I want to create a custom view that display it when an action occurs (like changed property) on another object (subclass of UIControl)
My approach was it
create a protocol whose UIControl objects can conform
create my custom view in which I can observe my delegate
Unfortunately, it's not work, worse, it's crash because compiler say "its not kvc-compliant"
Bellow, my code :
protocol CustomDelegate: class where Self : UIControl {
func respondeToControl() -> Bool
}
class CustomView: UIView {
weak var delegate: CustomDelegate? {
didSet{
observeIfControlIsPressed()
}
}
func observeIfControlIsPressed(){
if delegate != nil {
let keypath = delegate! as! UIControl
keypath.addObserver(keypath,
forKeyPath: "backgroundColor",
options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old],
context: nil)
}
}
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("background changed")
}
}
My question is, how I can re-design my solution to make it
work ?
You addObserver is wrong, the "observer" should be self no "keypath"
Try this:
keypath.addObserver(self, forKeyPath: "backgroundColor", options: [.new, .old], context: nil)
You did not read your error correctly.. For example:
protocol CustomDelegate : class where Self : UIControl {
func respondeToControl() -> Bool
}
class CustomView: UIView {
weak var delegate: CustomDelegate? {
didSet{
observeIfControlIsPressed()
}
}
func observeIfControlIsPressed(){
if delegate != nil {
let keypath = delegate! as! UIControl
keypath.addObserver(keypath,
forKeyPath: "backgroundColor",
options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old],
context: nil)
}
}
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("background changed")
}
}
class Foo : UIControl, CustomDelegate {
func respondeToControl() -> Bool {
return true
}
}
class ViewController: UIViewController {
let testBlock = UIView()
override func viewDidLoad() {
super.viewDidLoad()
let view = CustomView()
let button = Foo()
view.delegate = button
button.backgroundColor = UIColor.red
}
}
Throws an exception:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<Meh.Foo: 0x7fc977c1a0c0; baseClass = UIControl; frame = (0 0; 0 0); layer = <CALayer: 0x608000226480>>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: backgroundColor
It means Foo is observing foo but did not implement the observeValueForKeyPath function..
Now why is that? Well.. it's because you did:
keypath.addObserver(keypath, //keypath is observing itself..
forKeyPath: "backgroundColor",
options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old],
context: nil)
where keypath is your delegate.. and is adding observer on itself..
It should be:
keypath.addObserver(self, //self is observing keypath..
forKeyPath: "backgroundColor",
options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old],
context: nil)
The first parameter of addObserver needs to be the class that wants to do the observing.
That change will cause it to print: background changed.. Don't forget to remove the observer later.

NSBundleResourceRequest No Progress Update

When I try to observe the progress of a NSBundleResourceRequest, observeValue(forKeyPath: object: change: context:) is not called for the .new observing option. Therefore, the NSProgressIndicator isn't updated. Here is the setup and code:
Setup:
Xcode 8.3.1
Deployment Target iOS 10.3
Device: iPad 4
Resource tags (368 KB) are located located in Download Only On Demand and consist of thumbnails displayed in a UICollectionView. Thumbnail images are located in MyCollection.xcassets. All IBOutlets are connected.
Images are correctly displayed in the collection view, but progress bar remains at zero.
Code:
final class MyCollectionVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var myCollectionVCResourceRequest: NSBundleResourceRequest!
var myCollectionVCResourceRequestLoaded = false
static var myCollectionProgressObservingContext = UUID().uuidString
private let designThumbnailTags: Set<String> = ["Resource1", "Resource2", "Resource3"]
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadOnDemandResources()
}
func loadOnDemandResources() {
myCollectionVCResourceRequest = NSBundleResourceRequest(tags: designThumbnailTags)
myCollectionVCResourceRequest.progress.addObserver(self, forKeyPath: "fractionCompleted", options: [.new, .initial], context: &MyCollectionVC.myCollectionProgressObservingContext)
myCollectionVCResourceRequest.beginAccessingResources(completionHandler: { (error) in
print("Complete: \(self.myCollectionVCResourceRequest.progress.fractionCompleted)") // Prints Complete: 0.0
self.myCollectionVCResourceRequest.progress.removeObserver(self, forKeyPath: "fractionCompleted", context: &MyCollectionVC.myCollectionProgressObservingContext)
OperationQueue.main.addOperation({
guard error == nil else { self.handleOnDemandResourceError(error! as NSError); return }
self.myCollectionVCResourceRequestLoaded = true
self.updateViewsForOnDemandResourceAvailability()
self.fetchDesignThumbnailsWithOnDemandResourceTags(self.myCollectionVCResourceRequest) // Correctly creates Core Data Instances
})
})
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &MyCollectionVC.myCollectionProgressObservingContext {
OperationQueue.main.addOperation({
let progressObject = object as! Progress
self.progressView.progress = Float(progressObject.fractionCompleted)
print(Float(progressObject.fractionCompleted)) // Prints 0.0 as a result of including the .initial option
self.progressDetailLabel.text = progressObject.localizedDescription
})
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

rectForRowAtIndexPath crashing with defined indexPath in UITableView

The following code is crashing at
currentCellRect = tableView.rectForRowAtIndexPath(indexPaths[0])
But only sometimes.
public func showCellScrollCount(animated:Bool) {
self.tableView.addObserver(self, forKeyPath: "contentOffset", options: NSKeyValueObservingOptions.New, context: nil)
self.tableView.addObserver(self, forKeyPath: "dragging", options: NSKeyValueObservingOptions.New, context: nil)
}
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
switch (keyPath, object) {
case (.Some("contentOffset"), _):
self.updateScrollPosition()
default:
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
func updateScrollPosition() {
let indexPaths = tableView.indexPathsForVisibleRows
var currentCellRect:CGRect?
if let indexPaths = indexPaths {
if indexPaths.count > 0 {
currentCellRect = tableView.rectForRowAtIndexPath(indexPaths[0])
scrollCountView.currentScrollCountNum = indexPaths[0].row
}
}
}
It crashes with "BAD_ACCESS". Does anyone have any idea why?
...
EDIT: Is it possible it's happening because I'm calling tableView.reloadData() right before I add observers, and the call isn't finished yet?
Updated
Your code works fine since I added it to my tableView controller and called showCellScrollCount func once. Where you call showCellScrollCount, do you call it multiple times? It can be problem if so.
Update 3
here is the problem:
public var totalScrollCountNum = 0 {
didSet {
scrollCountView.totalScrollCountNum = totalScrollCountNum
showCellScrollCount(false)
}
}
move showCellScrollCount(false) to initializers

How to get if an observer is registered in Swift

I want to remove an observer after it run or when the view dissapeared. Here is the code but sometimes the observer was already removed when i want to remove it again. How to check if it is still registered?
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if(!didOnce){
if(keyPath == "myLocation"){
location = mapView.myLocation.coordinate;
self.mapView.animateToLocation(self.location!);
self.mapView.animateToZoom(15);
didOnce = true;
self.mapView.removeObserver(self, forKeyPath: "myLocation");
}
}
}
override func viewDidAppear(animated: Bool) {
didOnce = false;
}
override func viewWillDisappear(animated: Bool) {
if(!didOnce){
self.mapView.removeObserver(self, forKeyPath: "myLocation");
didOnce = true;
}
}
You're on the right track. Add an isObserving property to your class. Set it to true when you start observing, and set it to false when you stop observing. In all cases check the flag before starting/stopping observing to make sure you are not already in that state.
You can also add a willSet method to the property and have that code start/stop observing when the property changes states.
Main reason: addObserver() is called 1 time (at viewDidLoad or init), but removeObserver() can be called 2 times or more (base on the times viewWillDisappear() was called).
To resolve: move addObserver() to viewWillAppear():
private var didOnce = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.mapView.addObserver(self, forKeyPath: "myLocation", options: .new, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if(!didOnce){
if(keyPath == "myLocation"){
location = mapView.myLocation.coordinate;
self.mapView.animateToLocation(self.location!);
self.mapView.animateToZoom(15);
didOnce = true;
self.mapView.removeObserver(self, forKeyPath: "myLocation");
}
}
}
override func viewWillDisappear(animated: Bool) {
if(!didOnce){
self.mapView.removeObserver(self, forKeyPath: "myLocation");
didOnce = true;
}
}

Detect volume button press

Volume button notification function is not being called.
Code:
func listenVolumeButton(){
// Option #1
NSNotificationCenter.defaultCenter().addObserver(self, selector: "volumeChanged:", name: "AVSystemController_SystemVolumeDidChangeNotification", object: nil)
// Option #2
var audioSession = AVAudioSession()
audioSession.setActive(true, error: nil)
audioSession.addObserver(self, forKeyPath: "volumeChanged", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "volumeChanged"{
print("got in here")
}
}
func volumeChanged(notification: NSNotification){
print("got in here")
}
listenVolumeButton() is being called in viewWillAppear
The code is not getting to the print statement "got in here", in either case.
I am trying two different ways to do it, neither way is working.
I have followed this: Detect iPhone Volume Button Up Press?
Using the second method, the value of the key path should be "outputVolume". That is the property we are observing.
So change the code to,
var outputVolumeObserve: NSKeyValueObservation?
let audioSession = AVAudioSession.sharedInstance()
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {}
outputVolumeObserve = audioSession.observe(\.outputVolume) { (audioSession, changes) in
/// TODOs
}
}
The code above won't work in Swift 3, in that case, try this:
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {
print("some error")
}
audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
print("got in here")
}
}
With this code you can listen whenever the user taps the volume hardware button.
class VolumeListener {
static let kVolumeKey = "volume"
static let shared = VolumeListener()
private let kAudioVolumeChangeReasonNotificationParameter = "AVSystemController_AudioVolumeChangeReasonNotificationParameter"
private let kAudioVolumeNotificationParameter = "AVSystemController_AudioVolumeNotificationParameter"
private let kExplicitVolumeChange = "ExplicitVolumeChange"
private let kSystemVolumeDidChangeNotificationName = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
private var hasSetup = false
func start() {
guard !self.hasSetup else {
return
}
self.setup()
self.hasSetup = true
}
private func setup() {
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
return
}
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.clipsToBounds = true
rootViewController.view.addSubview(volumeView)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.volumeChanged),
name: kSystemVolumeDidChangeNotificationName,
object: nil
)
volumeView.removeFromSuperview()
}
#objc func volumeChanged(_ notification: NSNotification) {
guard let userInfo = notification.userInfo,
let volume = userInfo[kAudioVolumeNotificationParameter] as? Float,
let changeReason = userInfo[kAudioVolumeChangeReasonNotificationParameter] as? String,
changeReason == kExplicitVolumeChange
else {
return
}
NotificationCenter.default.post(name: "volumeListenerUserDidInteractWithVolume", object: nil,
userInfo: [VolumeListener.kVolumeKey: volume])
}
}
And to listen you just need to add the observer:
NotificationCenter.default.addObserver(self, selector: #selector(self.userInteractedWithVolume),
name: "volumeListenerUserDidInteractWithVolume", object: nil)
You can access the volume value by checking the userInfo:
#objc private func userInteractedWithVolume(_ notification: Notification) {
guard let volume = notification.userInfo?[VolumeListener.kVolumeKey] as? Float else {
return
}
print("volume: \(volume)")
}
import AVFoundation
import MediaPlayer
override func viewDidLoad() {
super.viewDidLoad()
let volumeView = MPVolumeView(frame: CGRect.zero)
for subview in volumeView.subviews {
if let button = subview as? UIButton {
button.setImage(nil, for: .normal)
button.isEnabled = false
button.sizeToFit()
}
}
UIApplication.shared.windows.first?.addSubview(volumeView)
UIApplication.shared.windows.first?.sendSubview(toBack: volumeView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
do { try AVAudioSession.sharedInstance().setActive(true) }
catch { debugPrint("\(error)") }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
do { try AVAudioSession.sharedInstance().setActive(false) }
catch { debugPrint("\(error)") }
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let key = keyPath else { return }
switch key {
case "outputVolume":
guard let dict = change, let temp = dict[NSKeyValueChangeKey.newKey] as? Float, temp != 0.5 else { return }
let systemSlider = MPVolumeView().subviews.first { (aView) -> Bool in
return NSStringFromClass(aView.classForCoder) == "MPVolumeSlider" ? true : false
} as? UISlider
systemSlider?.setValue(0.5, animated: false)
guard systemSlider != nil else { return }
debugPrint("Either volume button tapped.")
default:
break
}
}
When observing a new value, I set the system volume back to 0.5. This will probably anger users using music simultaneously, therefore I do not recommend my own answer in production.
If interested here is a RxSwift version.
func volumeRx() -> Observable<Void> {
Observable<Void>.create {
subscriber in
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
} catch let e {
subscriber.onError(e)
}
let outputVolumeObserve = audioSession.observe(\.outputVolume) {
(audioSession, changes) in
subscriber.onNext(Void())
}
return Disposables.create {
outputVolumeObserve.invalidate()
}
}
}
Usage
volumeRx()
.subscribe(onNext: {
print("Volume changed")
}).disposed(by: disposeBag)

Resources