Skip to content
This repository has been archived by the owner. It is now read-only.

Improve interoperability between CKRefreshControl and UIRefreshControl classes #9

Merged
merged 18 commits into from Jan 28, 2013
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
065b946
everything except +appearanceWhenContainedIn: figured out
johnhaitas Jan 26, 2013
52e2b1c
switching CKRefreshControl reference to UIRefreshControl when calling…
johnhaitas Jan 26, 2013
d233b0c
relocating commonInit method
johnhaitas Jan 26, 2013
c1405d1
we actually don't even need to bother setting the paragraphStyle for …
johnhaitas Jan 26, 2013
e513dde
removing _isMasquerading variable
johnhaitas Jan 26, 2013
b972f67
improve interoperability of CKRefreshControl and UIRefreshControl
johnhaitas Jan 26, 2013
27e7295
improved interoperability between CKRefreshControl and UIRefreshControl
johnhaitas Jan 26, 2013
9d95a2a
update to readme per recent updates to code
johnhaitas Jan 26, 2013
dcf274d
minor change to readme
johnhaitas Jan 26, 2013
aa988ec
improvement of +appearanceWhenContainedIn: method interoperability be…
johnhaitas Jan 26, 2013
e5feae2
back to setting default tintColor when nil is passed to setTintColor:
johnhaitas Jan 27, 2013
654da75
adding list of contributors to README file
johnhaitas Jan 28, 2013
52f6c0f
changing "Contributor" to "Contributors"
johnhaitas Jan 28, 2013
05a9f25
no need for assembly in CKParagraphStyle
johnhaitas Jan 28, 2013
5687161
linking to UIRefreshControl class reference in README.md
johnhaitas Jan 28, 2013
03826ad
fix for +appearance selector
johnhaitas Jan 28, 2013
caf418b
overriding +class and using objc_getClass rather than +class internally
johnhaitas Jan 28, 2013
abc4e96
improved handling of +class method
johnhaitas Jan 28, 2013
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

@@ -23,8 +23,6 @@
#import "CKParagraphStyle.h"
#import <objc/objc-runtime.h>

static BOOL _isMasquerading = NO;

@implementation CKParagraphStyle

#if __IPHONE_OS_VERSION_MAX_ALLOWED <= __IPHONE_5_1
@@ -33,8 +31,6 @@ @implementation CKParagraphStyle
#define IMP_WITH_BLOCK_TYPE id
#endif

static void *NSParagraphStyleKey;

- (id) initWithCoder: (NSCoder *) aDecoder
{
return [super init];
@@ -48,26 +44,12 @@ + (void) load
// CKParagraphStyle will masquerade as NSParagraphStyle
static dispatch_once_t registerNSParagraphStyleClass_onceToken;
dispatch_once(&registerNSParagraphStyleClass_onceToken, ^{
Class nsParagraphStyleClass = objc_allocateClassPair([self class], "NSParagraphStyle", 0);
objc_registerClassPair(nsParagraphStyleClass);
_isMasquerading = YES;

Class nsAttributedStringClass = [NSAttributedString class];
IMP paragraphStyleIMP = imp_implementationWithBlock((IMP_WITH_BLOCK_TYPE)(^NSParagraphStyle *(id dynamicSelf) {
return objc_getAssociatedObject(dynamicSelf, &NSParagraphStyleKey);
}));
BOOL added = class_addMethod(nsAttributedStringClass, @selector(paragraphStyle), paragraphStyleIMP, "@@:");
NSAssert(added, @"We tried to add the paragraphStyle method, and it failed. This is going to break things, so we may as well stop here.");

IMP setParagraphStyleIMP = imp_implementationWithBlock((IMP_WITH_BLOCK_TYPE)(^void(NSAttributedString *dynamicSelf, id paragraphStyle) {
if (dynamicSelf.paragraphStyle == paragraphStyle) {
return;
}
objc_setAssociatedObject(dynamicSelf, &NSParagraphStyleKey, paragraphStyle, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}));

added = class_addMethod(nsAttributedStringClass, @selector(setParagraphStyle:), setParagraphStyleIMP, "v@:@");
NSAssert(added, @"We tried to add the setParagraphStyle: method, and it failed. This is going to break things, so we may as well stop here.");
Class nsParagraphStyleClass = NSClassFromString(@"NSParagraphStyle");
if (!nsParagraphStyleClass)
{
nsParagraphStyleClass = objc_allocateClassPair(self, "NSParagraphStyle", 0);
objc_registerClassPair(nsParagraphStyleClass);
}
});
}

@@ -36,8 +36,6 @@
CKRefreshControlStateRefreshing
} CKRefreshControlState;

static BOOL _isMasquerading = NO;

