UICollectionViewDelegate in RxCocoa - rx-swift

I write an extension for UICollectionView which will listen the delegate's
shouldHighlightItemAt method,but it don't call.
public var shouldHighlightItem: ControlEvent<IndexPath> {
let source = self.delegate.methodInvoked(#selector(UICollectionViewDelegate.collectionView(_:shouldHighlightItemAt:)))
.map { a in
return try self.castOrThrow(IndexPath.self, a[1])
}
return ControlEvent(events: source)
}
}
how to write an extension for UICollectionView of rx shouldHighlightItemAt?

You cannot use methodInvoked(_:) with a delegate method that has a non void return type.
collectionView(_:shouldHighlightItemAt:) expects you to return a Bool value. So you cannot use methodInvoked(_:).
If you have a look at the implementation of methodInvoked(_:) it gives you an explanation why this does not work:
Delegate methods that have non void return value can't be observed
directly using this method
because:
those methods are not intended to be used as a notification mechanism, but as a behavior customization mechanism
there is no sensible automatic way to determine a default return value
There is however a suggestion how you could achieve what you are trying to do:
In case observing of delegate methods that have return type is
required, it can be done by
manually installing a PublishSubject or BehaviorSubject and implementing delegate method.
In your case it would work like this:
In RxCollectionViewDelegateProxy you add the 'PublishSubject' and implement the UICollectionViewDelegate method:
let shouldHighlightItemAtIndexPathSubject = PublishSubject<IndexPath>
public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
shouldHighlightItemAtIndexPathSubject.on(.next(indexPath))
return self._forwardToDelegate?.collectionView(collectionView, shouldHighlightItemAt: indexPath) ?? true // default value
}
In your UICollectionView RxExtension you can expose the desired Observable like this:
public var property: Observable<IndexPath> {
let proxy = RxCollectionViewDelegateProxy.proxy(for: base)
return proxy.shouldHighlightItemAtIndexPathSubject.asObservable()
}
I have not tested this, I merely took it from the RxCocoa source code and modified it to fit your needs. So in theory this should work, but you might have to tweak it a little bit ;-)

Related

How to write a Reactive extension to observe the change in a string property or parent class, provided by iOS

I am inheriting SLComposeServiceViewController. It has a contentText String that contains the text entered in one of its views. I want to write an extension like extension Reactive where Base: SLComposeServiceViewController to be able to observe any changes in the value of this variable.
I can't find proper syntax examples for achieving this
I tried something like
extension Reactive where Base: SLComposeServiceViewController {
public func controlProperty<T>(
editingEvents: ControlProperty<String> ,
getter: #escaping (Base) -> T,
setter: #escaping (Base, T) -> Void
) -> ControlProperty<T> {
}
}
The SLComposeServiceViewController doesn't provide a way to observe the contentText property so there is no hook that Rx can use to wrap it into an Observable. However, the class' textView property does have such a hook and the Observable is already provided inside RxCocoa.
Try this:
extension Reactive where Base: SLComposeServiceViewController {
public var contentText: ControlProperty<String> {
return base.textView.rx.string // could be rx.text if you are using UIKit instead of AppKit
}
}

'#selector' refers to a method that is not exposed to Objective-C

