Typeful eventing

Related tags

EventBus signals-ios
Overview

Signals

Build Status Coverage Status CocoaPods Compatible Carthage Compatible License Platform

Signals is an eventing library that enables you to implement the Observable pattern without using error prone and clumsy NSNotifications or delegates.

Features

  • Type-safety
  • Attach-and-forget observation
  • Configurable observation behaviour
  • Separate callback queues
  • Comprehensive Unit Test Coverage

Installation

CocoaPods

To integrate Signals into your project add the following to your Podfile:

pod 'UberSignals', '~> 2.5'

Carthage

To integrate Signals into your project using Carthage add the following to your Cartfile:

github "uber/signals-ios" ~> 2.5

Introduction

NSNotifications are inherently error prone. Prior to iOS 9, if a listener doesn’t de-register itself from a notification when it’s deallocated, firing the notification will crash the application. If you refactor the data you send with a notification, the compiler won't warn you but your app might crash at runtime.

NSNotifications are also unnecessarily broad. Anyone can listen in on them which couples separate components in your application implicitly together.

With NSNotifications you register a selector to be invoked when a notification fires. This makes code less readable by separating where you register for notifications and where you handle notifications.

NSNotifications also require a lot of boilerplate code to register unique names to use as notification identifiers.

Signals solves all of the above problems and provides an inline, type-safe and attach-and-forget way to observe events being fired by objects. It is also a great replacement for delegates when there is no need to return data from the delegates.

Usage

Make a class observable by declaring a Signals in its header and implementing it in its initializer:

// Defines a new Signal type. This type is named "NetworkResult", and has two parameters 
// of type NSData and NSError. Note that the postfix "Signal" is automatically added to 
// the type name. Also note that only objects are allowed in Signal signatures.
CreateSignalType(NetworkResult, NSData *result, NSError *error)

@interface UBNetworkRequest

// We define two signals for our NetworkRequest class.
// - onNetworkResult will fire when a network result has been retreived.
// - onNetworkProgress will fire whenever download progresses.

// This uses the new signal type - NetworkResultSignal - that we've defined.
@property (nonatomic, readonly) UBSignal<NetworkResultSignal> *onNetworkResult;

// This makes use of a pre-defined signal type, FloatSignal.
@property (nonatomic, readonly) UBSignal<FloatSignal> *onNetworkProgress;

@end

@implementation UBNetworkRequest

- (instancetype)init {
  self = [super init];
  if (self) {
    // In the initializer the instance creates our signal
    _onNetworkResult = (UBSignal<NetworkResultSignal> *)
         [[UBSignal alloc] initWithProtocol:@protocol(NetworkResultSignal)];
    _onProgress = (UBSignal<FloatSignal> *)
         [[UBSignal alloc] initWithProtocol:@protocol(FloatSignal)];
   }
   return self;
}

- (void)receivedNetworkResult(NSData *data, NSError *error) 
{
  // Signal all listeners we're done loading
  _onNetworkProgress.fire(@(1.0))
  
  // Signal all listeners that we have data or an error
  _onNetworkResult.fire(myData, myError);
}

...

@end

Any class who has access to the NetworkResult instance, can now register itself as a listener and get notified whenever the network operation has loaded:

[networkRequest.onNetworkResult addObserver:self 
            callback:^(typeof(self) self, NSData *data, NSError *error) {
    // Do something with the result. The self passed into the block is 
    // weakified by Signals to guard against retain cycles.
}];

To cancel a single observer, call cancel on the returned UBSignalObserver:

UBSignalObserver *observer = [networkRequest.onNetworkResult addObserver:self 
        callback:^(typeof(self) self, NSData *data, NSError *error) {
    ...
}];
...
[observer cancel];

Advanced usage

You can configure the observer to cancel itself after it has observed a signal firing once:

[networkRequest.onNetworkResult addObserver:self 
            callback:^(typeof(self) self, NSData *data, NSError *error) {
    ...
}].cancelsAfterNextFire = YES;

