iOS开发系列--App扩展开发

概述

从iOS 8 开始Apple引入了扩展(Extension)用于增强系统应用服务和应用之间的交互。它的出现让自定义键盘、系统分享集成等这些依靠系统服务的开发变成了可能。WWDC 2016上众多更新也都是围绕扩展这一主题来进行了的,例如开发的Siri、iMessage Apps其实都是依靠扩展来工作的。在最新的Xcode 8 beta中也增加了众多的Extension 模板帮助开发者更快的实现不同类型的扩展。因此今天有必要介绍一下扩展相关的开发内容。

扩展的生命周期

iOS对于扩展的支持已经由最初的6类到了如今iOS10的19类(相信随着iOS的发展扩展的覆盖面也会越来越广),当然不同类型的扩展其用途和用法均不尽相同,但是其工作原理和开发方式是类似的。下面列出扩展的几个共同点:

* 扩展依附于应用而不能单独发布和部署;

* 扩展和包含扩展的应用(containing app)生命周期是独立的,分别运行在两个不同的进程中;

* 扩展的运行依赖于宿主应用(或者叫载体应用 host app,而不是containing app)其生命周期由宿主应用确定;

* 对开发者而言扩展作为一个单独的target而存在;

* 扩展通常展现在系统UI或者其他应用中,运行应该尽可能的迅速而功能单一;

由于目前iOS 10正式版尚未发布,官方文档仅就目前9类扩展做了详细指导说明,感兴趣的话大家可以前往查看

官方对于应用扩展的生命周期描述如下图:

通常用户选择了一个扩展的操作时宿主会向扩展发出一个请求来启动此扩展,扩展的生命周期也由此开始(例如用户在分享菜单中选择了你的分享扩展),由于扩展本身由控制器组成,因此此时就会调用类似于viewDidLoad之类的方法进行界面布局和逻辑处理,执行完相应任务之后应该尽快将控制权交给宿主应用,扩展生命周期结束。

尽管扩展和容器应用的生命周期之间没有直接关系,但是扩展本身就是作为容器应用的扩展而存在的,因此扩展和容器应用之间的交互又是不可避免的。通常扩展会通过自定义Scheme的形式来调用容器应用,而容器应用完成响应操作之后通过数据共享将数据共享给扩展来使用。

Today扩展演示

前面说过目前iOS支持19类扩展入口,现在就以Today扩展(也叫做Widget)为例进行说明,在开始之前先对Today扩展有一个简单的认识,下图是微博、墨迹天气、网易云音乐的的Today扩展截图,微博扩展可以用来发送微博、查看更新,墨迹天气则用来展示今日和明日的天气,网易云音乐则是推荐一些相关的歌单、专辑。

我们今天的例子将利用Today扩展实现一个简单的“to do list”查看功能,在容器应用ToDoList中可以增加和删除待办事项,而Today插件则展示最新的几条待办事项,如果没有待办事项则展示添加按钮,点击添加或列表则导航到ToDoList应用。应用的主界面和Today扩展最终截图如下:

在开发之前首先思考一下要实现一个这样的ToDoList扩展需要注意哪些问题:

1. 首先ToDoList容器应用需要思考如何存储数据,因为容器应用完成之后要在Today中展现,前面说过扩展和容器应用没有任何关系,二者处于两个不同的沙盒之中,要实现数据资源共享则必须在开发之前思考如何存储数据的问题?

2. 由于ToDoList容器应用和其扩展ToDoListTodayExtension均要访问读取数据那么两者就存在重复读取数据的操作,也就是两者可能会存在较多的重复代码,如何复用这些代码?

3. 点击扩展列表或添加按钮要回到容器应用,由于扩展中禁用了UIApplication的openURL该如何实现跳转(事实上扩展中很多类型和方法被标记为NS_EXTENSION_UNAVAILABLE,其实思考一下也是合理的,扩展中的UIApplication是宿主应用并非容器应用,如果开发人员直接操作Today的宿主应用岂不危险?)?

