Documentation : Conflicts - iOS

This example presents conflicts arise when two users modify the same data.

Application consists of only one model, which has two fields. In order to show conflicts, one of the field will be modified by both users and then synchronized. Records, which are conflicted, will be marked with red background color. Main screen of application contains list of model entities and two buttons. User can add new entity, edit it's value and synchronize with mobeelizer cloud. There are two users in the system and there's a possibility to switch them on application life time.

Design Your App

First thing we have to do is create new application in Mobeelizer App Designer, called 'Conflicts'.


This example is focused on conflicts so set conflict resolving to 'manual' mode.


Next we will configure model. We have to go to 'Models' section, create new one and call it 'ConflictsEntity'.


Last thing to finish this step is to create two fields. Fields properties are specified in table below. Note that you shoud use default model credentials (CRUD operations avaliable for everyone).

NameRequired propertyTypeDefault value propertyAdditional propertiesCredentials
titleyestext--default
scoreyesinteger1min: 1, max: 5default

Next section to configure is 'Groups & Roles', by default there is one group called 'users' and one device category 'mobile' and this two together create role 'users-mobile'. This default configuration is perfect for our example and you don't have to change it. 
When everything is done in Create mode, deploy our application to test environment, and create two users with passwords:

  • a - usera
  • b - userb

Finally, download configured template for iOS project. Change class prefix to MD and model prefix to MMD.

Use the Mobeelizer SDK

Open project in XCode. We can see that several files has been generated and configured.  

Configure AppDelegate

First of all, there is app delegate with configured creation and destruction of Mobeelizer object. Generated application does not have any view. We will add storyboard: MDStoryboad to our application and set newly created storyboard as Main Storyboard in project settings. Furthermore, we have to remove setting new window from application: didFinishLaunchingWithOptions: in MDAppDelegate

MDAppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Initialize Mobeelizer SDK
    [Mobeelizer create];
 
    return YES;
}
- (void)applicationWillTerminate:(UIApplication *)application
{   
    // Terminate Mobeelizer SDK
    [Mobeelizer destroy];
}

MDAppDelegate.m

Configure Mobeelizer entity

Both application.xml and Mobeelizer.plist were generated. In Mobeelizer.plist we have to change mode from development to production. Moreover, model has been generated for ConflictsEntity. We will add guid,  owner and conflicted fields together with custom costructor. We will also overwrite isEqual function to compare guid.

MMDConflictsEntity.h
@property(strong, nonatomic) NSString* guid;
@property(strong, nonatomic) NSString* owner;
@property(nonatomic) BOOL conflicted;
@property(strong, nonatomic) NSString* title;
@property(nonatomic) NSUInteger score;

- (MMDConflictsEntity*) initWithTitle:(NSString*)title andScore:(NSUInteger)score;
MMDConflictsEntity.m
- (MMDConflictsEntity*) initWithTitle:(NSString*)title andScore:(NSUInteger)score{
    self.title = title;
    self.score = score;
    return self;
}

- (BOOL)isEqual:(id)object {
    if ([object isKindOfClass:[MMDConflictsEntity class]]) {
        MMDConflictsEntity* entity = object;
        if ([entity.guid isEqualToString:self.guid] && [entity.title isEqualToString:self.title] && entity.score == self.score) {
            return YES;
        }
    }
    return NO;
}

MMDConflictsEntity.h MMDConflictsEntity.m

Create user login and logout manager

Next, we will handle user functionality. We will create singleton class MDUserManager, which will hold information about currently logged user. There are two use cases to handle user functionality. Firstly, when application starts user 'a' will be logged in. Secondly, during runtime, users can be switched.

MDUserManager.h
@property (copy, nonatomic) NSString* user;
 
+ (SSUserManager*)instance;
- (BOOL) performLoginAsUser:(NSString *)login;
- (void) switchUser;
MDUserManager.m
- (BOOL) performLoginAsUser:(NSString *)login{
    self.user = login;
    NSString* password = nil;
    if ([login isEqualToString:USER_A]) {
        password = @"usera";
    } else {
        password = @"userb";
    }
 
	//Login to Mobeelizer instance
    MobeelizerOperationError *error = [Mobeelizer loginToInstance:@"test" withUser:login andPassword:password];
 
    if(error != nil) {
        NSLog(@"Joining failed: %@ - %@", error.code, error.message);
    }
    return error == nil;
}
 
-(void) switchUser{
    if ([self.user isEqualToString:USER_A]) {
        self.user = USER_B;
    } else {
        self.user = USER_A;
    }
 
    [self performLogout];
    [self performLoginAsUser:self.user];
}
 
