Commercetools Swift SDK

Swift 5.1 Platforms iOS | macOS | watchOS | tvOS | Linux SPM compatible

安装

要求

CocoaPods

CocoaPods 是 Cocoa 项目的依赖管理工具。 您可以使用以下命令安装它

$ gem install cocoapods

构建 CommercetoolsSDK 0.7+ 需要 CocoaPods 1.2.1+。

要使用 CocoaPods 将 CommercetoolsSDK 集成到您的 Xcode 项目中,请在您的 Podfile 中指定它

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

pod 'Commercetools', '~> 0.12'

然后,运行以下命令

$ pod install

开始使用

Commercetools SDK 使用名为 CommercetoolsConfig.plist.plist 配置文件来收集与 commercetools 平台通信所需的所有信息。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>projectKey</key>
	<string>Your Project Key</string>
	<key>clientId</key>
	<string>Your Client ID</string>
	<key>clientSecret</key>
	<string>Your Client Secret</string>
	<key>scope</key>
	<string>Your Client Scope</string>
	<key>authUrl</key>
	<string>Authorization Url/</string>
	<key>apiUrl</key>
	<string>API Url</string>
	<key>machineLearningApiUrl</key>
    <string>Machine Learning API</string>
	<key>anonymousSession</key>
    <true/>
    <key>keychainAccessGroupName</key>
    <string>AB123CDEF.yourKeychainGroup</string>
    <key>shareWatchSession</key>
    <true/>
    <key>emergencyContactInfo</key>
    <string>you@yourdomain.com</string>
    <key>storeKey</key>
    <string>your-global-store-key</string>
</dict>
</plist> 

或者,您可以指定包含这些属性的不同 .plist 文件的路径。

您还可以从 Merchant Center API Clients 部分下载客户端的配置 .plist 文件,创建时从下拉菜单中选择 iOS 模板。

在使用 Commercetools SDK 中的任何方法之前,请确保您已预先设置所需的配置。

import Commercetools

// Default configuration initializer reads from CommercetoolsConfig.plist file from your app bundle
if let configuration = Config() {
    
    // You can also specify custom logging level
    // configuration.logLevel = .error
    
    // Or completely disable all log messages from Commercetools SDK
    // configuration.loggingEnabled = false
    
    // Finally, you need set your configuration before using the SDK
    Commercetools.config = configuration
    
} else {
    // There are some errors in your .plist file, check log messages for more information
}

已认证和匿名使用

Commercetools 服务的端点可以在没有结账 (PlainToken) 的情况下使用,使用访客结账 (AnonymousToken) 或使用已登录的客户 (CustomerToken)。 根据配置,与 Commercetools 平台的所有交互都将默认使用 PlainTokenAnonymousToken 执行。

如果在某个时候您希望登录用户,可以使用 loginUser 方法来实现

let username = "swift.sdk.test.user@commercetools.com"
let password = "password"

Commercetools.loginCustomer(username, password: password, completionHandler: { result in
    if let error = result.errors?.first as? CTError, case .accessTokenRetrievalFailed(let reason) = error {
        // Handle error, and possibly get some more information from reason.message
    }
})

类似地,注销后,所有进一步的交互将继续使用新的匿名用户令牌。

Commercetools.logoutCustomer()

访问令牌和刷新令牌会在应用程序启动时保留。 为了检查当前处理的是经过身份验证的用户还是匿名用户,应使用 authState 属性

if Commercetools.authState == .plainToken {
    // Present login form or other logic
}

为了使您的应用程序支持匿名会话,您应该在您的配置 .plist 文件中将 anonymousSession 布尔属性设置为 true。 此外,可以通过调用以下方法来覆盖此设置,并提供可选的自定义 anonymous_id(用于指标和跟踪目的)

Commercetools.obtainAnonymousToken(usingSession: true, anonymousId: "some-custom-id", completionHandler: { error in
    if error == nil {
        // It is possible for token retrieval to fail, e.g custom token ID has already been taken,
        // in which case reason.message from the returned CTError instance is set to the anonymousId is already in use.
    }
})

当匿名会话以注册或登录结束时,购物车和订单将迁移到客户,并返回 CustomerSignInResult,从而可以访问客户个人资料和当前活动的购物车。 对于登录操作,您可以通过明确指定两个 AnonymousCartSignInMode 值之一来定义如何从当前活动的购物车中迁移行项目: .mergeWithExistingCustomerCart.useAsNewActiveCustomerCart

店内客户

使用商店时,客户可以全局注册,也可以注册特定于商店。 在后一种情况下,要在商店中登录客户,需要设置 storeKey 参数

let username = "swift.sdk.test.in.store.user@commercetools.com"
let password = "password"

Commercetools.loginCustomer(username, password: password, storeKey: "store-key", completionHandler: { result in
    if let error = result.errors?.first as? CTError {
        // Handle error, and possibly get some more information from reason.message
    }
})

