这是一篇介绍关于提升体验的文章,主要探讨如何使用 iCloud 账户进行安全、静默的验证,从而可以让用户跳过注册界面。文章最后附有 Swift 2 写的 Demo。简单意译了一下, 原文来自UX: iOS Onboarding without Signup Screens

对于初次用户,没有什么比注册界面一大堆需要填的表格更让他们下定决心放弃这个 App。在智能手机上打字是很痛苦的,特别是输入email和重复两次又长又臭的密码,却往往换来一大堆烦人的订阅邮件。

开发者在 App 用户使用习惯跟踪中发现,会有 50% 以上新用户会在来到注册界面选择立即离开这个应用。

APP 注册界面是“转化率杀手”!

对我来说,我对于在一个新 APP中填一堆表格也感到非常厌烦,如果遇到这样的 APP,我会立刻关闭并删掉 它。


The Solution: iCloud

幸运的是,在 iOS 中,一个完美的,内置的认证解决方案可以代替它——iCloud。

每个 iOS 用户都有 iCloud 帐号。

接下来我会讲解一些细节,不过首先还是看一下 iCloud 的相关背景知识吧。

过去几年 Apple 大力扩展了它的云功能,现在,iCloud 成为了这个生态系统服务的支柱。

最开始的时候,iCloud 是作为 Apple 自家 APP 的内部云存储系统使用的,后来,它向开发者开放了存储文件或键值对(在设备间同步设置的完美方案)的功能。然后到2014年,开发者甚至可以用 CloudKit 这个 Apple 的 iCloud 生态系统中的基础服务来进行文件存储了。


iCloud as Invisible Signup Screen

为什么这对用户体验很重要?

“魔法”在于 iCloud 认证机制对用户来说是静默的。

通过 iCloud, APP 不用再向用户要求任何 email 或密码,就能够识别唯一的使用者。

使用内置的静默的 iCloud 认证机制, APP 可以自动地为这个已登录 iCloud 的用户获得一个安全且全球唯一的标识,从而代替 email 和密码。

应用不应该向用户要 email 地址,永不。

更棒的是,这个解决方案更尊重用户隐私,因为 Apple 不会像 email、密码或者信用卡信息那样向 App 开发者暴露 iPhone 用户的隐私信息


A User’s iCloud ID Token

一个应用如果想要知道当前 iPhone, iPad 或 Mac OS X 用户的 iCloud ID,它首先需要在内部进行“询问”,这对用户是静默的,因为操作系统会告诉它。 如果用户已经登录了 iCloud ——通常都是—— app 就会收到一个代表这个 iCloud 用户的由33个字符组成的唯一码。

Hi, my name is “_cd2f38d1db30d2fe80df12c89f463a9e”

That iCloud ID token ...

  1. 是全球唯一的,每一个用户都有它自己的 token,如同 email 一样;
  2. 对同一个用户,在所有设备上都是同样的
  3. 不会泄露任何用户信息例如 email 之类
  4. 对不同应用是不一样的

慢着,最后一点意味着什么?

意味着 Apple 的 iCloud tokens 不仅仅根据用户生成,还与应用有关。 所以即使一个 APP 在网上公布了它所有的 token,另一个 APP 也不能使用这些 token,即使它们共享用户!


Summary

亲爱的 iOS 开发者们,产品经理们,创始人们,请不要再向用户要求 email 和密码了。请使用 iCloud ID token 代替吧,它更安全,而且不会打扰用户,对转化率更友好,更何况它易于使用。市场运营的同事们可能不喜欢它因为发不了推广邮件,但是现时来说,推送消息是个更好的工具来获取回流用户。

如果你计划推出 web 服务或者 Android 客户端,你可以在另一个设备向用户拿到 email,然后在后台很容易地与 token 关联到一起,让用户可以跨平台使用服务。

重要的事要说三次,请!不要!向用户要 email!至少在 iOS 上不要!