@interface CKRefreshControl ()
@property (nonatomic) CKRefreshControlState refreshControlState;
@end
@@ -52,10 +50,8 @@ @implementation CKRefreshControl {

- (id)init
{
Class uiRefreshControlClass = NSClassFromString(@"UIRefreshControl");

if (uiRefreshControlClass && !_isMasquerading)
return [[uiRefreshControlClass alloc] init];
if (![[UIRefreshControl class] isSubclassOfClass:[CKRefreshControl class]])
return (id)[[UIRefreshControl alloc] init];

if (self = [super init])
{
@@ -79,14 +75,10 @@ - (id) initWithCoder:(NSCoder *)aDecoder
self.attributedTitle = [aDecoder decodeObjectForKey:@"UIAttributedTitle"];

// we can set its refresh control when the table view controller sets its view
[[NSNotificationCenter defaultCenter] addObserverForName: CKRefreshControl_UITableViewController_DidSetView_Notification
object: nil
queue: nil
usingBlock: ^(NSNotification *notification) {
UITableViewController *tableViewController = notification.object;
if (tableViewController.refreshControl != (id)self)
tableViewController.refreshControl = (id)self;
} ];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(tableViewControllerDidSetView:)
name: CKRefreshControl_UITableViewController_DidSetView_Notification
object: nil ];
}
return self;
}
@@ -98,10 +90,23 @@ - (void) commonInit
[self setRefreshControlState:CKRefreshControlStateHidden];
}

- (void) tableViewControllerDidSetView: (NSNotification *) notification
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:CKRefreshControl_UITableViewController_DidSetView_Notification
object:nil];

UITableViewController *tableViewController = notification.object;
if (tableViewController.refreshControl != (id)self)
tableViewController.refreshControl = (id)self;
}

// remove notification observer in case notification never fired
- (void) awakeFromNib
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:CKRefreshControl_UITableViewController_DidSetView_Notification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver: self
name: CKRefreshControl_UITableViewController_DidSetView_Notification
object: nil ];
[super awakeFromNib];
}

@@ -131,10 +136,11 @@ - (void)populateSubviews {
[self addSubview:spinner];
}

