Basic caching using a file

This section shows you how to build a simple cache that stores a user's most recent messages and channels. This cache can be used to load data when a user views their channel list, or enters a channel to view their message history. Implementing even a basic cache such as this can greatly improve user experience, as users no longer encounter empty lists of channels or messages when their connectivity is unstable.

In the steps described below, we create a file per channel in the application's cache directory, write serialized data into the file to store a set amount of recent messages, configure the app to first load messages from the cache, and then finally replace them when the newest results are successfully fetched from the servers.


Serialize and deserialize SendBird objects

To store SendBird objects such as messages, channels, and users in local storage, we provide serialization and deserialization methods through our SDK. Use serialize to convert a SendBird object to binary data, which can then be natively stored in a file.

// In SBDBaseMessage.h
- (nullable NSData *)serialize;
+ (nullable instancetype)buildFromSerializedData:(NSData * _Nonnull)data;

// In SBDBaseChannel.h
- (nullable NSData *)serialize;
+ (nullable instancetype)buildFromSerializedData:(NSData * _Nonnull)data;

Save serialized messages

With serialization, you can store a channel and its most recent messages in a file. In this case, we are encoding the binary serialized data into a Base64 string. then storing each item in a new line. Normally, save data when onStop() is called in your user's chat screen.

+ (void)saveMessages:(NSArray<SBDBaseMessage *> * _Nonnull)messages channelUrl:(NSString * _Nonnull)channelUrl{
    // Serialize messages
    NSUInteger startIndex = 0;

    if (messages.count == 0) {
        return;
    }

    if (messages.count > 100) {
        startIndex = messages.count - 100;
    }

    NSMutableArray<NSString *> *serializedMessages = [[NSMutableArray alloc] init];
    for (; startIndex < messages.count; startIndex++) {
        NSString *requestId = nil;
        if ([messages[startIndex] isKindOfClass:[SBDUserMessage class]]) {
            requestId = ((SBDUserMessage *)messages[startIndex]).requestId;
        }
        else if ([messages[startIndex] isKindOfClass:[SBDFileMessage class]]) {
            requestId = ((SBDFileMessage *)messages[startIndex]).requestId;
        }

        NSData *messageData = [messages[startIndex] serialize];
        NSString *messageString = [messageData base64EncodedStringWithOptions:0];
        [serializedMessages addObject:messageString];
    }

    NSString *dumpedMessages = [serializedMessages componentsJoinedByString:@"\n"];
    NSString *dumpedMessagesHash = [[self class] sha256:dumpedMessages];

    // Save messages to temp file.
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *appIdDirectory = [documentsDirectory stringByAppendingPathComponent:[SBDMain getApplicationId]];

    NSString *uniqueTempFileNamePrefix = [[NSUUID UUID] UUIDString];
    NSString *tempMessageDumpFileName = [NSString stringWithFormat:@"%@.data", uniqueTempFileNamePrefix];
    NSString *tempMessageHashFileName = [NSString stringWithFormat:@"%@.hash", uniqueTempFileNamePrefix];

    NSString *tempMessageDumpFilePath = [appIdDirectory stringByAppendingPathComponent:tempMessageDumpFileName];
    NSString *tempMessageHashFilePath = [appIdDirectory stringByAppendingPathComponent:tempMessageHashFileName];

    NSError *errorCreateDirectory = nil;
    if ([[NSFileManager defaultManager] fileExistsAtPath:appIdDirectory] == NO) {
        [[NSFileManager defaultManager] createDirectoryAtPath:appIdDirectory withIntermediateDirectories:NO attributes:nil error:&errorCreateDirectory];
    }

    if (errorCreateDirectory != nil) {
        return;
    }

    NSString *messageFileNamePrefix = [[self class] sha256:[NSString stringWithFormat:@"%@_%@", [SBDMain getCurrentUser].userId, channelUrl]];
    NSString *messageDumpFileName = [NSString stringWithFormat:@"%@.data", messageFileNamePrefix];
    NSString *messageHashFileName = [NSString stringWithFormat:@"%@.hash", messageFileNamePrefix];

    NSString *messageDumpFilePath = [appIdDirectory stringByAppendingPathComponent:messageDumpFileName];
    NSString *messageHashFilePath = [appIdDirectory stringByAppendingPathComponent:messageHashFileName];

    // Check hash.
    NSString *previousHash;
    if (![[NSFileManager defaultManager] fileExistsAtPath:messageDumpFilePath]) {
        [[NSFileManager defaultManager] createFileAtPath:messageDumpFilePath contents:nil attributes:nil];
    }

    if (![[NSFileManager defaultManager] fileExistsAtPath:messageHashFilePath]) {
        [[NSFileManager defaultManager] createFileAtPath:messageHashFilePath contents:nil attributes:nil];
    }
    else {
        previousHash = [NSString stringWithContentsOfFile:messageHashFilePath encoding:NSUTF8StringEncoding error:nil];
    }

    if (previousHash != nil && [previousHash isEqualToString:dumpedMessagesHash]) {
        return;
    }

    // Write temp file.
    NSError *errorDump = nil;
    NSError *errorHash = nil;
    [dumpedMessages writeToFile:tempMessageDumpFilePath atomically:NO encoding:NSUTF8StringEncoding error:&errorDump];
    [dumpedMessagesHash writeToFile:tempMessageHashFilePath atomically:NO encoding:NSUTF8StringEncoding error:&errorHash];

    // Move temp to real file.
    if (errorDump == nil && errorHash == nil) {
        NSError *errorMoveDumpFile;
        NSError *errorMoveHashFile;

        [[NSFileManager defaultManager] removeItemAtPath:messageDumpFilePath error:nil];
        [[NSFileManager defaultManager] moveItemAtPath:tempMessageDumpFilePath toPath:messageDumpFilePath error:&errorMoveDumpFile];

        [[NSFileManager defaultManager] removeItemAtPath:messageHashFilePath error:nil];
        [[NSFileManager defaultManager] moveItemAtPath:tempMessageHashFilePath toPath:messageHashFilePath error:&errorMoveHashFile];

        if (errorMoveDumpFile != nil || errorMoveHashFile != nil) {
            [[NSFileManager defaultManager] removeItemAtPath:tempMessageDumpFilePath error:nil];
            [[NSFileManager defaultManager] removeItemAtPath:tempMessageHashFilePath error:nil];
            [[NSFileManager defaultManager] removeItemAtPath:messageDumpFilePath error:nil];
            [[NSFileManager defaultManager] removeItemAtPath:messageHashFilePath error:nil];
        }
    }
}

