SDK 简介

MaxLeap SDK 有三个压缩包:maxleap-sdk-ios.zip, maxleap-im-ios.zip, maxleap-sdk-iosext.zip

另外,还发布了一些开源 UI 组件。

1 SDK 类库

1.1 maxleap-sdk-ios.zip

下载 SDK

包含以下代码库:

MaxLeap.framework 核心库,下面的代码库都依赖它,详细功能使用指南

MaxLeapPay.framework 支付模块,支持支付宝支付、微信支付、银联支付。集成使用指南

MaxSocial.framework 社交模块,支持发帖、评论/点赞、关注、朋友圈、广场等功能,集成使用指南

MLQQUtils.framework QQ 登录模块,集成使用指南

MLWeChatUtils.framework 微信登录模块,集成使用指南

MLWeiboUtils.framework 微博登录模块,集成使用指南

MaxSocialShare.embeddedframework 社交分享组件,此组件可以单独使用,只依赖于第三方平台 SDK。集成使用指南源码地址

MaxIssues.embeddedframework 用户反馈组件,源码地址,使用指南请查阅源码仓库中的 README

MaxFAQ.embeddedframework FAQ 界面组件, 源码地址, 使用指南请查阅源码仓库中的 README

MaxLeap.framework

集成指南:请查阅SDK 集成小节

MaxLeap.framework 内置功能以及使用指南:

1.2 maxleap-im-ios.zip

下载 SDK

包含以下代码库:

SocketIOClientSwift.framework / SocketIO.framework 动态库,即时通讯基础功能代码库(注意:此文件可能与你的Xcode不兼容,导致 crash。1.4.1 版本以后的 zip 包里面会多一个文件 README.md,请先阅读这个文件,再作决定是否要用这个文件。)

MaxIMLibDynamic.framework 动态库,MaxLeap 提供的即时通讯功能代码库

MaxIMLib.framework 静态库,MaxIMLibDynamic.framework 的静态版本

MaxIMLibDynamic.framework 与 MaxIMLib.framework 二选一,不可以同时集成

使用指南

1.3 maxleap-sdk-iosext.zip

下载 SDK

这个压缩包只包含一个代码库 MaxLeapExt.framework,这个代码库是 MaxLeap.framework 的精简版本,支持 iOS 扩展程序。

这个代码库集成了以下功能:

2 开源 UI 组件

2.1 MaxLoginUI

登录组件,内置 注册界面,登录界面,其他登录界面(包含手机号登录,第三方平台账号登录)。

组件地址:https://github.com/MaxLeap/Module-MaxLogin-iOS

使用指南请查阅源码仓库中的 README

建议使用之前先了解云数据库(MLObject 的一些基本操作)账户管理系统

2.2 MaxIMUI

聊天 UI 组件,内置 联系人和群组列表界面,最近聊天列表,聊天界面。

组件地址:https://github.com/MaxLeap/Module-MaxIM-iOS

使用指南请查阅源码仓库中的 README

建议使用之前先了解 MaxIMLib.framework

2.3 MaxSocialUI

应用内社交组件,包含 说说发布界面,说说列表界面,广场界面,朋友圈界面等。

组件地址:https://github.com/MaxLeap/Module-MaxSocial-iOS

使用指南请查阅源码仓库中的 README

建议使用之前先了解 应用内社交基础模块(MaxSocial.framework)

2.4 MaxPayUI

移动支付界面组件。

组件地址:https://github.com/MaxLeap/Module-MaxPay-iOS

使用指南请查阅源码仓库中的 README

建议使用之前先了解 移动支付基础模块(MaxLeapPay.framework)

2.5 MaxShare

社交分享组件,此组件可以单独使用,只依赖于第三方平台 SDK。支持 新浪微博,微信好友,微信朋友圈,QQ好友,QQ空间 分享,但是都需要集成对应平台的 SDK,更详细内容请查阅社交分享使用指南

组件地址:https://github.com/MaxLeap/Module-MaxShare-iOS

使用指南请查阅源码仓库中的 README

2.6 MaxIssues

用户反馈组件,通过这个组件,用户可以创建一个会话,用来反馈问题。客服可以与之对话,近似实时聊天。

组件地址:https://github.com/MaxLeap/Module-MaxIssues-iOS

使用指南请查阅源码仓库中的 README

2.7 MaxFAQ

常见问题界面组件,包含 问题分类,问题列表,问题答案界面。可以在后台编辑问题,然后在客户端显示出来。

组件地址:https://github.com/MaxLeap/Module-MaxFAQ-iOS

使用指南请查阅源码仓库中的 README

SDK 集成

新项目集成

  1. 下载模板项目并解压

    请确保你使用的是最新的 Xcode (v7.0+), 并且目标平台版本为 iOS 7.0 或者更高。

    下载模板项目

  2. 配置项目

    在运行之前,还要进行一些配置:

    打开模板项目的 AppDelegate.m 文件,取消 application:didFinishLaunchingWithOptions: 中像下面这行的注释:

    [MaxLeap setApplicationId:@"your_application_id" clientKey:@"your_client_key" site:MLSiteCN];
    

    请把 your_application_idyour_client_key 替换成你自己应用的。最后一个参数 site 目前有两个值:MLSiteUS 对应 https://maxleap.com, MLSiteCN 对应 https://maxleap.cn。

  3. 现在可以运行了。

  4. 接下来测试配置是否正确

已有项目集成

使用 CocoaPods

CocoaPods 是 Objective-C 的依赖管理工具,现在已经支持 swift,它可以使第三方类库集成工作自动化,大大简化了这些工作。可以查看 CocoaPods 入门指南来进一步了解它。

在 Podfile 中合适的位置添加:

# MaxLeap 核心 SDK
pod "MaxLeap/Core"

# 微信登录
pod "MaxLeap/WeChatUtils"

# 微博登录
pod "MaxLeap/WeiboUtils"

# QQ 登录
pod "MaxLeap/QQUtils"

# 支付(支持微信,支付宝和银联三个支付渠道)
# 2.2.0 之前版本,银联支付和支付宝支付SDK都需要另外安装
# 2.2.0 之后版本,银联支付 SDK 需要另外安装
pod "MaxLeap/Pay"

# 应用内社交
pod "MaxLeap/Social"

然后再项目根目录执行 pod install 命令,就能将 MaxLeap SDK 集成到你的项目中。

手动安装

  1. 下载并解压缩 SDK

    请确认你使用的是Xcode最新版本(7.0+),目标平台为 iOS 7.0 或者更高版本。 下载 SDK

  2. 添加 SDK 到你的应用

    将解压后文件夹中的 MaxLeap.framework 拖至Xcode项目目标文件夹下。确保已勾选“Copy items to destination’s group folder”的复选框。

    drag_sdk_to_project

  3. 添加依赖

    确保“Enable Modules (C and Objective-C)” 和 “Link Frameworks Automatically”的生成设置为Yes。

    enable_modules

    点击 Targets → YourAppName → “Build Phases” 栏。
    展开 “Link Binary With Libraries”,如下图:

    add_dependencies

    点击“Link Binary With Libraries”左下角+号按钮,添加下列框架:

    MobileCoreServices.framework
    CoreTelephony.framework
    SystemConfiguration.framework
    libsqlite3.tbd
    libz.tbd

    如果报 CL 开头的类找不到,需要加入 CoreLocation.framework

连接云端应用

打开 AppDelegate.m 文件,并将如下import添加到文件顶部:

#import <MaxLeap/MaxLeap.h>

然后将以下代码复制到 application:didFinishLaunchingWithOptions: 方法中:

[MaxLeap setApplicationId:@"your_application_id" clientKey:@"your_client_id" site:MLSiteCN];

请把 your_application_idyour_client_id 替换成你自己的 MaxLeap 应用的。最后一个参数 site 目前有两个值:MLSiteUS 对应 https://maxleap.com, MLSiteCN 对应 https://maxleap.cn。

编译并运行!

测试是否可以连接到 MaxLeap 服务器

为了检测是否可以连接 MaxLeap 云服务和目标应用,我们可以在 appDelegate.mapplication:didFinishLaunchingWithOptions: 方法中加入以下代码:

#import <MaxLeap/MaxLeap.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [MaxLeap setApplicationId:@"your_application_id" clientKey:@"your_client_key" site:MLSiteCN];

    MLObject *obj = [MLObject objectWithoutDataWithClassName:@"Test" objectId:@"561c83c0226"];
    [obj fetchIfNeededInBackgroundWithBlock:^(MLObject * _Nullable object, NSError * _Nullable error) {
        if (error.code == kMLErrorInvalidObjectId) {
            NSLog(@"已经能够正确连接上您的云端应用");
        } else if (error && error.code < kMLErrorInternalServer) {
            NSLog(@"未知错误: %@", error);
        } else if (error && error.code == kMLErrorInternalServer) {
            NSLog(@"服务器出错: %@", error);
        } else if (error && error.code == kMLErrorConnectionFailed) {
            NSLog(@"网络错误: %@", error);
        } else {
            NSLog(@"\n\n应用访问凭证可能不正确,请检查。错误信息:\n%@\n\n", error);
        }
    }];
}

运行你的应用。然后查看 Xcode console 中打印的日志。

数据存储

简介

什么是数据存储服务

Cloud Data 是 MaxLeap 提供的数据存储服务,它建立在对象MLObject的基础上,每个MLObject包含若干键值对。所有MLObject均存储在 MaxLeap 上,你可以通过 iOS/Android Core SDK 对其进行操作,也可在 Console 中管理所有的对象。此外 MaxLeap 还提供一些特殊的对象,如MLUser(用户),MLFile(文件),MLGeoPoint (地理位置)等。

准备

云数据存储集成在 MaxLeap.framework 中,如果尚未安装,请先查阅SDK 集成小节,安装 SDK 并使之在 Xcode 中运行。

你还可以查看我们的 API 资料,了解有关我们 SDK 的更多详细信息。

注意:我们支持 iOS 7.0 及以上版本。

Cloud Object

存储在 Cloud Data 的对象称为 MLObject,而每个 MLObject 被规划至不同的 class 中(类似“表”的概念)。MLObject 包含若干键值对,且值为兼容 JSON 格式的数据。考虑到数据安全,MaxLeap 禁止客户端修改数据仓库的结构。你需要预先在 MaxLeap 开发者平台上创建需要用到的表,然后仔细定义每个表中的字段和其值类型。

新建

假设我们要保存一条数据到 Comment 类(数据表),它包含以下属性:

属性名值类型
content“我很喜欢这条分享”字符串
pubUserId1314520数字
isReadfalse布尔

我们建议使用驼峰式命名法来命名类名和字段名(如:NameYourclassesLikeThis(类名首字母大写), nameYourKeysLikeThis(列名首字母小写)),让代码看起来整齐美观。

首先,需要在云端数据仓库中添加 Comment 类,才能够往里面插入数据。 有关添加类等操作的说明,请查阅:控制台用户手册 - 云数据

MLObject 接口与 NSMutableDictionary 类似,但多了 saveInBackground 方法。现在我们保存一条 Comment:

MLObject *myComment = [MLObject objectWithClassName:@"Comment"];
myComment[@"content"] = @"我很喜欢这条分享";
myComment[@"pubUserId"] = @1314520;
myComment[@"isRead"] = @NO;
[myComment saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
        // myComment save succeed
    } else {
        // there was an error
    }
}];

该代码运行后,你可能想知道是否真的执行了相关操作。为确保数据正确保存,你可以在 MaxLeap 开发中心查看应用中的数据浏览器。你应该会看到类似于以下的内容:

objectId: "xWMyZ4YEGZ", content: "我很喜欢这条分享", pubUserId: 1314520, isRead: false,
createdAt:"2011-06-10T18:33:42Z", updatedAt:"2011-06-10T18:33:42Z"

注意:

  • Comment表何时创建: 出于数据安全考虑,MaxLeap 禁止客户端建表,所以在保存这条数据之前,必须先在开发者中心创建 Comment 这个表。
  • 表中同一属性值类型一致: 新建 comment 对象时,对应属性的值的数据类型要和创建该属性时一致,否则保存数据将失败。
  • 客户端可以添加字段: 例如,如果 Comment 表中没有 isRead 这个字段,那么保存时会自动添加这个字段,字段类型是第一次保存的 isRead 值的类型
  • 内建的属性: 每个 MLObject 对象有以下几个字段是不需要开发者指定的。这些字段的创建和更新是由系统自动完成的,请不要在代码里使用这些字段来保存数据。
属性名
objectId对象的唯一标识符
createdAt对象的创建时间
updatedAt对象的最后修改时间
  • 大小限制: MLObject 的大小被限制在128K以内。
  • 键的名称可以由英文字母、数字和下划线组成,但必须以字母开头,值的类型可为字符, 数字, 布尔, 数组或是 MLObject,为支持 JSON 编码的类型即可.

检索

获取 MLObject

你可以通过某条数据的 objectId, 获取这条数据的完整内容:

MLQuery *query = [MLQuery queryWithClassName:@"Comment"];
[query getObjectInBackgroundWithId:@"objectId" block:^(MLObject *object, NSError *error) {
    // Do something with the returned MLObject in the myComment variable.
    NSLog(@"%@", myComment);
}];
// The InBackground methods are asynchronous, so any code after this will run
// immediately.  Any code that depends on the query result should be moved
// inside the completion block above.

获取 MLObject 属性值

要从检索到的 MLObject 实例中获取值,你可以使用 objectForKey: 方法或 [] 操作符:

int pubUserId = [[myComment objectForKey:@"pubUserId"] intValue];
NSString *content = myComment[@"content"];
BOOL pubUserId = [myComment[@"cheatMode"] boolValue];

有三个特殊的值以属性的方式提供:

NSString *objectId = myComment.objectId;
NSDate *updatedAt = myComment.updatedAt;
NSDate *createdAt = myComment.createdAt;

若需要刷新已有对象,可以调用 -fetchInBackgroundWithBlock: 方法:

[myObject fetchInBackgroundWithBlock:^(MLObject *object, NSError *error) {
    // object 就是使用服务器数据填充后的 myObject
}];

更新

更新 MLObject 需要两步:首先获取需要更新的 MLObject,然后修改并保存。

// 根据 objectId 获取 MLObject
MLObject *object = [MLObject objectWithoutDataWithClassName:@"Comment" objectId:@"objectId"];
[object fetchInBackgroundWithBlock:^(MLObject *myComment, NSError *error) {
    // Now let's update it with some new data. In this case only isRead will get sent to the cloud
    myComment[@"isRead"] = @YES;
    [myComment saveInBackgroundWithBlock:nil];
}];
// The InBackground methods are asynchronous, so any code after this will run
// immediately.  Any code that depends on the query result should be moved
// inside the completion block above.

客户端会自动找出被修改的数据,所以只有 “dirty” 字段会被发送到服务器。你不需要担心其中会包含你不想更新的数据。

删除对象

删除 myComment 整条数据,这条数据的 objectId 不能为空:

[myComment deleteInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
        //
    } else {
         // there was an error
    }
}];

删除 MLObject 实例的某一属性

除了完整删除一个对象实例外,你还可以只删除实例中的某些指定的值。请注意只有调用 -saveInBackgroundWithBlock: 之后,修改才会同步到云端。

// After this, the content field will be empty
[myComment removeObjectForKey:@"content"];
// Saves the field deletion to the MaxLeap
[myComment saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
        //
    } else {
        // there was an error
    }
}];

批量操作

为了减少请求次数带来的浪费,可以使用批量操作接口,在一个请求中对多条数据进行创建,更新,删除,获取操作,接口有下面这些:

// 批量创建、更新
+[MLObject saveAllInBackground:block:]

// 批量删除
+[MLObject deleteAllInBackground:block:]

// 批量获取
+[MLObject fetchAllInBackground:block:]
+[MLObject fetchAllIfNeededInBackground:block:]

计数器

计数器是应用常见的功能需求之一。当某一数值类型的字段会被频繁更新,且每次更新操作都是将原有的值增加某一数值,此时,我们可以借助计数器功能,更高效的完成数据操作。并且避免短时间内大量数据修改请求引发冲突和覆盖。

比如纪录某用户游戏分数的字段"score",我们便会频繁地修改,并且当有几个客户端同时请求数据修改时,如果我们每次都在客户端请求获取该数据,并且修改后保存至云端,便很容易造成冲突和覆盖。

递增计数器

此时,我们可以利用-incrementKey:(增量为1),高效并且更安全地更新计数器类型的字段。如,为了更新记录某帖子的阅读次数字段 readCount,我们可以使用如下方式:

[myPost incrementKey:@"readCount"];
[myPost saveInBackgroundWithBlock:nil];

指定增量

你还可以使用 -incrementKey:byAmount: 实现任何数量的递增。注意,增量无需为整数,你还可以指定增量为浮点类型的数值。

递减计数器

要实现递减计数器,只需要向 -incrementKey:byAmount: 接口传入一个负数即可:

[myPost incrementKey:@"readCount" byAmount:@(-1)];
[myPost saveInBackgroundWithBlock:nil];

数组

你可以通过以下方式,将数组类型的值保存至 MLObject 的某字段(如下例中的 tags 字段)下:

增加至数组尾部

你可以使用 addObject:forKey:addObjectsFromArray:forKey:tags属性的值的尾部,增加一个或多个值。

[myPost addUniqueObjectsFromArray:@[@"flying", @"kungfu"] forKey:@"tags"];
[myPost saveInBackgroundWithBlock:nil]

同时,你还可以通过-addUniqueObject:forKey:addUniqueObjectsFromArray:forKey:,仅增加与已有数组中所有 item 都不同的值。插入位置是不确定的。

使用新数组覆盖

可以通过 setObject:forKey: 方法使用一个新数组覆盖 tags 中原有数组:

[myPost setObject:@[] forKey:@"tags"]

删除某数组字段的值

-removeObject:forKey:-removeObjectsInArray:forKey: 会从数组字段中删除每个给定对象的所有实例。

请注意 removeObject:forKey:removeObjectForKey: 的区别。

注意:Remove 和 Add/AddUnique 必需分开调用保存函数,否则数据不能正常上传和保存。

可变数组(NSMutableArray)

假如你在 MLObject 中存了可变数组,然后直接更改了这个数组中的元素,没用调用上面提到的 MLObject 的数组操作方法,保存时,本地的数组会覆盖云端的数组:

MLObject *obj; // an object retrieved from maxleap server
NSMutableArray *array = [NSMutableArray arrayWithObjects:@"a", nil];
obj[@"array"] = array;
[array addObject:@"b"];
[obj saveInBackground:nil];

// 云端这条数据的 array 字段值为 ["a", "b"]

除此之外,对 MLObject 中的 NSMutableDictionaryMLGeoPoint 直接作出更改后,调用 save 方法时也会被更新到云端。

关系数据

对象可以与其他对象相联系。如前面所述,我们可以把一个 MLObject 的实例 a,当成另一个 MLObject 实例 b 的属性值保存起来。这可以解决数据之间一对一或者一对多的关系映射,就像数据库中的主外键关系一样。

注:MaxLeap Services 是通过 Pointer 类型来解决这种数据引用的,并不会将数据 a 在数据 b 的表中再额外存储一份,这也可以保证数据的一致性。

使用 Pointer 实现

例如:一条微博信息会有多条评论。创建一条微博,并添加一条评论,你可以这样写:

// Create the post
MLObject *myPost = [MLObject objectWithClassName:@"Post"];
myPost[@"title"] = @"I'm Hungry";
myPost[@"content"] = @"Where should we go for lunch?";
// Create the comment
MLObject *myComment = [MLObject objectWithClassName:@"Comment"];
myComment[@"content"] = @"Let's do Sushirrito.";
// Add a relation between the Post and Comment
myComment[@"parent"] = myPost;
// This will save both myPost and myComment
[myComment saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
        //
    } else {
        // there was an error
    }
}];

我们可以使用 query 来获取这条微博所有的评论:

MLObject *myPost = ...
MLQuery *query = [MLQuery queryWithClassName:@"Comment"];
[query whereKey:@"parent" equalTo:myPost];
[query findObjectsInBackgroundWithBlock:^(NSArray *allComments, NSError *error) {
    // do something with all the comments of myPost
}];

你也可以通过 objectId 来关联已有的对象:

// Add a relation between the Post with objectId "1zEcyElZ80" and the comment
myComment[@"parent"] = [MLObject objectWithoutDataWithclassName:@"Post" objectId:@"1zEcyElZ80"];

默认情况下,当你获取一个对象的时候,关联的 MLObject 不会被获取。这些对象除了 objectId 之外,其他属性值都是空的,要得到关联对象的全部属性数据,需要再次调用 fetch 系方法(下面的例子假设已经通过 MLQuery 得到了 Comment 的实例):

MLObject *post = fetchedComment[@"parent"];
[post fetchInBackgroundWithBlock:^(MLObject *post, NSError *error) {
    NSString *title = post[@"title"];
    // do something with your title variable
}];

使用 MLRelation 实现关联

你可以使用 MLRelation 来建模多对多关系。这有点像 List 链表,但是区别之处在于,在获取附加属性的时候,MLRelation 不需要同步获取关联的所有 MLRelation 实例。这使得 MLRelation 比链表的方式可以支持更多实例,读取方式也更加灵活。例如,一个 User 可以赞很多 Post。这种情况下,就可以用getRelation()方法保存一个用户喜欢的所有 Post 集合。为了新增一个喜欢的 Post,你可以这样做:

MLUser *user = [MLUser currentUser];
MLRelation *relation = [user relationForKey:@"likes"];
[relation addObject:post]; // the post must be a saved object (has objectId)
// save the relation
[user saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
        //
    } else {
        // there was an error
    }
}];

你可以从 MLRelation 删除一个帖子,代码如下:

[relation removeObject:post];

默认情况下,这种关系中的对象列表不会被下载。你可以将 [relation query] 返回的 MLQuery 传入 -[query findObjectsInBackgroundWithBlock:] 获取 Post 列表。代码应如下所示:

// 注意,如果 relation.targetClass 或者 obj.objectId 如果为空,查询结果也为空
MLRelation *relation = [obj relationForKey:@"relation"];
MLQuery *query = [relation query];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    if (error) {
        // There was an error
    } else {
        // objects has all the Posts the current user liked.
    }
}];

若你只想要 Post 的一个子集,可以对 -[MLRelation query] 返回的 MLQuery 添加额外限制条件:

MLQuery *query = [relation query];
[query whereKey:@"title" hasSuffix:@"We"];
// Add other query constraints.

若要了解有关 MLQuery 的更多详细信息,请查看本指南的查询部分MLRelation 的工作方式类似于 MLObjectNSArray,因此你能对对象数组进行的任何查询(不含 includeKey:)均可对 MLRelation 执行。

数据类型

目前,我们使用的值的数据类型有 NSStringNSNumberMLObject。MaxLeap 还支持 NSDateNSDataNSNull