- (void) performLogout {
	//Logout from Mobeelizer
    [Mobeelizer logout];
}

MDUserManager.h MDUserManager.m

Next, we will move to user interface. Add a new file to project. Choose the UITableViewController subclass template and name the class MDUserContextController. We will add button to navigation bar, which will handle switching users. Controller will hold reference to button, because we will change appearance of button to show currently logged user.

MDUserContextController.h
#define RGB(r, g, b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1]
#define USER_A_COLOR RGB(255, 128, 0)
#define USER_B_COLOR RGB(0, 128, 0)
 
@property (strong, nonatomic) UIBarButtonItem *userButton;
MDUserContextController.m
- (void)viewDidLoad{
    [super viewDidLoad];
    [[self navigationController] setNavigationBarHidden:NO animated:NO];
    userButton = [[UIBarButtonItem alloc] initWithTitle:@"User" style:UIBarButtonItemStyleBordered target:self action:@selector(userButtonClicked:)];
    self.navigationItem.rightBarButtonItem = userButton;
 
    [[MDUserManager instance] performLoginAsUser:USER_A];
}

Now add function to handle button click:

MDUserContextController.m
- (IBAction)userButtonClicked:(id)sender {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[MDUserManager instance] switchUser];
 
        dispatch_async(dispatch_get_main_queue(), ^{
            [self refreshUserButton];
 
            [UIView beginAnimations:@"View Flip" context:nil];
            [UIView setAnimationDuration:0.75];
            [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
            [UIView setAnimationTransition: UIViewAnimationTransitionFlipFromRight forView:self.view cache:YES];
            [self reloadData];
            [UIView commitAnimations];
        });
    });
}
 
- (void)refreshUserButton {
    if ([[MDUserManager instance].user isEqualToString:USER_B]) {
        [userButton setTitle:@"B"];
        [userButton setTintColor:USER_B_COLOR];
    } else {
        [userButton setTitle:@"A"];
        [userButton setTintColor:USER_A_COLOR];
    }
}

MDUserContextController.h MDUserContextController.m

Currently, if any controller extends MDUserContextController, it will have functionality of switching users.

Create entity table view controller

Now, we will create view. Add navigation controller with table view controller to storyboard. Set prototype cell identifier to ConflictsSyncCell and set style to custom. Then set accessory to disclosure indicator. Add label to prototype cell, change its name to Title and set tag value to 1. Then add view to prototype cell and set tag value to 2. View will be placeholder for stars - rating of movie. Next, add a new file to the project. Choose the MDUserContextController subclass template and name the class MDConflictsController. Connect newly created controller to its design in storyboard. MDConflictsController will hold an array of table records. Moreover, we will mark added records with user image. Records will be ranked from 1 to 5 stars.

MDConflictsController.h
@property(strong, nonatomic) NSMutableArray *currentItems;
@property(strong, nonatomic) UIImage *userAImage;
@property(strong, nonatomic) UIImage *userBImage;
@property(strong, nonatomic) UIImage *starImage;
@property(nonatomic) int imageSize;
MDConflictsController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    [self addToolbarButtons];

    self.currentItems = [[NSMutableArray alloc] initWithArray:[self getItemsList]];
    userAImage = [UIImage imageNamed:@"userA.png"];
    userBImage = [UIImage imageNamed:@"userB.png"];
    starImage = [UIImage imageNamed:@"star_40.png"];
    CGSize size = CGSizeMake(20, 20);
    UIGraphicsBeginImageContext(size);
    [starImage drawInRect:CGRectMake(0, 0, size.width, size.height)];
    starImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    imageSize = 10;
}
-(void)addToolbarButtons{
    UIBarButtonItem* newButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(newClicked:)];
    [newButton setStyle:UIBarButtonItemStyleBordered];
    UIBarButtonItem *space = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
    [space setWidth:10];
    UIBarButtonItem* syncButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(syncClicked:)];
    [syncButton setStyle:UIBarButtonItemStyleBordered];

    self.toolbarItems = @[newButton, space, syncButton];
    self.navigationController.toolbarHidden = NO;
}

Current items array is intialized with MMDConflictsEntities from Mobeelizer database.

MDConflictsController.m
- (NSArray*)getItemsList {
	//Get MMDConflictsEntity from Mobeelizer database
    MobeelizerCriteriaBuilder *criteria = [[Mobeelizer database] find:[MMDConflictsEntity class]];
    criteria = [criteria addOrder:[MobeelizerOrder asc:@"title"]];
    return [criteria list];
}