Note: In this case, SHA256 hashing is used to generate a hash file for each stored data file. Using this hash file, you can check if the newly generated data differs from the one already stored in the cache, preventing unnecessary overwriting.


Load messages with deseralization

When your user enters a chat to view their message history, load saved messages from the cache.

+ (nullable NSArray<SBDBaseMessage *> *)loadMessagesInChannel:(NSString * _Nonnull)channelUrl {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *appIdDirectory = [documentsDirectory stringByAppendingPathComponent:[SBDMain getApplicationId]];
    NSString *messageFileNamePrefix = [[self class] sha256:[NSString stringWithFormat:@"%@_%@", [SBDMain getCurrentUser].userId, channelUrl]];
    NSString *dumpFileName = [NSString stringWithFormat:@"%@.data", messageFileNamePrefix];
    NSString *dumpFilePath = [appIdDirectory stringByAppendingPathComponent:dumpFileName];

    if (![[NSFileManager defaultManager] fileExistsAtPath:dumpFilePath]) {
        return nil;
    }

    NSError *errorReadDump;
    NSString *messageDump = [NSString stringWithContentsOfFile:dumpFilePath encoding:NSUTF8StringEncoding error:&errorReadDump];

    if (messageDump.length > 0) {
        NSArray *loadMessages = [messageDump componentsSeparatedByString:@"\n"];

        if (loadMessages.count > 0) {
            NSMutableArray<SBDBaseMessage *> *messages = [[NSMutableArray alloc] init];
            for (NSString *msgString in loadMessages) {
                NSData *msgData = [[NSData alloc] initWithBase64EncodedString:msgString options:0];


                SBDBaseMessage *message = [SBDBaseMessage buildFromSerializedData:msgData];
                [messages addObject:message];
            }

            return messages;
        }
    }

    return nil;
}

After receiving an updated message list from the SendBird servers, clear the current message list and replace it with the updated list. In effect, messages from the cache are overwritten almost instantly if the user's connection is normal.


Save and load channels

The process of caching channels is identical to caching messages. For the sake of brevity, an implementation is provided without additional explanations.

