Skip to content

Commit

Permalink
Merge branch 'doc' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Frizlab committed Feb 25, 2024
2 parents 3163815 + e6a4708 commit de6eff1
Show file tree
Hide file tree
Showing 66 changed files with 4,978 additions and 2,103 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ jobs:
NEW_BUILD_VERSION="$(cat ../new_build_version)"
rm -f ../new_build_version
eval xct versions set-build-version --targets\ eXtenderZ-{static,dynamic}-{macOS,visionOS,iOS,tvOS,watchOS} "$NEW_BUILD_VERSION"
eval xct versions set-marketing-version --targets\ eXtenderZ-{static,dynamic}-{macOS,visionOS,iOS,tvOS,watchOS} "$NEW_MARKETING_VERSION"
eval xct versions set-build-version --targets\ eXtenderZ-{static,dynamic} "$NEW_BUILD_VERSION"
eval xct versions set-marketing-version --targets\ eXtenderZ-{static,dynamic} "$NEW_MARKETING_VERSION"
TAG_NAME="$NEW_MARKETING_VERSION"
Expand Down
21 changes: 21 additions & 0 deletions Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ``eXtenderZ``

Change the behavior of objects at runtime using `isa-swizzling`.

## Overview

The eXtenderZ package gives you a convenient method to create new classes on the fly containing the code you want
and change the type of the instances you want to behave differently.

## Topics

### Adding an Extender to an Object

Usually you should add extenders on objects using the two first methods here.
These two methods will raise an exception if the extender cannot be added to the object.
The third method is the underlying method called by the two conveniences.
It is possible to call the third method directly.

- ``CHECKED_ADD_EXTENDER``
- ``HPNCheckedAddExtender``
- ``NSObject_WorkaroundForDoc/hpn_addExtender:``
65 changes: 65 additions & 0 deletions Examples/NSObject Extenders/HPNCoreDataUpdatesAggregator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Copyright 2021 happn
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

@import CoreData;
@import Foundation;



typedef NS_CLOSED_ENUM(NSUInteger, HPNAggregatedChangeType) {
HPNAggregatedChangeTypeUpdate, /* sourceIdx is set; ignore destIdx. */
HPNAggregatedChangeTypeInsert, /* destIdx is set; ignore sourceIdx. */
HPNAggregatedChangeTypeDelete, /* sourceIdx is set; ignore destIdx. */
HPNAggregatedChangeTypeMove /* sourceIdx and destIdx are set. */
};



@interface HPNCoreDataUpdate : NSObject

@property(nonatomic, retain) id object;

@property(nonatomic, assign) HPNAggregatedChangeType type;

@property(nonatomic, assign) NSUInteger sourceIdx;
@property(nonatomic, assign) NSUInteger destIdx;

@end



/* Currently only supports sources with one section only (expects all index
* paths to have section == 0).
* Support for more sections or section updates should not be very hard to add
* but I can't test it. */
@interface HPNCoreDataUpdatesAggregator : NSObject {
NSMutableArray *currentMovingUpdates;
NSMutableArray *currentStaticUpdates;
}

/* YES by default. If NO, object will always be nil in the handler. */
@property(nonatomic, assign) BOOL keepObject;

- (void)addChangeForObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath changeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;
- (void)removeAllChanges;

/* If you have an NSMutableArray, you can simply update, delete, move and insert
* the elements directly at the indexes given by the handler and you will be
* good to go!
* For the moves, delete source, then insert at destination (in this order). Do
* NOT exchanges source and destination! */
- (void)iterateAggregatedChangesWithHandler:(void (^)(HPNCoreDataUpdate *update))handler;

@end
247 changes: 247 additions & 0 deletions Examples/NSObject Extenders/HPNCoreDataUpdatesAggregator.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
Copyright 2021 happn
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

@import UIKit.UICollectionView;

#import "HPNCoreDataUpdatesAggregator.h"



@interface HPNCoreDataUpdate () /* Private, but adds properties, cannot be a named category. */

+ (NSArray *)coreDataUpdatesWithObject:(id)object type:(NSFetchedResultsChangeType)type sourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath;

+ (instancetype)atomicCoreDataUpdateWithObject:(id)object type:(NSFetchedResultsChangeType)type sourceIndex:(NSUInteger)sourceIndex destinationIndex:(NSUInteger)destinationIndex;
+ (instancetype)atomicCoreDataUpdateWithObject:(id)object type:(NSFetchedResultsChangeType)type sourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath;