你可以嵌套 NSDictionaryNSArray 对象,以在单一 MLObject 中存储具有复杂结构的数据。

一些示例:

NSNumber *number = @42;
NSString *string = [NSString stringWithFormat:@"the number is %@", number];
NSDate *date = [NSDate date];
NSData *data = [@"foo" dataUsingEncoding:NSUTF8StringEncoding];
NSArray *array = @[string, number];
NSDictionary *dictionary = @{@"number": number,
                             @"string": string};

NSNull *null = [NSNull null];

MLObject *bigObject = [MLObject objectWithclassName:@"BigObject"];
bigObject[@"myNumber"] = number;
bigObject[@"myString"] = string;
bigObject[@"myDate"] = date;
bigObject[@"myData"] = data;
bigObject[@"myArray"] = array;
bigObject[@"myDictionary"] = dictionary;
bigObject[@"myNull"] = null;
[bigObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (error) {
        // There was an error
    } else {
        // objects has all the Posts the current user liked.
    }
}];

我们不建议通过在 MLObject 中使用 NSData 字段来存储图像或文档等大型二进制数据。MLObject 的大小不应超过 128 KB。要存储更多数据,我们建议你使用 MLFile 或者 MLPrivateFile。更多详细信息请参考本指南的“文件”部分。

链式调用

从 2.2.0 版本开始,MLObject 部分 API 支持链式调用,这个特性在 swift 中比较有用。你可以像下面这样组织代码:

MLObject(className: "Test")
    .setObject("bar", forKey: "foo")
    .add("barz", forKey: "array")
    .incrementKey("count")
    .saveInBackground { (succeeded, error) in
        // ...
}

这些 API 有以下特征: OC 中返回值为 instancetype,swift 中返回值为 Self

- (instancetype)setObject:(id)object forKey:(NSString *)key;
open func setObject(_ object: Any, forKey key: String) -> Self

文件

MLFile 的创建和上传

MLFile 可以让你的应用程序将文件存储到服务器中,以应对文件太大或太多,不适宜放入普通 MLObject 的情况。比如常见的文件类型图像文件、影像文件、音乐文件和任何其他二进制数据(大小不超过 100 MB)都可以使用。

MLFile 上手很容易。首先,你要有 NSData 类型的数据,然后创建一个 MLFile 实例。下面的例子中,我们只是使用一个字符串:

NSData *data = [@"Working at MaxLeap is great!" dataUsingEncoding:NSUTF8StringEncoding];
MLFile *file = [MLFile fileWithName:@"resume.txt" data:data];

注意,在这个例子中,我们把文件命名为 resume.txt。这里要注意两点:

  • 你不需要担心文件名冲突。每次上传都会获得一个唯一标识符,所以上传多个文件名为 resume.txt 的文件不同出现任何问题。
  • 重要的是,你要提供一个带扩展名的文件名。这样 MaxLeap 就能够判断文件类型,并对文件进行相应的处理。所以,若你要储存 PNG 图片,务必使文件名以 .png 结尾。

然后,你可以把文件保存到云中。与 MLObject 相同,使用 -save 方法。

[file saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    // Handle success or failure here ...
}];

最后,保存完成后,你可以像其他数据一样把 MLFileMLObject 关联起来:

MLObject *jobApplication = [MLObject objectWithclassName:@"JobApplication"]
jobApplication[@"applicantName"] = @"Joe Smith";
jobApplication[@"applicantResumeFile"] = file;
[jobApplication saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    // Handle success or failure here ...
}];

你可以调用 -getDataInBackgroundWithBlock: 重新获取此数据。这里我们从另一 JobApplication 对象获取恢复文件:

MLFile *applicantResume = anotherApplication[@"applicantResumeFile"];
[applicationResume getDataInBackgroundWithBlock:^(NSData *data, NSError *err) {
    if (!error) {
        NSData *resumeData = data;
    }
}];

图像

通过将图片转换成 NSData 然后使用 MLFile 就可以轻松地储存图片。假设你有一个名为 imageUIImage,并想把它另存为 MLFile

UIImage *image = ...;
NSData *imageData = UIImagePNGRepresentation(image);
MLFile *imageFile = [MLFile fileWithName:@"image.png" data:imageData];

MLObject *userPhoto = [MLObject objectWithClassName:@"UserPhoto"];
userPhoto[@"imageName"] = @"My trip to Hawaii!";
userPhoto[@"imageFile"] = imageFile;
[userPhoto saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    // ...
}];

你的 MLFile 将作为保存操作的一部分被上传到 userPhoto 对象。还可以跟踪 MLFile上传和下载进度

你可以调用 -getDataInBackgroundWithBlock: 重新获取此图像。这里我们从另一个名为 anotherPhotoUserPhoto 获取图像文件:

MLFile *userImageFile = anotherPhoto[@"imageFile"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
    if (!error) {
        UIImage *image = [UIImage imageWithData:imageData];
    }
}];

进度

使用 saveInBackgroundWithBlock:progressBlock:getDataInBackgroundWithBlock:progressBlock:: 可以分别轻松了解 MLFile 的上传和下载进度。例如:

NSData *data = [@"MaxLeap is great!" dataUsingEncoding:NSUTF8StringEncoding];
MLFile *file = [MLFile fileWithName:@"resume.txt" data:data];
[file saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  // 成功或失败处理...
} progressBlock:^(int percentDone) {
  // 更新进度数据,percentDone 介于 0 和 100。
}];

你可以用 REST API 删除对象引用的文件。你需要提供主密钥才能删除文件。

如果你的文件未被应用中的任何对象引用,则不能通过 REST API 删除它们。你可以在应用的“设置”页面请求清理未使用的文件。请记住,该操作可能会破坏依赖于访问未被引用文件(通过其地址属性)的功能。当前与对象关联的文件将不会受到影响。

查询

我们已经知道如何使用 getObjectInBackgroundWithId:block:] 从 MaxLeap 中检索单个 MLObject。使用 MLQuery,还有其他多种检索数据的方法 —— 你可以一次检索多个对象,设置检索对象的条件以及缓存策略等。

基本查询

使用 MLQuery 查询 MLObject 分三步:

  1. 创建一个 MLQuery 对象,并指定对应的 leapClassName
  2. MLQuery 添加过滤条件;
  3. 执行 MLQuery:调用 findObjectsInBackgroundWithBlock: 来查询与过滤条件匹配的 MLObject 数据。

例如,查询指定人员所发的微博,可以使用 whereKey:equalTo: 方法限定键值:

MLQuery *query = [MLQuery queryWithclassName:@"Post"];
[query whereKey:@"publisher" equalTo:@"MaxLeap"];
[query findObjectsInBackgroundWithBlock:^(NSArray *posts, NSError *error) {
    if (!error) {
        // The find succeeded.
        NSLog(@"Successfully retrieved %d posts.", psots.count);
        // Do something with the found objects
        for (MLObject *object in posts) {
            NSLog(@"%@", object.objectId);
        }
    } else {
        // Log details of the failure
        NSLog(@"Error: %@ %@", error, [error userInfo]);
    }
}];

findObjectsInBackgroundWithBlock: 方法确保不阻塞当前线程并完成网络请求,然后在主线程执行 block

查询约束

为了充分利用 MLQuery,我们建议使用下列方法添加限制条件。但是,若你更喜欢用 NSPredicate,创建 MLQuery 时提供 NSPredicate 即可指定一系列的限制条件。

NSPredicate *predicate = [NSPredicate predicateWithFormat:
@"publisher = 'MaxLeap'"];
MLQuery *query = [MLQuery queryWithclassName:@"Post" predicate:predicate];

支持以下特性:

  • 指定一个字段和一个常数的简单比较操作,比如: =!=<><=>=BETWEEN
  • 正则表达式,如 LIKEMATCHESCONTAINSENDSWITH
  • 限定谓语,如 x IN {1, 2, 3}
  • 键存在谓语,如 x IN SELF
  • BEGINSWITH 表达式。
  • ANDORNOT 的复合谓语。
  • "key IN %@", subquery 的子查询。

不支持以下类型的谓语:

  • 聚合操作,如 ANY、SOME、ALL 或 NONE。
  • 将一个键与另一个键比较的谓语。
  • 带多个 OR 子句的复杂谓语。

设置过滤条件

有几种方法可以对 MLQuery 可以查到的对象设置限制条件。你可以用 whereKey:notEqualTo: 将具有特定键值对的对象过滤出来:

[query whereKey:@"publisher" notEqualTo:@"xiaoming"];

你可以给定多个限制条件,只有满足所有限制条件的对象才会出现在结果中。换句话说,这类似于 AND 类型的限制条件。

[query whereKey:@"publisher" notEqualTo:@"xiaoming"];
[query whereKey:@"createdAt" greaterThan:[NSDate dateWithTimeIntervalSinceNow:-3600]];

你可以通过设置 limit 来限制结果数量。默认结果数量限制为 100,但是 1 到 2000 之间的任意值都有效:

query.limit = 10; // limit to at most 10 results

skip 用来跳过返回结果中开头的一些条目,配合 limit 可以对结果分页:

query.skip = 10; // 跳过前 10 条结果

如果你想要确切的一个结果,更加方便的方法是使用 getFirstObjectInBackgroundWithBlock: 而不是 findObjectsInBackgroundWithBlock:

MLQuery *query = [MLQuery queryWithclassName:@"Post"];
[query whereKey:@"playerEmail" equalTo:@"xiaoming@example.com"];
[query getFirstObjectInBackgroundWithBlock:^(MLObject *object, NSError *error) {
    if (!object) {
        NSLog(@"The getFirstObject request failed.");
    } else {
        // The find succeeded.
        NSLog(@"Successfully retrieved the object.");
    }
}];
对结果排序

对于可排序的数据,如数字和字符串,你可以控制结果返回的顺序:

// Sorts the results in ascending order by the createdAt field
[query orderByAscending:@"createdAt"];
// Sorts the results in descending order by the createdAt field
[query orderByDescending:@"createdAt"];

一个查询可以使用多个排序键,如下:

// Sorts the results in ascending order by the score field if the previous sort keys are equal.
[query addAscendingOrder:@"score"];
// Sorts the results in descending order by the score field if the previous sort keys are equal.
[query addDescendingOrder:@"username"];
设置数值大小约束

对于可排序的数据,你还可以在查询中使用对比:

// Restricts to wins < 50
[query whereKey:@"wins" lessThan:@50];
// Restricts to wins <= 50
[query whereKey:@"wins" lessThanOrEqualTo:@50];
// Restricts to wins > 50
[query whereKey:@"wins" greaterThan:@50];
// Restricts to wins >= 50
[query whereKey:@"wins" greaterThanOrEqualTo:@50];
设置返回数据包含的属性

你可以限制返回的字段,通过调用 selectKeys: 并传入一个字段数组来实现。若要检索只包含 scoreplayerName 字段(以及特殊内建字段,如 objectIdcreatedAtupdatedAt)的对象:

MLQuery *query = [MLQuery queryWithclassName:@"Post"];
[query selectKeys:@[@"contents", @"publisher"]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // objects in results will only contain the contents and publisher fields
}];

稍后,可以通过对返回的对象调用 fetchIfNeededInBackgroundWithBlock: 提取其余的字段:

MLObject *object = (MLObject*)results[0];
[object fetchIfNeededInBackgroundWithBlock::^(MLObject *object, NSError *error) {
    // all fields of the object will now be available here.
}];
设置更多约束

若你想要检索与几个不同值匹配的对象,你可以使用 whereKey:containedIn:,并提供一组可接受的值。这通常在用单一查询替代多个查询时比较有用。例如,如果你检索某几个用户发的微博:

// Finds posts from any of Jonathan, Dario, or Shawn
NSArray *names = @[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"];
[query whereKey:@"publisher" containedIn:names];

若你想要检索与几个值都不匹配的对象,你可以使用 whereKey:notContainedIn:,并提供一组可接受的值。例如,如果你想检索不在某个列表里的用户发的微博:

// Finds posts from anyone who is neither Jonathan, Dario, nor Shawn
NSArray *names = @[@"Jonathan Walsh", @"Dario Wunsch", @"Shawn Simon"];
[query whereKey:@"playerName" notContainedIn:names];

若你想要检索有某一特定键集的对象,可以使用 whereKeyExists:。相反,若你想要检索没有某一特定键集的对象,可以使用 whereKeyDoesNotExist:

你可以使用 whereKey:matchesKey:inQuery: 方法获取符合以下要求的对象:对象中的一个键值与另一查询所得结果的对象集中的某一键值匹配。例如,获取用户所有粉丝发的微博:

MLQuery *commentQuery = [MLQuery queryWithClassName:@"Comment"];
[commentQuery whereKey:@"parent" equalTo:post];
MLQuery *postsQuery = [MLQuery queryWithClassName:@"Post"];
[postsQuery whereKey:@"author" matchesKey:@"author" inQuery:postsQuery];
[postsQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // ...
}];

类似地,你可以使用 whereKey:doesNotMatchKey:inQuery: 获取不符合以下要求的对象,对象中的一个键值与另一查询所得结果的对象集中的某一键值匹配。

不同属性值类型的查询

值类型为数组的查询

对于数组类型的键,你可以查找键的数组值包含 2 的对象,如下所示:

// Find objects where the array in arrayKey contains 2.
[query whereKey:@"arrayKey" equalTo:@2];

你还可以查找键数组值包含值 2、3 或 4 的对象,如下所示:

// Find objects where the array in arrayKey contains each of the
// elements 2, 3, and 4.
[query whereKey:@"arrayKey" containsAllObjectsInArray:@[@2, @3, @4]];

值类型为字符串的查询

使用 whereKey:hasPrefix: 将结果限制为以某一特定字符串开头的字符串值。与 MySQL LIKE 运算符类似,它包含索引,所以对大型数据集很有效:

// Finds barbecue sauces that start with "Big Daddy's".
MLQuery *query = [MLQuery queryWithclassName:@"Post"];
[query whereKey:@"title" hasPrefix:@"Big Daddy's"];

值类型为 MLObject 查询

MLObject 类型字段匹配 MLObject

有几种方法可以用于关系型数据查询。如果你想检索有字段与某一特定 MLObject 匹配的对象,可以像检索其他类型的数据一样使用 whereKey:equalTo:。例如,如果每个 Commentparent 字段中有一个 Post 对象,你可以提取某一特定 Post 的评论:

// Assume MLObject *myPost was previously created.
MLQuery *query = [MLQuery queryWithClassName:@"Comment"];
[query whereKey:@"post" equalTo:myPost];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // comments now contains the comments for myPost
}];

你还可以用 objectId 进行关系型查询:

MLObject *object = [MLObject objectWithoutDataWithClassName:@"Post" objectId:@"1zEcyElZ80"];
[query whereKey:@"parent" equalTo:object];
MLObject 类型字段匹配 Query

如果想要检索的对象中,有字段包含与其他查询匹配的 MLObject,你可以使用 whereKey:matchesQuery:注意,默认限值 100 和最大限值 1000 也适用于内部查询,因此在大型数据集中进行查询时,你可能需要谨慎构建查询条件才能按需要进行查询。为了查找包含图像的帖子的评论,你可以这样:

MLQuery *innerQuery = [MLQuery queryWithClassName:@"Post"];
[innerQuery whereKeyExists:@"image"];
MLQuery *query = [MLQuery queryWithClassName:@"Comment"];
[query whereKey:@"post" matchesQuery:innerQuery];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // comments now contains the comments for posts with images
}];

如果想要检索的对象中,有字段包含与其他查询不匹配的 MLObject,你可以使用 whereKey:doesNotMatchQuery:。为了查找不包含图像的帖子的评论,你可以这样:

MLQuery *innerQuery = [MLQuery queryWithClassName:@"Post"];
[innerQuery whereKeyExists:@"image"];
MLQuery *query = [MLQuery queryWithClassName:@"Comment"];
[query whereKey:@"post" doesNotMatchQuery:innerQuery];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // comments now contains the comments for posts without images
}];
返回指定 MLObject 类型的字段

在一些情况下,你可能想要在一个查询中返回多种类型的相关对象。你可以用 includeKey: 方法达到这个目的。例如,假设你要检索最新的十条评论,并且想要同时检索这些评论的相关帖子:

MLQuery *query = [MLQuery queryWithClassName:@"Comment"];
// Retrieve the most recent ones
[query orderByDescending:@"createdAt"];
// Only retrieve the MLt ten
query.limit = 10;
// Include the post data with each comment
[query includeKey:@"post"];
[query findObjectsInBackgroundWithBlock:^(NSArray *comments, NSError *error) {
    // Comments now contains the MLt ten comments, and the "post" field
    // has been populated. For example:
    for (MLObject *comment in comments) {
        // This does not require a network access.
        MLObject *post = comment[@"post"];
        NSLog(@"retrieved related post: %@", post);
    }
}];

你也可以使用点标记进行多层级检索。如果你想要包含帖子的评论以及帖子的作者,你可以操作如下:

[query includeKey:@"post.author"];

你可以通过多次调用 includeKey:,进行包含多个字段的查询。此功能也适用于 getFirstObjectInBackgroundWithBlock:getObjectInBackgroundWithId:block:MLQuery 辅助方法。

个数查询

计数查询可以对拥有 1000 条以上数据的类返回大概结果。如果你只需要计算符合查询的对象数量,不需要检索匹配的对象,可以使用 countObjects,而不是 findObjects。例如,要计算某一特定玩家玩过多少种游戏:

MLQuery *query = [MLQuery queryWithclassName:@"Post"];
[query whereKey:@"publisher" equalTo:@"Sean"];
[query countObjectsInBackgroundWithBlock:^(int count, NSError *error) {
    if (!error) {
        // The count request succeeded. Log the count
        NSLog(@"Sean has played %d games", count);
    } else {
        // The request failed
    }
}];

对于含超过 500,000 个对象的类,计数操作受超时设定的限制。这种情况下,可能经常遇到超时错误,或只能返回近似正确的结果。因此,在应用程序的设计中,最好能做到避免此类计数操作。

复合查询

如果想要查找与几个查询中的其中一个匹配的对象,你可以使用 orQueryWithSubqueries: 方法。例如,如果你想要查找赢得多场胜利或几场胜利的玩家,你可以:

MLQuery *fewReader = [MLQuery queryWithClassName:@"Post"];
[fewReader whereKey:@"readCount" lessThan:@10];
MLQuery *lotsOfReader = [MLQuery queryWithClassName:@"Post"];
[lotsOfReader whereKey:@"readCount" greaterThan:@100];
MLQuery *query = [MLQuery orQueryWithSubqueries:@[fewReader, lotsOfReader]];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    // results contains players with lots of wins or only a few wins.
}];

你可以给新创建的 MLQuery 添加额外限制条件,这相当于 “and” 运算符。

但是,请注意:在混合查询结果中查询时,我们不支持非过滤型限制条件(如 limitskiporderBy...:includeKey:)。

缓存查询结果

MLQuery 支持在磁盘上缓存查询结果,这是一个很实用的功能。它可以让你能够随时查到数据,即使是用户设备处于断网状态、应用刚刚启动或者是网络请求尚未完成时。当缓存占用过多空间时,MaxLeap 会自动清理。

默认设置下,MLQuery 不使用缓存,你可以通过设置 query.cachePolicy 来启用缓存,还可以使用 query.maxCacheAge 设置缓存有效期。例如,尝试从网络查询数据,当网络不可用时返回缓存的数据:

MLQuery *query = [MLQuery queryWithClassName:@"Post"];
query.cachePolicy = kMLCachePolicyNetworkElseCache;
query.maxCacheAge = 24*60*60; // 设置缓存有效期

[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    if (error.code = kMLErrorCacheMiss) {
        // 无法访问网络,也未命中缓存
    } else if (error) {
        // 无法访问网络,本次查询结果不会缓存
    } else {
        // 成功查询到数据,如果是通过网络查询到的,会缓存本次查询结果
    }
}];

缓存策略

MaxLeap 提供多种不同的缓存策略:

策略枚举说明
kMLCachePolicyIgnoreCache默认缓存策略)查询不从缓存加载结果,也不会将结果保存到缓存
kMLCachePolicyCacheOnly查询会忽略网络,仅从缓存加载结果。如果没有缓存的结果,则会返回一个错误码为 kMLErrorCacheMissNSError
kMLCachePolicyNetworkOnly查询不从缓存加载结果,但会将结果保存到缓存。
kMLCachePolicyCacheElseNetwork查询首先尝试从缓存加载结果,但如果加载失败则从网络加载结果。如果缓存和网络加载都不成功,则会产生 NSError
kMLCachePolicyNetworkElseCache查询首先尝试从网络加载结果,但如果加载失败则从缓存加载结果。如果网络和缓存加载都不成功,则会产生一个错误码为 kMLErrorCacheMissNSError
kMLCachePolicyCacheThenNetwork查询首先从缓存加载结果,然后再从网络加载。在这个策略下,回调会被调用两次。 第一次返回从缓存获取的结果,随后返回从网络获取的结果。

缓存相关的操作

你可以使用 MLQuery 中提供的方法对缓存进行一些操作。

  • 检查是否存在缓存的结果

    BOOL hasCache = [query hasCachedResult];
    
  • 用以下代码删除某个查询的缓存结果:

    [query clearCachedResult];
    
  • 用以下代码删除所有缓存结果:

    [MLQuery clearAllCachedResults];
    
  • 设置缓存结果的最大保留时间:

    q2.maxCacheAge = 24 * 60 * 60; // 一天
    

查询缓存也适用于 MLQuery 的辅助方法,包括 getFirstObjectInBackgroundWithBlockgetObjectInBackgroundWithId

链式调用

从 2.2.0 版本开始,MLObjectMLQuery 部分 API 支持链式调用,这个特性在 swift 中比较有用。你可以像下面这样组织代码:

MLObject(className: "Test")
    .setObject("bar", forKey: "foo")
    .add("barz", forKey: "array")
    .incrementKey("count")
    .saveInBackground { (succeeded, error) in
        // ...
}

MLQuery(className: "ChainTest")
    .whereKeyExists("exist1")
    .whereKey("foo", equalTo: "bar")
    .includeKey("key")
    .selectKeys(["key1", "key2"])
    .limit(10)
    .skip(10*3)
    .order(byAscending: "a")
    .findObjectsInBackground(block: { (objects, err) in
        // ...
    })

这些 API 有以下特征: OC 中返回值为 instancetype,swift 中返回值为 Self

- (instancetype)setObject:(id)object forKey:(NSString *)key;

- (instancetype)whereKeyExists:(NSString *)key;
open func setObject(_ object: Any, forKey key: String) -> Self

open func whereKeyExists(_ key: String) -> Self

MLObject 子类