- (void)setTintColor:(UIColor *)tintColor {
if (tintColor == nil) {
- (void)setTintColor: (UIColor *) tintColor
{
if (!tintColor)
tintColor = [UIColor colorWithWhite:0.5 alpha:1];
}

textLabel.textColor = tintColor;
arrow.tintColor = tintColor;
spinner.color = tintColor;
@@ -355,16 +361,29 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N

#pragma mark - Class methods

+ (Class) class
{
// cannot call +class from inside this method
Class uiRefreshControlClass = NSClassFromString(@"UIRefreshControl");
Class ckRefreshControlClass = NSClassFromString(@"CKRefreshControl");

if (![self isSubclassOfClass:ckRefreshControlClass])
return uiRefreshControlClass;

return [super class];
}

// If UIRefreshControl is available, we need to customize that class, not
// CKRefreshControl. Otherwise, the +appearance proxy is broken on iOS 6.
+ (id)appearance {
Class uiRefreshControlClass = NSClassFromString(@"UIRefreshControl");
if (uiRefreshControlClass) {
+ (id)appearance
{
if (![[UIRefreshControl class] isSubclassOfClass:[CKRefreshControl class]])
return [UIRefreshControl appearance];
}
else {
return [super appearance];
}

if ([self isEqual:[UIRefreshControl class]])
return [CKRefreshControl appearance];

return [super appearance];
}

+ (id)appearanceWhenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, ... {
@@ -382,28 +401,16 @@ + (id)appearanceWhenContainedIn:(Class<UIAppearanceContainer>)ContainerClass, ..
classes[i] = c;
}
va_end(list);

Class uiRefreshControlClass = NSClassFromString(@"UIRefreshControl");
if (uiRefreshControlClass) {
return [UIRefreshControl appearanceWhenContainedIn:ContainerClass, classes[0], classes[1], classes[2], classes[3], classes[4], classes[5], classes[6], classes[7], classes[8], classes[9], nil];
} else {
return [super appearanceWhenContainedIn:ContainerClass, classes[0], classes[1], classes[2], classes[3], classes[4], classes[5], classes[6], classes[7], classes[8], classes[9], nil];
}
}

// This is overridden so that things like
// [control isKindOfClass:[CKRefreshControl class]]
// will work on both iOS 5 and iOS 6.
+ (Class)class {
Class uiRefreshControlClass = NSClassFromString(@"UIRefreshControl");
if (![[UIRefreshControl class] isSubclassOfClass:[CKRefreshControl class]])
return [UIRefreshControl appearanceWhenContainedIn:ContainerClass, classes[0], classes[1], classes[2], classes[3], classes[4], classes[5], classes[6], classes[7], classes[8], classes[9], nil];

if (uiRefreshControlClass)
return uiRefreshControlClass;
if ([self isEqual:[UIRefreshControl class]])
return [CKRefreshControl appearanceWhenContainedIn:ContainerClass, classes[0], classes[1], classes[2], classes[3], classes[4], classes[5], classes[6], classes[7], classes[8], classes[9], nil];

return [super class];
return [super appearanceWhenContainedIn:ContainerClass, classes[0], classes[1], classes[2], classes[3], classes[4], classes[5], classes[6], classes[7], classes[8], classes[9], nil];
}


#if __IPHONE_OS_VERSION_MAX_ALLOWED <= __IPHONE_5_1
#define IMP_WITH_BLOCK_TYPE __bridge void*
#else
@@ -451,10 +458,32 @@ + (void)load
// CKRefreshControl will masquerade as UIRefreshControl
static dispatch_once_t registerUIRefreshControlClass_onceToken;
dispatch_once(&registerUIRefreshControlClass_onceToken, ^{
Class uiRefreshControlClass = objc_allocateClassPair([self class], "UIRefreshControl", 0);
objc_registerClassPair(uiRefreshControlClass);
_isMasquerading = YES;

Class *UIRefreshControlClassRef = NULL;
#if TARGET_CPU_ARM
__asm(
"movw %0, :lower16:(L_OBJC_CLASS_UIRefreshControl-(LPC0+4))\n"
"movt %0, :upper16:(L_OBJC_CLASS_UIRefreshControl-(LPC0+4))\n"
"LPC0: add %0, pc" : "=r"(UIRefreshControlClassRef)
);
#elif TARGET_CPU_X86_64
__asm("leaq L_OBJC_CLASS_UIRefreshControl(%%rip), %0" : "=r"(UIRefreshControlClassRef));
#elif TARGET_CPU_X86
void *pc = NULL;
__asm(
"calll L0\n"
"L0: popl %0\n"
"leal L_OBJC_CLASS_UIRefreshControl-L0(%0), %1" : "=r"(pc), "=r"(UIRefreshControlClassRef)
);
#else
#error Unsupported CPU
#endif
if (UIRefreshControlClassRef && *UIRefreshControlClassRef == Nil)
{
*UIRefreshControlClassRef = objc_allocateClassPair(self, "UIRefreshControl", 0);
objc_registerClassPair(*UIRefreshControlClassRef);
}

This comment has been minimized.

@0xced

0xced Jan 28, 2013

For full compatibility, I think you should use objc_duplicateClass(self, "UIRefreshControl", 0); instead of objc_allocateClassPair + objc_registerClassPair since this will add an intermediate CKRefreshControl class in the hierarchy.

This comment has been minimized.

@johnhaitas

johnhaitas Jan 28, 2013
Author Contributor

I deliberately took this approach rather than objc_duplicateClass so we can later test if UIRefreshControl is a subclass of CKRefreshControl. This was particularly useful with the +appearance proxy.

I used objc_duplicateClass for CKParagraphStyle.

This comment has been minimized.

@johnhaitas

johnhaitas Jan 28, 2013
Author Contributor

I borrowed the subclassing idea from @steipete's PSTCollectionView

@bjhomer would it be ok to acknowledge @0xced and @steipete in the README.md file?

This comment has been minimized.

@bjhomer

bjhomer Jan 28, 2013
Contributor

I have no problem with that


// Add UITableViewController.refreshControl if it isn't present
Class tableViewController = [UITableViewController class];
IMP refreshControlIMP = imp_implementationWithBlock((IMP_WITH_BLOCK_TYPE)(^id(id dynamicSelf) {
@@ -488,4 +517,28 @@ + (void)load
});
}

__asm(
#if defined(__OBJC2__) && __OBJC2__
".section __DATA,__objc_classrefs,regular,no_dead_strip\n"
#if TARGET_RT_64_BIT
".align 3\n"
"L_OBJC_CLASS_UIRefreshControl:\n"
".quad _OBJC_CLASS_$_UIRefreshControl\n"
#else
".align 2\n"
"L_OBJC_CLASS_UIRefreshControl:\n"
".long _OBJC_CLASS_$_UIRefreshControl\n"
#endif
#else
".section __TEXT,__cstring,cstring_literals\n"
"L_OBJC_CLASS_NAME_UIRefreshControl:\n"
".asciz \"UIRefreshControl\"\n"
".section __OBJC,__cls_refs,literal_pointers,no_dead_strip\n"
".align 2\n"
"L_OBJC_CLASS_UIRefreshControl:\n"
".long L_OBJC_CLASS_NAME_UIRefreshControl\n"
#endif
".weak_reference _OBJC_CLASS_$_UIRefreshControl\n"
);

@end
@@ -1,16 +1,15 @@
# CKRefreshControl

Open source 100% API-compatible replacement for `UIRefreshControl`, supporting iOS 5.0+
Open source 100% API-compatible replacement for [UIRefreshControl](http://developer.apple.com/library/ios/#documentation/uikit/reference/UIRefreshControl_class/Reference/Reference.html) , supporting iOS 5.0+

Using it is as simple as this:

#import <CKRefreshControl/CKRefreshControl.h>

UITableViewController *controller;

CKRefreshControl *refreshControl = [CKRefreshControl new];
[refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
controller.refreshControl = (id)refreshControl;
controller.refreshControl = [[UIRefreshControl alloc] init];
[controller.refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];

Then just link against the static library the `CKRefreshControl` project provides, and you're ready to go.

@@ -21,14 +20,12 @@ Then just link against the static library the `CKRefreshControl` project provide

### API Compatible

CKRefreshControl has exactly the same public API as UIRefreshControl. Thus, your code can treat either one as an instance of the other. Just use CKRefreshControl instead of UIRefreshControl everywhere, and you'll magically get iOS 5 support.
CKRefreshControl has exactly the same public API as UIRefreshControl. Thus, your code can treat either one as an instance of the other. Just use `CKRefreshControl` or `UIRefreshControl` throughout your code, and you'll magically get iOS 5 support.

We take advantage of this to provide excellent iOS 6 compatibility. When running on an iOS 6 device, `-[CKRefreshControl init]` actually returns a `UIRefreshControl` instance. No CKRefreshControl is ever initialized, and everything will work exactly as if CKRefreshControl did not even exist.

You can even use the `+appearance` proxies introduced in iOS 5.0; CKRefreshControl will appropriately forward the customizations to UIRefreshControl on iOS 6.

There is only one minor inconvenience. The `-[UITableViewController setRefreshControl:]` method in iOS 6 accepts a `UIRefreshControl *` parameter. Although CKRefreshControl is API compatible with UIRefreshControl, it is not a subclass (so it can be used on iOS 5). Thus, the compiler does not know that CKRefreshControl can be used as a UIRefreshControl. Thus, you must add an explicit cast to avoid a compiler warning. This is completely safe.

---

### Appearance
@@ -51,6 +48,15 @@ Yes! Instructure is using it in shipping code with no problems. That said, if yo

---

### Contributors

* [@bjhomer](http://github.com/bjhomer)
* [@johnhaitas](http://github.com/johnhaitas)
* [@steipete](http://github.com/steipete)
* [@0xced](http://github.com/0xced)

---

### License

CKRefreshControl, and all the accompanying source code, is released under the MIT license. You can see the full text of the license in the accompanying LICENSE.txt file.
@@ -30,7 +30,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
{
application.statusBarStyle = UIStatusBarStyleBlackOpaque;

[[CKRefreshControl appearanceWhenContainedIn:[AppearanceCustomizationController class], [UITabBarController class], nil] setTintColor:[UIColor greenColor]];
[[UIRefreshControl appearanceWhenContainedIn:[AppearanceCustomizationController class], nil] setTintColor:[UIColor greenColor]];

// Override point for customization after application launch.
return YES;
@@ -29,9 +29,8 @@ - (void)viewDidLoad
{
[super viewDidLoad];

CKRefreshControl *refreshControl = [CKRefreshControl new];
[refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
self.refreshControl = (id)refreshControl;
self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
}

- (void)doRefresh:(CKRefreshControl *)sender {
@@ -39,10 +39,9 @@ - (void)viewDidLoad
@"Black" : [UIColor blackColor],
};

CKRefreshControl *refreshControl = [CKRefreshControl new];
refreshControl.tintColor = [UIColor orangeColor];
[refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
self.refreshControl = (id)refreshControl;
self.refreshControl = [[UIRefreshControl alloc] init];
self.refreshControl.tintColor = [UIColor orangeColor];
[self.refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
}

- (void)doRefresh:(CKRefreshControl *)sender {
@@ -29,9 +29,8 @@ - (void)viewDidLoad
{
[super viewDidLoad];

CKRefreshControl *refreshControl = [CKRefreshControl new];
[refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
self.refreshControl = (id)refreshControl;
self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
}

- (void)doRefresh:(CKRefreshControl *)sender {
@@ -32,11 +32,10 @@ @implementation TextRefreshingController
- (void)viewDidLoad
{
[super viewDidLoad];

CKRefreshControl *refreshControl = [CKRefreshControl new];
refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"This is a test"];
[refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
self.refreshControl = (id)refreshControl;

self.refreshControl = [[UIRefreshControl alloc] init];
self.refreshControl.attributedTitle = [[NSAttributedString alloc] initWithString:@"This is a test"];
[self.refreshControl addTarget:self action:@selector(doRefresh:) forControlEvents:UIControlEventValueChanged];
}

- (void)doRefresh:(CKRefreshControl *)sender {
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.