@property(nonatomic, weak) HPNCoreDataUpdate *linkedUpdateForMove;
@property(nonatomic, readonly) BOOL isMove;

/* Only used by the algorithm to aggregate the updates. Defaults to NSNotFound. */
@property(nonatomic, assign) NSUInteger __idx;

/* Requires receiver to be a decomposed moved update. */
- (HPNCoreDataUpdate *)atomicMoveUpdate;

@end



@implementation HPNCoreDataUpdate

+ (NSArray *)coreDataUpdatesWithObject:(id)object type:(NSFetchedResultsChangeType)type sourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath
{
switch (type) {
case NSFetchedResultsChangeUpdate:
case NSFetchedResultsChangeInsert:
case NSFetchedResultsChangeDelete:
return @[[self atomicCoreDataUpdateWithObject:object type:type sourceIndexPath:sourceIndexPath destinationIndexPath:destinationIndexPath]];
case NSFetchedResultsChangeMove: {
HPNCoreDataUpdate *update1 = [HPNCoreDataUpdate atomicCoreDataUpdateWithObject:object type:NSFetchedResultsChangeDelete sourceIndexPath:sourceIndexPath destinationIndexPath:nil];
HPNCoreDataUpdate *update2 = [HPNCoreDataUpdate atomicCoreDataUpdateWithObject:object type:NSFetchedResultsChangeInsert sourceIndexPath:nil destinationIndexPath:destinationIndexPath];
update1.linkedUpdateForMove = update2;
update2.linkedUpdateForMove = update1;
return @[update1, update2];
}
default: NSAssert(NO, @"***** ERROR: Unexpected type %"NSUINT_FMT, type);
}
return nil;
}

+ (instancetype)atomicCoreDataUpdateWithObject:(id)object type:(NSFetchedResultsChangeType)type sourceIndex:(NSUInteger)sourceIndex destinationIndex:(NSUInteger)destinationIndex
{
HPNCoreDataUpdate *res = [self new];
res.object = object;
switch (type) {
case NSFetchedResultsChangeUpdate: res.type = HPNAggregatedChangeTypeUpdate; break;
case NSFetchedResultsChangeInsert: res.type = HPNAggregatedChangeTypeInsert; break;
case NSFetchedResultsChangeDelete: res.type = HPNAggregatedChangeTypeDelete; break;
case NSFetchedResultsChangeMove: res.type = HPNAggregatedChangeTypeMove; break;
default: NSAssert(NO, @"***** ERROR: Unexpected type %"NSUINT_FMT, type);
}
res.sourceIdx = sourceIndex;
res.destIdx = destinationIndex;
res.__idx = NSNotFound;
return res;
}

+ (instancetype)atomicCoreDataUpdateWithObject:(id)object type:(NSFetchedResultsChangeType)type sourceIndexPath:(NSIndexPath *)sourceIndexPath destinationIndexPath:(NSIndexPath *)destinationIndexPath
{
NSParameterAssert(sourceIndexPath.section == 0 && destinationIndexPath.section == 0);
return [self atomicCoreDataUpdateWithObject:object type:type
sourceIndex:(sourceIndexPath? sourceIndexPath.item: NSNotFound)
destinationIndex:(destinationIndexPath? destinationIndexPath.item: NSNotFound)];
}

- (NSString *)description
{
return [NSString stringWithFormat:@"HPNCoreDataUpdate<%p>, %"NSUINT_FMT", from %"NSUINT_FMT" to %"NSUINT_FMT" --> %p",
self, self.type, self.sourceIdx, self.destIdx, self.linkedUpdateForMove];
}

- (BOOL)isMove
{
return (self.linkedUpdateForMove != nil);
}