MaxLeap 的设计能让你尽快上手使用。你可以使用 MLObject 类访问所有数据,以及通过 objectForKey:[] 操作符访问任何字段。在成熟的代码库中,子类具有许多优势,包括简洁性、可扩展性和支持自动完成。子类化纯属可选操作,但它会将以下代码:

MLObject *game = [MLObject objectWithclassName:@"Game"];
game[@"displayName"] = @"Bird";
game[@"multiplayer"] = @YES;
game[@"price"] = @0.99;

转换为:

MyGame *game = [MyGame object];
game.displayName = @"Bird";
game.multiplayer = @YES;
game.price = @0.99;

创建 MLObject 子类

创建 MLObject 子类的步骤:

  1. 声明符合 MLSubclassing 协议的子类。
  2. 实现子类方法 + leapClassName。返回你传给 -initWithclassName: 方法的字符串, 也就是在服务器上创建的表名,这样以后可以使用 [Yourclass object] 来创建新对象了。
  3. MLObject+Subclass.h 导入你的 .m 文件。该操作导入了 MLSubclassing 协议中的所有方法的实现。其中 + leapClassName 的默认实现是返回类名(指 Objective C 中的类)。
  4. +[MaxLeap setApplicationId:clientKey:] 之前调用 +[Yourclass registerSubclass]。一个简单的方法是在类的 +load (Obj-C only) 或者 +initialize (both Obj-C and Swift) 方法中做这个事情。

下面的代码成功地声明、实现和注册了 MLObjectMyGame 子类:

// MyGame.h
@interface MyGame : MLObject <MLSubclassing>
+ (NSString *)leapClassName;
@end

// MyGame.m
// Import this header to let Armor know that MLObject privately provides most
// of the methods for MLSubclassing.
#import <MaxLeap/MLObject+Subclass.h>

@implementation MyGame
+ (void)load {
    [self registerSubclass];
}
+ (NSString *)leapClassName {
    return @"Game";
}
@end

属性的访问/修改

MLObject 子类添加自定义属性和方法有助于封装关于这个类的逻辑。借助 MLSubclassing,你可以将与同一个主题的所有相关逻辑放在一起,而不必分别针对事务逻辑和存储/传输逻辑使用单独的类。

MLObject 支持动态合成器(dynamic synthesizers),这一点与 NSManagedObject 类似。像平常一样声明一个属性,但是在你的 .m 文件中使用 @dynamic 而不用 @synthesize。下面的示例在 MyGame 类中创建了 displayName 属性:

// MyGame.h
@interface MyGame : MLObject <MLSubclassing>
+ (NSString *)leapClassName;
@property (retain) NSString *displayName;
@end

// MyGame.m
@dynamic displayName;

现在,你可以使用 game.displayName[game displayName] 访问 displayName 属性,并使用 game.displayName = @"Bird"[game setDisplayName:@"Bird"] 对其进行赋值。动态属性可以让 Xcode 提供自动完成功能和简单的纠错。

NSNumber 属性可使用 NSNumber 或其相应的基本类型来实现。请看下例:

@property BOOL multiplayer;
@property float price;

这种情况下,game[@"multiplayer"] 将返回一个 NSNumber,可以使用 boolValue 访问;game[@"price"] 将返回一个 NSNumber,可以使用 floatValue 访问。但是,multiplayer 属性实际上是 BOOLprice 属性实际上是 float。动态 getter 会自动提取 BOOLint 值,动态 setter 会自动将值装入 NSNumber 中。你可以使用任一格式。原始属性类型更易于使用,但是 NSNumber 属性类型明显支持 nil 值。

定义函数

如果你需要比简单属性访问更加复杂的逻辑,你也可以声明自己的方法:


@dynamic iconFile;

- (UIImageView *)iconView {
    MLImageView *view = [[MLImageView alloc] initWithImage:kPlaceholderImage];
    view.file = self.iconFile;
    [view loadInBackground];
    return view;
}

创建子类的实例

你应该使用类方法 object 创建新的对象。这样可以构建一个你定义的类型的实例,并正确处理子类化。要创建现有对象的引用,使用 objectWithoutDataWithObjectId:

子类的查询

你可以使用类方法 query 获取对特定子类对象的查询。下面的示例查询了用户可购买的装备:

MLQuery *query = [MyGame query];
[query whereKey:@"rupees" lessThanOrEqualTo:@0.99];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
    if (!error) {
        MyGame *firstArmor = objects[0];
        // ...
    }
}];

地理位置

MaxLeap 让你可以把真实的纬度和经度坐标与对象关联起来。通过在 MLObject 中添加 MLGeoPoint,可以在查询时实现将对象与参考点的距离临近性纳入考虑。这可以让你轻松某些事情,如找出距离与某个用户最近的其他用户或者距离某个用户最近的地标。

MLGeoPoint 字段说明

创建 MLGeoPoint

要将某个地点与对象联系起来,你首先要创建一个 MLGeoPoint。例如,要创建一个纬度为 40.0 度,经度为 -30.0 的点:

MLGeoPoint *point = [MLGeoPoint geoPointWithLatitude:40.0 longitude:-30.0];

然后,该点被作为常规字段储存在对象中。

placeObject[@"location"] = point;

地理位置查询

查询距离某地理位置最近的对象

有了一些具有空间坐标的对象后,找到哪些对象距离某个点最近将会产生很好的效应。这可以通过使用 whereKey:nearGeoPoint:MLQuery 添加另一限制条件完成。举例而言,找出距离某个用户最近的十个地点的方法如下:

// User's location
MLUser *userObject;
MLGeoPoint *userGeoPoint = userObject[@"location"];

// Create a query for places
MLQuery *query = [MLQuery queryWithClassName:@"PlaceObject"];

// Interested in locations near user.
[query whereKey:@"location" nearGeoPoint:userGeoPoint];

// Limit what could be a lot of points.
query.limit = 10;

// Final list of objects
[query findObjectsInBackgroundWithBlock:^(NSArray *placesObjects, NSError *error) {
    if (error) {
        // there was an error
    } else {
        // do something with placesObjects
    }
}];

此时,placesObjects 是按照与 userGeoPoint 的距离(由近及远)排列的一组对象。注意,若应用另一个 orderByAscending:/orderByDescending: 限制条件,该限制条件将优先于距离顺序。

查询某地理位置一定距离内的对象

若要用距离来限定获得哪些结果,请使用 whereKey:nearGeoPoint:withinMiles:whereKey:nearGeoPoint:withinKilometers:whereKey:nearGeoPoint:withinRadians:

查询一定地理位置范围内对象

你还可以查询包含在特定区域内的对象集合。若要查找位于某个矩形区域内的对象,请将 whereKey:withinGeoBoxFromSouthwest:toNortheast: 限制条件添加加至你的 MLQuery

MLGeoPoint *swOfSF = [MLGeoPoint geoPointWithLatitude:37.708813 longitude:-122.526398];
MLGeoPoint *neOfSF = [MLGeoPoint geoPointWithLatitude:37.822802 longitude:-122.373962];
MLQuery *query = [MLQuery queryWithclassName:@"PizzaPlaceObject"];
[query whereKey:@"location" withinGeoBoxFromSouthwest:swOfSF toNortheast:neOfSF];
[query findObjectsInBackgroundWithBlock:^(NSArray *pizzaPlacesInSF, NSError *error) {
    if (error) {
        // there was an error
    } else {
        // do something with pizzaPlacesInSF
    }
}];

请注意:

  1. 每个 MLObject 类仅可能有一个带 MLGeoPoint 对象的键。
  2. 点不应等于或大于最大范围值。纬度不能为 -90.0 或 90.0。经度不能为 -180.0 或 180.0。若纬度或经度设置超出边界,会引起错误。

数据安全

每个到达 MaxLeap 云服务的请求是由移动端 SDK,管理后台,云代码或其他客户端发出,每个请求都附带一个 security token。MaxLeap 后台可以根据请求的 security token 确定请求发送者的身份和授权,并在处理数据请求的时候,根据发送者的授权过滤掉没有权限的数据。

具体的介绍及操作方法,请参考 数据存储-使用指南

错误处理

MaxLeap iOS SDK 中有两种错误,一种是 exception,一种是 error。

Error

Error 通常是由网络错误或者服务器无法返回正确的结果引起的。每个错误都有个代码,代码含义请参考通用错误码

Exception

通常逻辑错误和参数错误会抛出一个异常,会使程序崩溃,遇到这种情况不要慌张,先仔细查看日志信息,弄清楚发生异常的原因。比较常见的异常如下:

  • You have to call +[MaxLeap setApplicationId:clientKey:site:] before using MaxLeap SDK.

    iOS SDK 的大多数功能都只能在初始化之后才能使用,否则就会抛出这个异常。

  • Operation <%@> is invalid on data type <%@>.

    MLObject 内建了很多用于更改数据的接口,有些接口只支持特定的数据类型,比如 -addObject:forKey: 只支持数据类型。假如对值为其他数据类型的字段执行这个操作,就会抛出这个异常。

  • You cannot increment a non-number.

    MLObject 内建了很多用于更改数据的接口,有些接口只支持特定的数据类型,比如 -incrementKey: 只支持数字类型。假如对值为其他数据类型的字段执行这个操作,就会抛出这个异常。

  • Operation <%@> is invalid after previous operation <%@>. Please save the object before perform operation <%@>.

    MLObject 有些操作不能在一个请求内完成,连续执行这些操作就会抛出这个异常。例如下面的代码:

    MLObject *a = [MLObject objectWithoutDataWithClassName:@"Song" objectId:@"573438df667a23000198b1f1"];
    MLObject *b = [MLObject objectWithoutDataWithClassName:@"Song" objectId:@"573438f7667a23000198b1f2"];
    MLObject *obj = [MLObject objectWithClassName:@"Test"];
    MLRelation *relation = [obj relationForKey:@"foo"];
    [relation addObject:a];
    [relation removeObject:b];
    // raise an `NSInternalInconsistencyException` exception, reason: 'Operation <Relation.add> is invalid after previous operation <Relation.remove>. Please save the object before perform operation <Relation.add>.'
    
  • You can't modify a relation after deleting it.

    删除一个 relation 之后,需要先保存数据,然后才可以更改这个 relation:

    MLObject *a = [MLObject objectWithoutDataWithClassName:@"Song" objectId:@"573438df667a23000198b1f1"];
    MLObject *obj = [MLObject objectWithClassName:@"Test"];
    [obj removeObjectForKey:@"relation"]; // relation 字段类型是 Relation
    MLRelation *relation = [obj relationForKey:@"relation"];
    [relation addObject:a];
    // 这里会抛出异常
    
    // 正确做法
    [obj removeObjectForKey:@"relation"]; // relation 字段类型是 Relation
    [obj saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
        if (succeeded) {
            [relation addObject:a];
            [obj saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
                // ...
            }];
        }
    }];
    
  • Found a circular dependency when saving object: %@.

    如果两个新建的 MLObject 之间存在循环引用,保存时会抛出这个异常。例如下面的代码:

    MLObject *a = [MLObject objectWithClassName:@"A"];
    MLObject *b = [MLObject objectWithClassName:@"B"];
    a[@"b"] = b;
    b[@"a"] = a;
    [a saveInBackgroundWithBlock:nil];
    

    可以先保存 a 或者 b, 然后再关联它们:

    MLObject *a = [MLObject objectWithClassName:@"A"];
    [a saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
        if (succeeded) {
            MLObject *b = [MLObject objectWithClassName:@"B"];
            a[@"b"] = b;
            b[@"a"] = a;
            [a saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
                // ...
            }];
        } else {
            // ...
        }
    }];
    
  • User cannot be saved unless they have been authenticated via logIn or signUp.

    未认证过的 MLUser 对象(user.isAuthenticated 为 NO)无法保存,这意味着以下代码会抛出异常:

    MLUser *user; // user.isAuthenticated 为 NO
    MLObject *a = [MLObject objectWithClassName:@"A"];
    a[@"user"] = user;
    [a saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
        NSLog(@"%@", a);
    }];
    
  • The class %@ must be registered with +registerSubclass before using MaxLeap.

    在 SDK 初始化之前需要调用 +registerSubclass 方法注册 MLObject 的子类,推荐在该子类的 +load 方法中调用。MLObject 子类创建方法请查阅MLObject 子类小节

    MLUser, MLInstallation 是在 SDK 初始化方法中注册,在 SDK 初始化之前,尝试创建它们的实例会抛出异常。

  • Can only call +registerSubclass on subclasses conforming to MLSubclassing.

    MLObject 的子类必须遵循 MLSubclassing 协议。

  • Subclasses of subclasses may not have separate +leapClassName definitions. %@ should inherit +leapClassName from %@.

    创建 MLObject 子类的子类时,不能定义跟父类不同的 leapClassName 值:

    @interface ClassA : MLObject <MLSubclassing>
    @end
    @implementation ClassA
    + (NSString *)leapClassName {return @"A";}
    @end
    
    @interface ClassB : ClassA
    @end
    @implementation ClassB
    + (NSString *)leapClassName {return @"B";}
    @end
    
    // ClassB 继承于 ClassA,但定义了与 ClassA 不同的 leapClassName
    // 执行下面这句的时候会抛出异常
    [ClassB registerSubclass];
    
  • Tried to register both %@ and %@ as the native MLObject subclass of %@. Cannot determine the right class to use because neither inherits from the other.

    同一个 leapClassName 下面只能注册有继承关系的 MLObject 子类:

    @interface ClassA : MLObject <MLSubclassing>
    @end
    @implementation ClassA
    + (NSString *)leapClassName {return @"A";}
    @end
    
    @interface ClassB : MLObject <MLSubclassing>
    @end
    @implementation ClassB
    + (NSString *)leapClassName {return @"A";}
    @end
    
    // ClassA 与 ClassB 定义了相同的 leapClassName,但是它们却不存在继承关系
    // 注册了其中一个类后,再注册另外一个时会抛出异常
    [ClassB registerSubclass];
    [ClassA registerSubclass];
    
  • Can only call -[MLObject init] on subclasses conforming to MLSubclassing.

    MLObject 的子类需要声明遵循 MLSubclassing 协议。

  • The init method of %@ set values on the object, which is not allowed.

    MLObject 检测到子类在初始化方法中修改了一些值,这些修改不应该在初始化方法中做。

  • Attempt to write readonly property%@of class%@.

    MLObject 子类的一个属性同时声明为 readonlydynamic 的时候,obj[@"foo"] = @"bar"; 语句将抛出异常。

  • Unsupported type encoding: %s.

    MLObject 子类声明为 dynamic 的属性类型不受支持时会抛出这个异常。受支持的类型为:ObjC object, char, int, short, long, long long, unsigned char, unsigned int, unsigned short, unsigned long, unsigned long long, float, double, bool, BOOL。

账号服务

准备

基础用户管理功能集成在 MaxLeap.framework 中,如果还没有集成,请先查阅SDK 集成小节,安装 SDK,并使之在 Xcode 中运行。

第三方登录需要集成 ML**Utils.framework 和第三方平台对应 SDK。

你还可以查看我们的 API 资料,了解有关我们 SDK 的更多详细信息。

注意:我们支持 iOS 7.0 及以上版本。

用户管理

我们提供了用于用户管理的类,叫做 MLUser,可自动处理用户帐户管理需要的很多功能。

你可以使用这个类在应用程序中添加用户帐户功能。

MLUserMLObject 的一个子类,拥有与之完全相同的特性,如键值对接口。MLObject 上的所有方法也存在于 MLUser 中。不同的是 MLUser 具有针对用户帐户的一些特殊的附加功能。 请先阅读 数据存储 MLObject 部分

SDK 自动创建匿名用户

因为一些特殊原因,SDK 中有一个逻辑:它会在没有用户登录的情况下自动创建一个匿名用户,有关匿名用户,请查看匿名用户介绍。

- 启动应用程序时,若 currentUser 为空,则会创建一个匿名用户 - 用户登出后,SDK 会自动创建一个匿名用户 - 这个过程是定时器驱动的,有一定的延迟,如果应用在某个时刻需要匿名登录,却发现当前用户为空,就需要手动创建匿名用户

注意:还需要特别注意的是,假如当前用户是一个匿名用户,这个时候直接调用注册接口,sdk 会把这个匿名用户更新成为一个普通用户,而不会创建一个新用户。

对于以上提到的用户当前用户匿名用户的含义以及其功能特性,在以下几个小节有详细解释。

字段说明

MLUser 除了继承自 MLObject 的属性之外,还有有几个特有的属性:

  • username:用户的用户名(必填)。
  • password:用户的密码(注册时必填)。
  • email:用户的电子邮箱地址(选填)。

切记,如果你通过这些属性设置 usernameemail,则无需使用 setObject:forKey: 方法进行设置。

注册用户

你的应用程序要做的第一件事就是让用户注册。以下代码示范了一个典型注册过程:

- (void)myMethod {
    MLUser *user = [MLUser user];
    user.username = @"my_name";
    user.password = @"my_password";
    user.email = @"email@example.com";
    // other fields can be set just like with MLObject
    user[@"mobilePhone"] = @"13500000000";
    [user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
        if (!error) {
            // Hooray! Let them use the app now.
        } else {
            NSString *errorString = [error userInfo][@"error"];
            // Show the errorString somewhere and let the user try again.
        }
    }];
}

这个调用将在你的 MaxLeap 应用中异步创建一个新的用户。创建前,它还会检查确保用户名和邮箱唯一。此外,MaxLeap 只保存密码的密文。我们从来不明文储存密码,也不会将密码明文传输回客户端。

注意: 我们使用的是 -[user signUpInBackgroundWithBlock:] 方法,而不是 -[user saveInBackgroundWithBlock:] 方法。应始终使用 -[user signUpInBackgroundWithBlock:] 方法创建新的 MLUser。调用 -[user saveInBackgroundWithBlock:] 可以完成用户的后续更新。

若注册不成功,你应该查看返回的错误对象。最可能的情况就是该用户名或邮箱已被其他用户使用。你应该将这种情况清楚地告诉用户,并要求他们尝试不同的用户名。

你可以使用电子邮箱地址作为用户名。只需让你的用户输入他们的电子邮箱,但是需要将它填写在用户名属性中 - MLUser 将可以正常运作。我们将在重置密码部分说明是如何处理这种情况的。

登录

当然,你让用户注册后,需要让他们以后登录到他们的帐户。为此,你可以使用类方法 +[MLUser logInWithUsernameInBackground:password:block:]

[MLUser logInWithUsernameInBackground:@"myname" password:@"mypass" block:^(MLUser *user, NSError *error) {
    if (user) {
        // Do stuff after successful login.
    } else {
        // The login failed. Check error to see why.
    }
}];

当前用户(可以用来判断用户登录状态)

当前用户是指当前已经登录的用户,使用方法 currentUser 可以获取到当前用户对象,这个对象会被 SDK 自动缓存起来。

可以使用缓存的 currentUser 对象实现自动登录,这样用户就不用每次打开应用都要登录了。

每当用户成功注册或者登录后,这个用户对象就会被缓存到磁盘中。这个缓存可以用来判断用户是否登录:

MLUser *currentUser = [MLUser currentUser];
if (currentUser) {
    // do stuff with the user
} else {
    // show the signup or login screen
}

你可以通过注销来清除他们的当前登录状态:

[MLUser logOut];
MLUser *currentUser = [MLUser currentUser]; // this will now be nil

注意:由于 SDK 会自动创建匿名用户,所以 currentUser 有值并不能代表用户已经登录,在检查用户登录状态时,推荐这种方式:

MLUser *currentUser = [MLUser currentUser];
if (currentUser) {
    if ([MLAnonymousUtils isLinkedWithUser:currentUser]) {
        // 已经匿名登录
    } else {
        // 常规登录
    }
} else {
    // 未登录
}

修改密码

可以通过更新 password 字段来更改密码:

[MLUser currentUser].password = @"the new password";
[[MLUser currentUser] saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
        // ...
    } else {
        // handle the error
    }
}];

为了安全起见,在更改密码前需要让用户输入旧密码并验证是否与当前账户匹配:

NSString *theOldPassword;
NSString *theNewPassword;

[[MLUser currentUser] checkIsPasswordMatchInBackground:theOldPassword block:^(BOOL isMatch, NSError *error) {
    if (isMatch) {
        [MLUser currentUser].password = theNewPassword;
        [[MLUser currentUser] saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
            if (succeeded) {
                // ...
            } else {
                // handle the error
            }
        }];
    } else {
        // handle the error
    }
}];

重置密码(使用邮箱)

刚把密码录入系统后就忘记密码的情况是存在的。这种情况下,我们的 SDK 提供一种方法让用户安全地重置密码。

若要开始密码重置流程,让用户填写电子邮箱地址,并调用:

[MLUser requestPasswordResetForEmailInBackground:@"email@example.com"];

该操作将尝试将给定的电子邮箱与用户电子邮箱或用户名字段进行匹配,并向用户发送密码重置邮件。这样,你可以选择让用户使用其电子邮箱作为用户名,或者你可以单独收集它并把它储存在电子邮箱字段。

密码重置流程如下:

  1. 用户输入电子邮箱地址,请求重置密码。
  2. MaxLeap 向其电子邮箱发送一封包含专用密码重置链接的邮件。
  3. 用户点击重置链接,进入专用 MaxLeap 页面,用户在该页面输入新密码。
  4. 用户输入新密码。现在,用户的密码已经被重置为他们指定的值。

注意:该流程中的消息传送操作将根据你在 MaxLeap 上创建该应用时指定的名称引用你的应用程序。

获取单个用户的信息

可以使用 -[MLUser fetchInBackgroundWithBlock:] 方法来获取单个用户的信息:

MLUser *user = [MLUser objectWithoutDataWithObjectId:@"56fc921f70c67600015941a2"];
// 如果 user 不是当前用户,只返回部分信息
[user fetchInBackgroundWithBlock:^(MLUser * _Nullable user, NSError * _Nullable error) {
    if (error) {
        // 出错了,检查 error 看看是什么原因
    } else {
        // ...
    }
}];

查询用户

出于安全考虑,不允许客户端查询用户表。下面的代码会得到一个没有权限的错误:

MLQuery *query = [MLUser query];
[query findObjectsInBackgroundWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    // 该请求始终会返回没有权限的错误
}];

邮箱验证

在 MaxLeap 应用设置中启用电子邮箱验证,可以让应用将部分使用体验提供给验证过电子邮箱地址的用户。电子邮箱验证会将 emailVerified 键添加到 MLUser 中。MLUseremail 被修改后,emailVerified 被设置为 false。随后,MaxLeap 会向用户发送一个邮件,其中包含一个链接,点击这个链接,可将 emailVerified 设置为 true

