diff --git a/.gitmodules b/.gitmodules index c84eafd..9a8e766 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "Objective-C"] - path = Objective-C +[submodule "basic/Objective-C"] + path = basic/Objective-C url = git@github.com:smilefam/SendBird-iOS-ObjectiveC.git -[submodule "Swift"] - path = Swift +[submodule "basic/Swift"] + path = basic/Swift url = git@github.com:smilefam/SendBird-iOS-Swift.git diff --git a/basic/Objective-C b/basic/Objective-C new file mode 160000 index 0000000..74aca14 --- /dev/null +++ b/basic/Objective-C @@ -0,0 +1 @@ +Subproject commit 74aca144f3c215ce185e96173620ef5bbf850d99 diff --git a/basic/Swift b/basic/Swift new file mode 160000 index 0000000..2e03a93 --- /dev/null +++ b/basic/Swift @@ -0,0 +1 @@ +Subproject commit 2e03a93c08b4a119b4f5e18965a5dc087d050ca1 diff --git a/syncmanager/README.md b/syncmanager/README.md index d25aaef..4b1a7eb 100644 --- a/syncmanager/README.md +++ b/syncmanager/README.md @@ -1,5 +1,5 @@ -# SendBird-iOS-LocalCache-Sample -The repository for a sample project that use `SendBird SDK Manager` for **LocalCache**. Manager offers an event-based data management so that each view would see a single spot by subscribing data event. And it stores the data into database which implements local caching for faster loading. +# SendBird SyncManager Sample +The repository for a sample project that use `SendBird SyncManager` for **LocalCache**. Manager offers an event-based data management so that each view would see a single spot by subscribing data event. And it stores the data into database which implements local caching for faster loading. ## SendBird SyncManager Framework Refers to [SendBird SyncManager Framework](https://github.com/smilefam/sendbird-syncmanager-ios) @@ -40,157 +40,416 @@ Now you can see installed SendBird framework by inspecting YOUR_PROJECT.xcworksp ## Usage ### Initialization +`SBSMSyncManager` is singlton class. And when `SBSMSyncManager` was initialized, a instance for `Database` is set up. So if you want to initialize `Database` as soon as possible, call `setup(_:)` first just after you get a user's ID. we recommend it is in `application(_:didFinishLaunchingWithOptions:)`. +```swift +// AppDelegate.swift +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + ... // get user's ID + SBSMSyncManager.setup(withUserId: userId) + ... +} +``` ```objc // AppDelegate.m -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... - [SBSMChannelManager sharedInstance]; + [SBSMSyncManager setupWithUserId:userId]; ... } ``` -### Channel Manager +### Collection + +`Collection` is a container to manage objects(channels, messages) related to a view. `SBSMChannelCollection` is attached to channel list view contoller and `SBSMMessageCollection` is attached to message list view contoller accordingly. The main purpose of `Collection` is, + +- To listen data event and deliver it as view event. +- To fetch data from cache or SendBird server and deliver the data as view event. + +To meet the purpose, each collection has event subscriber and data fetcher. Event subscriber listens data event so that it could apply data update into view, and data fetcher loads data from cache or server and sends the data to event handler. +#### Channel Collection +Channel is quite mutable data where chat is actively going - channel's last message and unread message count may update very often. Even the position of each channel is changing drastically since many apps sort channels by the most recent message. For that reason, `SBSMChannelCollection` depends mostly on server sync. Here's the process `SBSMChannelCollection` synchronizes data: + +1. It loads channels from cache and the view shows them. +2. Then it fetches the most recent channels from SendBird server and merges with the channels in view. +3. It fetches from SendBird server every time `fetch(_:)` is called in order to view previous channels. + +> Note: Channel data sync mechanism could change later. + +`SBSMChannelCollection` requires `SBDGroupChannelListQuery` instance of [SendBirdSDK](https://github.com/smilefam/sendbird-ios-framework) as it binds the query into the collection. Then the collection filters data with the query. Here's the code to create new `SBSMChannelCollection` instance. + +```swift +// swift +let query: SBDGroupChannelListQuery? = SBDGroupChannel.createMyGroupChannelListQuery() +// ...setup your query here +let channelCollection: SBSMChannelCollection? = SBSMChannelCollection.init(query: query) +``` ```objc -// GroupChannelListViewController.h +// objective-c +SBDGroupChannelListQuery *query = [SBDGroupChannel createMyGroupChannelListQuery]; +// ...setup your query here +SBSMChannelCollection *channelCollection = [SBSMChannelCollection collectionWithQuery:query]; +``` -#import -#import "SyncManager.h" +If the view is closed, which means the collection is obsolete and no longer used, remove collection explicitly. -@interface GroupChannelListViewController : UIViewController -@end +```swift +// swift +channelCollection.remove() +``` +```objc +// objective-c +[channelCollection remove]; +``` -// GroupChannelListViewController.m +As aforementioned, `SBSMChannelCollection` provides event handler with delegate. Event handler is named as `SBSMChannelCollectionDelegate` and it receives `SBSMChannelEventAction` and list of `channels` when an event has come. The `SBSMChannelEventAction` is a keyword to notify what happened to the channel list, and the `channel` is a kind of `SBDGroupChannel` instance. You can create an view controller instance and implement the event handler and add it to the collection. + +```swift +// swift + +// add delegate +channelCollection?.delegate = self + +// channel collection delegate +func collection(_ collection: SBSMChannelCollection, didReceiveEvent action: SBSMChannelEventAction, channels: [SBDGroupChannel]) { + switch (action) { + case SBSMChannelEventAction.insert: + // Insert channels on list + break + case SBSMChannelEventAction.update: + // Update channels of list + break + case SBSMChannelEventAction.remove: + // Remove channels of list + break + case SBSMChannelEventAction.move: + // Move channel of list + break + case SBSMChannelEventAction.clear: + // Clear(Remove all) channels + break + case SBSMChannelEventAction.none: + break + default: + break + } +} +``` +```objc +// objective-c -@implementation GroupChannelListViewController +// add delegate +channelCollection.delegate = self; -- (void)collection:(id _Nonnull)collection - updatedItmes:(nonnull NSArray > *)updatedItems - action:(SBSMChangeLogAction)action - error:(nullable NSError *)error { - switch(action) { - case SBSMChangeLogActionPrepend: - // Update UI - break; - case SBSMChangeLogActionAppend: - // Update UI +// channel collection delegate +- (void)collection:(SBSMChannelCollection *)collection didReceiveEvent:(SBSMChannelEventAction)action channels:(NSArray *)channels { + switch (action) { + case SBSMChannelEventActionInsert: { + // Insert channels on list break; - case SBSMChangeLogActionNew: - // Update UI + } + case SBSMChannelEventActionUpdate: { + // Update channels of list break; - case SBSMChangeLogActionChanged: - // Update UI + } + case SBSMChannelEventActionRemove: { + // Remove channels of list break; - case SBSMChangeLogActionDeleted: - // Update UI + } + case SBSMChannelEventActionMove: { + // Move channel of list break; - case SBSMChangeLogActionMoved: - // Update UI + } + case SBSMChannelEventActionClear: { + // Clear(Remove all) channels break; - case SBSMChangeLogActionCleared: - // Update UI + } + case SBSMChannelEventActionNone: + default: { break; + } } } +``` -// ... -// to load channels -SBDGroupChannelListQuery *query = [SBDGroupChannel createMyGroupChannelListQuery]; -// ...setup your query here +And data fetcher. Fetched channels would be delivered to delegate method. fetcher determines the `SBSMChannelEventAction` automatically so you don't have to consider duplicated data in view. -SBSMChannelCollection *collection = [SBSMChannelManager createChannelCollectionWithQuery:query]; -[collection loadWithFinishHandler:^(BOOL finished) { - // This callback is useful only to check the end of loading. - // The fetched channels would be translated into change logs and delivered to subscription. +```swift +// swift +channelCollection.fetch(completionHandler: {(error) in + // This callback is optional and useful to catch the moment of loading ended. +}) +``` +```objc +// objective-c +[channelCollection fetchWithCompletionHandler:^(SBDError * _Nullable error) { + // This callback is optional and useful to catch the moment of loading ended. }]; ``` -### Message Manager +#### Message Collection +Message is relatively static data and SyncManager supports full-caching for messages. `SBSMMessageCollection` conducts background synchronization so that it synchronizes all the messages until it reaches to the first message. Background synchronization does NOT affect view directly but store for local cache. For view update, explicitly call `fetch(_:_:)` with direction which fetches data from cache and sends the data into collection handler. + +Background synchronization ceases if the synchronization is done or synchronization request is failed. + +> Note: Background synchronization run in background thread. + +For various viewpoint(`viewpointTimestamp`) support, `SBSMMessageCollection` sets a timestamp when to fetch messages. The `viewpointTimestamp` is a timestamp to start background synchronization in both previous and next direction (and also the point where a user sees at first). Here's the code to create `SBSMMessageCollection`. +```swift +// swift +let filter: SBSMMessageFilter = SBSMMessageFilter.init(messageType: SBDMessageTypeFilter, customType: customTypeFilter, senderUserIds: senderUserIdsFilter) +let viewpointTimestamp: Int64 = getLastReadTimestamp() +// or LONG_LONG_MAX if you want to see the most recent messages + +let messageCollection: SBSMMessageCollection? = SBSMMessageCollection.init(channel: channel, filter: filter, viewpointTimestamp: viewpointTimestamp) +``` ```objc -// GroupChannelChattingViewController.h -#import -#import "SyncManager.h" - -@interface GroupChannelChattingViewController : UIViewController -@end - -// GroupChannelChattingViewController.m -@implementation GroupChannelChattingViewController - -- (void)collection:(id _Nonnull)collection - updatedItmes:(nonnull NSArray > *)updatedItems - action:(SBSMChangeLogAction)action - error:(nullable NSError *)error { - switch(action) { - case SBSMChangeLogActionPrepend: - break; - case SBSMChangeLogActionAppend: - break; - case SBSMChangeLogActionNew: +// objective-c +SBSMMessageFilter *filter = [SBSMMessageFilter filterWithMessageType:SBDMessageTypeFilter customType:customtypeFilter senderUserIds:senderUserIdsFilter]; +long long viewpointTimestamp = getLastReadTimestamp(); +// or LONG_LONG_MAX if you want to see the most recent messages + +SBSMMessageCollection *messageCollection = [SBSMMessageCollection collectionWithChannel:self.channel filter:filter viewpointTimestamp:LONG_LONG_MAX]; +``` + +You can dismiss collection when the collection is obsolete and no longer used. + +```swift +// swift +messageCollection.remove() +``` +```objc +[messageCollection remove]; +``` + +`SBSMMessageCollection` has event handler for delegate that you can implement and add to the collection. Event handler is named as `SBSMMessageCollectionDelegate` and it receives `SBSMMessageEventAction` and list of `messages` when an event has come. The `SBSMMessageEventAction` is a keyword to notify what happened to the message, and the `message` is a kind of `SBDBaseMessage` instance of [SendBird SDK](https://github.com/smilefam/sendbird-ios-framework). + +```swift +// swift + +// add delegate +messageCollection.delegate = self + +// message collection delegate +func collection(_ collection: SBSMMessageCollection, didReceiveEvent action: SBSMMessageEventAction, messages: [SBDBaseMessage]) { + switch action { + case SBSMMessageEventAction.insert: + self.chattingView?.insert(messages: messages, completionHandler: nil) + break + case SBSMMessageEventAction.update: + self.chattingView?.update(messages: messages, completionHandler: nil) + break + case SBSMMessageEventAction.remove: + self.chattingView?.remove(messages: messages, completionHandler: nil) + break + case SBSMMessageEventAction.clear: + self.chattingView?.clearAllMessages(completionHandler: nil) + break + case SBSMMessageEventAction.none: + break + default: + break + } +} +``` +```objc +// objective-c + +// add delegate +messageCollection.delegate = self; + +// message collection delegate +- (void)collection:(SBSMMessageCollection *)collection didReceiveEvent:(SBSMMessageEventAction)action messages:(NSArray *)messages { + switch (action) { + case SBSMMessageEventActionInsert: { + // break; - case SBSMChangeLogActionChanged: + } + case SBSMMessageEventActionUpdate : { + // break; - case SBSMChangeLogActionDeleted: + } + case SBSMMessageEventActionRemove: { + // break; - case SBSMChangeLogActionMoved: + } + case SBSMMessageEventActionClear: { + // break; - case SBSMChangeLogActionCleared: + } + case SBSMMessageEventActionNone: + default: break; } } +``` -// ... -// to load messages -SBDGroupChannel *channel; // channel of messages -NSDictionary filter = @{}; // compose your own filter +`SBSMMessageCollection` has data fetcher by direction: `SBSMMessageDirection.previous` and `SBSMMessageDirection.next`. It fetches data from cache only and never request to server directly. If no more data is available in a certain direction, it wait for the background synchronization internally and fetches the synced messages right after the synchronization progresses. -SBSMMessageCollection* collection = [SBSMMessageManager createMessageCollectionWithChannel:channel - filter:filter]; -[collection loadPreviousMessagesWithFinishHandler:^(BOOL finished) { - // This callback is useful only to check the end of loading. - // The fetched messages would be translated into change logs and delivered to subscription. +```swift +// swift +messageCollection.fetch(in: SBSMMessageDirection.previous, completionHandler: { (error) in + // Fetching from cache is done +}) +messageCollection.fetch(in: SBSMMessageDirection.next, completionHandler: { (error) in + // Fetching from cache is done +}) +``` +```objc +// objective-c +[messageCollection fetchInDirection:SBSMMessageDirectionPrevious completionHandler:^(SBDError * _Nullable error) { + // Fetching from cache is done +}]; +[messageCollection fetchInDirection:SBSMMessageDirectionNext completionHandler:^(SBDError * _Nullable error) { + // Fetching from cache is done }]; ``` -SyncManager listens message event handlers such as `didReceiveMessage`, `didUpdateMessage`, `didDeleteMessage`, and applies the change automatically. But they would not be called if the message is sent by `currentUser`. You can keep track of the message in callback instead. SyncManager provides some methods to apply the message event to collections. +Fetched messages would be delivered to delegate. fetcher determines the `SBSMMessageEventAction` automatically so you don't have to consider duplicated data in view. + +#### Handling uncaught messages + +SyncManager listens message event such as `channel(_:didReceive:)` and `channel(_:didUpdate:)`, and applies the change automatically. But they would not be called if the message is sent by `currentUser`. You can keep track of the message by calling related function when the `currentUser` sends or updates message. `SBSMMessageCollection` provides methods to apply the message event to collections. + +```swift +// swift +// call collection.appendMessage() after sending message +var previewMessage: SBDUserMessage? +channel.sendUserMessage(with: params, completionHandler: { (theMessage, theError) in + guard let message: SBDUserMessage = theMessage, let _: SBDError = theError else { + // delete preview message if sending message fails + messageCollection.deleteMessage(previewMessage) + return + } + + messageCollection.appendMessage(message) +}) + +if let thePreviewMessage: SBDUserMessage = previewMessage { + messageCollection.appendMessage(thePreviewMessage) +} + + +// call collection.updateMessage() after updating message +channel.sendUserMessage(with: params, completionHandler: { (theMessage, error) in + guard let message: SBDUserMessage = theMessage, let _: SBDError = error else { + return + } + + messageCollection.updateMessage(message) +}) +``` ```objc -// call [SBSMMessageManager appendMessage] after sending message -SBUserMessageParams *params = [[SBUserMessageParams alloc] init]; -params.message = @"your message"; -[channel sendUserMessage:param - completionHandler:^(SBDBaseMessage * _Nullable message, SBDError * _Nullable error) { - if(error == nil) { - [SBSMMessageManager appendMessage:message]; +// objective-c + +// call [collection appendMessage:] after sending message +__block SBDUserMessage *previewMessage = [channel sendUserMessageWithParams:params completionHandler:^(SBDUserMessage * _Nullable userMessage, SBDError * _Nullable error) { + if (error != nil) { + [messageCollection deleteMessage:previewMessage]; + return; } + + [self.messageCollection appendMessage:userMessage]; }]; -// call [SBSMMessageManager updateMessage] after updating message -SBUserMessageParams *params = [[SBUserMessageParams alloc] init]; -params.message = @"your message"; -[channel updateUserMessage:message.messageId, - params:params - completionHandler:^(SBDBaseMessage * _Nullable message, SBDError * _Nullable error) { - if(error == nil) { - [SBSMMessageManager updateMessage:message]; - } +if (previewMessage.requestId != nil) { + [messageCollection appendMessage:previewMessage]; +} + + +// call [collection updateMessage:] after updating message +[channel sendUserMessageWithParams:params completionHandler:^(SBDUserMessage * _Nullable userMessage, SBDError * _Nullable error) { + [self.messageCollection updateMessage:userMessage]; }]; +``` + +It works only for messages sent by `currentUser` which means the message sender should be `currentUser`. + +### Connection Lifecycle + +You should let SyncManager start synchronization after connect to SendBird. Call `resumeSynchronization()` on connection, and `pauseSynchronization()` on disconnection. Here's the code: + +```swift +// swift +let manager: SBSMSyncManager = SBSMSyncManager() +manager.resumeSynchronize() -// call [SBSMMessageManager deleteMessage] after deleting message -[channel deleteMessage:message - completionHandler:^(SBDBaseMessage * _Nullable message, SBDError * _Nullable error) { - if(error == nil) { - [SBSMMessageManager deleteMessage:message]; +let manager: SBSMSyncManager = SBSMSyncManager() +manager.pauseSynchronize() +``` +```objc +// objective-c +SBSMSyncManager *manager = [SBSMSyncManager manager]; +[manager resumeSynchronize]; + +SBSMSyncManager *manager = [SBSMSyncManager manager]; +[manager pauseSynchronize]; +``` + +The example below shows relation of connection status and resume synchronization. + +```swift +// swift + +// Request Connect to SendBird +SBDMain.connect(withUserId: userId) { (user, error) in + if let theError: NSError = error { + return + } + + let manager: SBSMSyncManager = SBSMSyncManager() + manager.resumeSynchronize() +} + +// SendBird Connection Delegate +func didSucceedReconnection() { + let manager: SBSMSyncManager = SBSMSyncManager() + manager.resumeSynchronize() +} +``` +```objc +// objective-c + +// Request Connect to SendBird +[SBDMain connectWithUserId:userId completionHandler:^(SBDUser * _Nullable user, SBDError * _Nullable error) { + if (error != nil) { + // + return; } + + SBSMSyncManager *manager = [SBSMSyncManager manager]; + [manager resumeSynchronize]; }]; + +// SendBird Connection Delegate +- (void)didSucceedReconnection { + SBSMSyncManager *manager = [SBSMSyncManager manager]; + [manager resumeSynchronize]; +} ``` -Once it is delivered to the collection, it'd not only apply the change into the current collection but also propagate the event into other collections so that the change could apply to other views automatically. It works only for messages sent by `currentUser`(from `[SBDMain getCurrentUser]`) which means the message sender should be `currentUser`. +### Cache clear -### Connection Lifecycle +Clearing cache is necessary when a user signs out (called `disconnect()` explicitly). + +```swift +// swift +SBDMain.disconnect { + let manager: SBSMSyncManager = SBSMSyncManager() + manager.clearCache() +} +``` +```objc +// objective-c +[SBDMain disconnectWithCompletionHandler:^{ + [[SBSMSyncManager manager] clearCache]; +}]; +``` + +> WARNING! DO NOT call `SBDMain.removeAllChannelDelegates()`. It does not only remove handlers you added, but also remove handlers managed by SyncManager. -Connection may not be stable in some environment. If SendBird recognizes disconnection, it would take steps for reconnection and manager would catch it and sync data automatically when the connection is back. For those who call `[SBDMain disconnectWithCompletionHandler:]` and `[SBDMain connectWithUserId:accessToken:completionHandler:]` explicitly to manage the lifecycle by their own, Manager provides methods `[SBSMChannelManager start]`, `[SBSMMessageManager start]` and `[SBSMChannelManager stop]`, `[SBSMMessageManager stop]` to acknowledge the event and do proper action in order to sync content.