TwitterKit filter timeline but inversely? - twitter

I'm displaying a twitter timeline in my app via Fabric TwitterKit.
I want to filter the timeline by showing only tweets # a specific user. I've read the documentation from here on how to filter tweets in the timeline. Unfortunately for me, the function they provide will exclude any tweet that contains the specified keyword/s in the timeline.
The behavior I'm looking for is one that will include only the tweets that contain the specified keyword/s in the timeline.
Is this possible to do with TwitterKit? I've been searching for an inverse filter for this but there seems to be none.

First of all it is not clear what is your source timeline. Have you considered using TWTRSearchTimelineDataSource and configuring Search API query? The Search API supports compound queries with some logical functions, so you might be able to create a query that will cover all your needs.
Note: given the link you provided in your question I assume your target platform is iOS. You can apply similar solution for Android.
If the Search API is not powerful enough for you, your other choice is to do some client-side filtering. I was not able to find iOS source code but some version of Twitter Kit for Android is available at GitHub. If you look at BasicTimelineFilter and
FilterTimelineDelegate, you can see that in Android filtering is actually performed on the client side. So you can do the same with any custom filtering in your iOS application. All you need is create a wrapper class that will implement TWTRTimelineDataSource protocol and do your custom filtering. Here is some sketch of how the code might look like in Objective-C (you can do the same in Swift of course):
Note: beware of bugs, I haven't even compiled this code
.h file
typedef BOOL (^SOTweetFilter)(TWTRTweet * tweet);
#interface SOFilteredTimelineDataSourceWrapper : NSObject<TWTRTimelineDataSource>
- (instancetype)initWithDataSource:(id<TWTRTimelineDataSource>)dataSource filter:(SOTweetFilter)filter;
#end
.m file
#implementation SOFilteredTimelineDataSourceWrapper
#property (nonatomic, strong, readwrite) id<TWTRTimelineDataSource> wrapped;
#property (nonatomic, copy) SOTweetFilter filter;
- (instancetype)initWithDataSource:(id<TWTRTimelineDataSource>)dataSource filter:(SOTweetFilter)filter {
if(!(self = [super init])) return self;
self.wrapped = dataSource;
self.filter = filter;
return self;
}
- (void)loadPreviousTweetsBeforePosition:(nullable NSString *)position completion:(TWTRLoadTimelineCompletion)completion {
// typedef void (^TWTRLoadTimelineCompletion)(NSArray<TWTRTweet *> * _Nullable tweets, TWTRTimelineCursor * _Nullable cursor, NSError * _Nullable error);
[wrapped loadPreviousTweetsBeforePosition:position completion:^(NSArray<TWTRTweet *> * _Nullable tweets, TWTRTimelineCursor * _Nullable cursor, NSError * _Nullable error) {
if(error) {
// forward error
completion(tweets, cursor, error);
}
else {
// filter results
NSArray* filtered = [tweets filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:(id evaluatedObject, NSDictionary<NSString *,id> *bindings) {
return self.filter(evaluatedObject);
}]];
completion(filtered, cursor, error);
}
}];
}
// delegate all properties to the wrapped
- (TWTRTimelineType)timelineType {
return wrapped.timelineType;
}
-(TWTRTimelineFilter *)timelineFilter {
return wrapped.timelineFilter;
}
-(void)setTimelineFilter:(TWTRTimelineFilter *)timelineFilter {
wrapped.timelineFilter = timelineFilter;
}
- (TWTRAPIClient *)APIClient{
return wrapped.APIClient;
}
- (void)setAPIClient:(TWTRAPIClient *)APIClient{
wrapped.APIClient = APIClient;
}
#end
The main idea is that you intercept loadPreviousTweetsBeforePosition:completion: call and add your additional processing before calling the original completion callback. Using such SOFilteredTimelineDataSourceWrapper you can wrap any other TWTRTimelineDataSource and do any filtering you want analyzing TWTRTweet.text. For mentions (i.e. handle-based) filtering you might want to take a look at Android implementation of normalizeHandle.

Related

Get notified on file changes in iCloud