// Saving channels
+ (void)saveChannels:(NSArray<SBDBaseChannel *> * _Nonnull)channels {
    // Serialize channels
    NSUInteger startIndex = 0;

    if (channels.count == 0) {
        return;
    }

    if (channels.count > 100) {
        startIndex = channels.count - 100;
    }

    NSMutableArray<NSString *> *serializedChannels = [[NSMutableArray alloc] init];
    for (; startIndex < channels.count; startIndex++) {
        NSData *channelData = [channels[startIndex] serialize];
        NSString *channelString = [channelData base64EncodedStringWithOptions:0];
        [serializedChannels addObject:channelString];
    }

    NSString *dumpedChannels = [serializedChannels componentsJoinedByString:@"\n"];
    NSString *dumpedChannelsHash = [[self class] sha256:dumpedChannels];

    // Save messages to temp file.
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *appIdDirectory = [documentsDirectory stringByAppendingPathComponent:[SBDMain getApplicationId]];

    NSString *uniqueTempFileNamePrefix = [[NSUUID UUID] UUIDString];
    NSString *tempChannelDumpFileName = [NSString stringWithFormat:@"%@_channellist.data", uniqueTempFileNamePrefix];
    NSString *tempChannelHashFileName = [NSString stringWithFormat:@"%@_channellist.hash", uniqueTempFileNamePrefix];

    NSString *tempChannelDumpFilePath = [appIdDirectory stringByAppendingPathComponent:tempChannelDumpFileName];
    NSString *tempChannelHashFilePath = [appIdDirectory stringByAppendingPathComponent:tempChannelHashFileName];

    NSError *errorCreateDirectory = nil;
    if ([[NSFileManager defaultManager] fileExistsAtPath:appIdDirectory] == NO) {
        [[NSFileManager defaultManager] createDirectoryAtPath:appIdDirectory withIntermediateDirectories:NO attributes:nil error:&errorCreateDirectory];
    }

    if (errorCreateDirectory != nil) {
        return;
    }

    NSString *channelFileNamePrefix = [NSString stringWithFormat:@"%@_channellist", [[self class] sha256:[SBDMain getCurrentUser].userId]];
    NSString *channelDumpFileName = [NSString stringWithFormat:@"%@.data", channelFileNamePrefix];
    NSString *channelHashFileName = [NSString stringWithFormat:@"%@.hash", channelFileNamePrefix];

    NSString *channelDumpFilePath = [appIdDirectory stringByAppendingPathComponent:channelDumpFileName];
    NSString *channelHashFilePath = [appIdDirectory stringByAppendingPathComponent:channelHashFileName];

    // Check hash.
    NSString *previousHash;
    if (![[NSFileManager defaultManager] fileExistsAtPath:channelDumpFilePath]) {
        [[NSFileManager defaultManager] createFileAtPath:channelDumpFilePath contents:nil attributes:nil];
    }

    if (![[NSFileManager defaultManager] fileExistsAtPath:channelHashFilePath]) {
        [[NSFileManager defaultManager] createFileAtPath:channelHashFilePath contents:nil attributes:nil];
    }
    else {
        previousHash = [NSString stringWithContentsOfFile:channelHashFilePath encoding:NSUTF8StringEncoding error:nil];
    }

    if (previousHash != nil && [previousHash isEqualToString:dumpedChannelsHash]) {
        return;
    }

    // Write temp file.
    NSError *errorDump = nil;
    NSError *errorHash = nil;
    [dumpedChannels writeToFile:tempChannelDumpFilePath atomically:NO encoding:NSUTF8StringEncoding error:&errorDump];
    [dumpedChannelsHash writeToFile:tempChannelHashFilePath atomically:NO encoding:NSUTF8StringEncoding error:&errorHash];

    // Move temp to real file.
    if (errorDump == nil && errorHash == nil) {
        NSError *errorMoveDumpFile;
        NSError *errorMoveHashFile;

        [[NSFileManager defaultManager] removeItemAtPath:channelDumpFilePath error:nil];
        [[NSFileManager defaultManager] moveItemAtPath:tempChannelDumpFilePath toPath:channelDumpFilePath error:&errorMoveDumpFile];

        [[NSFileManager defaultManager] removeItemAtPath:channelHashFilePath error:nil];
        [[NSFileManager defaultManager] moveItemAtPath:tempChannelHashFilePath toPath:channelHashFilePath error:&errorMoveHashFile];

        if (errorMoveDumpFile != nil || errorMoveHashFile != nil) {
            [[NSFileManager defaultManager] removeItemAtPath:tempChannelDumpFilePath error:nil];
            [[NSFileManager defaultManager] removeItemAtPath:tempChannelHashFilePath error:nil];
            [[NSFileManager defaultManager] removeItemAtPath:channelDumpFilePath error:nil];
            [[NSFileManager defaultManager] removeItemAtPath:channelHashFilePath error:nil];
        }
    }
}


// Loading channels
+ (nullable NSArray<SBDGroupChannel *> *)loadGroupChannels {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *channelFileNamePrefix = [NSString stringWithFormat:@"%@_channellist", [[self class] sha256:[SBDMain getCurrentUser].userId]];
    NSString *dumpFileName = [NSString stringWithFormat:@"%@.data", channelFileNamePrefix];
    NSString *appIdDirectory = [documentsDirectory stringByAppendingPathComponent:[SBDMain getApplicationId]];
    NSString *dumpFilePath = [appIdDirectory stringByAppendingPathComponent:dumpFileName];

    if (![[NSFileManager defaultManager] fileExistsAtPath:dumpFilePath]) {
        return nil;
    }

    NSError *errorReadDump;
    NSString *channelDump = [NSString stringWithContentsOfFile:dumpFilePath encoding:NSUTF8StringEncoding error:&errorReadDump];

    if (channelDump.length > 0) {
        NSArray *loadChannels = [channelDump componentsSeparatedByString:@"\n"];

        if (loadChannels.count > 0) {
            NSMutableArray<SBDGroupChannel *> *channels = [[NSMutableArray alloc] init];
            for (NSString *channelString in loadChannels) {
                NSData *channelData = [[NSData alloc] initWithBase64EncodedString:channelString options:0];

                SBDGroupChannel *channel = [SBDGroupChannel buildFromSerializedData:channelData];
                [channels addObject:channel];
            }

            return channels;
        }
    }

    return nil;
}