CocoaCocoaiOSObjective-CCocoa Storage & Playback Tutorial for Realtime Apps

Requires that the Storage and Playback add-on is enabled for your key. How do I enable add-on features for my keys? - see http://www.pubnub.com/knowledge-base/discussion/644/how-do-i-enable-add-on-features-for-my-keys
PubNub's Storage and Playback feature enables developers to store messages as they are published, and retrieve them at a later time. Before using this feature it must be enabled in the PubNub Admin Console.
Being able to pull an archive of messages from storage has many applications:
  • Populate chat, collaboration and machine configuration on app load.
  • Store message streams with real-time data management.
  • Retrieve historical data streams for reporting, auditing and compliance (HIPAA, SOX, Data Protection Directive, and more).
  • Replay live events with delivery of real-time data during rebroadcasting.
As needed, specific messages can be marked "do not archive" when published to prevent archiving on a per-message basis, and storage retention time can range from 1 day to forever.
These code samples build off code defined in the Pub & Sub tutorial, so before proceeding, be sure you have completed the Pub & Sub tutorial first.
To begin, lets populate a new channel with some test publishes that we'll pull from storage using the historyForChannel method.
/**
 Subscription process results arrive to listener which should adopt to PNObjectEventListener protocol
 and registered using:
 */
[self.client addListener:self]; 
[self.client subscribeToChannels: @[@"history_channel"] withPresence:YES];


// Handle new message from one of channels on which client has been subscribed.
- (void)client:(PubNub *)client didReceiveMessage:(PNMessageResult *)message {

	// Handle new message stored in message.data.message
	if (![message.data.channel isEqualToString:message.data.subscription]) {
 
		// Message has been received on channel group stored in message.data.subscription.
	}
	else {
 
		// Message has been received on channel stored in message.data.channel.
	}

	NSLog(@"Received message: %@ on channel %@ at %@", message.data.message,
		  message.data.channel, message.data.timetoken);
}

// Handle subscription status change.
- (void)client:(PubNub *)client didReceiveStatus:(PNStatus *)status {
	 
	if (status.operation == PNSubscribeOperation) {
		 
		// Check whether received information about successful subscription or restore.
		if (status.category == PNConnectedCategory || status.category == PNReconnectedCategory) {
 
			// Status object for those categories can be casted to `PNSubscribeStatus` for use below.
			PNSubscribeStatus *subscribeStatus = (PNSubscribeStatus *)status;
			if (subscribeStatus.category == PNConnectedCategory) {
 
				// This is expected for a subscribe, this means there is no error or issue whatsoever.

				for (NSInteger i = 0; i < 50; i++) {

					NSString *message = [NSString stringWithFormat:@"message %@", @(i)];
					[self.client publish:message toChannel:@"history_channel" 
						  withCompletion:^(PNPublishStatus *status) {

						if (!status.isError) {
					 
							// Message successfully published to specified channel.
						}
						else {
					 
							/**
							 Handle message publish error. Check 'category' property to find 
							 out possible reason because of which request did fail.
							 Review 'errorData' property (which has PNErrorData data type) of status
							 object to get additional information about issue.
					 
							 Request can be resent using: [status retry];
							 */
						}
					}];
				}
			}
			else {
				 
				/**
				 This usually occurs if subscribe temporarily fails but reconnects. This means there was 
				 an error but there is no longer any issue.
				 */
			}
		}
		else if (status.category == PNUnexpectedDisconnectCategory) {

			/**
			 This is usually an issue with the internet connection, this is an error, handle 
			 appropriately retry will be called automatically.
			 */
		}
		// Looks like some kind of issues happened while client tried to subscribe or disconnected from 
		// network.
		else {

			PNErrorStatus *errorStatus = (PNErrorStatus *)status;
			if (errorStatus.category == PNAccessDeniedCategory) {

				/**
				 This means that PAM does allow this client to subscribe to this channel and channel group 
				 configuration. This is another explicit error.
				 */
			}
			else {
					
				/**
				 More errors can be directly specified by creating explicit cases for other error categories 
				 of `PNStatusCategory` such as: `PNDecryptionErrorCategory`,  
				 `PNMalformedFilterExpressionCategory`, `PNMalformedResponseCategory`, `PNTimeoutCategory`
				 or `PNNetworkIssuesCategory`
				 */
			}
		}
	}
}
In the above example, we subscribe to history_channel, and onConnect, we'll publish a barrage of test messages to it. You should see these messages as they are published on the console.
Now that we've populated storage, we can pull from storage using the historyForChannel method call:
[self.client historyForChannel: @"my_channel" start:nil end:nil limit:100
				withCompletion:^(PNHistoryResult *result, PNErrorStatus *status) {

	if (!status) {

		/**
		 Handle downloaded history using: 
			result.data.start - oldest message time stamp in response
			result.data.end - newest message time stamp in response
			result.data.messages - list of messages
		 */
	}
	else {

		/**
		 Handle message history download error. Check 'category' property
		 to find out possible reason because of which request did fail.
		 Review 'errorData' property (which has PNErrorData data type) of status
		 object to get additional information about issue.

		 Request can be resent using: [status retry];
		 */
	}
}];
The response format is
[
	["message1", "message2", "message3",... ],
	"Start Time Token",
	"End Time Token"
]
By default, historyForChannel returns the last 100 messages, first-in-first-out (FIFO). Although we specified 100 for the limit, that is the default, and if we hadn't specified a limit, we would have still received 100. Try this example again, but specify 5 for limit to see what is returned.
Setting the reverse attribute to true will return the first 100 messages, first-in-first-out (FIFO). Try this example again, but specify YES for reverse to see what is returned.
The timetoken response value is a string, representing 17-digit precision unix time (UTC). To convert PubNub's timetoken to Unix timestamp (seconds), divide the timetoken number by 10,000,000 (10^7).
Given this example, you will get the last two messages published to the channel:
[self.client historyForChannel:@"my_channel" start:nil end:nil limit:2
                withCompletion:^(PNHistoryResult *result, PNErrorStatus *status) {
 
    if (!status) {
 
        /**
         Handle downloaded history using: 
            result.data.start - oldest message time stamp in response
            result.data.end - newest message time stamp in response
            result.data.messages - list of messages
         */
    }
    else {
 
        /**
         Handle message history download error. Check 'category' property
         to find out possible reason because of which request did fail.
         Review 'errorData' property (which has PNErrorData data type) of status
         object to get additional information about issue.
 
         Request can be resent using: [status retry];
         */
    }
}];
[[498,499],14272454823679518,14272454845442942]
To page for the next 2, use the set the start attribute to start timetoken value, and request again with all other settings the same:
[[496,497],14272454791882580,14272454823679518]
As illustrated, paging with the default reverse as YES pages 2 at-a-time starting from newest, in FIFO order. If you repeat these Paging example steps with reverse as NO, you will page 2 at-a-time as well, starting instead from the oldest messages, but still in FIFO order. You will know you are at the end of history when the returned start timetoken is 0.

 
Usage!
You can call the method by passing 0 or a valid time token as the argument.
// Pull out all messages newer then message sent at 14395051270438477.
[self historyFromStartDate:@(14395051270438477) onChannel:@"history_channel" 
	   withCompletionBlock:^(NSArray *messages) {
	
	NSLog(@"Messages from history: %@", messages);
}];