有三种 emailVerified 状态需要考虑:

  1. true - 用户通过点击 MaxLeap 发送给他们的链接确认电子邮箱地址。最初创建用户帐户时,MLUsers 没有 true 值。
  2. falseMLUser 对象最后一次刷新时,用户未确认其电子邮箱地址。若 emailVerifiedfalse,可以考虑调用 -[MLUser fetchInBackgroundWithBlock:],刷新用户信息。
  3. 缺失(undefined) - 电子邮箱验证关闭或没有填写 email

匿名用户

能够将数据和对象与具体用户关联非常有价值,但是有时你想在不强迫用户输入用户名和密码的情况下也能达到这种效果。

匿名用户是指能在无用户名和密码的情况下创建的但仍与任何其他 MLUser 具有相同功能的用户。登出后,匿名用户将被抛弃,其数据也不能再访问。

你可以使用 MLAnonymousUtils 创建匿名用户:

[MLAnonymousUtils logInWithBlock:^(MLUser *user, NSError *error) {
    if (error) {
        NSLog(@"Anonymous login failed.");
    } else {
        NSLog(@"Anonymous user logged in.");
    }
}];

你可以通过设置用户名和密码,然后调用 -[user signUpInBackgroundWithlock:] 的方式,或者通过登录或关联 微博微信 等服务的方式,将匿名用户转换为常规用户。转换的用户将保留其所有数据。想要判断当前用户是否为匿名用户,可以使用 +[MLAnonymousUtils isLinkedWithUser:] 方法:

if ([MLAnonymousUtils isLinkedWithUser:[MLUser currentUser]]) {
    // current user is anonymous
} else {
    // current user is regular
}

第三方登录

为简化用户的注册及登录流程,并且集成 MaxLeap 应用与 微博, 微信 等应用,MaxLeap 提供了第三方登录应用的服务。你可以同时使用第三方应用 SDK 与 MaxLeap SDK,并将 MLUser 与第三方应用的用户ID进行连接。

使用第三方账户认证登录流程大致如下:

  1. 点击第三方登录按钮,跳转到第三方账号登录认证界面(通过 sso 或者网页)
  2. 用户确认授权以后,SDK 拿到认证信息,然后向 MaxLeap 服务器发送登录请求
  3. 服务器使用认证信息查询与之绑定的 MLUser, 假如没有查询到,服务器会自动创建一个 MLUser,并将这个认证信息与之绑定,该 user 对象的 username 为随机生成,isNewtrue,不能使用用户名密码的方式登录;假如查询到一个 MLUser,直接返回这个对象。

除了第一次使用第三方账号认证登录时,MLUser 自动与之绑定之外,还可以手动为 MLUser 绑定第三方账号,该流程大致如下:

  1. 首先,用户需要手动登录一个账号, 也就是 MLUser, 不论是何种方式(账户名/密码,手机号/验证码,第三方登录)
  2. 用户在已登录的状态下点击绑定第三方账号按钮,跳转到第三方账号登陆认证界面(通过 sso 或者网页)
  3. 用户确认授权以后,SDK 拿到认证信息,然后联系 MaxLeap 服务器,尝试将该认证信息与 MLUser 对象绑定
  4. 假如该第三方账号之前已经与其它 MLUser 绑定,则本次绑定失败
  5. 绑定成功的用户,稍后就可以使用这个第三方账号登陆 MaxLeap 服务器了

第三方平台的集成方式有些许差别,请查阅以下几个小节来了解详细实现。

使用微博账号登陆

MaxLeap SDK 能够与微博 SDK 集成,使用微博账号登陆。

[MLWeiboUtils loginInBackgroundWithScope:@"all" block:^(MLUser * _Nullable user, NSError * _Nullable error) {
    if (user) {
        // 登陆成功
    } else {
        // 登陆失败
    }
}];

使用微博账号登录后,如果该微博用户并未与任何 MLUser 绑定,MaxLeap 将创建一个 MLUser,并与其绑定。

准备工作

若要通过 MaxLeap 使用微博,你需要:

  1. 前往微博开放平台创建微博应用
  2. 在 “微博应用 >> 应用信息 >> 高级信息” 中仔细填写授权回调页和取消授权回调页地址。授权回调页地址在集成微博 SDK 的时候需要用到,一般情况下填写默认值(https://api.weibo.com/oauth2/default.html)即可。
  3. 前往 MaxLeap 控制台,在 MaxLeap 应用设置 >> 用户验证 页面打开 “允许使用新浪微博登录” 开关。
  4. 下载 微博 iOS SDK
  5. 把 libWeiboSDK 文件夹添加到项目中,注意选择 Group Reference。
  6. 下载解压 MaxLeap iOS SDK
  7. 请确保已经按照快速入门指南正确集成了 MaxLeap.framework。
  8. MLWeiboUtils.framework 添加到项目中。
  9. 初始化 MLWeiboUtils,比如在 application:didFinishLaunchingWithOptions: 方法中:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        [MaxLeap setApplicationId:@"your_maxleap_appId" clientKey:@"your_maxleap_clientKey" site:MLSiteCN];
        [MLWeiboUtils initializeWeiboWithAppKey:@"your_weibo_app_key" redirectURI:@"微博应用授权回调页"];
        return YES;
    }
    
  10. 处理授权回调

    - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
        return [WeiboSDK handleOpenURL:url delegate:self];
    }
    
    - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation {
        return [WeiboSDK handleOpenURL:url delegate:self];
    }
    
    - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options {
        return [WeiboSDK handleOpenURL:url delegate:self];
    }
    
  11. 处理授权响应

    #pragma mark WeiboSDKDelegate
    
    - (void)didReceiveWeiboResponse:(WBBaseResponse *)response {
       if ([response isKindOfClass:[WBAuthorizeResponse class]]) {
           [MLWeiboUtils handleAuthorizeResponse:(WBAuthorizeResponse *)response];
        } else {
            // 处理其他请求的响应
        }
    }
    

若你遇到与微博相关的任何问题,请查阅 微博官方文档

MaxLeap 用户可通过以下两种主要方法使用微博:(1) 以微博用户身份登录,并创建 MLUser。(2) 将微博账号与已有的 MLUser 关联。

登录并注册新 MLUser

MLWeiboUtils 提供一种方法让你的 MLUser 可以通过 微博 登录或注册。这可以使用 logInWithBlock 方法实现:

[MLWeiboUtils loginInBackgroundWithScope:@"all" block:^(MLUser * _Nullable user, NSError * _Nullable error) {
    if (!user) {
        NSLog(@"微博登陆失败");
    } else if (user.isNew) {
        NSLog(@"用户使用微博账户成功注册并登陆");
    } else {
        NSLog(@"用户使用微博账户登陆");
    }
}];

该代码运行时,会出现以下情况:

  1. 若设备安装了新浪微博客户端,则会跳转到微博客户端请求授权,否则弹出微博授权网页。
  2. 用户确认授权,你的应用程序会收到回调。
  3. 你的应用程序收到授权响应,并交由 MLWeiboUtils 处理,[MLWeiboUtils handleAuthorizeResponse:(WBAuthorizeResponse *)response];
  4. 我们的 SDK 会收到微博数据并将其保存在 MLUser 中。如果是基于微博身份的新用户,那么该用户随后会被创建。
  5. 你的 block 被调用并带回这个用户对象(user)。

绑定 MLUser 与微博账号

若你想要将已有的 MLUser 与微博帐户关联起来,你可以按以下方式进行关联:

if (![MLWeiboUtils isLinkedWithUser:user]) {
    [MLWeiboUtils linkUserInBackground:user withScope:@"all" block:^(BOOL succeeded, NSError * _Nullable error) {
        if ([MLWeiboUtils isLinkedWithUser:user]) {
            NSLog(@"Woohoo, user linked with Weibo!");
        }
    }];
}

关联时发生的步骤与登录非常类似。区别是在成功登录中,将会使用来自微博的信息更新当前的 MLUser。今后通过微博进行的登录会返回已存在的 MLUser

解除绑定

若你想要取消用户与微博的关联,操作如下:

[MLWeiboUtils unlinkUserInBackground:user block:^(BOOL succeeded, NSError * _Nullable error) {
    if (!error && succeeded) {
        NSLog(@"The user is no longer associated with their Weibo account.");
    }
}];

在当前用户已经关联了微博账户的情况下,可以使用 [MLWeiboAccessToken currentAccessToken].accessToken 获取用户身份验证令牌。

使用微信账号登陆

集成微信 SDK 的过程与微博非常相似。

准备工作

若要与微信集成,你需要:

  1. 前往微信开放平台,创建微信移动应用。
  2. 前往 MaxLeap 控制台,在 MaxLeap 应用设置 >> 用户验证 页面打开 “允许使用微信登录” 开关。
  3. 下载微信 iOS SDK(iOS开发工具包64位)并解压
  4. 微信 SDK 文件夹应该有 libWeChatSDK.aWXApi.hWXApiObject.hWechatAuthSDK.h 四个文件,把这个文件夹添加到项目中,注意选择 Group Reference 选项
  5. 下载解压 MaxLeap iOS SDK
  6. 请确保已经按照快速入门指南正确集成了 MaxLeap.framework
  7. MLWeChatUtils.framework 添加到项目中。
  8. 初始化 MLWeChatUtils,比如在 application:didFinishLaunchingWithOptions: 方法中:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        [MaxLeap setApplicationId:@"your_maxleap_appId" clientKey:@"your_maxleap_clientKey" site:MLSiteCN];
        [MLWeChatUtils initializeWeChatWithAppId:@"your_weixin_appID" appSecret:@"your_weixin_AppSecret"];
        return YES;
    }
    
  9. 处理授权回调

    - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
        return [WXApi handleOpenURL:url delegate:self];
    }
    
    - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation {
        return [WXApi handleOpenURL:url delegate:self];
    }
    
    - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options {
        return [WXApi handleOpenURL:url delegate:self];
    }
    
  10. 处理授权响应

    #pragma mark WXApiDelegate
    
    - (void)onResp:(BaseResp *)resp {
       if ([resp isKindOfClass:[SendAuthResp class]]) {
           [MLWeChatUtils handleAuthorizeResponse:(SendAuthResp *)resp];
       } else {
           // 处理其他请求的响应
        }
    }
    

若你遇到与微信相关的任何问题,请查阅 微信官方文档

MaxLeap 用户可通过以下两种主要方法使用微信:(1) 以微信用户身份登录,并创建 MLUser。(2) 将微信账号与已有的 MLUser 关联。

登录并注册新 MLUser

MLWeChatUtils 提供了一个方法让你的 MLUser 可以通过微信登录或注册。这可以使用 logInWithBlock 方法实现:

[MLWeChatUtils loginInBackgroundWithScope:@"snsapi_userinfo" block:^(MLUser * _Nullable user, NSError * _Nullable error) {
    if (!user) {
        NSLog(@"微信登陆失败");
    } else if (user.isNew) {
        NSLog(@"用户使用微信账户成功注册并登陆");
    } else {
        NSLog(@"用户使用微信账户登陆");
    }
}];

该代码运行时,会出现以下情况:

  1. 跳转到微信客户端请求授权。
  2. 用户确认授权,你的应用程序会收到回调。
  3. 你的应用程序收到授权响应,并交由 MLWeChatUtils 处理,[MLWeChatUtils handleAuthorizeResponse:(WBAuthorizeResponse *)response];
  4. 我们的 SDK 会收到微博数据并将其保存在 MLUser 中。如果是基于微信身份的新用户,那么该用户随后会被创建。
  5. 你的 block 被调用并带回这个用户对象(user)。

绑定 MLUser 与微信账号

若你想要将已有的 MLUser 与微信帐户关联起来,你可以按以下方式进行关联:

if (![MLWeChatUtils isLinkedWithUser:user]) {
    [MLWeChatUtils linkUserInBackground:user withScope:@"snsapi_userinfo" block:^(BOOL succeeded, NSError * _Nullable error) {
        if ([MLWeChatUtils isLinkedWithUser:user]) {
            NSLog(@"Woohoo, user linked with Wechat!");
        }
    }];
}

关联时发生的步骤与登录非常类似。区别是在成功登录中,将会使用来自微信的信息更新当前的 MLUser。今后通过该微信账号进行的登录会返回已存在的 MLUser

解除绑定

若你想要取消用户与微信的关联,操作如下:

[MLWeChatUtils unlinkUserInBackground:user block:^(BOOL succeeded, NSError * _Nullable error) {
    if (!error && succeeded) {
        NSLog(@"The user is no longer associated with their Wechat account.");
    }
}];

在当前用户已经关联了微信账户的情况下,可以使用 [MLWeChatAccessToken currentAccessToken].accessToken 获取用户身份验证令牌。

使用 QQ 账号登陆

MaxLeap SDK 能够与 TencentOpenAPI SDK 集成,使用 QQ 账号登陆。

NSArray *permissions = @[@"get_user_info", @"get_simple_userinfo", @"add_t"];
[MLQQUtils loginInBackgroundWithPermissions:permissions block:^(MLUser * _Nullable user, NSError * _Nullable error) {
    if (user) {
        // 登陆成功
    } else {
        // 登陆失败
    }
}];

使用 QQ 账号登录后,如果该 QQ 用户并未与任何 MLUser 绑定,MaxLeap 将创建一个 MLUser,并与其绑定。

准备工作

若要通过 MaxLeap 使用 QQ ,你需要:

  1. 前往腾讯开放平台创建 QQ 应用
  2. 前往 MaxLeap 控制台,前往 MaxLeap 应用设置 >> 用户验证 页面,打开"允许QQ登录"选项。
  3. 下载并解压腾讯开发平台 SDK
  4. TencentOpenAPI.frameworkTencentOpenAPI_iOS_Bundle.bundle 添加到项目中。
  5. 下载解压 MaxLeap iOS SDK
  6. 请确保已经按照快速入门指南正确集成了 MaxLeap.framework
  7. MLQQUtils.framework 添加到项目中。
  8. 初始化 MLQQUtils,比如在 application:didFinishLaunchingWithOptions: 方法中:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        [MaxLeap setApplicationId:@"your_maxleap_appId" clientKey:@"your_maxleap_clientKey" site:MLSiteCN];
        [MLQQUtils initializeQQWithAppId:@"222222" qqDelegate:self]; // self 不能为空且需遵循 TencentSessionDelegate 协议
        return YES;
    }
    
  9. 实现 TencentSessionDelegate 协议方法

    #pragma mark TencentLoginDelegate
    
    // 以下三个方法保持空实现就可以,MLQQUtils 会置换这三个方法,但是会调用这里的实现
    
    - (void)tencentDidLogin {
    
    }
    
    - (void)tencentDidNotLogin:(BOOL)cancelled {
    
    }
    
    - (void)tencentDidNotNetWork {
    
    }
    

若你遇到与 TencentOpenAPI SDK 相关的任何问题,请查阅 腾讯官方文档

MaxLeap 用户可通过以下两种主要方法使用 QQ:(1) 以QQ用户身份登录,并创建 MLUser。(2) 将QQ账号与已有的 MLUser 关联。

登录并注册新 MLUser

MLQQUtils 提供一种方法让你的 MLUser 可以通过 微博 登录或注册。这可以使用 loginInBackgroundWithPermissions:block: 方法实现:

NSArray *permissions = @[@"get_user_info", @"get_simple_userinfo", @"add_t"];
[MLQQUtils loginInBackgroundWithPermissions:permissions block:^(MLUser * _Nullable user, NSError * _Nullable error) {
    if (!user) {
        NSLog(@"QQ登陆失败");
    } else if (user.isNew) {
        NSLog(@"用户使用QQ账户成功注册并登陆");
    } else {
        NSLog(@"用户使用QQ账户登陆");
    }
}];

该代码运行时,会出现以下情况:

  1. 若设备安装了新浪QQ客户端,则会跳转到QQ客户端请求授权,否则弹出QQ授权网页。
  2. 用户确认授权,你的应用程序会收到回调。
  3. 我们的 SDK 会收到QQ数据并将其保存在 MLUser 中。如果是基于QQ身份的新用户,那么该用户随后会被创建。
  4. 你的 block 被调用并带回这个用户对象(user)。

绑定 MLUser 与微博账号

若你想要将已有的 MLUser 与微博帐户关联起来,你可以按以下方式进行关联:

if (![MLQQUtils isLinkedWithUser:user]) {
    [MLQQUtils linkUserInBackground:user withPermissions:@[@"all"] block:^(BOOL succeeded, NSError * _Nullable error) {
        if ([MLQQUtils isLinkedWithUser:user]) {
            NSLog(@"Woohoo, user linked with QQ!");
        }
    }];
}

关联时发生的步骤与登录非常类似。区别是在成功登录中,将会使用来自微博的信息更新当前的 MLUser。今后通过微博进行的登录会返回已存在的 MLUser

解除绑定

若你想要取消用户与微博的关联,操作如下:

[MLQQUtils unlinkUserInBackground:user block:^(BOOL succeeded, NSError * _Nullable error) {
    if (!error && succeeded) {
        NSLog(@"The user is no longer associated with their QQ account.");
    }
}];

在当前用户已经关联了 QQ 账户的情况下,可以使用 [MLQQUtils tencentOAuth].accessToken 获取用户身份验证令牌。

短信验证服务

MaxLeap 短信服务支持的应用场景有以下四种:

  • 用户注册/登录:用户不再需要记住密码,只需要填写手机号和验证码就可以登录,如果用户还没有注册,则会自动注册
  • 用户绑定手机号:用户可以绑定手机号,之后可以使用 手机号/验证码 方式登录
  • 用户操作验证:例如银行金融类应用,用户在对资金进行敏感操作(例如转账、消费等)时,需要通过验证码来验证是否为用户本人操作。
  • 重设密码:用户忘记密码时,可以凭借手机验证码重设密码。

注意:短信验证服务的 API 必须配对使用,否则验证不会通过。+[MLUser requestLoginSmsCodeWithPhoneNumber:block:] 获取到的验证码只能在登录接口 +[MLUser loginWithPhoneNumber:smsCode:block:] 上使用,其他类似。

短信验证码登录

  1. 用户输入手机号

    引导用户正确输入,建议在调用 SDK 接口之前,验证一下手机号的格式。

  2. 请求发送验证码

    用户点击获取验证码按钮,发送成功后该按钮应该变成不可用状态,然后等待至少60秒再允许重新发送。 获取验证码按钮事件调用 +[MLUser requestLoginSmsCodeWithPhoneNumber:block:] 接口给用户发送验证码。

    NSString *phoneNumber = @"18512340000";
    /* verify the phoneNumber */
    // 请求验证码
    [MLUser requestLoginSmsCodeWithPhoneNumber:phoneNumber block:^(BOOL succeeded, NSError * __nullable error) {
        if (succeeded) {
            // 登录验证码发送成功
        } else {
            // 验证码发送失败,检查 error 分析失败原因
        }
    }];
    
  3. 用户输入验证码

    最好验证一下用户输入的是否为纯数字。

  4. 用户登录,调用 +[MLUser loginWithPhoneNumber:smsCode:block:] 接口登录

    [MLUser loginWithPhoneNumber:@"18512340000" 
                             smsCode:@"123456"
                               block:^(MLUser * _Nullable user, NSError * _Nullable error) 
     {
        if (user) {
            // login success
        } else {
            // login failed
        }
    }];
    

    如果不存在用户名为手机号 18512340000 的账户,则会创建一个新用户,用户名(username)为手机号,无密码,mobilePhone 字段也是手机号,mobilePhoneVerifiedtrue。如果存在,直接登录,返回用户的详细信息。

绑定手机号

成功绑定手机号的用户就可以使用 手机号/验证码 方式登录,也可以使用 短信验证码 重设密码。

如果用户填写了手机号,并保存到 mobilePhone 字段,此时手机号为未验证状态。如果用户使用某个功能的时候需要验证手机号,可以调用接口进行验证,验证成功后 mobilePhoneVerified 就会被置为 true

  1. 判断用户手机号是否已验证

    [[MLUser currentUser] fetchInBackgroundWithBlock:^(__kindof MLObject * _Nullable object, NSError * _Nullable error) {
        if ( ! error && [MLUser currentUser].mobilePhoneVerified) {
            // 手机号已验证
        }
    }];
    
  2. 上传手机号

    [MLUser currentUser][@"mobilePhone"] = @"135xxxxxxxx";
    [[MLUser currentUser] saveInBackground:^(BOOL succeeded, NSError *error) {
        // ...
    }];
    
  3. 请求短信验证码

    [[MLUser currentUser] requestMobilePhoneVerifySmsCodeWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
        if (succeeded) {
            // 发送成功
        }
    }];
    
  4. 调用验证接口,验证用户输入的验证码是否正确

    验证通过的用户就可以使用短信验证码方式登陆和重设密码。

    [[MLUser currentUser] verifyMobilePhoneWithSmsCode:@"123456" block:^(BOOL succeeded, NSError * _Nullable error) {
        if (succeeded) {
            // 验证成功, 服务器会把 mobilePhoneVerified 字段改为 true
        }
    }];
    

重设密码

MaxLeap 提供了通过手机号重设密码的功能,验证过手机号的用户(mobilePhoneVerifiedtrue)可以使用这种途径重设密码。

  1. 用户输入手机号,请求发送验证码

    [MLUser requestPasswordResetSmsCodeWithPhoneNumber:@"18512340000" block:^(BOOL succeeded, NSError * _Nullable error) {
        if (succeeded) {
            // 验证码发送成功
        }
    }];
    
  2. 用户输入验证码和新密码,重设密码

    建议要求用户输入两次新密码,以免用户输错

    [MLUser resetPasswordWithPhoneNumber:@"18512340000" 
                                      smsCode:@"123456" 
                                     password:@"sine*&wehIHd" 
                                        block:^(BOOL succeeded, NSError * _Nullable error) 
    {
        if (succeeded) {
            // 重设密码成功,建议要求用户使用新密码重新登录
        }
    }];
    

操作认证

注意,以下两个接口需要在用户登录的状态下使用,匿名登录也可以。

用户执行一些敏感操作(比如支付)时,可以使用短信来验证是否是本人操作。步骤如下:

  1. 用户点击支付按钮
  2. 调用接口发送短信验证码,并等待用户输入验证码

    建议提供一个重新发送验证码的按钮,验证码发送成功后需等待至少 60 秒才可以再次请求。

    注意,在执行这一步时,如果用户还没有提供手机号,则需要要求用户输入手机号。建议要求用户以手机号为用户名注册。

    [MLSmsCodeUtils requestSmsCodeWithPhoneNumber:@"18512340000" block:^(BOOL succeeded, NSError * _Nullable error) {
        if (succeeded) {
        // 验证码发送成功
        }
    }];
    
  3. 用户收到短信,输入验证码

  4. 调用接口验证用户输入的验证码是否有效。

    [MLSmsCodeUtils verifySmsCode:@"123456" phoneNumber:@"18512340000" block:^(BOOL succeeded, NSError * _Nullable error) {
        if (succeeded) {
          // 验证成功
        }
    }];
    

适配 iOS 9 系统