如果应用程序仅适用于特定商店,建议将全局商店密钥设置为配置 .plist 文件的一部分。 这样,SDK 将对所有后续请求使用 storeKey,用于特定于商店的端点。

外部 OAuth 令牌

Commercetools 平台和 SDK 提供了使用外部 OAuth 令牌的功能。 要设置来自您的应用程序的令牌,请使用 Commercetools.externalToken 属性。 设置后,此令牌将用于来自 SDK 的所有平台请求。 为了停止使用外部令牌,只需将此值设置为 nil

使用外部令牌时,手动处理过期和无效令牌场景非常重要。 完成处理程序将提供包含其他信息的 CTError,客户端应决定是需要刷新令牌,还是无法恢复(例如,删除的帐户)。

要获取有关设置和使用具有 commercetools 平台的外部 OAuth 的更多信息,请参阅此页面

在 App Extension 中使用 SDK

如果您的应用程序有扩展程序,并且您想在这些扩展程序中使用 Commercetools SDK,我们建议启用钥匙串共享。 通过允许钥匙串共享并在配置 .plist 中设置适当的访问组名称,SDK 会将所有令牌保存在共享钥匙串中。 确保在访问组名称中包含 *App ID Prefix / Team ID*。 因此,您可以在您的应用程序和任何扩展程序中使用具有相同授权状态和令牌的所有端点。 您的开发团队使用钥匙串共享的多个应用程序也是如此。

在 watchOS 上使用 SDK

由于 Apple Watch 上的钥匙串包含与配对 iPhone 上的钥匙串不同的条目集,因此无法通过在配置 .plist 中设置 keychainAccessGroupName 来在 iOS 和 watchOS 之间共享相同的客户会话。 相反,Commercetools SDK 使用 WatchConnectivity 将访问令牌从 iPhone 传输到 Apple Watch,这些访问令牌也安全地存储在 watchOS 钥匙串中。 您必须选择的唯一步骤是将 shareWatchSession 配置属性设置为 true

用户在 Apple Watch 上登录的常见方式是通过 iPhone 应用程序。 当从 iOS 应用程序收到访问令牌时,watchOS SDK 将发布通知,因此您可以检查新的 authState 并相应地执行 UI 更改。