- (void)historyNewerThen:(NSNumber *)beginTime onChannel:(NSString *)channelName 
	 withCompletionBlock:(void (^)(NSArray *messages))block {
	
	NSMutableArray *msgs = [NSMutableArray new];
	[self historyFromStartDate:beginTime onChannel:channelName
				  withProgress:^(NSArray *objects) {
		
		[msgs addObjectsFromArray:objects];
		if (objects.count < 100) { block(msgs); }
	}];
}

- (void)historyFromStartDate:(NSNumber *)beginTime onChannel:(NSString *)channelName 
				withProgress:(void (^)(NSArray *objects))block {
	
	__weak __typeof(self) weakSelf = self;
	[self.client historyForChannel:channelName start:beginTime end:nil limit:100 
						   reverse:NO includeTimeToken:YES 
				    withCompletion:^(PNHistoryResult *result, PNErrorStatus *status) {
					  
		__strong __typeof__(weakSelf) strongSelf = weakSelf;
		if (!status) {

			block(result.data.messages);
			if ([result.data.messages count] == 100) {

				[strongSelf historyFromStartDate:result.data.end onChannel:channelName
									withProgress:block];
			}
		}
		else {

			/**
			 Handle message history download error. Check 'category' property
			 to find out possible reason because of which request did fail.
			 Review 'errorData' property (which has PNErrorData data type) of status
			 object to get additional information about issue.

			 Request can be resent using: [status retry];
			*/
		}
	}];
}
To pull from a slice of time, just include both start and end attributes:
NSNumber *startDate = @([[NSDate dateWithTimeIntervalSinceNow:-3600] timeIntervalSince1970]);
NSNumber *endDate = @([[NSDate date] timeIntervalSince1970]);

[self.client historyForChannel:@"my_channel" start:startDate end:endDate limit:100 reverse:NO
				withCompletion:^(PNHistoryResult *result, PNErrorStatus *status) {
 
	if (!status) {
 
		/**
		 Handle downloaded history using: 
			result.data.start - oldest message time stamp in response
			result.data.end - newest message time stamp in response
			result.data.messages - list of messages
		 */
	}
	else {
 
		/**
		 Handle message history download error. Check 'category' property
		 to find out possible reason because of which request did fail.
		 Review 'errorData' property (which has PNErrorData data type) of status
		 object to get additional information about issue.
 
		 Request can be resent using: [status retry];
		 */
	}
}];

Retrieves up to 100 messages starting with the message stored before the start timetoken parameter value and older. In other words, the retrieval is from newest to oldest or in descending time direction.

first msg ------------ start tt ------------ last msg
               <------]

Retrieves up to 100 messages starting with the message stored at the end timetoken parameter (if one exists with that timetoken) value and newer. In other words, the retrieval is from oldest to newest or in ascending time direction.

first msg ------------ end tt ------------ last msg
                      [------------>

Retrieves up to 100 messages starting with the message stored after the start timetoken parameter value and newer. In other words, the retrieval is from oldest to newest or in ascending time direction.

first msg ------------ start tt ------------ last msg
                               [-------->

Retrieves up to 100 messages starting with the message stored at the end timetoken (if one exists with that timetoken) parameter value and older. In other words, the retrieval is from newest to oldest or in descending time direction.

first msg ------------ end tt ------------ last msg
                <------------]

Retrieves up to 100 messages starting with the message stored at the end timetoken (if one exists with that timetoken) parameter value and older but not older than the message stored after the start timetoken. In other words, the retrieval is from newest to oldest or in descending time direction ending with the message that comes after (is older) than the start timetoken value.

first msg ----- start tt -------- end tt ---- last msg
                        [   <-----------]