允许 http 请求

默认配置下,iOS 9 以上版本的系统会拦截 http 协议的请求,并打印如下一行文字:

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

~~问题是,部分社交平台接口都使用 http 协议。而且,应用中也可能需要访问 http 协议的接口。~~

~~有一个简单粗暴的解决办法就是,允许所有的 http 请求:~~

~~1. 在项目的 info.plist 文件中添加一个字段:NSAppTransportSecurity,值为字典类型~~ ~~2. 然后再在这个字典中添加一个字段:NSAllowsArbitraryLoads,值为 YES~~

WWDC 2016 中,苹果表示将继续收紧对 HTTP 接口的访问限制。从 2017 年 1 月 1 日起,所有新提交的应用不允许使用 NSAllowsArbitraryLoads 绕过 ATS 限制。最好保证应用中所有请求都是使用 HTTPS 协议,否则可能会在应用审核时遇到麻烦。MaxLeap iOS SDK 所有接口默认都使用 HTTPS 协议。

添加 Scheme 白名单

许多社交平台登录都需要跳转到它们的应用中进行,但是iOS 9 对 canOpenURL: 接口做了限制,导致许多社交平台的 SDK 无法正常跳转到对应的应用中进行分享。

解决办法如下:

  1. 在项目的 info.plist 中添加一个字段:LSApplicationQueriesSchemes,值类型为 Array
  2. 然后在这个数组中添加需要支持的 scheme,各平台的 scheme 列表如下:

    平台scheme
    Facebookfbauth2
    Twitter无需配置
    新浪微博sinaweibo,
    sinaweibohd,
    sinaweibosso,
    sinaweibohdsso,
    weibosdk,
    weibosdk2.5
    微信wechat,
    weixin
    QQmqqOpensdkSSoLogin,
    mqqopensdkapiV2,
    mqqopensdkapiV3,
    wtloginmqq2,
    mqq,
    mqqapi
    QQ空间mqzoneopensdk,
    mqzoneopensdkapi,
    mqzoneopensdkapi19,
    mqzoneopensdkapiV2,
    mqqOpensdkSSoLogin,
    mqqopensdkapiV2,
    mqqopensdkapiV3,
    wtloginmqq2,
    mqqapi,
    mqqwpa,
    mqzone,
    mqq
    注:若同时使用QQ和QQ空间,则只添加本格中的即可

如果没有把平台的 scheme 添加到白名单中,系统会打印如下文字信息:

-canOpenURL: failed for URL: “sinaweibohdsso://xxx” – error: “This app is not allowed to query for scheme sinaweibohdsso”

按照上述方法,把 sinaweibohdsso 加入白名单即可去掉这个日志。

FAQ

Q: 用户每次都请求短信验证码来登录的话,成本太高,有什么解决办法吗?

A: 让用户设置密码,之后用户就可以使用 手机号/密码 方式登录了。

注册时不设密码,注册完成后由用户自行更改密码:

i. 用户输入手机号
ii. 用户点击请求验证码按钮,程序调用 +[MLUser(MLSmsCodeUtils) requestLoginSmsCodeWithPhoneNumber:block:] 接口给用户发送验证码
iii. 用户收到验证码短信后,输入验证码
iv. 用户点击 注册/登录 按钮,程序调用 +[MLUser(MLSmsCodeUtils) loginWithPhoneNumber:smsCode:block:] 接口登录/注册
v. 用户前往用户信息界面,修改密码(具体操作请查阅修改密码短信重置密码两部分)
vi. 以后用户即可以通过 手机号/密码 登录,也可以使用 手机号/验证码 登录

云代码

简介

什么是云代码服务

云代码是部署运行在 MaxLeap 云引擎上的代码,你可以用它来实现较复杂的,需要运行在云端的业务逻辑。它类似于传统的运行在 Web server上的 Web Service或 RESTful API。它对外提供的接口也是 RESTful API,也正是以这种方式被移动应用调用。

目前支持 Java,其他语言尽请期待。

准备

云代码功能集成在 MaxLeap.framework 中,如果你尚未安装,请先查阅SDK 集成小节,安装 SDK 并使之在 Xcode 中运行。

你还可以查看我们的 API 资料,了解有关我们 SDK 的更多详细信息。

注意:我们支持 iOS 7.0 及以上版本。

首先,需要开发云代码,实现所需的接口和 HOOK,开发以及发布过程请根据你的需求选择对应服务端语言

Java 开发指南

云代码调用

发布云代码之后,客户端可以使用 +[MLCloudCode callFunctionInBackground:withParameters:block:] 方法调用云函数。

假如在 CloudCode 中定义了一个名称为 hello 的函数,带一个名字为 name 的参数,返回值为输入的参数字典。现在调用这个云函数:

[MLCloudCode callFunctionInBackground:@"hello"
                       withParameters:@{@"name":@"Alex"} 
                                block:^(id result, NSError *error) {
   if ( ! error ) {
     // result is @{@"name":@"Alex"}
   }
}];

在线参数

简介

什么是在线参数

每个应用在云端都有一个对应的MLConfig对象,用以存储该应用的参数。Cloud Config服务帮助你访问和操作云端参数。你可以通过 Console 在 MaxLeap 中配置应用参数,并且使用 iOS/Android SDK 读取云端的参数。

在线参数中添加参数

你可以通过Console向Cloud Config中增添应用参数。新建云端参数时,你需要指定该参数的以下属性:

属性名
Parameter参数名
Type参数类型
Value参数的值

你还可以为不同的Segment设置不同的参数值。

集成 SDK

在线参数集成在 MaxLeap.framework 中,如果你尚未安装,请先查阅SDK 集成小节,安装 SDK 并使之在 Xcode 中运行。

你还可以查看我们的 API 资料,了解有关我们 SDK 的更多详细信息。

注意:我们支持 iOS 7.0 及以上版本。

获取 MLConfig 对象

MLConfig *currentConfig = [MLConfig currentConfig];

获取MaxLeap中的参数值

// 获取 configname 对应的云参数,可能为 nil
id value1 = [currentConfig objectForKey:@"configname"];

// 获取 configname 对应的云参数值并转化为 NSString 返回,如果没有则返回传入的 defaultValue
NSString *stringValue = [currentConfig stringForKey:@"configname" defaultValue:@"default"];

跟上面类似的方法还有:

– dateForKey:defaultValue:
– arrayForKey:defaultValue:
– dictionaryForKey:defaultValue:
– fileForKey:defaultValue:
– geoPointForKey:defaultValue:
– boolForKey:defaultValue:
– numberForKey:defaultValue:
– integerForKey:defaultValue:
– floatForKey:defaultValue:
– doubleForKey:defaultValue:

获取最新的在线参数

在每次 app 进入前台时,SDK 会自动刷新 currentConfig

也可以调用以下代码手动刷新所有云参数

[MLConfig getConfigInBackgroundWithBlock:^(MLConfig *config, NSError *error) {
    // this config is currentConfig
}];

或者刷新某几个参数

// keys 传入 nil 等同于上面的方法,刷新全部参数的值
[MLConfig getValuesForKeys:@[@"key1", @"key2"] inBackgroundWithBlock:^(MLConfig *config, NSError *error) {
    // this config is currentConfig
}];

刷新后,如果有参数的值发生变化,就会在主线程调用相应的 valueChangedHandler()

监听

添加监听

[MLConfig addObserver:anObserver forKey:@"configname" valueChangedHandler:^(id newValue, id oldValue) {
    // the value changed
}];

移除监听

[MLConfig removeObserver:anObserver forKey:@"configname"];

anObserver 销毁之前必须移除监听者

[MLConfig removeObserver:anObserver]; // 一次性移除所有跟 anObserver 相关的监听回调

在线参数值类型

MLConfig 支持大部分 MLObject 支持的数据类型:

  • NSString
  • NSNumber
  • NSDate
  • NSArray
  • NSDictionary

移动支付

简介

目前支持支付宝、微信、银联支付等渠道,支持支付及查询交易记录功能。我们将持续更新,支持更多支付平台和更多功能,敬请期待。

在后台填写各支付渠道信息

在集成 MaxPay iOS SDK 之前,请确保正确填写了将要集成的支付渠道的支付参数。

  1. 创建 MaxLeap 应用
  2. 打开支付渠道配置页面(MaxLeap 控制台 -> 我的应用 -> 应用设置 -> 支付设置 -> 渠道配置),填写各支付渠道所需数据。

安装 SDK

使用 cocoapods 安装

  1. Podfile 中加上下面这行:

    # 2.2.0 之前版本,需要另外安装银联支付和支付宝支付SDK
    # 2.2.0 之后版本,需要另外安装银联支付SDK
    pod 'MaxLeap/Pay'
    
  2. 打开应用 终端, 执行以下命令:

    $ cd your_project_dir
    $ pod install
    
  3. application:didFinishLaunchingWithOptions: 方法中加入以下代码,初始化 MaxLeap SDK:

    [MaxLeap setApplicationId:@"your_maxleap_applicationId" clientKey:@"your_maxleap_clientKey" site:MLSiteCN];
    // 最后一个参数根据应用所属平台填写,如果是 maxleap.cn 填写 MLSiteCN,如果是 maxleap.com 填写 MLSiteUS
    

手动安装

  1. 下载并解压最新版本的 SDK
  2. 把解压得到的 MaxLeap.frameworkMaxLeapPay.framework 拖到项目中
  3. 添加以下依赖库
    MobileCoreServices.framework
    CoreTelephony.framework
    SystemConfiguration.framework
    libsqlite3.tbd
    libz.tbd

  4. application:didFinishLaunchingWithOptions: 方法中加入以下代码,初始化 MaxLeap SDK:

    [MaxLeap setApplicationId:@"your_maxleap_applicationId" clientKey:@"your_maxleap_clientKey" site:MLSiteCN];
    // 最后一个参数根据应用所属平台填写,如果是 maxleap.cn 填写 MLSiteCN,如果是 maxleap.com 填写 MLSiteUS
    

到这里 MaxLeapPay.framework 已经安装完成,但是所有的支付渠道应该都为不可用状态,因为还没有集成这些渠道对应的支付 SDK。

接入第三方支付平台

只集成 MaxLeapPay.framework 并不能使用第三方支付功能。

接入支付宝移动支付

  1. 下载并解压最新支付宝 SDK
  2. 找到 AliPay 文件夹,该文件夹包含 AliPaySDK.frameworkAliPaySDK.bundle,把该文件夹拖进项目中。
  3. 添加依赖 libc++.tbd

接入微信移动支付

  1. 下载并解压微信支付 SDK
  2. 找到微信 SDK 文件夹,该文件夹应包括 libWeChatSDK.aWXApi.hWXApiObject.h三个文件,把该文件夹拖进项目中。
  3. 添加依赖 libc++.tbd

接入银联手机控件支付

  1. 下载并解压银联手机支付控件建议使用 3.3.6 及以上版本
  2. 找到 “UPPaymentControl” 文件夹,该文件夹包括 libPaymentControl.aUPPaymentControl.h 两个文件,把该文件夹拖到项目中。
  3. 添加依赖 libc++.tbd

你可以设置支付环境,以用来测试。MaxLeapPay 提供了三种环境,Production(产品环境)、Sandbox(沙盒环境)、Offline(离线环境)。

沙盒环境和离线环境都可以用来测试,区别在于:沙盒环境会连接沙盒测试服务器,而离线环境不会连接服务器,只会使用一些模拟数据。

注意:不是所有渠道都支持沙盒环境和离线环境,如果某个渠道不支持当前环境,+[MaxLeapPay isChannelAvaliable:] 会返回 NO.

处理应用跳转回调

某些支付渠道需要跳转到相应平台应用中完成支付,MaxLeapPay 提供了统一的处理方法,

// iOS 4.2 -- iOS 8.4
// 如果需要兼容 iOS 6, iOS 7, iOS 8,需要实现这个代理方法
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {

    // 注意,这里由 `MaxLeapPay` 统一调用各支付平台 SDK 的 `handleOpenUrl:` 方法,可能与其他 SDK 的发生重复调用问题,请注意处理

    return [MaxLeapPay handleOpenUrl:url completion:^(MLPayResult * _Nonnull result) {
        // 支付应用结果回调,保证跳转支付应用过程中,即使调用方app被系统kill时,能通过这个回调取到支付结果。
    }];
}

// iOS 9.0 or later
// iOS 9 以及更新版本会优先调用这个代理方法,如果没有实现这个,则会调用上面那个
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:  (NSDictionary<NSString *,id> *)options {

    // 注意,这里由 `MaxLeapPay` 统一调用各支付平台 SDK 的 `handleOpenUrl:` 方法,可能与其他 SDK 的发生重复调用问题,请注意处理

    return [MaxLeapPay handleOpenUrl:url completion:^(MLPayResult * _Nonnull result) {
        // 支付应用结果回调,保证跳转支付应用过程中,即使调用方app被系统kill时,能通过这个回调取到支付结果。
    }];
}

使用

使用支付宝支付

支付宝移动支付

  1. 接入支付宝,参考支付宝移动支付接入文档.

  2. 登录 MaxLeap 后台,前往 应用设置->支付设置->支付宝,填写相关信息并保存。

  3. 在 Xcode 中,选择你的工程设置项,选中“TARGETS”一栏,在“info”标签栏的“URL type“中添加“URL scheme”, 格式自定义,不能与其它应用的重复,建议添加一个支付宝专用的,比如:alipay1234567

  4. 发起支付:

    // 1. 生成 payment 对象
    MLPayment *payment = [[MLPayment alloc] init];
    
    // 设置使用 AliApp 渠道支付,该渠道会打开支付宝应用进行支付,如果没有安装支付宝应用,支付宝 SDK 会打开一个网页进行支付
    payment.channel = MLPayChannelAliApp;
    
    // 生成交易流水号,流水号号要保证在商户系统中唯一
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyyMMddHHmmssSSS"];
    NSString *billNo = [formatter stringFromDate:[NSDate date]];
    payment.billNo = billNo;
    
    // payment 简要说明
    payment.subject = @"测试";
    
    // 总金额,单位:分
    payment.totalFee = 0.01 * 100;
    
    // 支付宝支付完成后通知支付结果时需要用到,填写上一步中配置的 URL Scheme
    payment.scheme = @"alipay1234567";
    
    // 配置自定义字段
    [payment.extraAttrs addEntriesFromDictionary:@{@"keyA":@"valueA"}];
    
    // 2. 开始支付流程
    [MaxLeapPay startPayment:payment completion:^(MLPayResult * _Nonnull result) {
        if (result.code == MLPaySuccess) {
            NSLog(@"支付成功");
        } else {
            NSLog(@"支付失败");
        }
    }];
    

使用微信支付

微信移动支付

  1. 注册微信移动应用,申请微信支付功能,申请流程可以参照微信官方文档-微信APP支付接入商户服务中心

  2. 登录 MaxLeap 后台,前往 应用设置->支付设置->微信App支付,填写相关信息并保存。

  3. 在Xcode中,选择你的工程设置项,选中“TARGETS”一栏,在“info”标签栏的“URL type“添加“URL scheme”,格式为你所注册的微信应用程序id(如下图所示)。

    drag_sdk_to_project

  4. 实现微信代理协议 WXApiDelegate

    @interface WXApiManager : NSObject <WXApiDelegate>
    @end
    
    @implementation WXApiManager
    
    - (void)onResp:(BaseResp*)resp {
        // 将 PayResponse 交给 MaxLeapPay 处理
        if ([resp isKindOfClass:[PayResp class]]) {
            [MaxLeapPay handleWXPayResponse:(PayResp *)resp];
        }
    }
    @end
    
  5. 配置微信 SDK,在 application:didFinishLaunchingWithOptions:方法中,加入以下代码:

    WXApiManager *wxDelegate = [[WXApiManager alloc] init];
    [MaxLeapPay setWXAppId:@"your_weixin_appId" wxDelegate:wxDelegate description:@"sample"];
    
  6. 发起支付:

    // 1. 生成 payment 对象
    MLPayment *payment = [[MLPayment alloc] init];
    
    // 设置通过”微信移动支付“渠道支付,该支付方式目前要求必须安装有微信应用,否则无法使用
    payment.channel = MLPayChannelWxApp;
    
    // 生成交易流水号,流水号要保证在商户系统中唯一
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyyMMddHHmmssSSS"];
    NSString *billNo = [formatter stringFromDate:[NSDate date]];
    payment.billNo = billNo;
    
    // payment 简要说明
    payment.subject = @"测试";
    
    // 总金额,单位:分
    payment.totalFee = 0.01 * 100;
    
    // 注意:这个值不需要设置,但是需要在 info.plist -> URL Types 中配置微信专用 URL Scheme, scheme 值为微信应用的 appId
    // payment.scheme = @"不需要设置";
    
    // 配置自定义字段
    [payment.extraAttrs addEntriesFromDictionary:@{@"keyA":@"valueA"}];
    
    // 2. 开始支付流程
    [MaxLeapPay startPayment:payment completion:^(MLPayResult * _Nonnull result) {
        if (result.code == MLPaySuccess) {
            NSLog(@"支付成功");
        } else {
            NSLog(@"支付失败");
        }
    }];
    

使用银联支付

MaxPay iOS SDK 通过调用银联官方的手机支付控件来完成银联支付。

银联手机控件支付

  1. 接入银联手机控件支付,得到银联商户代码和证书。

  2. 登录 MaxLeap 后台,前往 应用设置->支付设置->银联手机支付,填写相关信息并保存。

  3. 添加银联支付跳转应用白名单

    uppaywallet
    uppaysdk

  4. 在 Xcode 中,选择你的工程设置项,选中“TARGETS”一栏,在“info”标签栏的“URL type“中添加“URL scheme”, 格式自定义,不能与其它应用的重复,建议添加一个银联专用的,比如:unionpay1234567

  5. 发起支付

    // 1. 生成 payment 对象
    MLPayment *payment = [[MLPayment alloc] init];
    
    // 设置通过”银联手机控件支付“渠道支付
    payment.channel = MLPayChannelUnipayApp;
    
    // 生成交易流水号,流水号要保证在商户系统中唯一
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyyMMddHHmmssSSS"];
    NSString *billNo = [formatter stringFromDate:[NSDate date]];
    payment.billNo = billNo;
    
    // 简要说明
    payment.subject = @"测试";
    
    // 总金额,单位:分
    payment.totalFee = 0.01 * 100;
    
    // 银联支付完成后通知支付结果时需要用到,填写上一步中添加的 URL Scheme
    payment.scheme = @"unionpay1234567";
    
    // 银联需要配置 returnUrl
    payment.returnUrl = @"http://maxleap.cn/returnUrl";
    
    // 配置自定义字段
    [payment.extraAttrs addEntriesFromDictionary:@{@"keyA":@"valueA"}];
    
    // 2. 开始支付流程
    [MaxLeapPay startPayment:payment completion:^(MLPayResult * _Nonnull result) {
        if (result.code == MLPaySuccess) {
            NSLog(@"支付成功");
        } else {
            NSLog(@"支付失败");
        }
    }];
    

交易信息查询

  1. 如果知道交易流水号(billNo)和支付渠道(channel),可以使用 fetchOrderInfoWithBillNo:channel:block: 直接获取交易信息,在回调中,可以检查交易状态。

    NSString *billNo;
    MLPayChannel channel = MLPayChannelAliApp;
    [MaxLeapPay fetchOrderInfoWithBillNo:billNo channel:channel block:^(MLOrder *   _Nonnull order, NSError * _Nonnull error) {
        if (order) {
            if ([order.status isEqualToString:@"pay"]) {
                   // 已经成功支付
            } else {
               // 没有支付
            }
        } else {
            // 查询失败,交易不存在或者网络出错
        }
    }];
    
  2. 如果只知道交易流水号,也可以查询,但由于不同支付渠道中可能存在相同的流水号,因此查询结果中可能会有多条记录。

    [MaxLeapPay queryOrderWithBillNo:@"fffsa" block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
        if (error) {
            // 出错了
        } else {
            if (objects.count == 0) {
                // 交易记录不存在
            } else {
                // 查询到交易记录
            }
        }
    }];
    

返回代码

返回码可通过 payResult.code 获取。

返回码含义
-4支付环境或者渠道不支持
-3连接服务器时出错,详细信息请查看 payResult.error
-2用户取消
-1未知错误
0成功
1根据 appId 找不到对应的 APP 或者 clientKey 不正确
2支付要素在后台没有设置
3channel参数不合法
4缺少必填参数
5参数不合法
6证书错误
7渠道内部错误
14未知的服务器运行时错误,请与技术联系帮助查看

即时通讯

简介

使用 MaxLeap 的即时通讯服务,简称:MaxIM,可以轻松实现一个实时聊天应用,或者一个联机对战类的游戏。除聊天室外的聊天记录都保存在云端,离线消息会通过消息推送及时送达,推送的消息文本可以灵活定制。

安装

MaxIMLib.framework 依赖于 socket.io-client-swift,支持 iOS 8 以上版本的 iOS 系统。

使用 cocoapods 安装(推荐)

CocoaPods 是 Objective-C 的依赖管理工具,现在已经支持 swift,它可以使第三方类库集成工作自动化,大大简化了这些工作。可以查看 CocoaPods 入门指南来进一步了解它。

在 Podfile 中合适的位置添加(建议使用 1.3.1 以上版本,否则可能会遇到兼容性问题,导致意外的 crash):

use_frameworks!
pod "MaxIMLib/IMDynamic"

然后在项目根目录执行 pod install 命令,cocoapods 就会自动将 MaxLeap SDK 集成到你的项目中。

手动安装

注意:SocketIO.framework 使用 8.0 以上版本的 Xcode 编译,可能会带来兼容性问题(使用Xcode8.0编译的在Xcode8.1上使用会出现一些意外的crash),导致运行时 crash。你可以使用官方源码 socket.io-client-swift 解决这个问题。

需要 Xcode 7 或者更新版本。

  1. 下载最新版 MaxIMSDK
  2. 在 Xcode 中打开你的项目,导航到 Project -> Target -> General
  3. 把下载好的 MaxIMLibDynamic.frameworkSocketIO.framework 拖到 Embedded Binaries 下面
  4. 重要:导航到 Project -> Target -> Build Settings 找到 Embedded Content Contains Swift Code,并设置为 YES。
  5. 重要:导航到 Project -> Target -> Build Phases,点击左上角的 + 号,选择 New Run Script Phase,点击刚刚添加的 Run Script 前面的三角符号,展开它,把这段脚本(strip-frameworks)拷贝到代码区域中。

第四步和第五步很重要,不执行可能导致应用无法上传到 iTunes Connect。

为了方便模拟器调试,我们把支持 i386 和 x86_64 的代码也合并到 MaxIMLibDynamic.framework 里面,提交应用时应该去掉这些代码。但是 Xcode 在打包时会把整个动态库原封拷贝到应用程序包中(详情戳这里),在上传到 iTunes Connect 的时候就会出错。strip-frameworks 这个脚本的作用就是去掉多余的代码。