The callback is by default called on the same NSOperationQueue than the signal fires on. To have it fire on a different queue, simply change the operationQueue parameter of the returned UBSignalObserver.

[networkRequest.onNetworkResult addObserver:self 
            callback:^(typeof(self) self, NSData *data, NSError *error) {
    ....
}].operationQueue = NSOperationQueue.mainQueue;

Signals remember with what data they were last fired with and you can force an observer to fire

[[networkRequest.onNetworkResult addObserver:self 
            callback:^(typeof(self) self, NSData *data, NSError *error) {
    ....
}] firePreviousData];

Swift support

The protocol-based approach described above is the easiest way to define new Signal types. However, these are unfortunately not accessible from Swift code. In order for Swift to understand the type of your signals correctly, you have to create concrete sub-classes for each signal type. Signals provides two macros to do this: CreateSignalInterface to create the interface for your sub-class and CreateSignalImplementation to create the implementation. You then use the concrete sub-classes when you declare the signals for your class:

// Defines a new Signal interface, a sub-class of UBSignal with the given
// name and parameters
CreateSignalInterface(UBNetworkResultSignal, NSData *result, NSError *error)

@interface UBNetworkRequest

// We declare the signal with the concrete type
@property (nonatomic, readonly) UBNetworkResultSignal *onNetworkResult;

@end


// In your .m-file you also create the implementation for the sub-class

CreateSignalImplementation(UBNetworkResultSignal, NSData *result, NSError *error)

@implementation UBNetworkRequest

- (instancetype)init {
  self = [super init];
  if (self) {
    // You initialize it without a protocol
    _onNetworkResult = [[UBNetworkResultSignal alloc] init];
   }
   return self;
}

- (void)receivedNetworkResult(NSData *data, NSError *error) 
{
  // And use it as you normally would
  _onNetworkResult.fire(myData, myError);
}

Max observers

Signals have a default maximum observer count of 100 and signals will NSAssert that you don't add more observers to them. This is to make you aware of situations where you are unknowingly oversubscribing to a signal (e.g. beause of memory leaks or re-registering an observer).

If you have a legitimate case of increasing this limit, you can set the maxObservers property of a signal.

_onNetworkResult = (UBSignal<NetworkResultSignal> *)
      [[UBSignal alloc] initWithProtocol:@protocol(NetworkResultSignal)];
      
_onNetworkResult.maxObservers = 500;

Signal naming

Each signal type created with the CreateSignalType macro creates a new protocol so that the compiler can enforce type safety. This means that the name you choose for your signal types need to be unique to your project.

Frequently, a signal will fire no parameters or one parameter of the basic ObjC types. Signals therefore predefines a set of signal types that you can use:

EmptySignal, fires no parameters
IntegerSignal, fires a NSNumber
FloatSignal, fires a NSNumber
DoubleSignal, fires a NSNumber
BooleanSignal, fires a NSNumber
StringSignal, fires a NSString
ArraySignal, fires a NSArray
MutableArraySignal, fires a NSMutableArray
DictionarySignal, fires a NSDictionary
MutableDictionarySignal, fires a NSMutableDictionary

Contributions

We'd love for you to contribute to our open source projects. Before we can accept your contributions, we kindly ask you to sign our Uber Contributor License Agreement.

  • If you find a bug, open an issue or submit a fix via a pull request.
  • If you have a feature request, open an issue or submit an implementation via a pull request
  • If you want to contribute, submit a pull request.

License

Signals is released under a MIT license. See the LICENSE file for more information.