The Technical Solution in Swift 2

让我们开始干点实际的吧。

首先,请在 Xcode Project 中激活 CloudKit 。我们其实没用到 CloudKit 的存储功能,但是还是要将它激活,如下:

pic01

Activate CloudKit in Xcode

长话短说,我的解决办法是,用一个异步的函数来使用用户的 CKRecordID。CKRecordID 是一个可选的对象,包含着用户所有 CloudKit (=iCloud!) 数据。其中最重要的属性是 CKRecordID.recordName,就是iCloud ID token。另外,请记得先import CloudKit。

import CloudKit

/// async gets iCloud record ID object of logged-in iCloud user
func iCloudUserIDAsync(complete: (instance: CKRecordID?, error: NSError?) -> ()) {
    let container = CKContainer.defaultContainer()
    container.fetchUserRecordIDWithCompletionHandler() {
        recordID, error in
        if error != nil {
            print(error!.localizedDescription)
            complete(instance: nil, error: error)
        } else {
            print("fetched ID \(recordID?.recordName)")
            complete(instance: recordID, error: nil)
        }
    }
}

然后使用该函数去获得iCloud ID token:

import CloudKit

iCloudUserIDAsync() {
    recordID, error in
    if let userID = recordID?.recordName {
        print("received iCloudID \(userID)")
    } else {
        print("Fetched iCloudID was nil")
    }
}

当然,你应该检查一下有没有错误,然后将 token 缓存到 Keychain 来避免每次都去请求 iCloud ID。

如果你喜欢这篇文章,请分享或者留个评论吧。

谢谢!

iOS App Cocos2d-x 混编.


  1. 将cocos2d文件夹拖到UIKit项目文件夹中, 将libs.xcodeproj拖到Xcode项目里面
  2. Target Dependencies加入cocos2d的5个lib
  3. Link Binary With Libraries加入cocos2d的5个iOS lib
  4. 对照cocos2d Helloworld项目添加缺失的库,
  5. libs.xcodeproj的Architectures改成和项目一致(arm64这些)
  6. PROJECT的Always Search User Paths改为Yes, Header Search Path加入:
    $(inherited)
    $(SRCROOT)/cocos2d
    $(SRCROOT)/cocos2d/cocos
    $(SRCROOT)/cocos2d/cocos/base
    $(SRCROOT)/cocos2d/cocos/physics
    $(SRCROOT)/cocos2d/cocos/math
    $(SRCROOT)/cocos2d/cocos/2d
    $(SRCROOT)/cocos2d/cocos/ui
    $(SRCROOT)/cocos2d/cocos/network
    $(SRCROOT)/cocos2d/cocos/audio/include
    $(SRCROOT)/cocos2d/cocos/editor-support
    $(SRCROOT)/cocos2d/extensions
    $(SRCROOT)/cocos2d/external
    $(SRCROOT)/cocos2d/external/chipmunk/include/chipmunk
  1. TARGETS的Always Search User Paths改为Yes, Header Search Path加入以下两句:
    $(SRCROOT)/cocos2d/cocos/platform/ios
    $(SRCROOT)/cocos2d/cocos/platform/ios/Simulation
  1. 引用"cocos2d.h"的objc类后缀名都要改成.mm

注:

  1. 从Cocos2d Scene的页面返回时会崩溃的bug, 代码修改CCDirectorCaller:(v3.0, v3.2以后版本已修复)
    + (void)destroy
    {
        [s_sharedDirectorCaller destroy]; // modified
        [s_sharedDirectorCaller release];
        s_sharedDirectorCaller = nil;
    }

    - (void)destroy
    {// modified
        [displayLink invalidate];
        displayLink = nil;
        CCLOG("caller destroy");
    }
  1. CCEAGLView.mm在dealloc中removeObserver, 否则退出Cocos界面后所有弹出键盘都会崩溃:(v3.0, v3.2以后版本已修复)
    [[NSNotificationCenter defaultCenter] removeObserver:self];