The new Xcode 7.3 passing the parameter via addTarget usually works for me but in this case it's throwing the error in the title. Any ideas? It throws another when I try to change it to #objc
Thank you!
cell.commentButton.addTarget(self, action: #selector(FeedViewController.didTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
The selector it's calling
func didTapCommentButton(post: Post) {
}
In my case the function of the selector was private. Once I removed the private the error was gone. Same goes for fileprivate.
In Swift 4
You will need to add #objc to the function declaration. Until swift 4 this was implicitly inferred.
You need to use the #objc attribute on didTapCommentButton(_:) to use it with #selector.
You say you did that but you got another error. My guess is that the new error is that Post is not a type that is compatible with Objective-C. You can only expose a method to Objective-C if all of its argument types, and its return type, are compatible with Objective-C.
You could fix that by making Post a subclass of NSObject, but that's not going to matter, because the argument to didTapCommentButton(_:) will not be a Post anyway. The argument to an action function is the sender of the action, and that sender will be commentButton, which is presumably a UIButton. You should declare didTapCommentButton like this:
#objc func didTapCommentButton(sender: UIButton) {
// ...
}
You'll then face the problem of getting the Post corresponding to the tapped button. There are multiple ways to get it. Here's one.
I gather (since your code says cell.commentButton) that you're setting up a table view (or a collection view). And since your cell has a non-standard property named commentButton, I assume it's a custom UITableViewCell subclass. So let's assume your cell is a PostCell declared like this:
class PostCell: UITableViewCell {
#IBOutlet var commentButton: UIButton?
var post: Post?
// other stuff...
}
Then you can walk up the view hierarchy from the button to find the PostCell, and get the post from it:
#objc func didTapCommentButton(sender: UIButton) {
var ancestor = sender.superview
while ancestor != nil && !(ancestor! is PostCell) {
ancestor = view.superview
}
guard let cell = ancestor as? PostCell,
post = cell.post
else { return }
// Do something with post here
}
Try having the selector point to a wrapper function, which in turn calls your delegate function. That worked for me.
cell.commentButton.addTarget(self, action: #selector(wrapperForDidTapCommentButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
-
func wrapperForDidTapCommentButton(post: Post) {
FeedViewController.didTapCommentButton(post)
}
As you know selector[About] says that Objective-C runtime[About] should be used. Declarations that are marked as private or fileprivate are not exposed to the Objective-C runtime by default. That is why you have two variants:
Mark your private or fileprivate method declaration by #objc[About]
Use internal, public, open method access modifier[About]

NSView with a KVC property in Swift

I have a custom NSView class defined as:
class MyView: NSView
{
var someText: NSString
override func didChangeValueForKey(key: String)
{
println( key )
super.didChangeValueForKey( key )
}
// other stuff
}
What I want to be able to do is from outside of this class change the value of someText and have didChangeValueForKey notice that someText has changed so I can, for example, set needsDisplay to true for the view and do some other work.
How an I do this?
Are you sure you need KVC for this? KVC works fine in Swift, but there’s an easier way:
var SomeText: NSString {
didSet {
// do some work every time SomeText is set
}
}
There is no KVC mechanism for this because this isn't what KVC is for.
In Objective-C, you would implement the setter explicitly (or override if the property is originally from a superclass) and do your work there.
In Swift, the proper approach is the didSet mechanism.
didChangeValueForKey() is not part of KVC, it's part of KVO (Key-Value Observing). It is not intended to be overridden. It's intended to be called when one is implementing manual change notification (as a pair with willChangeValueForKey()).
More importantly, though, there's no reason to believe that it will be called at all for a property which is not being observed by anything. KVO swizzles the class in order to hook into the setters and other mutating accessors for those properties which are actually being observed. When such a property is changed (and supports automatic change notification), KVO calls willChangeValueForKey() and didChangeValueForKey() automatically. But for non-observed properties, those methods are not called.
Finally, in some cases, such as the indexed collection mutation accessors, KVO will use different change notification methods, such as willChange(_:valuesAtIndexes:forKey:) and didChange(_:valuesAtIndexes:forKey:).
If you really don't want to use didSet for some reason, you would use KVO to observe self for changes in the someText property and handle changes in observeValueForKeyPath(_:ofObject:change:context:). But this is a bad, clumsy, error-prone, inefficient way of doing a simple thing.
KVO and didSet are not mutually exclusive:
import Foundation
class C: NSObject {
dynamic var someText: String = "" {
didSet {
print("changed to \(someText)")
}
}
}
let c = C()
c.someText = "hi" // prints "changed to hi"
class Observer: NSObject {
init(_ c: C) {
super.init()
c.addObserver(self, forKeyPath: "someText", options: [], context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("observed change to \(object!.valueForKeyPath(keyPath!))")
}
}
let o = Observer(c)
c.someText = "test" // prints "changed to test" and "observed change to test"
I would add to Jaanus's answer that to make the property KVC compliant, you should declare it as dynamic var someText: NSString.
But if you don't need all the bells and whistles oh KVC, didSet is the way to go.
Update
As for didChangeValueForKey: – it is intended for the opposite, for you to notify value for key has changed (if it is not due to one of the cases covered by Foundation). You should use addObserver(_:forKeyPath:options:context:) and override observeValueForKeyPath(_:ofObject:change:context:) to be notified of changes.
Alternatively you can use one of many 3rd party solutions such as ReactiveCococa or Facebook's KVOController

Issue between override and non-override function in swift

Sorry I'm not very good at explaining this stuff. Basically I have the function below to handle remote control events.
UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
override func remoteControlReceivedWithEvent(event: UIEvent) {
if event.type == UIEventType.RemoteControl {
if event.subtype == UIEventSubtype.RemoteControlPlay {
stream.play()
} else if event.subtype == UIEventSubtype.RemoteControlPause {
stream.stop()
} else if event.subtype == UIEventSubtype.RemoteControlTogglePlayPause {
toggle()
}
}
}
Essentially, when I use the term "override" (shown above), I get the error
"Method does not override any method from its superclass.
If I leave out the "override", I get the error:
"Method 'remoteControlReceivedWithEvent' with Objective-C selector 'remoteControlReceivedWithEvent:' conflicts with method "remoteControlReceivedWithEvent" from superclass "UIResponder" with the same Objective-C selector.
I'm pretty new to all of this so I don't really understand what the issue is. Can someone please explain how to remedy this issue? Let me know if you need some more code or something.
Is there more code I have to use to set this up?
There is a mismatch of UIResponder method signature and your function implementation. UIResponder has optional Event as following:
func remoteControlReceibedWithEvent(_ event: UIEvent?)
So it can not override as there is no function with non-optional argument, but if you remove override it will conflict with ObjC implementation, as selector names are the same.

NSCollectionView does show nothing

I've tried to follow this guide:
Quick Start for Collection Views
using an NSImageView in the Collection View Item.
Nothing shows up, neither if i set the image with a Image Well neither if i set the array via code.
So i tried to do it programmatically, using
func representedObject(representedObject: AnyObject)
{
super.representedObject = representedObject
photoImageView.image = (representedObject as! NSImage)
println("\(representedObject)")
}
in the Collection View Item (subclassed).
If I don't subclass Collection View Item Xcode tells me that there is no prototype set, if i subclass it it tells that "could not load the nibName"... (it's in the storyboard with correct identity set)
I can't have this Collection View to work :-(
Anyway, i like the bindings... so i'd like to achieve the correct result with bindings..
I checked and rechecked every passage in the document at the link and everything seems fine. the main difference is that the document uses the app delegate, i'm using a view controller.
i translated KVC methods in swift, i think they are correct since i know them have been called. Here them are:
func insertObject(p: ClientPhoto, inClientPhotoArrayAtIndex index: Int) {
images.insertObject(p, atIndex: index)
}
func removeObjectFromClientPhotoArrayAtIndex(index: Int) {
images.removeObjectAtIndex(index)
}
func setClientPhotoArray(a: NSMutableArray) {
images = a
}
func clientPhotoArray() -> NSArray {
return images
}
Their are basically 2 ways to work with NSCollectionView. 1 is to set the itemPrototype property and the other is to override newItemForRepresentedObject. The override method is more flexible and has the advantage that you using the technique below you can create the nscollectionviewitem in storyboard and all the outlets will be set correctly. Here is an example of how I use it:
class TagsCollectionView: NSCollectionView {
// ...
override func newItemForRepresentedObject(object: AnyObject!) -> NSCollectionViewItem! {
let viewItem = MainStoryboard.instantiateControllerWithIdentifier("tagCollectionViewItem") as! TagCollectionViewItem
viewItem.representedObject = object
return viewItem
}

Resources