Comments
  • Make Signals available for Mac OS

    Make Signals available for Mac OS

    It would be nice to make Signals available for Mac OS. Changes:

    • Added the Mac OS framework target & shared scheme;
    • Renamed schemes & targets to UberSignals iOS and UberSignals OSX;
    • Updated the .travis.yml and UberSignals.podspec accordingly.
    enhancement 
    opened by sgl0v 3
  • Creates an accompanying convenience initializer method when creating a signal with `CreateSignalType`

    Creates an accompanying convenience initializer method when creating a signal with `CreateSignalType`

    Currently to init a new signal you need to do the following, which involves casting and is verbose:

    self.signal = (UBSignal<CustomDataSignal> *)[[UBSignal alloc] initWithProtocol:@protocol(CustomDataSignal)];
    

    This diff automatically generates a convenience initializer, so instead you can do this, no casting:

    self.signal = [UBSignal newCustomDataSignal];
    

    To accomplish this, I had to move the macros in UBSignal.h below the interface definition of UBSignal, so I broke the file down into separate files so the macros remain above the fold.

    opened by ebgraham 2
  • Correct the spelling of CocoaPods in README

    Correct the spelling of CocoaPods in README

    This pull requests corrects the spelling of CocoaPods 🤓 https://github.com/CocoaPods/shared_resources/tree/master/media

    Created with cocoapods-readme.

    opened by ReadmeCritic 2
  • Model notifies observers upon its load leading to the retain loop

    Model notifies observers upon its load leading to the retain loop

    I have a model, which notifies its observers upon its load. However, it is not possible to send itself in onLoad signal since it will lead to retain loop, e.g. Model holds strongly onLoad signal, whereas signal holds Model strongly as it is passed in .fire(model, error) method. Please, find mock listing below:

    CreateSignalType(UBModelLoadResult, UBLoadableModel   model, NSError    error)
    
    @interface UBLoadableModel
    
    @property (nonatomic, readonly) UBSignal<UBModelLoadResultSignal> *onModelLoaded;
    @property (nonatomic, readonly) UBSignal<EmptySignal> *onModelLoading;
    
    @end
    
    @implementation UBLoadableModel
    
    - (instancetype)init {
      self = [super init];
      if (self) {
        _onModelLoaded = (UBSignal<UBModelLoadResultSignal> *)
             [[UBSignal alloc] initWithProtocol:@protocol(UBModelLoadResultSignal)];
        _onModelLoading = (UBSignal<EmptySignal> *)
             [[UBSignal alloc] initWithProtocol:@protocol(EmptySignal)];
       }
       return self;
    }
    
    - (void)load
    {
      // Signal all listeners we're done loading
      self.onModelLoading.fire();
      
      // Signal all listeners that we have data or an error
      // This line leads to retain loop
      self.onModelLoaded.fire(self, nil);
    }
    
    @end
    
    @interface MyClass ()
    @property (nonatomic, strong) UBLoadableModel *model	
    	
    @end	
    	
    	
    @implementation MyClass
    
    - (void)run {
    	[model.onModelLoaded addObserver:self callback:^(typeof(self) self, UBLoadableModel model, NSError error) {
    		NSLog(@"Loaded")
    	}];	
    }
    
    @end
    
    

    There are two possible ways to resolve this issue:

    • send nil in the fire method, e.g. .fire(nil. error). A drawback for this solution is that the class that doesn't contain Model as a property won't be able to figure out which model has loaded.
    • wrap self into class with weak property which should hold Model, then send in .fire(...) method with this weakified object.

    The both of them are ugly. Can you share your knowledge of handling these kind of retain loops?

    opened by igavrysh 1
  • Failed to install via Carthage because of

    Failed to install via Carthage because of "UberSignals iOS Tests" target cannot be built

    While installing the framework via Carthage for iOS platform, the process fails with the following log from xcodebuild:

    === BUILD TARGET UberSignals iOS Tests OF PROJECT UberSignals WITH CONFIGURATION Release ===
    
    Check dependencies
    “Swift Language Version” (SWIFT_VERSION) is required to be configured correctly for targets which use Swift. Use the [Edit > Convert > To Current Swift Syntax…] menu to choose a Swift version or use the Build Settings editor to configure the build setting directly.
    “Swift Language Version” (SWIFT_VERSION) is required to be configured correctly for targets which use Swift. Use the [Edit > Convert > To Current Swift Syntax…] menu to choose a Swift version or use the Build Settings editor to configure the build setting directly.
    
    ** BUILD FAILED **
    
    
    The following build commands failed:
    	Check dependencies
    (1 failure)
    

    Carthage 0.23.0 Xcode 8.3.2 (8E2002)

    opened by evgeniyd 1
  • Add

    Add "queue" to UBSignal's addObserver method

    Having no way to pass in a queue at the point of observer creation opens up an opportunity for race conditions, especially since this interface specifically deals with concurrency

    opened by StanTwinB 1
  • Missed firePreviousData in README

    Missed firePreviousData in README

    Signals remember with what data they were last fired with and you can force an observer to fire

    [networkRequest.onNetworkResult addObserver:self 
                callback:^(typeof(self) self, NSData *data, NSError *error) {
        ....
    }];
    

    should be

    [[networkRequest.onNetworkResult addObserver:self 
                callback:^(typeof(self) self, NSData *data, NSError *error) {
        ....
    }] firePreviousData];
    
    opened by maximgavrilov 1
  • Update README about NSNotificationCenter deallocation

    Update README about NSNotificationCenter deallocation

    On iOS 9 and onwards, NSNotificationCenter stores its targets weakly. Therefore it automatically removes observers after they are deallocated.

    https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11NotificationCenter

    opened by kaandedeoglu 1
  • Fixed typo in the unit tests

    Fixed typo in the unit tests

    For some tests we set firstSignalFired variable to YES, instead of secondSignalFired.

    Re-created the pull request to use development branch, not master one.

    opened by sgl0v 1
  • Adds a class level helper method for creating empty signals

    Adds a class level helper method for creating empty signals

    Not sure if we want this. Just threw something up since I found myself writing a lot of this boilerplate. This is also the only 'special' signal protocol that comes in the framework, and perhaps warrants its own common case constructor.

    enhancement 
    opened by voznesenskym 1
  • Improve UBSignal initialization safety

    Improve UBSignal initialization safety

    UBSignal can be initialized with init however this is not always what someone wants to do, so to make this a little more explicit and safe, I'm marking init as unavailable, raising a runtime exception, and improving the headerdoc to denote that if you want an empty signal then you should pass in the EmptySignal protocol explicitly.

    Also bumping the major to 2.0 as this is a breaking change.

    opened by alanzeino 1
  • fatal error: 'UBSignal.h' file not found

    fatal error: 'UBSignal.h' file not found

    The framework is installed via Carthage. In attempt to import it,

    #import <UberSignals/UberSignals.h>
    // OR
    @import UberSignals;
    

    ...a Could not build module 'UberSignals' error occurs:

    While building module 'UberSignals' imported from /Users/admin/Developer/repo/demo-app/ExploreSF/ExploreSF/SignalTypes/EHRAvailableCountriesRequest.h:18:
    In file included from <module-includes>:1:
    /Users/admin/Developer/repo/demo-app/ExploreSF/Carthage/Build/iOS/UberSignals.framework/Headers/UberSignals.h:33:9: fatal error: 'UBSignal.h' file not found
    #import "UBSignal.h"
            ^
    1 error generated.
    In file included from /Users/admin/Developer/repo/demo-app/ExploreSF/ExploreSF/SignalTypes/EHRAvailableCountriesRequest.m:9:
    /Users/admin/Developer/repo/demo-app/ExploreSF/ExploreSF/SignalTypes/EHRAvailableCountriesRequest.h:18:9: fatal error: could not build module 'UberSignals'
    @import UberSignals;
     ~~~~~~~^~~~~~~~~~~
    2 errors generated.
    

    Carthage 0.23.0 Xcode 8.3.2 (8E2002)

    opened by evgeniyd 0
  • Support parameterized observer types

    Support parameterized observer types

    Right now, when you add an observer to a signal, the callback returns self as type id in Objective-C and AnyObject in Swift. If we could make this a parameterized type, that would be very useful for Swift. Perhaps we could leverage Swift's ability to do generic functions?

    enhancement 
    opened by NickEntin 0
Releases(2.5.1)
Owner
Uber Open Source
Open Source Software at Uber
Uber Open Source