I create a file with name File - 1.jpg on two different devices and put it in iCloud container.
I don't use UIDocument and even though I tried to use it, it does not create a conflict. Instead what I see is that documents are being automatically renamed and moved by iCloud.
So after upload one file or another becomes File - 2.jpg. All of this is fine but now I don't have a reference to file so I have no idea which is which...
Is there any way to get notified on the app side that file was renamed/moved/deleted in iCloud?
Eventually, I had to create a class that implements NSFilePresenter and point it to iCloud container folder.
Live updates from iCloud may be quite late and happen only when iCloud pulls metadata.
Also, I had to associate each created file with each device and iCloud account and persist this data, in my case in CoreData. This is where ubiquityIdentityToken becomes useful.
All file ops in iCloud container should certainly happen using NSFileCoordinator.
For add/remove events it's better to use NSMetadataQuery, NSFileCoordinator does not report those at all, but is still useful to detect when files were moved, this is what metadata query reports as updates.
This is a very basic boilerplate that can be used as a starting point:
#interface iCloudFileCoordinator () <NSFilePresenter>
#property (nonatomic) NSString *containerID;
#property (nonatomic) NSURL *containerURL;
#property (nonatomic) NSOperationQueue *operationQueue;
#end
#implementation iCloudFileCoordinator
- (instancetype)initWithContainerID:(NSString *)containerID {
self = [super init];
if(!self) {
return nil;
}
self.containerID = containerID;
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.qualityOfService = NSQualityOfServiceBackground;
[self addFilePresenter];
return self;
}
- (void)dealloc {
[self removeFilePresenter];
}
- (void)addFilePresenter {
[NSFileCoordinator addFilePresenter:self];
}
- (void)removeFilePresenter {
[NSFileCoordinator removeFilePresenter:self];
}
#pragma mark - NSFilePresenter
#pragma mark -
- (NSURL *)presentedItemURL {
NSURL *containerURL = self.containerURL;
if(containerURL) {
return containerURL;
}
NSFileManager *fileManager = [[NSFileManager alloc] init];
containerURL = [fileManager URLForUbiquityContainerIdentifier:self.containerID];
self.containerURL = containerURL;
return containerURL;
}
- (NSOperationQueue *)presentedItemOperationQueue {
return self.operationQueue;
}
- (void)presentedSubitemAtURL:(NSURL *)oldURL didMoveToURL:(NSURL *)newURL {
NSLog(#"Moved file from %# to %#", oldURL, newURL);
}
/*
... and other bunch of methods that report on sub item changes ...
*/
#end
Use CKSubscription:
Upon initialization of CKSubscription you can specify the notification options:
CKSubscriptionOptionsFiresOnRecordCreation
CKSubscriptionOptionsFiresOnRecordDeletion
CKSubscriptionOptionsFiresOnRecordUpdate
CKSubscriptionOptionsFiresOnce
iCloud Subscriptions
These looked useful for you too:
https://www.bignerdranch.com/blog/cloudkit-the-fastest-route-to-implementing-the-auto-synchronizing-app-youve-been-working-on/
http://www.raywenderlich.com/83116/beginning-cloudkit-tutorial

iOS, GTLFramework - how to fetch all videos from a channel using pageTokens

Here's my code for retrieving list of YouTube videos from a particular channel:
GTLServiceYouTube *service;
self.vidInfos = [[NSMutableArray alloc] init];
service = [[GTLServiceYouTube alloc] init];
service.APIKey = #"my-api-key";
GTLQueryYouTube *query1 = [GTLQueryYouTube queryForPlaylistItemsListWithPart:#"id,snippet"];
query1.playlistId = #"the-playlist-id-from-utube-channel";
query1.maxResults = 50;
[service executeQuery:query1 completionHandler:^(GTLServiceTicket *ticket, id object, NSError *error) {
if (!error) {
GTLYouTubePlaylistItemListResponse *playlistItems = object;
for (GTLYouTubePlaylistItem *playlistItem in playlistItems) {
[self.vidInfos addObject:playlistItem];
}
[self.tableView reloadData];
}else {
NSLog(#"%#", error);
}
}];
And, in cellForRowAtIndexPath method:
GTLYouTubePlaylistItem *itemToDisplay = [self.vidInfos objectAtIndex:indexPath.row];
cell.textLabel.text = itemToDisplay.snippet.title;
The query accepts max 50 as the maximum result limit. But I need to display the entire list, which is about 250 videos.
How do i do it? I read about using pageTokens, but I couldn't find any sample or code on how to use pageTokens, where to get them and where to pass them?
A GTLYouTubeListResponse, that you receive after making a query, have an NSString property that is called nextPageToken.
This property indicates the 'address' of the 'next page', in case you have several 'pages' of search results,
(Meaning, the amount of search results is higher than the amount you've set in the maxResults property, which, as you've said, have a 50 results limit)
So, using your question as an example of 250 results total, you have 5 search results 'pages' of 50 search results on each 'page'.
GTLYouTubeQuery have a corresponding pageToken property, which 'tells' the query what 'page' of the results you want to receive.
Could be another ways to achieve that, but this was just at the top of my head
and I think this is pretty simple and straightforward way of achieving this,
Anyway, the example below uses your code to demonstrate how this property can be used.
IMPORTANT NOTE !!!
In the code below, I'm 'looping' through ALL of the search results,
This is just for illustration purposes,
In your app, you would probably also want to create your own custom limit,
So if the user searches for a general keyword, that have TONS of results, you won't try to fetch them all which, among other disadvantages, will probably be more than he will ever read, make an unnecessary network and memory usage, and will 'hog' your google developer points (or whatever it is called, when it 'costs' you to make google API calls)
So, if we'll use your original code-
// First, make GTLServiceYouTube a property, so you could access it thru out your code
#interface YourViewControllerName ()
#property (nonatomic, strong) GTLServiceYouTube *service;
#end
// don't forget to still initialise it in your viewDidLoad
self.vidInfos = [[NSMutableArray alloc] init];
service = [[GTLServiceYouTube alloc] init];
service.APIKey = #"my-api-key";
GTLQueryYouTube *query1 = [GTLQueryYouTube queryForPlaylistItemsListWithPart:#"id,snippet"];
query1.playlistId = #"the-playlist-id-from-utube-channel";
query1.maxResults = 50;
// After you've created the query, we will pass it as a parameter to our method
// that we will create below
[self makeYoutubeSearchWithQuery:query1];
// Here we create a new method that will actually make the query itself,
// We do that, so it'll be simple to call a new search query, from within
// our query's completion block
-(void)makeYoutubeSearchWithQuery:(GTLQueryYouTube *)query {
[service executeQuery:query completionHandler:^(GTLServiceTicket *ticket, id object, NSError *error) {
if (!error) {
GTLYouTubePlaylistItemListResponse *playlistItems = object;
for (GTLYouTubePlaylistItem *playlistItem in playlistItems) {
[self.vidInfos addObject:playlistItem];
}
[self.tableView reloadData];
}else {
NSLog(#"%#", error);
}
// Here we will check if our response, has a value in the nextPageToken
// property, meaning there are more search results 'pages'.
// If it is not nil, we will just set our query's pageToken property
// to be our response's nextPageToken, and will call this method
// again, but this time pass the 'modified' query, so we'll make a new
// search, with the same parameters as before, but that will ask the 'next'
// page of results for our search
// It is advised to add some sort of your own limit to the below if statement,
// such as '&& [self.vidInfos count] < 250'
if(playlistItems.nextPageToken) {
query.pageToken = playlistItems.nextPageToken;
[self makeYoutubeSearchWithQuery:query];
} else {
NSLog(#"No more pages");
}
}];
}
Good luck mate.

objective-c how to batch multiple read operations

I am performing multiple read operations on the same resource stored on disk.
Sometimes the read operation itself takes longer than the time between requests for that same resource. In those cases it would make sense to batch the read operations into one read request from the disk and then return the same result to the various requester.
Initially I tried caching the result from the initial fetch resource request - but this didn't work because the time it took to read the resource was too long, and new requests came in - meaning that they would try to fetch the resource as well.
Is it possible to "append" the additional requests to the ones that are already in progress?
The code I have now follows this basic structure (which isn't good enough):
-(void)fileForKey:(NSString *)key completion:(void(^)(NSData *data) {
NSData *data = [self.cache threadSafeObjectForKey:key];
if (data) {
// resource is cached - so return it - no need to read from the disk
completion(data);
return;
}
// need to read the resource from disk
dispatch_async(self.resourceFetchQueue, ^{
// this could happen multiple times for the same key - because it could take a long time to fetch the resource - all the completion handlers should wait for the resource that is fetched the first time
NSData *fetchedData = [self fetchResourceForKey:key];
[self.cache threadSafeSetObject:fetchedData forKey:key];
dispatch_async(self.completionQueue, ^{
completion(fetchedData);
return;
});
});
}
I think you want to introduce a helper object
#interface CacheHelper{
#property (nonatomic, copy) NSData *data;
#property (nonatomic, assign) dispatch_semaphore_t dataReadSemaphore;
}
Your reader method now becomes something like
CacheHelper *cacheHelper = [self.cache threadSafeObjectForKey:key]
if (cacheHelper && cacheHelper.data)
{
completion(cacheHelper.data);
return;
}
if (cacheHelper)
{
dispatch_semaphore_wait(cacheHelper.dataReadSemaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(cacheHelper.dataReadSemaphore);
completion(cacheHelper.data)
return;
}
cacheHelper = [[CacheHelper alloc] init]
cacheHelper.dataReadSemaphore = dispatch_semaphore_create(0);
cacheHelper.data = [self fetchResourceForKey:key];
[self.cache threadSafeSetObject:cacheHelper forKey:key];
dispatch_semaphore_signal(cacheHelper.dataReadSemaphore);
completion(cacheHelper.data)
This is uncompiled code, so please check the spelling and logic, but I hope it explains the idea. I like this post if you want an introduction to semaphores.

CoreData and MagicalRecord - How should I properly handle updating data only when a user hits apply?

Given the following interface:
#interface Country : NSManagedObject
#property (nonatomic, retain) NSString * name;
#property (nonatomic, retain) NSNumber * isAggressive;
#end
I have a screen where a user may see the list of Countries and toggle the isAgressive flag. The options only get saved when the user hits apply. They also have the option to hit cancel.
Based on this, I use the following code to load all the countries when the screen loads:
tempContext = [NSManagedObjectContext MR_context];
// Load our countries.
countries = [Country MR_findAllSortedBy: #"name"
ascending: YES
inContext: tempContext];
I do so in a tempContext rather than the default context, as I don't want these objects to interfere with anything else.
On a cancel, I'm not doing anything specific. Just allowing the tempContext to leave scope. On apply, I'm attempting to perform the following:
// Save changes.
[MagicalRecord saveWithBlock: ^(NSManagedObjectContext * saveLocalContext)
{
[countries enumerateObjectsUsingBlock: ^(Country * country, NSUInteger countryIndex, BOOL * stop)
{
[country MR_inContext: saveLocalContext];
}];
} completion:^(BOOL success, NSError *error) {
NSLog(#"Completed: %#, %#.", success ? #"true" : #"false", error.localizedDescription);
//This is called when data is in the store, and is called on the main thread
}];
This, however does not seem to make any changes. When running in debug, I get the following log messages:
[NSManagedObjectContext(MagicalSaves) MR_saveWithOptions:completion:](0x6000001dc020) NO CHANGES IN ** UNNAMED ** CONTEXT - NOT SAVING
Completed: false, (null).
And my updates are not being saved. How should I properly deal with the updated objects and perform the save?
The problem is that [MagicalRecord saveWithBlock... saves the defaultContext rather than your tempContext.
Try calling something like [tempContext MR_saveToPersistentStoreWithCompletion ... instead
When you call [MagicalRecord saveWithBlock:], this method creates a new context for you to perform your save operations within the block. Your use case is slightly different. You already have a scratch context to work with, so you want to use the following pattern:
NSManagedObjectContext *scratchContext = ...;
country = [Country MR_createInContext:scratchContext];
country.name = #"Belgium";
//...what ever other data is entered here.
//Somewhere in your apply method
[self.scratchContext MR_saveToPersistentStoreAndWait];
There are a few variations on the save method, have a look at the header and source code for more details. But basically, you have 2 options. The first is save and block, or wait for it to complete. The second is save in the background. You can pass in a completion block to know when the save operation is complete and if it was successful or not.

AFNetworking getting response JSON from the wrong endpoint

I am seeing a really weird and random issue in my code that I can't track down. I am getting crashes in my data model init methods when returning from AFNetworking JSON request methods. When the app does crash I am able to step back in the call stack to debug the what the JSON request/response was. The weird part is when I check the URL, request, and resonseJSON. The responseJSON does not match the expected result of the URL/request. It's like I am getting some other API methods call and data. Because the data/JSON is not what I expect the app will crash on model init.
The data I get back is usually different and not always the same. Sometimes the data is from endpoint A and sometimes it is from B, it's never consistent. It does however seem to crash consistently in the same model object.
Request endpoint A data but I get back endpoint B data. When I debug the AFHttpOperation when it crashes I see this is the result. It's almost like 2 calls are getting crossed and is some type of race condition. Below is a sample of my model object, Rest client, and model access layer.
Model Object
#implementation Applications
- (id)initWithData:(NSDictionary *)appData forLocation:(Location *)location inCategory:(Category *)category {
// appData is the JSON returned by The Rest Client and AFNetworking
self = [super init];
DDLogVerbose(#"appData = %#", appData);
if (self) {
_location = location;
_listeners = [NSMutableArray mutableArrayUsingWeakReferences];
_devices = [[NSMutableDictionary alloc] init];
_category = category;
_subscriptions = [Utility sanitizeArray:appData[#"Subscriptions"]];
}
return self;
}
#end
#implementation Location
- (void)refreshApplications {
[[Model shared] appsForLocation:self
success:^(NSObject *obj) {
self.apps = nil; //we have to get our apps again
self.apps = [NSMutableArray array];
NSArray *newApps = (NSArray *) obj;
for (NSDictionary *app in newApps) {
**// This is where it's crashing!**
Applications *newApp = [[Applications alloc] initWithData:app
forLocation:self
inCategory:[[SmartAppCategory alloc] init]];
[self.apps addObject:newApp];
}
[self notifyListeners];
}
error:nil];
}
#end
Rest Client
#interface Rest
+ (Rest *)sharedClient;
- (void)GET:(NSString *)path parameters:(NSDictionary *)params success:(SuccessCallback)sCallback error:(ErrorCallback)eCallback;
#end
#implementation Rest
+ (Rest *)sharedClient {
static dispatch_once_t token;
static Rest *shared = nil;
dispatch_once(&token, ^{
shared = [[Rest alloc] init];
});
return shared;
}
- (id)init {
self = [super init];
if (self) {
[self createClients];
}
return self;
}
- (void)createClients {
// Setup the Secure Client
// Private implementation properties
self.secureClient = [[AFOAuth2Client alloc] initWithBaseURL:baseUrl clientID:OAUTH2_CLIENT_ID secret:OAUTH2_CLIENT_SECRET];
[self.secureClient setParameterEncoding:AFJSONParameterEncoding];
AFOAuthCredential *credential = (AFOAuthCredential *) [NSKeyedUnarchiver unarchiveObjectWithData:[KeyChainStore dataForKey:KEYCHAIN_SETTINGS_AFOAuthCredential]];
if (credential) {
[self.secureClient setAuthorizationHeaderWithToken:credential.accessToken];
}
// Setup the Anonymous Client
self.anonymousClient = [[AFHTTPClient alloc] initWithBaseURL:baseUrl];
[self.anonymousClient setParameterEncoding:AFJSONParameterEncoding];
[self.anonymousClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
}
- (void)GET:(NSString *)path parameters:(NSDictionary *)params success:(SuccessCallback)sCallback error:(ErrorCallback)eCallback {
[_secureClient getPath:path
parameters:params
success:^(AFHTTPRequestOperation *operation, id responseObject) {
DDLogVerbose(#"Success Path: %# JSON: %#", path, responseObject);
if (sCallback) sCallback(responseObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[Rest callErrorBlock:eCallback withOperation:operation];
}];
}
#end
Model Access Layer
#interface Model
+ (Model *)shared;
- (void)appsForLocation:(Location *)location success:(SuccessCallback)success error:(ErrorCallback)error;
#end
#implementation Model
- (void)appsForLocation:(Location *)location success:(SuccessCallback)success error:(ErrorCallback)error {
NSString *path = [NSString stringWithFormat:#"/api/locations/%#/apps/", location.locationId];
[[Rest sharedClient] GET:path parameters:nil success:success error:error];
}
#end
A Location is a root object in the application and it will be told to refresh often. Either through UI interaction, events, or data Deserialization the the refreshApplications will execute to get more data from the server. Meanwhile other requests and events are going on in the application to get and send data to the API is JSON. Some of these GET calls to other endpoints seem to be messing with the response data.
Questions
How could this be happening with AFNetworking?
Am I being too quick to blame AFNetowrking and should I be looking for other places in my system that could be crossing the responses? I do have a load balanced backend hosted at Amazon.
Is this an endpoint issue?
How can I better debug and reproduce this issue? It only comes up at random times and is very hard to replicate. I have to continually run and re-run the application in hopes that it is crash.
Are there any advanced debugging techniques that I can use to back trace this call/crash using xcode?
I recommend that you use Charles proxy to double-check that the data you're receiving is correct. There's a trial version available that works identically to the registered version for 30 days. My first guess is that there's either some sort of buggy cache layer between you and your server, or your server is buggy. An HTTP proxy like Charles will allow you to confirm or reject this hypothesis.
This page explains how to set up Charles to proxy non-HTTPS connections from iOS devices.
To debug non-HTTPS as well as HTTPS Traffic use the mitmproxy
It allows you to inspect all packages and also resend them and much more.
With this you can check what really happens and if the backend is the problem or if AFNetworking has a Bug.
And as a cool side effect mitmproxy is totally free and Open-Sourced under the MIT Licensed.
On their website you will find some handy tutorials specific for iOS.

Resources