- (HPNCoreDataUpdate *)atomicMoveUpdate
{
NSParameterAssert(self.linkedUpdateForMove != nil);
NSAssert(self.linkedUpdateForMove.linkedUpdateForMove == self, @"***** ERROR: I'm invalid (linked update of linked update is not me)! %@", self);
NSAssert(self.object == self.linkedUpdateForMove.object, @"***** ERROR: I'm invalid (linked object != my object)! %@ - %@", self, self.linkedUpdateForMove);
HPNCoreDataUpdate *delete = (self.type == HPNAggregatedChangeTypeDelete? self: self.linkedUpdateForMove);
HPNCoreDataUpdate *insert = (self.type == HPNAggregatedChangeTypeInsert? self: self.linkedUpdateForMove);
NSAssert(delete.type == HPNAggregatedChangeTypeDelete, @"***** ERROR: I'm invalid (delete is not delete)! %@ - %@", self, delete);
NSAssert(insert.type == HPNAggregatedChangeTypeInsert, @"***** ERROR: I'm invalid (insert is not insert)! %@ - %@", self, insert);
return [self.class atomicCoreDataUpdateWithObject:insert.object type:NSFetchedResultsChangeMove sourceIndex:delete.sourceIdx destinationIndex:insert.destIdx];
}

@end



@implementation HPNCoreDataUpdatesAggregator

- (id)init
{
if ((self = [super init]) != nil) {
_keepObject = YES;
currentStaticUpdates = [NSMutableArray arrayWithCapacity:50];
currentMovingUpdates = [NSMutableArray arrayWithCapacity:50];
}

return self;
}

- (void)addChangeForObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath changeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
NSParameterAssert(indexPath.section == 0 && newIndexPath.section == 0);
if (!self.keepObject) anObject = nil;
switch (type) {
case NSFetchedResultsChangeUpdate: [currentStaticUpdates addObject:[HPNCoreDataUpdate atomicCoreDataUpdateWithObject:anObject type:type sourceIndexPath:indexPath destinationIndexPath:newIndexPath]]; break;
case NSFetchedResultsChangeInsert: [currentMovingUpdates addObject:[HPNCoreDataUpdate atomicCoreDataUpdateWithObject:anObject type:type sourceIndexPath:indexPath destinationIndexPath:newIndexPath]]; break;
case NSFetchedResultsChangeDelete: [currentMovingUpdates addObject:[HPNCoreDataUpdate atomicCoreDataUpdateWithObject:anObject type:type sourceIndexPath:indexPath destinationIndexPath:newIndexPath]]; break;
case NSFetchedResultsChangeMove: [currentMovingUpdates addObjectsFromArray:[HPNCoreDataUpdate coreDataUpdatesWithObject:anObject type:type sourceIndexPath:indexPath destinationIndexPath:newIndexPath]]; break;
default:
/* The NSAssert below is INVALID! Apparently, on iOS 8, when the app is
* compiled with Xcode 7, we receive changes of type 0, which is
* invalid... */
// NSAssert(NO, @"***** ERROR: Unknown change type %"NSUINT_FMT, type);
;
}
}

- (void)removeAllChanges
{
[currentStaticUpdates removeAllObjects];
[currentMovingUpdates removeAllObjects];
}

#define ASSERT_VALID_INDEX(idx) NSAssert(idx != NSNotFound, @"***** INTERNAL ERROR (invalid index path %"NSUINT_FMT")", idx)
#define ASSERT_INVALID_INDEX(idx) NSAssert(idx == NSNotFound, @"***** INTERNAL ERROR (valid index path %"NSUINT_FMT")", idx)