这几个问题在下面的演示中将逐一解答,首先要简单实现一个ToDoList应用,这里就不得不考虑第一个问题,怎么样存储数据才能保证后面的扩展开发能够正常访问这些数据。事实上iOS 8 新增了App Groups功能用于实现应用之间的数据共享问题(当然这个功能在OS X现在应该叫做macOS,早就出现了),在Xcode中开启并设置App Groups,Xcode - Capabilities中找到App Groups打开并添加一个名为“group.com.cmjstudio.todolist”组(注意组名称必须以group开头,这一步操作相当于在iOS的开发证书中启用App Groups服务并注册分组,同时在Xcode - Build Settings - Code Signing Entitlements中配置对应的分组配置文件。从Xcode 8开始,证书配置将变得异常简单,不用过多的登录开发者账号管理证书)。添加完分组之后将在项目中生成一个ToDoList.entitlements文件(这其实就是一个xml配置文件,事实上日后如果添加其他服务,其配置也会添加到这个文件中)。既然App Groups和开发证书相关,也就是说同一个开发证书下发布的应用只要配置了相同的组就可以实现数据的共享。App Groups支持的常用数据共享包括NSUserDefaults、NSFileManager、NSFileCoordinator、NSFilePresenter、UIPasteboard、KeyChain、NSURLSession等,这里不妨将数据存储到NSUserDefaults中。

下面将快速创建一个简单的ToDoList,使用UITableView进行展示,数据的操作逻辑放到TaskService.swift中:

import Foundation

let TaskServiceDataKey = "TaskServiceData"
public struct TaskService {
    public static let ToDoListGroupName = "group.com.cmjstudio.todolist"

    public static func addItem(title:String){
        let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName)
        var items = self.getItems()
        items.append(title)
        userDefault?.setObject(items, forKey: TaskServiceDataKey)
        userDefault?.synchronize()
    }

    public static func removeItem(title:String){
        let items = self.getItems()
        let newItems = items.filter { (item) -> Bool in
            item != title
        }
        let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName)
        userDefault?.setObject(newItems, forKey: TaskServiceDataKey)
        userDefault?.synchronize()
    }

    public static func getItems() -> [String]{
        let userDefault = NSUserDefaults(suiteName: TaskService.ToDoListGroupName)
        var tasks = [String]()
        if let array = userDefault?.stringArrayForKey(TaskServiceDataKey) {
            tasks = array
        }
        return tasks

    }
}

实现了ToDoList之后接下来就是进行扩展开发。首先在项目中添加一个名为“ToDoListTodayExtension”的Today Extension类型的Target,并选择激活这个Scheme以便后面测试。然后可以看到在项目根目录创建了一个“ToDoListTodayExtension”文件夹,它包含一个TodayViewController、MainInterface.storyboard和一个info.plist。在info.plist中定义了扩展入口点“com.apple.widget-extension”同时指定了MainInterface作为展示入口,当然很容易就可以猜到TodayViewController是MainInterface.storyboard中控制器对应的class。TodayViewController.swift是一个UIViewController控制器:

class TodayViewController: UIViewController, NCWidgetProviding {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view from its nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) {
        // Perform any setup necessary in order to update the view.

        // If an error is encountered, use NCUpdateResult.Failed
        // If there‘s no update required, use NCUpdateResult.NoData
        // If there‘s an update, use NCUpdateResult.NewData

        completionHandler(NCUpdateResult.newData)
    }

}

可以看出这个类还实现了NCWidgetProviding协议,其中最重要的两个方法就是用于自定义边距的widgetMarginInsets方法和更新插件的widgetPerformUpdate方法。此时如果编译运行(注意之前已经激活扩展的sheme,也就是从扩展运行)并且选择宿主程序Today就会看到一个带有“Hello World”字样的扩展,这其实就是MainInterface的默认布局(注意此时在Products中会生成一个ToDoListTodayExtension.appex就是对应的扩展包)。

