CallKit iOS 教程
原文:CallKit Tutorial for iOS
作者:József Vesza
译者:kmyhy
对 VoIP App 开发者来说,iOS 的支持并不友好。尤其是它的通知发送这一块,太糙了。你的 App 允许在后台,你唯一的选择就是使用常规的通知,这也太容易搞丢了。和内置的、丰富的电话 UI 一比,突然你的 App 是如此的不和谐。
幸好,苹果在 iOS 10 中推出了 CallKit,让这一切发生了改变!
在这个教程中,你将通过编写一个 App 领略到 CallKit 的风采:
- 通过系统服务监听来点和去电。
- 用电话通讯录识别或拦截来电。
注意:CallKit 无法在模拟器上运行。为了配合本教程,你必须使用一台装有 iOS 10.2 的 iPhone。
开始
从此处下载本教程的开始项目,然后解压缩。为了在设备上调试项目,你必须对代码进行签名。打开项目文件,在项目导航器中选择 Hotline。
你需要修改 bundle ID。选中项目,在 General 窗口,找到 Identity 一栏。将 bundle ID 修改为其它:
然后,找到 Signing 栏。从下拉框中选择你的开发团队(以我为例,是我自己的个人团队)。确保勾选上 Automatically manage signing。这允许 Xcode 自动创建 App 所用的 provisioning profile。
运行 App 进行测试。
目前 App 还没有什么内容,但你会在开始项目中发现几个源文件。它们大部分用于创建UI,处理用户交互,其中比较值得注意的是这两个类:
- Call 类代表一个电话通话。这个类暴露了一些属性,用于识别呼叫(比如它的 UUID 或者回调),以及生命周期回调,什么时候用户开始、接听或挂起。
- CallManager 维护了 App 中的呼出列表,拥有添加和移除方法。在本教程中,你会扩展这个类。
CallKit 是什么?
CallKit 是一个新框架,用于改善 VoIP 的体验,允许 App 和原生的 Phone UI 紧密集成,你的 App 将能够:
- 调用原生的呼入界面,无论锁屏/不锁屏状态。
- 从原生电话 App 的通讯录、个人收藏、最近通话中发起通话。
本节中,你将学习 CallKit 的构成。下图显示了几个重要对象:
在使用 CallKit 时,有两个主要的类:CXProvider和 CXCallController。分别介绍如下。
CXProvider
你的 App 使用 CXProvider 来将外部通知报告给系统。通常是外部事件,比如来电。
当有事件发生,CXProvider 会创建一个 call update 来通知系统。什么是 call update?call update 用于封装新的或者改变了的和通话有关的信息。它用 CXCallUpdate 类来描述,这个类暴露了这些属性:呼入者姓名、是否是音频通话还是视频通话。
当系统想通知 App 有收到一个事件时,它会以 CXAction 的形式通知。CXAction 是一个抽象类,表示电话的动作。针对不同 action,CallKit 会提供不同的 CXAction 实现。例如,呼出用 CXStartCallAction 来表示,CXAnswerCallAction 则用于接听呼入。Action 通过唯一的 UUID 来识别,它要么是 fail 要么是 fulfill。
App 通过 CXProviderDelegate 和 CXProvider 打交道,这个协议定义了 CXProvider 的生命周期事件方法,以及来电 Action。
CXCallController
App 使用 CXCallController 来让系统知道用户发起的请求,比如“呼叫”动作。CXProvider 和 CXCallController 的最大不同在于:CXProvider 的工作是通知系统,而 CXCallController 则代表用户向用户发起请求。
CXCallController 在发起请求时使用了事务。事务用 CXTransaction 来表示,它会包含一个或多个 CXAction 实例。CXCallCotroller 将事务发送给系统,如果一切正常,系统会响应对应的 action 给 CXProvider。
理论还不少,但怎样使用它们呢?
来电
下图显示了来电的高度抽象的模型:
- 当来电呼入时,App 会创建一个 CXCallUpdate 然后通过 CXProvider 发送给系统。
- 系统会发布一个 incoming call 给它的服务。
- 当用户接听起电话时,系统会发送一个 CXAnswerCallAction 给 CXProvider。
- App 可以通过实现对应的 CXProviderDelegate 协议方法来回应这个动画。
第一步是创建 CXProvider 的委托。
回到 Xcode,在项目导航器中,选中 App 文件夹,点击菜单 File\New…,然后选择 iOS\Source\Swift File。名字命名为 ProviderDelegate,然后点 Create。
在文件中添加代码:
import AVFoundation
import CallKit
class ProviderDelegate: NSObject {
// 1.
fileprivate let callManager: CallManager
fileprivate let provider: CXProvider
init(callManager: CallManager) {
self.callManager = callManager
// 2.
provider = CXProvider(configuration: type(of: self).providerConfiguration)
super.init()
// 3.
provider.setDelegate(self, queue: nil)
}
// 4.
static var providerConfiguration: CXProviderConfiguration {
let providerConfiguration = CXProviderConfiguration(localizedName: "Hotline")
providerConfiguration.supportsVideo = true
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.phoneNumber]
return providerConfiguration
}
}
这段代码解释如下:
- ProviderDelegate 需要和 CXProvider 和 CXCallController 打交道,因此保持两个对二者的引用。属性用 fileprivate 修饰,这样你就可以从同一个文件中的扩展中访问它们了。
- 用一个 CXProviderConfiguration 初始化 CXProvider,前者在后面会定义成一个静态属性。CXProviderConfiguration 用于定义通话的行为和能力。
- 为了能够响应来自于 CXProvider 的事件,你需要设置它的委托。这句代码会导致一个编译错误,因为 ProviderDelegate 还没有实现 CXProviderDelegate 协议。
- 在这个 App 中,CXProviderConfiguration 支持视频通话、电话号码处理,并将通话群组的数字限制为 1 个。更多的定制化,请参考 CallKit 文档。
在 providerConfiguration 下面,添加一个工具方法:
func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)?) {
// 1.
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
update.hasVideo = hasVideo
// 2.
provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error == nil {
// 3.
let call = Call(uuid: uuid, handle: handle)
self.callManager.add(call: call)
}
// 4.
completion?(error as? NSError)
}
}
这个工具方法允许 App 通过 CXProvider API 来报告一个来电。代码解释如下:
- 准备向系统报告一个 call update 事件,它包含了所有的来电相关的元数据。
- 调用 CXProvider 的reportIcomingCall(with:update:completion:)方法通知系统有来电。
- completion 回调会在系统处理来电时调用。如果没有任何错误,你就创建一个 Call 实例,将它添加到 CallManager 的通话列表。
- 调用 completion 块,如果它不为空的话。
这个方法被其它类所调用,为了模拟来电呼入。
接下来是实现协议方法。仍然在 ProviderDelegate.swift 文件中,声明一个新的扩展,实现 CXProviderDelegate:
extension ProviderDelegate: CXProviderDelegate {
func providerDidReset(_ provider: CXProvider) {
stopAudio()
for call in callManager.calls {
call.end()
}
callManager.removeAllCalls()
}
}
CXProviderDelegate 只实现一个 required 的方法,providerDidReset(_:)。当 CXProvider 被 reset 时,这个方法被调用,这样你的 App 就可以清空所有去电,会到干净的状态。在这个方法中,你会停止所有的呼出音频会话,然后抛弃所有激活的通话。
现在 ProviderDelegate 提供了一个方法去报告来电,让我们来用用它!
在项目导航器中选择 App 文件夹,打开 AppDelegate.swift。在类中添加一个新属性:
lazy var providerDelegate: ProviderDelegate = ProviderDelegate(callManager: self.callManager)
providerDelegate 已经整装待发!在 AppDelegate 中添加如下方法:
func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)?) {
providerDelegate.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion)
}
这个方法向其它类暴露 providerDelegate 的工具方法。
最后一块拼图是将它和 UI 连接到一起。展开 UI/View Controllers 文件夹,打开 CallsViewController.swift,这是 App 主界面的控制器。找到空的 unwindSegueForNewCall(_:)方法,替换为如下代码:
@IBAction private func unwindForNewCall(_ segue: UIStoryboardSegue) {
// 1.
let newCallController = segue.source as! NewCallViewController
guard let handle = newCallController.handle else { return }
let videoEnabled = newCallController.videoEnabled
// 2.
let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) {
AppDelegate.shared.displayIncomingCall(uuid: UUID(), handle: handle, hasVideo: videoEnabled) { _ in
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
}
}
这段代码的大意是:
- 从 NewCallViewController 中读取这次通话的属性,它是 unwind segue 的起始 view controller。
- 用户可以在 action 结束之前挂起 App,这样 App 会使用后台任务。
现在一切就绪,运行 App,进行如下操作:
- 点击右上角的 + 按钮;
- 输入任意数字,在 segmented 控件中选择 Incoming,然后点 Done。
- 锁屏。这步很重要,因为这是唯一能够访问完整的原生呼入界面的方法。
几秒钟后,你会看到原生的呼入通话 UI:
但是,一旦你要接听电话,你会看到 UI 会仍然停留在下面的状态:
这是因为你还没有实现和接听电话对应的方法。回到 Xcode,打开 ProviderDelegate.swift,在类扩展中添加:
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
// 1.
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// 2.
configureAudioSession()
// 3.
call.answer()
// 4.
action.fulfill()
}
// 5.
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
startAudio()
}
这段代码大意如下:
- 从 callManager 中获得一个引用,UUID 指定为要接听的动画的 UUID。
- 设置通话要用的 audio session 是 App 的责任。系统会以一个较高的优先级来激活这个 session。
- 通过调用 answer,你会表明这个通话现在激活。
- 在处理一个 CXAction 时,重要的一点是,要么你拒绝它(fail),要么满足它(fullfill)。如果处理过程中没有发生错误,你可以调用 fullfill() 表示成功。
- 当系统激活 CXProvider 的 audio session时,委托会被调用。这给你一个机会开始处理通话的音频。
运行 App,再次开始呼入一个通话。当你接听时,系统会成功地变成去电状态。
如果你解锁 iPhone,你会看到 iOS 和 App 都会显示出正确的呼出状态。
结束通话
接听通话会带来一个问题:没有办法结束通话。这个 App 将会支持两种结束通话的方式:从原生的通话界面,或者从 App 中进行结束。
下图显示这两种结束通话的情况:
注意第一步有所不同:当用户从通话界面结束通话(1a)时,系统会自动发送一个 CXEndCallAction 给 CXProvider。但是,如果你想用 Hotline App 来结束通话(1b),那么应该有你来将 CXAction 封装成 CXTransaction,然后请求系统。当系统处理完请求,它会发送 CXEndCallCation 给 CXProvider。
不管哪种方法,你的 App 必须实现相应的 CXProviderDelegate 方法。打开 ProviderDelegate.swift,在类的扩展中添加下列方法:
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
// 1.
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// 2.
stopAudio()
// 3.
call.end()
// 4.
action.fulfill()
// 5.
callManager.remove(call: call)
}
还不是太难!代码解释如下:
- 从 callManager 获得一个 call 对象。
- 当 call 即将结束时,停止这次通话的音频处理。
- 调用 end() 方法修改本次通话的状态,以允许其他类和新的状态交互。
- 将 action 标记为 fulfill。
- 当你不再需要这个通话时,可以让 callManager 回收它。
这只实现了从原生通话界面结束的情况。为了从 App 结束通话,你必须修改 CallManager。在项目导航器的 Call Management 文件夹下,打开 CallManager.swift。
CallManager 需要和 CXCallController 通信,因此需要一个它的引用。添加属性:
private let callController = CXCallController()
在类中添加下列方法:
Now add the following methods to the class:
func end(call: Call) {
// 1.
let endCallAction = CXEndCallAction(call: call.uuid)
// 2.
let transaction = CXTransaction(action: endCallAction)
requestTransaction(transaction)
}
// 3.
private func requestTransaction(_ transaction: CXTransaction) {
callController.request(transaction) { error in
if let error = error {
print("Error requesting transaction: \(error)")
} else {
print("Requested transaction successfully")
}
}
}
代码解释如下:
- 先创建一个 CXEndCallAction。将通话的 UUID 传递给构造函数,以便在后面可以识别通话。
- 然后将 action 封装成 CXTransaction,以便发送给系统。
- 最后,调用 callController 的 request(_:completion:) 。系统会请求 CXProvider 执行这个 CXTransaction,这会导致你刚刚实现的委托方法被调用。
最后是将代码和 UI 连接起来。打开 CallsViewController.swift,在 tableView(_:cellForRowAt:) 方法下面,添加代码:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let call = callManager.calls[indexPath.row]
callManager.end(call: call)
}
当用户在 cell 上使用轻扫-删除手势时,App 会请求 CallManager 结束对应的通话。
运行 App,执行下列操作:
- 点击 + 按钮。
- 输入数字,选择 Incoming,点击 Done。
- 几秒钟后,你会接到一个来电。当你接听时,你会看到这个通话会在列表中出现。
- 在 cell 上向左轻扫,点 End。
这时,通话结束。无论锁屏还是不锁屏,无论 App 是否在前台,这个 App 都会报告通话。
其它提供者动作
如果你看过 CXProviderDelegate 的文档,你会注意到 CXProvider 还会执行许多 CXAction,包括静音、群组或者设置呼叫等待(通话保持)。后面一个听起来不错,我们现在就来实现它。
当用户在 cell 上轻扫-删除时,App 会请求 CallManager 去结束对应的通话。
当用户想设置某个通话为“保持”状态,App 会发送一个 CXSetHeldCallAction 给提供者。你的任务就是实现相关的委托方法。打开 ProviderDelegate.swift,在类扩展中添加如下方法:
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// 1.
call.state = action.isOnHold ? .held : .active
// 2.
if call.state == .held {
stopAudio()
} else {
startAudio()
}
// 3.
action.fulfill()
}
代码非常简单:
- 获得 CXCall 对象之后,我们要根据 action 的 isOnHold 属性来设置它的 state。
- 根据状态的不同,分别进行启动或停止音频会话。
- 标记 action 为 fulfill。
因为这个动作是用户发起的,我们还需要修改 CallManager 类。打开 CallManager.swift,在 end(call:) 方法后添加方法:
func setHeld(call: Call, onHold: Bool) {
let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
let transaction = CXTransaction()
transaction.addAction(setHeldCallAction)
requestTransaction(transaction)
}
这段代码和 end(call:) 非常像。事实上,二者唯一的不同是,后者封装在 transaction 中的是一个 CXSetHeldCallAction 对象。这个 action 包含了通话的 UUID 以及保持状态。
然后将这个方法和 UI 连接起来。打开 CallsViewController.swift,找到 UITableViewDelegate 的扩展处。在这个扩展的 tableView(_:titleForDeleteConfirmationButtonForRowAt:) 方法后面添加下列方法。
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let call = callManager.calls[indexPath.row]
call.state = call.state == .held ? .active : .held
callManager?.setHeld(call: call, onHold: call.state == .held)
tableView.reloadData()
}
当用户在某行上点击,上述代码会改变对应通话的保持状态。
运行 App,开始新的呼入。如果你点击这个通话对应的行,你会注意到状态标签会从 Acitve 变成 On Hold。
处理呼出通话
最后还有一个用户发起的动作,需要我们实现,那就是呼出。打开 ProviderDelegate.swift ,在 CXProviderDelegate 类扩展中添加方法:
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let call = Call(uuid: action.callUUID, outgoing: true, handle: action.handle.value)
// 1.
configureAudioSession()
// 2.
call.connectedStateChanged = { [weak self, weak call] in
guard let strongSelf = self, let call = call else { return }
if call.connectedState == .pending {
strongSelf.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
} else if call.connectedState == .complete {
strongSelf.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
}
}
// 3.
call.start { [weak self, weak call] success in
guard let strongSelf = self, let call = call else { return }
if success {
action.fulfill()
strongSelf.callManager.add(call: call)
} else {
action.fail()
}
}
}
当有呼出请求时,provider 会调用这个方法:
- 当我们用 UUID 创建出 Call 对象之后,我们就应该去配置 App 的音频会话。和呼入通话一样,你的唯一任务就是配置。真正的处理在后面进行,也就是在 provider(_:didActivate) 委托方法被调用时。
- delegate 会监听通话的生命周期。它首先会会报告的就是呼出通话开始连接。当通话最终连上时,delegate 也会被通知。
- 调用 call.start() 方法会导致 call 的生命周期变化。如果连接成功,则标记 action 为 fullfill。
现在 ProviderDelegate 已经能够处理呼出了。接下来是让 App 进行一次呼出通话。
打开 CallManager.swift,添加如下方法:
func startCall(handle: String, videoEnabled: Bool) {
// 1
let handle = CXHandle(type: .phoneNumber, value: handle)
// 2
let startCallAction = CXStartCallAction(call: UUID(), handle: handle)
// 3
startCallAction.isVideo = videoEnabled
let transaction = CXTransaction(action: startCallAction)
requestTransaction(transaction)
}
这个方法将一个 CXStartCallAction 放到 CXTransaction 中,然后向系统发起请求。
一个 CXHandle 对象表示了一次操作,同时指定了操作的类型和值。Hotline App 支持对电话号码进行操作,因此我们在操作中指定了电话号码。
一个 CXStartCallAction 用一个 UUID 和一个操作作为输入。
你可以通过 action 的 isVideo 属性指定通话是音频还是视频。
然后在 UI 中使用新方法。打开 CallsViewController.swift 将 unwindForNewCall(_:) 方法修改为:
@IBAction private func unwindForNewCall(_ segue: UIStoryboardSegue) {
let newCallController = segue.source as! NewCallViewController
guard let handle = newCallController.handle else { return }
let incoming = newCallController.incoming
let videoEnabled = newCallController.videoEnabled
if incoming {
let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) {
AppDelegate.shared.displayIncomingCall(uuid: UUID(), handle: handle, hasVideo: videoEnabled) { _ in
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
}
} else {
callManager.startCall(handle: handle, videoEnabled: videoEnabled)
}
}
代码中进行了一些调整:当 incoming 为 false 时,view controller 会请求 CallManager 开始一次呼出通话。
这就是打电话功能了。接下来我们测试一下!运行 App。点击 + 按钮,呼出一次通话,确保你选择了 segmented 控件中的 Outgoing。
你应该能够在列表中看到新的通话。注意状态标签会根据当前通话的不同阶段变化:
管理多个通话
你很容易就会想到,Hotline 的用户会收到多个通话。你可以模拟一下,先呼出一次,再呼入一次,然后在呼入进来之前按下 Home 键。这时,App 会显示如下画面:
系统让用户来决定如何处理这种问题。根据用户的选择,它会在一个 CXTransaction 中加入多个 action。例如,如果用户选择结束去电并接听来电,系统会先创建一个 CXEndCallActon,然后是一个CSStartCallAction。两个 action 都放在一个 transaction 中发送给 provider,provider 需要分别进行处理。因此,如果你的 App 能够分别对两个请求进行响应的话,那就不需要再多做什么了!
你可以测试上面说的情况;通话列表会根据你的选择进行显示。App 一次只能处理一个音频会话。如果你选择恢复通话,另一个会自动变成保持通话状态。
创建扩展通讯录
通讯录扩展是 CallKit 提供的一个新功能。它允许你的 VoIP App:
- 将号码添加到系统的黑名单。
- 识别来电号码或者其它唯一识别标记,比如 email 地址。
当系统收到来电,它会在通讯录中进行陪陪,如果没有找到结果,它会在 App 的扩展通讯录中查找。那就让我们在 Hotline 中添加一个扩展通讯录吧!
返回 Xcode,点击菜单 File\New\Target… 然后选择 Call Directory Extension。Xcode 会自动创建一个新文件 CallDirectoryHandler.swift。在项目导航器中选中它,看一下的内容。
第一个方法是 beginRequest(with:)。这个方法在扩展被初始化时调用。如果发生错误,扩展会告诉宿主 App 取消这次扩展请求(通过调用 cancelRequest(withError:)方法)。另外两个方法用于构建 App 的通讯录。
addBlockingPhoneNumber(to:) 方法用于定义要阻塞的电话号码。修改这个方法为:
private func addBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1234 ]
for phoneNumber in phoneNumbers {
context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
}
}
以指定的号码调用 addBlockingEntry(withNextSequentialPhoneNumber:) 方法,将这个号码添加到黑名单。当某个号码被阻塞,系统电话 provider 不会显示任何来自这个号码的来电。
然后是 addIdentificationPhoneNumbers(to:) 方法。将这个方法修改为:
private func addIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1111 ]
let labels = [ "RW Tutorial Team" ]
for (phoneNumber, label) in zip(phoneNumbers, labels) {
context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
}
}
将某个号码和 label 作为参数调用 addIdentificationEntry(withNextSequentialPhoneNumber:label:) 方法将创建一个新的 identification entry。当系统收到这个号码的来电时,电话 UI 上会显示这个 label 给用户。
来测试一下。在设备上运行 App。但是你的扩展并没有被激活。你需要经过以下步骤来激活它:
- 打开设置程序。
- 选择电话。
- 选择来电阻止与身份识别
- 将 Hotline 开关打开
注意:如果你无法让系统识别出你的扩展,请退出 App 并重新打开。有时候 iOS 在使用你的扩展时会有点问题。
测试来电阻止其实很简单:点开 Hotline,以号码 1234 来进行一次呼入。你会注意到系统不会报告任何来电。事实上,你可以在 ProviderDelegate 的reportIncomingCall(uuid:handle:hasVideo:completion:) 方法中打一个断点,你会看到 reportNewIncomingCall 这句代码甚至会报错。
要测试身份识别,再次运行 Hotline,模拟一次呼入,这次,号码输入 1111。你会看到如下的通话界面:
恭喜!你创建了一个 App,用 CallKit 提供了原生的 VoIP 体验!:]
结束
你可以从这里下载最终完成的项目。
如果你想学习更多关于 CallKit 的内容,请看 WWDC 2016 第 230 讲会议视频。
希望你喜欢这篇 CallKit 教程。有任何建议或问题,请在下面留言。
CallKit iOS 教程的更多相关文章
- 最详细在Windows安装Xamarin.iOS教程
最详细在Windows安装Xamarin.iOS教程 来源:http://www.cnblogs.com/llyfe2006/articles/3098280.html 本文展示了如何设立Xamari ...
- Xamarin iOS教程之键盘的使用和设置
Xamarin iOS教程之键盘的使用和设置 Xamarin iOS使用键盘 在文本框和文本视图中可以看到,当用户在触摸这些视图后,就会弹出键盘.本节将主要讲解键盘的输入类型定义.显示键盘时改变输入视 ...
- Xamarin iOS教程之显示和编辑文本
Xamarin iOS教程之显示和编辑文本 Xamarin iOS显示和编辑文本 在一个应用程序中,文字是非常重要的.它就是这些不会说话的设备的嘴巴.通过这些文字,可以很清楚的指定这些应用程序要表达的 ...
- Xamarin iOS教程之视图显示图像
Xamarin iOS教程之视图显示图像 Xamarin iOS显示图像 在主视图中显示一个图像,可以让开发者的应用程序变的更有趣,例如,在一些应用程序开始运行时,都会通过图像来显示此应用程序的玩法或 ...
- Xamarin iOS教程之使用按钮接接收用户输入
Xamarin iOS教程之使用按钮接接收用户输入 Xamarin iOS使用按钮接接收用户输入 按钮是用户交互的最基础控件.即使是在iPhone或者iPad中,用户使用最多操作也是通过触摸实现点击. ...
- Xamarin iOS教程之添加和定制视图
Xamarin iOS教程之添加和定制视图 Xamarin iOS用户界面——视图 在iPhone或者iPad中,用户看到的摸到的都是视图.视图是用户界面的重要组成元素.例如,想要让用户实现文本输入时 ...
- Xamarin iOS教程之申请付费开发者账号下载证书
Xamarin iOS教程之申请付费开发者账号下载证书 Xamarin iOS使用真机测试应用程序 在讲解iOS Simulator时,已经提到了虽然iOS Simulator可以模仿真实的设备,但是 ...
- Xamarin iOS教程之编辑界面编写代码
Xamarin iOS教程之编辑界面编写代码 Xamarin iOS的Interface Builder Interface Builder被称为编辑界面.它是一个虚拟的图形化设计工具,用来为iOS应 ...
- Xamarin iOS教程之自定义视图
Xamarin iOS教程之自定义视图 Xamarin iOS自定义视图 工具栏中的视图在实际应用开发中用的很多,但是为了吸引用户的眼球,开发者可以做出一些自定义的视图. [示例2-33]以下将实现一 ...
随机推荐
- Excel的单元格设置下拉选项并填充颜色
如何在Excel的单元格中加入下拉选项 方法/步骤 第一步:打开excel文档,选中需加入下拉选项的单元格. 第二步:点击菜单中的“数据”->“数据有效性”->“数据 ...
- Spring.Net+NHibernate+Castle学习网站
1.刘冬 http://www.cnblogs.com/GoodHelper/archive/2009/10/16/1584243.html 2.学习资料 http://www.cnblogs.co ...
- LeetCode:括号的分数【856】
LeetCode:括号的分数[856] 题目描述 给定一个平衡括号字符串 S,按下述规则计算该字符串的分数: () 得 1 分. AB 得 A + B 分,其中 A 和 B 是平衡括号字符串. (A) ...
- QQ空间动态内容,好友信息,点赞爬虫脚本
一.安装基础的软件包: 1.准备好火狐浏览器,并下载geckodriver,将geckodriver加入到环境变量:下载geckodriver的地址:https://pan.baidu.com/s/1 ...
- C#中ReferenceEquals和Equals的区别
ReferenceEquals()判断两个字符串是否指向相同的内存地址:(判断引用) Equals,先判断两个字符串有相同的内存位置,是则两个字符串相等:否则逐字符比较两个字符串,判断是否相等(先判断 ...
- LAMP脚本
A goal is a dream with a deadline. Much effort, much prosperity. 环境:CentOS release 6.5 2.6.32-431.e ...
- Sqoop-将MySQL数据导入到hive orc表
sqoop创建并导入数据到hive orc表 sqoop import \ --connect jdbc:mysql://localhost:3306/spider \ --username root ...
- C# 实现汉字转拼音
/// <summary> /// 生成拼音简码 /// </summary> /// <param name="unicodeString"> ...
- LinkedBlockingQueue 与ConcurrentLinkedQueue队列的不同与同
LinkedBlockingQueue 的API中,从队列中获取元素,有以下几个方法: 1.take():原文:Retrieves and removes the head of this queue ...
- 使用ssm整合是创建Maven项目报错Failure to transfer com.thoughtworks.xstream:xstream:pom:1.3.1
Description Resource Path Location TypeFailure to transfer com.thoughtworks.xstream:xstream:pom:1.3. ...