NotificationCenter.default.addObserver(self, selector: #selector(checkAuthState), name: Notification.Name.WatchSynchronization.DidReceiveTokens, object: nil)

func checkAuthState() {
    if Commercetools.authState == .customerToken {
        // The customer is logged in, present the appropriate screen
    } else {
        // The customer is not logged in, present the login message if needed 
    }
}

使用 Commercetools 端点

对于任何可用的端点类,使用和管理通过可用端点提供的资源都非常容易。

根据资源的功能,您可以通过特定的 UUID 检索,使用更详细的查询选项,还可以执行创建或更新操作。

所有这些功能都由任何特定支持端点的静态方法提供。 例如,您可以使用提供的 Cart 类创建购物车

var cartDraft = CartDraft(currency: "EUR")

Cart.create(cartDraft, result: { result in
	if let cart = result.model, result.isSuccess {
		// Do any work with created `Cart` instance, i.e:
		if cart.cartState == .active {
			// Our cart is active!
		}
	}
})

如果您需要来自尚未在我们 SDK 中实现的端点的资源,您可以轻松创建表示该端点的类,并符合适当的协议,这些协议负责许多常见用例的抽象端点实现。

以下列表表示当前支持的抽象端点。 对于每个协议,都提供了一个默认扩展,它几乎总是可以满足您的需求

当前支持的端点

项目设置

为了获得当前 Commercetools 项目支持的国家/地区、语言和货币,您应该使用项目设置端点

Project.settings { result in
    if let settings = result.model {
        // use settings.currencies, settings.countries, settings.languages, etc
    }
}

购物车

购物车端点支持所有常见操作

Cart.active(result: { result in
    if let cart = result.model, result.isSuccess {
        // Cart successfully retrieved, response contains currently active cart
    } else {
        // Your user might not have an active cart at the moment
    }
})
Cart.query(limit: 2, offset: 1, result: { result in
    if let response = result.model, let count = response.count,
            let results = response.results, result.isSuccess {
        // response contains an array of cart instances
    }
})
var cartDraft = CartDraft()
cartDraft.currency = "EUR"

Cart.create(cartDraft, result: { result in
    if let cart = result.model, let cartState = cart.cartState, result.isSuccess {
        // Cart successfully created, and cart contains 
    }
})
let draft = LineItemDraft(productVariantSelection: .productVariant(productId: productId, variantId: variantId))

let updateActions = UpdateActions<CartUpdateAction>(version: version, actions: [.addLineItem(lineItemDraft: draft)])
Cart.update(cartId, actions: updateActions, result: { result in
    if let cart = result.model, result.isSuccess {
        // Cart successfully updated, response contains updated cart
    }
})
let version = 1 // Set the appropriate current version

Cart.delete(cartId, version: version, result: { result in
    if let cart = result.model, result.isSuccess {
        // Cart successfully deleted
    }
})
Cart.byId("cddddddd-ffff-4b44-b5b0-004e7d4bc2dd", result: { result in
    if let cart = result.model, cart.cartState == .active && result.isSuccess {
        // response contains cart dictionary
    }
})

还有一组用于店内操作的购物车方法。 参数与上述方法相同,但增加了一个指定商店密钥的参数 (storeKey: String)。

类别

使用常规移动范围,可以通过 UUID 检索和查询类别。

Category.query(limit: 10, offset: 1, result: { result in
    if let response = result.model, let count = response.count,
            let categories = response.results, result.isSuccess {
        // categories contains an array of category objects
    }
})
Category.byId("cddddddd-ffff-4b44-b5b0-004e7d4bc2dd", result: { result in
    if let category = result.model, result.isSuccess {
        // response contains category objects
    }
})

客户

客户端点为您提供了几种可以从您的 iOS 应用程序使用的操作

// Optionally set `storeKey` for customers registered in a specific store.
Customer.profile { result in
    if let profile = result.model, let firstName = profile.firstName,
            let lastName = profile.lastName, result.isSuccess {
        // E.g present user profile details
    }
}
var customerDraft = CustomerDraft()
customerDraft.email = "new.swift.sdk.test.user@commercetools.com"
customerDraft.password = "password"

// Optionally set `storeKey` to sign up the customer in that store.
Commercetools.signUpCustomer(customerDraft, result: { result in
    if let customer = result.model?.customer, let version = customer.version, result.isSuccess {
        // User has been successfully signed up.
        // Now, you'd probably want to present the login form, or simply
        // use AuthManager to login user automatically
    }
})
var options = SetFirstNameOptions()
options.firstName = "newName"

let updateActions = UpdateActions<CustomerUpdateAction>(version: version, actions: [.setFirstName(options: options)])
// Optionally set `storeKey` for customers registered in a specific store.
Customer.update(actions: updateActions, result: { result in
    if let customer = result.model, let version = customer.version, result.isSuccess {
    	// User profile successfully updated
    }
})
// Optionally set `storeKey` for customers registered in a specific store.
Customer.delete(version: version, result: { result in
    if let customer = result.model, result.isSuccess {
        // Customer was successfully deleted
    }
})
let  version = 1 // Set the appropriate current version

// Optionally set `storeKey` for customers registered in a specific store.
Customer.changePassword(currentPassword: "password", newPassword: "newPassword", version: version, result: { result in
    if let customer = result.model, result.isSuccess {
    	// Password has been changed, and now AuthManager has automatically obtained new access token
    }
})
let token = "" // Usually this token is retrieved from the password reset link, in case your app does support universal links

// Optionally set `storeKey` for customers registered in a specific store.
Customer.resetPassword(token: token, newPassword: "password", result: { result in
    if let customer = result.model, let email = customer.email, result.isSuccess {
        // Password has been successfully reset, now would be a good time to present the login screen
    }
})
let token = "" // Usually this token is retrieved from the activation link, in case your app does support universal links

// Optionally set `storeKey` for customers registered in a specific store.
Customer.verifyEmail(token: token, result: { result in
    if let customer = result.model, let email = customer.email, result.isSuccess {
        // Email has been successfully verified, probably show UIAlertController with this info
    }
})

订单

订单端点提供了从现有 Cart 创建订单的功能,还可以通过 UUID 检索订单,并执行订单查询。

运输方式

为了在结帐期间向客户展示运输选项,您需要使用运输方式端点

ShippingMethod.for(cart: cart) { result in
    if let shippingMethods = result.model, result.isSuccess {
        // present shipping methods to the customer
    }
}
ShippingMethod.for(country: "DE") { result in
    if let shippingMethods = result.model, result.isSuccess {
        // present shipping methods to the customer
    }
}
let predicate = "name=\"DHL\""

ShippingMethod.query(predicates: [predicate], result: { result in
    if let response = result.model, let count = response.count,
			let results = response.results, result.isSuccess {
        // results contains an array of shipping method objects
    }
})
ShippingMethod.byId("cddddddd-ffff-4b44-b5b0-004e7d4bc2dd", result: { result in
    if let shippingMethod = result.model, result.isSuccess {
        // response contains product projection object
    }
})

产品投影

您的 iOS 应用程序检索产品数据的最常见方式是通过使用产品投影端点。 当前支持以下操作

ProductProjection.search(sort: ["name.en asc"], limit: 10, lang: Locale(identifier: "en"), text: "Michael Kors", result: { result in
    if let response = result.model, let total = response.total,
    		let results = response.results, result.isSuccess {
        // results contains an array of product projection objects
    }
})
ProductProjection.suggest(lang: Locale(identifier: "en"), searchKeywords: "michael", result: { result in
    if let response = result.json, let keywords = response["searchKeywords.en"] as? [[String: AnyObject]],
    		let firstKeyword = keywords.first?["text"] as? String, result.isSuccess {
        // keywords contains an array of suggestions in dictionary representation
    }
})
let predicate = "slug(en=\"michael-kors-bag-30T3GTVT7L-lightbrown\")"

ProductProjection.query(predicates: [predicate], sort: ["name.en asc"], limit: 10, offset: 10, result: { result in
    if let response = result.model, let count = response.count,
			let results = response.results, result.isSuccess {
        // results contains an array of product projection objects
    }
})
ProductProjection.byId("cddddddd-ffff-4b44-b5b0-004e7d4bc2dd", result: { result in
    if let product = result.model, result.isSuccess {
        // response contains product projection object
    }
})

产品类型

使用常规移动范围,可以通过 UUID、key 查询产品类型。

ProductType.query(limit: 10, offset: 1, result: { result in
    if let response = result.model, let count = response.count,
            let productTypes = response.results, result.isSuccess {
        // productTypes contains an array of product type objects
    }
})
ProductType.byId("cddddddd-ffff-4b44-b5b0-004e7d4bc2dd", result: { result in
    if let productType = result.model, result.isSuccess {
        // response contains product type object
    }
})
ProductType.byKey("main", result: { result in
    if let productType = result.model, result.isSuccess {
        // response contains product type object
    }
})

商店

使用 view_stores 范围,可以通过 UUID、key 查询商店。

Store.query(limit: 10, offset: 1, result: { result in
    if let response = result.model, let count = response.count,
            let stores = response.results, result.isSuccess {
        // stores contains an array of store objects
    }
})
Store.byId("cddddddd-ffff-4b44-b5b0-004e7d4bc2dd", result: { result in
    if let store = result.model, result.isSuccess {
        // response contains store object
    }
})
Store.byKey("unionSquare", result: { result in
    if let store = result.model, result.isSuccess {
        // response contains store object
    }
})

使用机器学习端点

对于 Commercetools 提供的 机器学习 API,SDK 有一组单独的端点。

当前支持的机器学习端点

相似产品

可以使用一种方法启动相似产品的搜索请求,另一种方法检查进度并获取搜索结果。

let request = SimilarProductSearchRequest(limit: 10, similarityMeasures: SimilarityMeasures(name: 1))

SimilarProducts.initiateSearch(request: request) { result in
    if let taskToken = result.model {
        // use taskToken.taskId to monitor for progress
    }
}
SimilarProducts.status(for: taskToken.taskId) { result in
    if let taskStatus = result.model, taskStatus == .success {
        // task has been completed, use taskStatus.result to get the products from `PagedQueryResult`
    }
}

类别推荐

可以使用提供的查询方法搜索特定产品 ID 的最合适类别。

CategoryRecommendations.query(productId: product.id) { result in
    if let results = result.model?.results {
        // results contains an array of `ProjectCategoryRecommendation`s
    }
}

处理结果

为了检查使用 Commercetools 服务的任何操作是否已成功执行,您应该使用相关结果的 isSuccessisFailure 属性。 对于所有成功的操作,都有两个属性,可用于使用实际响应。 推荐用于所有已合并模型的端点的是 model。 此属性已在上面的所有示例中使用。 或者,如果您正在编写自定义端点,并且不希望添加模型属性和映射,则 json 属性将使您可以访问 [String: Any](从 Commercetools 平台接收的 JSON 的字典表示形式)。

对于所有失败的操作,应使用相关结果中的 errors 属性来呈现或处理特定问题。 CTError 实例是枚举,包含七种主要情况,其中每种情况都包含 FailureReason,以及一些额外的关联值,具体取决于特定情况。

测试设置

如果需要实现与 Commercetools 服务通信的自定义端点,建议对该端点进行测试。 我们的 XCTestCase 扩展提供了有关如何设置测试配置的良好示例。 对于某些测试,常规移动客户端范围就足够了(在大多数情况下是 view_products manage_my_profile manage_my_orders)。 如果您的测试需要使用更高级别的权限(范围)进行设置或配置,您也可以进行设置。 出于安全原因,SDK 测试从环境变量中使用此配置。

在测试类中设置辅助端点也非常容易。 您可以声明一个符合特定端点协议的私有类

private class Foobar: QueryEndpoint, ByIdEndpoint, CreateEndpoint, UpdateEndpoint, DeleteEndpoint {
    static let path = "foobar"
}