接下来就可以进行扩展的界面布局了,你可以选择Storyboard或者code布局,需要注意的是Today扩展的宽度永远都会是屏幕宽度,布局时不需要过多关心,而高度则需要通过调整TodayViewController的preferredContentSize来完成。

另外,这里我们需要思考一个问题:如何使用之前容器应用中编写的TaskService.swift,因为它已经包含了数据的读取方法,我们没有必要在扩展中再实现一遍相同的操作。根据前面文章中关于Swift的命名空间和作用域的介绍应该可以想到将其提取到一个公共的命名空间中,而命名空间的实现通常是使用一个target实现的,这也正是官方推荐的做法。创建一个framework类型的Target并且将TaskSerivce.swift放到这个framework中,ToDoList和ToDoListTodayExtension均使用这个framework(在项目中增加一个名为“ToDoListKit”的Cocoa Touch Framework类型的Target,同时注意将TaskService.swift和对应的类和方法声明为公共方法,在使用TaskService的中使用import ToDoListKit导入这个Framework)。

在TodayViewController中增加UITableView和UIButton,当没有数据时展示UIButton,点击按钮可以通过extensionContext跳转到容器应用并增加新的代办事项,前面提到过在扩展中是无法直接利用UIApplication打开应用的因为扩展在宿主应用中运行,但是在控制器中增加了一个NSExtensionContext类型的上下文来管理扩展操作,这样也就解决了上面说到的第三个问题。扩展的高度则通过preferredContentSize来进行设置,然后根据记录数动态设置其高度,没有数据则设置为一行记录的高度来展示添加按钮。

import UIKit
import NotificationCenter
import ToDoListKit

private let TodayViewControllerMaxCellCount = 3
private let TodayViewControllerCellHeight:CGFloat = 44.0
private let TodayViewControllerTableViewCellKey = "TodayViewControllerTableViewCell"
class TodayViewController: UIViewController, NCWidgetProviding,UITableViewDataSource,UITableViewDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setup()
        self.loadData()
    }

    func widgetPerformUpdate(completionHandler: ((NCUpdateResult) -> Void)) {

        // Perform any setup necessary in order to update the view.

        // If an error is encountered, use NCUpdateResult.Failed
        // If there‘s no update required, use NCUpdateResult.NoData
        // If there‘s an update, use NCUpdateResult.NewData
        self.loadData()
        completionHandler(NCUpdateResult.NewData)
    }

    func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
        return UIEdgeInsetsZero
    }

    // MARK: - UITableView数据源和代理方法
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.data.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier(TodayViewControllerTableViewCellKey)
        if cell == nil {
            cell = UITableViewCell(style: .Subtitle, reuseIdentifier: TodayViewControllerTableViewCellKey)
            cell.textLabel?.textColor = UIColor.whiteColor()
            cell.detailTextLabel?.textColor = UIColor.whiteColor()
        }
        let item = self.data[indexPath.row]

        cell.imageView?.image = UIImage(named: "calendar")
        cell.textLabel?.text = "Date & Time"
        cell.detailTextLabel?.text = item
        return cell
    }

    // MARK: - 事件响应
    @IBAction func addButtonClick(sender: UIButton) {
        let url = NSURL(string: "todolist://add")
        self.extensionContext?.openURL(url!, completionHandler: nil)
    }

    // MARK: - 私有方法
    private func setup(){
        self.addButton.layer.cornerRadius = 3.0
        self.tableView.rowHeight = TodayViewControllerCellHeight
    }

    private func loadData(){
        self.data = [String]()
        let items = TaskService.getItems()
        // 控制最多显示条数
        for i in 0..<items.count {
            self.data.append(items[i])
            if i >= TodayViewControllerMaxCellCount {
                break
            }
        }
        self.layoutUI()
        self.tableView.reloadData()
    }

    private func layoutUI(){
        if self.data.count > 0 {
            self.addButton.hidden = true
            self.tableView.hidden = false
            self.preferredContentSize.height = CGFloat(self.data.count) * TodayViewControllerCellHeight
        } else {
            self.addButton.hidden = false
            self.tableView.hidden = true
            self.preferredContentSize.height = TodayViewControllerCellHeight
        }

    }

    // MARK: - 私有属性
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var addButton: UIButton!

    private var data:[String]!
}