如果需要接受离线推送消息,还需要集成 MaxLeap.framework,集成方法请查阅:SDK 集成小节。另外,在创建 MLIMCient 实例的时候需要传入当前的 installationId。

初始化 IM 客户端,创建 MLIMClient 实例

客户端配置 MLIMClientConfiguration

  • baseURL: 默认为 https://im.maxleap.cn
  • appId: MaxLeap 应用 ApplicationId
  • clientKey: MaxLeap 应用 Client Key
  • installationId: 设备标识,用户登陆时会与这个 id 所标识的设备绑定,当用户断线后,服务器会尝试将消息通过远程推送发送至这台设备,一般可以使用 currentInstallation.installationId
  • shouldLog: 是否打印 socket.io 的日志信息,默认为 NO
  • autoReconnect: 断线后是否自动重连,默认为 YES
  • reconnectAttempts: 自动重连次数,默认为无限次
  • reconnectWait: 自动重连间隔时间,单位:秒;默认为 10秒
  • voipEnabled: 是否将 socket 注册为 voip 服务

创建 MLIMClient 实例

注意:MLIMLib 不能很好地支持多个不同配置的 MLIMClient 实例

// 客户端配置
MLIMClientConfiguration *configuration = [MLIMClientConfiguration 
defaultConfiguration];

// 必选配置
configuration.appId = @"Your_MaxLeap_ApplicationId";
configuration.clientKey = @"Your_MaxLeap_ClientKey";

// 可选配置,用户登陆时会与当前设备绑定,用户处于断线状态时,服务器会尝试将消息通过远程推送发送至该设备
configuration.installationId = [MLInstallation currentInstallation].installationId;

MLIMClient *client = [MLIMClient clientWithConfiguration:configuration];

登录状态管理

登录

MaxIM 支持多种登录方式,还支持非 MaxLeap 账号系统。

登录方式

使用已有账号系统

  1. 使用一个用户 ID 直接建立连接登录

使用 MaxLeap 账号系统

  1. 使用用户名和密码验证登录
  2. 使用手机号和短信验证码登录
  3. 使用第三方平台认证信息登录

使用一个用户 ID 直接建立连接登录

用户 ID 需匹配正则表达式 [a-zA-Z0-9_\-]+

现在登录 Tom 这个 ID,如果 Tom 这个 ID 不存在,系统会创建一个。实现如下:

// 登录,不需要密码
[client loginWithUserId:@"Tom" completion:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        NSLog(@"登录成功");
    }
}];

使用用户名和密码验证登录

此登录方式会使用 MaxLeap 账户系统的用户名与密码校验,需用户名与密码相匹配才能成功登录。登录成功后会使用 MLUser 的 objectId 作为 IM 系统的用户 ID。

// 登录,需要用户名和密码
[client loginWithUsername:@"Tom" password:@"pwd" completion:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        NSLog(@"登录成功, 用户 ID 为: %@", client.currentUser.uid);
    }
}];

使用手机号和短信验证码登录

此登录方式无需注册。但是,用户每次登录时,都需要填写手机号,然后请求一个短信验证码。

NSString *phoneNumber;
// 用户填写手机号,请求短信验证码
[MLUser requestLoginSmsCodeWithPhoneNumber:phoneNumber block:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // 验证码发送成功,...
    }
}];
//...
// 用户收到短信后填写验证码
NSString *smsCode;
// 登录
[client loginWithPhoneNumber:phoneNumber smsCode:smsCode completion:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

使用第三方平台授权数据登录

[MLUser currentUser].oauthData 需要 MaxLeap v2.0.9 以上版本支持。

#import <MaxLeap/MLUser.h>

NSDictionary *authData = [MLUser currentUser].oauthData;
[client loginWithThirdPartyOAuth:authData completion:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

断开连接(不是登出)

应用进入后台一段时间后,可能需要暂时断开连接。手动断开连接(并非登出)代码如下:

[client pause];

假设用户现在只使用当前终端登录,客户端暂时断开连接后,用户会出于离线状态。离线状态下的消息会通过远程推送的方式送达,这需要客户端打开远程推送功能。详情请查阅 离线消息推送 一节。

重新连接

用户切换回前台后需要手动连接。

[client resume];

登出(注销)

调用该方法,会解除与当前设备的绑定。当前设备将不会再收到任何消息,包括离线推送消息

[client logoutWithCompletion:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        NSLog(@"注销成功");
    } else {
        NSLog(@"注销失败, error: %@", error);
    }
}];

陌生人聊天

发消息给陌生人

[client sendMessage:message toStranger:@"strangerId" completion:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

获取与陌生人的聊天记录:

与陌生人的聊天记录会在云端保存一年。

获取与 strangerA 最近 10 条聊天记录
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
[client.currentUser getLatestChatsWithStranger:@"strangerA" before:ts limit:10 block:^(NSArray<MLIMMessage *> * _Nullable messages, NSError * _Nullable error) {
    // ...
}];

获取最近联系过的陌生人列表:

// 可选项
NSDictionary *params = @{@"limit":@"10", // 限制返回陌生人数量
                         @"skip":@"0",   // 跳过结果中的前面几条
                         @"ids":@"id1,id2,id3" // 显式指定陌生人 ID
                         };
[client.currentUser fetchStrangersWithDetail:YES params:params completion:^(NSArray<MLIMRelationInfo *> * _Nullable result, NSError * _Nullable error) {
    // ...
}];

获取某个陌生人的信息

[client.currentUser getInfoOfStranger:@"strangerA" completion:^(MLIMRelationInfo * _Nonnull info, NSError * _Nullable error) {
    // ...
    if (info.online) {
        // 用户 strangerA 在线
    }
}];

监听陌生人在线状态

一般情况下,用户上下线事件不回推送给陌生人。如果想获取陌生人在线事件,只能通过 restful api 获取:-[MLIMUser getInfoOfStranger:completion:]

如果某用户开启了 notifyAll 选项(iOS SDK 目前不提供此接口),他的在线状态变化将推送至所有人,我们可以监听到他的上下线事件:

  1. 实现代理 MLIMClientDelegate 接口:

    #pragma mark - MLIMClientDelegate
    
    - (void)client:(MLIMClient *)client someoneDidOnline:(MLIMRelationInfo *)aFriend {
        // ...
    }
    
    - (void)client:(MLIMClient *)client someoneDidOffline:(MLIMRelationInfo *)aFriend {
        // ...
    }
    
  2. 好友上下线的时候,都会发布通知,通过监听通知实现:

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didOnline:) name:MLIMSomeoneOnlineNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didOffline:) name:MLIMSomeoneOfflineNotification object:nil];
    
    - (void)didOnline:(NSNotification *)notification {
        NSString *userId = notification.userInfo[@"id"];
        // ...
    }
    
    - (void)didOffline:(NSNotification *)notification {
        NSString *userId = notification.userInfo[@"id"];
        // ...
    }
    

好友管理

加好友

使用此接口添加对方为好友,无需经过对方的同意,自己也会出现在对方好友列表中。

[client.currentUser addFriend:@"friendUserId" completion:^(NSDictionary * _Nonnull result, NSError * _Nullable error) {
    // ...
}];

批量加好友

[client.currentUser batchAddFriends:@[@"a", @"b"] completion:^(NSArray<NSDictionary *> * _Nonnull result, NSError * _Nullable error) {
    // ...
}];

删除好友

[client.currentUser deleteFriend:@"friendUserId" completion:^(BOOL success, NSError * _Nullable error) {
    // ...
}];

监听好友上下线事件

  1. 实现代理 MLIMClientDelegate 接口:

    #pragma mark - MLIMClientDelegate
    
    - (void)client:(MLIMClient *)client friendDidOnline:(MLIMRelationInfo *)aFriend {
        // ...
    }
    
    - (void)client:(MLIMClient *)client friendDidOffline:(MLIMRelationInfo *)aFriend {
        // ...
    }
    
  2. 好友上下线的时候,都会发布通知,通过监听通知实现:

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didOnline:) name:MLIMFriendOnlineNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didOffline:) name:MLIMFriendOfflineNotification object:nil];
    
    - (void)didOnline:(NSNotification *)notification {
        NSString *userId = notification.userInfo[@"id"];
        // ...
    }
    
    - (void)didOffline:(NSNotification *)notification {
        NSString *userId = notification.userInfo[@"id"];
        // ...
    }
    
  3. 好友上下线的时候,MLIMRelationInfoonline 属性会跟着改变(注意:此功能不支持多个 client 实例)

获取所有好友信息

[client.currentUser fetchFriendsWithDetail:YES completion:^(BOOL success, NSError * _Nullable error) {
    NSLog(@"friends: %@", client.currentUser.friends);
    // ...
}];
// 注:该方法第一个参数表示是否获取好友详细信息,如果为 YES 则拉取全部信息,否则只返回好友 ID
// 获取成功后,好友信息会保存在 user.friends 数组中

拉取单个好友详细数据

假如只知道好友的 ID,要拿好友详细信息,代码如下:

[client.currentUser getFriendInfo:@"fid" completion:^(MLIMRelationInfo * _Nonnull info, NSError * _Nullable error) {
    // ...
}];

群组管理

建立群组

NSString *owner = self.client.currentUser.uid;
// 创建群组,并把 Jerry 拉进群
[MLIMGroup createWithOwner:owner name:@"Tom's group" members:@[owner, @"Jerry"]  block:^(MLIMGroup * _Nonnull group, NSError * _Nonnull error) {
    if (group) {
        // 创建成功
    } else {
        // 创建失败,检查 error 查看失败原因
    }
}];

加入群组:

MLIMGroup *group = [MLIMGroup groupWithId:@"gid"];
[group addMembers:@[@"Bob"] block:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // 成功 ...
    }
}];

获取所有已加入的群组

// 设置是否获取群组详细信息,如果为 YES 则拉取全部信息,否则只返回群组 ID
// 获取成功后,好友信息会保存在 user.groups 数组中
BOOL getGroupDetail = YES;
[client.currentUser fetchGroupsWithDetail:getGroupDetail completion:^(BOOL success, NSError * _Nullable error) {
    if (success) {
        NSLog(@"groups: %@", client.currentUser.groups);
    }
    // ...
}];

获取指定群组的信息

MLIMGroup *group = [MLIMGroup groupWithId:@"gid"];
[group fetchWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
    NSLog(@"group: %@", group);
    // 查看日志,看看 group 中多了什么?
    // ...
}];
// 获取到的值会自动填充到 group 对应的属性中

退出群组:

MLIMGroup *group = [MLIMGroup groupWithId:@"gid"];
[group removeMembers:@[@"Bob"] block:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // 成功 ...
    }
}];

解散群组

[group deleteWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

聊天室管理

建立聊天室

NSString *owner = self.client.currentUser.uid;
// 创建群组,并把 Jerry 拉进聊天室
[MLIMRoom createWithName:@"Tom's room" members:@[owner, @"Jerry"]  block:^(MLIMGroup * _Nonnull group, NSError * _Nonnull error) {
    if (group) {
        // 创建成功
    } else {
        // 创建失败
    }
}];

加入聊天室:

MLIMRoom *room = [MLIMRoom roomWithId:@"rid"];
[room addMembers:@[@"Bob"] block:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // 成功 ...
    }
}];

获取所有加入的聊天室

[client.currentUser fetchRoomsWithDetail:YES completion:^(BOOL success, NSError * _Nullable error) {
    NSLog(@"rooms: %@", client.currentUser.rooms);
    // ...
}];
// 注:该方法第一个参数表示是否获取聊天室详细信息,如果为 YES 则拉取全部信息,否则只返回聊天室 ID
// 获取成功后,聊天室信息会保存在 user.rooms 数组中

获取指定聊天室的信息

MLIMRoom *room = [MLIMRoom roomWithId:@"gid"];
[room fetchWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];
// 获取到的值会自动填充到 room 对应的属性中

退出聊天室:

MLIMRoom *room = [MLIMRoom roomWithId:@"rid"];
[room removeMembers:@[@"Bob"] block:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // 成功 ...
    }
}];

解散聊天室

MLIMRoom *room = [MLIMRoom roomWithId:@"rid"];
[room deleteWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

游客管理

创建或更新游客

创建游客和更新游客信息使用的是同一个接口。如果传入的属性字典中有 id 字段,并且这个游客已经存在,那就是更新操作,否则系统会创建一个新的游客。

创建游客:

// 注意:这个字典中没有 id 字段
NSDictionary *attrs = @{@"foo":@"bar", @"age":@23};
[MLIMPassenger createOrUpdatePassengerWithAttributes:attrs
                                          completion:^(MLIMPassenger * _Nullable passenger, NSError * _Nullable error)
{
    // ...
}];

更新游客信息:

假设存在一个 id 为 772b12084d7c413a9d03df04363b71dd 的游客,更新他的信息:

// 注意:这个字典中必须填写 id 字段
NSDictionary *attrs = @{@"id":@"772b12084d7c413a9d03df04363b71dd", 
                        @"foo":@"bar", 
                        @"age":@23};
[MLIMPassenger createOrUpdatePassengerWithAttributes:attrs
                                          completion:^(MLIMPassenger * _Nullable passenger, NSError * _Nullable error)
{
    // ...
}];

如果你已经持有一个 passenger 对象(passenger.pid 不能为空),可以这样更新:

MLIMPassenger *passenger;
NSDictionary *attrs = @{@"nickname":@"xiaobao"};
[passenger updatePassengerAttributes:attrs completion:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // ...
    }
}];

根据游客 ID 获取游客信息

NSString *pid = @"772b12084d7c413a9d03df04363b71dd";
MLIMPassenger *passenger = [MLIMPassenger passengerWithId:pid];
[passenger fetchWithCompletion:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // ...
    }
}];

消息(聊天)

消息实体类

MaxIMLib 中, MLIMMessage 代表一条消息。它其中字段的含义如下:

  1. mediaType: 消息媒体类型,目前支持文本,图片,音频,视频四种类型

  2. text: 文本消息内容,如果 mediaType 不是文本类型,该字段内容会被忽略

  3. attachmentUrl: 非文本消息(如音频消息,图片消息)的附件地址,文本消息忽略该字段

  4. sender: 发送者,表示谁发送过来的,MaxIM 支持多终端登录和同步消息,发送方有可能是当前登录用户,说明这条消息是该用户使用别的终端发送的消息

    sender.type 消息来源类型,好友/群组/聊天室/游客
    sender.userId 发送者的 ID
    sender.groupId 如果消息来自群组,该字段表示该群组的 ID,否则为 nil
    sender.roomId 如果消息来自聊天室,该字段表示该群组的 ID,否则为 nil

  5. receiver: 接收方,跟 sender 有一样的结构

  6. status: 消息状态,发送中,发送成功,发送失败等

  7. sendTimestamp: 消息发送时间,距离1970年的秒数

聊天

  • 跟单个用户聊天需要先加对方好友,同样的,向群组和聊天室发送消息需要先加入对应的群组和聊天室。
  • 好友离线时,消息会通过离线消息推送发给对方,详细信息请查阅离线消息推送小节。
  • 好友消息和群组消息会在云端保存七天,聊天室消息不会保存。

发送文本消息

发送给好友/群组/聊天室的消息是通过 socket 发送的。

// 登录成功的状态下
// 创建一条文本消息
MLIMMessage *message = [MLIMMessage messageWithText:@"Hi!"];

// 消息发给谁是通过设置 receiver 实现的,注意 receiver 只能设置一次,第二次改变可能会失效

// 指定消息的接收者为好友 Jerry,这条消息就会发送给 Jerry
message.receiver.userId = @"Jerry";
// 指定消息的接收者为群组 GroupA,这条消息就会发送给ID为 GroupA 的群组
message.receiver.groupId = @"GroupA";
// 指定消息的接收者为聊天室 RoomA,这条消息就会发送给ID为 RoomA 的聊天室
message.receiver.roomId = @"RoomA";

[client sendMessage:message completion:^(BOOL succeeded, NSError * _Nullable error) {
    if (!succeeded) {
        NSLog(@"消息发送失败, error: %@", error);
    }
    [self.tableView reloadData];
    [self scrollToBottom];
}];

也可以使用 SDK 提供的便捷方法,这些便捷方法会自动更改 message.receiver

-[MLIMClient sendMessage:toFriend:completion:]
-[MLIMClient sendMessage:toGroup:completion:]
-[MLIMClient sendMessage:toRoom:completion:]

接收消息

  1. 实现代理方法

    - (void)jerryLogin {
        MLIMClientConfiguration *configuration = [MLIMClientConfiguration defaultConfiguration];
        configuration.appId = @"Your_MaxLeap_ApplicationId";
        configuration.clientKey = @"Your_MaxLeap_ClientKey";
    
        self.client = [MLIMClient clientWithConfiguration:configuration];
        self.client.delegate = self;
        [self.client loginWithUserId:@"Jerry" completion:^(BOOL succeeded, NSError * _Nullable error) {
             // ...
        }];
    }
    
    #pragma mark - MLIMClientDelegate
    
    // 接收好友的消息
    - (void)client:(MLIMClient *)client didReceiveMessage:(MLIMMessage *)message fromFriend:(MLIMRelationInfo *)aFriend {
        if ([aFriend.uid isEqualToString:@"Tom"]) {
            if ([message.sender.userId isEqualToString:client.currentUser.uid]) {
                // NSLog(@"Did receive Jerry's message send via another client.");
            } else {
                // NSLog(@"Did receive Tom's message");
            }
        }
    }
    
    // 接收群组消息
    - (void)client:(MLIMClient *)client didReceiveMessage:(MLIMMessage *)message fromGroup:(MLIMGroup *)group {
        if ([message.sender.userId isEqualToString:client.currentUser.uid]) {
            // NSLog(@"Did receive Jerry's message send to the group via another client.");
        } else {
            // NSLog(@"Did receive group message:%@", message);
        }
    }
    
    // 接收聊天室消息
    - (void)client:(MLIMClient *)client didReceiveMessage:(MLIMMessage *)message fromRoom:(MLIMRoom *)room {
        if ([message.sender.userId isEqualToString:client.currentUser.uid]) {
            // NSLog(@"Did receive Jerry's message send to the room via another client.");
        } else {
            // NSLog(@"Did receive room message:%@", message);
        }
    }
    
  2. 监听通知

    // 所有非系统消息都会通过这个通知发送
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMessage:) name:MLIMClientDidReceiveMessageNotification object:nil];
    
    - (void)didReceiveMessage:(NSNotification *)notification {
    
        // 取出这条消息
        MLIMMessage *message = notification.userInfo[@"msg"];
    
        // 判断消息是否来自好友 Jerry
        BOOL fromFriend = message.sender.type == MLIMMessageTargetTypeFriend && [message.sender.userId isEqualToString:@"Jerry"];
    
        // 判断消息是否来自群组 GroupA 
        BOOL fromGroup = message.sender.type == MLIMMessageTargetTypeGroup && [message.sender.groupId isEqualToString:@"GroupA"];
    
        // 判断消息是否来自聊天室 RoomA
        BOOL fromRoom = message.sender.type == MLIMMessageTargetTypeRoom && [message.sender.roomId isEqualToString:@"RoomA"];
    
        // 当前用户
        NSString *cuid = client.currentUser.uid;
    
        // 判断消息是否是当前用户使用其他终端发送给 Jerry 的
        BOOL fromSelfToFriend = [message.sender.userId isEqualToString:cuid]
        && [message.receiver.userId isEqualToString:@"Jerry"];
    
        // 判断消息是否是当前用户使用其他终端发送给群组 GroupA 的
        BOOL fromSelfToFriend = [message.sender.userId isEqualToString:cuid]
        && [message.receiver.groupId isEqualToString:@"GroupA"];
    
        // 判断消息是否是当前用户使用其他终端发送给聊天室 RoomA 的
        BOOL fromSelfToFriend = [message.sender.userId isEqualToString:cuid]
        && [message.receiver.roomId isEqualToString:@"RoomA"];
    }
    

多媒体消息

除了基本的文字聊天,MaxIM 也支持多媒体消息,多媒体消息在初始化时需要多媒体文件。

构建多媒体消息:

// 图片消息
MLIMMessage *imageMsg = [MLIMMessage messageWithImage:image];

// 视频消息
NSString *path = [[NSBundle mainBundle] pathForResource:@"IMG_0018" ofType:@"m4v"];
MLIMMessage *videoMsg = [MLIMMessage messageWithVideoFileAtPath:path];

// 音频消息
MLIMMessage *message = [MLIMMessage messageWithAudioFileAtPath:audioFilePath];

调用发送消息的接口时会先上传多媒体文件,为了聊天实时性,请严格控制多媒体文件的大小。

[client sendMessage:message progress:^(int percentDone) {
    NSLog(@"消息附件上传进度: %d%%", percentDone);
} completion:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // 消息发送成功
    } else {
        NSLog(@"消息发送失败, error: %@", error);
    }
}];

多终端消息同步

MaxIM 支持多终端同时登录和多终端消息同步。如果用户同时登录的终端A和终端B,他使用终端A发送消息,那么终端B会收到这条消息,判断方法如下:

if ([message.sender.userId isEqualToString:client.currentUser.uid]) {
    // 这条消息是当前登录用户使用别的终端发送的
}

系统消息

系统消息不是通过 socket 发送的。

发送系统消息

MLIMMessage *msg = [MLIMMessage messageWithText:@"test"];
// 注意:发送目标只能设置一次
// 设置发送目标为某个用户
msg.receiver.userId = @"Jerry";

// 设置发送目标为
msg.receiver.groupId = @"GroupA";

// 设置发送目标为聊天室
msg.receiver.roomId = @"RoomA";

// 发送消息
[client sendSystemMessage:message progress:^(int percentDone) {
    // 多媒体消息附件上传进度
} completion:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

也可以使用 SDK 提供的便捷接口发送消息:

-[MLIMClient sendSystemMessage:toUser:completion:]
-[MLIMClient sendSystemMessage:toGroup:completion:]
-[MLIMClient sendSystemMessage:toRoom:completion:]

发送给所有用户:

[client sendSystemMessageToAllUsers:msg completion:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // ...
    }
}];

接受系统消息

接收到的系统消息不会带有 sender 和 receiver 信息。

  1. 实现代理方法

    #pragma mark - MLIMClientDelegate
    
    - (void)client:(MLIMClient *)client didReceiveSystemMessage:(MLIMMessage *)message {
        NSLog(@"Did receive room message:%@" message);
        // 接收到的系统消息可能不带有发送者以及接受者的信息
    }
    
  2. 监听通知

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveSysMessage:) name:MLIMClientDidReceiveSystemMessageNotification object:nil];
    
    - (void)didReceiveSysMessage:(NSNotification *)notification {
        // 取出这条消息
        MLIMMessage *message = notification.userInfo[@"msg"];
        // message 不带有 receiver 和 sender 信息
        // ...
    }
    