Cocos2d-x中下载网络图片并缓存.


当前在游戏开发中,无论大型游戏还是休闲小游戏,都喜欢加入好友系统,那就涉及到获取好友头像的需求,那在Cocos2d-x中如果获取网络图片呢?

先封装一个下载图片的工具类,顺便做一下缓存吧,要不每次都下载多耗流量啊——

std::string GImageUtils::downloadImageWithUrl(const std::string &fullUrl, const ccHttpRequestCallback & callback)
{
    // - - - - - - - - - - 图片缓存 - - - - - - - - - -
    std::string filePath = __String::createWithFormat("%s", FileUtils::getInstance()->getWritablePath().c_str())->getCString();

    std::vector<std::string> splitStr = GStringUtils::split(fullUrl.c_str(), "http://");
    if (splitStr.size() > 1) {
        filePath += GStringUtils::replace_all_distinct(splitStr.at(1), "/", "-") ;

        FILE* fp = fopen(filePath.c_str(),"rb");
        if (fp)
        {
            CCLOG("has file! filePath: %s", filePath.c_str());
            return filePath;// 如果有缓存图片则直接返回路径
        }
    }

    // - - - - - - - - - - 下载图片 - - - - - - - - - -
    auto request = new HttpRequest();
    // required fields
    request->setRequestType(HttpRequest::Type::GET);
    request->setUrl(fullUrl.c_str());
    request->setResponseCallback(callback);

    // optional fields
    request->setTag("_Http_GetImage_");
    HttpClient::getInstance()->send(request);
    request->release();
    return kImageNoCacheTag;// 没有缓存返回Tag区分情况处理
}

std::string GImageUtils::onGettedImage(HttpClient *sender, HttpResponse *response)
{
    if (!response) {
        return kImageDownloadErrorTag;
    }

    if (0 != strlen(response->getHttpRequest()->getTag())) {
        CCLOG("HttpConnect: - %s Completed. -", response->getHttpRequest()->getTag());
    }

    long statusCode = response->getResponseCode();
    char statusString[64] = {};
    sprintf(statusString, "HttpConnect: HTTP Status Code: %ld, tag = %s.", statusCode, response->getHttpRequest()->getTag());
    CCLOG("HttpConnect: response code: %ld", statusCode);

    if (!response->isSucceed()) {
        CCLOG("HttpConnect: response failed!!!...");
        CCLOG("HttpConnect: error buffer: %s.", response->getErrorBuffer());
        return kImageDownloadErrorTag;
    }

    // dump data
    std::vector<char> *buffer = response->getResponseData();

    if (strcmp(response->getHttpRequest()->getTag(), "_Http_GetImage_") != 0) {
        return kImageDownloadErrorTag;
    }

    // - - - - - - - - - - 下载成功,生成图片 - - - - - - - - - -
    auto img = new Image();
    img->initWithImageData((unsigned char*)buffer->data(), buffer->size());

    // - - - - - - - - - - 缓存图片 - - - - - - - - - -
    std::string filePath = __String::createWithFormat("%s", FileUtils::getInstance()->getWritablePath().c_str())->getCString();

    // 保存到本地文件,把图片网址作为文件名
    std::string bufffff(buffer->begin(),buffer->end());
    std::vector<std::string> splitStr = GStringUtils::split(response->getHttpRequest()->getUrl(), "http://");
    if (splitStr.size() > 1) {
        filePath += GStringUtils::replace_all_distinct(splitStr.at(1), "/", "-") ;
    }

    CCLOG("filePath: %s", filePath.c_str());
    FILE *fp = fopen(filePath.c_str(), "wb+");
    fwrite(bufffff.c_str(), 1, buffer->size(), fp);
    fclose(fp);

    img->release();

    return filePath;// 返回图片路径
}