注意:官方已经明确指出Today扩展不支持UIScrollView滚动,建议显示最新数据或者更多的数据通过分页实现。

此外在扩展中使用了一个日历图标calendar,而在容器应用ToDoList中这个图片已经存在于Assets.xcassets中,但在扩展中没办法直接访问容器应用中的资源。一种解决方式是直接往扩展中添加一个calendar图标;另一种就是直接选择扩展这个Target—Build Phases—Copy Bundle Resources 然后添加容器中的资源。这么做的好处是尽管实际运行中存在两份资源,但是开发过程中只需要维护一份。在ToDoListTodayExtension中我们选择第二种方式(当然如果你确实需要进行资源文件共享而不是使用两份资源,你也可以通过NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier(gropuName)来读取容器应用中的文件,但在这里不太适合)。

当然接下来就是给ToDoListTodayExtension扩展配置App Groups,配置方法类似,唯一需要注意的是Group名称必须和前面保持一致,设置为“group.com.cmjstudio.todolist”。最后运行结果如下:

分享扩展

前面说过现在iOS支持的扩展类型越来越多,给开发者提供了更多的交互方式,除了Today扩展之外分享扩展应该是另一个比较常见得扩展类型,比如常用的QQ、微信、微博等都实现了分享扩展。下面再以一个分享扩展为例简单介绍一下这种扩展的开发过程。

假设现在有一个图片社区应用“MyPicture”,用户可以分享各种图片和摄影作品,在系统相册中用户可以选择自己喜欢的图片直接分享到“MyPicture”。关于应用和扩展的创建过程不再赘述,假设已经创建完应用扩展“MyPictureShareExtension”。默认情况下分享扩展编辑界面如下:

首先这个扩展的info.plist相比Today Extension多了一些配置选项,例如可以编辑扩展名称、语言等。这里进行设置如下:

* 扩展显示名称Bundle display name名称为“MyPicture”。

* 配置扩展激活的规则NSExtensionActivationRule,增加最大支持分享图片数NSExtensionActivationSupportsImageWithMaxCount为9,如果超过九张则不显示分享按钮,同时此项配置也确保在网页分享、文件分享中不再出现“MyPicture”扩展。