聊天记录

好友和群组聊天记录会在云端保存七天,其他消息不会保存。

获取好友的聊天记录

// 获取当前时间之前最新的十条历史消息(包括自己发的)
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
[client.currentUser getLatestChatsWithFriend:@"friend_uid" beforeTimestamp:ts limit:10 block:^(NSArray<MLIMMessage *> * _Nullable messages, NSError * _Nullable error) {
    if (!error) {
        NSLog(@"lastest history messages: %@", messages);
    }
}];

获取群组聊天记录

// 获取当前时间最新的十条消息(包括自己发送的)
NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
[group getLatestMessagesBefore:ts limit:10 completion:^(NSArray<MLIMMessage *> * _Nullable messages, NSError * _Nullable error) {
    if (!error) {
        NSLog(@"lastest group messages: %@", messages);
    }
}];

获取游客最新的聊天记录

NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
NSString *pid = @"772b12084d7c413a9d03df04363b71dd";
MLIMPassenger *passenger = [MLIMPassenger passengerWithId:pid];
[passenger getHistoryMessagesWithUser:@"wind"
                           beforeTime:ts
                                limit:20
                           completion:^(NSArray<MLIMMessage *> *_Nullable messages,
                                        NSError * _Nullable error)
{
    NSLog(@"messages: %@, error: %@", messages, error);
}];

离线推送消息

当用户离线,并且没用注销的时候,如果收到好友或者群组消息,系统会尝试给该用户发送推送。为了使用户能正常接收推送消息,请客户端远程推送功能。

开启远程推送的流程如下:

集成 MaxLeap.framework

MaxIM 离线消息推送依赖于 MaxLeap 推送服务,所以需要集成 MaxLeap.framework,集成方法请查阅:QuickStart - Core SDK

另外,在创建 MLIMCient 实例的时候需要传入当前的 installationId :

MLIMClientConfiguration *configuration = [MLIMClientConfiguration 
defaultConfiguration];
configuration.appId = @"Your_MaxLeap_ApplicationId";
configuration.clientKey = @"Your_MaxLeap_ClientKey";

// 需要配置 installationId 才能收到离线推送
configuration.installationId = [MLInstallation currentInstallation].installationId;

MLIMClient *client = [MLIMClient clientWithConfiguration:configuration];

配置

首先要申请并上传远程推送证书,详细步骤请参照:iOS 推送证书设置指南

appDelegate.m 中,可以使用下面的代码开启远程推送

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    [MaxLeap setApplicationId:@"5552f51660b2056aa87dd9e0" clientKey:@"c3JscE50TWNnVzg4SkZlUnFsc3E2QQ" site:MLSiteCN];

    [self registerRemoteNotifications];

    [MLMarketingManager enable];
    // 统计推送点击事件
    [MLMarketingManager handlePushNotificationOpened:launchOptions];

    return YES;
}

- (void)registerRemoteNotifications {
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        UIUserNotificationSettings *pushsettings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge|UIUserNotificationTypeSound|UIUserNotificationTypeAlert categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:pushsettings];
    } else {
//#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeAlert];
//#endif
    }
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    // 将 device token 保存到 MaxLeap 服务器,以便服务器向本设备发送远程推送
    [[MLInstallation currentInstallation] setDeviceTokenFromData:deviceToken];
    [[MLInstallation currentInstallation] saveInBackgroundWithBlock:nil];
}

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    [application registerForRemoteNotifications];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    NSLog(@"%@ %@", NSStringFromSelector(_cmd), userInfo);
    [self parseMessageEntityFromNotificationPayload:userInfo];
}

// 实现这个代理方法,需要打开 remote notification background mode
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler {
    [self parseMessageEntityFromNotificationPayload:userInfo];
    completionHandler(UIBackgroundFetchResultNoData);
}

- (void)parseMessageEntityFromNotificationPayload:(NSDictionary *)userInfo {
    if (userInfo[@"parrot"] && [userInfo[@"parrot"] isKindOfClass:[NSString class]]) {
        NSData *jsonData = [userInfo[@"parrot"] dataUsingEncoding:NSUTF8StringEncoding];
        NSDictionary *messagePayload = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
        if (messagePayload) {
            MLIMMessage *message = [[MLIMMessage alloc] initWithPayloadDictionary:messagePayload];
            // ...
        }
    }
}

自定义属性

MaxIM 系统可以给一个游客、用户、群组或者聊天室设置自定义属性。自定义属性保存在一个 JSON Object 中,键值必须是 JSON 支持的数据类型,可以嵌套,键值对可以随意增加。

该节提到的 API 适用于 MLIMUserMLIMGroupMLIMRoomMLIMPassenger 请参照 [游客] 小节。

部分更新自定义属性:

id object; // MLIMUser, MLIMGroup 或 MLIMRoom

// 只更新该字典中存在的键值对,其他的不受影响。
NSDictionary *attrs = @{@"nickname":@"acher", @"age":@29};

[object updateAttributes:attrs completion:^(BOOL success, NSError * _Nullable error) {
    NSLog(@"attributes: %@", object.attributes);
}];

覆盖更新自定义属性

id object; // MLIMUser, MLIMGroup 或 MLIMRoom

// 不同于部分更新,该接口直接使用新的字典覆盖用户属性
NSDictionary *attrs = @{@"nickname":@"acher", @"age":@29};

[object replaceAttributes:attrs completion:^(BOOL success, NSError * _Nullable error) {
    NSLog(@"attributes: %@", object.attributes);
}];

获取自定义属性

id object; // MLIMUser, MLIMGroup 或 MLIMRoom

[object fetchAttributesWithCompletion:^(NSDictionary * _Nullable attrs, NSError * _Nullable error) {
    NSLog(@"attributes: %@", object.attributes);
}];

获取单个自定义属性的值

id object; // MLIMUser, MLIMGroup 或 MLIMRoom

[object getAttributeForKey:@"age" completion:^(id  _Nullable value, NSError * _Nullable error) {
    // 注意:如果 age 对应的值为空,value 会是一个 NSNull 对象
    NSLog(@"value: %@", value);
}];

删除所有的自定义属性

id object; // MLIMUser, MLIMGroup 或 MLIMRoom

[object deleteAttributesWithCompletion:^(BOOL success, NSError * _Nullable error) {
    if (success) {
        //...
    }
}];

查询

MaxIM 也支持对用户、群组、聊天室进行查询,根据它们的自定义属性进行过滤。SDK 使用一个 MLIMQuery 来实现查询,它使用起来跟 MLQuery 类似,但是简化很多。

查询分为三步:

  1. 创建一个 MLIMQuery 对象;
  2. MLIMQuery 对象添加过滤条件;
  3. 执行查询方法,获取与过滤条件相匹配的数据。

例如,查询自定义属性的 type 值为 1 的用户,并按照 age 升序排列:

MLIMQuery *query = [MLIMQuery query];
[query whereAttribute:@"type" equalTo:@"1"];
[query orderByAscending:@"age"];
[query findUserWithBlock:^(NSArray<MLIMUser *> * _Nullable result, NSError * _Nullable error) {
    XCTAssertTrue(result.count <= query.limit);
    fulfill();
}];

查询约束

设置查询约束, 这个约束支持模糊查询:

注意:equalTo: 参数值是 String 类型

[query whereAttribute:@"type" equalTo:@"1"];

也可以添加多个约束,它们之间是 AND 的关系:

[query whereAttribute:@"type" equalTo:@"1"];
[query whereAttribute:@"gender" equalTo:@"male"];

可以通过设置 limit 来限制结果的数量,默认的数量限制为 20:

query.limit = 30; // 最多返回三十条数据

skip 用来跳过查询结果中开头的一些数据,配合 limit 可以对结果进行分页:

query.skip = 2*30; // 跳过前 60 条数据,如果 limit 为 30,就是获取第三页数据

对结果进行排序:

对于可排序的数据,如数字和字符串,你可以控制结果返回的顺序:

// Sorts the results in ascending order by the createdAt field
[query orderByAscending:@"createdAt"];
// Sorts the results in descending order by the createdAt field
[query orderByDescending:@"createdAt"];

一个查询可以使用多个排序键,如下:

// Sorts the results in ascending order by the score field if the previous sort keys are equal.
[query addAscendingOrder:@"score"];
// Sorts the results in descending order by the score field if the previous sort keys are equal.
[query addDescendingOrder:@"username"];

错误码

错误码含义备注
5001非法的参数错误
5002数据库异常
5003未授权的操作
5004请求的对象不存在
5005请求参数冲突
5006文件存储服务异常
5007无法完成图片处理
5008成员数已经达到上限
5009未支持的操作
5010请求参数超长
5011文件大小超过限制
5012文件上传错误

推送证书设置指南

  1. 生成推送证书,参照苹果官方文档《App Distribution Guide》的 Creating a Universal Push Notification Client SSL Certificate 小节。

  2. 安装证书

    下载并双击证书,点击弹出框右下角的添加按钮,把证书导入到钥匙串中。

    钥匙串中选择左边的 loginMy Certificates,这时应该能在右边找到刚刚导入的证书。

  3. 导出 .p12 文件

    注意不要展开 private key

  4. 将文件保存为 Personal Information Exchange (.p12) 格式。

  5. 上传证书

    MaxLeap 管理平台:应用设置 - 推送通知 上,选择对应的应用程序,上传之前获得的 .p12 文件。这是集成 MaxLeap 推送的必要步骤。

应用内社交

简介

应用内社交,在应用开发中出现的场景非常多,包括用户间关注(好友)、朋友圈(时间线)、状态广场、互动(评论、点赞)等常用功能,应用内社交可以认为是一个应用基础通用功能。

集成

MaxSocial.framework 依赖于 MaxLeap.framework,如果还没集成 MaxLeap.framework, 请先查阅SDK 集成小节,集成并配置好 MaxLeap.framework.

使用 Cocoapods 安装

Podfile 中加上下面这行:

pod 'MaxLeap/Social'

打开应用 终端,执行以下命令:

$ cd your_project_dir
$ pod install

手动安装

  1. 下载并解压最新 SDK
  2. 把解压得到的 MaxSoical.framework 拖到项目中。

使用方法

首先根据 userId 创建一个用户对象:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];

用户关系

关注其他用户:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
NSString *anotherUserId = @"anotherUserId";

// user 关注 anotherUser
// reverse 表示是否要自动反向关注
[user followUser:anotherUserId reverse:YES block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    // 关注的实现方式是插入一条 relation 数据,表示 A 关注了 B,如果自动反向关注,则创建两条这样的数据
    // 结果数组中会有两个字典对象
    NSLog(@"result: %@, error: %@", objects, error);
}];

取消关注:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
NSString *anotherUserId = @"anotherUserId";

[user unfollowUser:anotherUserId block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

查询是否关注了某用户:

NSString *a = @"sd";
NSString *b = @"asdgaeesdage";
[MaxSocialUser queryWhetherUser:a isFollowingUser:b resultBlock:^(BOOL isFollowing, BOOL isReverse, NSError * _Nonnull error) {
    // isFollowing 为 YES 表示 a 关注了 b
    // isReverse 为 YES 表示 a 和 b 相互关注了
}];

屏蔽某个人,A 屏蔽 B:

// A 屏蔽 B, 假如,B 还未关注 A,调用这个接口后,B 会关注 A,但不能看 A 的动态
NSString *userBId = @"";
BOOL block = YES; // 是否屏蔽 userB,YES 表示屏蔽,NO 表示取消屏蔽
MaxSocialUser *userA = [MaxSocialUser userWithId:@"userAId"];
[userA block:block user:userBId completion:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

获取关注列表,返回的列表是 MaxSocialRelationInfo 对象数组:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
MaxSocialQuery *query = [MaxSocialQuery new]; // 使用默认查询条件,也可以对其属性进行更改
[user getFolloweesWithQuery:query block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    NSLog(@"followeeeeeees: %@, error: %@", objects, error);
}];

获取粉丝列表,返回的列表是 MaxSocialRelationInfo 对象数组: 注意:这个接口跟上面的接口只相差一个字母,是 get followers

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
MaxSocialQuery *query = [MaxSocialQuery new]; // 使用默认查询条件,也可以对其属性进行更改
[user getFollowersWithQuery:query block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    NSLog(@"followers: %@, error: %@", objects, error);
}];

根据 relation 的 objectId 获取这条数据:

NSString *relationInfoId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getRelationInfoWithId:relationInfoId block:^(MaxSocialRelationInfo * _Nullable relation, NSError * _Nullable error) {
    // ...
}];

根据 relation 的 objectId 删除这条数据:

NSString *relationInfoId = nil;
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user deleteRelationInfoWithId:relationInfoId block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

位置

更新用户的地理位置:

MaxSocialLocation *location = [MaxSocialLocation locationWithLatitude:22 longitude:35];
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user updateLocation:location block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

获取用户的地理位置信息:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getLocationInfoWithBlock:^(MaxSocialLocationInfo * _Nullable location, NSError * _Nullable error) {
    NSLog(@"user location: %@, error: %@", location, error);
}];

删除用户的地理位置信息:

NSString *locatioinInfoId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user deleteLocationInfoWithObjectId:locatioinInfoId block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

查询附近的用户,实际返回的是一个 MaxSocialLocationInfo 对象列表,可以通过 MaxSocialLocationInfouserId 属性获取附近用户的信息:

MaxSocialLocation *location = [MaxSocialLocation locationWithLatitude:22 longitude:35];
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user queryUserNearLocation:location distance:10086 block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    NSLog(@"People nearby: %@, error: %@", objects, error);
}];

根据 MaxSocialLocationInfoobjectId 获取详细内容:

NSString *locationInfoId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user fetchLocationInfoWithObjectId:locationInfoId block:^(MaxSocialLocationInfo * _Nullable location, NSError * _Nullable error) {
     // ...
}];

说说

发布说说:

可以发表四种类型的说说:1. 纯文字,2. 纯链接,3. 文字 + 链接,4. 文字 + 图片
同时可以控制是否发布说说到广场上,发布到广场上的说说会同时出现在朋友圈和广场,否则说说只会出现在朋友圈

// 纯文字
MaxSocialShuoShuoContent *content = [MaxSocialShuoShuoContent contentWithText:@"text"];

// 纯链接
MaxSocialShuoShuoContent *content = [MaxSocialShuoShuoContent contentWithURL:[NSURL URLWithString:@"http://www.google.com"]];

// 文字 + 链接
MaxSocialShuoShuoContent *content = [MaxSocialShuoShuoContent contentWithText:@"test" url:[NSURL URLWithString:@"http://www.google.com"]];

// 文字 + 图片,目前图片还只支持传入 FileUrl 数组
MaxSocialShuoShuoContent *content = [MaxSocialShuoShuoContent contentWithText:@"text" imageURLs:imageFileUrls];

// 创建说说对象
MaxSocialShuoShuo *shuoshuo = [MaxSocialShuoShuo new];
shuoshuo.content = content;

// 填写地理位置
shuoshuo.location = [MaxSocialLocation locationWithLatitude:42.8 longitude:135.2];

// toSquare 控制是否将说说发布到广场。
// YES 表示发布到广场,说说将同时出现在广场和朋友圈;NO 表示只发布到朋友圈,说说将不会出现在广场上
BOOL toSquare = YES;

// 发表说说
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user postShuoShuo:shuoshuo toSquare:toSquare block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

根据说说的 objectId 获取说说详细数据:

NSString *shuoId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user fetchShuoShuoWithId:shuoId block:^(MaxSocialShuoShuo * _Nullable status, NSError * _Nullable error) {
    // ...
}];

删除说说:

NSString *shuoId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user deleteShuoShuoWithId:shuoId block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

获取说说的图片名字列表,图片名字即图片的 ID,可以用来下载图片:

NSString *shuoId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getImageNamesOfShuoShuo:shuoId block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    NSLog(@"image names: %@, error: %@", objects, error);
}];

下载说说中的图片:

MaxSocialShuoShuo *shuoshuo;
NSString *shuoId = shuoshuo.objectId;
NSString *imgName = shuoshuo.content.imageNames.firstObject;
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user downloadImageWithName:imgName ofShuoShuo:shuoId progress:^(int percentDone) {
    NSLog(@"download progress: %d%%", percentDone);
} completion:^(NSString * _Nullable string, NSError * _Nullable error) {
    NSLog(@"downloaded image path: %@", string);
}];

删除说说的图片:

NSString *shuoId = @"id";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user deleteImagesOfShuoShuo:shuoId block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

获取用户自己的说说列表:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
MaxSocialQuery *query = [MaxSocialQuery new]; // default query
[user getShuoShuoWithQuery:query block:^(NSDictionary * _Nullable result, NSError * _Nullable error) {
    NSLog(@"self shuoshuo: %@, error: %@", result, error);
}];

获取广场上最新的说说:

MaxSocialQuery *query = [MaxSocialQuery new]; // default query
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getLatestShuoShuoInSquareWithQuery:query block:^(NSDictionary * _Nullable result, NSError * _Nullable error) {
    NSLog(@"latest shuoshuo on square: %@, error: %@", result, error);
}];

获取朋友圈最新的说说:

MaxSocialQuery *query = [MaxSocialQuery new]; // default query
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getLatestShuoShuoInFriendCycleWithQuery:query block:^(NSDictionary * _Nullable result, NSError * _Nullable error) {
    NSLog(@"latest shuoshuo on friend cycle: %@, error: %@", result, error);
    fulfill();
}];

获取附近的说说:

MaxSocialLocation *location = [MaxSocialLocation locationWithLatitude:22 longitude:34];
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getShuoShuoNearLocation:location distance:10086 block:^(NSDictionary * _Nullable result, NSError * _Nullable error) {
    NSLog(@"nearest shuoshuo: %@, error: %@", result, error);
}];

获取用户附近的说说列表:

MaxSocialLocation *location = [MaxSocialLocation locationWithLatitude:22 longitude:34];
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getShuoShuoNearLocation:location distance:10086 block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    NSLog(@"nearest status: %@, error: %@", objects, error);
}];

评论

评论分为两种:文字评论和点赞

对说说添加文字评论:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user createCommentForShuoShuo:@"shuoId" withContent:@"hello" block:^(MaxSocialComment * _Nullable comment, NSError * _Nullable error) {
    // ...
}];

对说说点赞:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user likeShuoShuo:@"shuoId" block:^(MaxSocialComment * _Nullable comment, NSError * _Nullable error) {
    // comment.isLike 应该为 YES.
    // 可以通过评论对象 comment 的 isLike 属性判断该评论是点赞还是文字评论
}];

删除评论:

NSString *commentId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user deleteCommentWithId:commentId block:^(BOOL succeeded, NSError * _Nullable error) {
    // ...
}];

根据评论的 objectId 获取评论内容:

NSString *commentId = @"";
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getCommentWithId:commentId block:^(MaxSocialComment * _Nullable comment, NSError * _Nullable error) {
    // ...
}];

查询某条说说的评论列表:

NSString *shuoId = @"";
MaxSocialQuery *query = [MaxSocialQuery new];
MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getCommentOfShuoshuo:shuoId withQuery:query block:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    // ...
}];

列出未读评论:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user getUnreadCommentWithBlock:^(NSArray * _Nullable objects, NSError * _Nullable error) {
    NSLog(@"unread comments: %@, error: %@", objects, error);
}];

把评论标记为已读:

MaxSocialUser *user = [MaxSocialUser userWithId:@"userId"];
[user markCommentAsRead:@"commetId" completion:^(BOOL updated, NSError * _Nullable error) {
    // ...
}];

社交分享

简介

目前支持分享到新浪微博、微信好友、微信朋友圈、QQ好用、QQ空间。还支持扩展自定义的分享按钮。

使用

集成

  1. 使用 cocoapods 安装

    Podfile 中加上下面这行:

    pod 'MaxShare'
    

    打开应用 终端,执行以下命令:

    $ cd your_project_dir
    $ pod install
    
  2. 手动安装

    1. 下载最新版本的 MaxSocialShare SDK
    2. 解压后得到 MaxSocialShare.embeddedframeworkPlatformSDK 文件夹,将它们拖到到项目中。其中 PlatformSDK 文件夹中是各平台的官方 SDK,是可选的。
    3. 添加依赖库

      CoreTelephony
      SystemConfiguration
      libsqlite3
      libc++
      libz

      ImageIO (Weibo SDK 依赖)

    4. 在 Target -> Build Settings -> Other Link Flags 中加上 -ObjC

认证说明

MaxLeap 中用一个用户系统,可以使用第三方登录,认证信息保存在 MLUser 中。

MaxSocialShare 不依赖于 MaxLeap, 不会自动使用 MLUser 中的第三方平台认证信息,也不会保存分享过程中的认证信息。

初始化分享环境

初始化新浪微博分享环境:

首先需要前往微博开放平台创建微博应用

重要:微博应用 >> 应用信息 >> 高级信息 中填写自己应用的 bundleID

重要:微博应用 >> 应用信息 >> 高级信息 中仔细填写授权回调页和取消授权回调页地址。这个地址在集成微博 SDK 的时候需要用到。

如果集成了 MaxLeap 微博登录模块 MLWeiboUtils.framework,启动代码中应该包含了下面这行:

[MLWeiboUtils initializeWeiboWithAppKey:@"your_weibo_appKey" redirectURI:@"https://api.weibo.com/oauth2/default.html"];

调用这行代码以后,新浪微博分享环境已经初始化。

如果没有集成微博登录模块,则需要下面这行代码来初始化:

[WeiboSDK registerApp:@"your_weibo_appKey"];
// MaxSocialShare 认证时会使用默认的 `redirectUrl`: https://api.weibo.com/oauth2/default.html

初始化腾讯 QQ 分享环境:

首先需要前往腾讯开放平台创建 QQ 应用

如果集成了 MaxLeap 微博登录模块 MLQQUtils.framework,启动代码中应该包含了下面这行:

[MLQQUtils initializeQQWithAppId:@"222222" qqDelegate:self];

调用这行代码以后,QQ 分享环境已经初始化,无需另外配置。

如果没有集成微博登录模块,则需要下面这行代码来初始化:

TencentOAuth *oauth = [[TencentOAuth alloc] initWithAppId:@"your_tecent_appId" andDelegate:delegate];
// 这个 oauth 对象会作用于全局,需要一直存在

注意事项:

  • 1. 必须在真机上运行并且安装了 QQ 的情况下,才会出现 QQ 分享按钮

初始化微信分享环境:

首先需要前往微信开放平台,创建微信移动应用。

如果集成了 MaxLeap 微博登录模块 MLWeChatUtils.framework,启动代码中应该包含了下面这行代码:

[MLWeChatUtils initializeWeChatWithAppId:@"wx_appId" appSecret:@"wx_app_secret" wxDelegate:self];