Creating new item takes random movie and creates MMDConflictsEntity. We will introduce two objects MDMovie and MDMovies. MDMovie is simple object consisting of title and director property. 

MDMovie.h
@property(copy, nonatomic) NSString* title;
@property(copy, nonatomic) NSString* director;

-(MDMovie*) initWithDictionary:(NSDictionary*)dictionary;

 

MDMovie.m
-(MDMovie*) initWithDictionary:(NSDictionary*)dictionary {
    self.title = [dictionary valueForKey:@"title"];
    self.director = [dictionary valueForKey:@"director"];
    return self;
}

 MDMovie.h MDMovie.m

 MDMovies is singleton, which generates random movie from sample plist. 

MDMovies.m
- (id)init {
    if (movies == nil) {
        self = [super init];
        if (self) {
            NSString *path = [[NSBundle mainBundle] pathForResource:@"Movies" ofType:@"plist"];
            movies = [[NSArray alloc] initWithContentsOfFile:path];
        }
    }
    return self;
}

- (MDMovie*)getRandomMovie {
    int randomIndex = arc4random() % [movies count];
    return [[MDMovie alloc] initWithDictionary:movies[randomIndex]];
} 

MDMovies.h MDMovies.m

New entity is saved in Mobeelizer database and then is inserted into table. Movie titles are sorted, that is why we insert new title into correct place.

MDConflictsController.m
- (void)newClicked:(id)sender {
    MDMovie* movie = [[MDMovies instance] getRandomMovie];
    NSUInteger score = (arc4random() % 5) + 1;
    MMDConflictsEntity* newItem = [[MMDConflictsEntity alloc] initWithTitle:movie.title andScore:score];

	//Save entity in Mobeelizer database
    [[Mobeelizer database] save:newItem];
    
    currentItems = [[NSMutableArray alloc] initWithArray:[self getItemsList]];

    NSInteger row = -1;
    for (NSInteger i=0; i<currentItems.count; i++) {
        if ([newItem isEqual:(MMDConflictsEntity*)currentItems[i]]) {
            row = i;
            break;
        }
    }

    NSIndexPath* indexPath = [NSIndexPath indexPathForRow:row inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
    [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}

Second button is responsible for synchronization. After synchronization finishes we have to reload data in table.

MDConflictsController.m
- (void)syncClicked:(id) sender {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
		//Synchronize with Mobeelizer cloud
        MobeelizerOperationError *error = [Mobeelizer sync];
        if(error != nil) {
            NSLog(@"Sync failure: %@ - %@", error.code, error.message);
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            [self reloadData];
        });
    });
}
-(void)reloadData{
    currentItems = [[NSMutableArray alloc] initWithArray:[self getItemsList]];
    [self.tableView reloadData];
}

In order to emphasize conflicted records, we will mark them with red backgound color.

MDConflictsController.m
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    MMDConflictsEntity* entity = currentItems[[indexPath row]];
    if (entity.conflicted) {
        cell.backgroundColor = [UIColor redColor];
    } else {
        cell.backgroundColor = [UIColor whiteColor];
    }
}

Finally, we have to configure our table view controller.

MDConflictsController.m
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return currentItems.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MMDConflictsEntity* item = currentItems[[indexPath row]];
    UITableViewCell* cell = [self createCellForItem:item atRow:indexPath.row];

    if ([item respondsToSelector:@selector(owner)]) {
        UIImageView* userLabel = (UIImageView*)[cell.contentView viewWithTag:1001];
        CGFloat userLabelY = (cell.contentView.frame.size.height - 30) /2; // 7
        CGFloat userLabelX = (cell.contentView.frame.size.width - 37); // 283
        if (userLabel == nil) {
            CGRect userLabelRect = CGRectMake(userLabelX, userLabelY, 30, 30);
            userLabel = [[UIImageView alloc] initWithFrame:userLabelRect];
            [userLabel setTag:1001];
            [cell.contentView addSubview:userLabel];
        }
        if ([item.owner isEqual:USER_A]) {
            userLabel.image = userAImage;
        } else {
            userLabel.image = userBImage;
        }
    }
    return cell;
}

- (UITableViewCell*)createCellForItem:(MMDConflictsEntity*)item atRow:(NSInteger)row {
    static NSString *CellIdentifier = @"ConflictsSyncCell";
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    UILabel* titleLabel = (UILabel*)[cell viewWithTag:1];
    titleLabel.text = item.title;

    UIView* scoreView = (UIView*)[cell viewWithTag:2];
    for (UIView *scoreSubview in scoreView.subviews) {
        [scoreSubview removeFromSuperview];
    }

    CGFloat offset = (5-item.score) * 5;
    for (NSInteger i=0; i<item.score; i++) {
        UIImageView *starView = [[UIImageView alloc] initWithImage:starImage];
        starView.frame = CGRectMake(offset + (imageSize*i), 0, imageSize, imageSize);
        [scoreView addSubview:starView];
    }
    return cell;
}