更多配置参加Apple官方文档 (SystemExtensionKeys)[https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/SystemExtensionKeys.html#//apple_ref/doc/uid/TP40014212-SW2],事实上激活规则还支持更为复杂的断言配置。

其次,Share Extension对应的控制器继承于SLComposeServiceViewController,其中最常用的方法和属性如下:

  • charactersRemaining:剩余字符数,显示在分享界面左下方,例如这里设置为最大200。
  • isContentValid():分享内容验证(例如验证分享内容中是否包含特殊字符),此方法再编辑过程中会不断调用,如果此方法返回false则分享按钮不可用,这里可以通过判断输入动态修改charactersRemaining
  • didSelectPost():发送点击事件,通常在此方法中会上传图片和内容。
  • configurationItems():用于自定义sheet选项,显示在分享界面下方,可以接收点击事件,这里我们会导航到另一个自定义编辑界面用于选择分类。

下图是我们即将实现的最终效果,点击Category可以选择图片分类:

这里重点关注图片的发送过程,在Share Extension中是无法直接获取到图片的(因为我们分享的内容可能是图片,也可能是网页、视频等,因此SLComposeServiceViewController也不太可能会直接提供图片访问接口),所有的访问数据包含进在extensionContextinputItems中,这是一个NSInputItem类型的数组。每个NSInputItem都包含一个attachments集合,它的每个元素都是NSItemProvider类型,每个NSItemProvider就包含了对应的图片、视频、链接、文件等信息,通过它就可以获取到我们需要的图片资源。但是需要注意,通过NSItemProvider进行资源获取的过程较长,同时也会阻塞线程,如果直接在didSelectPost方法中获取图片资源势必造成用户长时间等待,比较好的体验是在presentationAnimationDidFinish方法中就异步调用NSItemProviderloadItemForTypeIdentifier方法进行图片资源加载,并存储到数组中以便在didSelectPost方法中使用。

此外,为了获取更好的用户体验,图片的上传过程同样需要放到后台进行,首先想到的就是使用NSURLSession的后台会话模式,值得一提的是在这个过程中必须指定NSURLSessionConfigurationsharedContainerIdentifier,因为上传的过程中首先会将资源缓存到本地,而扩展是没办法直接访问宿主应用的缓存空间的,配置sharedContainerIdentifier以便利通过App Group使用容器应用的缓存空间。具体实现如下:

import UIKit
import Social
import MobileCoreServices
import Alamofire

private let ShareViewControllerContentTextMax = 200
private let ShareViewControllerDefaultCategoryTitle = "Category"
class ShareViewController: SLComposeServiceViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.imageDatas = [NSData]()
        self.charactersRemaining = ShareViewControllerContentTextMax
        self.placeholder = "Please enter description"
    }

    // 显示分享界面,在此时则异步加载图片到self.images,避免在didSelectPost中再加载图片影响体验
    override func presentationAnimationDidFinish() {
        // 用户输入项
        guard let extensionItem = self.extensionContext?.inputItems.first else { return }
        guard let attachments = extensionItem.attachments as? [NSItemProvider] else { return }
        for attachment in attachments {
            let imageType = kUTTypeImage as String
            if attachment.hasItemConformingToTypeIdentifier(imageType) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
                    attachment.loadItemForTypeIdentifier(imageType, options: nil, completionHandler: { (coding, error) in
                        if error == nil {
                            guard let fileURL = coding as? NSURL else { return }
                            guard let data = NSData(contentsOfURL: fileURL) else { return }
                            self.imageDatas.append(data)
//                            guard let image = UIImage(data: data) else { return }
//                            self.images.append(image)
                        }
                    })
                })
            }
        }
    }

    // 内容验证,输入过程中会不断调用此方法
    override func isContentValid() -> Bool {
        if let text = self.contentText {
            let len = text.characters.count
            if  len > ShareViewControllerContentTextMax {
                return false
            }
            self.charactersRemaining = ShareViewControllerContentTextMax - len
        }
        return true
    }

    // 发送分享内容
    override func didSelectPost() {
        // 上传图片和编辑内容、分类
        self.upload()
        // 通知host app 操作完成
        self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
    }

    // 自定义分享编辑界面sheet
    override func configurationItems() -> [AnyObject]! {
        return [self.categorySheetItem]
    }

    // MARK: - 私有方法
    private func selectCategory(){
        let temp = CategoryTableViewController(style: .Grouped)
        temp.selectedCategory = self.categorySheetItem.title
        temp.selectedCategoryHandler = {
            [weak self]category in
            guard let weakSelf = self else { return }
            weakSelf.categorySheetItem.title = category
        }
        self.pushConfigurationViewController(temp)
    }

    private func upload(){
        let urlStr = "http://requestb.in/v34h3lv3"
        self.manager.upload(.POST,urlStr, multipartFormData: {
            (formData) -> Void in
            for data in self.imageDatas {
                formData.appendBodyPart(data: data, name: "image", mimeType: "image/jpeg")
            }
            // add parameter
            if self.contentText != nil {
                formData.appendBodyPart(data: self.contentText.dataUsingEncoding(NSUTF8StringEncoding)!, name: "content")
            }
            if self.categorySheetItem.title != ShareViewControllerDefaultCategoryTitle {
                formData.appendBodyPart(data: self.categorySheetItem.title.dataUsingEncoding(NSUTF8StringEncoding)!, name: "category")
            }
        }){
            encodingResult in
            switch encodingResult {
            case Manager.MultipartFormDataEncodingResult.Success(_, _, _):
                debugPrint("request")
            case let Manager.MultipartFormDataEncodingResult.Failure(error):
                debugPrint(error)
            }
        }

    }

    // MARK: - 私有属性
    private lazy var categorySheetItem:SLComposeSheetConfigurationItem = {
        let temp = SLComposeSheetConfigurationItem()
        temp.title = ShareViewControllerDefaultCategoryTitle
        temp.tapHandler = self.selectCategory
        return temp
    }()

    // 自定义上传配置,在后台上传避免阻塞UI,注意:由于NSURLSession上传过程中需要先限缓存到本地但是扩展应用本身是没办法使用Host App缓存控件的,因此注意设置sharedContainerIdentifier,使用容器应用的空间
    private lazy var manager:Alamofire.Manager = {
        let configName = "com.cmjstudio.mypicture.backgroundsession"
        let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName)
        configuration.sharedContainerIdentifier = "group.com.cmjstudio.mypicture"