- (void)iterateAggregatedChangesWithHandler:(void (^)(HPNCoreDataUpdate *update))handler
{
/* ********* Let's call the updates. ********* */
for (HPNCoreDataUpdate *update in currentStaticUpdates) {
/* The assert below is WRONG!
* See: http://stackoverflow.com/a/32213076 */
// ASSERT_INVALID_INDEX(update.destinationIndex);
ASSERT_VALID_INDEX(update.sourceIdx);

handler(update);
}

/* ********* Let's sort/reindex the inserts, deletes and moves. ********* */
[currentMovingUpdates sortUsingComparator:^NSComparisonResult(HPNCoreDataUpdate *obj1, HPNCoreDataUpdate *obj2) {
NSParameterAssert([obj1 isKindOfClass:HPNCoreDataUpdate.class]);
NSParameterAssert([obj2 isKindOfClass:HPNCoreDataUpdate.class]);
NSParameterAssert(obj1.type == HPNAggregatedChangeTypeDelete || obj1.type == HPNAggregatedChangeTypeInsert);
NSParameterAssert(obj2.type == HPNAggregatedChangeTypeDelete || obj2.type == HPNAggregatedChangeTypeInsert);

if (obj1.type == HPNAggregatedChangeTypeDelete && obj2.type == HPNAggregatedChangeTypeInsert) return NSOrderedAscending;
if (obj1.type == HPNAggregatedChangeTypeInsert && obj2.type == HPNAggregatedChangeTypeDelete) return NSOrderedDescending;

if (obj1.type == HPNAggregatedChangeTypeInsert) {
NSParameterAssert(obj1.destIdx != obj2.destIdx);
NSParameterAssert(obj2.type == HPNAggregatedChangeTypeInsert);
if (obj1.destIdx < obj2.destIdx) return NSOrderedAscending;
return NSOrderedDescending;
}
NSParameterAssert(obj1.type == HPNAggregatedChangeTypeDelete && obj2.type == HPNAggregatedChangeTypeDelete);
NSParameterAssert(obj1.sourceIdx != obj2.sourceIdx);
if (obj1.sourceIdx < obj2.sourceIdx) return NSOrderedDescending;
return NSOrderedAscending;
}];
NSUInteger i = 0, n = currentMovingUpdates.count;
while (i < n) {
HPNCoreDataUpdate *update = currentMovingUpdates[i];
NSAssert(update.__idx == NSNotFound || update.__idx == i, @"***** INTERNAL ERROR: Invalid idx.");
update.__idx = i;

if (update.type == HPNAggregatedChangeTypeInsert && update.isMove && update.linkedUpdateForMove.__idx != i-1) {
NSAssert(i > 0, @"***** INTERNAL LOGIC ERROR");
NSAssert(update.__idx == i, @"***** INTERNAL LOGIC ERROR");
NSAssert(update.__idx > update.linkedUpdateForMove.__idx + 1, @"***** INTERNAL ERROR");
NSUInteger destIdx = update.linkedUpdateForMove.__idx;
for (NSUInteger j = i; j > destIdx + 1; --j) {
NSAssert(currentMovingUpdates[j] == update, @"***** INTERNAL LOGIC ERROR");
HPNCoreDataUpdate *swappedUpdate = currentMovingUpdates[j-1];
if (swappedUpdate.type == HPNAggregatedChangeTypeInsert && update.type == HPNAggregatedChangeTypeInsert) {
if (update.destIdx <= swappedUpdate.destIdx) ++swappedUpdate.destIdx;
else {
NSAssert(update.destIdx > 0, @"***** INTERNAL LOGIC ERROR");
--update.destIdx;
}
} else if (swappedUpdate.type == HPNAggregatedChangeTypeInsert && update.type == HPNAggregatedChangeTypeDelete) {
if (update.sourceIdx > swappedUpdate.destIdx) ++update.sourceIdx;
else {
NSAssert(update.sourceIdx < swappedUpdate.destIdx, @"***** INTERNAL LOGIC ERROR"); /* Equality case is not possible. */
NSAssert(swappedUpdate.destIdx > 0, @"***** INTERNAL LOGIC ERROR");
--swappedUpdate.destIdx;
}
} else if (swappedUpdate.type == HPNAggregatedChangeTypeDelete && update.type == HPNAggregatedChangeTypeInsert) {
if (swappedUpdate.sourceIdx >= update.destIdx) ++swappedUpdate.sourceIdx;
else ++update.destIdx;
} else {
NSAssert(swappedUpdate.type == HPNAggregatedChangeTypeDelete && update.type == HPNAggregatedChangeTypeDelete, @"***** INTERNAL LOGIC ERROR");
if (swappedUpdate.sourceIdx < update.sourceIdx) ++update.sourceIdx;
else if (swappedUpdate.sourceIdx > update.sourceIdx) {
NSAssert(swappedUpdate.sourceIdx > 0, @"***** INTERNAL LOGIC ERROR");
--swappedUpdate.sourceIdx;
}
/* In case of equality, there's nothing to do. */
}
[currentMovingUpdates exchangeObjectAtIndex:j withObjectAtIndex:j-1];
}
}
++i;
}
i = 0;
while (i < n) {
HPNCoreDataUpdate *update = currentMovingUpdates[i++];
if (!update.isMove) handler(update);
else {
NSAssert(update.linkedUpdateForMove == currentMovingUpdates[i], @"***** INTERNAL LOGIC ERROR");
handler(update.atomicMoveUpdate);
++i;
}
}

/* ********* Finally, let's remove all the registered changes as they are applied. ********* */
[self removeAllChanges];
}

@end
Loading

0 comments on commit de6eff1

Please sign in to comment.