Create entity detail view

We have set table view controller to present records. However, in order to obtain conflicts we have to introduce updating data. We would like to select movie in list and edit it's score. That is why, add another table view controller to MDStoryboard. Connect cell from MDConflictsController to newly created view and from selection segue pick push option.  This will enable transition from selected record to detail view. Now select new table view and open Attributes Inspector. Change content setting from dynamic prototypes to static cells. Now select table view section and, also in Attributes Inspector, change number of rows to 5 and header to Rating. Then, add a new file to the project. Choose UITableViewController subclass template and name the class MDConflictsDetailController. We need selected entity and row number. Moreover, we need delegate to MDConflictsController in order to update modified entity. Connect newly created controller to its design in storyboard.

MDConflictsDetailController.h
@property (nonatomic) NSUInteger row;
@property (strong, nonatomic) MDConflictsController* delegate;
@property (strong, nonatomic) MMDConflictsEntity* entity;
@property (strong, nonatomic) UIImage* starImage;
@property (nonatomic) int imageSize;

We can mark movie from 1 to 5 stars, therefore we have create 5 rows. 

MDConflictsDetailController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell* cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
    CGFloat width = self.imageSize * (indexPath.row + 1);
    CGRect rect = CGRectMake((cell.bounds.size.width-width)/2, (cell.bounds.size.height-self.imageSize)/2, width, self.imageSize);

    UIView* starsView = [[UIView alloc] initWithFrame:rect];
    for (NSInteger i=0; i<indexPath.row+1; i++) {
        UIImageView *starView = [[UIImageView alloc] initWithImage:self.starImage];
        starView.frame = CGRectMake(self.imageSize*i, 0, self.imageSize, self.imageSize);
        [starsView addSubview:starView];
    }
    [cell addSubview:starsView];

    if (indexPath.row == self.entity.score-1) {
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
    } else {
        cell.accessoryType = UITableViewCellAccessoryNone;
    }
    return cell;
}

In MDConflictsDetailController we mark movie. We can choose how many stars to give. After choosing proper number of stars, we change entity score and update record in Mobeelizer database. Moreover, we will move back to table view with all records.

MDConflictsDetailController.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell* oldCell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:self.entity.score-1 inSection:0]];
    oldCell.accessoryType = UITableViewCellAccessoryNone;

    self.entity.score = indexPath.row + 1;
	
	//update entity in Mobeelizer database
    [[Mobeelizer database] save:self.entity];
    [self.delegate updateRow:self.row withItem:self.entity];

    UITableViewCell* newCell = [self.tableView cellForRowAtIndexPath:indexPath];
    newCell.accessoryType = UITableViewCellAccessoryCheckmark;

    [[self navigationController] popViewControllerAnimated:YES];
}

MDConflictsDetailController.h MDConflictsDetailController.m

Last, but not the least we have to implement transition between MDConflictsController to MDConflictsDetailController. Since we are using storyboard in our example, we have to implement prepareForSegue:sender:

MDConflictsController.m
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    MDConflictsDetailController *vc = [segue destinationViewController];
    NSUInteger row = [self.tableView indexPathForSelectedRow].row;
    vc.delegate = self;
    vc.row =  row;
    vc.entity = self.currentItems[row];

    [super prepareForSegue:segue sender:sender];
}

MDConflictsController.h MDConflictsController.m

There are few files, which are necessary Movies.plistuserA.pnguserB.pngstar_40.png.

Conclusion

To conclude, this example presents conflicts in Mobeelizer system. There are three conflict resolving strategies: manual, overwrite, discard. In our example we have choosen manual option. We could observe that after update different values to cloud no data was lost in synchronization. Data was only marked as conflicted and we should resolve conflict manually.


Attachments:

MMDConflictsEntity.h (application/octet-stream)
MMDConflictsEntity.m (application/octet-stream)
MDConflictsDetailController.h (application/octet-stream)
MDConflictsDetailController.m (application/octet-stream)
MDConflictsController.h (application/octet-stream)
MDConflictsController.m (application/octet-stream)
Conflicts.png (image/png)
ConflictResolving.png (image/png)
ConflictsEntity.png (image/png)
TitleFieldReq.png (image/png)
ScoreField.png (image/png)
AddUser.png (image/png)
ConflictsTemplate.png (image/png)
star_40.png (image/png)