//        configuration.HTTPAdditionalHeaders = Alamofire.Manager.defaultHTTPHeaders
        let manager = Alamofire.Manager(configuration: configuration)
        manager.startRequestsImmediately = true
        manager.backgroundCompletionHandler = {
            debugPrint("completed.")
        }
        return manager
    }()

    private var imageDatas:[NSData]!

}

注意:网络操作部分这里直接选择Alamofire进行上传,如果想自己实现图片上传,可以查看iOS开发系列–网络开发。另外,如果需要自定义分享编辑界面可以让ShareViewController继承自UIViewController,具体细节参见Apple指导文档

由于使用了NSURLSession的后台会话,当执行完相关操作后会调用容器应用的application(application, identifier, completionHandler) 方法,如有必要有些操作可以在此方法中进行处理。

总结

本文着重介绍了Today Extension和Share Extension两种扩展,其实扩展是比较大的一块内容,各类扩展实现方法也不尽相同,但是其生命周期、核心原理是类似的,本文也不再一一探讨。相信iOS 10中更加丰富的扩展类型也会让应用之间的交互越来越丰富,有兴趣的朋友也可以访问下载Xcode 8 beta版进行探索,有时间我们也会写一篇关于Intent Extension、Message Extensiond等新增扩展应用的文章。

时间: 2025-01-01 11:36:43

iOS开发系列--App扩展开发的相关文章

C#程序员学习Android开发系列之搭建开发环境

接触Android好久了,记得09年刚在中国大陆有点苗头的时候,我就知道了google有个Android,它是智能机操作系统.后来在Android出1.5版本之后,我第一时间下载了eclipse开发工具.adt以及android sdk,体验了一把android开发,记得当时搭建开发环境相当麻烦.由于android开发是基于Java的,所以除了上述的工具之外,首先需要安装jdk,记得当时流行的是jdk1.5(后来是1.6,现在是1.7,未来是1.8),安装好之后需要设置环境变量. 时隔4年之后,

把安卓源码中的system app独立出来,像开发普通app那样开发

个人建议首先按照android源代码的ide/eclipse中的格式化xml和import导入到你编译的eclipse中,如果你编译的android源代码是2.3以上的版本的,建议用JDK6以上,提醒各位的是new androidProject不是Java project. 此时你导入想DeskClock.Camera等没有调用到hide标签的项目时,这些项目都不会报错,你可以直接进行相应的修改或研究,呵呵,如果相应直接在eclipse或手机上运行的话,可能会提示你数字签名不正确,这个你自己改下

S5PV210开发系列一_开发环境以及启动模式