调用这行代码以后,微信分享环境已经初始化,无需另外配置。

如果没有集成微信登录模块,则需要下面这行代码来初始化:

[WXApi registerApp:@"your_wx_appId"];

注意事项:

  • 1. 必须在真机上运行且安装微信客户端的情况下,才会出现微信分享按钮
  • 2. 初始化时使用的微信 appId 必须是在微信后台申请的,否则会报 由于bad_param,无法分享到微信 错误

iOS 9 适配

允许 http 请求

默认配置下,iOS 9 以上版本系统会拦截 http 协议的请求,并打印如下一行文字:

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

~~问题是,部分社交平台接口都使用 http 协议。而且,应用中也可能需要访问 http 协议的接口。~~

~~有一个简单粗暴的解决办法就是,允许所有的 http 请求:~~

~~1. 在项目的 info.plist 文件中添加一个字段:NSAppTransportSecurity,值为字典类型~~ ~~2. 然后再在它下面添加一个字段:NSAllowsArbitraryLoads,值为 YES~~

WWDC 2016 中,苹果表示将继续收紧对 HTTP 接口的访问限制。从 2017 年 1 月 1 日起,所有新提交的应用不允许使用 NSAllowsArbitraryLoads 绕过 ATS 限制。最好保证应用中所有请求都是使用 HTTPS 协议,否则可能会在应用审核时遇到麻烦。MaxLeap iOS SDK 所有接口默认都使用 HTTPS 协议。

添加 Scheme 白名单

许多社交平台分享都需要跳转到它们的应用中进行,iOS 9 对 canOpenURL: 接口做了限制,导致许多社交平台的 SDK 无法正常跳转到对应的应用中进行分享。

解决办法如下:

  1. 在项目的 info.plist 中添加一个字段:LSApplicationQueriesSchemes,值类型为 Array
  2. 然后在这个数组中添加需要支持的 scheme,各平台的 scheme 列表如下:

    平台scheme
    新浪微博sinaweibo,
    sinaweibohd,
    sinaweibosso,
    sinaweibohdsso,
    weibosdk,
    weibosdk2.5
    微信wechat,
    weixin
    QQmqqOpensdkSSoLogin,
    mqqopensdkapiV2,
    mqqopensdkapiV3,
    wtloginmqq2,
    mqq,
    mqqapi
    QQ空间mqzoneopensdk,
    mqzoneopensdkapi,
    mqzoneopensdkapi19,
    mqzoneopensdkapiV2,
    mqqOpensdkSSoLogin,
    mqqopensdkapiV2,
    mqqopensdkapiV3,
    wtloginmqq2,
    mqqapi,
    mqqwpa,
    mqzone,
    mqq
    注:若同时使用QQ和QQ空间,则只添加本格中的即可

如果没有把平台的 scheme 添加到白名单中,系统会打印如下文字信息:

-canOpenURL: failed for URL: “sinaweibohdsso://xxx” – error: “This app is not allowed to query for scheme sinaweibohdsso”

按照上述方法,把 sinaweibohdsso 加入白名单即可。

分享内容

注意: 腾讯 QQ 分享和微信分享需要安装相应的客户端,否则分享按钮不会出现。

分享需要创建一个 MLShareItem 对象,然后设置类型和相关数据。再按下分享按钮时,不受支持的数据会被忽略。

分享接口说明

+ (void)shareItem:(MLShareItem *)item
    withContainer:(nullable MaxSocialContainer *)container
       completion:(MLSActivityViewControllerCompletionBlock)block;

该接口有三个参数:

  • item: 要分享的内容
  • container: iPad 会使用这个参数让分享界面以 popover 的形式弹出来
  • block: 用户按下分享按钮后,调用对应平台分享接口的情况,并不是分享结果

使用方法:

// 创建一个 MLShareItem 对象
MLShareItem *item = [MLShareItem itemWithMediaType:MLSContentMediaTypeText];
item.title = @"title";
item.detail = @"detail";

// 创建一个 MaxSocialContainer 对象
// 如果不需要支持 iPad,可以为空
MaxSocialContainer *container = [MaxSocialContainer containerWithBarButtonItem:sender];
[MaxSocialShare shareItem:item withContainer:container completion:^(MLSActivityType activityType, BOOL completed, NSError * _Nullable activityError) {
    if (completed) {
        // 调用分享接口成功
    } else {
        // ...
    }
}];

分享示例

注意: 微信 SDK 限制缩略图大小为不超过 32k,如果点击微信按钮后得到分享失败的错误提示,请检查相关数据的规格是否合乎微信 SDK 的要求。更多信息请查阅微信开发者须知

  • 分享文本

    MLShareItem *textItem = [MLShareItem itemWithMediaType:MLSContentMediaTypeText];
    // textItem.title = @"文字标题"; // optional, 目前 QQ, 微信,微博 都不支持
    textItem.detail = @"文字内容"; // required
    [MaxSocialShare shareItem:textItem completion:^(MLSActivityType activityType, BOOL completed, NSError * _Nullable activityError) {
        NSLog(@"share activity (%d) completed: %d", activityType, completed);
    }];
    
  • 分享图片

    MLShareItem *imageItem = [MLShareItem itemWithMediaType:MLSContentMediaTypeImage];
    imageItem.attachmentURL = imageUrl; // required,支持 fileURL 和 远程图片链接
    
    imageItem.title = @"图片标题"; // optional, 只有QQ支持
    imageItem.detail = @"图片描述"; // optional, 只有QQ支持
    imageItem.previewImageData = preview; // optional, 只有QQ支持
    
    [MaxSocialShare shareItem:imageItem completion:^(MLSActivityType activityType, BOOL completed, NSError * _Nullable activityError) {
        NSLog(@"share activity (%d) completed: %d", activityType, completed);
    }];
    
  • 分享网页

    MLShareItem *webpageItem = [MLShareItem itemWithMediaType:MLSContentMediaTypeWebpage];
    
    // 腾讯,微博,微信都支持以下字段
    webpageItem.title = @"网页标题";
    webpageItem.detail = @"网页描述";
    webpageItem.webpageURL = [NSURL URLWithString:@"网页地址"];
    webpageItem.previewImageData = previewImageData; // 预览图, 微信限制不大于 32k
    
    [MaxSocialShare shareItem:webpageItem completion:^(MLSActivityType activityType, BOOL completed, NSError * _Nullable activityError) {
        NSLog(@"share activity (%d) completed: %d", activityType, completed);
    }];
    
  • 分享音乐

    MLShareItem *musicItem = [MLShareItem itemWithMediaType:MLSContentMediaTypeMusic];
    
    musicItem.title = @"音乐标题";
    musicItem.detail = @"音乐描述";
    musicItem.previewImageData = previewImageData; // 预览图, 微信限制不大于 32k
    musicItem.attachmentURL = [NSURL URLWithString:@"音乐数据流地址"];
    
    musicItem.webpageURL = [NSURL URLWithString:@"音乐网页地址"]; // 微博,微信支持,QQ不支持
    
    [MaxSocialShare shareItem:musicItem completion:^(MLSActivityType activityType, BOOL completed, NSError * _Nullable activityError) {
        NSLog(@"share activity (%d) completed: %d", activityType, completed);
    }];
    
  • 分享视频

    MLShareItem *videoItem = [MLShareItem itemWithMediaType:MLSContentMediaTypeVideo];
    
    // 以下字段三个平台都支持
    videoItem.title = @"视频标题";
    videoItem.detail = @"视频描述";
    videoItem.previewImageData = previewImageData; // 预览图像,微信限制不大于 32k
    
    // 微信,微博支持,QQ 不支持
    videoItem.webpageURL = [NSURL URLWithString:@"视频网页地址"];
    // QQ, 微博支持,微信不支持
    videoItem.attachmentURL = [NSURL URLWithString:@"视频数据流地址"];
    
    [MaxSocialShare shareItem:videoItem completion:^(MLSActivityType activityType, BOOL completed, NSError * _Nullable activityError) {
        NSLog(@"share activity (%d) completed: %d", activityType, completed);
    }];
    

扩展

MaxSocialShare 支持类似 UIActivityViewController 的扩展方法,可以添加框架现在不支持的平台分享按钮。

扩展通过继承 MLSActivity 来完成。下面我们写一个自定义的分享按钮:

CustomActivity.h 文件:

#import <MaxSocialShare/MaxSocialShare.h>

@interface CustomActivity : MLSActivity

@end

CustomActivity.m 文件:

#import "CustomActivity.h"

// 定义 custom type,注意值不要和已有的重复了
MLSActivityType MLSActivityTypeCustom = 6;

@implementation CustomActivity

+ (MLSActivityType)type {
    // 返回 CustomActivity 的类型,如果之前已经注册了跟 MLSActivityTypeCustom 类型值相同的 activity,则会覆盖它
    return MLSActivityTypeCustom;
}

+ (BOOL)canPerformWithActivityItem:(MLShareItem *)activityItem {
    // 检查 activityItem 来判断是否能处理它
    return YES;
}

- (nullable NSString *)title {
    // 分享按钮的标题
    return @"Custom";
}

- (nullable UIImage *)image {
    // 分享按钮的图片
    return [UIImage imageNamed:@"custom_share_btn_icon"];
}

- (void)prepareWithActivityItem:(MLShareItem *)activityItem {
    // 在这个方法里面对 activityItem 做一些预处理操作
}

- (void)perform {
    // 做具体的分享操作

    // 重要,操作完成后一定要调一下这个方法,传入错误表示分享操作失败,传入 nil 表示分享成功
    [self activityDidFinishWithError:nil];
}

@end

重要:最后一步操作:注册 CustomActivity

[MLSActivity registerActivityClass:[CustomActivity class]];

推荐把这行代码写到 CustomActivity 类的 +load 方法中,这样,当 CustomActivity 类加载时,就会自动注册。

@implementation CustomActivity

+ (void)load {
    [MLSActivity registerActivityClass:[CustomActivity class]];
}

@end

定制

此项目已开源,如果 SDK 无法满足你的需求,可以在这里下载项目源码,进行个性化定制。

数据分析

简介

什么是 MaxLeap 数据分析服务

MaxLeap 数据分析服务通过客户端及 Cloud Data,收集应用及用户的各种数据,并在 MaxLeap 中进行专业分析,最终生成面向运营者的报表。

启用服务

统计分析功能集成在 MaxLeap.framework 中,如果还没有继承这个框架,请先查阅SDK 集成小节,安装 SDK 并使之在 Xcode 中运行。

安装SDK完成后,MaxLeap 服务将自动帮助你追踪应用内的一些数据。自动收集的数据包括:

  1. 终端信息
  2. 应用启动和退出
  3. 应用崩溃等异常信息

MaxLeap 数据分析服务的默认状态为开启

页面访问路径统计

可以统计每个 View 停留时长,请确保配对使用,而且这些 view 之间不要有嵌套关系:

- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    [MLAnalytics beginLogPageView:@"PageOne"];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [MLAnalytics endLogPageView:@"PageOne"];
}

自定义事件

自定义事件可以实现在应用程序中埋点,以纪录用户的点击行为并且采集相应数据。

字段说明

字段名类型描述
eventIdString事件名
keyString事件参数
valueString事件参数的值

请注意, 自定义事件名 (event_id) 请尽量保持其为静态值, 否则可能出现数目庞大的自定义事件列表, 而无法达到了解与分析用户行为的目的.

统计某事件发生次数

[MLAnalytics trackEvent:@"event_id"];

统计事件各属性被触发次数

示例:统计电商应用中“购买”事件发生的次数,以及购买的商品类型及数量,那么在购买的函数里调用:

NSDictionary *dict = @{@"type" : @"book", @"quantity" : @"3"};
[MLAnalytics trackEvent:@"purchase" parameters:dict];

应用内支付统计

  • 统计开始支付事件
// transaction 不能为空
[MLAnalytics onPurchaseRequest:transaction isSubscription:NO];
  • 统计支付成功事件
// transaction 不能为空
[MLAnalytics onPurchaseSuccess:transaction isSubscription:NO];
  • 统计支付失败事件
// transaction 不能为空
[MLAnalytics onPurchaseFailed:transaction isSubscription:NO];
  • 统计支付支付取消事件
// transaction 不能为空
[MLAnalytics onPurchaseCancelled:transaction isSubscription:NO];

推送营销

简介

什么是 MaxLeap 推送营销服务

推送营销服务是 MaxLeap 提供的营销和信息发布功能。目前提供两种消息模式:推送消息 和 应用内消息。你可以通过推送消息方式向指定人群推送消息,也可以通过应用内消息,在应用内向有某种行为的用户显示特定内容。你还可以在消息中设置用户点击后的目标 Activity。消息的创建,设置和发送均在Console中完成。

准备

推送营销功能集成在 MaxLeap.framework 中,如果你尚未安装,请先查阅SDK 集成小节,安装 SDK 并使之在 Xcode 中运行。 你还可以查看我们的 API 资料,了解有关我们 SDK 的更多详细信息。

注意:我们支持 iOS 7.0 及以上版本。

应用内消息

默认情况下,推送营销功能处于关闭状态,不会接收消息。启用这个功能很简单,只需要 [MLMarketingManager enable] 一行代码,如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [MaxLeap setApplicationId:@"your_application_id" clientKey:@"yout_client_key"];
    [MLMarketingManager enable];
}

此时,应用就会接收应用内消息,但是还不能接收和统计远程推送。

推送消息

推送消息帮助你迅速地将消息展示给大量的用户。发送推送消息后,无论用户是否打开应用,都将在状态栏看见它。你可以在Console中自定义发送消息的内容,并且传递若干参数(键值对)至客户端。用户点击推送消息后,应用会根据参数决定目标界面。

配置

首先要申请并上传远程推送证书,详细步骤请参照:iOS 推送证书设置指南

appDelegate.m 中,你可以使用下面的代码开启远程推送

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    [MaxLeap setApplicationId:@"5552f51660b2056aa87dd9e0" clientKey:@"c3JscE50TWNnVzg4SkZlUnFsc3E2QQ" site:MLSiteCN];

    [self registerRemoteNotifications];

    // 开启推送营销功能,默认关闭
    [MLMarketingManager enable];

    NSDictionary *aps = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
    if (aps) {
        NSLog(@"app was opened by remote notification: %@", aps);

        // 统计推送点击事件
        // 注意防止重复统计
        if (NO == [self respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]) {
            [MLMarketingManager handlePushNotificationOpened:launchOptions];
        }
    }

    return YES;
}

- (void)registerRemoteNotifications {
    if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        UIUserNotificationSettings *pushsettings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge|UIUserNotificationTypeSound|UIUserNotificationTypeAlert categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:pushsettings];
    } else {
//#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:UIRemoteNotificationTypeBadge|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeAlert];
//#endif
    }
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    // 将 device token 保存到 MaxLeap 服务器,以便服务器向本设备发送远程推送
    // 请解除注释将下面几行代码
//#if DEBUG
    [[MLInstallation currentInstallation] setDeviceTokenFromData:deviceToken forSandbox:YES];
//#else
    [[MLInstallation currentInstallation] setDeviceTokenFromData:deviceToken forSandbox:NO];
//#endif
    [[MLInstallation currentInstallation] saveInBackgroundWithBlock:nil];
}

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    [application registerForRemoteNotifications];
}


// 下面两个代理方法实现其中一个即可
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    // 统计推送点击事件
    [MLMarketingManager handlePushNotificationOpened:userInfo];
}

// 实现这个代理方法以后,不需要在 didFinishLaunchingWithOptions 中统计推送点击事件
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

    // 统计推送点击事件
    [MLMarketingManager handlePushNotificationOpened:userInfo];
    completionHandler(UIBackgroundFetchResultNoData);
}

统计推送点击率

收到远程推送时使用以下代码推送推送点击事件:

objc [MLMarketingManager handlePushNotificationOpened:notificationPayload];

设置 Badge

badge 是 iOS 用来标记应用程序未读消息(通知)的一个数字,出现在应用图标右上角。MaxLeap 支持保存 badge 值到服务器,然后由后台来管理每个用户推送的 badge 值。

SDK 初始化时,会自动将应用实际 badge 值([UIApplication sharedApplication].applicationIconBadgeNumber)上传到 MaxLeap 服务器。

在后台发送推送消息时,后台会自动把每个 installation.badge 加 1.

上传 badge 值

[MLInstallation currentInstallation].badge = 5;
[[MLInstallation currentInstallation] saveInBackgroundWithBlock:^(BOOL succeeded, NSError * _Nullable error) {
    if (succeeded) {
        // 上传成功
    } else {
        // 上传失败
    }
}];

注意:设置 badge 值操作并不会更改应用图标实际 badge 值,本地仍然需要调用 -[UIApplication setApplicationIconBadgeNumber:] 方法。

推送证书设置指南

  1. 生成推送证书,参照苹果官方文档《App Distribution Guide》的 Creating a Universal Push Notification Client SSL Certificate 小节。

  2. 安装证书

    下载并双击证书,点击弹出框右下角的添加按钮,把证书导入到钥匙串中。

    钥匙串中选择左边上半部分的 登陆 和下半部分的 我的证书,这时应该能在右边找到刚刚导入的证书。

  3. 导出 .p12 文件

    注意不要展开 private key

  4. 将文件保存为 Personal Information Exchange (.p12) 格式。

  5. 上传证书

    MaxLeap 管理平台:应用设置 - 推送通知 上,选择对应的应用程序,上传之前获得的 .p12 文件。这是集成 MaxLeap 推送的必要步骤。

用户支持

简介

用户支持服务是 MaxLeap 为开发者提供的一套标准应用客服方案。在客户端,此方案提供完整的 FAQ 的显示页面及用户反馈对话页面。在Console端,用户支持服务提供FAQ 的管理及用户反馈的处理界面。

注:MLHelpCenter 已经被拆分为 MaxFAQMaxIssues 两个项目,并且开源。

准备工作

  1. 安装并配置 MaxLeap Core SDK. 详细步骤,请查看SDK 集成小节
  2. 安装并配置用户支持 SDK. 详细步骤,请查看QuickStart - HelpCenter

展示 FAQ 页面

  1. 调用下面的方法展示 FAQ 界面

    [[MLHelpCenter sharedInstance] showFAQs:self]; // self 为弹出 App Issues 界面的 ViewController
    
  2. 调出问题反馈界面

    在 FAQ 界面的右上角会有一个按钮 Contact Us,点击后会进入问题界面。

    你也可以在任何有需要的地方调用下面的代码直接进入问题反馈界面:

    [[MLHelpCenter sharedInstance] showConversation:self]; // self 为弹出 App Issues 界面的 ViewController
    

自定义 UI

简介

你可以改变 SDK UI 的字体、颜色和背景图片。这些可以通过编辑一个 .plist 文件做到。

开始自定义

在 MLHelpCenter.emmbeddedframework 中有三个 bundle:

MLHelpCenterThemes.bundle字体、颜色、图片等设置
MLHelpCenterImages.bundle图片资源
MLHelpCenterLocalizable.bundleSDK 界面使用的 strings 文件,用于本地化

SDK 的 UI 自定义是通过编辑 MLHelpCenterThemes.bundle/HelpCenterTheme.plist 文件来完成的.
请确保项目中加入了 MLHelpCenterThemes.bundle 并且存在 HelpCenterTheme.plist 这个文件。

颜色颜色以十六进制的格式指定,例如:FF0000 是红色
图片图片需要在 MLHelpCenterImages.bundle 里面,你可以通过图片的文件名来指定。
MLHelpCenter 支持 iOS image naming convention(例如:在 retina 屏幕上使用 @2x 的图片,如果能找到的话)。
字体你可以通过查阅 iosfonts.com 来获取不同版本 iOS 上可用的字体。

这些属性会应用到所有界面的导航栏上。

Title font nameNavigationBar Title 的字体
Title font sizeNavigation bar 的 title 字体大小。它是一个无单位的数字,例如:18。
Title colorNavigation bar 的 title 颜色。
Background colorNavigation bar 背景颜色
Bar button font nameNavigation bar 上按钮的字体。
Bar button font sizeNavigation bar 上按钮的字体大小。它是一个无单位的数字。
Bar button text colorNavigation bar 上按钮的文字颜色。
Contact us button imageContact Us 按钮的图标。默认情况下,这个按钮只显示文字 “Contact Us” 。
Contact us button image highlightedContact Us 按钮高亮时的图标。

还有一些只能在 iOS 6 上生效的属性:

Title shadow color (iOS 6)Navigation bar 的 title 阴影颜色,只在 iOS 6 上生效。
Title shadow offset (iOS 6)Navigation bar 的 title 阴影偏移量,只在 iOS 6 上生效。
Background image (iOS 6)Navigation bar 背景图片,只在 iOS 6 上生效。
Background image landscape (iOS 6)用于横屏时 navigation bar 的背景图片,只在 iOS 6 上生效。
Bar shadow image name (iOS 6)Navigation bar 阴影图片,只在 iOS 6 上生效。

navigationbar

Title Images

除了上面的一些应用于全局的 navigation bar 的一些属性,你还可以为每个界面设置不同的 title image 来替换以文字方式显示的标题。能够设置 title image 的界面有以下五个:

  1. FAQ section list view: FAQ section 列表界面
  2. FAQ item list view: FAQ item 列表界面,就是进入一个 section 之后的界面
  3. FAQ item content view: 显示一条 FAQ 内容的界面
  4. New conversation view: 新建会话界面
  5. Conversation view: 会话界面

在 .plist 文件中相应条目下都有 Title image 这个字段,对其指定一个图片名字就可以将相应的界面的标题文字换成图片。

titleimage

用户反馈界面

可以设置会话界面的消息框背景和消息字体、颜色等。

Title image会话界面的标题图片
Message text font name消息文本字体
Message text font size消息文本字体大小
Message text color left左侧消息文本颜色
Message text color right右侧消息文本颜色
Date text font name表示消息日期的文字字体
Date text font size表示消息日期的文字文字字体大小
Date text color表示消息日期的文字颜色

chatview

使用自定义图片包

Image bundle name: 自定义的图片包的相对于 main bundle 的相对路径,例如: MLHelpCenterImages.bundle。

部分图片名字对照图:

imgnames1 imgnames2

使用自定义字体

  1. 把字体文件添加到项目中。在应用 info.plist 文件中添加一个键 “Fonts provided by application"。在这个键下面,列出你想要在应用中使用的所有的字体的名字。 registercustomfont

  2. 复制字体全名。字体全名可以在字体文件简介中获取。选中字体文件,按下 COMMAND ⌘ + I(查看简介) 可以打开字体简介面板。 fontsinfoview

  3. 把字体名字粘贴到 MLHelpCenterThemes.bundle/HelpCenterTheme.plist 需要的地方。 setfonts

  4. 在模拟器中测试字体。错误的字体名字将被 SDK 忽略。 theresult