S5PV210开发系列一 开发环境以及启动模式 象棋小子          1048272975 ARM核以其高性能.低功耗.低成本广泛应用在各个领域,包括ARM7.ARM9.ARM11.Cortex-M.Cortex-A等这几个系列.众多的半导体商如NXP.Freescale.Atmel.Samsung.TI等都设计了基于ARM核的自家通用处理器,ARM核从低成本控制处理器到高性能应用处理器,已经深入到我们生活的方方面面.笔者此处就Samsung的Cortex-A8处理器S5PV210作一个简

测试开发系列之Python开发mock接口(一)

本次测试开发系列给大家分享一下怎么用python开发mock接口.本次介绍下什么是mock接口,以及咱们开发之前需要做的准备. 什么是mock接口呢,举个栗子,你在一家电商公司,有查看商品.购物.支付.发货.收获等等等一大堆功能,你是一个测试人员,测测测,测到支付功能的时候,你就要调用第三方支付接口了,真实支付,直接扣你支付宝/微信/银行卡里面的钱了,这下大事不好,测个试还得把自己钱搭进去(当然一般公司都会报销测试的支付费用,但是走报销麻烦),还有一些大额支付的的,可能你账户里面根本就没那么钱,

Android开发系列之UI开发

在app开发的过程中,我们会使用到大量的控件,了解各种控件的特性,熟练的使用它们是非常重要的,本篇会详细介绍几种常见控件的使用方法,废话不多说,直接上代码. 一.TextView 二.EditText 三.ImageView 四.ProgressBar 五.AlertDialog 六.详解四种基本布局 一个丰富的界面不仅需要有多个控件组成,更要选择合适的布局使各个控件有条不絮地摆放在界面上,让界面看起来更佳美观.android开发过程中主要有四种基本布局:LinearLayout.Relativ

微信公众号开发系列-微信企业号开发相关参数

微信企业号出来之后,本人也抱着前面开发订阅号和服务号的经验来探究了一番,这里整理了下再开发企业号时候碰到的一些接口参数.企业号开发文档详见http://qydev.weixin.qq.com/wiki/index.php?title=首页 1.创建自定义菜单参数解释: 菜单管理的创建操作,官方定义如下所示. 请求说明 Https请求方式: POST https://qyapi.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN&

Android开发系列之搭建开发环境

接触Android好久了,记得09年刚在中国大陆有点苗头的时候,我就知道了google有个Android,它是智能机操作系统.后来在Android出1.5版本之后,我第一时间下载了eclipse开发工具.adt以及android sdk,体验了一把android开发,记得当时搭建开发环境相当麻烦.由于android开发是基于Java的,所以除了上述的工具之外,首先需要安装jdk,记得当时流行的是jdk1.5(后来是1.6,现在是1.7,未来是1.8),安装好之后需要设置环境变量. 时隔4年之后,

微信公众号开发系列-微信企业号开发相关參数

微信企业号出来之后,本人也抱着前面开发订阅号和服务号的经验来探究了一番,这里整理了下再开发企业号时候碰到的一些接口參数.企业号开发文档详见http://qydev.weixin.qq.com/wiki/index.php?title=首页 1.创建自己定义菜单參数解释: 菜单管理的创建操作.官方定义例如以下所看到的. 请求说明 Https请求方式: POST https://qyapi.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_T

Python爬虫开发系列之一》开发IDE安装

中国有句古话说:工欲善其事,必先利其器! 在我最开始学 Python 的时候,因为没有去探索好用的工具,吃了很多苦头.磕磕绊绊走过来之后才知道,好的工具给效率带来的提升不是从 1 到 1.1 倍速,而是从 1 到 10 倍速. 所以说编写和运行程序之前我们必须要先把开发环境配置好,只有配置好了环境并且有了更方便的开发工具我们才能更加高效地用程序实现相应的功能达到事半工倍的效果,然而很多情况下我们可能在最开始就卡在环境配置上,如果这个过程花费了太多时间,想必学习的兴趣就下降了大半,所以本章专门开发