树的芳香由风决定,人生的芳香由自己决定!
1、即时通讯技术 即时通讯(IM:Instant Messaging):又称实时通讯,支持用户在线实时交谈,允许两人或多人使用网络实时的传递文字消息、文件、语音与视频交流。 即时通讯在开发中使用的场景有许多,如 AOL、Yahoo IM、MSN、QQ 以及微信等聊天软件,在电商 APP 集成买家与卖家的实时沟通等。它们最大的区别在于各自通讯协议的实现,所以即时通讯技术的核心在于它的传输协议,协议是用来说明信息在网络上如何传输。 如果有了统一的传输协议,那么应当可以实现各个 IM 之间的直接通讯,为了创建即时通讯的统一标准,人们多次努力,试图统一各大主要 IM 供应商的标准(AOL、Yahoo 及 Microsoft),但无一成功,且每一种 IM 仍然继续使用自己所拥有的协议。目前已经出现过的 IM 协议包括: SIP :IETF 的对话初始协议,是建立 VOIP 连接的 IETF 标准,而 VOIP 就是网络电话。 SIMPLE:即时通讯对话初始协议和表示扩展协议。 APEX :应用交换协议。 PRIM :显示和即时通讯协议。 XMPP :基于 XML 且开放的可扩展通讯和表示协议,常称为 Jabber 协议。 当前实现即时通讯的方案: XMPP :可扩展通讯和表示协议。 EaseMob:环信,提供即时通信的一个第三平台,是在 XMPP 的基础上进行的二次开发。 2、XMPP 2.1 XMPP 简介 1、XMPP 诞生的由来 设计一款全世界都使用的即时通讯协议,无论使用什么即时通讯软件,都可以互联互通。 2、XMPP 起源 最初 XMPP 作为一个框架开发,目标是支持企业环境内的即时消息传递和联机状态应用程序。 XMPP 的前身是 Jabber(1998 年),是一个开源组织定义的网络即时通信协议。 XMPP 是一个分散型通信网络,这意味着,只要网络基础设施允许,任何 XMPP 用户都可以向其他任何 XMPP 用户传递消息。 多个 XMPP 服务器也可以通过一个专门的 “服务器-服务器” 协议相互通信,提供了创建分散型社交网络和协作框架的可能性。 XMPP 协议曾经是 Google 力推的即时通信协议,其代表作品是 GTalk。 3、XMPP 概述 XMPP:The Extensible Messaging and Presence Protocol,可扩展通讯和表示协议,是一种基于 XML 的即时通讯协议,用于即时消息以及在线现场探测。 它继承了在 XML(可扩展标记语言)环境中灵活的发展性,这表明 XMPP 是可扩展的。 XMPP 规范了用于即时通信在网络上的数据传输格式,它的核心是 XML 流传输协议的定义,可用于服务类实时通讯、表示和需求,响应服务中的 XML 数据元流式传输。 使得 XMPP 能够在一个比以往网络通信协议更规范的平台上。借助于 XML 易于解析和阅读的特性,使得 XMPP 的协议能够非常漂亮。 XMPP 包含了针对服务器端的软件协议,使之能与另一个进行通话,这使得开发者更容易建立客户应用程序或给一个配置好的系统添加功能。 促进服务器之间的准即时操作。这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。 XMPP 是一个典型的 C/S 架构,基本的网络形式是客户端通过 TCP/IP 连接到服务器,通过 Socket 建立连接(目的是为了保持长连接),然后在之上传输 XML 流。 XMPP 以 Jabber 协议为基础,而 Jabber 是即时通讯中常用的开放式协议。 XMPP 的技术规格已被定义在 RFC 3920 及 RFC 3921,文档定义了登录,退出,获取好友,发送消息等等的 XML 数据传输协议。 XMPP 的扩展协议 Jingle 使得其支持语音和视频,目前 iOS 尚不支持。 2.2 XMPP 基本结构 C/S 和 P2P 基本结构图 XMPP 是一个典型的 C/S 架构。 而不是像大多数即时通讯软件一样,使用 P2P 客户端到客户端的架构。 也就是说在大多数情况下,当两个客户端进行通讯时,他们的消息都是通过服务器传递的。 优点:采用这种架构,主要是为了简化客户端,将大多数工作放在服务器端进行。 XMPP 中定义了三个角色:客户端,服务器,网关。 通信能够在这三者的任意两个之间双向发生。 服务器:同时承担了客户端信息记录,连接管理和信息的路由功能。 网关:承担着与异构即时通信系统的互联互通,异构系统可以包括 SMS(短信),MSN,ICQ 等。 基本的网络形式,单客户端通过 TCP/IP 连接到单服务器,然后在之上传输 XML 流。 2.3 XMPP 工作原理 节点连接到服务器。 服务器利用本地目录系统中的证书对其认证。 节点指定目标地址,让服务器告知目标状态。 服务器查找、连接并进行相互认证。 节点之间进行交互。 2.4 XMPP 传输内容 XMPP 应用传输的是与即时通讯相关的指令。 XMPP 传输的即时通讯指令的逻辑与以往相仿,只是协议的形式变成了 XML 格式的纯文本。这不但使得解析容易了,人也容易阅读了,方便了开发和查错。 XMPP 的核心部分就是一个在网络上分片段发送 XML 的流协议。这个流协议是 XMPP 的即时通讯指令的传递基础,也是一个非常重要的可以被进一步利用的网络基础协议,可以说 XMPP 用 TCP 传的是 XML 流。 XMPP 是一种类似于 HTTP 协议的一种数据传输协议。 其过程就如同 “解包装 --> 包装” 的过程,只需要理解其接收的类型及返回的类型,便可以很好的利用 XMPP 来进行数据通讯。 XMPP 核心文件是基于 TCP 的 XML 流的传输,XMPPFrame 框架是通过代理的方式实现消息传递的。 XMPP 客户端获取到服务器发送过来的好友消息,客户端需要对 XML 进行解析,使用的解析框架的 KissXML 框架,而不是 NSXMLParser/GDataXML。 为了简化开发,XMPP 的引用程序通常会将 XMPPStream 放置在 AppDelegate 中,以便于全局访问。 2.5 XMPP JID 每个 XMPP 客户端用户必须拥有一个全局惟一标识符。 基于历史原因,这些标识符称为 Jabber ID 或 JID。鉴于协议的分布式特征,JID 应包含联系用户所需的所有信息,JID 的结构类似于电子邮件地址,但不要求 JID 同时也是有效的电子邮件收件人。 客户端和服务器节点,被统称为 XMPP 实体,都拥有 JID。例如:SomeCorp 公司的员工 John Doe 可能拥有 JID:用户名@服务器名称。 John.Doe@somecorp.com,其中 somecorp.com 是 SomeCorp 公司的 XMPP 服务器的地址,John.Doe 是 John Doe 的用户名。 2.6 XMPP 的优缺点 1、优点 开放:XMPP 协议是自由,开放,公开的,并且易于了解。而且在客户端,服务器,组件,源码库等方面都已经各自有多种实现。 标准:互联网工程工作小组(IETF)已经将 Jabber 的核心 XML 流协议以 XMPP 之名,正式列为认可的实时通信及 Presence 技术。而 XMPP 的技术规格已被定义在 RFC 3920 及 RFC 3921。任何 IM 供应商在遵循 XMPP 协议下,都可与 Google Talk 实现连接。 证实可用:第一个 Jabber(现在 XMPP)技术是 Jeremie Miller 在 1998 年开发的,现在已经相当稳定,数以百计的开发者为 XMPP 技术而努力。今日的互联网上有数以万计的 XMPP 服务器运作著,并有数以百万计的人们使用 XMPP 实时传讯软件。 分布式:XMPP 网络的架构和电子邮件十分相像。XMPP 核心协议通信方式是先创建一个 stream,XMPP 以 TCP 传递 XML 数据流,没有中央主服务器。任何人都可以运行自己的 XMPP 服务器,使个人及组织能够掌控他们的实时传讯体验。 安全:任何 XMPP 协议的服务器可以独立于公众 XMPP 网络(例如在企业内部网络中),而使用 SASL 及 TLS 等技术的可靠安全性,已自带于核心 XMPP 技术规格中。 可扩展:XML 命名空间的威力可使任何人在核心协议的基础上建造客制化的功能。为了维持通透性,常见的扩展由 XMPPStandards Foundation。 弹性佳:XMPP 除了可用在实时通信的应用程序,还能用在网络管理、内容供稿、协同工具、文件共享、游戏、远程系统监控等。 多样性:用 XMPP 协议来建造及布署实时应用程序及服务的公司及开放源代码计划分布在各种领域。用 XMPP 技术开发软件,资源及支持的来源是多样的,使得使你不会陷于被 “绑架” 的困境。 2、缺点 数据负载太重:随着通常超过 70% 的 XMPP 协议的服务器的数据流量的存在和近 60% 的被重复转发,XMPP 协议目前拥有一个大型架空中存在的数据提供给多个收件人。新的议定书正在研究,以减轻这一问题。 没有二进制数据传输:XMPP 协议的方式被编码为一个单一的长的 XML 文件,因此无法提供修改二进制数据。因此, 文件传输协议一样使用外部的 HTTP。如果不可避免,XMPP 协议还提供了带编码的文件传输的所有数据使用的 Base64。至于其他二进制数据加密会话(encrypted conversations)或图形图标(graphic icons)以嵌入式使用相同的方法。 2.7 XMPP 开发架构 开发架构: iOS 框架:XMPPFramework 服务器:Openfire 数据库:MySQL 3、EaseMob 环信 3.1 环信简介 环信是一个第三平台,提供即时通信(IM:Instant Messaging)的服务。 环信使用的是 XMPP 协议,它是在 XMPP 的基础上进行的二次开发,对服务器 Openfire 和客户端进行功能模型的添加和客户端 SDK 的封装。 环信的本质还是使用 XMPP,基于 Socket 的网络通信,在网络上传输的数据也是 XML。 环信内部实现了数据缓存,会把聊天记录添加到数据库,把附件(如音频文件,图片文件)下载到本地,使程序员更多时间是花到用户即时体验上。 环信内部已经实现了视频,音频,图片,其它附件发送功能。 环信使用公司可以节约时间成本,不需要公司内部搭建服务器,环信日活动量在 30 万以下,永远免费。 客户端的开发,使用环信 SDK 比使用 XMPPFramework 更简洁方便。 公司如要开发即时通讯软件,建议首选环信,环信占用市场份额较大。 3.2 环信开发架构 开发架构: 前提准备: 注册成为环信开发者。 在开发者后台创建 APP 获取 Key。 下载官方 SDK。 官方 开发文档。 根据官网导入 SDK 和相应依赖,初始化应用。 4、XMPP Openfire 服务器的搭建 具体讲解见 iOS - XMPP Openfire 服务器的搭建。 5、XMPP 的使用 具体讲解见 iOS - XMPP 的使用。 6、EaseMob 环信的使用 具体讲解见 iOS - EaseMob 环信的使用。
1、环信 环信使用的是 XMPP 协议,它是在 XMPP 的基础上进行的二次开发,对服务器 Openfire 和客户端进行功能模型的添加和客户端 SDK 的封装。环信的本质还是使用 XMPP,基于 Socket 的网络通信,在网络上传输的数据也是 XML。 开发架构: 前提准备: 注册成为环信开发者。 在开发者后台创建 APP 获取 Key。 下载官方 SDK。 官方 开发文档。 根据官网导入 SDK 和相应依赖,初始化应用。 2、环信集成 环信开发文档
前言 提前下载好相关软件,且安装目录最好安装在全英文路径下。如果路径有中文名,那么可能会出现一些莫名其妙的问题。 提前准备好的软件: jdk-8u91-macosx-x64.dmg mysql-5.7.17-macos10.12-x86_64.dmg mysql-workbench-community-6.3.9-osx-x86_64.dmg openfire_4_1_1.dmg Openfire 官网 MySQL 官网 JDK 官网 在安装配置 Openfire 或其他 xmpp 服务器前,需要先安装 MySQL 数据库。 MySQL 安装具体讲解见 iOS - MySQL 的安装配置。 1、下载安装 Openfire 在 Openfire 官网下载最新的 Mac 版本 Openfire 安装包。 下载完后双击安装包,点击 pkg 文件,在安装引导下进行傻瓜式安装。安装完成后,进入系统偏好设置,点击 Openfire 图标。 进入 Openfire 偏好设置界面。点击 Start Openfire,让 OpenFire 服务开始启动(默认是启动的),启动完毕后,我们就可以点击 Administration 下的按钮,进入服务器后台,然后会要求输入管理员账号密码。 Openfire 服务启动不了问题解决 安装好之后,第一次是可以启动 openfire 服务器的,但是电脑重启后,就再也不能启动服务器了,每次一点击 “Start Openfire”,然后加载一下,状态还是 “Start Openfire” 没变化,有时甚至还会跳出错误提示框,提示 “Could not start the Openfire server”。 解决方案如下: 1)首先需要确认是否已经安装了 Java 的运行环境,以及 JAVA jdk 是否与当前 OS 系统版本,Openfire 版本成对应,如果不是,就请先安装相匹配对应的软件。 在终端中输入 java -version,就可以查看电脑有没有安装 JAVA 运行环境。 2)如果软件,环境对应的,最终的解决办法是 打开终端,输入以下命令: // 获取 Openfire 目录的访问权限 sudo chmod -R 777 /usr/local/openfire/bin // 以超级管理员的权限运行脚本 sudo su cd /usr/local/openfire/bin // 设置 Java 的环境变量 export JAVA_HOME=`/usr/libexec/java_home` // 输出检验环境变量的值 echo $JAVA_HOME 输入上面的命令后回车,就会出现后面的这些语句 /Library/Java/JavaVirtualMachines/jdk1.8.0_51.jdk/Contents/Home 接着在终端,输入以下命令: cd /usr/local/openfire/bin // 运行 Openfire shell 脚本 ./openfire.sh 输入上面的命令后回车,就会出现后面的这些语句 Openfire 4.1.2 [2016-2-21 2:47:51] 管理平台开始监听: http://jhq0228-macbookair.local:9090 https://jhq0228-macbookair.local:9091 Successfully loaded plugin 'admin'. 执行完这些命令之后,服务器就可以启动了,每次开机后,都启动不了的话,都试下这个方法。 2、配置 Openfire 在 Openfire 偏好设置界面中,点击 Open Admin Console,进入 web 配置页面,开始配置 Openfire 服务器。 2.1 选择语言 简体中文 2.2 服务器设置 域:如果只是本地机器上登录,可以设置为本地的域 127.0.0.1。需要远端登录的话,设置为相应的 IP 地址或域名即可。此处设置为 Mac 的机器名。 Server Host Name (FQDN):服务器名,不能为 IP 地址。 2.3 数据库设置 1、选择数据库 如果要设置外部数据库(推荐,比如:MySQL),选择标准数据库连接。 前期 MySQL 数据库准备工作 MySQL 安装具体讲解见 iOS - MySQL 的安装配置。 1)设置 /usr/local/openfire 文件夹的访问权限为可读写 方法1:在 finder 中前往文件夹 /usr/local/,右键 openfire 文件夹,显示简介,点击如图右下角中的锁图标解锁,并设置权限为可以读写。 方法2:打开终端,输入如下命令 $ sudo chmod 777 /usr/local/openfire 其中 777 表示授权可读写权限,000 表示无访问权限。 2)在终端中登陆 MySQL,输入以下命令,然后输入数据库的 root 密码登录 $ mysql -u root -p 回车后,终端输出 Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 946 Server version: 5.7.17 MySQL Community Server (GPL) Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 3)在终端输入以下命令,创建数据库 openfire create database openfire; 回车后,终端输出 Query OK, 1 row affected (0.03 sec) 4)在终端输入以下命令,导入 openfire 资源文件夹 resources/database 下的数据表 use openfire; source /usr/local/openfire/resources/database/openfire_mysql.sql 在终端出现一排导入过程 5)在终端输入以下命令,刷新权限 flush privileges; 回车后,终端输出 Query OK, 0 rows affected (0.07 sec) 6)在终端输入以下命令,退出 MySQL exit 2、设置数据库连接 设置标准数据库连接 1)数据库驱动选项 选择 MySQL,前提是已安装 MySQL。 2)JDBC 驱动程序类 默认不变,默认为: com.mysql.jdbc.Driver 3)数据库 URL 形式如下: jdbc:mysql://你的主机名:端口号/数据库名称 jdbc:mysql://[host-name]:3306/[database-name]?rewriteBatchedStatements=true [host-name] :主机名 [database-name]:数据库名称 这里设置为: jdbc:mysql://localhost:3306/openfire 其中主机名 [host-name] 改为 localhost。 其中数据库名称 [database-name] 改为 openfire。 解决数据库字符编码问题,可以在后面加 ?useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8 最终的 url 形式是 jdbc:mysql://localhost:3306/openfire?useUnicode=true&characterEncoding=UTF-8&characterSetResults=UTF-8 注意:前提是已存在一个名为 openfire 的数据库,否则会报如下错误,连接配置不成功。openfire 数据库的创建具体见前面所讲的 “前期 MySQL 数据库准备工作”。 The Openfire database schema does not appear to be installed. Follow the installation guide to fix this error. 4)用户名和密码 这里的用户名密码,是访问 MySQL 数据库时使用的帐号:root,和安装 MySQL 设置的 root 密码。 2.4 特性设置 如果不打算使用 LDAP,则保持默认设置即可。 特性设置时出错问题解决 解决方法 在 OpenFire 偏好设置中重启 OpenFire,然后重新进入 OpenFire web 配置页面,重新开始配置 Openfire 服务器即可。 2.5 管理员账户设置 可以随便填写一个管理员邮箱,输入要设置的密码即可。管理员账号默认为 "admin",如果不设置密码,则默认密码为 "admin"。 自定义管理员账户名方法 在终端输入以下命令,输入数据库的 root 密码,登陆具体的数据库(openfire) mysql -u root -p openfire 删除表 “ofUser” 中的 admin 帐户 delete from ofUser where username = 'admin'; 创建自定义管理员(用户名:qianchia,密码:123456) insert into ofUser (username, plainPassword, encryptedPassword, name, email, creationDate, modificationDate) values('qianchia','123456','123456','Administrator','qianchia@icloud.com','0','0'); 查看用户 select * from ofUser; 如果可以往数据库里插入用户但是在用户摘要却没有数据,这是因为 openfire 的数据库驱动包太旧了,而安装的数据库太新了,把 openfire 里的驱动包换成新的就行了,路径:/usr/local/openfire/lib。 2.6 登陆管理控制台 完成安装后可以输入用户名和密码登陆管理控制台 默认的管理员帐号是 “admin”,默认管理员密码 “admin”,如果上面设置了新密码,则管理员密码是新密码。如果重设了用户名,必须重启 openfire 服务器。 无法登录管理控制平台问题解决 安装 Openfire 后 admin 无法登录管理控制平台。登录时提示:Login failed:make sure your username and password are correct and that you’re an admin or moderator。 解决方案如下: 1)使用 MySQL 查看工具进入数据库,进入表 “ofuser”,将该表清空,然后执行该 SQL INSERT INTO ofUser (username, plainPassword, name, email, creationDate, modificationDate) VALUES ('admin', 'admin', 'Administrator', 'admin@example.com', '0', '0'); 2)关闭 Openfire 服务,就是从其控制台 stop 然后再 start,再用用户名:admin,密码:admin 登录即可。 每次重启 Mac 电脑都需要重新配置 Openfire 问题解决 解决方法: 打开 /usr/local/openfire/conf/ 文件夹。 将 openfire.xml 和 security.xml 两个文件的权限设置为读与写。 重新完成 Openfire 配置。 3、卸载 Openfire 方法 1、卸载之前首先要停止 Openfire 服务。 系统偏好中点击 Openfire 图标,如下图 在 Openfire 偏好设置界面中,点击 Stop Openfire。 2、删除 Openfire 文件。 在终端里,输入以下三条命令执行即可。 sudo rm -rf /Library/PreferencePanes/Openfire.prefPane 以上执行后需要输入管理员密码。 sudo rm -rf /usr/local/openfire sudo rm /Library/LaunchDaemons/org.jivesoftware.openfire.plist 4、测试服务器 4.1 添加测试账户 服务器配置完成之后,我们可以创建几个用户,然后客户端可以使用这些用户信息登录,互相传输消息。 4.2 XMPP 客户端设置与使用 有许多通信聊天客户端可以支持 XMPP 协议,比如,Mac 电脑就自带了一个 “信息” app,“信息” app 就支持 jabber 通信协议(XMPP 的别名)。 打开 Mac 的 “信息” app,点击菜单 信息 -> 添加账户,选择其他 “信息” 账户... 选择 jabber 账户类型,填写相关信息 账户类型:Jabber 用户名:上边添加的测试账户名,格式必须为:名称@openfire服务器名称 密码:用户名对应的密码 服务器:openfire 服务器地址,可以使用自动查找服务器和端口 端口:openfire 服务器客户端端口 然后,提示验证证书,选择继续。 登录成功。 登录成功后在 openfire 服务器端可以看到用户的登录状态。
前言 提前下载好相关软件,且安装目录最好安装在全英文路径下。如果路径有中文名,那么可能会出现一些莫名其妙的问题。 提前准备好的软件: mysql-5.7.17-macos10.12-x86_64.dmg mysql-workbench-community-6.3.9-osx-x86_64.dmg MySQL 官网 1、下载安装 MySQL 1.1 下载 MySQL 访问 MySQL 下载官网,然后在页面中会看到 “MySQL Community Server” 下方有一个 “DOWNLOAD” 点击。 进入 MySQL 的下载界面,如果用的是 Mac OS 来访问的话那么就会默认为你选好了 Mac OS X 平台,而下面罗列的都是在 Mac OS 上能用的 MySQL 的版本,如果是用的其他平台,在 “Select Platform” 选项的下拉列表中选一下就好了。 在 Mac OS 上的 MySQL 的版本很多,其中有按平台来的,比如 10.5/10.6 等平台,然后有 32 位的和 64 位的,这个你按照自己系统的情况来进行选择,然后就是文件的后缀名有 .tar.gz 的和 .dmg 的,这里我选择的是 .dmg 的。点击右侧的 Download 进行下载。 然后会跳转到另外一个界面,这个界面是提示你需不需要注册的,直接选择最下面的 “No thanks, just start my download.”,然后进行下载就 OK 了。 1.2 安装 MySQL MySQL server 安装目录 /usr/local/mysql 下面,子目录 /usr/local/mysql/bin 中包含了 MySQL server 的可执行脚本命令,同时,MySQL server 安装了一个配置程序,方便我们开启/关闭 MySQL 数据库服务器。 打开 MySQL 的安装包,双击 pkg 文件安装。 一路向下,记得保存最后弹出框中的密码,它是你的 mysql root 账号的密码。 安装完成后在系统偏好设置的最下边会出现 MySQL 图标。 点击图标,进入 MySQL 偏好设置,开启 MySQL Server 服务。 1.3 修改 root 账户密码的方法 1、关闭 MySQL Server 服务:苹果 -> 系统偏好设置 -> 最下边点 MySQL,在弹出页面中关闭 MySQL Server 服务(点击 Stop MySQL Server)。 2、进入终端 1)在终端输入: // 苹果系统下 mysql server 的安装地址 $ cd /usr/local/mysql/bin/ 2)回车后,在终端输入: // 登录管理员权限 $ sudo su 3)输入 Mac 管理员密码,登录管理员权限 回车后,终端会输出: sh-3.2# 4)在终端输入以下命令来禁止 mysql 验证功能: // 回车后输入以下命令来禁止 mysql 验证功能 ./mysqld_safe --skip-grant-tables & 回车后 mysql 会自动重启(偏好设置中 MySQL 的状态会变成 running),终端会输出: [1] 19805 sh-3.2# 2016-02-17T22:15:50.6NZ mysqld_safe Logging to '/usr/local/mysql/ data/JHQ0228-MacBookAir.local.err'. Logging to '/usr/local/mysql/data/JHQ0228-MacBookAir.local.err'. 2016-02-17T22:15:50.6NZ mysqld_safe Starting mysqld daemon with databases from /usr/local/mysql/data 3、继续在终端 1)在终端输入: ./mysql 回车后,终端会输出: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 6 Server version: 5.7.17 MySQL Community Server (GPL) Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> 2)在终端输入命令: FLUSH PRIVILEGES; 回车后,终端会输出: Query OK, 0 rows affected (0.03 sec) 3)在终端输入命令: SET PASSWORD FOR 'root'@'localhost' = PASSWORD('你的新密码'); 回车后,终端会输出: Query OK, 0 rows affected, 1 warning (0.01 sec) 以上几步完成后密码就修改成功了,现在就可以用新设置的密码去登陆 mysql 了。 2、下载安装 MySQL Workbench MySQL Workbench 是一款专为 MySQL 设计的 ER / 数据库建模工具。 2.1 下载 MySQL Workbench 访问 MySQL 下载官网,然后在页面中会看到 “MySQL Workbench” 下方有一个 “DOWNLOAD” 点击。 然后同样选择版本之后选择服务器进行下载,这里貌似只有一个版本。 2.2 安装 MySQL Workbench 下载完成之后安装就非常简单,双击即可安装。安装完成之后在 “应用程序” 里面就能看到 MySQLWorkbench.app 程序了,双击打开。到这里 MySql Workbench 就安装完毕了。 3、管理配置 MySQL 3.1 建立一个新连接 点击 MySQL Connections 后面的加号(),点击之后就会出现一个 “Setup New Connection” 的对话框,填写完 Connection Name 之后点击 OK。即可完成一个连接到本地数据库的连接。 完成之后在主界面的就会出现刚才建立的连接,如下图。 单击连接名或者选中一个连接之后点击 “Open Connection”,输入密码,即可进入这个操作数据库的界面。 这些所有的前提都是数据库服务得打开,在系统偏好设置的 MySQL 中进行设置。 网络上,广为流传这样的结论,mysql 的默认账号是 root,默认的密码是空。当我点击 OK 的时候,提示 “登录访问被拒绝”。Google 一下,找到一个解决方案:通过 mysqld_safe 指令使得 mysql 不需要验证就可以登录,登录成功之后,使用 mysql workbench 修改用户密码。Mac 上 MySQL root 密码忘记或权限错误的解决办法见本文中的 1.3 章节。 如果提示 Access denied for user ''@'localhost' to database 'mysql',原因是,mysql 中存在一个匿名用户,如果我们不删除匿名用户,即使使用其他用户登录,都会自动跳转使用匿名用户登录。解决方法参考: 方法一: 1)关闭 mysql # service mysqld stop 2)屏蔽权限 # mysqld_safe --skip-grant-table 屏幕出现:Starting demo from ..... 3)新开起一个终端输入 # mysql -u root mysql mysql> UPDATE user SET Password=PASSWORD('newpassword') where USER='root'; mysql> FLUSH PRIVILEGES;//记得要这句话,否则如果关闭先前的终端,又会出现原来的错误 mysql> \q 方法二: 1)关闭mysql # service mysqld stop 2)屏蔽权限 # mysqld_safe --skip-grant-table 屏幕出现:Starting demo from ..... 3)新开起一个终端输入 # mysql -u root mysql mysql> delete from user where USER=''; mysql> FLUSH PRIVILEGES;//记得要这句话,否则如果关闭先前的终端,又会出现原来的错误 mysql> \q 3.2 加入系统环境变量 在终端中输入: $ cd /usr/local/mysql/bin $ mysql -uroot -p 终端会输出 -bash: mysql: command not found 这说明我们还需要将 mysql 加入系统环境变量。 在终端输入: $ cd /usr/local/mysql/bin $ ls 查看此目录下是否有 mysql,如下图: 在终端输入以下命令: $ vim ~/.bash_profile 在该文件中添加 mysql/bin 的目录,如下图: PATH=$PATH:/usr/local/mysql/bin 添加完成后,按 esc,然后输入 :wq 保存退出。 最后在终端输入: $ source ~/.bash_profile 现在你就可以通过 mysql -uroot -p 登录 mysql 了,登录过程中会让你输入 mysql root 的密码。 输入密码登录成功后终端会输出: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 125 Server version: 5.7.17 MySQL Community Server (GPL) Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 登录成功后,可以通过下面的命令修改密码: $ SET PASSWORD FOR 'root'@'localhost' = PASSWORD('newpass'); 4、MySQL 卸载方法 Mac 下 MySQL 的 dmg 格式安装内有安装文件,却没有卸载文件,如果要卸载 MySQL,需要手动去删。 先在 MySQL 的偏好设置中,停止所有 MySQL Server 服务。 然后在终端中输入以下命令。 sudo rm /usr/local/mysql sudo rm -rf /usr/local/mysql* sudo rm -rf /Library/StartupItems/MySQLCOM sudo rm -rf /Library/PreferencePanes/My* vim /etc/hostconfig (and removed the line MYSQLCOM=-YES-) rm -rf ~/Library/PreferencePanes/My* sudo rm -rf /Library/Receipts/mysql* sudo rm -rf /Library/Receipts/MySQL* sudo rm -rf /var/db/receipts/com.mysql.*
1、蓝牙介绍 具体讲解见 蓝牙 技术信息 蓝牙协议栈 2、iBeacon 具体讲解见 Beacon iBeacon 是苹果公司 2013 年 9 月发布的移动设备用 OS(iOS7)上配备的新功能。其工作方式是,配备有低功耗蓝牙(BLE)通信功能的设备使用 BLE 技术向周围发送自己特有的 ID,接收到该 ID 的应用软件会根据该 ID 采取一些行动。比如,在店铺里设置 iBeacon 通信模块的话,便可让 iPhone 和 iPad 上运行一资讯告知服务器,或者由服务器向顾客发送折扣券及进店积分。此外,还可以在家电发生故障或停止工作时使用 iBeacon 向应用软件发送资讯。 苹果 WWDC 14 之后,对 iBeacon 加大了技术支持和对其用于室内地图的应用有个更明确的规划。苹果公司公布了 iBeacon for Developers 和 Maps for Developers 等专题页面。 3、iOS 蓝牙 3.1 常见简称 MFi:make for ipad ,iphone, itouch 专们为苹果设备制作的设备,开发使用 ExternalAccessory 框架。认证流程挺复杂的,而且对公司的资质要求较高,详见 iOS - MFi 认证。 BLE:buletouch low energy,蓝牙 4.0 设备因为低耗电,所以也叫做 BLE,开发使用 CoreBluetooth 框架。 GATT Profile(Generic Attribute Profile):GATT 配置文件是一个通用规范,用于在 BLE 链路上发送和接收被称为 “属性”(Attribute)的数据块。目前所有的 BLE 应用都基于 GATT。 1) 定义两个 BLE 设备通过叫做 Service 和 Characteristic 的东西进行通信。中心设备和外设需要双向通信的话,唯一的方式就是建立 GATT 连接。 2) GATT 连接是独占的。基于 GATT 连接的方式的,只能是一个外设连接一个中心设备。 3) 配置文件是设备如何在特定的应用程序中工作的规格说明,一个设备可以实现多个配置文件。 GAP(Generic Access Profile):用来控制设备连接和广播,GAP 使你的设备被其他设备可见,并决定了你的设备是否可以或者怎样与合同设备进行交互。 1) GATT 连接,必需先经过 GAP 协议。 2) GAP 给设备定义了若干角色,主要两个:外围设备(Peripheral)和中心设备(Central)。 3) 在 GAP 中外围设备通过两种方式向外广播数据:Advertising Data Payload(广播数据)和 Scan Response Data Payload(扫描回复)。 Profile:并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG(一个以制定蓝牙规范,以推动蓝牙技术为宗旨的跨国组织)或者外设设计者预先定义的 Service 的集合。 Service:服务,是把数据分成一个个的独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,可以自己设置。每个外设会有很多服务,每个服务中包含很多字段,这些字段的权限一般分为读 read,写 write,通知 notiy 几种,就是我们连接设备后具体需要操作的内容。 Characteristic:特征,GATT 事务中的最低界别,Characteristic 是最小的逻辑数据单元,当然它可能包含一个组关联的数据,例如加速度计的 X/Y/Z 三轴值。与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。每个设备会提供服务和特征,类似于服务端的 API,但是机构不同。 Description:每个 Characteristic 可以对应一个或多个 Description 用户描述 Characteristic 的信息或属性。 Peripheral、Central:外设和中心,发起连接的是 Central,被连接的设备为 Peripheral。 3.2 工作模式 蓝牙通信中,首先需要提到的就是 central 和 peripheral 两个概念。这是设备在通信过程中扮演的两种角色。直译过来就是 [中心] 和 [周边(可以理解为外设)]。iOS 设备既可以作为 central,也可以作为 peripheral,这主要取决于通信需求。 例如在和心率监测仪通信的过程中,监测仪作为 peripheral,iOS 设备作为 central。区分的方式即是这两个角色的重要特点:提供数据的是谁,谁就是 peripheral;需要数据的是谁,谁就是 central。就像是 client 和 server 之间的关系一样。 那怎么发现 peripheral 呢 在 BLE 中,最常见的就是广播。实际上,peripheral 在不停的发送广播,希望被 central 找到。广播的信息中包含它的名字等信息。如果是一个温度调节器,那么广播的信息应该还会包含当前温度什么的。那么 central 的作用则是去 scan,找到需要连接的 peripheral,连接后便可进行通信了。 当 central 成功连上 peripheral 后,它便可以获取 peripheral 提供的所有 service 和 characteristic。通过对 characteristic 的数据进行读写,便可以实现 central 和 peripheral 的通信。 CoreBluetooth 框架的核心其实是两个东西,central 和 peripheral, 对应他们分别有一组相关的 API 和类。 这两组 API 分别对应不同的业务场景,如下图,左侧叫做中心模式,就是以你的手机(App)作为中心,连接其他的外设的场景。而右侧称为外设模式,使用手机作为外设连接其他中心设备操作的场景。 iOS 设备(App)作为 central 时: 当 central 和 peripheral 通信时,绝大部分操作都在 central 这边。此时,central 被描述为 CBCentralManager,这个类提供了扫描、寻找、连接 peripheral(被描述为 CBPeripheral)的方法。 下图标示了 central 和 peripheral 在 Core Bluetooth 中的表示方式: 当你操作 peripheral 的时候,实际上是在和它的 service 和 characteristic 打交道,这两个分别由 CBService 和 CBCharacteristic 表示。 iOS 设备(App)作为 Peripheral 时: 在 OS X 10.9
1、MFi 认证 1.1 什么是 MFi 认证 苹果 MFi 认证,是苹果公司(Apple Inc.)对其授权配件厂商生产的外置配件的一种标识使用许可,是 Apple 公司 “Made for iOS” 的英文缩写。 市面上认证产品的显著标识就是在包装正面出现如下白底黑字的苹果 MFi 授权 logo,如本文开头图片所示。苹果公司允许授权厂商在产品包装上印上授权标签。有句话叫无商不奸,如果消费者担心生产商作假,未授权的硬件也偷偷贴上 MFi 的授权标签,那么可以登录苹果的官方网站,进行查询。如下图 1.2 为什么要做 MFi 认证 从苹果角度来看,为了更好的巩固苹果的生态圈,只有集成了有 MFi 芯片,才能跟 iPhone、iPod,iPad 进行连接通信。而只有经过了 MFi 认证的企业才能批量购买 MFi 芯片,并且 MFi 芯片的供销链条都有很严格的监督管理,所以这样苹果可以严格控制只有那些满足苹果规范和要求的外设才能加入到苹果生态圈。 从生产厂商来看,经过苹果官方授权,配件产品能完美兼容苹果智能设备;提交 MFi 认证过程中,硬件设备需要经过苹果要求的 ATS 自测以及苹果的严格测试,产品质量更有保证;消费者也更加信任经过了 MFi 认证授权的配件;最后成功获得 MFi 授权这也成为技术与质量实力的一种标志,因为 MFi 认证通过率仅 2%,其中大部分企业因为申请资格不符合直接被拒绝。 从 iOS 开发人员来看,MFi 认证是由硬件生产商主导进行申请的,是苹果对外设配件的一种认证和授权。但是很多外设跟苹果进行连接,并不只是跟 iOS 设备硬件或者 iOS 系统配合就可以完成对应的功能(比如充电、CarPlay、播放 iPod 音乐(A2DP)、接听蓝牙电话(HPF)或者提供 GPS 输入源等)。很多时候为了实现特定的需求,需要由 iOS App 的配合,由 iOS App 跟对应外设进行连接和通信,传输相关的控制命令对外设进行控制,或者传输相关的外设数据进行展示。iOS App 跟外设的连接方式有网络、EAP 和 BLE,其中 EAP 是苹果官方推荐的跟外设连接的方式。只有经过 MFi 认证的外设才能使用 EAP 跟 App 进行通信。 1.3 如何做 MFi 认证 MFi 认证的流程比较复杂,可以归纳总结为三个部分,如下图所示 其中黄色背景标注的部分是可能跟 iOS App 开发者相关的。其他部分则都是由硬件生产商主导进行的,作为 iOS 开发人员并不需要参与。 1、申请人提交申请资料 首先,收集公司资料信息,这些资料主要包括了认证负责人联系信息,企业情况介绍,公司组织架构、企业网站,物料品质控制以及 ISO 体系证书等资料。然后是在苹果 MFi 官网 上进行注册,并提交第一步收集到的公司资料,进行账号申请。 接下来苹果会进行 MFi 体系审核。这个是非常关键的一个步骤。主要考察公司对 MFi 芯片的管理体系,看公司是否有规范的流程和系统来管理 MFi 芯片,能有效防止转售芯片或者挪用芯片(把芯片用到未通过 MFi 认知的项目上),苹果会安排专人或者代理公司来抽查。 如果 MFi 体系审核过了,苹果还会对公司其他情况进行考察,来评估该公司是否满足 MFi 会员的资格。审核的标准主要看公司相关资质,是否有较大的生产规模;是否拥有自主品牌;品牌在业内是否有较高的地位(主要表现为各类荣誉);是否曾为其他国际知名企业供货;研发人员是否达到苹果要求的人数等,申请者一定保证申报资料的真实性,苹果公司都会一一核实。 如果这些条件都满足,恭喜你公司成为了 MFi 会员,能够有资格购买样品芯片,并且拿到苹果提供的 MFi 官方开发文档,该文档的每一页都是带有申请人姓名水印的,禁止对外公开,如果被发现,有可能会被取消 MFi 会员资格。据说大部分的企业都会被卡在会员资格审核这一步。 2、提交产品计划,研发和自测 如果你的公司是属于那幸运的那一小部分通过了 MFi 会员资格审核,拿到了苹果的 MFi 研发官方文档,也购买了 MFi 样品芯片,那么就可以提交产品计划,进行产品研发和自测了。 提交产品计划是非常关键的一步,需要根据要研发的公司产品的形态、所用技术方案和需要支持的 iOS 设备、iOS 的相关信息都进行详细的描述,其中比较重要信息有。 1)附件概览(Accessory Overview) 技术方案(Technology)如果你是做支持 CapPlay 的车机,那么就选择 CarPlay,否则都应该选择 iAP;如果你的硬件需要跟 iPhone 连接,并且处理相关业务,而不仅仅是充电线或者数据线,那么在 Components 里应该选择 Authentication coprocessor. 2)固件和硬件(Firmware & Hardware) 现在所有的 MFi 认证的硬件都需要支持 iAP2 协议,所以必须要选 iAP2 或者同时支持 iAP2 和 iAP1。然后外设硬件跟苹果设备是如何通信的,是使用 USB 的 Host 模式,还 USB 的 Devices 模式,还是串口或者蓝牙,这个需要根据产品的需求、特性进行选择。 3)选择硬件所支持的 iAP2 的特性 4)选择所支持的苹果设备型号。 根据产品的设计选择所需要支持的苹果设备型号,包括 iPad,iPhone 和 iPod 的各种型号。 5)App 相关的信息 这部分也是 iOS 开发者需要重点关注的部分,包括 App 的版本号,BundleID 和协议字符串以及 iOS App 的主要功能特性描述,这部分信息需要跟最后送 MFi 审核时附带的 App 测试包的信息保持一致。提交了产品计划之后,就可以拿到 PPID(Product Plan ID)。这个 PPID 也是跟 iOS App 开发者需要关注的。当 App 开发完成,提交 AppStore 上线时,需要在版本审核备注信息里带上这个 PPID,否则审核是过不了的。 接下来就可以进行产品研发了。主要是硬件生成商需要根据苹果提供的开发文档进行硬件和驱动认证程序的开发。而 iOS App 开发者则主要是需要集成 iOS 系统提供的一个系统框架 ExternalAccessory.framework,并且在 info.plist 中配置好协议字符串(Supported external accessory protocols)。当 iOS 设备通过 USB 线或者蓝牙连接到对应硬件时,iOS 系统会把符合 MFi 认证要求的外设抽象成了一个流对象,App 通过指定的协议字符串来创建一个 EASession 类的实例来访问到该流对象,就能通过 NSInputStream 和 NSOutputStream 跟硬件件进行通信了。这部分功能实现可以参考苹果官方的 EADemo 进行入门和学习。 产品研发完成后需要进行 ATS(Accessory Test System)自测,并提供自测报告。ATS 自测苹果会提供 ATS Box 的测试工具和软件,主要是针对硬件进行电气特性相关的测试,包括各个节点的电压电流值是否满足苹果要求,然后传输带宽是否稳定,是否达到苹果要求等等。 自测完成之后就可以把硬件和所配套的软件(iOS App 的 ipa 安装包)送到苹果指定的测试实验室进行认证测试。iOS 开发者在这个步骤需要关注的是如何打包 ipa 包。因为如果直接用开发证书打包,那么苹果测试人员的 iPhone 不在你开发证书的设备列表中,是无法安装的。如果用企业证书打包的话,可能 AppStore 发布证书对应的 bundleid 跟企业证书的 bundleid 不一致,所以也不可行。所以推荐的做法是,等到产品研发完成和自测之后,就带上产品计划中拿到的 PPID,提交 AppStore 进行审核。等审核通过之后,就可以直接从 AppStore 下载对应的 ipa 安装包,配合硬件一起送 MFi 认证测试了。 3、测试审核和批量生产 这个阶段也是硬件生产商主导进行的,跟 iOS app 开发者关系不大。当硬件的 MFi 认证送审通过之后,还需要对产品的包装也提交认证和审核。审核通过之后,就可以获得苹果授权进行 MFi 芯片的批量购买,然后根据销售计划进行硬件的批量生产和销售了。 整个 MFi 认证的周期大概需要 3 个月到半年的时间,并且每次提交认证测试都需要支付一笔 600 美金的测试费用,所寄去测试的硬件测试样品苹果也是不会寄回来的。 2、NCM 将只能用于 CarPlay 最近 MFi 开发的苹果官方文档更新到 R25(《Accessory Interface Specification R25.pdf》)了,相比之前的版本,在 CarPlay 的章节中多了这样一句话 “Accessories must not use the NCM interface for anything other than CarPlay”,如下图所示: 这意味着什么呢? 意味着除了 CarPlay,后面所有其他跟 iPhone 连接的外设都不能使用 NCM 的方式跟 iPhone 上的 App 进行连接和通信了。NCM 只能用于 CarPlay,否则外设将无法通过 MFi 认证。 如果你的外设是采用的 NCM 方式跟 App 进行通信,并且目前还没有通过 MFi 认证,需要尽快调整方案,建议改成 EAP(External Accessory Protocol)连接方式,这个是苹果官方推荐连接方式。否则,肯定是过不了 MFi 认证的。 如果你的外设采用的 NCM 方式跟 App 进行通信,并且已经经过了 MFi 认证。那么可以保持现状,不用担心。有人会说苹果会不会后面升级 iOS 系统时,在某个版本中直接在系统底层做限制,如果不是 CarPlay 模式就不能切出 NCM 的端口? 这种担心,小编认为是多余的,因为苹果也是最新的 Spec 才加入这种限制,意味着之前肯定有很多外设采用 NCM 并且通过了苹果的 MFi 认证,苹果需要保证 iPhone 升级 iOS 系统后也能兼容以前的外设。 NCM 按理说是一种非常标准的 USB 传输方式,它把 USB 端口虚拟成标准的网络端口,具有带宽高、天然支持多通道等优点,那苹果为什么要做出这种限制呢?小编猜测还是跟苹果想要严格把控 iOS 系统生态有关。 因为如果采用 EAP,那么对应的 App 就必须集成苹果的 EA 框架(ExternalAccessory.framework),提交 AppStore 审核时,AppStore 通过代码扫描就能扫描到 App 使用到了 EA 框架,知道该 App 需要跟外设进行通信,就需要 App 必须提供对应外设的 PPID(Product Plan ID),如果不能提供 PPID 就会被 AppStore 拒绝。这样就能更严格控制 iOS 系统生态,外设和对应的 App 都在苹果的控制范围之内。 如果 App 采用 NCM 的方式跟外设通信,在 App 层面来说 NCM 就是标准的网络通信,使用 TCP/IP 协议。App 提交 AppStore 审核时,如果不明说这个 App 可以连接某种 MFi 外设,苹果是完全不知道的,这样苹果就在一定程度上丢失对 App 的把控。
1、引言 现在低功耗蓝牙(BLE)连接都是建立在 GATT (Generic Attribute Profile) 协议之上。GATT 是一个在蓝牙连接之上的发送和接收很短的数据段的通用规范,这些很短的数据段被称为属性(Attribute)。 2、GAP 详细介绍 GATT 之前,需要了解 GAP(Generic Access Profile),它用来控制设备连接和广播。GAP 使你的设备被其他设备可见,并决定了你的设备是否可以或者怎样与合同设备进行交互。例如 Beacon 设备就只是向外广播,不支持连接,小米手环等设备就可以与中心设备连接。 2.1 设备角色 GAP 给设备定义了若干角色,其中主要的两个是:外围设备(Peripheral)和中心设备(Central)。 外围设备:这一般就是非常小或者简单的低功耗设备,用来提供数据,并连接到一个更加相对强大的中心设备。例如小米手环。 中心设备:中心设备相对比较强大,用来连接其他外围设备。例如手机等。 2.2 广播数据 在 GAP 中外围设备通过两种方式向外广播数据:Advertising Data Payload(广播数据)和 Scan Response Data Payload(扫描回复),每种数据最长可以包含 31 byte。这里广播数据是必需的,因为外设必需不停的向外广播,让中心设备知道它的存在。扫描回复是可选的,中心设备可以向外设请求扫描回复,这里包含一些设备额外的信息,例如设备的名字。 2.3 广播流程 GAP 的广播工作流程如下图所示。 从图中我们可以清晰看出广播数据和扫描回复数据是怎么工作的。外围设备会设定一个广播间隔,每个广播间隔中,它会重新发送自己的广播数据。广播间隔越长,越省电,同时也不太容易扫描到。 2.4 广播的网络拓扑结构 大部分情况下,外设通过广播自己来让中心设备发现自己,并建立 GATT 连接,从而进行更多的数据交换。也有些情况是不需要连接的,只要外设广播自己的数据即可。用这种方式主要目的是让外围设备,把自己的信息发送给多个中心设备。因为基于 GATT 连接的方式,只能是一个外设连接一个中心设备。使用广播这种方式最典型的应用就是苹果的 iBeacon。广播工作模式下的网络拓扑图如下: 3、GATT GATT 的全名是 Generic Attribute Profile(姑且翻译成:普通属性协议),它定义两个 BLE 设备通过叫做 Service 和 Characteristic 的东西进行通信。GATT 就是使用了 ATT(Attribute Protocol)协议,ATT 协议把 Service, Characteristic 以及对应的数据保存在一个查找表中,此查找表使用 16 bit ID 作为每一项的索引。 一旦两个设备建立起了连接,GATT 就开始起作用了,这也意味着,你必需完成前面的 GAP 协议。这里需要说明的是,GATT 连接,必需先经过 GAP 协议。实际上,在 Android 开发中,可以直接使用设备的 MAC 地址,发起连接,可以不经过扫描的步骤。这并不意味不需要经过 GAP,实际上在芯片级别已经给你做好了,蓝牙芯片发起连接,总是先扫描设备,扫描到了才会发起连接。 GATT 连接需要特别注意的是:GATT 连接是独占的。也就是一个 BLE 外设同时只能被一个中心设备连接。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当设备断开,它又开始广播。 中心设备和外设需要双向通信的话,唯一的方式就是建立 GATT 连接。 3.1 GATT 连接的网络拓扑 下图展示了 GATT 连接网络拓扑结构。这里很清楚的显示,一个外设只能连接一个中心设备,而一个中心设备可以连接多个外设。 一旦建立起了连接,通信就是双向的了,对比前面的 GAP 广播的网络拓扑,GAP 通信是单向的。如果你要让两个设备外设能通信,就只能通过中心设备中转。 3.2 GATT 通信事务 GATT 通信的双方是 C/S 关系。外设作为 GATT 服务端(Server),它维持了 ATT 的查找表以及 service 和 characteristic 的定义。中心设备是 GATT 客户端(Client),它向 Server 发起请求。需要注意的是,所有的通信事件,都是由客户端(也叫主设备,Master)发起,并且接收服务端(也叫从设备,Slave)的响应。 一旦连接建立,外设将会给中心设备建议一个连接间隔(Connection Interval),这样,中心设备就会在每个连接间隔尝试去重新连接,检查是否有新的数据。但是,这个连接间隔只是一个建议,你的中心设备可能并不会严格按照这个间隔来执行,例如你的中心设备正在忙于连接其他的外设,或者中心设备资源太忙。 下图展示一个外设(GATT 服务端)和中心设备(GATT 客户端)之间的数据交换流程,可以看到的是,每次都是主设备发起请求: 3.3 GATT 结构 GATT 事务是建立在嵌套的 Profiles, Services 和 Characteristics 之上的的,如下图所示: 1、Profile 并不是实际存在于 BLE 外设上的,它只是一个被 Bluetooth SIG 或者外设设计者预先定义的 Service 的集合。例如心率 Profile(Heart Rate Profile)就是结合了 Heart Rate Service 和 Device Information Service。所有官方通过 GATT Profile 的列表可以从这里找到。 2、Service 是把数据分成一个个的独立逻辑项,它包含一个或者多个 Characteristic。每个 Service 有一个 UUID 唯一标识。 UUID 有 16 bit 的,或者 128 bit 的。16 bit 的 UUID 是官方通过认证的,需要花钱购买,128 bit 是自定义的,这个就可以自己随便设置。 官方通过了一些标准 Service,完整列表在这里。以 Heart Rate Service 为例,可以看到它的官方通过 16 bit UUID 是 0x180D,包含 3 个 Characteristic:Heart Rate Measurement, Body Sensor Location 和 Heart Rate Control Point,并且定义了只有第一个是必须的,其它是可选实现的。 3、Characteristic 是在 GATT 事务中的最低界别的,Characteristic 是最小的逻辑数据单元,当然它可能包含一组关联的数据,例如加速度计的 X/Y/Z 三轴值。 与 Service 类似,每个 Characteristic 用 16 bit 或者 128 bit 的 UUID 唯一标识。你可以免费使用 Bluetooth SIG 官方定义的标准 Characteristic,使用官方定义的,可以确保 BLE 的软件和硬件能相互理解。当然,你可以自定义 Characteristic,这样的话,就只有你自己的软件和外设能够相互理解。 举个例子,Heart Rate Measurement Characteristic,这是上面提到的 Heart Rate Service 必需实现的 Characteristic,它的 UUID 是 0x2A37。它的数据结构是,开始 8 bit 定义心率数据格式,接下来就是对应格式的实际心率数据。 实际上,和 BLE 外设打交道,主要是通过 Characteristic。你可以从 Characteristic 读取数据,也可以往 Characteristic 写数据。这样就实现了双向的通信。所以你可以自己实现一个类似串口(UART)的 Sevice,这个 Service 中包含两个 Characteristic,一个被配置只读的通道(RX),另一个配置为只写的通道(TX)。 3.4 更多内容 Bluetooth SIG 官方文档 蓝牙核心协议文档 Bluetooth Developer Portal 官方通过的 BLE Profile 官方通过的 BLE Service 官方通过的 BLE Characteristic 移动开发资源 Android BLE GUIDE - Android developer 官网的入门文章,里面有实例代码和讲解视频。 Application Accelerator Kit - iOS, Android or Windows Phone 移动开发样例 视频: Core Bluetooth 101 - WWDC 2012 关于 BLE 开发的视频。
1、UIView 动画 具体讲解见 iOS - UIView 动画 2、UIImageView 动画 具体讲解见 iOS - UIImageView 动画 3、CADisplayLink 定时器 具体讲解见 iOS - OC NSTimer 定时器 CADisplayLink 是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个 runloop 中,并给它提供一个 target 和 selector 在屏幕刷新的时候调用。 CADisplayLink 使用场合相对专一,适合做 UI 的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer 的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在 UI 相关的动画或者显示内容使用 CADisplayLink 比起用 NSTimer 的好处就是我们不需要再格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。 4、CALayer 绘图层 具体讲解见 iOS - CALayer 绘图层 在 iOS 系统中,你能看得见摸得着的东西基本上都是 UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是 UIView。其实 UIView 之所以能显示在屏幕上,完全是因为它内部的一个层。在创建 UIView 对象时,UIView 内部会自动创建一个层(即 CALayer 对象),通过 UIView 的 layer 属性可以访问这个层。当 UIView 需要显示到屏幕上时,会调用 drawRect: 方法进行绘图,并且会将所有内容绘制在自己的层上,绘图完毕后,系统会将层拷贝到屏幕上,于是就完成了 UIView 的显示。换句话说,UIView 本身不具备显示的功能,是它内部的层才有显示功能。 5、核心动画基本概念 Core Animation 中文翻译为核心动画,它是一组非常强大的动画处理 API,使用它能做出非常炫丽的动画效果,而且往往是事半功倍。也就是说,使用少量的代码就可以实现非常强大的功能。Core Animation 可以用在 macOS 和 iOS 平台。乔帮主在 2007 年的 WWDC 大会上亲自为你演示 Core Animation 的强大:点击查看视频。 Core Animation 的动画执行过程都是在后台操作的,不会阻塞主线程。 要注意的是,Core Animation 是直接作用在 CALayer 上的,并非 UIView。 核心动画 和 UIView 动画 的区别: 核心动画一切都是假象,并不会真实的改变图层的属性值,如果以后做动画的时候,不需要与用户交互,通常用核心动画(转场)。 UIView 动画必须通过修改属性的真实值,才有动画效果。 5.1 核心动画继承结构 核心动画继承结构 5.2 核心动画使用步骤 Xcode6 之前的版本,使用它需要先添加 QuartzCore.framework 和引入对应的框架 <QuartzCore/QuartzCore.h>。 开发步骤: 1、首先得有 CALayer。 2、初始化一个 CAAnimation 对象,并设置一些动画相关属性。 3、通过调用 CALayer 的 addAnimation:forKey: 方法,增加 CAAnimation 对象到 CALayer 中,这样就能开始执行动画了。 4、通过调用 CALayer 的 removeAnimationForKey: 方法可以停止 CALayer 中的动画。 5.3 CAAnimation 1、CAAnimation 简介 CAAnimation 是所有动画对象的父类,负责控制动画的持续时间和速度,是个抽象类,不能直接使用,应该使用它具体的子类。 属性说明: removedOnCompletion :默认为 YES,代表动画执行完毕后就从图层上移除,图形会恢复到动画执行前的状态。 如果想让图层保持显示动画执行后的状态,那就设置为 NO,不过还要设置 fillMode 为 kCAFillModeForwards。 timingFunction :速度控制函数,控制动画运行的节奏。 delegate :动画代理,需要遵守协议 CAAnimationDelegate。 // 来自 CAMediaTiming 协议的属性 duration :动画的持续时间。 repeatCount :重复次数,无限循环可以设置 HUGE_VALF 或者 MAXFLOAT。 repeatDuration :重复时间。 fillMode :决定当前对象在非 active 时间段的行为。比如动画开始之前或者动画结束之后。 beginTime :可以用来设置动画延迟执行时间,若想延迟 2s,就设置为 CACurrentMediaTime()+2,CACurrentMediaTime() 为图层的当前时间。 2、CAAnimation 动画填充模式 设置 fillMode 属性值(要想 fillMode 有效,需要设置 removedOnCompletion = NO) kCAFillModeRemoved :这个是默认值,也就是说当动画开始前和动画结束后,动画对 layer 都没有影响,动画结束后,layer 会恢复到之前的状态。 kCAFillModeForwards :当动画结束后,layer 会一直保持着动画最后的状态。 kCAFillModeBackwards :在动画开始前,只需要将动画加入了一个 layer,layer 便立即进入动画的初始状态并等待动画开始。 kCAFillModeBoth :这个其实就是上面两个的合成,动画加入后开始之前,layer 便处于动画初始状态,动画结束后 layer 保持动画最后的状态。 3、CAAnimation 速度控制函数 CAMediaTimingFunction 速度控制函数 kCAMediaTimingFunctionLinear :线性,匀速,给你一个相对静态的感觉。 kCAMediaTimingFunctionEaseIn :渐进,动画缓慢进入,然后加速离开。 kCAMediaTimingFunctionEaseOut :渐出,动画全速进入,然后减速的到达目的地。 kCAMediaTimingFunctionEaseInEaseOut :渐进渐出,动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。 4、CAAnimation 动画代理方法 CAAnimation 在分类中定义了代理方法 @interface NSObject (CAAnimationDelegate) /* Called when the animation begins its active duration. */ // 动画开始执行的时候触发这个方法 - (void)animationDidStart:(CAAnimation *)anim; /* Called when the animation either completes its active duration or * is removed from the object it is attached to (i.e. the layer). 'flag' * is true if the animation reached the end of its active duration * without being removed. */ // 动画执行完毕的时候触发这个方法 - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag; @end 5.4 CAPropertyAnimation CAPropertyAnimation 是 CAAnimation 的子类,也是个抽象类,要想创建动画对象,应该使用它的两个子类: CABasicAnimation CAKeyframeAnimation 属性说明: keyPath :通过指定 CALayer 的一个属性名称为 keyPath(NSString 类型), 并且对 CALayer 的这个属性的值进行修改,达到相应的动画效果。 比如,指定 @“position” 为 keyPath,就修改 CALayer 的 position 属性的值,以达到平移的动画效果。 5.5 CALayer 上动画的暂停和恢复 1、CALayer 上动画的暂停 // 暂停 CALayer 的动画,自定义方法 - (void)pauseLayer:(CALayer*)layer { CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; // 让 CALayer 的时间停止走动 layer.speed = 0.0; // 让 CALayer 的时间停留在 pausedTime 这个时刻 layer.timeOffset = pausedTime; } 2、CALayer 上动画的恢复 // 恢复 CALayer 的动画,自定义方法 - (void)resumeLayer:(CALayer*)layer { CFTimeInterval pausedTime = layer.timeOffset; // 让 CALayer 的时间继续行走 layer.speed = 1.0; // 取消上次记录的停留时刻 layer.timeOffset = 0.0; // 取消上次设置的时间 layer.beginTime = 0.0; // 计算暂停的时间(这里也可以用 CACurrentMediaTime() - pausedTime) CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; // 设置相对于父坐标系的开始时间(往后退 timeSincePause) layer.beginTime = timeSincePause; } 6、基本动画 CABasicAnimation 基本动画,是 CAPropertyAnimation 的子类。 属性说明: fromValue :keyPath 相应属性的初始值 toValue :keyPath 相应属性的结束值 byValue :keyPath 相应属性的改变值 动画过程说明: keyPath 内容是 CALayer 的可动画 Animatable 属性。 随着动画的进行,在长度为 duration 的持续时间内,keyPath 相应属性的值从 fromValue 渐渐地变为 toValue。 如果 fillMode = kCAFillModeForwards 同时 removedOnComletion = NO,那么在动画执行完毕后,图层会保持显示动画执行后的状态。但在实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变。 CABasicAnimation 虽然能够做很多基本的动画效果,但是有个局限性,只能让 CALayer 的属性从某个值渐变到另一个值,仅仅是在 2 个值之间渐变。 6.1 平移动画 1、方法 1 // 说明这个动画对象要对 CALayer 的 position 属性执行动画 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"]; // 动画持续 1.5s anim.duration = 1.5; // position 属性值从 (50, 80) 渐变到 (300, 350) anim.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 80)]; anim.toValue = [NSValue valueWithCGPoint:CGPointMake(250, 350)]; // 设置动画的代理 anim.delegate = self; // 保持动画执行后的状态 anim.removedOnCompletion = NO; anim.fillMode = kCAFillModeForwards; // 添加动画对象到图层上 [self.myView.layer addAnimation:anim forKey:@"translate"]; 第 2 行设置的 keyPath 是 @"position",说明要修改的是 CALayer 的 position 属性,也就是会执行平移动画。 注意第 8、9 行,这里并不是直接使用 CGPoint 这种结构体类型,而是要先包装成 NSValue 对象后再使用。这 2 行代码表示 CALayer 从位置 (50, 80) 移动到位置 (250, 350)。 如果将第 9 行的 toValue 换成 byValue,代表 CALayer 从位置 (50, 80) 开始向右移动 250、向下移动 350,也就是移动到位置 (300, 430)。 anim.byValue = [NSValue valueWithCGPoint:CGPointMake(250, 350)]; 默认情况下,动画执行完毕后,动画会自动从 CALayer 上移除,CALayer 又会回到原来的状态。为了保持动画执行后的状态,可以加入第 15、16 行代码。 第 19 行后面的 @"translate" 是给动画对象起个名称,以后可以调用 CALayer 的 removeAnimationForKey: 方法根据动画名称停止相应的动画。 第 12 行是设置动画的代理,可以监听动画的执行过程,这里设置控制器为代理。需要遵守协议 CAAnimationDelegate,代理需要实现的方法有: - (void)animationDidStart:(CAAnimation *)anim { NSLog(@"%s", __func__); } - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { NSLog(@"%s", __func__); } 打印结果为: 09:11:10.880 CALayerDemo[12721:622836] -[ViewController animationDidStart:] 09:11:12.419 CALayerDemo[12721:622836] -[ViewController animationDidStop:finished:],position:{50, 80} 实际上,动画执行完毕后,并没有真正改变 CALayer 的 position 属性的值。 效果 2、方法 2 // 说明这个动画对象要对 CALayer 的 transform 属性执行动画 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"]; // 动画持续 1.5s anim.duration = 1.5; // 平移,x 轴移动 250,y 轴移动 350,z 轴移动 0 CATransform3D form = CATransform3DMakeTranslation(250, 350, 0); anim.toValue = [NSValue valueWithCATransform3D:form]; // 添加动画对象到图层上 [self.myView.layer addAnimation:anim forKey:nil]; 效果 6.2 缩放动画 1、方法 1 // 说明这个动画对象要对 CALayer 的 bounds 属性执行动画 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"bounds"]; // 动画持续 2s anim.duration = 2; // 缩放,width 从 100 变为 30,height 从 100 变为 30 // anim.fromValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)]; // fromValue 可以省略 anim.toValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 30, 30)]; // 添加动画对象到图层上 [self.myView.layer addAnimation:anim forKey:nil]; 效果 2、方法 2 // 说明这个动画对象要对 CALayer 的 transform 属性执行动画 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"]; // 动画持续 1.5s anim.duration = 1.5; // 缩放,width 从 100 变为 200,height 从 100 变为 150 // anim.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)]; anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(2.0, 1.5, 1.0)]; // 添加动画对象到图层上 [self.myView.layer addAnimation:anim forKey:nil]; 效果 3、心跳效果 // 说明这个动画对象要对 CALayer 的 transform.scale 属性执行动画 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; // 动画持续 1.0s anim.duration = 1.0; // 缩放倍数 anim.toValue = @0.5; // 设置动画执行次数 anim.repeatCount = MAXFLOAT; // 保持动画执行后的状态 anim.removedOnCompletion = NO; anim.fillMode = kCAFillModeForwards; // 添加动画对象到图层上 [imageView.layer addAnimation:anim forKey:nil]; 效果 6.3 旋转动画 1、方法 // 说明这个动画对象要对 CALayer 的 transform.rotation 属性执行动画 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; // 动画持续 3.5s anim.duration = 3.5; // 缩放倍数 anim.toValue = @(M_PI_4 * 3); // 添加动画对象到图层上 [self.myView.layer addAnimation:anim forKey:nil]; 效果 2、方法 // 说明这个动画对象要对 CALayer 的 transform 属性执行动画 CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"]; // 动画持续 3.5s anim.duration = 3.5; // 绕着 (0, 0, 1) 这个向量轴顺时针旋转 45° // anim.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0, 0, 0, 1)]; anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_4 * 3, 0, 0, 1)]; // 设置动画执行次数 anim.repeatCount = MAXFLOAT; // 保持动画执行后的状态 anim.removedOnCompletion = NO; anim.fillMode = kCAFillModeForwards; // 添加动画对象到图层上 [self.myView.layer addAnimation:anim forKey:nil]; 效果 7、关键帧动画 CAKeyframeAnimation 关键帧动画,是 CAPropertyAnimation 的子类。 与 CABasicAnimation 的区别是: CABasicAnimation 只能从一个数值(fromValue)变到另一个数值(toValue),而 CAKeyframeAnimation 会使用一个 NSArray 保存这些数值。 CABasicAnimation 可看做是只有2个关键帧的 CAKeyframeAnimation。 属性说明: values :上述的 NSArray 对象。里面的元素称为 “关键帧” (keyframe)。动画对象会在指定的时间(duration)内, 依次显示 values 数组中的每一个关键帧。 path :可以设置一个 CGPathRef、CGMutablePathRef,让图层按照路径轨迹移动。path 只对 CALayer 的 anchorPoint 和 position 起作用。如果设置了 path,那么 values 将被忽略。 keyTimes :可以为对应的关键帧指定对应的时间点,其取值范围为 0 到 1.0,keyTimes 中的每一个时间值都对应 values 中的每一帧。如果没有设置 keyTimes,各个关键帧的时间是平分的。 7.1 平移动画 1、添加动画关键帧 CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation"]; // CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position"]; anim.duration = 5.0; // 动画起始位置,从 position 点出开始 NSValue *pointVal1 = [NSValue valueWithCGPoint:CGPointMake(0, 0)]; // 水平移动 200 - 0,垂直方向不变(0 - 0 = 0) NSValue *pointVal2 = [NSValue valueWithCGPoint:CGPointMake(200, 0)]; // 垂直移动 200 - 0,水平位置不变(200 - 200 = 0) NSValue *pointVal3 = [NSValue valueWithCGPoint:CGPointMake(200, 200)]; // 水平移动 0 - 200,垂直方向不变(200 - 200 = 0) NSValue *pointVal4 = [NSValue valueWithCGPoint:CGPointMake(0, 200)]; // 垂直移动 0 - 200,水平位置不变(0 - 0 = 0) NSValue *pointVal5 = [NSValue valueWithCGPoint:CGPointMake(0, 0)]; // 添加动画关键帧 anim.values = @[pointVal1, pointVal2, pointVal3, pointVal4, pointVal5]; [self.myView.layer addAnimation:anim forKey:nil]; 效果 2、添加动画路径 CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation"]; // CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position"]; anim.duration = 5.0; // 添加动画路径 anim.path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 200, 200)].CGPath; [self.myView.layer addAnimation:anim forKey:nil]; 效果 3、划定路径移动 DrawView.m @interface DrawView () @property (nonatomic, strong) UIBezierPath *path; @end @implementation DrawView /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 移除上一个动画 [self.subviews.firstObject.layer removeAnimationForKey:@"drawAnimation"]; // 获取触摸起始点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; // 创建路径 self.path = [UIBezierPath bezierPath]; // 设置起点 [self.path moveToPoint:startPoint]; } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; [self.path addLineToPoint:touchPoint]; [self setNeedsDisplay]; } /// 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 添加关键帧动画 CAKeyframeAnimation *anim = [CAKeyframeAnimation animation]; anim.keyPath = @"position"; // 添加动画路径 anim.path = self.path.CGPath; anim.duration = 3; anim.repeatCount = MAXFLOAT; anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; // 给子视图添加核心动画 [self.subviews.firstObject.layer addAnimation:anim forKey:@"drawAnimation"]; } /// 触摸取消 - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event { [self touchesEnded:touches withEvent:event]; } /// 绘图 - (void)drawRect:(CGRect)rect { [self.path stroke]; } @end ViewController.m DrawView *myDrawView = [[DrawView alloc] initWithFrame:self.view.bounds]; myDrawView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:myDrawView]; UIImageView *imv = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)]; imv.image = [UIImage imageNamed:@"heart2"]; [myDrawView addSubview:imv]; 效果 7.2 缩放动画 缩放动画 CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; anim.duration = 5.0; // 动画起始大小,1 不缩放 NSNumber *scaleVal1 = @1; // 放大 2 倍 NSNumber *scaleVal2 = @2; // 不缩放,缩小至原来大小 NSNumber *scaleVal3 = @1; // 缩小 0.5 倍 NSNumber *scaleVal4 = @0.5; // 不缩放,放大至原来大小 NSNumber *scaleVal5 = @1; // 添加动画关键帧 anim.values = @[scaleVal1, scaleVal2, scaleVal3, scaleVal4, scaleVal5]; [self.myView.layer addAnimation:anim forKey:nil]; 效果 7.3 旋转动画 1、旋转动画 CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"]; anim.duration = 5.0; // 动画起始角度,0 不旋转 NSNumber *rotationVal1 = @0; // 旋转 M_PI_4 * 1 NSNumber *rotationVal2 = @(M_PI_4 * 1); // 旋转 M_PI_4 * 2 NSNumber *rotationVal3 = @(M_PI_4 * 2); // 旋转 M_PI_4 * 3 NSNumber *rotationVal4 = @(M_PI_4 * 3); // 旋转 M_PI_4 * 4 NSNumber *rotationVal5 = @(M_PI_4 * 4); // 添加动画关键帧 anim.values = @[rotationVal1, rotationVal2, rotationVal3, rotationVal4, rotationVal5]; [self.myView.layer addAnimation:anim forKey:nil]; 效果 2、图标抖动 CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"]; anim.repeatCount = MAXFLOAT; NSNumber *rotationVal1 = @(-5 / 180.0 * M_PI); NSNumber *rotationVal2 = @(5 / 180.0 * M_PI); NSNumber *rotationVal3 = @(-5 / 180.0 * M_PI); // 添加动画关键帧 anim.values = @[rotationVal1, rotationVal2, rotationVal3]; [self.myView.layer addAnimation:anim forKey:nil]; 效果 8、转场动画 CATransition 转场动画,是 CAAnimation 的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果。UINavigationController 就是通过 CATransition 实现了将控制器的视图推入屏幕的动画效果。 动画属性: type :动画类型。 subtype :动画方向。 startProgress :动画起点(在整体动画的百分比)。 endProgress :动画终点(在整体动画的百分比)。 设置动画类型 基本型: kCATransitionFade 交叉淡化过渡 kCATransitionPush 新视图把旧视图推出去 kCATransitionMoveIn 新视图移到旧视图上面 kCATransitionReveal 将旧视图移开,显示下面的新视图 用字符串表示的类型: fade push moveIn reveal 和系统自带的四种一样 pageCurl 向上翻页效果 pageUnCurl 向下翻页效果 rippleEffect 水滴效果 suckEffect 收缩效果,如一块布被抽走 cube 立方体翻滚效果 alignedCube 立方体翻滚效果 flip 翻转效果 alignedFlip 翻转效果 oglFlip 翻转效果 rotate 旋转 cameraIrisHollowOpen 相机镜头打开效果 cameraIrisHollowClose 相机镜头关闭效果 cameraIris 相机镜头打开关闭效果 kCATransitionFade 和 fade 效果,kCATransitionPush 和 push 效果 kCATransitionMoveIn 和 moveIn 效果,kCATransitionReveal 和 reveal 效果 pageCurl 效果,pageUnCurl 效果 rippleEffect 效果,suckEffect 效果 cube、alignedCube 效果,flip、alignedFlip、oglFlip 效果 rotate 效果,cameraIris 效果 cameraIrisHollowOpen 效果,cameraIrisHollowClose 效果 设置动画方向 四种预设,某些类型中此设置无效: kCATransitionFromRight kCATransitionFromLeft kCATransitionFromTop kCATransitionFromBottom UIView 实现转场动画方法 // 单视图 + (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; 参数说明: view:需要进行转场动画的视图 duration:动画的持续时间 options:转场动画的类型 animations:将改变视图属性的代码放在这个 block 中 completion:动画结束后,会自动调用这个 block // 双视图 + (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion; 参数说明: duration:动画的持续时间 options:转场动画的类型 completion:动画结束后,会自动调用这个 block 8.1 添加转场动画 添加转场动画 @property (weak, nonatomic) IBOutlet UIView *animView; @property (weak, nonatomic) IBOutlet UIImageView *imageView; - (IBAction)btnClick:(id)sender { static int i = 2; // 加载图片 self.imageView.image = [UIImage imageNamed:[NSString stringWithFormat:@"page%d", i]]; i = (i == 2) ? 1 : i + 1; // 添加转场动画 CATransition *anim = [CATransition animation]; // 设置动画效果 anim.type = @"flip"; // 设置动画方向 anim.subtype = kCATransitionFromLeft; // 设置动画时间 anim.duration = 1.0; // 添加转场动画 [self.animView.layer addAnimation:anim forKey:nil]; } 效果 9、动画组 CAAnimationGroup 动画组,是 CAAnimation 的子类,可以保存一组动画对象,将 CAAnimationGroup 对象加入层后,组中所有动画对象可以同时并发运行。 属性说明: animations :用来保存一组动画对象的 NSArray。默认情况下,一组动画对象是同时运行的, 也可以通过设置动画对象的 beginTime 属性来更改动画的开始时间。 9.1 添加动画组 添加动画组 // 同时旋转,缩放,平移 CAAnimationGroup *anim = [CAAnimationGroup animation]; CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; rotation.toValue = @(M_PI_4 * 3); CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; scale.toValue = @0.5; CABasicAnimation *translation = [CABasicAnimation animationWithKeyPath:@"transform.translation"]; translation.toValue = [NSValue valueWithCGPoint:CGPointMake(300, 300)]; // 添加动画 anim.animations = @[rotation, scale, translation]; anim.duration = 3.0; anim.removedOnCompletion = NO; anim.fillMode = kCAFillModeForwards; // 添加动画组 [self.myView.layer addAnimation:anim forKey:nil]; 效果 10、核心动画的使用 具体讲解见 iOS - Core Animation 核心动画的使用
1、简单使用示例 1.1 时钟 QClockView.h @interface QClockView : UIView /// 创建时钟界面 + (instancetype)q_clockViewWithFrame:(CGRect)frame; @end QClockView.m #define CLOCK_WIDTH self.bounds.size.width @interface QClockView () // 表盘 @property (nonatomic, strong) UIImageView *clockView; // 指针 @property (nonatomic, strong) CALayer *secondLayer; @property (nonatomic, strong) CALayer *minuteLayer; @property (nonatomic, strong) CALayer *hourLayer; @end @implementation QClockView /// 创建时钟界面 + (instancetype)q_clockViewWithFrame:(CGRect)frame { QClockView *clockView = [[self alloc] initWithFrame:frame]; return clockView; } /// 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 添加表盘 self.clockView = [[UIImageView alloc] init]; self.clockView.image = [UIImage imageNamed:@"clock"]; [self addSubview:self.clockView]; // 添加定时器 [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeChange) userInfo:nil repeats:YES]; [self timeChange]; } return self; } /// 布局子控件 - (void)layoutSubviews { [super layoutSubviews]; self.clockView.frame = self.bounds; } // 定时器响应事件处理 - (void)timeChange { // 获取当前的系统的时间 // 获取当前日历对象 NSCalendar *calendar = [NSCalendar currentCalendar]; // 获取日期的组件:年月日小时分秒 NSDateComponents *cmp = [calendar components:NSCalendarUnitSecond | NSCalendarUnitMinute | NSCalendarUnitHour fromDate:[NSDate date]]; // 获取秒 NSInteger second = cmp.second + 1; // 获取分 NSInteger minute = cmp.minute; // 获取小时 NSInteger hour = cmp.hour; // 计算秒针转多少度,一秒钟秒针转 6° CGFloat secondA = second * 6; // 计算分针转多少度,一分钟分针转 6° CGFloat minuteA = minute * 6; // 计算时针转多少度,一小时时针转 30°,每分钟时针转 0.5° CGFloat hourA = hour * 30 + minute * 0.5; // 旋转秒针,使用 layer 的隐式动画属性产生动画效果 self.secondLayer.transform = CATransform3DMakeRotation(secondA / 180 * M_PI, 0, 0, 1); // 旋转分针 self.minuteLayer.transform = CATransform3DMakeRotation(minuteA / 180 * M_PI, 0, 0, 1); // 旋转小时 self.hourLayer.transform = CATransform3DMakeRotation(hourA / 180 * M_PI, 0, 0, 1); } // 添加秒针 - (CALayer *)secondLayer { if (_secondLayer == nil) { _secondLayer = [CALayer layer]; _secondLayer.backgroundColor = [UIColor redColor].CGColor; _secondLayer.anchorPoint = CGPointMake(0.5, 1); _secondLayer.position = CGPointMake(CLOCK_WIDTH * 0.5, CLOCK_WIDTH * 0.5); _secondLayer.bounds = CGRectMake(0, 0, 1, CLOCK_WIDTH * 0.5 - 20); [self.clockView.layer addSublayer:_secondLayer]; } return _secondLayer; } // 添加分针 - (CALayer *)minuteLayer { if (_minuteLayer == nil) { _minuteLayer = [CALayer layer]; _minuteLayer.backgroundColor = [UIColor blackColor].CGColor; _minuteLayer.anchorPoint = CGPointMake(0.5, 1); _minuteLayer.position = CGPointMake(CLOCK_WIDTH * 0.5, CLOCK_WIDTH * 0.5); _minuteLayer.bounds = CGRectMake(0, 0, 4, CLOCK_WIDTH * 0.5 - 20); _minuteLayer.cornerRadius = 2; [self.clockView.layer addSublayer:_minuteLayer]; } return _minuteLayer; } // 添加时针 - (CALayer *)hourLayer { if (_hourLayer == nil) { _hourLayer = [CALayer layer]; _hourLayer.backgroundColor = [UIColor blackColor].CGColor; _hourLayer.anchorPoint = CGPointMake(0.5, 1); _hourLayer.position = CGPointMake(CLOCK_WIDTH * 0.5, CLOCK_WIDTH * 0.5); _hourLayer.bounds = CGRectMake(0, 0, 4, CLOCK_WIDTH * 0.5 - 40); _hourLayer.cornerRadius = 2; [self.clockView.layer addSublayer:_hourLayer]; } return _hourLayer; } @end ViewController.m // 创建时钟界面 QClockView *clockView = [QClockView q_clockViewWithFrame:CGRectMake(100, 100, 200, 200)]; [self.view addSubview:clockView]; 效果 1.2 转盘 QWheelButton.h @interface QWheelButton : UIButton @end QWheelButton.m @implementation QWheelButton /// 设置按钮中 imageView 的尺寸 - (CGRect)imageRectForContentRect:(CGRect)contentRect { // 计算 imageView 控件尺寸,contentRect 为按钮的尺寸 CGFloat W = 40; CGFloat H = 46; CGFloat X = (contentRect.size.width - W) * 0.5; CGFloat Y = 20; return CGRectMake(X, Y, W, H); } /// 设置按钮接收点击事件的区域 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGFloat btnW = self.bounds.size.width; CGFloat btnH = self.bounds.size.height; // 计算不接收点击事件的区域 CGFloat X = 0; CGFloat Y = btnH / 2; CGFloat W = btnW; CGFloat H = Y; CGRect rect = CGRectMake(X, Y, W, H); if (CGRectContainsPoint(rect, point)) { return nil; } else { return [super hitTest:point withEvent:event]; } } @end QWheelView.h @interface QWheelView : UIView /// 创建转盘界面 + (instancetype)q_wheelViewWithFrame:(CGRect)frame; /// 开始转动 - (void)q_startAnimating; /// 停止转动 - (void)q_stopAnimating; @end QWheelView.m #import "QWheelButton.h" #define WHEEL_WIDTH self.bounds.size.width @interface QWheelView () <CAAnimationDelegate> /// 转盘控件 @property (nonatomic, strong) UIImageView *backImageView; @property (nonatomic, strong) UIImageView *centerImagerView; @property (nonatomic, strong) UIButton *startButton; /// 当前选中的按钮 @property (nonatomic, weak) UIButton *selectedButton; /// 显示定时器 @property (nonatomic, strong) CADisplayLink *link; @end @implementation QWheelView /// 创建转盘界面 + (instancetype)q_wheelViewWithFrame:(CGRect)frame { QWheelView *wheelView = [[self alloc] initWithFrame:frame]; return wheelView; } /// 开始转动 - (void)q_startAnimating { self.link.paused = NO; } /// 停止转动 - (void)q_stopAnimating { self.link.paused = YES; } /// 初始化转盘控件 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 添加转盘界面 self.backImageView = [[UIImageView alloc] init]; self.backImageView.image = [UIImage imageNamed:@"LuckyBaseBackground"]; [self addSubview:self.backImageView]; self.centerImagerView = [[UIImageView alloc] init]; self.centerImagerView.image = [UIImage imageNamed:@"LuckyRotateWheel"]; self.centerImagerView.userInteractionEnabled = YES; [self addSubview:self.centerImagerView]; // 添加开始选号按钮 self.startButton = [[UIButton alloc] init]; [self.startButton setBackgroundImage:[UIImage imageNamed:@"LuckyCenterButton"] forState:UIControlStateNormal]; [self.startButton setBackgroundImage:[UIImage imageNamed:@"LuckyCenterButtonPressed"] forState:UIControlStateHighlighted]; [self.startButton addTarget:self action:@selector(startPicker:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:self.startButton]; // 添加号码按钮 // 加载大图片 UIImage *bigImage = [UIImage imageNamed:@"LuckyAstrology"]; UIImage *selBigImage = [UIImage imageNamed:@"LuckyAstrologyPressed"]; // 获取当前使用的图片像素和点的比例 CGFloat scale = [UIScreen mainScreen].scale; CGFloat imageW = bigImage.size.width / 12 * scale; CGFloat imageH = bigImage.size.height * scale; for (int i = 0; i < 12; i++) { QWheelButton *button = [QWheelButton buttonWithType:UIButtonTypeCustom]; [self.centerImagerView addSubview:button]; // 设置按钮图片 CGRect clipR = CGRectMake(i * imageW, 0, imageW, imageH); [button setImage:[self getImageWithClipRect:clipR fromImage:bigImage] forState:UIControlStateNormal]; [button setImage:[self getImageWithClipRect:clipR fromImage:selBigImage] forState:UIControlStateSelected]; [button setBackgroundImage:[UIImage imageNamed:@"LuckyRototeSelected"] forState:UIControlStateSelected]; // 监听按钮的点击 [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside]; // 默认选中第一个 if (i == 0) { [self buttonClick:button]; // button.backgroundColor = [UIColor blueColor]; } } } return self; } /// 布局子控件 - (void)layoutSubviews { [super layoutSubviews]; // 转盘界面 self.backImageView.frame = CGRectMake(0, 0, WHEEL_WIDTH, WHEEL_WIDTH); self.centerImagerView.layer.position = CGPointMake(WHEEL_WIDTH * 0.5, WHEEL_WIDTH * 0.5); self.centerImagerView.bounds = CGRectMake(0, 0, WHEEL_WIDTH, WHEEL_WIDTH); // 开始选号按钮 self.startButton.frame = CGRectMake((WHEEL_WIDTH - WHEEL_WIDTH / 3.5) / 2, (WHEEL_WIDTH - WHEEL_WIDTH / 3.5) / 2, WHEEL_WIDTH / 3.5, WHEEL_WIDTH / 3.5); // 号码按钮 CGFloat btnW = 68; CGFloat btnH = 143; for (int i = 0; i < 12; i++) { QWheelButton *button = self.centerImagerView.subviews[i]; // 按钮尺寸位置 button.bounds = CGRectMake(0, 0, btnW, btnH); button.layer.position = CGPointMake(WHEEL_WIDTH * 0.5, WHEEL_WIDTH * 0.5); button.layer.anchorPoint = CGPointMake(0.5, 1); // 按钮的旋转角度 CGFloat radion = (30 * i) / 180.0 * M_PI; button.transform = CGAffineTransformMakeRotation(radion); } } /// 定时器触发事件处理 - (void)angleChange { // 每一次调用旋转多少 90 / 60.0 CGFloat angle = (90 / 60.0) * M_PI / 180.0; // 持续旋转,需要设置 centerImagerView 的 layer.position 值 self.centerImagerView.transform = CGAffineTransformRotate(self.centerImagerView.transform, angle); } /// 按钮选中点击事件处理 - (void)buttonClick:(UIButton *)button { // 取消之前的按钮选择状态 self.selectedButton.selected = NO; // 设置当前点击的按钮选择状态 button.selected = YES; self.selectedButton = button; } /// 点击开始选号的时候 - (void)startPicker:(UIButton *)button { // 不需要定时器旋转 self.link.paused = YES; // 中间的转盘快速的旋转,并且不需要与用户交互 CABasicAnimation *anim = [CABasicAnimation animation]; anim.keyPath = @"transform.rotation"; anim.toValue = @(M_PI * 2 * 3); anim.duration = 0.5; anim.delegate = self; [self.centerImagerView.layer addAnimation:anim forKey:nil]; // 根据选中的按钮获取旋转的度数,通过 transform 获取角度 CGFloat angle = atan2(self.selectedButton.transform.b, self.selectedButton.transform.a); // 点击哪个星座,就把当前星座指向中心点上面 self.centerImagerView.transform = CGAffineTransformMakeRotation(-angle); } /// 动画协议方法 - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ self.link.paused = NO; }); } /// 懒加载定时器 - (CADisplayLink *)link { if (_link == nil) { _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(angleChange)]; [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } return _link; } /// 裁剪图片 - (UIImage *)getImageWithClipRect:(CGRect)clipR fromImage:(UIImage *)image { // 裁剪图片,裁剪区域是以像素为基准 CGImageRef cgImage = CGImageCreateWithImageInRect(image.CGImage, clipR); return [UIImage imageWithCGImage:cgImage]; } @end ViewController.m // 创建转盘界面 QWheelView *wheelView = [QWheelView q_wheelViewWithFrame:CGRectMake(40, 150, 300, 300)]; [self.view addSubview:wheelView]; self.wheelView = wheelView; // 开始转动 [self.wheelView q_startAnimating]; // 停止转动 [self.wheelView q_stopAnimating]; 效果 1.3 折叠图片 一张图片必须要通过两个控件展示,旋转的时候,只旋转一部分控件,通过 layer 控制图片的显示内容,可以让一张完整的图片通过两个控件显示。 @interface ViewController () // 上下两部分图片绘制控件 @property (nonatomic, strong) UIImageView *topImageView; @property (nonatomic, strong) UIImageView *bottomImageView; // 触控控件 @property (nonatomic, strong) UIView *dragView; // 渐变图层 @property (nonatomic, strong) CAGradientLayer *gradientL; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor cyanColor]; // 添加上下两部分图片绘制控件,两个控件叠加在一起,且在位于 drawView 中间 self.bottomImageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 250, 200, 100)]; self.bottomImageView.image = [UIImage imageNamed:@"demo"]; [self.view addSubview:self.bottomImageView]; self.topImageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 250, 200, 100)]; self.topImageView.image = [UIImage imageNamed:@"demo"]; [self.view addSubview:self.topImageView]; // 设置图片显示的尺寸 self.topImageView.layer.contentsRect = CGRectMake(0, 0, 1, 0.5); // 范围 0 ~ 1 self.topImageView.layer.anchorPoint = CGPointMake(0.5, 1); self.bottomImageView.layer.contentsRect = CGRectMake(0, 0.5, 1, 0.5); self.bottomImageView.layer.anchorPoint = CGPointMake(0.5, 0); // 添加渐变图层 self.gradientL = [CAGradientLayer layer]; self.gradientL.frame = self.bottomImageView.bounds; self.gradientL.opacity = 0; // 设置透明度 self.gradientL.colors = @[(id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor]; // 设置渐变颜色 // self.gradientL.colors = @[(id)[UIColor redColor].CGColor, // (id)[UIColor greenColor].CGColor, // (id)[UIColor yellowColor].CGColor]; // self.gradientL.locations = @[@0.1, @0.4, @0.5]; // 设置渐变定位点 // gradientL.startPoint = CGPointMake(0, 1); // 设置渐变开始点,取值 0~1 [self.bottomImageView.layer addSublayer:self.gradientL]; // 添加触控控件 self.dragView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 200, 200)]; UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; [self.dragView addGestureRecognizer:pan]; // 添加手势 [self.view addSubview:self.dragView]; } // 拖动的时候旋转上部分内容 - (void)pan:(UIPanGestureRecognizer *)pan { // 获取偏移量 CGPoint transP = [pan translationInView:self.dragView]; // 旋转角度,往下逆时针旋转 CGFloat angle = -transP.y / 200.0 * M_PI; CATransform3D transfrom = CATransform3DIdentity; transfrom.m34 = -1 / 500.0; // 增加旋转的立体感,近大远小,d:距离图层的距离 transfrom = CATransform3DRotate(transfrom, angle, 1, 0, 0); self.topImageView.layer.transform = transfrom; // 设置阴影效果 self.gradientL.opacity = transP.y * 1 / 200.0; if (pan.state == UIGestureRecognizerStateEnded) { // 反弹 // 弹簧效果的动画,SpringWithDamping:弹性系数,越小弹簧效果越明显 [UIView animateWithDuration:0.6 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:10 options:UIViewAnimationOptionCurveEaseInOut animations:^{ self.topImageView.layer.transform = CATransform3DIdentity; } completion:nil]; } } @end 效果 1.4 图片倒影 QRepView.h @interface QRepView : UIView @end QRepView.m @implementation QRepView // 设置控件主层的类型 + (Class)layerClass { return [CAReplicatorLayer class]; } @end ViewController.m #import "QRepView.h" @interface ViewController () @property (nonatomic, strong) QRepView *repView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor cyanColor]; self.repView = [[QRepView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)]; [self.view addSubview:self.repView]; UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.repView.bounds]; imageView.image = [UIImage imageNamed:@"demo.jpg"]; [self.repView addSubview:imageView]; // 创建复制图层 CAReplicatorLayer *repLayer = (CAReplicatorLayer *)self.repView.layer; // 设置子层数量 repLayer.instanceCount = 2; // 往下面平移旋转控件 CATransform3D translation = CATransform3DMakeTranslation(0, self.repView.bounds.size.height, 0); CATransform3D rotateTranslation = CATransform3DRotate(translation, M_PI, 1, 0, 0); repLayer.instanceTransform = rotateTranslation; // 设置阴影 repLayer.instanceAlphaOffset = -0.1; repLayer.instanceRedOffset = -0.1; repLayer.instanceGreenOffset = -0.1; repLayer.instanceBlueOffset = -0.1; } @end 效果 1.5 音量振动条 创建音量振动条 @property (nonatomic, strong) UIView *vibrationBarView; self.vibrationBarView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)]; self.vibrationBarView.backgroundColor = [UIColor lightGrayColor]; [self.view addSubview:self.vibrationBarView]; // 创建复制图层 CAReplicatorLayer *repLayer = [CAReplicatorLayer layer]; // 可以把图层里面所有子层复制 repLayer.frame = self.vibrationBarView.bounds; [self.vibrationBarView.layer addSublayer:repLayer]; // 创建震动条图层 CALayer *layer = [CALayer layer]; layer.anchorPoint = CGPointMake(0.5, 1); layer.position = CGPointMake(30, self.vibrationBarView.bounds.size.height); layer.bounds = CGRectMake(0, 0, 30, 150); layer.backgroundColor = [UIColor whiteColor].CGColor; [repLayer addSublayer:layer]; // 设置缩放动画 CABasicAnimation *anim = [CABasicAnimation animation]; anim.keyPath = @"transform.scale.y"; anim.toValue = @0.1; anim.duration = 0.5; anim.repeatCount = MAXFLOAT; anim.autoreverses = YES; // 设置动画反转 [layer addAnimation:anim forKey:nil]; // 设置复制层中子层 repLayer.instanceCount = 4; // 设置复制层里面有多少个子层,包括原始层 repLayer.instanceTransform = CATransform3DMakeTranslation(45, 0, 0); // 设置复制子层偏移量,不包括原始层,相对于原始层 x 偏移 repLayer.instanceDelay = 0.1; // 设置复制层动画延迟时间 repLayer.instanceColor = [UIColor greenColor].CGColor; // 设置复制子层背景色 repLayer.instanceGreenOffset = -0.3; 效果 1.6 活动指示器 创建活动指示器 @property (nonatomic, strong) UIView *activityIndicatorView; self.activityIndicatorView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)]; self.activityIndicatorView.backgroundColor = [UIColor redColor]; [self.view addSubview:self.activityIndicatorView]; // 创建复制图层 CAReplicatorLayer *repLayer = [CAReplicatorLayer layer]; // 可以把图层里面所有子层复制 repLayer.frame = self.activityIndicatorView.bounds; [self.activityIndicatorView.layer addSublayer:repLayer]; // 创建指示器图层 CALayer *layer = [CALayer layer]; layer.transform = CATransform3DMakeScale(0, 0, 0); layer.position = CGPointMake(self.activityIndicatorView.bounds.size.width / 2, 20); layer.bounds = CGRectMake(0, 0, 10, 10); layer.backgroundColor = [UIColor greenColor].CGColor; [repLayer addSublayer:layer]; // 设置缩放动画 CGFloat duration = 1.0; CABasicAnimation *anim = [CABasicAnimation animation]; anim.keyPath = @"transform.scale"; anim.fromValue = @1; anim.toValue = @0; anim.duration = duration; anim.repeatCount = MAXFLOAT; [layer addAnimation:anim forKey:nil]; // 设置复制层中子层 CGFloat count = 20; CGFloat angle = M_PI * 2 / count; repLayer.instanceCount = count; // 设置复制层里面有多少个子层,包括原始层 repLayer.instanceTransform = CATransform3DMakeRotation(angle, 0, 0, 1); // 设置复制子层偏移量,不包括原始层,相对于原始层 x 偏移 repLayer.instanceDelay = duration / count; // 设置复制层动画延迟时间 效果 1.7 粒子运动效果 QParticleMotionView.h @interface QParticleMotionView : UIView /// 创建粒子运动效果界面 + (instancetype)q_particleMotionViewWithFrame:(CGRect)frame; /// 开始运动 - (void)q_startAnimating; /// 清除运动 - (void)q_clearAnimating; @end QParticleMotionView.m @interface QParticleMotionView () /// 贝塞尔路径 @property (nonatomic, strong) UIBezierPath *path; /// 复制图层 @property (nonatomic, strong) CAReplicatorLayer *repLayer; /// 圆点图层 @property (nonatomic, strong) CALayer *dotLayer; /// 圆点个数 @property (nonatomic, assign) NSInteger dotCount; @end @implementation QParticleMotionView /// 创建粒子运动效果界面 + (instancetype)q_particleMotionViewWithFrame:(CGRect)frame { QParticleMotionView *particleMotionView = [[self alloc] initWithFrame:frame]; particleMotionView.backgroundColor = [UIColor whiteColor]; return particleMotionView; } /// 开始运动 - (void)q_startAnimating { // 创建关键帧动画 CAKeyframeAnimation *anim = [CAKeyframeAnimation animation]; anim.keyPath = @"position"; anim.path = self.path.CGPath; anim.duration = 3; anim.repeatCount = MAXFLOAT; [self.dotLayer addAnimation:anim forKey:@"dotAnimation"]; // 复制子层 self.repLayer.instanceCount = self.dotCount; self.repLayer.instanceDelay = 0.2; } /// 清除运动 - (void)q_clearAnimating { // 移除动画 [self.dotLayer removeAnimationForKey:@"dotAnimation"]; // 清空路径 self.path = nil; // 清空子层总数 self.dotCount = 0; [self setNeedsDisplay]; } /// 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.dotCount = 0; // 创建复制图层 self.repLayer = [CAReplicatorLayer layer]; self.repLayer.frame = self.bounds; [self.layer addSublayer:self.repLayer]; // 创建圆点图层 CGFloat wh = 10; self.dotLayer = [CALayer layer]; self.dotLayer.frame = CGRectMake(0, -1000, wh, wh); self.dotLayer.cornerRadius = wh / 2; self.dotLayer.backgroundColor = [UIColor blueColor].CGColor; [self.repLayer addSublayer:self.dotLayer]; } return self; } /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸起始点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; // 设置起点 [self.path moveToPoint:startPoint]; } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; // 添加线到某个点 [self.path addLineToPoint:touchPoint]; // 重绘 [self setNeedsDisplay]; self.dotCount ++; } /// 绘制贝塞尔路径 - (void)drawRect:(CGRect)rect { [self.path stroke]; } /// 懒加载路径 - (UIBezierPath *)path { if (_path == nil) { _path = [UIBezierPath bezierPath]; } return _path; } @end ViewController.m @property (nonatomic, strong) QParticleMotionView *particleMotionView; // 创建粒子运动效果界面 CGRect frame = CGRectMake(0, 100, self.view.bounds.size.width, self.view.bounds.size.height - 100); self.particleMotionView = [QParticleMotionView q_particleMotionViewWithFrame:frame]; [self.view addSubview:self.particleMotionView]; // 开始运动 [self.particleMotionView q_startAnimating]; // 清除运动 [self.particleMotionView q_clearAnimating]; 效果 1.8 划定路径移动 DrawView.m @interface DrawView () @property (nonatomic, strong) UIBezierPath *path; @end @implementation DrawView /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 移除上一个动画 [self.subviews.firstObject.layer removeAnimationForKey:@"drawAnimation"]; // 获取触摸起始点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; // 创建路径 self.path = [UIBezierPath bezierPath]; // 设置起点 [self.path moveToPoint:startPoint]; } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; [self.path addLineToPoint:touchPoint]; [self setNeedsDisplay]; } /// 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 添加关键帧动画 CAKeyframeAnimation *anim = [CAKeyframeAnimation animation]; anim.keyPath = @"position"; // 添加动画路径 anim.path = self.path.CGPath; anim.duration = 3; anim.repeatCount = MAXFLOAT; anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; // 给子视图添加核心动画 [self.subviews.firstObject.layer addAnimation:anim forKey:@"drawAnimation"]; } /// 触摸取消 - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event { [self touchesEnded:touches withEvent:event]; } /// 绘图 - (void)drawRect:(CGRect)rect { [self.path stroke]; } @end ViewController.m DrawView *myDrawView = [[DrawView alloc] initWithFrame:self.view.bounds]; myDrawView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:myDrawView]; UIImageView *imv = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)]; imv.image = [UIImage imageNamed:@"heart2"]; [myDrawView addSubview:imv]; 效果 1.9 粘性效果按钮 QGooButton.h @interface QGooButton : UIButton @end QGooButton.m /// 最大圆心距离 #define MAX_DISTANCE 80 #define WIDTH self.bounds.size.width @interface QGooButton () /// 小圆视图 @property (nonatomic, strong) UIView *smallCircleView; /// 不规则矩形 @property (nonatomic, strong) CAShapeLayer *shapeLayer; /// 小圆原始半径 @property (nonatomic, assign) CGFloat oriSmallRadius; /// 小圆原始中心 @property (nonatomic, assign) CGPoint oriSmallCenter; @end @implementation QGooButton /// 懒加载 - (CAShapeLayer *)shapeLayer { if (_shapeLayer == nil) { // 展示不规则矩形,通过不规则矩形路径生成一个图层 _shapeLayer = [CAShapeLayer layer]; _shapeLayer.fillColor = self.backgroundColor.CGColor; [self.superview.layer insertSublayer:_shapeLayer below:self.layer]; } return _shapeLayer; } - (UIView *)smallCircleView { if (_smallCircleView == nil) { _smallCircleView = [[UIView alloc] init]; _smallCircleView.backgroundColor = self.backgroundColor; // 小圆添加按钮的父控件上 [self.superview insertSubview:_smallCircleView belowSubview:self]; } return _smallCircleView; } /// 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 记录小圆最初始半径和中心 self.oriSmallRadius = WIDTH / 2; // 设置按钮圆角 self.layer.cornerRadius = WIDTH / 2; // 设置按钮文字颜色和字体大小 [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; self.titleLabel.font = [UIFont systemFontOfSize:18]; // 添加手势 UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; [self addGestureRecognizer:pan]; } return self; } - (void)layoutSubviews { [super layoutSubviews]; static int flag = 1; if (flag) { flag = 0; // 设置小圆位置和尺寸 self.smallCircleView.center = self.center; self.smallCircleView.bounds = self.bounds; self.smallCircleView.layer.cornerRadius = WIDTH / 2; } } /// 拖动手势事件处理 - (void)pan:(UIPanGestureRecognizer *)pan { // 获取手指偏移量 CGPoint transP = [pan translationInView:self]; // 移动控件位置 CGPoint center = self.center; center.x += transP.x; center.y += transP.y; self.center = center; // 复位 [pan setTranslation:CGPointZero inView:self]; // 显示后面圆,后面圆的半径,随着两个圆心的距离不断增加而减小 // 计算圆心距离 CGFloat d = [self circleCenterDistanceWithBigCircleCenter:self.center smallCircleCenter:self.smallCircleView.center]; // 计算小圆的半径 CGFloat smallRadius = self.oriSmallRadius - d / 10; // 设置小圆的尺寸 self.smallCircleView.bounds = CGRectMake(0, 0, smallRadius * 2, smallRadius * 2); self.smallCircleView.layer.cornerRadius = smallRadius; // 当圆心距离大于最大圆心距离 if (d > MAX_DISTANCE) { // 可以拖出来 // 隐藏小圆 self.smallCircleView.hidden = YES; // 移除不规则的矩形 [self.shapeLayer removeFromSuperlayer]; self.shapeLayer = nil; } else if (d > 0 && self.smallCircleView.hidden == NO) { // 有圆心距离,并且圆心距离不大,才需要展示 // 展示不规则矩形,通过不规则矩形路径生成一个图层 self.shapeLayer.path = [self pathWithBigCirCleView:self smallCirCleView:self.smallCircleView].CGPath; } // 手指抬起的时候 if (pan.state == UIGestureRecognizerStateEnded) { // 当圆心距离大于最大圆心距离 if (d > MAX_DISTANCE) { // 展示 gif 动画 UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds]; NSMutableArray *arrM = [NSMutableArray array]; for (int i = 1; i < 9; i++) { UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%d",i]]; [arrM addObject:image]; } imageView.animationImages = arrM; imageView.animationRepeatCount = 1; imageView.animationDuration = 0.5; [imageView startAnimating]; [self addSubview:imageView]; self.backgroundColor = [UIColor clearColor]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self removeFromSuperview]; }); } else { // 移除不规则矩形,还原 [self.shapeLayer removeFromSuperlayer]; self.shapeLayer = nil; // 还原位置 [UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{ // 设置大圆中心点位置 self.center = self.smallCircleView.center; } completion:^(BOOL finished) { // 显示小圆 self.smallCircleView.hidden = NO; }]; } } } /// 计算两个圆心之间的距离 - (CGFloat)circleCenterDistanceWithBigCircleCenter:(CGPoint)bigCircleCenter smallCircleCenter:(CGPoint)smallCircleCenter { CGFloat offsetX = bigCircleCenter.x - smallCircleCenter.x; CGFloat offsetY = bigCircleCenter.y - smallCircleCenter.y; return sqrt(offsetX * offsetX + offsetY * offsetY); } /// 描述两圆之间一条矩形路径 - (UIBezierPath *)pathWithBigCirCleView:(UIView *)bigCirCleView smallCirCleView:(UIView *)smallCirCleView { CGPoint bigCenter = bigCirCleView.center; CGFloat x2 = bigCenter.x; CGFloat y2 = bigCenter.y; CGFloat r2 = bigCirCleView.bounds.size.width / 2; CGPoint smallCenter = smallCirCleView.center; CGFloat x1 = smallCenter.x; CGFloat y1 = smallCenter.y; CGFloat r1 = smallCirCleView.bounds.size.width / 2; // 获取圆心距离 CGFloat d = [self circleCenterDistanceWithBigCircleCenter:bigCenter smallCircleCenter:smallCenter]; CGFloat sinθ = (x2 - x1) / d; CGFloat cosθ = (y2 - y1) / d; // 坐标系基于父控件 CGPoint pointA = CGPointMake(x1 - r1 * cosθ , y1 + r1 * sinθ); CGPoint pointB = CGPointMake(x1 + r1 * cosθ , y1 - r1 * sinθ); CGPoint pointC = CGPointMake(x2 + r2 * cosθ , y2 - r2 * sinθ); CGPoint pointD = CGPointMake(x2 - r2 * cosθ , y2 + r2 * sinθ); CGPoint pointO = CGPointMake(pointA.x + d / 2 * sinθ , pointA.y + d / 2 * cosθ); CGPoint pointP = CGPointMake(pointB.x + d / 2 * sinθ , pointB.y + d / 2 * cosθ); UIBezierPath *path = [UIBezierPath bezierPath]; // A [path moveToPoint:pointA]; // AB [path addLineToPoint:pointB]; // 绘制 BC 曲线 [path addQuadCurveToPoint:pointC controlPoint:pointP]; // CD [path addLineToPoint:pointD]; // 绘制 DA 曲线 [path addQuadCurveToPoint:pointA controlPoint:pointO]; return path; } @end ViewController.m // 创建粘性按钮 QGooButton *gooButton = [[QGooButton alloc] initWithFrame:CGRectMake(200, 200, 50, 50)]; [gooButton setBackgroundColor:[UIColor redColor]]; [gooButton setTitle:@"10" forState:UIControlStateNormal]; [self.view addSubview:gooButton]; 效果 1.10 启动动画 QWelcomeView.h @interface QWelcomeView : UIView /// 创建欢迎视图 + (instancetype)q_weicomeView; @end QWelcomeView.m @interface QWelcomeView () @property (weak, nonatomic) IBOutlet UIImageView *backView; @property (weak, nonatomic) IBOutlet UIImageView *sloganView; @property (weak, nonatomic) IBOutlet UIImageView *iconView; @property (weak, nonatomic) IBOutlet UILabel *textView; @end @implementation QWelcomeView /// 创建欢迎视图 + (instancetype)q_weicomeView{ return [[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self) owner:nil options:nil].firstObject; } /// 初始化 - (void)awakeFromNib { [super awakeFromNib]; self.backView.image = [UIImage imageNamed:@"ad_background"]; self.sloganView.image = [UIImage imageNamed:@"compose_slogan"]; self.iconView.image = [UIImage imageNamed:@"QianChia0123"]; self.textView.text = @"欢迎回来"; } /// 已经添加到父视图上 - (void)didMoveToSuperview { [super didMoveToSuperview]; // 设置头像圆角 self.iconView.layer.cornerRadius = 50; self.iconView.layer.masksToBounds = YES; // 头像下移 self.iconView.transform = CGAffineTransformMakeTranslation(0, 50); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [UIView animateWithDuration:1.0 animations:^{ // 文字图片慢慢消失 self.sloganView.alpha = 0; } completion:^(BOOL finished) { // 显示头像 self.iconView.hidden = NO; [UIView animateWithDuration:1.0 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:0 options:UIViewAnimationOptionCurveLinear animations:^{ // 头像往上移动的动画,弹簧效果 self.iconView.transform = CGAffineTransformIdentity; } completion:^(BOOL finished) { self.textView.alpha = 0; self.textView.hidden = NO; // 文字慢慢显示 [UIView animateWithDuration:0.5 animations:^{ // 欢迎回来 的文字慢慢显示 self.textView.alpha = 1; } completion:^(BOOL finished) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 移除 [self removeFromSuperview]; }); }]; }]; }]; }); } @end AppDelegate.m #import "QWelcomeView.h" - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 创建窗口 self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIStoryboard *stroyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; UIViewController *vc = [stroyboard instantiateInitialViewController]; self.window.rootViewController = vc; [self.window makeKeyAndVisible]; // 显示欢迎界面 QWelcomeView *welcomeV = [QWelcomeView q_weicomeView]; welcomeV.frame = self.window.bounds; // 注意:一定要给界面设置 Frame [self.window addSubview:welcomeV]; return YES; } 效果 1.11 菜单按钮动画 QMenuItemModel.h @interface QMenuItemModel : NSObject @property (nonatomic, strong) NSString *title; @property (nonatomic, strong) UIImage *image; + (instancetype)q_menuItemWithTitle:(NSString *)title image:(UIImage *)image; @end QMenuItemModel.m @implementation QMenuItemModel + (instancetype)q_menuItemWithTitle:(NSString *)title image:(UIImage *)image { QMenuItemModel *itme = [[self alloc] init]; itme.title = title; itme.image = image; return itme; } @end QMenuItemButton.h @interface QMenuItemButton : UIButton @end QMenuItemButton.m #define kImageRatio 0.8 @implementation QMenuItemButton - (void)awakeFromNib { [super awakeFromNib]; [self setUp]; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setUp]; } return self; } - (void)setUp { self.imageView.contentMode = UIViewContentModeCenter; self.titleLabel.textAlignment = NSTextAlignmentCenter; [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; } /// 布局子控件 - (void)layoutSubviews { [super layoutSubviews]; // UIImageView CGFloat imageX = 0; CGFloat imageY = 0; CGFloat imageW = self.bounds.size.width; CGFloat imageH = self.bounds.size.height * kImageRatio; self.imageView.frame = CGRectMake(imageX, imageY, imageW, imageH); // UILabel CGFloat labelY = imageH; CGFloat labelH = self.bounds.size.height - labelY; self.titleLabel.frame = CGRectMake(imageX, labelY, imageW, labelH); } @end QComposeItemViewController.h @interface QComposeItemViewController : UIViewController @end QComposeItemViewController.m #import "QMenuItemModel.h" #import "QMenuItemButton.h" @interface QComposeItemViewController () @property (nonatomic, strong) NSArray *items; @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, assign) int btnIndex; @property (nonatomic, strong) NSMutableArray *itemButtons; @end @implementation QComposeItemViewController /// - (void)viewDidLoad { [super viewDidLoad]; // 添加所有 item 按钮 [self setUpAllBtns]; // 按钮按顺序的从下往上偏移 self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timeChange) userInfo:nil repeats:YES]; } /// 添加所有 item 按钮 - (void)setUpAllBtns { int cols = 3; int col = 0; int row = 0; CGFloat x = 0; CGFloat y = 0; CGFloat wh = 100; CGFloat margin = ([UIScreen mainScreen].bounds.size.width - cols * wh) / (cols + 1); CGFloat oriY = 300; for (int i = 0; i < self.items.count; i++) { UIButton *btn = [QMenuItemButton buttonWithType:UIButtonTypeCustom]; col = i % cols; row = i / cols; x = margin + col * (margin + wh); y = row * (margin + wh) + oriY; btn.frame = CGRectMake(x, y, wh, wh); // 设置按钮的图片和文字 QMenuItemModel *item = self.items[i]; [btn setImage:item.image forState:UIControlStateNormal]; [btn setTitle:item.title forState:UIControlStateNormal]; // 偏移到底部 btn.transform = CGAffineTransformMakeTranslation(0, self.view.bounds.size.height); [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchDown]; [btn addTarget:self action:@selector(btnClick1:) forControlEvents:UIControlEventTouchUpInside]; [self.itemButtons addObject:btn]; [self.view addSubview:btn]; } } /// 给所有的按钮做动画 - (void)setUpAllBtnAnim { for (UIButton *btn in self.itemButtons) { [self setUpOneBtnAnim:btn]; } } /// 给一个按钮做动画 - (void)setUpOneBtnAnim:(UIButton *)btn { [UIView animateWithDuration:0.8 delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseIn animations:^{ btn.transform = CGAffineTransformIdentity; } completion:nil]; } /// 定时器响应事件处理 - (void)timeChange { // 让一个按钮做动画 if (self.btnIndex == self.itemButtons.count) { // 定时器停止 [self.timer invalidate]; return; } // 获取按钮 UIButton *btn = self.itemButtons[self.btnIndex]; [self setUpOneBtnAnim:btn]; self.btnIndex++; } /// 按钮按下响应事件处理 - (void)btnClick:(UIButton *)btn { [UIView animateWithDuration:0.5 animations:^{ btn.transform = CGAffineTransformMakeScale(1.2, 1.2); }]; } /// 按钮点击响应事件处理 - (void)btnClick1:(UIButton *)btn { [UIView animateWithDuration:0.5 animations:^{ btn.transform = CGAffineTransformMakeScale(2.0, 2.0); btn.alpha = 0; }]; NSLog(@"%s", __func__); } /// 懒加载 - (NSArray *)items { if (_items == nil) { // 创建模型对象 QMenuItemModel *item1 = [QMenuItemModel q_menuItemWithTitle:@"点评" image:[UIImage imageNamed:@"tabbar_compose_review"]]; QMenuItemModel *item2 = [QMenuItemModel q_menuItemWithTitle:@"更多" image:[UIImage imageNamed:@"tabbar_compose_more"]]; QMenuItemModel *item3 = [QMenuItemModel q_menuItemWithTitle:@"拍摄" image:[UIImage imageNamed:@"tabbar_compose_camera"]]; QMenuItemModel *item4 = [QMenuItemModel q_menuItemWithTitle:@"相册" image:[UIImage imageNamed:@"tabbar_compose_photo"]]; QMenuItemModel *item5 = [QMenuItemModel q_menuItemWithTitle:@"文字" image:[UIImage imageNamed:@"tabbar_compose_idea"]]; QMenuItemModel *item6 = [QMenuItemModel q_menuItemWithTitle:@"签到" image:[UIImage imageNamed:@"tabbar_compose_review"]]; _items = @[item1, item2, item3, item4, item5, item6]; } return _items; } - (NSMutableArray *)itemButtons { if (_itemButtons == nil) { _itemButtons = [NSMutableArray array]; } return _itemButtons; } @end ViewController.m // 点击加号按钮 - (IBAction)btnClick:(id)sender { QComposeItemViewController *vc = [[QComposeItemViewController alloc] init]; [self presentViewController:vc animated:YES completion:nil]; } 效果 1.12 引导页动画 ViewController.m @interface ViewController () <UIScrollViewDelegate> @property (weak, nonatomic) IBOutlet UIImageView *sunView; @property (weak, nonatomic) IBOutlet UIImageView *personView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; UIImage *bgImage = [UIImage imageNamed:@"520_userguid_bg"]; // ScrollView UIScrollView *scorollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; scorollView.contentSize = bgImage.size; scorollView.delegate = self; scorollView.decelerationRate = 0.5; [self.view insertSubview:scorollView atIndex:0]; // bg UIImageView *bgImageView = [[UIImageView alloc] initWithImage:bgImage]; CGRect rect = bgImageView.frame; rect.size.height = self.view.bounds.size.height; bgImageView.frame = rect; [scorollView addSubview:bgImageView]; // cg UIImage *cgImage = [UIImage imageNamed:@"520_userguid_cg"]; UIImageView *cgImageView = [[UIImageView alloc] initWithImage:cgImage]; rect = cgImageView.frame; rect.size.height = self.view.bounds.size.height; cgImageView.frame = rect; [bgImageView addSubview:cgImageView]; // fg UIImage *fgImage = [UIImage imageNamed:@"520_userguid_fg"]; UIImageView *fgImageView = [[UIImageView alloc] initWithImage:fgImage]; rect = cgImageView.frame; rect.size.height = self.view.bounds.size.height; fgImageView.frame = rect; [bgImageView addSubview:fgImageView]; // cloud UIImage *cloudImage = [UIImage imageNamed:@"520_userguid_cloud"]; UIImageView *cloudImageView = [[UIImageView alloc] initWithImage:cloudImage]; [bgImageView addSubview:cloudImageView]; } // 只要一滚动就会调用 - (void)scrollViewDidScroll:(UIScrollView *)scrollView { // 获取 scrollView 偏移量 CGFloat offsetX = scrollView.contentOffset.x; int intOffsetX = (int)offsetX; // 切换人物的图片 NSString *imageName = [NSString stringWithFormat:@"520_userguid_person_taitou_%d", (intOffsetX % 2 + 1)]; self.personView.image = [UIImage imageNamed:imageName]; // 旋转小太阳 self.sunView.transform = CGAffineTransformRotate(self.sunView.transform, 5 / 180.0 * M_PI); } @end 效果
1、CALayer 绘图层 在 iOS 系统中,你能看得见摸得着的东西基本上都是 UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是 UIView。其实 UIView 之所以能显示在屏幕上,完全是因为它内部的一个层。在创建 UIView 对象时,UIView 内部会自动创建一个层(即 CALayer 对象),通过 UIView 的 layer 属性可以访问这个层。当 UIView 需要显示到屏幕上时,会调用 drawRect: 方法进行绘图,并且会将所有内容绘制在自己的层上,绘图完毕后,系统会将层拷贝到屏幕上,于是就完成了 UIView 的显示。换句话说,UIView 本身不具备显示的功能,是它内部的层才有显示功能。 CALayer 的简单使用 CALayer 是被定义在 QuartzCore 框架中的,通过操作 CALayer 对象,可以很方便地调整 UIView 的一些界面属性,比如:阴影、圆角大小、边框宽度和颜色等。 2、基本绘图层属性设置 1、设置阴影 self.redView.layer.shadowOpacity = 1; // 阴影不透明度 self.redView.layer.shadowOffset = CGSizeMake(10, 10); // 阴影偏移量 self.redView.layer.shadowColor = [UIColor yellowColor].CGColor; // 阴影颜色 self.redView.layer.shadowRadius = 10; // 阴影圆角半径 2、圆角半径 self.redView.layer.cornerRadius = 75; // 主层半径 3、边框 self.redView.layer.borderWidth = 3; // 边框宽度 self.redView.layer.borderColor = [UIColor blueColor].CGColor; // 边框颜色 4、imageView 圆角半径设置 self.imageView.layer.cornerRadius = 75; // 主层半径 // 超出主层边框的内容全部裁剪掉,image 在视图层上 self.imageView.layer.masksToBounds = YES; // 是否对非主层裁剪 效果 3、形变属性设置 3.1 视图形变 1、单一形变 // 旋转 /* (CGFloat angle) 旋转 45 度,需要输入的参数为弧度,45/180 * M_PI,1 度 = PI/180 弧度 */ [UIView animateWithDuration:1 animations:^{ self.redView.transform = CGAffineTransformMakeRotation(0.25 * M_PI); }]; // 缩放 /* (CGFloat sx, CGFloat sy) (1, 2) 宽度和高度的放大倍数 */ [UIView animateWithDuration:1 animations:^{ self.redView.transform = CGAffineTransformMakeScale(1, 2); }]; // 平移 /* (CGFloat tx, CGFloat ty) (100, 100) 水平和垂直方向的移动距离 */ [UIView animateWithDuration:1 animations:^{ self.redView.transform = CGAffineTransformMakeTranslation(100, 100); }]; 效果 2、 叠加形变 // 旋转 + 缩放 [UIView animateWithDuration:1 animations:^{ CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(0.25 * M_PI); self.redView.transform = CGAffineTransformScale(rotationTransform, 1.5, 1.5); }]; // 旋转 + 平移 [UIView animateWithDuration:1 animations:^{ CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(0.25 * M_PI); self.redView.transform = CGAffineTransformTranslate(rotationTransform, 100, 0); }]; // 缩放 + 平移 [UIView animateWithDuration:1 animations:^{ CGAffineTransform scaleTransform = CGAffineTransformMakeScale(1, 1.5); self.redView.transform = CGAffineTransformTranslate(scaleTransform, 100, 0); }]; // 旋转 + 缩放 + 平移 [UIView animateWithDuration:1 animations:^{ CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(0.25 * M_PI); CGAffineTransform rotationScaleTransform = CGAffineTransformScale(rotationTransform, 1.5, 1.5); self.redView.transform = CGAffineTransformTranslate(rotationScaleTransform, 100, 0); }]; 效果 3、累加形变 // 连续旋转 [UIView animateWithDuration:1 animations:^{ self.redView.transform = CGAffineTransformRotate(self.redView.transform, 0.25 * M_PI); }]; // 连续缩放 [UIView animateWithDuration:1 animations:^{ self.redView.transform = CGAffineTransformScale(self.redView.transform, 1.5, 1.5); }]; // 连续平移 [UIView animateWithDuration:1 animations:^{ self.redView.transform = CGAffineTransformTranslate(self.redView.transform, 50, 50); }]; 效果 4、还原形变 // 还原所有形变 self.redView.transform = CGAffineTransformIdentity; 3.2 绘图层形变 1、单一形变 // 旋转 /* (CGFloat angle) 旋转 45 度,需要输入的参数为弧度,45/180 * M_PI,1 度 = PI/180 弧度 */ [UIView animateWithDuration:1 animations:^{ self.redView.layer.affineTransform = CGAffineTransformMakeRotation(0.25 * M_PI); }]; // 缩放 /* (CGFloat sx, CGFloat sy) (1, 2) 宽度和高度的放大倍数 */ [UIView animateWithDuration:1 animations:^{ self.redView.layer.affineTransform = CGAffineTransformMakeScale(1, 2); }]; // 平移 /* (CGFloat tx, CGFloat ty) (100, 100) 水平和垂直方向的移动距离 */ [UIView animateWithDuration:1 animations:^{ self.redView.layer.affineTransform = CGAffineTransformMakeTranslation(100, 100); }]; // 还原所有形变 self.redView.layer.affineTransform = CGAffineTransformIdentity; 效果 2、快速进行绘图层形变,KVC // 旋转 [UIView animateWithDuration:1 animations:^{ [self.redView.layer setValue:@(0.25 * M_PI) forKeyPath:@"transform.rotation"]; }]; // 缩放 [UIView animateWithDuration:1 animations:^{ [self.redView.layer setValue:@1.5 forKeyPath:@"transform.scale"]; }]; 效果 3、绘图层 3D 形变 // 旋转 /* (CGFloat angle, CGFloat x, CGFloat y, CGFloat z) 旋转角度,x y z 轴的坐标,为 0 时在此轴上不旋转 */ [UIView animateWithDuration:1 animations:^{ self.imageView.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0); }]; // 缩放 /* (CGFloat sx, CGFloat sy, CGFloat sz),x y z 轴的缩放倍数 */ [UIView animateWithDuration:1 animations:^{ self.imageView.layer.transform = CATransform3DMakeScale(0.5, 0.5, 1); }]; // 平移 /* (CGFloat tx, CGFloat ty, CGFloat tz),x y z 轴的平移量 */ [UIView animateWithDuration:1 animations:^{ self.imageView.layer.transform = CATransform3DMakeTranslation(100, 100, 0); }]; 效果 3.3 获取形变值 获取旋转的角度 // 根据 transform 获取旋转角度 CGFloat angle = atan2(self.redView.transform.b, self.redView.transform.a); 4、创建新的绘图层 UIView 内部默认有个 CALayer 对象(层),通过 layer 属性可以访问这个层。要注意的是,这个默认的层不允许重新创建,但可以往层里面添加子层。UIView 可以通过 addSubview: 方法添加子视图,类似地,CALayer 可以通过 addSublayer: 方法添加子层。 1、添加一个简单的图层 // 创建图层 CALayer *myLayer = [CALayer layer]; myLayer.frame = CGRectMake(100, 100, 200, 200); myLayer.backgroundColor = [UIColor redColor].CGColor; [self.view.layer addSublayer:myLayer]; 效果 2、添加一个显示图片的图层 // 创建图层 CALayer *myLayer = [CALayer layer]; myLayer.frame = CGRectMake(100, 100, 200, 200); myLayer.backgroundColor = [UIColor redColor].CGColor; // 设置图层内容 myLayer.contents = (id)[UIImage imageNamed:@"demo2.jpg"].CGImage; [self.view.layer addSublayer:myLayer]; 效果 3、为什么 CALayer 中使用 CGColorRef 和 CGImageRef 这 2 种数据类型,而不用 UIColor 和 UIImage ? 首先要知道:CALayer 是定义在 QuartzCore 框架中的;CGImageRef、CGColorRef 两种数据类型是定义在 CoreGraphics 框架中的;UIColor、UIImage 是定义在 UIKit 框架中的。 其次,QuartzCore 框架和 CoreGraphics 框架是可以跨平台使用的,在 iOS 和 Mac OS X 上都能使用,但是 UIKit 只能在 iOS 中使用。 因此,为了保证可移植性,QuartzCore 不能使用 UIImage、UIColor,只能使用 CGImageRef、CGColorRef。 不过很多情况下,可以通过 UIKit 对象的特定方法,得到 CoreGraphics 对象,比如 UIImage 的 CGImage 方法可以返回一个 CGImageRef。 4、UIView 和 CALayer 的选择 细心的朋友不难发现,其实前面的 2 个效果不仅可以通过添加层来实现,还可以通过添加 UIView 来实现。比如,第 1 个红色的层可以用一个 UIView 来实现,第 2 个显示图片的层可以用一个 UIImageView 来实现。既然 CALayer 和 UIView 都能实现相同的显示效果,那究竟该选择谁好呢? 其实,对比 CALayer,UIView 多了一个事件处理的功能。也就是说,CALayer 不能处理用户的触摸事件,而 UIView 可以。 所以,如果显示出来的东西需要跟用户进行交互的话,用 UIView;如果不需要跟用户进行交互,用 UIView 或者 CALayer 都可以。 当然,CALayer 的性能会高一些,因为它少了事件处理的功能,更加轻量级。 5、UIView 和 CALayer 的关系 UIView 可以通过 subviews 属性访问所有的子视图,类似地,CALayer 也可以通过 sublayers 属性访问所有的子层。 UIView 可以通过 superview 属性访问父视图,类似地,CALayer 也可以通过 superlayer 属性访问父层。 下面再看一张 UIView 和 CALayer 的关系图,如果两个 UIView 是父子关系,那么它们内部的 CALayer 也是父子关系。 5、绘图层隐式动画属性 在前面已经提到,每一个 UIView 内部都默认关联着一个 CALayer,我们可称这个 Layer 为 Root Layer(根层)。所有的非 Root Layer,也就是手动创建的 CALayer 对象,都存在着隐式动画。 当对非 Root Layer 的部分属性进行相应的修改时,默认会自动产生一些动画效果,这些属性称为 Animatable Properties 可动画属性。 列举几个常见的 Animatable Properties: bounds :用于设置 CALayer 的宽度和高度。修改这个属性会产生缩放动画。 backgroundColor :用于设置 CALayer 的背景色。修改这个属性会产生背景色的渐变动画。 position :用于设置 CALayer 的位置。修改这个属性会产生平移动画。比如:假设 一开始 CALayer 的 position 为(100, 100),然后在某个时刻修改为 (200, 200),那么整个 CALayer 就会在短时间内从 (100, 100) 这个 位置平移到 (200, 200) 1、隐式动画属性设置 self.myLayer.bounds = CGRectMake(0, 0, 100, 100); self.myLayer.backgroundColor = [UIColor greenColor].CGColor; self.myLayer.position = CGPointMake(arc4random_uniform(200) + 20, arc4random_uniform(400) + 50); self.myLayer.transform = CATransform3DMakeRotation(arc4random_uniform(360), 0, 0, 1); 效果 2、可以通过动画事务(CATransaction)关闭默认的隐式动画效果。 [CATransaction begin]; [CATransaction setDisableActions:YES]; self.myLayer.position = CGPointMake(10, 10); [CATransaction commit]; 6、绘图层 position 和 anchorPoint 属性 position 和 anchorPoint 属性都是 CGPoint 类型的。 position :位置,可以用来设置 CALayer 在父层中的位置,它是以父层的左上角为坐标原点(0, 0)。 anchorPoint :锚点,称为 "定位点",它决定着 CALayer 身上的哪个点会在 position 属性所指的位置。 它的 x、y 取值范围都是 0~1,默认值为 (0.5, 0.5)。 1、anchorPoint 为默认值(0.5, 0.5) CALayer *myLayer = [CALayer layer]; myLayer.backgroundColor = [UIColor redColor].CGColor; myLayer.bounds = CGRectMake(0, 0, 100, 100); // 设置层的位置 myLayer.position = CGPointMake(100, 100); [self.view.layer addSublayer:myLayer]; 设置了 myLayer 的 position 为(100, 100),又因为 anchorPoint 默认是(0.5, 0.5),所以最后的效果是 myLayer 的中点会在父层的(100, 100)位置。 2、anchorPoint 为(0, 0) 若将 anchorPoint 改为(0, 0),myLayer 的左上角会在(100, 100)位置。 myLayer.anchorPoint = CGPointMake(0, 0); 3、anchorPoint 为(1, 1) 若将 anchorPoint 改为(1, 1),myLayer 的右下角会在(100, 100)位置。 myLayer.anchorPoint = CGPointMake(1, 1); 4、anchorPoint 为(0, 1) 将 anchorPoint 改为(0, 1),myLayer 的左下角会在(100, 100)位置。 myLayer.anchorPoint = CGPointMake(0, 1); 7、自定义绘图层 7.1 自定义绘图层方法 1 创建一个 CALayer 的子类,然后覆盖 drawInContext: 方法,使用 Quartz2D API 进行绘图。 QCLayer.h @interface QCLayer : CALayer @end QCLayer.m @implementation QCLayer #pragma mark 绘制一个实心三角形 - (void)drawInContext:(CGContextRef)ctx { // 设置为蓝色 CGContextSetRGBFillColor(ctx, 0, 0, 1, 1); // 设置起点 CGContextMoveToPoint(ctx, 50, 0); // 从 (50, 0) 连线到 (0, 100) CGContextAddLineToPoint(ctx, 0, 100); // 从 (0, 100) 连线到 (100, 100) CGContextAddLineToPoint(ctx, 100, 100); // 合并路径,连接起点和终点 CGContextClosePath(ctx); // 绘制路径 CGContextFillPath(ctx); } @end ViewController.m QCLayer *layer = [QCLayer layer]; // 设置层的宽高 layer.bounds = CGRectMake(0, 0, 100, 100); // 设置层的位置 layer.position = CGPointMake(100, 100); // 开始绘制图层,需要调用这个方法,才会触发 drawInContext: 方法的调用,然后进行绘图 [layer setNeedsDisplay]; [self.view.layer addSublayer:layer]; 效果 7.2 自定义绘图层方法 2 设置 CALayer 的 delegate,然后让 delegate 实现 drawLayer:inContext: 方法,当 CALayer 需要绘图时,会调用 delegate 的 drawLayer:inContext: 方法进行绘图。 这里要注意的是:不能再将某个 UIView 设置为 CALayer 的 delegate,因为 UIView 对象已经是它内部根层的 delegate,再次设置为其他层的 delegate 就会出问题。UIView 和它内部 CALayer 的默认关系图: 创建新的层,设置 delegate,然后添加到控制器的 view 的 layer 中。 CALayer *layer = [CALayer layer]; // 设置 delegate,这里的 self 是指控制器 layer.delegate = self; // 设置层的宽高 layer.bounds = CGRectMake(0, 0, 100, 100); // 设置层的位置 layer.position = CGPointMake(100, 100); // 开始绘制图层,需要调用这个方法,才会通知 delegate 进行绘图 [layer setNeedsDisplay]; [self.view.layer addSublayer:layer]; 让 CALayer 的 delegate(前面设置的是控制器)实现 drawLayer:inContext: 方法 #pragma mark 画一个矩形框 - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { // 设置蓝色 CGContextSetRGBStrokeColor(ctx, 0, 0, 1, 1); // 设置边框宽度 CGContextSetLineWidth(ctx, 10); // 添加一个跟层一样大的矩形到路径中 CGContextAddRect(ctx, layer.bounds); // 绘制路径 CGContextStrokePath(ctx); } 效果 7.3 UIView 的详细显示过程 当 UIView 需要显示时,它内部的层会准备好一个 CGContextRef(图形上下文),然后调用 delegate(这里就是 UIView)的 drawLayer:inContext: 方法,并且传入已经准备好的 CGContextRef 对象。而 UIView 在 drawLayer:inContext: 方法中又会调用自己的 drawRect: 方法 平时在 drawRect: 中通过 UIGraphicsGetCurrentContext() 获取的就是由层传入的 CGContextRef 对象,在 drawRect: 中完成的所有绘图都会填入层的 CGContextRef 中,然后被拷贝至屏幕。 8、渐变图层 渐变图层 CAGradientLayer : CALayer 添加渐变图层 CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = self.imageView.bounds; // 设置透明度 gradientLayer.opacity = 0.5; // 设置渐变颜色 gradientLayer.colors = @[(id)[UIColor clearColor].CGColor, (id)[UIColor blackColor].CGColor]; [self.imageView.layer addSublayer:gradientLayer]; CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = self.imageView.bounds; // 设置透明度 gradientLayer.opacity = 0.5; // 设置渐变颜色 gradientLayer.colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor greenColor].CGColor, (id)[UIColor yellowColor].CGColor]; // 设置渐变定位点 gradientLayer.locations = @[@0.1, @0.4, @0.8]; // 设置渐变开始点,取值 0~1 gradientLayer.startPoint = CGPointMake(0, 1); [self.imageView.layer addSublayer:gradientLayer]; 效果 9、复制图层 复制图层 CAReplicatorLayer : CALayer,可以把图层里面所有子层复制 添加复制图层 CAReplicatorLayer *repLayer = [CAReplicatorLayer layer]; repLayer.frame = self.view.bounds; [self.view.layer addSublayer:repLayer]; // 添加子层 [repLayer addSublayer:self.imageView.layer]; // 设置有多少个子层,包括原始层 repLayer.instanceCount = 4; // 设置子层偏移量,不包括原始层,相对于原始层 x 偏移 repLayer.instanceTransform = CATransform3DMakeTranslation(70, 0, 0); // 设置子层背景色 repLayer.instanceColor = [UIColor greenColor].CGColor; // 设置子层阴影 repLayer.instanceAlphaOffset = -0.1; repLayer.instanceRedOffset = -0.1; repLayer.instanceGreenOffset = -0.1; repLayer.instanceBlueOffset = -0.1; // 设置子层动画延迟时间,子层有动画时有效 repLayer.instanceDelay = 0; CAReplicatorLayer *repLayer = [CAReplicatorLayer layer]; repLayer.frame = self.view.bounds; [self.view.layer addSublayer:repLayer]; // 添加子层 CALayer *layer = [CALayer layer]; layer.anchorPoint = CGPointMake(0.5, 1); layer.position = CGPointMake(100, 300); layer.bounds = CGRectMake(0, 0, 30, 150); layer.backgroundColor = [UIColor whiteColor].CGColor; [repLayer addSublayer:layer]; // 添加子层动画 CABasicAnimation *anim = [CABasicAnimation animation]; anim.keyPath = @"transform.scale.y"; anim.toValue = @0.1; anim.duration = 0.5; anim.repeatCount = MAXFLOAT; anim.autoreverses = YES; [layer addAnimation:anim forKey:nil]; // 设置子层 repLayer.instanceCount = 4; repLayer.instanceTransform = CATransform3DMakeTranslation(45, 0, 0); repLayer.instanceDelay = 0.1; repLayer.instanceColor = [UIColor greenColor].CGColor; repLayer.instanceGreenOffset = -0.3; 效果
1、UIView 动画 核心动画 和 UIView 动画 的区别: 核心动画一切都是假象,并不会真实的改变图层的属性值,如果以后做动画的时候,不需要与用户交互,通常用核心动画(转场)。 UIView 动画必须通过修改属性的真实值,才有动画效果。 1.1 block 方式 设置控件位置、尺寸、透明度等的代码,放在 animateWithDuration: block 中,将自动以动画的方式改变。 // 开始动画,动画持续时间 2 秒 [UIView animateWithDuration:1.0 animations:^{ // 设置动画结束后的效果值 // 改变控件的位置和尺寸,改变后的位置或大小 self.redView.frame = CGRectMake(150, 50, 50, 50); } completion:^(BOOL finished) { // 动画完成后的操作 // 开始一个新的动画 [UIView animateWithDuration:1.0 animations:^{ // 改变控件的位置和尺寸,改变后的位置或大小 self.redView.frame = CGRectMake(50, 150, 80, 80); }]; }]; 效果 弹簧效果的动画 [UIView animateWithDuration:1.0 delay:0 usingSpringWithDamping:0.2 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ // SpringWithDamping: 弹性系数,越小弹簧效果越明显 self.redView.frame = CGRectMake(150, 50, 50, 50); } completion:nil]; 效果 1.2 动画块方式 设置控件位置、尺寸、透明度等的代码,放在 beginAnimations: 和 commitAnimations 之间,将自动以动画的方式改变。 // 开始一个动画块 [UIView beginAnimations:nil context:nil]; // 动画设置 // 设置动画时间 [UIView setAnimationDuration:1.0]; // default = 0.2 // 设置延时 [UIView setAnimationDelay:0.0]; // 设置指定的时间后开始执行动画,default = 0.0 // 设置动画执行节奏 /* UIViewAnimationCurveEaseInOut, // slow at beginning and end 开始和结束慢速,默认 UIViewAnimationCurveEaseIn, // slow at beginning 开始慢速 UIViewAnimationCurveEaseOut, // slow at end 结束慢速 UIViewAnimationCurveLinear // 匀速 */ [UIView setAnimationCurve:UIViewAnimationCurveLinear]; // 设置重复次数 [UIView setAnimationRepeatCount:MAXFLOAT]; // default = 0.0. May be fractional // 设置是否自动返回,以动画的方式返回 [UIView setAnimationRepeatAutoreverses:YES]; // default = NO. used if repeat count is non-zero // 设置是否从当前状态开始动画 [UIView setAnimationBeginsFromCurrentState:YES]; // default = NO // 设置代理 [UIView setAnimationDelegate:self]; // default = nil // 设置动画开始时执行的代理方法,自定义方法 [UIView setAnimationWillStartSelector:@selector(startAnimations)]; // default = NULL // 设置动画结束时执行的代理方法,自定义方法 [UIView setAnimationDidStopSelector:@selector(stopAnimations)]; // default = NULL // 动画之行后效果值 // 设置透明度,改变后的透明度 self.redView.alpha = 1.0; // 改变控件的位置和尺寸,改变后的位置或大小 self.redView.frame = CGRectMake(150, 150, 80, 80); // 结束一个动画块 [UIView commitAnimations]; // 动画开始时执行的代理方法,自定义方法 - (void)startAnimations { NSLog(@"startAnimations"); } // 动画结束时执行的代理方法,自定义方法 - (void)stopAnimations { NSLog(@"stopAnimations"); } 效果 1.3 形变属性方式 具体讲解见 iOS - CALayer 绘图层:3、形变属性设置。
1、UIImageView 动画 1.1 播放图片集 播放图片集 @property (nonatomic, strong) UIImageView *playImageView; self.playImageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:self.playImageView]; // 创建图片集 NSMutableArray *imageArray = [NSMutableArray arrayWithCapacity:0]; for (int i = 1; i < 30; i++) { // 添加图片 [imageArray addObject:[UIImage imageNamed:[NSString stringWithFormat:@"%d.jpg", i]]]; } // 播放图片集 self.playImageView.animationImages = imageArray; // 设置播放的图片集(需将图片添加到数组 imageArray 中) self.playImageView.animationDuration = 29; // 设置播放整个图片集的时间 self.playImageView.animationRepeatCount = 0; // 设置循环播放次数,默认为 0 一直循环 [self.playImageView startAnimating]; // 开始播放 // [self.playImageView stopAnimating]; // 停止播放动画 效果 1.2 汤姆猫 汤姆猫 #import <AudioToolbox/AudioToolbox.h> @property (nonatomic, strong) UIImageView *playImageView; // 创建播放视图 self.playImageView = [[UIImageView alloc] initWithFrame:self.view.bounds]; self.playImageView.image = [UIImage imageNamed:@"background.jpg"]; [self.view addSubview:self.playImageView]; // 创建功能按钮 const CGFloat viewWith = self.view.bounds.size.width; const CGFloat viewHeight = self.view.bounds.size.height; const CGFloat gap = 10; const CGFloat buttonWith = self.view.bounds.size.width / 5; const CGFloat buttonHeight = buttonWith; // 功能按钮图片集 NSArray *buttonImageNameArray = @[@"fart.png", @"cymbal.png", @"drink.png", @"eat.png", @"pie.png", @"scratch.png"]; for (int i = 0; i < 11; i++) { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; [self.playImageView addSubview:button]; self.playImageView.userInteractionEnabled = YES; if (i < 6) { // 两边功能按钮的布局 if (i < 3) { button.frame = CGRectMake(gap, viewHeight / 2 + (buttonHeight + gap ) * (i % 3), buttonWith, buttonHeight); } else { button.frame = CGRectMake(viewWith - buttonWith - gap, viewHeight / 2 + (buttonHeight + gap) * (i % 3), buttonWith, buttonHeight); } [button setBackgroundImage:[UIImage imageNamed:buttonImageNameArray[i]] forState:UIControlStateNormal]; } else { // 隐藏按钮的布局 if (i == 6){ // 头 button.frame = CGRectMake(viewWith/4, viewHeight/5, viewWith/2, viewHeight/4); } else if (i == 7){ // 肚子 button.frame = CGRectMake(viewWith/3, viewHeight/3*2, viewWith/3, viewHeight/7); } else if (i == 8){ // 左脚 button.frame = CGRectMake(viewWith/4*2, viewHeight/6*5, viewWith/6, viewHeight/7); } else if (i == 9){ // 右脚 button.frame = CGRectMake(viewWith/4, viewHeight/6*5, viewWith/5, viewHeight/7); } else{ // 尾巴 button.frame = CGRectMake(viewWith/9*6, viewHeight/7*5, viewWith/7, viewHeight/5); } // button.backgroundColor = [UIColor yellowColor]; } button.tag = 100 + i; // 设置按钮事件 [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside]; } // 点击按钮事件处理 - (void)buttonClick:(UIButton *)button { switch (button.tag - 100) { case 0: // fart 放屁 [self playAnimation:@"fart"]; [self performSelector:@selector(playVoice:) withObject:@"fart" afterDelay:0.5]; break; case 1: // cymbal 敲锣 [self playAnimation:@"cymbal"]; [self performSelector:@selector(playVoice:) withObject:@"cymbal" afterDelay:0.5]; break; case 2: // drink 喝牛奶 [self playAnimation:@"drink"]; [self performSelector:@selector(playVoice:) withObject:@"drink" afterDelay:0.5]; break; case 3: // eat 吃小鸟 [self playAnimation:@"eat"]; [self performSelector:@selector(playVoice:) withObject:@"eat" afterDelay:0.5]; break; case 4: // pie 撇东西 [self playAnimation:@"pie"]; [self performSelector:@selector(playVoice:) withObject:@"pie" afterDelay:0.5]; break; case 5: // scratch 抓屏幕 [self playAnimation:@"scratch"]; [self performSelector:@selector(playVoice:) withObject:@"scratch" afterDelay:1.5]; break; case 6: // knockout 头 [self playAnimation:@"knockout"]; [self performSelector:@selector(playVoice:) withObject:@"knockout" afterDelay:0.5]; break; case 7: // stomach 肚子 [self playAnimation:@"stomach"]; [self performSelector:@selector(playVoice:) withObject:@"stomach" afterDelay:0.5]; break; case 8: // foot_left 左脚 [self playAnimation:@"foot_left"]; [self performSelector:@selector(playVoice:) withObject:@"foot_left" afterDelay:0.5]; break; case 9: // foot_right 右脚 [self playAnimation:@"foot_right"]; [self performSelector:@selector(playVoice:) withObject:@"foot_right" afterDelay:0.5]; break; case 10: // angry 尾巴 [self playAnimation:@"angry"]; [self performSelector:@selector(playVoice:) withObject:@"angry" afterDelay:0.8]; break; default: break; } } // 播放动画 - (void)playAnimation:(NSString *)key { // 读取 plist 文件获取图片数量 NSDictionary *imageNumDictionary = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"TomCat" ofType:@"plist"]]; int imageNum = [[imageNumDictionary objectForKey:key] intValue]; NSMutableArray *imageArray = [NSMutableArray arrayWithCapacity:0]; for (int i = 0; i < imageNum; i++) { [imageArray addObject:[UIImage imageNamed:[NSString stringWithFormat:@"%@_%.2d.jpg", key, i]]]; } self.playImageView.animationImages = imageArray; self.playImageView.animationDuration = imageNum/13; self.playImageView.animationRepeatCount = 1; [self.playImageView startAnimating]; // 播放动画 } // 播放声音 - (void)playVoice:(NSString *)key { // 添加声音 SystemSoundID soundID; AudioServicesCreateSystemSoundID((__bridge CFURLRef)([NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:key ofType:@"wav"]]), &soundID); AudioServicesPlayAlertSound(soundID); // 播放声音 } 效果
1、Quartz 2D 简介 Quartz 2D 属于 Core Graphics(所以大多数相关方法的都是以 CG 开头),是 iOS/Mac OSX 提供的在内核之上的强大的 2D 绘图引擎,并且这个绘图引擎是设备无关的。也就是说,不用关心设备的大小,设备的分辨率,只要利用 Quartz 2D,这些设备相关的会自动处理。 1、Quartz 2D 在 iOS 开发中的价值 绘制一些系统 UIKit 框架中不好展示的内容,例如饼图 自定义一些控件 不添加 UI 控件的情况下,使 UI 内容更丰富 绘制图形:线条\三角形\矩形\圆\弧等 绘制文字 绘制\生成图片(图像) 读取\生成 PDF 截图\裁剪图片 自定义 UI 控件 iOS 中,大部分控件都是 Quartz 2D 绘制出来的 2、Quartz 2D 提供的强大功能 透明层(transparency layers) 阴影 基于 path 的绘图(path-based drawing) 离屏渲染(offscreen rendering) 复杂的颜色处理(advanced color management) 抗锯齿渲染(anti-aliased rendering) PDF 创建,展示,解析 配合 Core Animation, OpenGL ES, UIKit 完成复杂的功能 3、画板/图形上下文 既然提到绘图,那自然有一个容器来包含绘制的结果,然后把这个结果渲染到屏幕上去,而 Quartz 2D 的容器就是 CGContextRef 数据模型,这种数据模型是 C 的结构体,存储了渲染到屏幕上需要的一切信息。 图形上下文就相当于画布,不同类型的画布就是决定着画得内容将展示在哪里。Quartz 2D 提供了以下几种类型的 Graphics Context Bitmap Graphics Context:位图上下文,在这个上下文上绘制或者渲染的内容,可以获取成图片(需要主动创建一个位图上下文来使用,使用完毕,一定要销毁)。 PDF Graphics Context Window Graphics Context Layer Graphics Context:图层上下文,针对 UI 控件的上下文。 Printer Graphics Context 4、绘制模型 Quartz 2D 采用 painter’s model,意味着每一次绘制都是一层,然后按照顺序一层层的叠加到画板上。 5、数据类型 Quartz 2D 中的数据类型都是透明的,也就是说用户只需要使用即可,不需要实际访问其中的变量。Quartz 2D 的 API 是纯 C 语言的,来自于 Core Graphics 框架,数据类型和函数基本都以 CG 作为前缀。 CGPathRef :路径类型,用来绘制路径(注意带有 ref 后缀的一般都是绘制的画板) CGImageRef:绘制 bitmap CGLayerRef:绘制 layer,layer 可复用,可离屏渲染 CGPatternRef :重复绘制 CGFunctionRef:定义回调函数,CGShadingRef 和 CGGradientRef 的辅助类型 CGShadingRef 和 CGGradientRef:绘制渐变(例如颜色渐变) CGColorRef 和 CGColorSpaceRef:定义如何处理颜色 CGFontRef:绘制文字 其他类型 6、绘制状态 在使用 Quartz 2D 进行绘图的时候,经常需要设置颜色、字体,设置 context 的坐标原点变换,context 旋转。这些影响的都是当前绘制状态。Context 中利用堆栈的方式来保存绘制状态。调用 CGContextSaveGState 来保存当前绘制状态的 copy 到堆栈中,利用 CGContextRestoreGState 弹出堆栈最顶层的绘制状态,设置为当前的绘制状态。注意,不是所有的参数都会保存,以下表格中的参数会保存。 7、Quartz 2D 的内存管理 使用含有 “Create” 或 “Copy” 的函数创建的对象,使用完后必须释放,否则将导致内存泄露。使用不含有 “Create” 或 “Copy” 的函数获取的对象,则不需要释放。 如果 retain 了一个对象,不再使用时,需要将其 release 掉,可以使用 Quartz 2D 的函数来指定 retain 和 release 一个对象。例如,如果创建了一个 CGColorSpace 对象,则使用函数CGColorSpaceRetain 和 CGColorSpaceRelease 来 retain 和 release 对象。 也可以使用 Core Foundation 的 CFRetain 和 CFRelease。注意不能传递 NULL 值给这些函数。 8、drawRect 方法 因为在 drawRect: 方法中才能取得跟 view 相关联的图形上下文,才让我们可以在 drawRect: 方法中绘制。注意:在其他地方拿不到 view 相关的上下文,所以不能实现绘制。 在 drawRect: 方法中取得上下文后,就可以绘制东西到 view 上。View 内部有个 layer(图层)属性,drawRect: 方法中取得的是一个 Layer Graphics Context,因此,绘制的东西其实是绘制到 view 的 layer 上去了。View 之所以能显示东西,完全是因为它内部的 layer。 drawRect: 方法的调用 当 view 第一次显示到屏幕上(被加到 UIWindow 上显示出来)时,系统会创建好一个跟当前 view 相关的 Layer 上下文。 系统会通过此上下文,在 drawRect: 方法中绘制好当前 view 的内容。 主动让 view 重绘内容的时候,调用 setNeedsDisplay 或者 setNeedsDisplayInRect: 方法。我们主动调用 drawRect: 方法是无效的。 调用 view 的 setNeedsDisplay 或者 setNeedsDisplayInRect: 方法后,屏幕并不是立即刷新,而是会在下一次刷新屏幕的时候把绘制的内容显示出来。 9、绘图的核心步骤 获得上下文。 绘制/拼接绘图路径。 将路径添加到上下文。 渲染上下文。 所有的绘图,都是这个步骤,即使使用贝塞尔路径,也只是对这个步骤进行了封装。对于绘图而言,拿到上下文很关键。 10、自定义 view 如何利用 Quartz 2D 绘制东西到 view 上 首先,得有图形上下文,因为它能保存绘图信息,并且决定着绘制到什么地方去。 其次,那个图形上下文必须跟view相关联,才能将内容绘制到 view 上面。 自定义 view 的步骤 新建一个类,继承自 UIView。 实现 - (void)drawRect:(CGRect)rect 方法,然后在这个方法中。 取得跟当前 view 相关联的图形上下文。 绘制相应的图形内容。 利用图形上下文将绘制的所有内容渲染显示到 view 上面。 2、Quartz 2D 基本设置 2.1 Quartz 2D 坐标系 和 UIKit 的坐标系不一样,Quartz 2D 的坐标系是在左下角的。Quartz 2D 利用坐标系的旋转位移等操作来绘制复杂的动画。 但是有两个地方的坐标系是正常的 UIKit 坐标系 UIView 的 context 通过这个方法 UIGraphicsBeginImageContextWithOptions 返回的 context。 Quartz 2D 中的圆形坐标 2.2 Stroke 描边 影响描边的因素 线的宽度 - CGContextSetLineWidth 交叉线的处理方式 - CGContextSetLineJoin 线顶端的处理方式 - CGContextSetLineCap 进一步限制交叉线的处理方式 - CGContextSetMiterLimit 是否要虚线 - Line dash pattern 颜色控件 - CGContextSetStrokeColorSpace 画笔颜色 - CGContextSetStrokeColor/CGContextSetStrokeColorWithColor 描边模式 - CGContextSetStrokePattern CGContextSetMiterLimit 如果当前交叉线绘图模式是 kCGLineJoinMiter(CGContextSetLineJoin),Quartz 2D 根据设置的 miter 值来判断线的 join 是 bevel 或者 miter。具体的模式是:将 miter 的长度除以线的宽度,如果小于设置的 mitetLimit 值,则 join style 为 bevel。 - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextMoveToPoint(ctx, 10, 10); CGContextAddLineToPoint(ctx, 50, 50); CGContextAddLineToPoint(ctx, 10, 90); CGContextSetLineWidth(ctx, 10.0); CGContextSetLineJoin(ctx, kCGLineJoinMiter); CGContextSetMiterLimit(ctx, 10.0); CGContextStrokePath(ctx); } 效果,将 Miter 设置为 1,则效果如下 2.3 Fill 填充 Quartz 2D 填充的时候会认为 subpath 是封闭的,然后根据规则来填充。有两种规则 nonzero winding number rule:沿着当前点,画一条直线到区域外,检查交叉点,如果交叉点从左到右,则加一,从右到左,则减去一。如果结果不为 0,则绘制。可参见这个 link。 even-odd rule:沿着当前点,花一条线到区域外,然后检查相交的路径,偶数则绘制,奇数则不绘制。 相关函数 CGContextEOFillPath:用 even-odd rule 来填充 CGContextFillPath :用 nonzero winding number rule 方式填充 CGContextFillRect/CGContextFillRects:填充指定矩形区域内 path CGContextFillEllipseInRect:填充椭圆 CGContextDrawPath :绘制当前 path(根据参数 stroke/fill) 2.4 Clip 切割/遮盖 顾名思义,根据 path 只绘制指定的区域,在区域外的都不会绘制。 相关函数 CGContextClip :按照 nonzero winding number rule 规则切割 CGContextEOClip:按照 even-odd 规则切割 CGContextClipToRect :切割到指定矩形 CGContextClipToRects:切割到指定矩形组 CGContextClipToMask :切割到 mask 举个例子,截取圆形区域。 - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextAddArc(ctx, 125, 125, 100, 0, M_PI * 2, true); CGContextSetFillColorWithColor(ctx, [UIColor lightGrayColor].CGColor); // 按指定的路径切割,要放在区域填充之前(下一句之前) CGContextClip(ctx); CGContextFillRect(ctx, rect); // 上面两句相当于这一句 // CGContextFillPath(ctx); } 效果,切割前后 2.5 Subpath 子路径 很简单,在 stroke/fill 或者 CGContextBeginPath/CGContextClosePath 以后就新开启一个子路径。注意 CGContextClosePath,会连接第一个点和最后一个点。 - (void)drawRect:(CGRect)rect { CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextBeginPath(ctx); CGContextAddArc(ctx, 125, 125, 100, 0, M_PI * 2, true); CGContextSetFillColorWithColor(ctx, [UIColor lightGrayColor].CGColor); CGContextClosePath(ctx); CGContextFillPath(ctx); } 2.6 Blend 混合模式 Quartz 2D 中,默认的颜色混合模式采用如下公式 result = (alpha * foreground) + (1 - alpha) * background 可以使用 CGContextSetBlendMode 来设置不同的颜色混合模式,注意设置 blend 是与 context 绘制状态相关的,一切与状态相关的设置都要想到状态堆栈。 官方文档里的例子,blend 模式较多,具体参见官方文档。 background 和 foreGround Normal Blend Mode 效果 Multiply Blend Mode 效果,交叉部分会显得比较暗,用上一层和底层相乘,至少和一层一样暗。 Screen Blend Mode 效果,交叉部分比较亮,上层的 reverse 和下层的 reverse 相乘,至少和一个一样亮。 2.7 CTM 状态矩阵 Quartz 2D 默认采用设备无关的 user space 来进行绘图,当 context(画板)建立之后,默认的坐标系原点以及方向也就确认了,可以通过 CTM(current transformation matrix)来修改坐标系的原点。从数组图像处理的角度来说,就是对当前 context state 乘以一个状态矩阵。其中的矩阵运算开发者可以不了解。 最初的状态 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextAddRect(context, CGRectMake(50, 50, 100, 50)); CGContextSetFillColorWithColor(context,[UIColor blueColor].CGColor); CGContextFillPath(context); } Translate 平移 在绘制之前,进行坐标系移动,代码中,我们是还是在(50,50)点绘制,但是要注意,当前坐标系的原点已经移了。 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // Translate CGContextTranslateCTM(context, 50, 50); CGContextAddRect(context, CGRectMake(50, 50, 100, 50)); CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor); CGContextFillPath(context); } Rotate 旋转 在 Transform 的基础上我们再 Rotate 45 度,注意 CGContextRotateCTM 传入的参数是弧度。 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // Translate CGContextTranslateCTM(context, 50, 50); // Rotate CGContextRotateCTM(context, M_PI_4); CGContextAddRect(context, CGRectMake(50, 50, 100, 50)); CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor); CGContextFillPath(context); } Scale 缩放 对于 Scale 相对来说,好理解一点,无非就是成比例放大缩小。 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // Translate CGContextTranslateCTM(context, 50, 50); // Rotate CGContextRotateCTM(context, M_PI_4); // Scale CGContextScaleCTM(context, 0.5, 0.5); CGContextAddRect(context, CGRectMake(50, 50, 100, 50)); CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor); CGContextFillPath(context); } Affine Transforms 可以通过以下方法先创建放射矩阵,然后然后再把放射矩阵映射到 CTM。 CGAffineTransform CGAffineTransformTranslate CGAffineTransformMakeRotation CGAffineTransformRotate CGAffineTransformMakeScale CGAffineTransformScale 2.8 GState 状态保存恢复 在复杂的绘图中,我们可能只是想对一个 subpath 设置,如进行旋转、移动和缩放等,这时候状态堆栈就起到作用了。 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // 保存状态,入栈 CGContextSaveGState(context); CGContextTranslateCTM(context, 50, 50); CGContextRotateCTM(context, M_PI_4); CGContextScaleCTM(context, 0.5, 0.5); CGContextAddRect(context, CGRectMake(50, 50, 100, 50)); CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor); CGContextFillPath(context); // 恢复,推出栈顶部状态 CGContextRestoreGState(context); // 这里坐标系已经回到了最开始的状态 CGContextAddRect(context, CGRectMake(0, 0, 50, 50)); CGContextFillPath(context); } - (void)drawRect:(CGRect)rect { // 描述第一条路径 UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(10, 125)]; [path addLineToPoint:CGPointMake(240, 125)]; // 获取上下文,保存上下文状态 CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSaveGState(ctx); // 设置属性绘制路径 path.lineWidth = 10; [[UIColor redColor] set]; [path stroke]; // 描述第二条路径 path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(125, 10)]; [path addLineToPoint:CGPointMake(125, 240)]; // 还原上下文状态 CGContextRestoreGState(ctx); // 绘制路径 [path stroke]; } 效果 2.9 Shadow 阴影 shadow(阴影)的目的是为了使 UI 更具有立体感。注意 Shadow 也是绘制状态相关的,意味着如果仅仅要绘制一个 subpath 的 shadow,要注意 save 和 restore 状态。 shadow 主要有三个影响因素,其中不同的 blur 效果如图。 x off-set 决定阴影沿着 x 的偏移量 y off-set 决定阴影沿着 y 的偏移量 blur value 决定了阴影的边缘区域是不是模糊的 相关函数 CGContextSetShadow CGContextSetShadowWithColor:唯一区别是设置了阴影颜色 参数 context:绘制画板 offset :阴影偏移量,参考 context 的坐标系 blur :非负数,决定阴影的模糊程度 设置阴影 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextAddArc(context, 50, 50, 100, 0, M_PI_2, 0); CGContextSetLineCap(context, kCGLineCapRound); CGContextSetLineWidth(context, 10.0); // 设置阴影 CGContextSetShadow(context, CGSizeMake(15.0, 15.0), 1.0); // CGContextSetShadowWithColor(context, CGSizeMake(15.0, 15.0), 8.0, [UIColor redColor].CGColor); CGContextStrokePath(context); } 效果 2.10 Gradient 渐变 渐变无非就是从一种颜色逐渐变换到另一种颜色,Quartz 2D 提供了两种渐变模型。通过这两种渐变的嵌套使用,Quartz 2D 能够绘制出非常漂亮的图形。 axial gradient:线性渐变,使用的时候设置好两个顶点的颜色,也可以设置中间过渡色。 radial gradient:圆形渐变,这种模式的渐变允许一个圆到另一个圆的渐变,一个点到一个圆的渐变。 可以对渐变结束或者开始的额外区域使用指定颜色填充。 渐变的两种绘制模型 CGGradient:使用这种数据类型只需要制定两个顶点的颜色,以及绘制模式,其余的 Quartz 2D 会给绘制,但是渐变的数学模型不灵活。 CGShading :使用这种数据类型需要自己定义 CFFunction 来计算每一个点的渐变颜色,较为复杂,但是能够更灵活的绘制。 1、CGGradient 绘制 创建一个 CGGradient 对象,指定颜色域(一般就是 RGB),指定颜色变化的数组,指定对应颜色位置的数组,指定每个数组数据的个数。 用 CGContextDrawLinearGradient 或者 CGContextDrawRadialGradient 绘制。 释放 CGGradient 对象。 CGGradientCreateWithColorComponents 函数 CGGradientRef __nullable CGGradientCreateWithColorComponents(CGColorSpaceRef cg_nullable space, const CGFloat * cg_nullable components, const CGFloat * __nullable locations, size_t count) 参数: space :颜色域 components:颜色变化的数组 locations :对应颜色位置的数组 count :每个数组数据的个数 线性渐变 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // 设置渐变 CGColorSpaceRef deviceRGB = CGColorSpaceCreateDeviceRGB(); CGFloat components[8] = {1.0, 0.0, 0.0, 1.0, // 红色 0.0, 1.0, 0.0, // 绿色 1.0}; CGFloat locations[2] = {0.0, 1.0}; size_t num_of_locations = 2; CGGradientRef gradient = CGGradientCreateWithColorComponents(deviceRGB, components, locations, num_of_locations); // 渐变开始结束点位置 CGPoint startPoint = CGPointMake(0, 0); CGPoint endPoint = CGPointMake(250, 250); // 创建线性渐变 CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0); CGColorSpaceRelease(deviceRGB); CGGradientRelease(gradient); } 效果 圆形渐变 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); // 设置渐变 CGColorSpaceRef deviceRGB = CGColorSpaceCreateDeviceRGB(); CGFloat components[8] = {1.0, 0.0, 0.0, 1.0, // 红色 0.0, 1.0, 0.0, // 绿色 1.0}; CGFloat locations[2] = {0.0, 1.0}; size_t num_of_locations = 2; CGGradientRef gradient = CGGradientCreateWithColorComponents(deviceRGB, components, locations, num_of_locations); // 渐变开始结束圆心位置 CGPoint startCenter = CGPointMake(80, 80); CGPoint endCenter = CGPointMake(120, 120); // 渐变开始结束半径 CGFloat startRadius = 0.0; CGFloat endRadius = 100.0; // 创建圆形渐变 CGContextDrawRadialGradient(context, gradient, startCenter, startRadius, endCenter, endRadius, 0); CGColorSpaceRelease(deviceRGB); CGGradientRelease(gradient); } 效果 2.11 Bitmap 位图 Bitmap 叫做位图,每一个像素点由 1-32bit 组成。每个像素点包括多个颜色组件和一个 Alpha 组件(例如:RGBA)。 iOS 中指出如下格式的图片 JPEG, GIF, PNG, TIF, ICO, GMP, XBM 和 CUR。其他格式的图片要给 Quartz 2D 传入图片的数据分布信息。 数据类型 CGImageRef,在 Quartz 2D 中,Bitmap 的数据由 CGImageRef 封装。由以下几个函数可以创建 CGImageRef 对象 CGImageCreate:最灵活,但也是最复杂的一种方式,要传入 11 个参数。 CGImageSourceCreate:ImageAtIndex:通过已经存在的 Image 对象来创建 CGImageSourceCreate:ThumbnailAtIndex:和上一个函数类似,不过这个是创建缩略图 CGBitmapContextCreateImage:通过 Copy Bitmap Graphics 来创建 CGImageCreateWith:ImageInRect:通过在某一个矩形内数据来创建 函数 CGImageCreate CGImageRef _Nullable CGImageCreate(size_t width, size_t height, size_t bitsPerComponent, size_t bitsPerPixel, size_t bytesPerRow, CGColorSpaceRef _Nullable space, CGBitmapInfo bitmapInfo, CGDataProviderRef _Nullable provider, const CGFloat * _Nullable decode, bool shouldInterpolate, CGColorRenderingIntent intent); 参数: width/height :图片的像素宽度,高度 bitsPerComponent:每个 component 的占用 bit 个数,和上文提到的一样 bitsPerPixel :每个像素点占用的 bit 个数。例如 32bit RGBA 中,就是 32 bytesPerRow :每一行占用的 byte 个数 colorspace :颜色空间 bitmapInfo :和上文提到的那个函数一样 provider :bitmap 的数据源 decode :解码 array,传入 null,则保持原始数据 interpolation :是否要像素差值来平滑图像 intent :指定了从一个颜色空间 map 到另一个颜色空间的方式 函数 CGBitmapContextCreate CGContextRef _Nullable CGBitmapContextCreate(void * _Nullable data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef _Nullable space, uint32_t bitmapInfo); 参数: data :是一个指针,指向存储绘制的 bitmap context 的实际数据的地址,最少大小为 bytesPerRow * height。可以传入 null,让 Quartz 自动分配计算 width, height:bitmap 的宽度,高度,以像素为单位 bytesPerRow :每一行的 byte 数目。如果 data 传入 null,这里传入 0,则会自动计算一个 component 占据多少位。对于 32bit 的 RGBA 空间,则是 8(8*4=32) space :颜色空间,一般就是 DeviceRGB bitmapInfo :一个常量,指定了是否具有 alpha 通道,alpha 通道的位置,像素点存储的数据类型是 float 还是 Integer 等信息 其中 bitmapInfo 可以传入的参数如下 enum CGImageAlphaInfo { kCGImageAlphaNone, kCGImageAlphaPremultipliedLast, kCGImageAlphaPremultipliedFirst, kCGImageAlphaLast, kCGImageAlphaFirst, kCGImageAlphaNoneSkipLast, kCGImageAlphaNoneSkipFirst, kCGImageAlphaOnly }; 1、重绘图片 原图(2560 * 1600) 重新绘制成 250 * 100,并在图片中间加上我们自定义的绘制 - (void)drawRect:(CGRect)rect { CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); // 图片绘图区域的大小 CGSize targetSize = CGSizeMake(250, 125); // 获取图形上下文 CGContextRef bitmapCtx = CGBitmapContextCreate(NULL, targetSize.width, targetSize.height, 8, targetSize.width * 4, rgb, kCGImageAlphaPremultipliedFirst); // 绘制图片 CGRect imageRect; imageRect.origin = CGPointMake(0, 0); // 设置图片的位置,左下角坐标系 imageRect.size = CGSizeMake(250, 100); // 设置图片的大小 UIImage *imageToDraw = [UIImage imageNamed:@"image.jpg"]; CGContextDrawImage(bitmapCtx, imageRect, imageToDraw.CGImage); // 绘制自定义图形 CGContextAddArc(bitmapCtx, 100, 40, 20, M_PI_4, M_PI_2, true); CGContextSetLineWidth(bitmapCtx, 4.0); CGContextSetStrokeColorWithColor(bitmapCtx, [UIColor redColor].CGColor); CGContextStrokePath(bitmapCtx); // 渲染生成 CGImage CGImageRef imageRef = CGBitmapContextCreateImage(bitmapCtx); // 转换成 UIImage UIImage *image = [[UIImage alloc] initWithCGImage:imageRef]; CGImageRelease(imageRef); CGContextRelease(bitmapCtx); CGColorSpaceRelease(rgb); UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; [self addSubview:imageView]; } 效果 2、截取图片 截取图片 - (void)drawRect:(CGRect)rect { CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); // 图片绘图区域的大小 CGSize targetSize = CGSizeMake(250, 125); // 获取图形上下文 CGContextRef bitmapCtx = CGBitmapContextCreate(NULL, targetSize.width, targetSize.height, 8, targetSize.width * 4, rgb, kCGImageAlphaPremultipliedFirst); UIImage *imageToDraw = [UIImage imageNamed:@"image.jpg"]; // 渲染生成 CGImage CGRect imageRect = CGRectMake(0, 0, 250, 100); // 左上角坐标系,位置设置不起作用 CGImageRef partImageRef = CGImageCreateWithImageInRect(imageToDraw.CGImage, imageRect); // 转换成 UIImage UIImage *image = [[UIImage alloc] initWithCGImage:partImageRef]; CGImageRelease(partImageRef); CGContextRelease(bitmapCtx); CGColorSpaceRelease(rgb); UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; [self addSubview:imageView]; } 效果 3、Quartz 2D 常用函数 1、常用拼接路径函数 // 新建一个起点 void CGContextMoveToPoint(CGContextRef c, CGFloat x, CGFloat y) // 添加新的线段到某个点 void CGContextAddLineToPoint(CGContextRef c, CGFloat x, CGFloat y) // 封闭路径 void CGContextClosePath(CGContextRef cg_nullable c) // 添加一个矩形 void CGContextAddRect(CGContextRef c, CGRect rect) // 添加一个椭圆 void CGContextAddEllipseInRect(CGContextRef context, CGRect rect) // 添加一个圆弧 void CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, int clockwise) 2、常用绘制路径函数 // Mode 参数决定绘制的模式 void CGContextDrawPath(CGContextRef c, CGPathDrawingMode mode) // 绘制空心路径 void CGContextStrokePath(CGContextRef c) // 绘制实心路径 void CGContextFillPath(CGContextRef c) 一般以 CGContextDraw、CGContextStroke、CGContextFill 开头的函数,都是用来绘制路径的。 3、图形上下文栈的操作函数 // 将当前的上下文 Copy 一份,保存到栈顶(那个栈叫做 “图形上下文栈”) void CGContextSaveGState(CGContextRef c) // 将栈顶的上下文出栈,替换掉当前的上下文 void CGContextRestoreGState(CGContextRef c) 4、矩阵操作函数 // 缩放 void CGContextScaleCTM(CGContextRef c, CGFloat sx, CGFloat sy) // 旋转 void CGContextRotateCTM(CGContextRef c, CGFloat angle) // 平移 void CGContextTranslateCTM(CGContextRef c, CGFloat tx, CGFloat ty) 利用矩阵操作,能让绘制到上下文中的所有路径一起发生变化。 4、贝塞尔路径 贝塞尔路径(UIBezierPath)是 UIKit 框架中对 Quartz 2D 绘图的封装。实际操作起来,使用贝塞尔路径,更为方便。用法与 CGContextRef 类似,但是 OC 对其进行了封装,更加面向对象。 具体讲解见 Quartz 2D 贝塞尔曲线 二阶贝塞尔曲线示意图 三阶贝塞尔曲线示意图 贝塞尔路径常用的方法 // 设置起始点 - (void)moveToPoint:(CGPoint)point; // 添加直线到一点 - (void)addLineToPoint:(CGPoint)point; // 封闭闭路径 - (void)closePath; // 返回一个描述椭圆的路径 + (UIBezierPath *)bezierPathWithOvalInRect:(CGRect)rect; // 贝塞尔曲线 - (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint; // 三次贝塞尔曲线 - (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2; // 绘制圆弧 - (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise; 5、基本图形绘制 5.1 绘制直线 1、绘制直线 在 Quartz 2D 中,使用方法 CGContextMoveToPoint 移动画笔到一个点来开始新的子路径,使用 CGContextAddLineToPoint 来从当前开始点添加一条线到结束点,CGContextAddLineToPoint 调用后,此时的终点会重新设置为新的开始点。贝塞尔路径是对 Quartz 2D 绘图的 OC 封装。 方式 1,最原始方式 - (void)drawRect:(CGRect)rect { // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 创建路径 CGMutablePathRef path = CGPathCreateMutable(); // 描述路径, 设置起点,path:给哪个路径设置起点 CGPathMoveToPoint(path, NULL, 50, 50); // 添加一根线到某个点 CGPathAddLineToPoint(path, NULL, 200, 200); // 把路径添加到上下文 CGContextAddPath(ctx, path); // 渲染上下文 CGContextStrokePath(ctx); } 方式 2,简化方式 - (void)drawRect:(CGRect)rect { // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径,设置起点 CGContextMoveToPoint(ctx, 50, 50); // 添加一根线到某个点 CGContextAddLineToPoint(ctx, 200, 200); // 渲染上下文 CGContextStrokePath(ctx); } 方式 3,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // 创建路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 设置起点 [path moveToPoint:CGPointMake(50, 50)]; // 添加一根线到某个点 [path addLineToPoint:CGPointMake(200, 200)]; // 绘制路径 [path stroke]; } 方式 4,原始方式和贝塞尔路径方式同时使用 - (void)drawRect:(CGRect)rect { // 创建路径 UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(10, 125)]; [path addLineToPoint:CGPointMake(240, 125)]; // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 添加路径 CGContextAddPath(ctx, path.CGPath); // 设置属性 CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); CGContextSetLineWidth(ctx, 5); // 绘制路径 CGContextStrokePath(ctx); } - (void)drawRect:(CGRect)rect { // 获取上下文,描述路径 CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextMoveToPoint(ctx, 50, 50); CGContextAddLineToPoint(ctx, 200, 200); // 创建贝塞尔路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 添加路径 CGContextAddPath(ctx, path.CGPath); // 设置属性 [[UIColor redColor] set]; path.lineWidth = 5; // 绘制路径 [path stroke]; } 效果 2、设置画线状态 线的顶端模式,使用 CGContextSetLineCap 来设置,一共有三种 线的相交模式,使用CGContextSetLineJoin 来设置,一共也有三种 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // 画线 // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径 CGContextMoveToPoint(ctx, 50, 50); CGContextAddLineToPoint(ctx, 200, 200); // 画第二条线,默认下一根线的起点就是上一根线终点 // CGContextMoveToPoint(ctx, 200, 50); CGContextAddLineToPoint(ctx, 50, 225); // 设置画线状态,一定要放在渲染之前 // 设置颜色 CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); // 设置线宽 CGContextSetLineWidth(ctx, 5); // 设置相交样式 CGContextSetLineJoin(ctx, kCGLineJoinRound); // 设置顶端样式 CGContextSetLineCap(ctx, kCGLineCapSquare); // 渲染 CGContextStrokePath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // 画线 // 创建路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 描述路径 [path moveToPoint:CGPointMake(50, 50)]; [path addLineToPoint:CGPointMake(200, 200)]; // 画第二条线,默认下一根线的起点就是上一根线终点 // [path moveToPoint:CGPointMake(200, 50)]; [path addLineToPoint:CGPointMake(50, 225)]; // 设置画线状态,一定要放在渲染之前 // 设置颜色 [[UIColor redColor] set]; // 设置线宽 path.lineWidth = 5; // 设置相交样式 path.lineJoinStyle = kCGLineJoinRound; // 设置顶端样式 path.lineCapStyle = kCGLineCapSquare; // 渲染 [path stroke]; } 效果 5.2 绘制虚线 绘制虚线 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // phase :第一个虚线段从哪里开始,例如传入 3,则从第 3 个单位开始 // lengths:一个 C 数组,表示绘制部分和空白部分的分配。例如传入 [2, 2],则绘制 2 个单位,然后空白 2 个单位,以此重复 // count : lengths 的数量 // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 绘制直线 CGContextMoveToPoint(ctx, 50, 50); CGContextAddLineToPoint(ctx, 200, 200); // 设置虚线 CGFloat lengths[] = {5}; CGContextSetLineDash(ctx, 1, lengths, 1); // 渲染 CGContextStrokePath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // phase :第一个虚线段从哪里开始,例如传入 3,则从第 3 个单位开始 // pattern:一个 C 数组,表示绘制部分和空白部分的分配。例如传入 [2, 2],则绘制 2 个单位,然后空白 2 个单位,以此重复 // count : lengths 的数量 // 创建路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 绘制直线 [path moveToPoint:CGPointMake(50, 50)]; [path addLineToPoint:CGPointMake(200, 200)]; // 设置虚线 CGFloat lengths[] = {5}; [path setLineDash:lengths count:1 phase:1]; // 渲染 [path stroke]; } 效果 5.3 绘制曲线 Quartz 2D 使用计算机图形学中的多项式来绘制曲线,支持二次和三次曲线。 1、绘制二次曲线 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // cpx, cpy:控制点 // x, y :曲线终点 // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 设置起点 CGContextMoveToPoint(ctx, 50, 200); // 绘制曲线 CGContextAddQuadCurveToPoint(ctx, 125, 50, 200, 200); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // controlPoint:控制点 // endPoint :曲线终点 // 创建路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 设置起点 [path moveToPoint:CGPointMake(50, 200)]; // 绘制曲线 [path addQuadCurveToPoint:CGPointMake(200, 200) controlPoint:CGPointMake(125, 50)]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 2、绘制三次曲线 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // cp1x, cp1y:控制点 1 // cp2x, cp2y:控制点 2 // x, y :曲线终点 // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 设置起点 CGContextMoveToPoint(ctx, 50, 100); // 绘制曲线 CGContextAddCurveToPoint(ctx, 100, 10, 150, 190, 200, 100); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // controlPoint1:控制点 1 // controlPoint2:控制点 2 // endPoint :曲线终点 // 创建路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 设置起点 [path moveToPoint:CGPointMake(50, 100)]; // 绘制曲线 [path addCurveToPoint:CGPointMake(200, 100) controlPoint1:CGPointMake(100, 10) controlPoint2:CGPointMake(150, 190)]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 3、绘制图形设置 方式 1,原始方式 // 设置空心路径的颜色 CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); // 设置实心路径的填充颜色 CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 CGContextFillPath(ctx); 方式 2,贝塞尔路径方式 // 设置空心路径的颜色 [[UIColor redColor] setStroke]; // 设置实心路径的填充颜色 [[UIColor redColor] setFill]; // 设置空心路径和实心路径的颜色 [[UIColor redColor] set]; // 绘制空心路径 [path stroke]; // 绘制实心路径 [path fill]; 效果 5.4 绘制三角形 绘制三角形 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径 CGContextMoveToPoint(ctx, 100, 50); CGContextAddLineToPoint(ctx, 20, 200); CGContextAddLineToPoint(ctx, 200, 200); // 封闭路径,自动连接首尾 CGContextClosePath(ctx); // 渲染 // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // 创建路径 UIBezierPath *path = [UIBezierPath bezierPath]; // 描述路径 [path moveToPoint:CGPointMake(100, 50)]; [path addLineToPoint:CGPointMake(20, 200)]; [path addLineToPoint:CGPointMake(200, 200)]; // 封闭路径,自动连接首尾 [path closePath]; // 渲染 // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 5.5 绘制矩形 绘制矩形 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径 CGContextAddRect(ctx, CGRectMake(20, 50, 200, 100)); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // 创建路径,绘制图形 UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(20, 50, 200, 100)]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 5.6 绘制圆角矩形 绘制圆角矩形 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // x1, y1:圆角两个切线的交点 // x2, y2:圆角终点 // radius:圆角半径 // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); CGFloat x = 20; CGFloat y = 50; CGFloat w = 200; CGFloat h = 100; CGFloat r = 20; CGContextMoveToPoint(ctx, x, y + r); CGContextAddArcToPoint(ctx, x, y, x + r, y, r); // 左上角 CGContextAddLineToPoint(ctx, x + w - r, y); CGContextAddArcToPoint(ctx, x + w, y, x + w, y + r, r); // 右上角 CGContextAddLineToPoint(ctx, x + w, y + h - r); CGContextAddArcToPoint(ctx, x + w, y + h, x + w - r, y + h, r); // 右下角 CGContextAddLineToPoint(ctx, x + r, y + h); CGContextAddArcToPoint(ctx, x, y + h, x, y + h - r, r); // 左下角 CGContextClosePath(ctx); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // rect :矩形位置尺寸 // cornerRadius:圆角半径 // 创建路径,绘制图形 UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(20, 50, 200, 100) cornerRadius:20]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 5.7 绘制圆弧 绘制圆弧 Quartz 2D 提供了两个方法来绘制圆弧 CGContextAddArc,普通的圆弧一部分(以某圆心,某半径,某弧度的圆弧)。 CGContextAddArcToPoint,用来绘制圆角。 函数体 void CGContextAddArcToPoint(CGContextRef cg_nullable c, CGFloat x1, CGFloat y1, CGFloat x2, CGFloat y2, CGFloat radius) 参数 c :图形上下文 x1, y1:和当前点 (x0, y0) 决定了第一条切线(x0, y0)-> (x1, y1) x2, y2:和 (x1, y1) 决定了第二条切线 radius:相切的半径。 也就是说,绘制一个半径为 radius 的圆弧,和上述两条直线都相切。图中的两条红线就是上文提到的两条线,分别是 (x0,y0) -> (x1,y1) 和 (x1,y1) -> (x2,y2),那么和这两条线都想切的自然就是图中的蓝色圆弧了. 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // x, y :圆心 // radius :半径 // startAngle:开始弧度 // endAngle :结束弧度 // clockwise :方向,false 顺时针,true 逆时针 // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径 CGContextAddArc(ctx, 125, 125, 100, 0, M_PI_2, false); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } - (void)drawRect:(CGRect)rect { // x1, y1:圆角两个切线的交点 // x2, y2:圆角终点 // radius:圆角半径 // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 设置起点 CGContextMoveToPoint(ctx, 225, 125); // 绘制圆弧 CGContextAddArcToPoint(ctx, 225, 225, 125, 225, 100); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // Center :圆心 // radius :半径 // startAngle:开始弧度 // endAngle :结束弧度 // clockwise :方向,YES 顺时针,NO 逆时针 // 创建路径,绘制图形 UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(125, 125) radius:100 startAngle:0 endAngle:M_PI_2 clockwise:YES]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } - (void)drawRect:(CGRect)rect { // Center :圆心 // radius :半径 // startAngle:开始弧度 // endAngle :结束弧度 // clockwise :方向,YES 顺时针,NO 逆时针 // 创建路径,绘制图形 UIBezierPath *path = [UIBezierPath bezierPath]; // 设置起点 [path moveToPoint:CGPointMake(225, 125)]; // 绘制圆弧 [path addArcWithCenter:CGPointMake(125, 125) radius:100 startAngle:0 endAngle:M_PI_2 clockwise:YES]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 5.8 绘制扇形 绘制扇形 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径 CGContextAddArc(ctx, 125, 125, 100, 0, M_PI_4, NO); // 绘制到圆心的直线 CGContextAddLineToPoint(ctx, 125, 125); // 封闭路径 CGContextClosePath(ctx); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // 创建路径,绘制图形 UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(125, 125) radius:100 startAngle:0 endAngle:M_PI_4 clockwise:YES]; // 绘制到圆心的直线 [path addLineToPoint:CGPointMake(125, 125)]; // 封闭路径 [path closePath]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 5.9 绘制圆形 绘制圆形 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径 CGContextAddArc(ctx, 125, 125, 100, 0, M_PI * 2, NO); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // 创建路径,绘制图形 UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(125, 125) radius:100 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 5.10 绘制椭圆形 绘制椭圆形 在矩形中设置不同的宽高方式创建。 方式 1,原始方式 - (void)drawRect:(CGRect)rect { // 获取上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 描述路径 CGContextAddEllipseInRect(ctx, CGRectMake(20, 50, 200, 100)); // 绘制空心路径 CGContextStrokePath(ctx); // 绘制实心路径 // CGContextFillPath(ctx); } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { // 创建路径,绘制图形 UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(20, 50, 200, 100)]; // 绘制空心路径 [path stroke]; // 绘制实心路径 // [path fill]; } 效果 6、统计图绘制 6.1 绘制折线图 绘制折线图 方式 1,原始方式 - (void)drawRect:(CGRect)rect { CGFloat x1 = 0; CGFloat x2 = 0; CGFloat h1 = 0; CGFloat h2 = 0; CGFloat y1 = 0; CGFloat y2 = 0; CGFloat w = rect.size.width / (self.datas.count - 1); CGFloat largeNum = [self.datas[0] floatValue]; for (int i = 0; i < self.datas.count; i++) { if ([self.datas[i] floatValue] > largeNum) { largeNum = [self.datas[i] floatValue]; } } for (int i = 0; i < self.datas.count - 1; i++) { x1 = w * i; x2 = w * (i + 1); h1 = [self.datas[i] floatValue] / (largeNum * 1.2) * rect.size.height; h2 = [self.datas[i + 1] floatValue] / (largeNum * 1.2) * rect.size.height; y1 = rect.size.height - h1; y2 = rect.size.height - h2; CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextMoveToPoint(ctx, x1, y1); CGContextAddLineToPoint(ctx, x2, y2); CGContextSetStrokeColorWithColor(ctx, self.color.CGColor); CGContextStrokePath(ctx); } } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { CGFloat x1 = 0; CGFloat x2 = 0; CGFloat h1 = 0; CGFloat h2 = 0; CGFloat y1 = 0; CGFloat y2 = 0; CGFloat w = rect.size.width / (self.datas.count - 1); CGFloat largeNum = [self.datas[0] floatValue]; for (int i = 0; i < self.datas.count; i++) { if ([self.datas[i] floatValue] > largeNum) { largeNum = [self.datas[i] floatValue]; } } for (int i = 0; i < self.datas.count - 1; i++) { x1 = w * i; x2 = w * (i + 1); h1 = [self.datas[i] floatValue] / (largeNum * 1.2) * rect.size.height; h2 = [self.datas[i + 1] floatValue] / (largeNum * 1.2) * rect.size.height; y1 = rect.size.height - h1; y2 = rect.size.height - h2; UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(x1, y1)]; [path addLineToPoint:CGPointMake(x2, y2)]; [self.color set]; [path stroke]; } } 使用 // LineView.h @interface LineView : UIView @property (nonatomic, strong) NSArray<NSNumber *> *datas; @property (nonatomic, strong) UIColor *color; + (instancetype)lineViewWithFrame:(CGRect)frame datas:(NSArray<NSNumber *> *)datas colors:(UIColor *)color; @end // LineView.m + (instancetype)lineViewWithFrame:(CGRect)frame datas:(NSArray<NSNumber *> *)datas colors:(UIColor *)color { LineView *line = [[self alloc] init]; line.frame = frame; line.datas = datas; line.color = color; return line; } // ViewController.m CGRect frame = CGRectMake(50, 40, self.view.bounds.size.width - 100, self.view.bounds.size.width - 100); NSArray *datas = @[@30, @60, @50, @28]; LineView *lineView = [LineView lineViewWithFrame:frame datas:datas colors:[UIColor blueColor]]; lineView.layer.borderWidth = 1; [self.view addSubview:lineView]; 效果 6.2 绘制柱形图 绘制柱形图 方式 1,原始方式 - (void)drawRect:(CGRect)rect { CGFloat x = 0; CGFloat y = 0; CGFloat h = 0; CGFloat m = self.margin; CGFloat w = self.margin ? ((rect.size.width - m) / (self.datas.count) - m) : (rect.size.width / (2 * self.datas.count + 1)); CGFloat largeNum = [self.datas[0] floatValue]; for (int i = 0; i < self.datas.count; i++) { if ([self.datas[i] floatValue] > largeNum) { largeNum = [self.datas[i] floatValue]; } } for (int i = 0; i < self.datas.count; i++) { x = self.margin ? ((m + w) * i + m) : (2 * w * i + w); h = [self.datas[i] floatValue] / (largeNum * 1.2) * rect.size.height; y = rect.size.height - h; CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextAddRect(ctx, CGRectMake(x, y, w, h)); CGContextSetFillColorWithColor(ctx, self.colors[i].CGColor); CGContextFillPath(ctx); } } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { CGFloat x = 0; CGFloat y = 0; CGFloat h = 0; CGFloat m = self.margin; CGFloat w = self.margin ? ((rect.size.width - m) / (self.datas.count) - m) : (rect.size.width / (2 * self.datas.count + 1)); CGFloat largeNum = [self.datas[0] floatValue]; for (int i = 0; i < self.datas.count; i++) { if ([self.datas[i] floatValue] > largeNum) { largeNum = [self.datas[i] floatValue]; } } for (int i = 0; i < self.datas.count; i++) { x = self.margin ? ((m + w) * i + m) : (2 * w * i + w); h = [self.datas[i] floatValue] / (largeNum * 1.2) * rect.size.height; y = rect.size.height - h; UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(x, y, w, h)]; [self.colors[i] set]; [path fill]; } } 使用 // BarView.h @interface BarView : UIView @property (nonatomic, strong) NSArray<NSNumber *> *datas; @property (nonatomic, strong) NSArray<UIColor *> *colors; @property (nonatomic, assign) CGFloat margin; + (instancetype)barViewWithFrame:(CGRect)frame datas:(NSArray<NSNumber *> *)datas colors:(NSArray<UIColor *> *)colors margin:(CGFloat)margin; @end // BarView.m + (instancetype)barViewWithFrame:(CGRect)frame datas:(NSArray<NSNumber *> *)datas colors:(NSArray<UIColor *> *)colors margin:(CGFloat)margin { BarView *bar = [[self alloc] init]; bar.frame = frame; bar.datas = datas; bar.colors = colors; bar.margin = margin; return bar; } // ViewController.m CGRect frame = CGRectMake(50, 40, self.view.bounds.size.width - 100, self.view.bounds.size.width - 100); NSArray *datas = @[@30, @60, @50, @28]; NSArray *colors = @[[UIColor redColor], [UIColor yellowColor], [UIColor blueColor], [UIColor greenColor]]; CGFloat margin = 20; BarView *barView = [BarView barViewWithFrame:frame datas:datas colors:colors margin:margin]; barView.layer.borderWidth = 1; [self.view addSubview:barView]; 效果 6.3 绘制饼图 绘制饼图 方式 1,原始方式 - (void)drawRect:(CGRect)rect { CGFloat radius = MIN(rect.size.width, rect.size.height) * 0.5; CGFloat rx = rect.size.width * 0.5; CGFloat ry = rect.size.height * 0.5; CGFloat startA = self.startAngle; CGFloat angle = 0; CGFloat endA = startA; CGFloat sum = 0; for (int i = 0; i < self.datas.count; i++) { sum += [self.datas[i] floatValue]; } for (int i = 0; i < self.datas.count; i++) { startA = endA; angle = [self.datas[i] floatValue] / sum * M_PI * 2; endA = startA + angle; CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextAddArc(ctx, rx, ry, radius, startA, endA, NO); CGContextAddLineToPoint(ctx, rx, ry); CGContextClosePath(ctx); CGContextSetFillColorWithColor(ctx, self.colors[i].CGColor); CGContextFillPath(ctx); } } 方式 2,贝塞尔路径方式 - (void)drawRect:(CGRect)rect { CGFloat radius = MIN(rect.size.width, rect.size.height) * 0.5; CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5); CGFloat startA = self.startAngle; CGFloat angle = 0; CGFloat endA = startA; CGFloat sum = 0; for (int i = 0; i < self.datas.count; i++) { sum += [self.datas[i] floatValue]; } for (int i = 0; i < self.datas.count; i++) { startA = endA; angle = [self.datas[i] floatValue] / sum * M_PI * 2; endA = startA + angle; UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES]; [path addLineToPoint:center]; [path closePath]; [self.colors[i] set]; [path fill]; } } 使用 // PieView.h @interface PieView : UIView @property (nonatomic, strong) NSArray<NSNumber *> *datas; @property (nonatomic, strong) NSArray<UIColor *> *colors; @property (nonatomic, assign) CGFloat startAngle; + (instancetype)pieViewWithFrame:(CGRect)frame datas:(NSArray<NSNumber *> *)datas colors:(NSArray<UIColor *> *)colors startAngle:(CGFloat)startAngle; @end // PieView.m + (instancetype)pieViewWithFrame:(CGRect)frame datas:(NSArray<NSNumber *> *)datas colors:(NSArray<UIColor *> *)colors startAngle:(CGFloat)startAngle { PieView *pie = [[self alloc] init]; pie.frame = frame; pie.datas = datas; pie.colors = colors; pie.startAngle = startAngle; return pie; } // ViewController.m CGRect frame = CGRectMake(50, 40, self.view.bounds.size.width - 100, self.view.bounds.size.width - 100); NSArray *datas = @[@30, @60, @50, @28]; NSArray *colors = @[[UIColor redColor], [UIColor yellowColor], [UIColor blueColor], [UIColor greenColor]]; CGFloat startAngle = -M_PI_2; PieView *pieView = [PieView pieViewWithFrame:frame datas:datas colors:colors startAngle:startAngle]; pieView.layer.borderWidth = 1; [self.view addSubview:pieView]; 效果 7、使用第三方框架绘制图表 使用第三方框架 Charts 绘制 iOS 图表,Charts 是一款用于绘制图表的框架,可以绘制柱状图、折线图、K线图、饼状图等。GitHub 源码 Charts 具体讲解见 Quartz 2D 第三方框架绘制图表 效果 折线图 柱状图 饼图 8、文本处理 8.1 在控件视图上绘制/添加文本 如果绘制东西到 view 等视图控件上面,必须写在 drawRect 方法里面,不管有没有手动获取到上下文。 1、绘制/添加文本 在指定位置绘制文本,文本不会自动换行 - (void)drawRect:(CGRect)rect { NSString *string = @"QianChia"; // 不设置文本属性 [string drawAtPoint:CGPointZero withAttributes:nil]; // 设置文本属性 [string drawAtPoint:CGPointZero withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:50]}]; } 在指定区域绘制文本,文本会自动换行 - (void)drawRect:(CGRect)rect { NSString *string = @"QianChia"; // 不设置文本属性 [string drawInRect:rect withAttributes:nil]; // 设置文本属性 [string drawInRect:rect withAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:50]}]; } 效果 2、设置文本属性 NSMutableDictionary *textDict = [NSMutableDictionary dictionary]; // 设置文本字体 textDict[NSFontAttributeName] = [UIFont systemFontOfSize:50]; // 设置文本颜色 textDict[NSForegroundColorAttributeName] = [UIColor redColor]; // 设置文本的空心线条宽度 textDict[NSStrokeWidthAttributeName] = @5; // 设置文本的空心线条颜色,要使此设置有效必须设置空心线条宽度,此设置有效时前景色设置项无效 textDict[NSStrokeColorAttributeName] = [UIColor blueColor]; // 设置文本阴影,用 drawInRect 方式绘制,不添加空心属性时,文字自动换行后此设置无效 NSShadow *shadow = [[NSShadow alloc] init]; shadow.shadowColor = [UIColor blackColor]; shadow.shadowOffset = CGSizeMake(4, 4); shadow.shadowBlurRadius = 3; textDict[NSShadowAttributeName] = shadow; 效果 9、图片处理 9.1 在控件视图上绘制/添加图片 如果绘制东西到 view 等视图控件上面,必须写在 drawRect 方法里面,不管有没有手动获取到上下文。 跟 view 相关联的上下文是 layer 图层上下文,需要在在 view 的 drawRect 方法中获取。跟 image 相关的上下文是 Bitmap 位图上下文,需要我们手动创建。 1、绘制/添加图片 在指定位置绘制图片,图片不会进行缩放 - (void)drawRect:(CGRect)rect { UIImage *image = [UIImage imageNamed:@"demo2"]; [image drawAtPoint:CGPointZero]; } 在指定区域绘制图片,图片会进行缩放 - (void)drawRect:(CGRect)rect { UIImage *image = [UIImage imageNamed:@"demo2"]; [image drawInRect:rect]; } 在指定区域绘制图片,图片会以平铺的样式填充 - (void)drawRect:(CGRect)rect { UIImage *image = [UIImage imageNamed:@"demo3"]; [image drawAsPatternInRect:rect]; } 在图片上绘制图片,不是绘制在 view 视图控件上,不需写在 drawRect 方法里面 UIImage *backImage = [UIImage imageNamed:@"demo5"]; UIImage *headImage = [UIImage imageNamed:@"demo6"]; // size :图片画板(上下文)尺寸(新图片的尺寸) // opaque:是否透明,NO 不透明,YES 透明 // scale :缩放,如果不缩放,设置为 0 // 开启一个位图上下文 UIGraphicsBeginImageContextWithOptions(backImage.size, NO, 0); // UIGraphicsBeginImageContext(backImage.size); // 绘制背景图片 CGRect backRect = CGRectMake(0, 0, backImage.size.width, backImage.size.height); [backImage drawInRect:backRect]; // 绘制头像图片 CGFloat scale = 5; CGFloat w = backRect.size.width / scale; CGFloat h = backRect.size.height / scale; CGFloat x = (backRect.size.width - w) / 2; CGFloat y = (backRect.size.height - h) / 2; CGRect headRect = CGRectMake(x, y, w, h); [headImage drawInRect:headRect]; // 获取绘制好的图片 UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 关闭位图上下文 UIGraphicsEndImageContext(); 效果 2、图片修剪 裁剪图片 先设置裁剪区域,再在指定的区域绘制图片,再裁剪/遮盖掉裁剪区域之外的部分。 UIImage *image = [UIImage imageNamed:@"demo2"]; // 开启图片上下文 UIGraphicsBeginImageContextWithOptions(image.size, NO, 0); // UIGraphicsBeginImageContext(image.size); // 设置裁剪区域,超出裁剪区域的内容全部裁剪/遮盖掉,必须放在绘制图片之前 UIRectClip(CGRectMake(50, 50, 100, 200)); // 绘制图片 [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; // 获取绘制好的图片 UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 关闭图片上下文 UIGraphicsEndImageContext(); 效果 擦除图片 先在指定的区域绘制图片,再擦除/遮盖掉擦除区域之内的部分。 UIImage *image = [UIImage imageNamed:@"demo2"]; // 开启图片上下文 UIGraphicsBeginImageContextWithOptions(image.size, NO, 0); // UIGraphicsBeginImageContext(image.size); // 获取图片上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 绘制图片 [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; // 设置擦除区域,擦除/遮盖掉指定区域的图片,必须放在绘制图片之后 CGContextClearRect(ctx, CGRectMake(50, 50, 100, 200)); // 获取绘制好的图片 UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 关闭图片上下文 UIGraphicsEndImageContext(); 效果 切割图片 切割掉切割区域之外的部分。 UIImage *image = [UIImage imageNamed:@"demo2"]; // 设置切割区域 CGRect cutRect = CGRectMake(0, 0, image.size.width / 2, image.size.height); // 切割图片 CGImageRef cgImage = CGImageCreateWithImageInRect(image.CGImage, cutRect); // 转换为 UIImage 格式图片 UIImage *newImage = [[UIImage alloc] initWithCGImage:cgImage]; CGImageRelease(cgImage); 效果 9.2 截取屏幕 具体实现代码见 GitHub 源码 QExtension 1、截取全屏幕图 @implementation UIImage (Draw) + (UIImage *)q_imageWithScreenShot { UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; // 开启图片上下文 UIGraphicsBeginImageContextWithOptions(keyWindow.bounds.size, NO, [UIScreen mainScreen].scale); // UIGraphicsBeginImageContext(keyWindow.bounds.size); // 获取图片上下文 CGContextRef context = UIGraphicsGetCurrentContext(); // 在 context 上渲染 [keyWindow.layer renderInContext:context]; // 从图片上下文获取当前图片 UIImage *screenShot = UIGraphicsGetImageFromCurrentImageContext(); // 关闭图片上下文 UIGraphicsEndImageContext(); return screenShot; } @end // 截取全屏幕图 UIImage *image = [UIImage q_imageWithScreenShot]; UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); 效果 2、截取指定视图控件屏幕图 @implementation UIImage (Draw) + (UIImage *)q_imageWithScreenShotFromView:(UIView *)view { // 开启图片上下文 UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale); // UIGraphicsBeginImageContext(view.bounds.size); // 获取图片上下文 CGContextRef context = UIGraphicsGetCurrentContext(); // 在 context 上渲染 [view.layer renderInContext:context]; // 从图片上下文获取当前图片 UIImage *screenShot = UIGraphicsGetImageFromCurrentImageContext(); // 关闭图片上下文 UIGraphicsEndImageContext(); return screenShot; } @end // 截取指定视图控件屏幕图 UIImage *image = [UIImage q_imageWithScreenShotFromView:self.imageView]; UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); 效果 9.3 调整图片尺寸 具体实现代码见 GitHub 源码 QExtension 调整图片尺寸 @implementation UIImage (Draw) - (UIImage *)q_imageByScalingAndCroppingToSize:(CGSize)size { // 开启图片上下文 UIGraphicsBeginImageContextWithOptions(size, NO, 0); // UIGraphicsBeginImageContext(size); // 在指定的区域内绘制图片 [self drawInRect:CGRectMake(0, 0, size.width, size.height)]; // 从图片上下文获取当前图片 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); // 关闭图片上下文 UIGraphicsEndImageContext(); return image; } @end // 调整图片的尺寸 UIImage *image = [UIImage imageNamed:@"demo2"]; UIImage *newImage = [image q_imageByScalingAndCroppingToSize:CGSizeMake(150, 150)]; 效果 9.4 裁剪圆形图片 具体实现代码见 GitHub 源码 QExtension 裁剪圆形图片 @implementation UIImage (Draw) - (UIImage *)q_imageByCroppingToRound { // 开启图片上下文 UIGraphicsBeginImageContextWithOptions(self.size, NO, 0); // UIGraphicsBeginImageContext(self.size); // 设置裁剪路径 CGFloat w = self.size.width; CGFloat h = self.size.height; CGFloat wh = MIN(self.size.width, self.size.height); CGRect clipRect = CGRectMake((w - wh) / 2, (h - wh) / 2, wh, wh); UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:clipRect]; // 裁剪 [path addClip]; // 绘制图片 [self drawAtPoint:CGPointZero]; // 从图片上下文获取当前图片 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); // 关闭图片上下文 UIGraphicsEndImageContext(); // 切割图片 CGRect cutRect = CGRectMake(w - wh, h - wh, wh * 2, wh * 2); CGImageRef imageRef = image.CGImage; CGImageRef cgImage = CGImageCreateWithImageInRect(imageRef, cutRect); UIImage *newImage = [[UIImage alloc] initWithCGImage:cgImage]; CGImageRelease(cgImage); return newImage; } @end // 裁剪圆形图片 UIImage *image = [UIImage imageNamed:@"demo2"]; UIImage *newImage = [image q_imageByCroppingToRound]; 效果 9.5 添加图片水印 具体实现代码见 GitHub 源码 QExtension 水印在图片上加的防止他人盗图的半透明 logo、文字、图标。有时候,在手机客户端 app 中也需要用到水印技术。比如,用户拍完照片后,可以在照片上打个水印,标识这个图片是属于哪个用户的。 添加图片水印 @implementation UIImage (Draw) - (UIImage *)q_imageWithWaterMarkString:(nullable NSString *)string attributes:(nullable NSDictionary<NSString *, id> *)attrs image:(nullable UIImage *)image frame:(CGRect)frame { // 开启图片上下文 UIGraphicsBeginImageContextWithOptions(self.size, NO, 0); // UIGraphicsBeginImageContext(self.size); // 绘制背景图片 CGRect backRect = CGRectMake(0, 0, self.size.width, self.size.height); [self drawInRect:backRect]; CGRect strRect = frame; // 添加图片水印 if (image) { if ((frame.origin.x == -1) && (frame.origin.y == -1)) { CGFloat w = frame.size.width; CGFloat h = frame.size.height; CGFloat x = (backRect.size.width - w) / 2; CGFloat y = (backRect.size.height - h) / 2; [image drawInRect:CGRectMake(x, y, w, h)]; } else { [image drawInRect:frame]; strRect = CGRectMake(frame.origin.x + frame.size.width + 5, frame.origin.y, 1, 1); } } // 添加文字水印 if (string) { if ((frame.origin.x == -1) && (frame.origin.y == -1)) { } else { [string drawAtPoint:strRect.origin withAttributes:attrs]; } } // 获取绘制好的图片 UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); // 关闭位图上下文 UIGraphicsEndImageContext(); return newImage; } @end UIImage *image = [UIImage imageNamed:@"demo2"]; // 设置水印文本属性 NSMutableDictionary *textAttrs = [NSMutableDictionary dictionary]; textAttrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:50]; textAttrs[NSForegroundColorAttributeName] = [[UIColor redColor] colorWithAlphaComponent:0.2]; textAttrs[NSStrokeWidthAttributeName] = @5; // 添加图片水印 self.imageView.image = [image q_imageWithWaterMarkString:@"QianChia" attributes:textAttrs image:nil frame:CGRectMake(30, 300, 50, 50)]; UIImage *image = [UIImage imageNamed:@"demo5"]; // 添加图片水印 self.imageView.image = [image q_imageWithWaterMarkString:nil attributes:nil image:[UIImage imageNamed:@"demo8"] frame:CGRectMake(-1, -1, 88, 88)]; 效果 10、Quartz 2D 的使用 10.1 绘制下载进度按钮 具体实现代码见 GitHub 源码 QExtension 具体讲解见 Quartz 2D 下载进度按钮绘制 // 创建进度按钮 QProgressButton *progressButton = [QProgressButton q_progressButtonWithFrame:CGRectMake(100, 100, 100, 50) title:@"开始下载" lineWidth:10 lineColor:[UIColor blueColor] textColor:[UIColor redColor] backgroundColor:[UIColor yellowColor] isRound:YES]; // 设置按钮点击事件 [progressButton addTarget:self action:@selector(progressUpdate:) forControlEvents:UIControlEventTouchUpInside]; // 将按钮添加到当前控件显示 [self.view addSubview:progressButton]; // 设置按钮的进度值 self.progressButton.progress = progress; // 设置按钮的进度终止标题,一旦设置了此标题进度条就会停止 self.progressButton.stopTitle = @"下载完成"; 效果 10.2 绘制手势截屏 具体实现代码见 GitHub 源码 QExtension 具体讲解见 Quartz 2D 手势截屏绘制 // 创建手势截屏视图 QTouchClipView *touchClipView = [QTouchClipView q_touchClipViewWithView:self.imageView clipResult:^(UIImage * _Nullable image) { // 获取处理截屏结果 if (image) { UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil); } }]; // 添加手势截屏视图 [self.view addSubview:touchClipView]; 效果 10.3 绘制手势锁 具体实现代码见 GitHub 源码 QExtension 具体讲解见 Quartz 2D 手势锁绘制 // 设置 frame CGFloat margin = 50; CGFloat width = self.view.bounds.size.width - margin * 2; CGRect frame = CGRectMake(margin, 200, width, width); // 创建手势锁视图界面,获取滑动结果 QTouchLockView *touchLockView = [QTouchLockView q_touchLockViewWithFrame:frame pathResult:^(BOOL isSucceed, NSString * _Nonnull result) { // 处理手势触摸结果 [self dealTouchResult:result isSucceed:isSucceed]; }]; [self.view addSubview:touchLockView]; 效果 10.4 绘制画板 具体讲解见 Quartz 2D 画板绘制 10.4.1 绘制简单画板 绘制简单画板 // 创建画板 CGRect frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, 200); PaintBoardView *paintBoard = [[PaintBoardView alloc] initWithFrame:frame]; [self.view addSubview:paintBoard]; 效果 10.4.2 绘制画板封装 具体实现代码见 GitHub 源码 QExtension 1、创建简单画板 // 创建简单画板 CGRect frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, 200); QPaintBoardView *paintBoardView = [QPaintBoardView q_paintBoardViewWithFrame:frame]; // 可选属性值设置 paintBoardView.paintLineWidth = 5; // default is 1 paintBoardView.paintLineColor = [UIColor redColor]; // default is blackColor paintBoardView.paintBoardColor = [UIColor cyanColor]; // default is whiteColor [self.view addSubview:paintBoardView]; self.paintBoardView = paintBoardView; // 撤销绘画结果 [self.paintBoardView q_back]; // 清除绘画结果 [self.paintBoardView q_clear]; // 获取绘画结果 UIImage *image = [self.paintBoardView q_getPaintImage]; 效果 2、创建画板 // 创建画板 QPaintBoardView *paintBoard = [QPaintBoardView q_paintBoardViewWithFrame:self.view.bounds lineWidth:0 lineColor:nil boardColor:nil paintResult:^(UIImage * _Nullable image) { if (image) { NSData *data = UIImagePNGRepresentation(image); [data writeToFile:@"/Users/JHQ0228/Desktop/Images/pic.png" atomically:YES]; } }]; [self.view addSubview:paintBoard]; 效果 10.5 刮奖模拟 刮奖 - (IBAction)scratchBtnClick:(id)button { [button removeFromSuperview]; [self.forImageView removeFromSuperview]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { CGPoint startPoint = [touches.anyObject locationInView:self.cerImageView]; CGRect startRect = CGRectMake(startPoint.x - 10, startPoint.y - 10, 20, 20); [self clearRect:startRect imageView:self.cerImageView]; } - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { CGPoint touchPoint = [touches.anyObject locationInView:self.cerImageView]; CGRect touchRect = CGRectMake(touchPoint.x - 10, touchPoint.y - 10, 20, 20); [self clearRect:touchRect imageView:self.cerImageView]; } - (void)clearRect:(CGRect)rect imageView:(UIImageView *)imageView { UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 0); CGContextRef ctx = UIGraphicsGetCurrentContext(); [imageView.layer renderInContext:ctx]; // 设置擦除区域 CGContextClearRect(ctx, rect); UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); imageView.image = newImage; UIGraphicsEndImageContext(); } 效果
1、贝塞尔曲线 贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。贝塞尔曲线是计算机图形学中相当重要的参数曲线,在一些比较成熟的位图软件中也有贝塞尔曲线工具,如 PhotoShop 等。在 Flash4 中还没有完整的曲线工具,而在 Flash5 里面已经提供出贝塞尔曲线工具。 二阶贝塞尔曲线示意图 三阶贝塞尔曲线示意图 贝塞尔路径(UIBezierPath)是 iOS UIKit 框架中对 Quartz 2D 绘图的封装。实际操作起来,使用贝塞尔路径,更为方便。用法与 CGContextRef 类似,但是 OC 对其进行了封装,更加面向对象。 贝塞尔路径常用的方法 // 设置起始点 - (void)moveToPoint:(CGPoint)point; // 添加直线到一点 - (void)addLineToPoint:(CGPoint)point; // 封闭闭路径 - (void)closePath; // 返回一个描述椭圆的路径 + (UIBezierPath *)bezierPathWithOvalInRect:(CGRect)rect; // 贝塞尔曲线 - (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint; // 三次贝塞尔曲线 - (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2; // 绘制圆弧 - (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise; 2、基本图形的绘制 贝塞尔路径基本图形的绘制详情见 Quartz 2D 二维绘图 基本图形绘制 3、二三阶贝塞尔曲线示例 3.1 二阶贝塞尔曲线示例 QBezierPathView.h @interface QBezierPathView : UIView @end QBezierPathView.m @interface QBezierPathView () /// 路径 @property (nonatomic, strong) UIBezierPath *path; /// 起始点 @property (nonatomic, assign) CGPoint startP; /// 终止点 @property (nonatomic, assign) CGPoint endP; /// 控制点 @property (nonatomic, assign) CGPoint controlP; /// 线的颜色 @property (nonatomic, strong) UIColor *pathColor; /// 线的宽度 @property (nonatomic, assign) CGFloat pathWidth; /// 当前触摸的点 @property (nonatomic, assign) NSUInteger currentTouchP; @end @implementation QBezierPathView /// 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 设置初始值 self.startP = CGPointMake(20, 300); self.endP = CGPointMake(250, 300); self.controlP = CGPointMake(100, 100); self.pathColor = [UIColor redColor]; self.pathWidth = 2; } return self; } /// 绘制二阶贝塞尔曲线 - (void)drawRect:(CGRect)rect { // 绘制贝塞尔曲线 self.path = [UIBezierPath bezierPath]; [self.path moveToPoint:self.startP]; [self.path addQuadCurveToPoint:self.endP controlPoint:self.controlP]; self.path.lineWidth = self.pathWidth; [self.pathColor setStroke]; [self.path stroke]; // 绘制辅助线 self.path = [UIBezierPath bezierPath]; self.path.lineWidth = 1; [[UIColor grayColor] setStroke]; CGFloat lengths[] = {5}; [self.path setLineDash:lengths count:1 phase:1]; [self.path moveToPoint:self.controlP]; [self.path addLineToPoint:self.startP]; [self.path stroke]; [self.path moveToPoint:self.controlP]; [self.path addLineToPoint:self.endP]; [self.path stroke]; // 绘制辅助点及信息 self.path = [UIBezierPath bezierPathWithArcCenter:self.startP radius:4 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; [[UIColor blackColor] setStroke]; [self.path fill]; self.path = [UIBezierPath bezierPathWithArcCenter:self.endP radius:4 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; [[UIColor blackColor] setStroke]; [self.path fill]; self.path = [UIBezierPath bezierPathWithArcCenter:self.controlP radius:3 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; self.path.lineWidth = 2; [[UIColor blackColor] setStroke]; [self.path stroke]; CGRect startMsgRect = CGRectMake(self.startP.x + 8, self.startP.y - 7, 50, 20); [@"起始点" drawInRect:startMsgRect withAttributes:nil]; CGRect endMsgRect = CGRectMake(self.endP.x + 8, self.endP.y - 7, 50, 20); [@"终止点" drawInRect:endMsgRect withAttributes:nil]; CGRect control1MsgRect = CGRectMake(self.controlP.x + 8, self.controlP.y - 7, 50, 20); [@"控制点" drawInRect:control1MsgRect withAttributes:nil]; } /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; CGRect startR = CGRectMake(self.startP.x - 4, self.startP.y - 4, 8, 8); CGRect endR = CGRectMake(self.endP.x - 4, self.endP.y - 4, 8, 8); CGRect controlR = CGRectMake(self.controlP.x - 4, self.controlP.y - 4, 8, 8); // 判断当前触摸点 if (CGRectContainsPoint(startR, startPoint)) { self.currentTouchP = 1; } else if (CGRectContainsPoint(endR, startPoint)) { self.currentTouchP = 2; } else if (CGRectContainsPoint(controlR, startPoint)) { self.currentTouchP = 3; } } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; // 限制触摸点的边界 if (touchPoint.x < 0) { touchPoint.x = 0; } if (touchPoint.x > self.bounds.size.width) { touchPoint.x = self.bounds.size.width; } if (touchPoint.y < 0) { touchPoint.y = 0; } if (touchPoint.y > self.bounds.size.height) { touchPoint.y = self.bounds.size.height; } // 设置当前触摸点的值 switch (self.currentTouchP) { case 1: self.startP = touchPoint; break; case 2: self.endP = touchPoint; break; case 3: self.controlP = touchPoint; break; default: break; } // 刷新 [self setNeedsDisplay]; } /// 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 释放之前的触摸点 self.currentTouchP = 0; } /// 触摸取消 - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event { [self touchesEnded:touches withEvent:event]; } @end ViewController.m CGRect frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, 400); QBezierPathView *pathView = [[QBezierPathView alloc] initWithFrame:frame]; pathView.backgroundColor = [UIColor whiteColor]; pathView.layer.borderWidth = 1; [self.view addSubview:pathView]; 效果 3.2 三阶贝塞尔曲线示例 QBezierPathView.h @interface QBezierPathView : UIView @end QBezierPathView.m @interface QBezierPathView : UIView @interface QBezierPathView () /// 路径 @property (nonatomic, strong) UIBezierPath *path; /// 起始点 @property (nonatomic, assign) CGPoint startP; /// 终止点 @property (nonatomic, assign) CGPoint endP; /// 控制点 @property (nonatomic, assign) CGPoint controlP1; @property (nonatomic, assign) CGPoint controlP2; /// 线的颜色 @property (nonatomic, strong) UIColor *pathColor; /// 线的宽度 @property (nonatomic, assign) CGFloat pathWidth; /// 当前触摸的点 @property (nonatomic, assign) NSUInteger currentTouchP; @end @implementation QBezierPathView /// 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 设置初始值 self.startP = CGPointMake(20, 300); self.endP = CGPointMake(250, 300); self.controlP1 = CGPointMake(100, 100); self.controlP2 = CGPointMake(200, 350); self.pathColor = [UIColor redColor]; self.pathWidth = 2; } return self; } /// 绘制二阶贝塞尔曲线 - (void)drawRect:(CGRect)rect { // 绘制贝塞尔曲线 self.path = [UIBezierPath bezierPath]; [self.path moveToPoint:self.startP]; [self.path addCurveToPoint:self.endP controlPoint1:self.controlP1 controlPoint2:self.controlP2]; self.path.lineWidth = self.pathWidth; [self.pathColor setStroke]; [self.path stroke]; // 绘制辅助线 self.path = [UIBezierPath bezierPath]; self.path.lineWidth = 1; [[UIColor grayColor] setStroke]; CGFloat lengths[] = {5}; [self.path setLineDash:lengths count:1 phase:1]; [self.path moveToPoint:self.controlP1]; [self.path addLineToPoint:self.startP]; [self.path stroke]; [self.path moveToPoint:self.controlP1]; [self.path addLineToPoint:self.controlP2]; [self.path stroke]; [self.path moveToPoint:self.controlP2]; [self.path addLineToPoint:self.endP]; [self.path stroke]; // 绘制辅助点及信息 self.path = [UIBezierPath bezierPathWithArcCenter:self.startP radius:4 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; [[UIColor blackColor] setStroke]; [self.path fill]; self.path = [UIBezierPath bezierPathWithArcCenter:self.endP radius:4 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; [[UIColor blackColor] setStroke]; [self.path fill]; self.path = [UIBezierPath bezierPathWithArcCenter:self.controlP1 radius:3 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; self.path.lineWidth = 2; [[UIColor blackColor] setStroke]; [self.path stroke]; self.path = [UIBezierPath bezierPathWithArcCenter:self.controlP2 radius:3 startAngle:0 endAngle:M_PI * 2 clockwise:YES]; self.path.lineWidth = 2; [[UIColor blackColor] setStroke]; [self.path stroke]; CGRect startMsgRect = CGRectMake(self.startP.x + 8, self.startP.y - 7, 50, 20); [@"起始点" drawInRect:startMsgRect withAttributes:nil]; CGRect endMsgRect = CGRectMake(self.endP.x + 8, self.endP.y - 7, 50, 20); [@"终止点" drawInRect:endMsgRect withAttributes:nil]; CGRect control1MsgRect = CGRectMake(self.controlP1.x + 8, self.controlP1.y - 7, 50, 20); [@"控制点1" drawInRect:control1MsgRect withAttributes:nil]; CGRect control2MsgRect = CGRectMake(self.controlP2.x + 8, self.controlP2.y - 7, 50, 20); [@"控制点2" drawInRect:control2MsgRect withAttributes:nil]; } /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; CGRect startR = CGRectMake(self.startP.x - 4, self.startP.y - 4, 8, 8); CGRect endR = CGRectMake(self.endP.x - 4, self.endP.y - 4, 8, 8); CGRect controlR1 = CGRectMake(self.controlP1.x - 4, self.controlP1.y - 4, 8, 8); CGRect controlR2 = CGRectMake(self.controlP2.x - 4, self.controlP2.y - 4, 8, 8); // 判断当前触摸点 if (CGRectContainsPoint(startR, startPoint)) { self.currentTouchP = 1; } else if (CGRectContainsPoint(endR, startPoint)) { self.currentTouchP = 2; } else if (CGRectContainsPoint(controlR1, startPoint)) { self.currentTouchP = 3; } else if (CGRectContainsPoint(controlR2, startPoint)) { self.currentTouchP = 4; } } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; // 限制触摸点的边界 if (touchPoint.x < 0) { touchPoint.x = 0; } if (touchPoint.x > self.bounds.size.width) { touchPoint.x = self.bounds.size.width; } if (touchPoint.y < 0) { touchPoint.y = 0; } if (touchPoint.y > self.bounds.size.height) { touchPoint.y = self.bounds.size.height; } // 设置当前触摸点的值 switch (self.currentTouchP) { case 1: self.startP = touchPoint; break; case 2: self.endP = touchPoint; break; case 3: self.controlP1 = touchPoint; break; case 4: self.controlP2 = touchPoint; break; default: break; } // 刷新 [self setNeedsDisplay]; } /// 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 释放之前的触摸点 self.currentTouchP = 0; } /// 触摸取消 - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event { [self touchesEnded:touches withEvent:event]; } @end ViewController.m CGRect frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, 400); QBezierPathView *pathView = [[QBezierPathView alloc] initWithFrame:frame]; pathView.backgroundColor = [UIColor whiteColor]; pathView.layer.borderWidth = 1; [self.view addSubview:pathView]; 效果
1、Charts 简介 使用第三方框架 Charts 绘制 iOS 图表。GitHub 源码 Charts Charts 是一款用于绘制图表的框架,可以绘制柱状图、折线图、K线图、饼状图等。Charts 只有 Swift 版本。 LineChart (with legend, simple design) LineChart (with legend, simple design) LineChart (cubic lines) LineChart (gradient fill) Combined-Chart (bar- and linechart in this case) BarChart (with legend, simple design) BarChart (grouped DataSets) Horizontal-BarChart PieChart (with selection, ...) ScatterChart (with squares, triangles, circles, ... and more) CandleStickChart (for financial data) BubbleChart (area covered by bubbles indicates the value) RadarChart (spider web chart) 2、在项目中集成 Charts 2.1 在 OC 项目中手动导入 Charts 框架 1、下载 Charts 框架。 解压后的文件夹里面的内容是这个样子的,如下图。 下载完成后,仔细看一下所需环境,很重要!如下图。 在自己的工程中使用的时候可以删掉框架中的 Demo 等文件,如下图。 2、新建工程,导入 Charts.xcodeproj 工程 新建工程,取名为 ChartsDemo。复制 Charts 整个文件到 ChartsDemo 工程中 将 Charts 文件夹中的 Charts.xcodeproj 工程文件导入到 ChartsDemo 工程中,注意导入的是 Charts.xcodeproj 工程,而不是 Charts 的文件夹,如下图。 3、编译导入的 Charts.xcodeproj 工程 选中 Charts.xcodeproj 工程,在 TARGETS => Build Setting => Architectures => Base SDK 中设置 SDKs 为 iOS 环境。 在编译选项中选中 Charts => Generic iOS Devices 进行编译,编译成功后可以看到 Charts.xcodeproj 工程的 Products 中 Charts.framework 由红色变为黑色。 4、添加 Charts.framework 在 ChartsDemo 工程的 TARGETS => General => Embedded Binaries 中,点击 + 号添加 iOS 的 Charts.framework,如下图。 5、建立 OC 和 Swift 的桥接文件 在 ChartsDemo 工程中新建一个 Swift 文件,名字随便取,这时候会提示是否建立桥接文件,直接选 Create Bridging Header 选项,如下图。 创建完成后删除刚才创建的 Swift 文件,然后在桥接头文件中添加 Charts 引入。 在使用 Charts 的文件中引入桥接头文件。引入完成之后,选择 iOS 模拟器编译一下,如果有错,Clean 一下再次编译,编译没有错误说明导入成功。 6、测试 在 ViewController.m 中进行测试,代码如下。 BarChartView *barChatView = [[BarChartView alloc] initWithFrame:CGRectMake(10, 100, 300, 300)]; barChatView.backgroundColor = [UIColor cyanColor]; [self.view addSubview:barChatView]; 效果如下。由于没有给数据,所以显示的是 No chart date aviailable。至此,集成 Charts 完毕。 3、绘制折线图 3.1 LineChart 设置 1、属性设置 在控制器中我们首先需要创建一个 LineChartView 的对象,在 Charts 框架中折线图用到的类是 LineChartView。 self.lineChartView = [[LineChartView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width - 10, 400)]; self.lineChartView.center = self.view.center; [self.view addSubview:self.lineChartView]; // 设置基本样式 self.lineChartView.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; self.lineChartView.delegate = self; // 设置代理,需遵守 ChartViewDelegate 协议 self.lineChartView.noDataText = @"暂无数据"; // 设置交互样式 self.lineChartView.scaleYEnabled = NO; // 取消 Y 轴缩放 self.lineChartView.doubleTapToZoomEnabled = NO; // 取消双击缩放 self.lineChartView.dragEnabled = YES; // 启用拖拽图标 self.lineChartView.dragDecelerationEnabled = YES; // 拖拽后是否有惯性效果 self.lineChartView.dragDecelerationFrictionCoef = 0.9; // 拖拽后惯性效果的摩擦系数(0~1),数值越小,惯性越不明显 在 LineChartView 中有两个属性 rightAxis 和 leftAxis 属于 ChartYAxis 类,是分别用来设置左边 Y 轴和右边 Y 轴的,可以根据自己的需求去设置。 self.lineChartView.rightAxis.enabled = NO; // 不绘制右边轴 ChartYAxis *leftAxis = self.lineChartView.leftAxis; // 获取左边 Y 轴 leftAxis.inverted = NO; // 是否将 Y 轴进行上下翻转 leftAxis.axisMinValue = 0; // 设置 Y 轴的最小值 leftAxis.axisMaxValue = 105; // 设置 Y 轴的最大值 leftAxis.axisLineWidth = 1.0 / [UIScreen mainScreen].scale; // 设置 Y 轴线宽 leftAxis.axisLineColor = [UIColor blackColor]; // 设置 Y 轴颜色 leftAxis.labelPosition = YAxisLabelPositionOutsideChart; // label 文字位置 leftAxis.valueFormatter = [[YAxisValueFormatter alloc] init]; // label 文字样式,自定义格式,默认时不显示特殊符号 leftAxis.labelTextColor = [self colorWithHexString:@"#057748"]; // label 文字颜色 leftAxis.labelFont = [UIFont systemFontOfSize:10.0f]; // label 文字字体 leftAxis.labelCount = 5; // label 数量,数值不一定, // 如果 forceLabelsEnabled 等于 YES, // 则强制绘制制定数量的 label, 但是可能不平均 leftAxis.forceLabelsEnabled = NO; // 不强制绘制指定数量的 label leftAxis.gridLineDashLengths = @[@3.0f, @3.0f]; // 设置虚线样式的网格线 leftAxis.gridColor = [UIColor colorWithRed:200/255.0f green:200/255.0f blue:200/255.0f alpha:1]; // 网格线颜色 leftAxis.gridAntialiasEnabled = YES; // 网格线开启抗锯齿 在这里要注意的是,一般情况下 y 轴的数据是 double 类型的并且是没有特殊符号的,如果想要做到像图中的那样的百分数类型是要去设置的,然后我们发现 ChartYAxis 类中有一个属性 valueFormatter,这个属性就是用来设置数据格式的,但比较麻烦的是这个属性必须遵循 IChartAxisValueFormatter 的协议,所以我们需要去自定义一个 YAxisValueFormatter 类来完成。 @import Charts; @interface YAxisValueFormatter : NSObject <IChartAxisValueFormatter> @end @implementation YAxisValueFormatter /// 实现协议方法,返回 y 轴的数据 - (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis { // value 为 y 轴的值 return [NSString stringWithFormat:@"%ld%%",(NSInteger)value]; } @end y 轴设置完了,现在我们来设置 x 轴,在 LineChartView 中也有个属性 xAxis 使用来设置 x 轴的样式的,它属于 ChartXAxis 类,现在我们来创建它。 ChartXAxis *xAxis = self.lineChartView.xAxis; xAxis.labelPosition = XAxisLabelPositionBottom; // 设置 X 轴的显示位置,默认是显示在上面的 xAxis.axisLineWidth = 1.0 / [UIScreen mainScreen].scale; // 设置 X 轴线宽 xAxis.axisLineColor = [UIColor blackColor]; // 设置 X 轴颜色 xAxis.granularityEnabled = YES; // 设置重复的值不显示 xAxis.valueFormatter = [[XAxisValueFormatter alloc] init]; // label 文字样式,自定义格式,默认时不显示特殊符号 xAxis.labelTextColor = [self colorWithHexString:@"#057748"]; // label 文字颜色 xAxis.drawGridLinesEnabled = NO; // 不绘制网格线 xAxis.gridColor = [UIColor clearColor]; // 网格线颜色 同样的一般情况下 x 轴的数据也是 double 类型的并且是没有特殊符号的,所以我们需要去自定义一个 XAxisValueFormatter 类来完成。 #import <Foundation/Foundation.h> @import Charts; @interface XAxisValueFormatter : NSObject <IChartAxisValueFormatter> @end @implementation XAxisValueFormatter /// 实现协议方法,返回 x 轴的数据 - (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis { // value 为 x 轴的值 return [NSString stringWithFormat:@"%ld天", (NSInteger)value]; } @end 到这里我们 lineChartView 的 x 轴和 y 轴都设置好了,但是还少了一个选中数据滑动的时候值会变化的标签, LineChartView 中有个 marker 属性就是我们要找的,marker 属性遵循 IChartMarker 协议,是 id 类型,ChartMarkerView 和 ChartMarkerImage 都有遵循 IChartMarker 协议,都是可以用的,可以根据自己的需求自己选择。 ChartMarkerView *markerY = [[ChartMarkerView alloc] init]; markerY.offset = CGPointMake(-999, -8); markerY.chartView = self.lineChartView; [markerY addSubview:self.markYLabel]; self.lineChartView.marker = markerY; - (UILabel *)markYLabel { if (!_markYLabel) { _markYLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, -5, 35, 25)]; _markYLabel.font = [UIFont systemFontOfSize:15.0]; _markYLabel.textAlignment = NSTextAlignmentCenter; _markYLabel.text = @""; _markYLabel.textColor = [UIColor whiteColor]; _markYLabel.backgroundColor = [UIColor grayColor]; } return _markYLabel; } 2、数据设置 折线图的数据 data 属性是属于 LineChartData 类,所以 setData 方法需要返回一个 LineChartData 的对象,但是在 LineChartData 的对象的初始化方法中 - (nonnull instancetype)initWithDataSets:(NSArray<id <IChartDataSet>> * _Nullable)dataSets 发现还需要一个装有 LineChartDataSet 的数组,LineChartDataSet 其实就是每一条折线,而绘制每一条折线又需要数据,所以我们先从数据开始一步一步创建。 NSMutableArray *chartVals1 = [[NSMutableArray alloc] init]; for (int i = 0; i < vals_count; i++) { double xValue = i + 1; double yValue = arc4random() % 100; // 设置表格数据,x 轴和 y 轴的值 ChartDataEntry *entry = [[ChartDataEntry alloc] initWithX:xValue y:yValue]; [chartVals1 addObject:entry]; } 数据完成后,我们就要来创建折线了,创建 LineChartDataSet 对象,如果要只显示数据的最高点,需要设置 valueFormatter 和前面的一样,自定义一个 ChartMaxDataValueFormatter 类来完成。 @import Charts; @interface ChartMaxDataValueFormatter : NSObject <IChartValueFormatter> - (instancetype)initWithYDataVals:(NSArray *)yVals; @end @interface ChartMaxDataValueFormatter () @property (nonatomic, strong) NSArray *yDataValueArray; @property (nonatomic, assign) double maxDataSetIndex; @end @implementation ChartMaxDataValueFormatter - (instancetype)initWithYDataVals:(NSArray *)yVals { if (self = [super init]) { self.yDataValueArray = yVals; NSMutableArray *muArr = [NSMutableArray arrayWithArray:yVals]; [muArr sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { ChartDataEntry *entry1 =(ChartDataEntry *)obj1; ChartDataEntry *entry2 =(ChartDataEntry *)obj2; if (entry1.y >= entry2.y){ return NSOrderedAscending; } else { return NSOrderedDescending; } }]; self.maxDataSetIndex =((ChartDataEntry * )muArr[0]).x; } return self; } /// 实现协议方法,只显示折线上数据的最大值 - (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler { if (entry.x == self.maxDataSetIndex) { return [NSString stringWithFormat:@"%ld%%", (NSInteger)entry.y]; } else { return @""; } } @end 最后还有个 ChartView 的选中数据代理方法,主要用来显示选中折线数据的时候 label 值的变化。 - (void)chartValueSelected:(ChartViewBase * _Nonnull)chartView entry:(ChartDataEntry * _Nonnull)entry highlight:(ChartHighlight * _Nonnull)highlight { // 设置滑动时 Y 值标签 self.markYLabel.text = [NSString stringWithFormat:@"%ld%%", (NSInteger)entry.y]; // 将点击的数据滑动到中间 [self.lineChartView centerViewToAnimatedWithXValue:entry.x yValue:entry.y axis:[self.lineChartView.data getDataSetByIndex:highlight.dataSetIndex].axisDependency duration:1.0]; } 3.2 LineChart 绘制 XAxisValueFormatter.h /// 设置 X 轴 数据格式 @import Charts; @interface XAxisValueFormatter : NSObject <IChartAxisValueFormatter> @end XAxisValueFormatter.m @implementation XAxisValueFormatter /// 实现协议方法,返回 x 轴的数据 - (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis { // value 为 x 轴的值 return [NSString stringWithFormat:@"%ld天", (NSInteger)value]; } @end YAxisValueFormatter.h /// 设置 Y 轴 数据格式 @import Charts; @interface YAxisValueFormatter : NSObject <IChartAxisValueFormatter> @end YAxisValueFormatter.m @implementation YAxisValueFormatter /// 实现协议方法,返回 y 轴的数据 - (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis { // value 为 y 轴的值 return [NSString stringWithFormat:@"%ld%%",(NSInteger)value]; } @end ChartDataValueFormatter.h /// 设置 折线显示的 数据格式 @import Charts; @interface ChartDataValueFormatter : NSObject <IChartValueFormatter> @end ChartDataValueFormatter.m @implementation ChartDataValueFormatter /// 实现协议方法,返回折线拐点上显示的数据格式 - (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler { return [NSString stringWithFormat:@"%.1f", entry.y]; } @end ChartMaxDataValueFormatter.h /// 设置 折线显示的 数据格式,只显示最大值 @import Charts; @interface ChartMaxDataValueFormatter : NSObject <IChartValueFormatter> - (instancetype)initWithYDataVals:(NSArray *)yVals; @end ChartMaxDataValueFormatter.m @interface ChartMaxDataValueFormatter () @property (nonatomic, strong) NSArray *yDataValueArray; @property (nonatomic, assign) double maxDataSetIndex; @end @implementation ChartMaxDataValueFormatter - (instancetype)initWithYDataVals:(NSArray *)yVals { if (self = [super init]) { self.yDataValueArray = yVals; NSMutableArray *muArr = [NSMutableArray arrayWithArray:yVals]; [muArr sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { ChartDataEntry *entry1 =(ChartDataEntry *)obj1; ChartDataEntry *entry2 =(ChartDataEntry *)obj2; if (entry1.y >= entry2.y){ return NSOrderedAscending; } else { return NSOrderedDescending; } }]; self.maxDataSetIndex =((ChartDataEntry * )muArr[0]).x; } return self; } /// 实现协议方法,只显示折线上数据的最大值 - (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler { if (entry.x == self.maxDataSetIndex) { return [NSString stringWithFormat:@"%ld%%", (NSInteger)entry.y]; } else { return @""; } } @end ViewController.m #import "ChartsDemo-Bridging-Header.h" // 自定义数据格式 #import "YAxisValueFormatter.h" #import "XAxisValueFormatter.h" #import "ChartDataValueFormatter.h" #import "ChartMaxDataValueFormatter.h" // 遵守协议 @interface ViewController () <ChartViewDelegate> // 折线图 @property (nonatomic, strong) LineChartView *lineChartView; // 滑动时 Y 值标签 @property (nonatomic,strong) UILabel *markYLabel; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; // 创建折线图 [self createdLineView]; // 设置折线图的数据 self.lineChartView.data = [self setData]; } // 创建折线图 - (void)createdLineView { // 初始化折线图对象 self.lineChartView = [[LineChartView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width - 10, 400)]; self.lineChartView.center = self.view.center; [self.view addSubview:self.lineChartView]; // 设置基本样式 self.lineChartView.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; self.lineChartView.delegate = self; // 设置代理,需遵守 ChartViewDelegate 协议 self.lineChartView.noDataText = @"暂无数据"; // 设置交互样式 self.lineChartView.scaleYEnabled = NO; // 取消 Y 轴缩放 self.lineChartView.doubleTapToZoomEnabled = NO; // 取消双击缩放 self.lineChartView.dragEnabled = YES; // 启用拖拽图标 self.lineChartView.dragDecelerationEnabled = YES; // 拖拽后是否有惯性效果 self.lineChartView.dragDecelerationFrictionCoef = 0.9; // 拖拽后惯性效果的摩擦系数(0~1),数值越小,惯性越不明显 // 设置滑动时 Y 值标签 ChartMarkerView *markerY = [[ChartMarkerView alloc] init]; markerY.offset = CGPointMake(-999, -8); markerY.chartView = self.lineChartView; [markerY addSubview:self.markYLabel]; self.lineChartView.marker = markerY; // 设置 X 轴样式 ChartXAxis *xAxis = self.lineChartView.xAxis; xAxis.labelPosition = XAxisLabelPositionBottom; // 设置 X 轴的显示位置,默认是显示在上面的 xAxis.axisLineWidth = 1.0 / [UIScreen mainScreen].scale; // 设置 X 轴线宽 xAxis.axisLineColor = [UIColor blackColor]; // 设置 X 轴颜色 xAxis.granularityEnabled = YES; // 设置重复的值不显示 xAxis.valueFormatter = [[XAxisValueFormatter alloc] init]; // label 文字样式,自定义格式,默认时不显示特殊符号 xAxis.labelTextColor = [self colorWithHexString:@"#057748"]; // label 文字颜色 xAxis.drawGridLinesEnabled = NO; // 不绘制网格线 xAxis.gridColor = [UIColor clearColor]; // 网格线颜色 // 设置 Y 轴样式 self.lineChartView.rightAxis.enabled = NO; // 不绘制右边轴 ChartYAxis *leftAxis = self.lineChartView.leftAxis; // 获取左边 Y 轴 leftAxis.inverted = NO; // 是否将 Y 轴进行上下翻转 leftAxis.axisMinValue = 0; // 设置 Y 轴的最小值 leftAxis.axisMaxValue = 105; // 设置 Y 轴的最大值 leftAxis.axisLineWidth = 1.0 / [UIScreen mainScreen].scale; // 设置 Y 轴线宽 leftAxis.axisLineColor = [UIColor blackColor]; // 设置 Y 轴颜色 leftAxis.labelPosition = YAxisLabelPositionOutsideChart; // label 文字位置 leftAxis.valueFormatter = [[YAxisValueFormatter alloc] init]; // label 文字样式,自定义格式,默认时不显示特殊符号 leftAxis.labelTextColor = [self colorWithHexString:@"#057748"]; // label 文字颜色 leftAxis.labelFont = [UIFont systemFontOfSize:10.0f]; // label 文字字体 leftAxis.labelCount = 5; // label 数量,数值不一定, // 如果 forceLabelsEnabled 等于 YES, // 则强制绘制制定数量的 label, 但是可能不平均 leftAxis.forceLabelsEnabled = NO; // 不强制绘制指定数量的 label leftAxis.gridLineDashLengths = @[@3.0f, @3.0f]; // 设置虚线样式的网格线 leftAxis.gridColor = [UIColor colorWithRed:200/255.0f green:200/255.0f blue:200/255.0f alpha:1]; // 网格线颜色 leftAxis.gridAntialiasEnabled = YES; // 网格线开启抗锯齿 // 添加限制线 ChartLimitLine *limitLine = [[ChartLimitLine alloc] initWithLimit:80 label:@"限制线"]; // 设置限制值和标题 limitLine.lineWidth = 2; // 限制线的宽度 limitLine.lineColor = [UIColor greenColor]; // 限制线的颜色 limitLine.lineDashLengths = @[@5.0f, @5.0f]; // 虚线样式 limitLine.labelPosition = ChartLimitLabelPositionRightTop; // label 位置 limitLine.valueTextColor = [self colorWithHexString:@"#057748"]; // label 文字颜色 limitLine.valueFont = [UIFont systemFontOfSize:12]; // label 字体 [leftAxis addLimitLine:limitLine]; // 添加到 Y 轴上 leftAxis.drawLimitLinesBehindDataEnabled = YES; // 设置限制线绘制在折线图的下面 // 设置折线图描述 self.lineChartView.chartDescription.enabled = YES; // 显示折线图描述,默认 YES 显示 self.lineChartView.descriptionText = @"折线图"; // 折线图描述 self.lineChartView.descriptionFont = [UIFont systemFontOfSize:15]; // 折线图描述字体 self.lineChartView.descriptionTextColor = [UIColor darkGrayColor]; // 折线图描述颜色 // 设置折线图图例 self.lineChartView.legend.enabled = YES; // 显示图例,默认 YES 显示 self.lineChartView.legend.form = ChartLegendFormLine; // 图例的样式 self.lineChartView.legend.formSize = 30; // 图例中线条的长度 self.lineChartView.legend.textColor = [UIColor darkGrayColor]; // 图例文字颜色 // 设置动画效果,可以设置 X 轴和 Y 轴的动画效果 [self.lineChartView animateWithXAxisDuration:1.0f]; } // 设置折线图数据 - (LineChartData *)setData{ int vals_count = 50; // 要显示多少条数据 // 设置表格数据,包含 x 值和 y 值 NSMutableArray *chartVals1 = [[NSMutableArray alloc] init]; for (int i = 0; i < vals_count; i++) { double xValue = i + 1; double yValue = arc4random() % 100; // 设置表格数据,x 轴和 y 轴的值 ChartDataEntry *entry = [[ChartDataEntry alloc] initWithX:xValue y:yValue]; [chartVals1 addObject:entry]; } // 绘制折线图 LineChartDataSet *set1 = nil; if (self.lineChartView.data.dataSetCount > 0) { LineChartData *data = (LineChartData *)self.lineChartView.data; set1 = (LineChartDataSet *)data.dataSets[0]; set1.values = chartVals1; // set1.valueFormatter = [[ChartMaxDataValueFormatter alloc] initWithYDataVals:chartVals1]; // 设置数据显示的格式,只显示最大值 return data; } else { // 创建 LineChartDataSet 对象 set1 = [[LineChartDataSet alloc] initWithValues:chartVals1 label:@"lineName 1"]; // label 折线图图例名称 // 设置折线的样式 set1.lineWidth = 1.0 / [UIScreen mainScreen].scale; // 折线宽度 [set1 setColor:[self colorWithHexString:@"#007FFF"]]; // 折线颜色 set1.drawValuesEnabled = YES; // 是否在拐点处显示数据,默认 YES set1.valueColors = @[[UIColor brownColor]]; // 折线拐点处显示数据的颜色 set1.drawSteppedEnabled = NO; // 是否开启绘制阶梯样式的折线图,默认 NO // set1.valueFormatter = [[ChartMaxDataValueFormatter alloc] initWithYDataVals:chartVals1]; // 设置数据显示的格式,只显示最大值 // 折线拐点样式 set1.drawCirclesEnabled = YES; // 是否绘制拐点,默认 YES set1.circleRadius = 4.0f; // 拐点半径 set1.circleColors = @[[UIColor redColor], [UIColor greenColor]]; // 拐点颜色 // 拐点中间的空心样式 set1.drawCircleHoleEnabled = YES; // 是否绘制中间的空心,默认 YES set1.circleHoleRadius = 2.0f; // 空心的半径 set1.circleHoleColor = [UIColor blackColor]; // 空心的颜色 // 折线的颜色填充样式 // 第一种填充样式:单色填充 // set1.drawFilledEnabled = YES; // 是否填充颜色,默认 NO // set1.fillColor = [UIColor redColor]; // 填充颜色 // set1.fillAlpha = 0.3; // 填充颜色的透明度 // 第二种填充样式:渐变填充 set1.drawFilledEnabled = YES; // 是否填充颜色,默认 NO NSArray *gradientColors = @[(id)[ChartColorTemplates colorFromString:@"#FFFFFFFF"].CGColor, (id)[ChartColorTemplates colorFromString:@"#FF007FFF"].CGColor]; CGGradientRef gradientRef = CGGradientCreateWithColors(nil, (CFArrayRef)gradientColors, nil); set1.fill = [ChartFill fillWithLinearGradient:gradientRef angle:90.0f]; // 赋值填充颜色对象 CGGradientRelease(gradientRef); // 释放 gradientRef set1.fillAlpha = 0.3f; // 透明度 // 点击选中拐点的交互样式 set1.highlightEnabled = YES; // 选中拐点,是否开启高亮效果(显示十字线),默认 YES set1.highlightColor = [self colorWithHexString:@"#c83c23"]; // 点击选中拐点的十字线的颜色 set1.highlightLineWidth = 1.0 / [UIScreen mainScreen].scale; // 十字线宽度 set1.highlightLineDashLengths = @[@5, @5]; // 十字线的虚线样式 // 将 LineChartDataSet 对象放入数组中 NSMutableArray *dataSets = [[NSMutableArray alloc] init]; [dataSets addObject:set1]; // 添加第二个 LineChartDataSet 对象 LineChartDataSet *set2 = [set1 copy]; set2.label = @"lineName 2"; NSMutableArray *chartVals2 = [[NSMutableArray alloc] init]; for (int i = 0; i < vals_count; i++) { double xValue = i + 1; double yValue = arc4random() % 100; ChartDataEntry *entry = [[ChartDataEntry alloc] initWithX:xValue y:yValue]; [chartVals2 addObject:entry]; } set2.values = chartVals2; [set2 setColor:[UIColor redColor]]; set2.drawFilledEnabled = YES; set2.fillColor = [UIColor redColor]; set2.fillAlpha = 0.1; [dataSets addObject:set2]; // 创建 LineChartData 对象, 此对象就是 lineChartView 需要最终数据对象 LineChartData *data = [[LineChartData alloc] initWithDataSets:dataSets]; [data setValueFont:[UIFont fontWithName:@"HelveticaNeue-Light" size:8.f]]; // 文字字体 [data setValueTextColor:[UIColor grayColor]]; // 文字颜色 // 自定义数据显示格式 ChartDataValueFormatter *formatter = [[ChartDataValueFormatter alloc] init]; // 统一设置数据显示的格式 [data setValueFormatter:formatter]; return data; } } #pragma mark - ChartViewDelegate // 点击选中折线拐点时回调 - (void)chartValueSelected:(ChartViewBase * _Nonnull)chartView entry:(ChartDataEntry * _Nonnull)entry highlight:(ChartHighlight * _Nonnull)highlight { // 设置滑动时 Y 值标签 self.markYLabel.text = [NSString stringWithFormat:@"%ld%%", (NSInteger)entry.y]; // 将点击的数据滑动到中间 [self.lineChartView centerViewToAnimatedWithXValue:entry.x yValue:entry.y axis:[self.lineChartView.data getDataSetByIndex:highlight.dataSetIndex].axisDependency duration:1.0]; } // 没有选中折线拐点时回调 - (void)chartValueNothingSelected:(ChartViewBase * _Nonnull)chartView { } // 放大折线图时回调 - (void)chartScaled:(ChartViewBase * _Nonnull)chartView scaleX:(CGFloat)scaleX scaleY:(CGFloat)scaleY { } // 拖拽折线图时回调 - (void)chartTranslated:(ChartViewBase * _Nonnull)chartView dX:(CGFloat)dX dY:(CGFloat)dY { } // 懒加载滑动时 Y 值标签 - (UILabel *)markYLabel { if (!_markYLabel) { _markYLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, -5, 35, 25)]; _markYLabel.font = [UIFont systemFontOfSize:15.0]; _markYLabel.textAlignment = NSTextAlignmentCenter; _markYLabel.text = @""; _markYLabel.textColor = [UIColor whiteColor]; _markYLabel.backgroundColor = [UIColor grayColor]; } return _markYLabel; } // 由十六进制创建颜色 - (UIColor *)colorWithHexString:(NSString *)color { NSString *cString = [[color stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString]; // String should be 6 or 8 characters if ([cString length] < 6) { return [UIColor clearColor]; } // strip "0X" or "#" if it appears if ([cString hasPrefix:@"0X"]) cString = [cString substringFromIndex:2]; if ([cString hasPrefix:@"#"]) cString = [cString substringFromIndex:1]; if ([cString length] != 6) return [UIColor clearColor]; // Separate into r, g, b substrings NSRange range; range.location = 0; range.length = 2; // r NSString *rString = [cString substringWithRange:range]; // g range.location = 2; NSString *gString = [cString substringWithRange:range]; // b range.location = 4; NSString *bString = [cString substringWithRange:range]; // Scan values unsigned int r, g, b; [[NSScanner scannerWithString:rString] scanHexInt:&r]; [[NSScanner scannerWithString:gString] scanHexInt:&g]; [[NSScanner scannerWithString:bString] scanHexInt:&b]; return [UIColor colorWithRed:((float) r / 255.0f) green:((float) g / 255.0f) blue:((float) b / 255.0f) alpha:1.0f]; } @end 效果 如果绘制光滑的曲线图,可以设置 set 的 mode 值为 LineChartModeCubicBezier,不设置 drawValuesEnabled 的值。 // 绘制光滑曲线 set1.mode = LineChartModeCubicBezier; set1.cubicIntensity = 0.2; // 是否开启绘制阶梯样式的折线图,默认 NO // set1.drawSteppedEnabled = NO; 效果 4、绘制柱状图 XAxisValueFormatter.h /// 设置 X 轴 数据格式 @import Charts; @interface XAxisValueFormatter : NSObject <IChartAxisValueFormatter> @end XAxisValueFormatter.m @implementation XAxisValueFormatter /// 实现协议方法,返回 x 轴的数据 - (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis { // value 为 x 轴的值 return [NSString stringWithFormat:@"%ld天", (NSInteger)value]; } @end YAxisValueFormatter.h /// 设置 Y 轴 数据格式 @import Charts; @interface YAxisValueFormatter : NSObject <IChartAxisValueFormatter> @end YAxisValueFormatter.m @implementation YAxisValueFormatter /// 实现协议方法,返回 y 轴的数据 - (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis { // value 为 y 轴的值 return [NSString stringWithFormat:@"%ld%%",(NSInteger)value]; } @end ChartDataValueFormatter.h /// 设置 柱形上显示的 数据格式 @import Charts; @interface ChartDataValueFormatter : NSObject <IChartValueFormatter> @end ChartDataValueFormatter.m @implementation ChartDataValueFormatter /// 实现协议方法,返回柱形上显示的数据格式 - (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler { return [NSString stringWithFormat:@"%.1f", entry.y]; } @end ChartMaxDataValueFormatter.h /// 设置 柱形上显示的 数据格式,只显示最大值 @import Charts; @interface ChartMaxDataValueFormatter : NSObject <IChartValueFormatter> - (instancetype)initWithYDataVals:(NSArray *)yVals; @end ChartMaxDataValueFormatter.m @interface ChartMaxDataValueFormatter () @property (nonatomic, strong) NSArray *yDataValueArray; @property (nonatomic, assign) double maxDataSetIndex; @end @implementation ChartMaxDataValueFormatter - (instancetype)initWithYDataVals:(NSArray *)yVals { if (self = [super init]) { self.yDataValueArray = yVals; NSMutableArray *muArr = [NSMutableArray arrayWithArray:yVals]; [muArr sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { ChartDataEntry *entry1 =(ChartDataEntry *)obj1; ChartDataEntry *entry2 =(ChartDataEntry *)obj2; if (entry1.y >= entry2.y){ return NSOrderedAscending; } else { return NSOrderedDescending; } }]; self.maxDataSetIndex =((ChartDataEntry * )muArr[0]).x; } return self; } /// 实现协议方法,只显示柱形上数据的最大值 - (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler { if (entry.x == self.maxDataSetIndex) { return [NSString stringWithFormat:@"%ld%%", (NSInteger)entry.y]; } else { return @""; } } @end ViewController.m #import "ChartsDemo-Bridging-Header.h" // 自定义数据格式 #import "YAxisValueFormatter.h" #import "XAxisValueFormatter.h" #import "ChartDataValueFormatter.h" #import "ChartMaxDataValueFormatter.h" // 遵守协议 @interface ViewController () <ChartViewDelegate> // 柱状图 @property (nonatomic, strong) BarChartView *barChartView; // 滑动时 Y 值标签 @property (nonatomic,strong) UILabel *markYLabel; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; // 创建柱状图 [self createdBarView]; // 设置柱状图的数据 self.barChartView.data = [self setData]; } // 创建柱状图 - (void)createdBarView { // 初始化柱状图对象 self.barChartView = [[BarChartView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width - 10, 400)]; self.barChartView.center = self.view.center; [self.view addSubview:self.barChartView]; // 设置基本样式 self.barChartView.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; self.barChartView.delegate = self; // 设置代理,需遵守 ChartViewDelegate 协议 self.barChartView.noDataText = @"暂无数据"; // 没有数据时的文字提示 self.barChartView.drawValueAboveBarEnabled = YES; // 数值显示在柱形的上面还是下面 self.barChartView.drawBarShadowEnabled = NO; // 是否绘制柱形的阴影背景 // 设置交互样式 self.barChartView.scaleYEnabled = NO; // 取消 Y 轴缩放 self.barChartView.doubleTapToZoomEnabled = NO; // 取消双击缩放 self.barChartView.dragEnabled = YES; // 启用拖拽图标 self.barChartView.dragDecelerationEnabled = YES; // 拖拽后是否有惯性效果 self.barChartView.dragDecelerationFrictionCoef = 0.9; // 拖拽后惯性效果的摩擦系数(0~1),数值越小,惯性越不明显 // 设置滑动时 Y 值标签 ChartMarkerView *markerY = [[ChartMarkerView alloc] init]; markerY.offset = CGPointMake(-17, -25); // 设置显示位置 markerY.chartView = self.barChartView; [markerY addSubview:self.markYLabel]; self.barChartView.marker = markerY; // 设置 X 轴样式 ChartXAxis *xAxis = self.barChartView.xAxis; xAxis.labelPosition = XAxisLabelPositionBottom; // 设置 X 轴的显示位置,默认是显示在上面的 xAxis.axisLineWidth = 1.0 / [UIScreen mainScreen].scale; // 设置 X 轴线宽 xAxis.axisLineColor = [UIColor blackColor]; // 设置 X 轴颜色 xAxis.valueFormatter = [[XAxisValueFormatter alloc] init]; // label 文字样式,自定义格式,默认时不显示特殊符号 xAxis.labelTextColor = [UIColor brownColor]; // label 文字颜色 xAxis.drawGridLinesEnabled = NO; // 不绘制网格线 xAxis.gridColor = [UIColor clearColor]; // 网格线颜色 // 设置 Y 轴样式 self.barChartView.rightAxis.enabled = NO; // 不绘制右边轴 ChartYAxis *leftAxis = self.barChartView.leftAxis; // 获取左边 Y 轴 leftAxis.inverted = NO; // 是否将 Y 轴进行上下翻转 leftAxis.axisMinValue = 0; // 设置 Y 轴的最小值 leftAxis.axisMaxValue = 105; // 设置 Y 轴的最大值 leftAxis.axisLineWidth = 1.0 / [UIScreen mainScreen].scale; // 设置 Y 轴线宽 leftAxis.axisLineColor = [UIColor blackColor]; // 设置 Y 轴颜色 leftAxis.labelPosition = YAxisLabelPositionOutsideChart; // label 文字位置 leftAxis.valueFormatter = [[YAxisValueFormatter alloc] init]; // label 文字样式,自定义格式,默认时不显示特殊符号 leftAxis.labelTextColor = [UIColor brownColor]; // label 文字颜色 leftAxis.labelFont = [UIFont systemFontOfSize:10.0f]; // label 文字字体 leftAxis.labelCount = 5; // label 数量,数值不一定, // 如果 forceLabelsEnabled 等于 YES, // 则强制绘制制定数量的 label, 但是可能不平均 leftAxis.forceLabelsEnabled = NO; // 不强制绘制指定数量的 label leftAxis.gridLineDashLengths = @[@3.0f, @3.0f]; // 设置虚线样式的网格线 leftAxis.gridColor = [UIColor colorWithRed:200/255.0f green:200/255.0f blue:200/255.0f alpha:1]; // 网格线颜色 leftAxis.gridAntialiasEnabled = YES; // 网格线开启抗锯齿 // 添加限制线 ChartLimitLine *limitLine = [[ChartLimitLine alloc] initWithLimit:80 label:@"限制线"]; // 设置限制值和标题 limitLine.lineWidth = 2; // 限制线的宽度 limitLine.lineColor = [UIColor greenColor]; // 限制线的颜色 limitLine.lineDashLengths = @[@5.0f, @5.0f]; // 虚线样式 limitLine.labelPosition = ChartLimitLabelPositionRightTop; // label 位置 limitLine.valueTextColor = [UIColor grayColor]; // label 文字颜色 limitLine.valueFont = [UIFont systemFontOfSize:12]; // label 字体 [leftAxis addLimitLine:limitLine]; // 添加到 Y 轴上 leftAxis.drawLimitLinesBehindDataEnabled = YES; // 设置限制线绘制在柱状图的下面 // 设置柱状图描述 self.barChartView.chartDescription.enabled = YES; // 显示柱状图描述,默认 YES 显示 self.barChartView.descriptionText = @"柱状图"; // 柱状图描述 self.barChartView.descriptionFont = [UIFont systemFontOfSize:15]; // 柱状图描述字体 self.barChartView.descriptionTextColor = [UIColor darkGrayColor]; // 柱状图描述颜色 // 设置柱状图图例 self.barChartView.legend.enabled = NO; // 显示图例,默认 YES 显示 // 设置动画效果,可以设置 X 轴和 Y 轴的动画效果 [self.barChartView animateWithYAxisDuration:1.0f]; } // 设置柱状图数据 - (BarChartData *)setData { int vals_count = 50; // 要显示多少条数据 // 设置表格数据,包含 x 值和 y 值 NSMutableArray *chartVals1 = [[NSMutableArray alloc] init]; for (int i = 0; i < vals_count; i++) { double xValue = i + 1; double yValue = arc4random() % 100; // 设置表格数据,x 轴和 y 轴的值 BarChartDataEntry *entry = [[BarChartDataEntry alloc] initWithX:xValue y:yValue]; [chartVals1 addObject:entry]; } // 绘制柱状图 // 创建 BarChartDataSet 对象 BarChartDataSet *set1 = [[BarChartDataSet alloc] initWithValues:chartVals1 label:nil]; // 设置柱形的样式 [set1 setColors:ChartColorTemplates.material]; // 设置柱形图颜色 set1.drawValuesEnabled = YES; // 是否显示数据,默认 YES set1.valueColors = @[[UIColor brownColor]]; // 显示数据的颜色 set1.highlightEnabled = YES; // 点击选中柱形图是否有高亮效果,(双击空白处取消选中) // set1.valueFormatter = [[ChartMaxDataValueFormatter alloc] initWithYDataVals:chartVals1]; // 设置数据显示的格式,只显示最大值 // 将 BarChartDataSet 对象放入数组中 NSMutableArray *dataSets = [[NSMutableArray alloc] init]; [dataSets addObject:set1]; // 创建 BarChartData 对象, 此对象就是 BarChartData 需要最终数据对象 BarChartData *data = [[BarChartData alloc] initWithDataSets:dataSets]; [data setValueFont:[UIFont fontWithName:@"HelveticaNeue-Light" size:8.f]]; // 文字字体 [data setValueTextColor:[UIColor grayColor]]; // 文字颜色 // 自定义数据显示格式 ChartDataValueFormatter *formatter = [[ChartDataValueFormatter alloc] init];// 统一设置数据显示的格式 [data setValueFormatter:formatter]; return data; } #pragma mark - ChartViewDelegate // 点击选中柱形图时的代理方法 - (void)chartValueSelected:(ChartViewBase * _Nonnull)chartView entry:(ChartDataEntry * _Nonnull)entry highlight:(ChartHighlight * _Nonnull)highlight { // 设置滑动时 Y 值标签 self.markYLabel.text = [NSString stringWithFormat:@"%ld%%", (NSInteger)entry.y]; // 将点击的数据滑动到中间 [self.barChartView centerViewToAnimatedWithXValue:entry.x yValue:entry.y axis:[self.barChartView.data getDataSetByIndex:highlight.dataSetIndex].axisDependency duration:1.0]; } // 没有选中柱形图时的代理方法,当选中一个柱形图后,在空白处双击,就可以取消选择,此时会回调此方法 - (void)chartValueNothingSelected:(ChartViewBase * _Nonnull)chartView { } // 捏合放大或缩小柱形图时的代理方法 - (void)chartScaled:(ChartViewBase * _Nonnull)chartView scaleX:(CGFloat)scaleX scaleY:(CGFloat)scaleY { } // 拖拽图表时的代理方法 - (void)chartTranslated:(ChartViewBase * _Nonnull)chartView dX:(CGFloat)dX dY:(CGFloat)dY { } // 懒加载滑动时 Y 值标签 - (UILabel *)markYLabel { if (!_markYLabel) { _markYLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, -5, 35, 25)]; _markYLabel.font = [UIFont systemFontOfSize:15.0]; _markYLabel.textAlignment = NSTextAlignmentCenter; _markYLabel.text = @""; _markYLabel.textColor = [UIColor whiteColor]; _markYLabel.backgroundColor = [UIColor grayColor]; } return _markYLabel; } @end 效果 如果要显示水平柱状图,只需要把 BarChartView 换成 HorizontalBarChartView 即可。 // 声明柱状图 @property (nonatomic, strong) HorizontalBarChartView *barChartView; // 初始化柱状图对象 self.barChartView = [[HorizontalBarChartView alloc] initWithFrame:frame]; 效果 5、绘制饼图 ChartDataValueFormatter.h /// 设置 折线显示的 数据格式 @import Charts; @interface ChartDataValueFormatter : NSObject <IChartValueFormatter> @end ChartDataValueFormatter.m @implementation ChartDataValueFormatter /// 实现协议方法,返回折线拐点上显示的数据格式 - (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler { return [NSString stringWithFormat:@"%.0f%%", entry.y]; } @end ViewController.m #import "ChartsDemo-Bridging-Header.h" // 自定义数据格式 #import "ChartDataValueFormatter.h" // 遵守协议 @interface ViewController () <ChartViewDelegate> // 饼状图 @property (nonatomic, strong) PieChartView *pieChartView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; // 创建饼状图 [self createdPieView]; // 设置饼状图的数据 self.pieChartView.data = [self setData]; } // 创建饼状图 - (void)createdPieView { // 初始化饼状图对象 self.pieChartView = [[PieChartView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width - 10, 400)]; self.pieChartView.center = self.view.center; [self.view addSubview:self.pieChartView]; // 设置基本样式 self.pieChartView.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; self.pieChartView.delegate = self; // 设置代理,需遵守 ChartViewDelegate 协议 self.pieChartView.noDataText = @"暂无数据"; // 没有数据时的文字提示 [self.pieChartView setExtraOffsetsWithLeft:30 top:0 right:30 bottom:0]; // 饼状图距离边缘的间隙 self.pieChartView.usePercentValuesEnabled = YES; // 是否根据所提供的数据, 将显示数据转换为百分比格式 self.pieChartView.drawSliceTextEnabled = YES; // 是否显示区块文本 // 设置交互样式 self.pieChartView.dragDecelerationEnabled = YES; // 拖拽后是否有惯性效果 self.pieChartView.dragDecelerationFrictionCoef = 0.9; // 拖拽后惯性效果的摩擦系数(0~1),数值越小,惯性越不明显 // 设置饼状图中间的空心样式 // 空心有两个圆组成, 一个是 hole, 一个是 transparentCircle, transparentCircle 里面是 hole self.pieChartView.drawHoleEnabled = YES; // 饼状图是否是空心 self.pieChartView.holeColor = [UIColor clearColor]; // 空心颜色 self.pieChartView.holeRadiusPercent = 0.5; // 空心半径占比 self.pieChartView.transparentCircleRadiusPercent = 0.52; // 半透明空心半径占比 self.pieChartView.transparentCircleColor = [UIColor colorWithRed:210/255.0 green:145/255.0 blue:165/255.0 alpha:0.3]; // 半透明空心的颜色 // 设置饼状图中心的文本 if (self.pieChartView.isDrawHoleEnabled == YES) { self.pieChartView.drawCenterTextEnabled = YES; // 是否显示中间文字 // 普通文本 // self.pieChartView.centerText = @"饼状图"; // 中间文字 // 富文本 NSMutableAttributedString *centerText = [[NSMutableAttributedString alloc] initWithString:@"饼状图"]; [centerText setAttributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:16], NSForegroundColorAttributeName: [UIColor orangeColor]} range:NSMakeRange(0, centerText.length)]; self.pieChartView.centerAttributedText = centerText; } // 设置饼状图描述 self.pieChartView.chartDescription.enabled = YES; // 显示饼状图描述,默认 YES 显示 self.pieChartView.descriptionText = @"饼状图"; self.pieChartView.descriptionFont = [UIFont systemFontOfSize:15]; self.pieChartView.descriptionTextColor = [UIColor grayColor]; // 设置饼状图图例 self.pieChartView.legend.enabled = YES; // 显示图例,默认 YES 显示 self.pieChartView.legend.form = ChartLegendFormCircle; // 图示样式: 方形、线条、圆形 self.pieChartView.legend.formSize = 12; // 图示大小 self.pieChartView.legend.position = ChartLegendPositionBelowChartCenter; // 图例在饼状图中的位置 self.pieChartView.legend.maxSizePercent = 1; // 图例在饼状图中的大小占比, 这会影响图例的宽高 self.pieChartView.legend.formToTextSpace = 5; // 文本间隔 self.pieChartView.legend.font = [UIFont systemFontOfSize:10]; // 字体大小 self.pieChartView.legend.textColor = [UIColor grayColor]; // 字体颜色 // 设置动画效果,可以设置 X 轴和 Y 轴的动画效果 [self.pieChartView animateWithYAxisDuration:1.0f]; } // 设置饼状图数据 - (PieChartData *)setData { int vals_count = 5; // 饼状图总共有几块组成 // 每个区块的数据,包含 区块名称 和 区块数据 NSMutableArray *chartVals1 = [[NSMutableArray alloc] init]; for (int i = 0; i < vals_count; i++) { NSString *partLabel = [NSString stringWithFormat:@"part%d", i]; // 设置区块名称 double partValue = arc4random() % 100; // 设置区块值 PieChartDataEntry *entry = [[PieChartDataEntry alloc] initWithValue:partValue label:partLabel]; [chartVals1 addObject:entry]; } // 绘制饼状图 // 创建 PieChartDataSet 对象 PieChartDataSet *set1 = [[PieChartDataSet alloc] initWithValues:chartVals1 label:nil]; // 设置区块 NSMutableArray *colors = [[NSMutableArray alloc] init]; [colors addObjectsFromArray:ChartColorTemplates.vordiplom]; [colors addObjectsFromArray:ChartColorTemplates.joyful]; [colors addObjectsFromArray:ChartColorTemplates.colorful]; [colors addObjectsFromArray:ChartColorTemplates.liberty]; [colors addObjectsFromArray:ChartColorTemplates.pastel]; [colors addObject:[UIColor colorWithRed:51/255.f green:181/255.f blue:229/255.f alpha:1.f]]; set1.colors = colors; // 设置区块颜色 set1.sliceSpace = 0; // 相邻区块之间的间距 set1.selectionShift = 8; // 选中区块时, 放大的半径 // 设置区块名称 set1.xValuePosition = PieChartValuePositionInsideSlice; // 区块名称位置 set1.entryLabelColor = [UIColor redColor]; // 区块名称颜色 set1.entryLabelFont = [UIFont systemFontOfSize:15]; // 区块名称字体 // 设置区块数据 set1.drawValuesEnabled = YES; // 是否绘制显示数据 set1.yValuePosition = PieChartValuePositionOutsideSlice; // 区块数据位置 // 数据与区块之间的用于指示的折线样式 set1.valueLinePart1OffsetPercentage = 0.85; // 折线中第一段起始位置相对于区块的偏移量, 数值越大, 折线距离区块越远 set1.valueLinePart1Length = 0.5; // 折线中第一段长度占比 set1.valueLinePart2Length = 0.4; // 折线中第二段长度最大占比 set1.valueLineWidth = 1; // 折线的粗细 set1.valueLineColor = [UIColor brownColor]; // 折线颜色 // 将 PieChartDataSet 对象放入数组中 NSMutableArray *dataSets = [[NSMutableArray alloc] init]; [dataSets addObject:set1]; // 创建 PieChartData 对象, 此对象就是 PieChartData 需要最终数据对象 PieChartData *data = [[PieChartData alloc] initWithDataSets:dataSets]; [data setValueFont:[UIFont systemFontOfSize:10]]; [data setValueTextColor:[UIColor brownColor]]; // 自定义数据显示格式 ChartDataValueFormatter *formatter = [[ChartDataValueFormatter alloc] init];// 统一设置数据显示的格式 [data setValueFormatter:formatter]; return data; } #pragma mark - ChartViewDelegate // 点击选中柱形图时的代理方法 - (void)chartValueSelected:(ChartViewBase * _Nonnull)chartView entry:(ChartDataEntry * _Nonnull)entry highlight:(ChartHighlight * _Nonnull)highlight { } // 没有选中柱形图时的代理方法,当选中一个柱形图后,在空白处双击,就可以取消选择,此时会回调此方法 - (void)chartValueNothingSelected:(ChartViewBase * _Nonnull)chartView { } // 捏合放大或缩小柱形图时的代理方法 - (void)chartScaled:(ChartViewBase * _Nonnull)chartView scaleX:(CGFloat)scaleX scaleY:(CGFloat)scaleY { } // 拖拽图表时的代理方法 - (void)chartTranslated:(ChartViewBase * _Nonnull)chartView dX:(CGFloat)dX dY:(CGFloat)dY { } @end 效果 如果不需要空心样式的饼状图, 可以将饼状图的 drawHoleEnabled 赋值为 NO, 将中间的文本去掉即可。 // 饼状图是否是空心 self.pieChartView.drawHoleEnabled = NO; 效果 每个区块之间如果需要间距, 可以通过 dataSet 对象的 sliceSpace 属性设置, // 相邻区块之间的间距 set1.sliceSpace = 4; 效果 6、绘制雷达图 XAxisValueFormatter.h /// 设置 X 轴 数据格式 @import Charts; @interface XAxisValueFormatter : NSObject <IChartAxisValueFormatter> @end XAxisValueFormatter.m @implementation XAxisValueFormatter /// 实现协议方法,返回 x 轴的数据 - (NSString *)stringForValue:(double)value axis:(ChartAxisBase *)axis { // value 为 x 轴的值 return [NSString stringWithFormat:@"%ld 月", (NSInteger)value + 1]; } @end ChartDataValueFormatter.h /// 设置 折线显示的 数据格式 @import Charts; @interface ChartDataValueFormatter : NSObject <IChartValueFormatter> @end ChartDataValueFormatter.m @implementation ChartDataValueFormatter /// 实现协议方法,返回辐射线拐点上显示的数据格式 - (NSString * _Nonnull)stringForValue:(double)value entry:(ChartDataEntry * _Nonnull)entry dataSetIndex:(NSInteger)dataSetIndex viewPortHandler:(ChartViewPortHandler * _Nullable)viewPortHandler { return [NSString stringWithFormat:@"%.0f%%", entry.y]; } @end ViewController.m #import "ChartsDemo-Bridging-Header.h" // 自定义数据格式 #import "XAxisValueFormatter.h" #import "ChartDataValueFormatter.h" // 遵守协议 @interface ViewController () <ChartViewDelegate> // 雷达图 @property (nonatomic, strong) RadarChartView *radarChartView; // 滑动时 Y 值标签 @property (nonatomic,strong) UILabel *markYLabel; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; // 创建雷达图 [self createdPieView]; // 设置雷达图的数据 self.radarChartView.data = [self setData]; } // 创建雷达图 - (void)createdPieView { // 初始化雷达图对象 self.radarChartView = [[RadarChartView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width - 20, 400)]; self.radarChartView.center = self.view.center; [self.view addSubview:self.radarChartView]; // 设置基本样式 self.radarChartView.backgroundColor = [UIColor colorWithRed:230/255.0f green:253/255.0f blue:253/255.0f alpha:1]; self.radarChartView.delegate = self; // 设置代理,需遵守 ChartViewDelegate 协议 self.radarChartView.noDataText = @"暂无数据"; // 没有数据时的文字提示 // 设置交互样式 self.radarChartView.rotationEnabled = YES; // 是否允许转动 self.radarChartView.highlightPerTapEnabled = YES; // 是否能被选中 // 设置雷达图线条样式 self.radarChartView.webLineWidth = 1.0; // 主干线线宽,辐射线 self.radarChartView.webColor = [UIColor lightGrayColor]; // 主干线颜色 self.radarChartView.innerWebLineWidth = 1.0; // 边线宽度,圆形线 self.radarChartView.innerWebColor = [UIColor lightGrayColor]; // 边线颜色 self.radarChartView.webAlpha = 0.5; // 雷达图线的透明度 // 设置滑动时 Y 值标签 ChartMarkerView *markerY = [[ChartMarkerView alloc] init]; markerY.offset = CGPointMake(-17, -25); // 设置显示位置 markerY.chartView = self.radarChartView; [markerY addSubview:self.markYLabel]; self.radarChartView.marker = markerY; // 设置 X 轴 label 样式,由中心向外延伸的线 ChartXAxis *xAxis = self.radarChartView.xAxis; xAxis.valueFormatter = [[XAxisValueFormatter alloc] init]; // label 文字样式,自定义格式,默认时不显示特殊符号 xAxis.labelFont = [UIFont systemFontOfSize:15]; // 字体 xAxis.labelTextColor = [UIColor colorWithRed:5/255.0f green:119/255.0f blue:72/255.0f alpha:1]; // 颜色 // 设置 Y 轴 label 样式,圆形的线 ChartYAxis *yAxis = self.radarChartView.yAxis; yAxis.axisMinValue = 0.0; // 最小值,中心的值 yAxis.axisMaxValue = 100.0; // 最大值,最外边的值 yAxis.drawLabelsEnabled = YES; // 是否显示 label 值 yAxis.labelCount = 5; // label 个数 yAxis.labelFont = [UIFont systemFontOfSize:9]; // label 字体 yAxis.labelTextColor = [UIColor lightGrayColor]; // label 颜色 // 设置雷达图描述 self.radarChartView.chartDescription.enabled = YES; // 显示雷达图描述,默认 YES 显示 self.radarChartView.descriptionText = @"雷达图"; self.radarChartView.descriptionFont = [UIFont systemFontOfSize:15]; self.radarChartView.descriptionTextColor = [UIColor grayColor]; // 设置雷达图图例 self.radarChartView.legend.enabled = YES; // 显示图例,默认 YES 显示 self.radarChartView.legend.form = ChartLegendFormCircle; // 图示样式: 方形、线条、圆形 self.radarChartView.legend.formSize = 12; // 图示大小 self.radarChartView.legend.position = ChartLegendPositionBelowChartCenter; // 图例在雷达图中的位置 self.radarChartView.legend.maxSizePercent = 1; // 图例在雷达图中的大小占比, 这会影响图例的宽高 self.radarChartView.legend.formToTextSpace = 5; // 文本间隔 self.radarChartView.legend.font = [UIFont systemFontOfSize:10]; // 字体大小 self.radarChartView.legend.textColor = [UIColor grayColor]; // 字体颜色 // 设置动画效果,可以设置 X 轴和 Y 轴的动画效果 [self.radarChartView animateWithXAxisDuration:1.4 yAxisDuration:1.4 easingOption:ChartEasingOptionEaseOutBack]; } // 设置雷达图数据 - (RadarChartData *)setData { int vals_count = 12; // 维度的个数 // 每个维度的名称或描述 NSMutableArray *chartVals1 = [[NSMutableArray alloc] init]; for (int i = 0; i < vals_count; i++) { double partValue = arc4random() % 70 + 30; // 设置区块值 RadarChartDataEntry *entry = [[RadarChartDataEntry alloc] initWithValue:partValue]; [chartVals1 addObject:entry]; } // 绘制雷达图 // 创建 RadarChartDataSet 对象 RadarChartDataSet *set1 = [[RadarChartDataSet alloc] initWithValues:chartVals1 label:@"RadarName 1"]; // 设置数据块 set1.lineWidth = 0.5; // 数据折线线宽 [set1 setColor:[UIColor redColor]]; // 数据折线颜色 set1.drawFilledEnabled = YES; // 是否填充颜色 set1.fillColor = [UIColor orangeColor]; // 填充颜色 set1.fillAlpha = 0.5; // 填充透明度 // 设置数据块折点样式 set1.drawHighlightCircleEnabled = NO; // 是否绘制选中的拐点处的圆形 set1.highlightCircleStrokeColor = [UIColor greenColor]; // 拐点处的圆形颜色 [set1 setDrawHighlightIndicators:NO]; // 是否绘制选中拐点处的十字线 // 设置显示数据 set1.drawValuesEnabled = YES; // 是否绘制显示数据 set1.valueFont = [UIFont systemFontOfSize:9]; // 字体 set1.valueTextColor = [UIColor grayColor]; // 颜色 // 创建 RadarChartData 对象, 此对象就是 PieChartData 需要最终数据对象 RadarChartData *data = [[RadarChartData alloc] initWithDataSets:@[set1]]; [data setValueFont:[UIFont systemFontOfSize:15]]; [data setValueTextColor:[UIColor brownColor]]; // 自定义数据显示格式 ChartDataValueFormatter *formatter = [[ChartDataValueFormatter alloc] init];// 统一设置数据显示的格式 [data setValueFormatter:formatter]; return data; } #pragma mark - ChartViewDelegate // 点击选中柱形图时的代理方法 - (void)chartValueSelected:(ChartViewBase * _Nonnull)chartView entry:(ChartDataEntry * _Nonnull)entry highlight:(ChartHighlight * _Nonnull)highlight { // 设置滑动时 Y 值标签 self.markYLabel.text = [NSString stringWithFormat:@"%ld%%", (NSInteger)entry.y]; } // 没有选中柱形图时的代理方法,当选中一个柱形图后,在空白处双击,就可以取消选择,此时会回调此方法 - (void)chartValueNothingSelected:(ChartViewBase * _Nonnull)chartView { } // 捏合放大或缩小柱形图时的代理方法 - (void)chartScaled:(ChartViewBase * _Nonnull)chartView scaleX:(CGFloat)scaleX scaleY:(CGFloat)scaleY { } // 拖拽图表时的代理方法 - (void)chartTranslated:(ChartViewBase * _Nonnull)chartView dX:(CGFloat)dX dY:(CGFloat)dY { } // 懒加载滑动时 Y 值标签 - (UILabel *)markYLabel { if (!_markYLabel) { _markYLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, -5, 35, 25)]; _markYLabel.font = [UIFont systemFontOfSize:15.0]; _markYLabel.textAlignment = NSTextAlignmentCenter; _markYLabel.text = @""; _markYLabel.textColor = [UIColor whiteColor]; _markYLabel.backgroundColor = [UIColor grayColor]; } return _markYLabel; } @end 效果
1、绘制画板 1.1 绘制简单画板 PaintBoardView.h @interface PaintBoardView : UIView @end PaintBoardView.m @interface PaintBoardView () /// 路径 @property (nonatomic, strong) UIBezierPath *path; /// 保存所有路径的数组 @property (nonatomic, strong) NSMutableArray *pathsArrayM; @end @implementation PaintBoardView /// 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor whiteColor]; } return self; } /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸起始点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; // 添加路径描绘起始点 [self.path moveToPoint:startPoint]; // 添加一条触摸路径描绘 [self.pathsArrayM addObject:self.path]; } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; // 添加路径描绘 [self.path addLineToPoint:touchPoint]; // 刷新视图 [self setNeedsDisplay]; } /// 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取绘制结果 UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0); CGContextRef ctx = UIGraphicsGetCurrentContext(); [self.layer renderInContext:ctx]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); NSData *data = UIImagePNGRepresentation(image); [data writeToFile:@"/Users/JHQ0228/Desktop/Images/pic.png" atomically:YES]; } /// 触摸取消 - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event { [self touchesEnded:touches withEvent:event]; } /// 绘制图形 - (void)drawRect:(CGRect)rect { for (UIBezierPath *path in self.pathsArrayM) { // 绘制路径 [path stroke]; } } /// 懒加载 - (UIBezierPath *)path { if (_path == nil) { _path = [UIBezierPath bezierPath]; } return _path; } - (NSMutableArray *)pathsArrayM { if (_pathsArrayM == nil) { _pathsArrayM = [NSMutableArray array]; } return _pathsArrayM; } @end ViewController.m // 创建画板 CGRect frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, 200); PaintBoardView *paintBoard = [[PaintBoardView alloc] initWithFrame:frame]; [self.view addSubview:paintBoard]; 效果 1.2 绘制画板封装 具体实现代码见 GitHub 源码 QExtension QPaintBoardPath.h @interface QPaintBoardPath : UIBezierPath /// 线的颜色 @property (nonatomic, strong) UIColor *pathColor; /// 线的宽度 @property (nonatomic, assign) CGFloat pathWidth; @end QPaintBoardPath.m @implementation QPaintBoardPath @end QPaintBoardView.h @interface QPaintBoardView : UIView /// 画线的宽度,default is 1,max is 30 @property (nonatomic, assign) CGFloat paintLineWidth; /// 画笔的颜色,default is blackColor @property (nonatomic, strong) UIColor *paintLineColor; /// 画板的颜色,default is whiteColor @property (nonatomic, strong) UIColor *paintBoardColor; /** * 创建画板视图控件,获取绘画结果 * * @param frame 画板视图控件 frame * @param lineWidth 画笔的线宽,default is 1,max is 30 * @param lineColor 画笔的颜色,default is blackColor * @param boardColor 画板的颜色,default is whiteColor * @param result 绘画结果 * * @return 手势锁视图控件 */ + (instancetype)q_paintBoardViewWithFrame:(CGRect)frame lineWidth:(CGFloat)lineWidth lineColor:(nullable UIColor *)lineColor boardColor:(nullable UIColor *)boardColor paintResult:(void (^)(UIImage * _Nullable image))result; /** * 创建简单画板视图控件 * * @param frame 画板视图控件 frame * * @return 手势锁视图控件 */ + (instancetype)q_paintBoardViewWithFrame:(CGRect)frame; /** * 获取绘画结果 * * @return 绘画结果图片 */ - (UIImage * _Nullable)q_getPaintImage; /** * 清除绘画结果 */ - (void)q_clear; /** * 撤销绘画结果 */ - (void)q_back; @end QPaintBoardView.m #import "QPaintBoardPath.h" @interface QPaintBoardView () /// 路径 @property (nonatomic, strong, nullable) QPaintBoardPath *path; /// 保存所有路径的数组 @property (nonatomic, strong) NSMutableArray *pathsArrayM; /// 绘画结果 @property (nonatomic, copy) void (^resultBlock)(UIImage * _Nullable); /// 按钮工具条 @property (nonatomic, strong) UIView *toolView; /// 画笔设置视图 @property (nonatomic, strong) UIView *brushSetingView; /// 颜色选择视图 @property (nonatomic, strong) UIScrollView *colorSelectedView; /// 画板设置视图 @property (nonatomic, strong) UIScrollView *boardSetingView; /// 记录线的颜色 @property (nonatomic, strong) UIColor *lastPaintLineColor; /// 记录线的宽度 @property (nonatomic, assign) CGFloat lastPaintLineWidth; @end @implementation QPaintBoardView #pragma mark - 创建画板 /// 创建画板视图控件,获取绘画结果 + (instancetype)q_paintBoardViewWithFrame:(CGRect)frame lineWidth:(CGFloat)lineWidth lineColor:(nullable UIColor *)lineColor boardColor:(nullable UIColor *)boardColor paintResult:(void (^)(UIImage * _Nullable image))result { QPaintBoardView *paintBoardView = [[self alloc] init]; // CGRect tmpFrame = frame; // tmpFrame.size.height = frame.size.height + 44; paintBoardView.frame = frame; paintBoardView.paintLineWidth = (lineWidth > 30 ? 30 : lineWidth) ? : 1; paintBoardView.paintLineColor = lineColor ? : [UIColor blackColor]; paintBoardView.paintBoardColor = boardColor ? : [UIColor whiteColor]; paintBoardView.resultBlock = result; return paintBoardView; } /// 创建简单画板视图控件 + (instancetype)q_paintBoardViewWithFrame:(CGRect)frame { QPaintBoardView *paintBoardView = [[self alloc] initWithFrame:frame]; paintBoardView.paintLineWidth = 1; paintBoardView.paintLineColor = [UIColor blackColor]; paintBoardView.paintBoardColor = [UIColor whiteColor]; return paintBoardView; } #pragma mark - 自定义画板 /// 初始化,自定义画板界面 - (instancetype)init { if (self = [super init]) { self.clipsToBounds = YES; // 添加工具按钮视图 self.toolView = [[UIView alloc] init]; self.toolView.backgroundColor = [UIColor blackColor]; [self addSubview:self.toolView]; NSArray *imageNames = @[@"btn_brush", @"btn_board", @"btn_eraser", @"btn_back", @"btn_clear", @"btn_save"]; NSArray *selectedImageNames = @[@"btn_brush_pressed", @"btn_board_pressed", @"btn_eraser_pressed"]; for (NSUInteger i = 0; i < 6; i++) { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.tag = i; [button setBackgroundImage:[self q_getBundleImageWithName:imageNames[i]] forState:UIControlStateNormal]; [button addTarget:self action:@selector(toolButtonClick:) forControlEvents:UIControlEventTouchUpInside]; [self.toolView addSubview:button]; if (i < 3) { [button setBackgroundImage:[self q_getBundleImageWithName:selectedImageNames[i]] forState:UIControlStateSelected]; } } // 添加画笔设置视图 self.brushSetingView = [[UIView alloc] init]; self.brushSetingView.backgroundColor = [UIColor grayColor]; [self addSubview:self.brushSetingView]; [self sendSubviewToBack:self.brushSetingView]; UIView *widthBackView = [[UIView alloc] init]; widthBackView.layer.borderWidth = 1; widthBackView.layer.borderColor = [UIColor lightGrayColor].CGColor; UIView *widthView = [[UIView alloc] init]; [widthBackView addSubview:widthView]; [self.brushSetingView addSubview:widthBackView]; UISlider *widthSlider = [[UISlider alloc] init]; widthSlider.thumbTintColor = [UIColor orangeColor]; [widthSlider addTarget:self action:@selector(widthSliderClick:) forControlEvents:UIControlEventValueChanged]; [self.brushSetingView addSubview:widthSlider]; UIButton *colorSelectedBtn = [[UIButton alloc] init]; colorSelectedBtn.layer.borderWidth = 1; colorSelectedBtn.layer.borderColor = [UIColor lightGrayColor].CGColor; [colorSelectedBtn addTarget:self action:@selector(colorSelectedBtnClick:) forControlEvents:UIControlEventTouchUpInside]; [self.brushSetingView addSubview:colorSelectedBtn]; // 添加画笔颜色选择视图 self.colorSelectedView = [[UIScrollView alloc] init]; self.colorSelectedView.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.3]; self.colorSelectedView.showsHorizontalScrollIndicator = NO; [self addSubview:self.colorSelectedView]; [self sendSubviewToBack:self.colorSelectedView]; NSArray *colorArray = @[[UIColor blackColor], [UIColor whiteColor], [UIColor redColor], [UIColor greenColor], [UIColor blueColor], [UIColor cyanColor], [UIColor magentaColor], [UIColor orangeColor], [UIColor yellowColor], [UIColor darkGrayColor], [UIColor lightGrayColor], [UIColor brownColor], [UIColor grayColor], [UIColor purpleColor]]; for (NSUInteger i = 0; i < 14; i++) { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.layer.borderWidth = 1; button.layer.borderColor = [UIColor lightGrayColor].CGColor; [button setBackgroundColor:colorArray[i]]; [button addTarget:self action:@selector(colorSelectedClick:) forControlEvents:UIControlEventTouchUpInside]; [self.colorSelectedView addSubview:button]; } // 添加画板设置视图 self.boardSetingView = [[UIScrollView alloc] init]; self.boardSetingView.backgroundColor = [UIColor grayColor]; self.boardSetingView.showsHorizontalScrollIndicator = NO; [self addSubview:self.boardSetingView]; [self sendSubviewToBack:self.boardSetingView]; for (NSUInteger i = 0; i < 14; i++) { UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.layer.borderWidth = 1; button.layer.borderColor = [UIColor lightGrayColor].CGColor; [button setBackgroundColor:colorArray[i]]; [button addTarget:self action:@selector(boardColorSelectedClick:) forControlEvents:UIControlEventTouchUpInside]; [self.boardSetingView addSubview:button]; } } return self; } /// 布局子控件 - (void)layoutSubviews { [super layoutSubviews]; if (self.subviews.count) { // 设置工具按钮视图 self.toolView.frame = CGRectMake(0, self.bounds.size.height - 44, self.bounds.size.width, 44); for (NSUInteger i = 0; i < 6; i++) { CGFloat margin = (self.bounds.size.width - 44 * 6) / 7; CGFloat x = margin + (margin + 44) * i; self.toolView.subviews[i].frame = CGRectMake(x, 0, 44, 44); } // 设置画笔设置视图 self.brushSetingView.frame = CGRectMake(0, self.bounds.size.height - 44, self.bounds.size.width, 60); self.brushSetingView.subviews[0].frame = CGRectMake(15, 15, 30, 30); self.brushSetingView.subviews[0].layer.cornerRadius = 15; self.brushSetingView.subviews[0].layer.masksToBounds = YES; CGFloat w = self.paintLineWidth; self.brushSetingView.subviews[0].subviews[0].frame = CGRectMake(15 - w / 2, 15 - w / 2, w, w); self.brushSetingView.subviews[0].subviews[0].layer.cornerRadius = w / 2; self.brushSetingView.subviews[0].subviews[0].layer.masksToBounds = YES; self.brushSetingView.subviews[0].subviews[0].backgroundColor = self.paintLineColor; self.brushSetingView.subviews[1].frame = CGRectMake(60, 15, self.bounds.size.width - 60 - 80, 32); UISlider *slider = self.brushSetingView.subviews[1]; slider.value = self.paintLineWidth / 30; self.brushSetingView.subviews[2].frame = CGRectMake(self.bounds.size.width - 60, 15, 50, 30); self.brushSetingView.subviews[2].backgroundColor = self.paintLineColor; // 设置画笔颜色选择视图 self.colorSelectedView.frame = CGRectMake(0, self.bounds.size.height - 44, self.bounds.size.width, 60); self.colorSelectedView.contentSize = CGSizeMake(14 * (50 + 20) + 20, 50); for (NSUInteger i = 0; i < 14; i++) { CGFloat x = 20 + (20 + 50) * i; self.colorSelectedView.subviews[i].frame = CGRectMake(x, 10, 50, 40); } // 设置画板设置视图 self.boardSetingView.frame = CGRectMake(0, self.bounds.size.height - 44, self.bounds.size.width, 60); self.boardSetingView.contentSize = CGSizeMake(14 * (50 + 20) + 20, 50); for (NSUInteger i = 0; i < 14; i++) { CGFloat x = 20 + (20 + 50) * i; self.boardSetingView.subviews[i].frame = CGRectMake(x, 10, 50, 40); } } } /// 工具按钮点击事件处理 - (void)toolButtonClick:(UIButton *)btn { switch (btn.tag) { case 0: { // 画笔设置 [self exitEraseState]; if (btn.isSelected == NO) { [self hideBoardSetingView]; [self showColorSelectedView]; [self showBrushSetingView]; } else { [self hideColorSelectedView]; [self hideBrushSetingView]; } break; } case 1: { // 画板设置 [self exitEraseState]; if (btn.isSelected == NO) { [self hideColorSelectedView]; [self hideBrushSetingView]; [self showBoardSetingView]; } else { [self hideBoardSetingView]; } break; } case 2: { // 擦除 [self hideBoardSetingView]; [self hideColorSelectedView]; [self hideBrushSetingView]; if (btn.selected == NO) { [self enterEraseState]; } else { [self exitEraseState]; } break; } case 3: { // 撤销 [self hideBoardSetingView]; [self hideColorSelectedView]; [self hideBrushSetingView]; [self q_back]; break; } case 4: { // 清除 [self hideBoardSetingView]; [self hideColorSelectedView]; [self hideBrushSetingView]; [self exitEraseState]; [self q_clear]; break; } case 5: { // 获取绘制结果 [self hideBoardSetingView]; [self hideColorSelectedView]; [self hideBrushSetingView]; [self exitEraseState]; if (self.resultBlock) { self.resultBlock([self q_getPaintImage]); } break; } default: break; } } /// 画笔线宽设置按钮点击事件处理 - (void)widthSliderClick:(UISlider *)slider { if (slider.value == 0) { self.paintLineWidth = 1; } else { self.paintLineWidth = slider.value * 30; } CGFloat w = self.paintLineWidth; self.brushSetingView.subviews[0].subviews[0].frame = CGRectMake(15 - w / 2, 15 - w / 2, w, w); self.brushSetingView.subviews[0].subviews[0].layer.cornerRadius = w / 2; } /// 画笔颜色选择按钮点击事件处理 - (void)colorSelectedBtnClick:(UIButton *)btn { if (btn.selected == NO) { [self showColorSelectedView]; } else { [self hideColorSelectedView]; } } /// 画笔颜色选择点击响应事件处理 - (void)colorSelectedClick:(UIButton *)btn { self.paintLineColor = btn.backgroundColor; self.brushSetingView.subviews[0].subviews[0].backgroundColor = btn.backgroundColor; self.brushSetingView.subviews[2].backgroundColor = btn.backgroundColor; } /// 画板颜色选择点击响应事件处理 - (void)boardColorSelectedClick:(UIButton *)btn { self.paintBoardColor = btn.backgroundColor; } /// 显示画笔设置视图 - (void)showBrushSetingView { UIButton *setBrushBtn = self.toolView.subviews[0]; if (setBrushBtn.selected == NO) { setBrushBtn.selected = YES; [UIView animateWithDuration:0.2 animations:^{ CGRect frame = CGRectMake(0, self.bounds.size.height - 44 - 60, self.bounds.size.width, 60); self.brushSetingView.frame = frame; }]; } } /// 隐藏画笔设置视图 - (void)hideBrushSetingView { UIButton *setBrushBtn = self.toolView.subviews[0]; if (setBrushBtn.selected) { setBrushBtn.selected = NO; [UIView animateWithDuration:0.2 animations:^{ CGRect frame = CGRectMake(0, self.bounds.size.height - 44, self.bounds.size.width, 60); self.brushSetingView.frame = frame; }]; } } // 显示画笔颜色选择视图 - (void)showColorSelectedView { UIButton *colorSelectedBtn = self.brushSetingView.subviews[2]; if (colorSelectedBtn.selected == NO) { colorSelectedBtn.selected = YES; [UIView animateWithDuration:0.2 animations:^{ CGRect frame = CGRectMake(0, self.bounds.size.height - 44 - 60 - 60, self.bounds.size.width, 60); self.colorSelectedView.frame = frame; }]; } } /// 隐藏画笔颜色选择视图 - (void)hideColorSelectedView { UIButton *colorSelectedBtn = self.brushSetingView.subviews[2]; if (colorSelectedBtn.selected) { colorSelectedBtn.selected = NO; [UIView animateWithDuration:0.2 animations:^{ CGRect frame = CGRectMake(0, self.bounds.size.height - 44, self.bounds.size.width, 60); self.colorSelectedView.frame = frame; }]; } } /// 显示画板设置视图 - (void)showBoardSetingView { UIButton *setBoardBtn = self.toolView.subviews[1]; if (setBoardBtn.selected == NO) { setBoardBtn.selected = YES; [UIView animateWithDuration:0.2 animations:^{ CGRect frame = CGRectMake(0, self.bounds.size.height - 44 - 60, self.bounds.size.width, 60); self.boardSetingView.frame = frame; }]; } } /// 隐藏画板设置视图 - (void)hideBoardSetingView { UIButton *setBoardBtn = self.toolView.subviews[1]; if (setBoardBtn.selected) { setBoardBtn.selected = NO; [UIView animateWithDuration:0.2 animations:^{ CGRect frame = CGRectMake(0, self.bounds.size.height - 44, self.bounds.size.width, 60); self.boardSetingView.frame = frame; }]; } } /// 进入擦除状态 - (void)enterEraseState { UIButton *setEraseBtn = self.toolView.subviews[2]; if (setEraseBtn.selected == NO) { setEraseBtn.selected = YES; self.lastPaintLineColor = self.paintLineColor; self.paintLineColor = self.paintBoardColor; self.lastPaintLineWidth = self.paintLineWidth; self.paintLineWidth = self.paintLineWidth + 5; } } /// 退出擦除状态 - (void)exitEraseState { UIButton *setEraseBtn = self.toolView.subviews[2]; if (setEraseBtn.isSelected) { setEraseBtn.selected = NO; self.paintLineColor = self.lastPaintLineColor; self.paintLineWidth = self.lastPaintLineWidth; } } #pragma mark - 绘制图案 /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { if (self.subviews.count) { [self hideColorSelectedView]; [self hideBrushSetingView]; [self hideBoardSetingView]; } // 获取触摸起始点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; // 添加路径描绘起始点 [self.path moveToPoint:startPoint]; // 记录线的属性 self.path.pathColor = self.paintLineColor; self.path.pathWidth = self.paintLineWidth; // 添加一条触摸路径描绘 [self.pathsArrayM addObject:self.path]; } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; // 添加路径描绘 [self.path addLineToPoint:touchPoint]; // 刷新视图 [self setNeedsDisplay]; } /// 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 销毁 path self.path = nil; } /// 触摸取消 - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event { [self touchesEnded:touches withEvent:event]; } /// 绘制图形,只要调用 drawRect 方法就会把之前的内容全部清空 - (void)drawRect:(CGRect)rect { for (QPaintBoardPath *path in self.pathsArrayM) { // 绘制路径 path.lineWidth = path.pathWidth; [path.pathColor setStroke]; path.lineCapStyle = kCGLineCapRound; path.lineJoinStyle = kCGLineJoinRound; [path stroke]; } } /// 获取绘画结果 - (UIImage * _Nullable)q_getPaintImage { UIImage *image = nil; CGSize boardSize = self.bounds.size; if (self.subviews.count) { boardSize.height = self.bounds.size.height - 44; } if (self.pathsArrayM.count) { UIGraphicsBeginImageContextWithOptions(boardSize, NO, 0); CGContextRef ctx = UIGraphicsGetCurrentContext(); [self.layer renderInContext:ctx]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } return image; } /// 清除绘画结果 - (void)q_clear { if (self.pathsArrayM.count) { [self.pathsArrayM removeAllObjects]; [self setNeedsDisplay]; } } /// 撤销绘画结果 - (void)q_back { if (self.pathsArrayM.count) { [self.pathsArrayM removeLastObject]; [self setNeedsDisplay]; } } /// 懒加载 - (QPaintBoardPath * _Nullable)path { // path 每次绘制完成后需要销毁,否则无法清除之前绘制的路径 if (_path == nil) { _path = [QPaintBoardPath bezierPath]; } return _path; } - (NSMutableArray *)pathsArrayM { if (_pathsArrayM == nil) { _pathsArrayM = [NSMutableArray array]; } return _pathsArrayM; } /// 设置属性值 - (void)setPaintBoardColor:(UIColor *)paintBoardColor { _paintBoardColor = paintBoardColor; self.backgroundColor = paintBoardColor; } /// 加载 bundle 中的图片 - (UIImage *)q_getBundleImageWithName:(NSString *)name { NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"QPaintBoardView.bundle"]; UIImage *image = [[UIImage imageWithContentsOfFile:[bundlePath stringByAppendingPathComponent:name]] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; return image; } @end 1、创建简单画板 // 创建简单画板 CGRect frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, 200); QPaintBoardView *paintBoardView = [QPaintBoardView q_paintBoardViewWithFrame:frame]; // 可选属性值设置 paintBoardView.paintLineWidth = 5; // default is 1 paintBoardView.paintLineColor = [UIColor redColor]; // default is blackColor paintBoardView.paintBoardColor = [UIColor cyanColor]; // default is whiteColor [self.view addSubview:paintBoardView]; self.paintBoardView = paintBoardView; // 撤销绘画结果 [self.paintBoardView q_back]; // 清除绘画结果 [self.paintBoardView q_clear]; // 获取绘画结果 UIImage *image = [self.paintBoardView q_getPaintImage]; 效果 2、创建画板 // 创建画板 QPaintBoardView *paintBoard = [QPaintBoardView q_paintBoardViewWithFrame:self.view.bounds lineWidth:0 lineColor:nil boardColor:nil paintResult:^(UIImage * _Nullable image) { if (image) { NSData *data = UIImagePNGRepresentation(image); [data writeToFile:@"/Users/JHQ0228/Desktop/Images/pic.png" atomically:YES]; } }]; [self.view addSubview:paintBoard]; 效果
1、绘制手势截屏 具体实现代码见 GitHub 源码 QExtension QTouchClipView.h @interface QTouchClipView : UIView /** * 创建手势截屏视图控件,获取截屏结果 * * @param view 截取图片的视图控件 * @param result 手势截屏结果 * * @return 手势截屏视图控件 */ + (instancetype)q_touchClipViewWithView:(UIView *)view clipResult:(void (^)(UIImage * _Nullable image))result; @end QTouchClipView.m @interface QTouchClipView () /// 截取图片的视图控件 @property (nonatomic, strong) UIView *baseView; /// 滑动手势结果 @property (nonatomic, copy) void (^resultBlock)(UIImage * _Nullable); /// 触摸开始结束点 @property (nonatomic, assign) CGPoint startP; @property (nonatomic, assign) CGPoint endP; @end @implementation QTouchClipView /// 创建手势截屏视图控件,获取截屏结果 + (instancetype)q_touchClipViewWithView:(UIView *)baseView clipResult:(void (^)(UIImage * _Nullable image))result { QTouchClipView *clipView = [[self alloc] initWithFrame:baseView.frame]; clipView.baseView = baseView; clipView.resultBlock = result; return clipView; } /// 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5]; } return self; } /// 触摸开始 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸起始点位置 CGPoint startPoint = [touches.anyObject locationInView:self]; self.startP = startPoint; } /// 触摸移动 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 获取触摸点位置 CGPoint touchPoint = [touches.anyObject locationInView:self]; self.endP = touchPoint; // 刷新视图 [self setNeedsDisplay]; } /// 触摸结束 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event { // 截取屏幕图片 UIGraphicsBeginImageContextWithOptions(self.baseView.bounds.size, NO, 0); CGContextRef ctx = UIGraphicsGetCurrentContext(); [self.baseView.layer renderInContext:ctx]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); // 切割图片 CGFloat x = self.startP.x; CGFloat y = self.startP.y; CGFloat w = self.endP.x - x; CGFloat h = self.endP.y - y; CGRect cutRect = CGRectMake(x * 2, y * 2, w * 2, h * 2); CGImageRef cgImage = CGImageCreateWithImageInRect(image.CGImage, cutRect); UIImage *newImage = [[UIImage alloc] initWithCGImage:cgImage]; CGImageRelease(cgImage); // 返回截取结果 if (self.resultBlock) { self.resultBlock(newImage); } // 移除截取视图控件 [self removeFromSuperview]; self.startP = CGPointZero; self.endP = CGPointZero; // 刷新视图 [self setNeedsDisplay]; } /// 触摸取消 - (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event { [self touchesEnded:touches withEvent:event]; } /// 绘制触摸区域 - (void)drawRect:(CGRect)rect { CGFloat x = self.startP.x; CGFloat y = self.startP.y; CGFloat w = self.endP.x - x; CGFloat h = self.endP.y - y; CGRect clipRect = CGRectMake(x, y, w, h); UIBezierPath *path = [UIBezierPath bezierPathWithRect:clipRect]; [[[UIColor whiteColor] colorWithAlphaComponent:0.2] setFill]; [path fill]; } @end ViewController.m // 创建手势截屏视图 QTouchClipView *touchClipView = [QTouchClipView q_touchClipViewWithView:self.imageView clipResult:^(UIImage * _Nullable image) { // 获取处理截屏结果 if (image) { UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil); } }]; // 添加手势截屏视图 [self.view addSubview:touchClipView]; 效果
1、绘制下载进度按钮 具体实现代码见 GitHub 源码 QExtension QProgressButton.h @interface QProgressButton : UIButton /// 进度值,范围 0 ~ 1 @property (nonatomic, assign) CGFloat progress; /// 进度终止状态标题,一旦设置了此标题进度条就会停止 @property (nonatomic, strong) NSString *stopTitle; /** * 创建带进度条的按钮 * * @param frame 按钮的 frame 值 * @param title 进按钮的标题 * @param lineWidth 进度条的线宽,default is 2 * @param lineColor 进度条线的颜色,default is greenColor * @param textColor 进度值的颜色,default is blackColor * @param backColor 按钮的背景颜色,default is clearColor * @param isRound 按钮是否显示为圆形,default is YES * * @return 带进度条的按钮 */ + (instancetype)q_progressButtonWithFrame:(CGRect)frame title:(NSString *)title lineWidth:(CGFloat)lineWidth lineColor:(nullable UIColor *)lineColor textColor:(nullable UIColor *)textColor backColor:(nullable UIColor *)backColor isRound:(BOOL)isRound; @end QProgressButton.m @interface QProgressButton () /// 进度条的线宽 @property (nonatomic, assign) CGFloat lineWidth; /// 进度条线的颜色 @property (nonatomic, strong) UIColor *lineColor; /// 按钮的背景颜色 @property (nonatomic, strong) UIColor *backColor; /// 按钮是否显示为圆形 @property (nonatomic, assign, getter=isRound) BOOL round; @end @implementation QProgressButton /// 创建带进度条的按钮 + (instancetype)q_progressButtonWithFrame:(CGRect)frame title:(NSString *)title lineWidth:(CGFloat)lineWidth lineColor:(nullable UIColor *)lineColor textColor:(nullable UIColor *)textColor backColor:(nullable UIColor *)backColor isRound:(BOOL)isRound { QProgressButton *progressButton = [[self alloc] init]; progressButton.lineWidth = lineWidth ? : 2; progressButton.lineColor = lineColor ? : [UIColor colorWithRed:76/255.0 green:217/255.0 blue:100/255.0 alpha:1.0]; progressButton.backColor = backColor ? : [UIColor clearColor]; progressButton.round = isRound; // 设置按钮的实际 frame if (isRound) { CGRect tmpFrame = frame; tmpFrame.origin.y = frame.origin.y - (frame.size.width - frame.size.height) * 0.5; tmpFrame.size.height = frame.size.width; progressButton.frame = tmpFrame; } else { progressButton.frame = frame; } // 设置显示的标题和颜色 [progressButton setTitle:title forState:UIControlStateNormal]; [progressButton setTitleColor:(textColor ? : [UIColor blackColor]) forState:UIControlStateNormal]; return progressButton; } /// 绘制进度条 - (void)drawRect:(CGRect)rect { // 设置按钮圆角 self.layer.masksToBounds = YES; self.layer.cornerRadius = rect.size.height * 0.5; // 绘制按钮的背景颜色 UIBezierPath *path = [UIBezierPath bezierPathWithRect:rect]; [self.backColor set]; [path fill]; // 设置进度终止时显示的内容 if (self.stopTitle) { // 设置下载完成后的标题 [self setTitle:self.stopTitle forState:UIControlStateNormal]; return; } if (self.progress <= 0) { return; } // 清除按钮背景图片 [self setBackgroundImage:nil forState:UIControlStateNormal]; // 设置进度值 [self setTitle:[NSString stringWithFormat:@"%.2f%%", self.progress * 100] forState:UIControlStateNormal]; if (self.isRound) { CGPoint center = CGPointMake(rect.size.height * 0.5, rect.size.height * 0.5); CGFloat radius = (rect.size.height - self.lineWidth) * 0.5; CGFloat startA = - M_PI_2; CGFloat endA = startA + self.progress * 2 * M_PI; // 绘制进度条背景 path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0 endAngle:2 * M_PI clockwise:YES]; [[[UIColor lightGrayColor] colorWithAlphaComponent:0.5] set]; path.lineWidth = self.lineWidth; [path stroke]; // 绘制进度条 path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startA endAngle:endA clockwise:YES]; path.lineWidth = self.lineWidth; path.lineCapStyle = kCGLineCapRound; [self.lineColor set]; [path stroke]; } else { CGFloat w = self.progress * rect.size.width; CGFloat h = rect.size.height; // 绘制进度条背景 path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, rect.size.width, rect.size.height)]; [[[UIColor lightGrayColor] colorWithAlphaComponent:0.5] set]; [path fill]; // 绘制进度条 path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, w, h)]; [self.lineColor set]; [path fill]; } } /// 设置进度值 - (void)setProgress:(CGFloat)progress { _progress = progress; [self setNeedsDisplay]; } /// 设置进度终止状态标题 - (void)setStopTitle:(NSString *)stopTitle { _stopTitle = stopTitle; [self setNeedsDisplay]; } @end ViewController.m // 创建进度按钮 QProgressButton *progressButton = [QProgressButton q_progressButtonWithFrame:CGRectMake(100, 100, 100, 50) title:@"开始下载" lineWidth:10 lineColor:[UIColor blueColor] textColor:[UIColor redColor] backColor:[UIColor yellowColor] isRound:YES]; // 设置按钮点击事件 [progressButton addTarget:self action:@selector(progressUpdate:) forControlEvents:UIControlEventTouchUpInside]; // 将按钮添加到当前控件显示 [self.view addSubview:progressButton]; // 设置按钮的进度值 self.progressButton.progress = progress; // 设置按钮的进度终止标题,一旦设置了此标题进度条就会停止 self.progressButton.stopTitle = @"下载完成"; 效果
1、CardIO 识别 框架 GitHub 下载地址 配置 1、把框架整个拉进自己的工程,然后在 TARGETS => Build Phases => Link Binary With Libraries 里边分别加入下面这几个框架。 Accelerate.framework MobileCoreServices.framework CoreMedia.framework AudioToolbox.framework AVFoundation.framework 2、在TARGETS => Build Settings => Other Linker Flags 中添加 -ObjC 和 -lc++ 。 3、在 iOS8 + 系统中使用相机需要在 Info.plist 中添加 Privacy - Camera Usage Description,并设置其值。 4、在我们需要调用的文件中导入 // 导入头文件 #import "CardIO.h" #import "CardIOPaymentViewControllerDelegate.h // 遵守协议 <CardIOPaymentViewControllerDelegate> 开始扫描银行卡 [CardIOUtilities preload]; CardIOPaymentViewController *scanViewController = [[CardIOPaymentViewController alloc] initWithPaymentDelegate:self]; [self presentViewController:scanViewController animated:YES completion:nil]; 取消扫描 // CardIOPaymentViewControllerDelegate 协议方法 - (void)userDidCancelPaymentViewController:(CardIOPaymentViewController *)paymentViewController { [[[UIAlertView alloc] initWithTitle:@"User cancelled sca" message:nil delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil] show]; [self dismissViewControllerAnimated:YES completion:nil]; } 扫描完成 // CardIOPaymentViewControllerDelegate 协议方法 - (void)userDidProvideCreditCardInfo:(CardIOCreditCardInfo *)cardInfo inPaymentViewController:(CardIOPaymentViewController *)paymentViewController { // 获取扫描结果 // cardNumber 是扫描的银行卡号,显示的是完整号码,而 redactedCardNumber 只显示银行卡后四位,前面的用 * 代替了,返回的银行卡号都没有空格 NSString *redactedCardNumber = cardInfo.cardNumber; // 卡号 NSUInteger expiryMonth = cardInfo.expiryMonth; // 月 NSUInteger expiryYear = cardInfo.expiryYear; // 年 NSString *cvv = cardInfo.cvv; // CVV 码 // 显示扫描结果 NSString *msg = [NSString stringWithFormat:@"Number: %@\n\n expiry: %02lu/%lu\n\n cvv: %@", [self dealCardNumber:redactedCardNumber], expiryMonth, expiryYear, cvv]; [[[UIAlertView alloc] initWithTitle:@"Received card info" message:msg delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil] show]; [self dismissViewControllerAnimated:YES completion:nil]; } // 对银行卡号进行每隔四位加空格处理,自定义方法 - (NSString *)dealCardNumber:(NSString *)cardNumber { NSString *strTem = [cardNumber stringByReplacingOccurrencesOfString:@" " withString:@""]; NSString *strTem2 = @""; if (strTem.length % 4 == 0) { NSUInteger count = strTem.length / 4; for (int i = 0; i < count; i++) { NSString *str = [strTem substringWithRange:NSMakeRange(i * 4, 4)]; strTem2 = [strTem2 stringByAppendingString:[NSString stringWithFormat:@"%@ ", str]]; } } else { NSUInteger count = strTem.length / 4; for (int j = 0; j <= count; j++) { if (j == count) { NSString *str = [strTem substringWithRange:NSMakeRange(j * 4, strTem.length % 4)]; strTem2 = [strTem2 stringByAppendingString:[NSString stringWithFormat:@"%@ ", str]]; } else { NSString *str = [strTem substringWithRange:NSMakeRange(j * 4, 4)]; strTem2 = [strTem2 stringByAppendingString:[NSString stringWithFormat:@"%@ ", str]]; } } } return strTem2; } 效果
1、QRCode 在 iOS7 以前,在 iOS 中实现二维码和条形码扫描,我们所知的有,两大开源组件 ZBar 与 ZXing。iOS7 之后可以利用系统原生 API 生成二维码, iOS8 之后可以生成条形码, 系统默认生成的颜色是黑色。 1、ZBar 在扫描的灵敏度上,和内存的使用上相对于 ZXing 上都是较优的,但是对于 “圆角二维码” 的扫描确很困难。 2、ZXing 是 Google Code 上的一个开源的条形码扫描库,是用 java 设计的,连 Google Glass 都在使用的。但有人为了追求更高效率以及可移植性,出现了 c++ port。Github 上的 Objectivc-C port,其实就是用 OC 代码封装了一下而已,而且已经停止维护。这样效率非常低,在 instrument 下面可以看到 CPU 和内存疯涨,在内存小的机器上很容易崩溃。 3、AVFoundation 无论在扫描灵敏度和性能上来说都是最优的,所以毫无疑问我们应该切换到 AVFoundation,需要兼容 iOS 6 或之前的版本可以用 ZBar 或 ZXing 代替。 在 iOS8 + 系统中使用相机需要在 Info.plist 中添加 Privacy - Camera Usage Description,并设置其值。使用相册需要在 Info.plist 中添加 Privacy - Photo Library Usage Description,并设置其值。 按照下图在 Info.plist 文件中将 Localization native development region 的值改为 China。如果不设置此项弹出的相册页面中显示的按钮等为英文菜单。 2、系统原生二维码 2.1 扫描二维码 官方提供的接口非常简单,直接看代码,主要使用的是 AVFoundation。 // 包含头文件 #import <AVFoundation/AVFoundation.h> // 遵守协议 <AVCaptureMetadataOutputObjectsDelegate> // 输入输出的中间桥梁 @property (nonatomic, strong) AVCaptureSession *session; // 扫描窗口 @property (nonatomic, strong) UIImageView *scanView; // 创建扫描视图窗口,自定义方法 - (void)createdScanView { CGFloat margin = 50; CGRect scanFrame = CGRectMake(margin, margin + 20, self.view.bounds.size.width - margin * 2, self.view.bounds.size.width - margin * 2); self.scanView = [[UIImageView alloc] initWithFrame:scanFrame]; self.scanView.image = [UIImage imageNamed:@"scan_bg2"]; [self.view addSubview:self.scanView]; // 创建扫描 [self startScan]; } // 创建扫描,自定义方法 - (void)startScan { // 获取摄像设备 AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; // 创建输入流 AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil]; // 创建输出流 AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init]; // 设置代理,在主线程里刷新 [output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; // 设置有效扫描区域 output.rectOfInterest = [self getScanCropWithScanViewFrame:self.scanView.frame readerViewBounds:self.view.bounds]; // 初始化链接对象 self.session = [[AVCaptureSession alloc] init]; // 设置采集率,高质量 [self.session setSessionPreset:AVCaptureSessionPresetHigh]; [self.session addInput:input]; [self.session addOutput:output]; // 设置扫码支持的编码格式(如下设置条形码和二维码兼容) output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode128Code]; AVCaptureVideoPreviewLayer *layer = [AVCaptureVideoPreviewLayer layerWithSession:self.session]; layer.videoGravity = AVLayerVideoGravityResizeAspectFill; layer.frame = self.view.layer.bounds; [self.view.layer insertSublayer:layer atIndex:0]; // 开始捕获 [self.session startRunning]; } // 设置扫描区域的比例关系,自定义方法 - (CGRect)getScanCropWithScanViewFrame:(CGRect)scanViewFrame readerViewBounds:(CGRect)readerViewBounds { CGFloat x, y, width, height; x = scanViewFrame.origin.y / readerViewBounds.size.height; y = scanViewFrame.origin.x / readerViewBounds.size.width; width = scanViewFrame.size.height / readerViewBounds.size.height; height = scanViewFrame.size.width / readerViewBounds.size.width; return CGRectMake(x, y, width, height); } // 获取扫描结果,AVCaptureMetadataOutputObjectsDelegate 协议方法 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection { if (metadataObjects.count > 0) { // 停止扫描 [self.session stopRunning]; AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0]; // 获取扫描结果 NSString *resultString = metadataObject.stringValue; // 输出扫描字符串 [[[UIAlertView alloc] initWithTitle:@"扫描成功" message:resultString delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; } } 一些初始化的代码加上实现代理方法便完成了二维码扫描的工作,这里我们需要注意的是,在二维码扫描的时候,我们一般都会在屏幕中间放一个方框,用来显示二维码扫描的大小区间,这里我们在AVCaptureMetadataOutput 类中有一个 rectOfInterest 属性,它的作用就是设置扫描范围。这个 CGRect 参数和普通的 Rect 范围不太一样,它的四个值的范围都是 0-1,表示比例。rectOfInterest 都是按照横屏来计算的,所以当竖屏的情况下 x 轴和 y 轴要交换一下,宽度和高度设置的情况也是类似。 效果 2.2 读取二维码 读取主要用到 CoreImage 不过要强调的是读取二维码的功能只有在 iOS8 之后才支持,我们需要在相册中调用一个二维码,将其读取,代码如下 // 遵守协议 <UIImagePickerControllerDelegate, UINavigationControllerDelegate> // 打开相册,选取图片,自定义方法 - (void)readQRCode { if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) { // 初始化相册拾取器 UIImagePickerController *picker = [[UIImagePickerController alloc] init]; // 设置资源 picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; // 设置代理 picker.delegate = self; [self presentViewController:picker animated:YES completion:nil]; } else { NSString *errorStr = [NSString stringWithFormat:@"请在系统设置->隐私->照片中允许 \"%@\" 使用照片。", [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleNameKey]]; [[[UIAlertView alloc] initWithTitle:@"读取失败" message:errorStr delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; } } // 获取选中的图片,imagePickerController 协议方法 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { // 获取选择的图片 UIImage *image = info[UIImagePickerControllerOriginalImage]; // 识别图片中的二维码 NSString *resultString = [self recognizeQRCodeFromImage:image]; // 输出扫描字符串 [[[UIAlertView alloc] initWithTitle:@"读取成功" message:resultString delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; // 返回 [picker dismissViewControllerAnimated:YES completion:nil]; } // 识别图片中的二维码 - (NSString *)recognizeQRCodeFromImage:(UIImage *)image { CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage]; // 初始化扫描仪,设置识别类型和识别质量 CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}]; // 扫描获取的特征组 NSArray *features = [detector featuresInImage:ciImage]; if (features.count >= 1) { // 获取扫描结果 CIQRCodeFeature *feature = [features objectAtIndex:0]; NSString *resultString = feature.messageString; return resultString; } else { return @"该图片不包含二维码"; } } 效果 2.3 长按识别二维码 这个功能有很多的地方在用,最让人熟知的我想便是微信了,其实实现方法还是很简单的。 // 创建图片,添加长按手势 - (void)recognizeQRCode { CGFloat margin = 50; CGRect scanFrame = CGRectMake(margin, margin + 20, self.view.bounds.size.width - margin * 2, self.view.bounds.size.width - margin * 2); UIImageView *imageView = [[UIImageView alloc] initWithFrame:scanFrame]; imageView.image = [UIImage imageNamed:@"demo"]; imageView.userInteractionEnabled = YES; [self.view addSubview:imageView]; UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(dealLongPress:)]; [imageView addGestureRecognizer:longPress]; } // 处理长按手势,识别图片中的二维码 - (void)dealLongPress:(UIGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateBegan) { UIImageView *pressedImageView = (UIImageView *)gesture.view; if (pressedImageView.image) { // 识别图片中的二维码 NSString *resultString = [self recognizeQRCodeFromImage:pressedImageView.image]; // 输出扫描字符串 [[[UIAlertView alloc] initWithTitle:@"识别成功" message:resultString delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; } else { [[[UIAlertView alloc] initWithTitle:@"识别失败" message:@"该图片不包含二维码" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; } } } // 识别图片中的二维码 - (NSString *)recognizeQRCodeFromImage:(UIImage *)image { CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage]; // 初始化扫描仪,设置识别类型和识别质量 CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}]; // 扫描获取的特征组 NSArray *features = [detector featuresInImage:ciImage]; if (features.count >= 1) { // 获取扫描结果 CIQRCodeFeature *feature = [features objectAtIndex:0]; NSString *resultString = feature.messageString; return resultString; } else { return @"该图片不包含二维码"; } } 效果 2.4 生成二维码 生成二维码,其实也是用到 CoreImage,但是步骤繁琐一些,代码如下 // 创建 ImageView,存放生成的二维码 - (void)createQRCode { CGFloat margin = 50; CGRect frame = CGRectMake(margin, margin + 20, self.view.bounds.size.width - margin * 2, self.view.bounds.size.width - margin * 2); UIImageView *imageView = [[UIImageView alloc] initWithFrame:frame]; [self.view addSubview:imageView]; // 生成二维码 [self createQRCodeToImageView:imageView fromString:@"qianchia" withIcon:[UIImage imageNamed:@"demo1"] withColor:[UIColor redColor]]; } // 生成二维码 - (void)createQRCodeToImageView:(UIImageView *)imageView fromString:(NSString *)inputString withIcon:(UIImage *)icon withColor:(UIColor *)color { // 创建过滤器 CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"]; // 恢复默认 [filter setDefaults]; // 给过滤器添加数据 NSData *data = [inputString dataUsingEncoding:NSUTF8StringEncoding]; // 通过 KVO 设置滤镜 inputMessage 数据 [filter setValue:data forKey:@"inputMessage"]; // 设置二维码颜色 UIColor *onColor = color ? : [UIColor blackColor]; UIColor *offColor = [UIColor whiteColor]; CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor" keysAndValues:@"inputImage", filter.outputImage, @"inputColor0", [CIColor colorWithCGColor:onColor.CGColor], @"inputColor1", [CIColor colorWithCGColor:offColor.CGColor], nil]; // 获取输出的二维码 CIImage *outputImage = colorFilter.outputImage; // CIImage *outputImage = [filter outputImage]; CIContext *context = [CIContext contextWithOptions:nil]; CGImageRef cgImage = [context createCGImage:outputImage fromRect:[outputImage extent]]; // 将 CIImage 转换成 UIImage,并放大显示 UIImage *qrImage = [UIImage imageWithCGImage:cgImage scale:1.0 orientation:UIImageOrientationUp]; // 重绘 UIImage,默认情况下生成的图片比较模糊 CGFloat scale = 100; CGFloat width = qrImage.size.width * scale; CGFloat height = qrImage.size.height * scale; UIGraphicsBeginImageContext(CGSizeMake(width, height)); CGContextRef context1 = UIGraphicsGetCurrentContext(); CGContextSetInterpolationQuality(context1, kCGInterpolationNone); [qrImage drawInRect:CGRectMake(0, 0, width, height)]; qrImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); CGImageRelease(cgImage); // 添加头像 if (icon) { UIGraphicsBeginImageContext(qrImage.size); [qrImage drawInRect:CGRectMake(0, 0, qrImage.size.width, qrImage.size.height)]; // 设置头像大小 CGFloat scale = 5; CGFloat width = qrImage.size.width / scale; CGFloat height = qrImage.size.height / scale; CGFloat x = (qrImage.size.width - width) / 2; CGFloat y = (qrImage.size.height - height) / 2; [icon drawInRect:CGRectMake( x, y, width, height)]; UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); imageView.image = newImage; } else { imageView.image = qrImage; } } 效果 2.5 对原生二维码的封装 具体实现代码见 GitHub 源码 QExtension // 包含头文件 #import "QExtension.h" 扫描/识别二维码 // 创建二维码扫描视图控制器 QQRCode *qrCode = [QQRCode q_qrCodeWithResult:^(BOOL isSucceed, NSString *result) { if (isSucceed) { [[[UIAlertView alloc] initWithTitle:@"Succeed" message:result delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; } else { [[[UIAlertView alloc] initWithTitle:@"Failed" message:result delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; } }]; // 设置我的二维码信息 qrCode.myQRCodeInfo = @"http://weixin.qq.com/r/xUqbg1-ENgJJrRvg9x-X"; qrCode.headIcon = [UIImage imageNamed:@"demo6"]; // 打开扫描视图控制器 [self presentViewController:qrCode animated:YES completion:nil]; 效果 识别二维码 // 创建图片,添加长按手势 self.imageView.image = [UIImage imageNamed:@"demo4"]; UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(dealLongPress:)]; [self.imageView addGestureRecognizer:longPress]; // 处理长按手势,识别图片中的二维码 - (void)dealLongPress:(UIGestureRecognizer *)gesture { if (gesture.state == UIGestureRecognizerStateBegan) { UIImageView *pressedImageView = (UIImageView *)gesture.view; UIImage *image = pressedImageView.image; // 识别图片中的二维码 NSString *result = [image q_stringByRecognizeQRCode]; [[[UIAlertView alloc] initWithTitle:@"Succeed" message:result delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil] show]; } } 效果 生成二维码 // 生成普通的二维码 UIImage *qrImage = [UIImage q_imageWithQRCodeFromString:@"http://weixin.qq.com/r/xUqbg1-ENgJJrRvg9x-X" headIcon:nil color:nil backColor:nil]; // 生成带头像的二维码 UIImage *qrImage = [UIImage q_imageWithQRCodeFromString:@"http://weixin.qq.com/r/xUqbg1-ENgJJrRvg9x-X" headIcon:[UIImage imageNamed:@"demo6"] color:[UIColor blackColor] backColor:[UIColor whiteColor]]; // 生成指定图片大小的二维码 UIImage *qrImage = [UIImage q_imageWithQRCodeFromString:@"http://weixin.qq.com/r/xUqbg1-ENgJJrRvg9x-X" imageSize:CGSizeMake(2048, 2048) headIcon:[UIImage imageNamed:@"demo6"] headFrame:CGRectMake(819, 819, 410, 410) color:nil backColor:nil]; 效果 2.6 对原生条形码的封装 具体实现代码见 GitHub 源码 QExtension // 包含头文件 #import "QExtension.h" 扫描条形码 同上面 2.5 的 "扫描/识别二维码"。 生成条形码 // 生成条形码 UIImage *qrImage = [UIImage q_imageWithBarCodeFromString:@"cnblogs: QianChia" color:nil backColor:nil]; // 生成指定图片大小的条形码 UIImage *qrImage = [UIImage q_imageWithBarCodeFromString:@"cnblogs: QianChia" imageSize:CGSizeMake(1024, 512) color:[UIColor blueColor] backColor:[UIColor redColor]]; 效果 3、ZBarSDK 二维码使用 使用 ZBarSDK,可以扫描条形码和二维码。 配置 1、添加 SDK 的依赖库和框架。在 项目设置 => TARGETS => Build Phases => Link Binary With Libraries 中依次添加以下库或框架: AVFoundation.framework CoreMedia.framework CoreVideo.framework QuartzCore.framework libiconv.tbd 2、由于 ZBarSDK 不支持 Bitcode,需要在 Xcode 的项目设置 => TARGETS => Build Settings => Build Options => Enable Bitcode 的值设置为 NO,否则无法进行真机调试。 3、在 iOS8 + 系统中使用相机需要在 Info.plist 中添加 Privacy - Camera Usage Description,并设置其值。 4、在需要使用 ZBarSDK 的文件中 // 包含头文件 #import "ZBarSDK.h" // 遵守协议 <ZBarReaderViewDelegate> 创建扫描 // 声明扫描视图 @property (nonatomic, strong) ZBarReaderView *scanView; // 实例化扫描视图 self.scanView = [[ZBarReaderView alloc] init]; // 设置扫描视图的位置尺寸 float width = self.view.bounds.size.width - 40; self.scanView.frame = CGRectMake(20, 30, width, width); // 设置代理人 self.scanView.readerDelegate = self; // 添加扫描视图 [self.view addSubview:self.scanView]; // 关闭闪光灯 self.scanView.torchMode = 0; // 开始扫描 [self.scanView start]; 获取扫描结果 // ZBarReaderViewDelegate 协议方法 - (void)readerView:(ZBarReaderView *)readerView didReadSymbols:(ZBarSymbolSet *)symbols fromImage:(UIImage *)image { for (ZBarSymbol *symbol in symbols) { // 获取扫描结果 NSString *scanString = symbol.data; // 显示扫描结果 self.scanResult.text = scanString; } // 停止扫描 [self.scanView stop]; } 效果 4、ZCZBarSDK 二维码使用 使用 ZCZBarSDK,可以扫描条形码和二维码。 配置 1、添加 SDK 的依赖库和框架。在 项目设置 => TARGETS => Build Phases => Link Binary With Libraries 中依次添加以下库或框架: libiconv.tbd 2、由于 ZCZBarSDK 不支持 Bitcode,需要在 Xcode 的项目设置 => TARGETS => Build Settings => Build Options => Enable Bitcode 的值设置为 NO,否则无法进行真机调试。 3、在 iOS8 + 系统中使用相机需要在 Info.plist 中添加 Privacy - Camera Usage Description,并设置其值。使用相册需要在 Info.plist 中添加 Privacy - Photo Library Usage Description,并设置其值。 4、在需要使用 ZCZBarSDK 的文件中 // 包含头文件 #import "ZCZBarViewController.h" 生成二维码 // 将字符串 self.detailTF.text 中的内容生成的二维码存放到 ImageView(self.codeImageView)中。 if (self.detailTF.text.length != 0) { [ZCZBarViewController createImageWithImageView:self.codeImageView String:self.detailTF.text Scale:100]; } 扫描二维码 // isQRCode: 是否关闭条形码扫描,专门扫描二维码。 ZCZBarViewController *zzvc = [[ZCZBarViewController alloc] initWithIsQRCode:NO Block:^(NSString *result, BOOL isSucceed) { if (isSucceed) { self.scanResult.text = [NSString stringWithFormat:@"%@", result]; } }]; // 打开扫描视图控制器 [self presentViewController:zzvc animated:YES completion:nil]; 效果
1、系统方式创建分享 按照下图在 Info.plist 文件中将 Localization native development region 的值改为 China。如果不设置此项弹出的分享页面中显示的按钮为英文说明。 UIActivityViewController 方式创建 // 设置分享的内容 NSString *textToShare = @"请大家登录《iOS云端与网络通讯》服务网站。"; UIImage *imageToShare = [UIImage imageNamed:@"swift"]; NSURL *urlToShare = [NSURL URLWithString:@"http://m.baidu.com"]; // 创建分享视图控制器 /* activityItems: 分享的内容 applicationActivities: 分享的类型,默认(nil)时为 UIActivity */ NSArray *items = @[textToShare, imageToShare, urlToShare]; UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; // 设置不出现的分享按钮 /* Activity 类型又分为 “操作” 和 “分享” 两大类: UIActivityCategoryAction 操作: UIActivityTypeAirDrop AirDrop AirDrop UIActivityTypePrint 打印 Print UIActivityTypeSaveToCameraRoll 保存到相册 Save Image UIActivityTypeAssignToContact 添加到联系人 AssignToContact UIActivityTypeAddToReadingList 添加到 Safari 阅读列表 AddToReadingList UIActivityTypeCopyToPasteboard 复制到剪贴板 Copy UIActivityTypeOpenInIBooks 在 iBook 中打开 UIActivityCategoryShare 分享: UIActivityTypeMail 邮箱 Mail UIActivityTypeMessage 短信 Message UIActivityTypePostToTwitter 分享到 Twitter UIActivityTypePostToFacebook 分享到 Facebook UIActivityTypePostToVimeo 分享到 Vimeo(视频媒体) UIActivityTypePostToFlickr 分享到 Flickr(网络相簿) UIActivityTypePostToWeibo 分享到 新浪微博 UIActivityTypePostToTencentWeibo 分享到 腾讯微博 */ // 添加到此数组中的系统分享按钮项将不会出现在分享视图控制器中 activityVC.excludedActivityTypes = @[UIActivityTypeAssignToContact, UIActivityTypePrint]; // 显示分享视图控制器 [self presentViewController:activityVC animated:YES completion:nil]; // 分享完成 activityVC.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { // 分享完成或退出分享时调用该方法 if (completed) { NSLog(@"分享完成"); } else { NSLog(@"取消分享"); } }; 效果 2、系统方式自定义分享 按照下图在 Info.plist 文件中将 Localization native development region 的值改为 China。如果不设置此项弹出的分享页面中显示的按钮为英文说明。 自定义按钮 myUIActivity.h #import <UIKit/UIKit.h> @interface myUIActivity : UIActivity <UINavigationControllerDelegate> @end myUIActivity.m #import "myUIActivity.h" /* 自定义分享按钮 */ @implementation myUIActivity // 设置分享按钮的类型 - (NSString *)activityType { // 在 completionWithItemsHandler 回调里可以用于判断,一般取当前类名 return NSStringFromClass([myUIActivity class]); } // 设置分享按钮的标题 - (NSString *)activityTitle { // 设置显示在分享框里的名称 return @"自定义分享按钮"; } // 设置分享按钮的图片 - (UIImage *)activityImage { // 图片自定变为黑白色,默认尺寸为 56 * 56 像素 return [UIImage imageNamed:@"JHQ0228"]; } // 设置是否显示分享按钮 - (BOOL)canPerformWithActivityItems:(NSArray *)activityItems { // 这里一般根据用户是否授权等来决定是否要隐藏分享按钮 return YES; } // 预处理分享数据 - (void)prepareWithActivityItems:(NSArray *)activityItems { // 解析分享数据时调用,可以进行一定的处理 NSLog(@"prepareWithActivityItems"); // 手动执行分享操作,保存到相册 UIImageWriteToSavedPhotosAlbum(activityItems[1], nil, nil, nil); } // 执行分享 - (UIViewController *)activityViewController { // 点击自定义分享按钮时调用,跳转到自定义的视图控制器 NSLog(@"activityViewController"); return nil; } // 执行分享 - (void)performActivity { // 点击自定义分享按钮时调用 NSLog(@"performActivity"); } // 完成分享 - (void)activityDidFinish:(BOOL)completed { // 分享视图控制器退出时调用 NSLog(@"activityDidFinish"); } @end 使用自定义按钮 ViewController.m #import "myUIActivity.h" // 设置分享的内容 NSString *textToShare = @"请大家登录《iOS云端与网络通讯》服务网站。"; UIImage *imageToShare = [UIImage imageNamed:@"swift"]; NSURL *urlToShare = [NSURL URLWithString:@"http://m.baidu.com"]; // 设置分享的类型 myUIActivity *myActivity = [[myUIActivity alloc] init]; // 创建分享视图控制器 NSArray *items = @[textToShare, imageToShare, urlToShare]; NSArray *activities = @[myActivity]; UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:activities]; // 设置不出现的分享按钮 activityVC.excludedActivityTypes = @[UIActivityTypeAirDrop]; // 显示分享视图控制器 [self presentViewController:activityVC animated:YES completion:nil]; // 分享完成 activityVC.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { // 分享完成或退出分享时调用该方法 if (completed) { NSLog(@"分享完成"); } else { NSLog(@"取消分享"); } }; 效果 3、友盟 登录/分享 集成 U-Share 快速集成多平台分享、登录功能。帮助应用或游戏快速具备国内外多平台分享、第三方登录功能,SDK 包最小,集成成本最低,平台覆盖最全,并基于友盟+大数据,提供最为权威、实时的用户画像、分享回流等数据分析,助力产品开发与推广。 覆盖国内外近 30 家社交平台,支持文本、图片、音乐、视频、链接等多种内容类型的分享,并提供了主流游戏平台的 SDK。 国内平台:微信、朋友圈、QQ、Qzone、新浪微博、腾讯微博、人人、豆瓣、易信、短信、邮件等。 国外平台:Facebook、Twitter、Instagram、Google+、LINE、WhatsApp、Pinterest、Evernote、Pocket、LinkedIn、KakaoTalk 等。 U-Share 集成流程 U-Share 第三方账号申请及绑定 U-Share 快速集成文档 U-Share API 说明 U-Share SDK 下载 集成友盟社会化组件流程 1.1 注册友盟账号 登陆友盟官网,在我的产品页面添加新应用,然后获取到 Appkey。 1.2 申请第三方账号 参照文档:申请第三方账号 1.3 绑定第三方账号 参照文档:绑定第三方账号到友盟后台 1.4 下载 SDK 进入下载 SDK 页面,勾选自己需要的功能进行下载。 1.5 技术支持 官方微博: umengsocial 开发者社区:http://bbs.umeng.com/thread-5908-1-1.html?from=qianming 技术支持:联系客服 3.1 第三方账号申请及绑定 3.1.1 申请第三方账号 进行分享、授权操作需要在第三方平台创建应用并提交审核,友盟默认提供了大多数平台的测试账号,但如果需要将分享、授权来源、分享到 QQ、Qzone 的 icon 更改为自己 APP 的应用,就需要自己申请第三方账号。 新浪微博 登录新浪微博开放平台,填写相关应用信息并上传 icon 图片。注意修改安全域名为 sns.whalecloud.com 同时设置授权回调页为 http://sns.whalecloud.com/sina2/callback 安全域名设置在应用信息 --> 基本信息,具体位置参考下 授权回调页、取消授权回调页设置在应用信息 --> 高级信息,具体位置参考下图 安全域名的修改需要二次审核通过才生效,授权回调页修改即时生效 微信 登录微信开放平台,填写相关应用信息,审核通过后获取到微信 AppID 及 AppSecret,如果需要微信登录功能,需要申请微信登录权限。 QQ 及 Qzone QQ 及 Qzone 使用同一个 AppID 及 Appkey,登录腾讯开放平台,选择移动应用,填写相关应用信息并提交审核,未审核前通过只能使用测试账号,添加测试账号方法如下:选择用户能力 --> 进阶社交能力 --> 应用调试者,添加测试账号必须在申请者好友列表中,如下图 人人网 登录人人开放平台,填写相关应用信息,同时填写应用根域名为 sns.whalecloud.com 具体位置: 基本信息 --> 应用根域名 如图 豆瓣 登录豆瓣开放平台,创建应用并填写相关应用信息,注意权限必须选择广播,同时填写回调地址为 http://sns.whalecloud.com/douban/callback 3.1.2 绑定第三方账号到友盟后台 目前需要在友盟后台绑定的第三方账号为:新浪微博、腾讯微博、人人网、豆瓣、Qzone,其余平台如微信、QQ 直接在代码中设置。 绑定地址:http://umeng.com/apps,登录友盟网站 -> 左上角选择你们的产品 -> 组件 -> 社会化组件 -> 设置 短链接开关 短链接开关只对新浪微博、腾讯微博、人人网、豆瓣四个平台有效,开启短链接开关,分享文案中附加的链接会被转码,同时可以统计到分享回流率(点击链接的次数),关闭短链接开关则无法统计,短链接开关默认为关闭状态。 文字截断开关 文字截断开关只对新浪微博、腾讯微博、人人网、豆瓣四个平台有效,同时只对使用自定义分享编辑页或没有分享编辑页用户有效,当分享文案超出字数限制时自动截断,开关状态默认关闭。 3.2 U-Share SDK 集成 3.2.1 下载 U-Share SDK 通过 iOS 社会化组件下载页面选择所需的社交平台后进行下载。 3.2.2 加入 U-Share SDK 将 U-Share SDK 添加到工程 添加项目配置,在 Other Linker Flags 加入 -ObjC 加入依赖系统库 libsqlite3.tbd 添加平台相应的依赖库,根据集成的不同平台加入相关的依赖库,未列出平台则不用添加。添加方式:选中项目 Target -> Linked Frameworks and Libraries 列表中添加 注:Twitter 平台加入后需添加 TwitterKit.framework/Resources/TwitterKitResources.bundle。 3.3 U-Share SDK 平台配置 从这一步骤就开始需要第三方 appKey 和 appSecret 等信息,可参考第三方账号申请及绑定申请所需的平台账号。 3.3.1 配置各平台 URL Scheme 添加 URL Types URL Scheme 是通过系统找到并跳转对应 app 的一类设置,通过向项目中的 info.plist 文件中加入 URL types 可使用第三方平台所注册的 appkey 信息向系统注册你的 app,当跳转到第三方应用授权或分享后,可直接跳转回你的 app。 添加 URL Types 有如下几处,都可进行设置 1、通过工程设置面板 2、通过 info.plist 文件编辑 3、直接编辑 info.plist 中 XML 代码 配置第三方平台 URL Scheme 未列出则不需设置 3.3.2 适配 iOS 9/10 系统 iOS9 系统后 Apple 对 HTTP 请求及访问外部应用做了更加严格的要求,包括 HTTP 白名单、跳转第三方应用白名单等,具体设置第三方平台参数请参照适配 iOS9/10 系统。 HTTPS 传输安全 Apple 将从 2017 年开始执行 ATS(App Transport Security),所有进行审核的应用中网络请求全部支持 HTTPS,届时以下配置将会失效,请提前做好准备。 以 iOS10 SDK 编译的工程会默认以 SSL 安全协议进行网络传输,即 HTTPS,如果依然使用 HTTP 协议请求网络会报系统异常并中断请求。目前可用如下两种方式保持用 HTTP 进行网络连接: 在 info.plist 中加入安全域名白名单(右键 info.plist 用 source code 打开) <key>NSAppTransportSecurity</key> <dict> <!-- 配置允许 http 的任意网络End--> <key>NSExceptionDomains</key> <dict> <!-- 集成新浪微博对应的HTTP白名单--> <key>sina.com.cn</key> <dict> <key>NSIncludesSubdomains</key> <true/> <key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/> </dict> <key>sinaimg.cn</key> <dict> <key>NSIncludesSubdomains</key> <true/> <key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/> </dict> <key>sinajs.cn</key> <dict> <key>NSIncludesSubdomains</key> <true/> <key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/> </dict> <key>sina.cn</key> <dict> <!-- 适配iOS10 --> <key>NSExceptionMinimumTLSVersion</key> <string>TLSv1.0</string> <key>NSIncludesSubdomains</key> <true/> <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/> </dict> <key>weibo.cn</key> <dict> <!-- 适配iOS10 --> <key>NSExceptionMinimumTLSVersion</key> <string>TLSv1.0</string> <key>NSIncludesSubdomains</key> <true/> <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/> </dict> <key>weibo.com</key> <dict> <!-- 适配iOS10 --> <key>NSExceptionMinimumTLSVersion</key> <string>TLSv1.0</string> <key>NSIncludesSubdomains</key> <true/> <key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/> </dict> <!-- 新浪微博--> <!-- 集成人人授权对应的HTTP白名单--> <key>renren.com</key> <dict> <key>NSIncludesSubdomains</key> <true/> <key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSThirdPartyExceptionRequiresForwardSecrecy</key> <false/> </dict> <!-- 人人授权--> </dict> </dict> 若新版 Xcode 控制台输出 “[] tcp_connection_xxx“ 等内容,可以在运行按钮旁的选择 target 选项内的 Edit Scheme - Run - Arguments - Enviroment variables 中增加 OS_ACTIVITY_MODE=disable,可将相关日志关闭。 配置 ApplicationQueriesSchemes(应用间跳转) 如果你的应用使用了如 SSO 授权登录或跳转到第三方分享功能,在 iOS9/10 下就需要增加一个可跳转的白名单,即 LSApplicationQueriesSchemes,否则将在 SDK 判断是否跳转时用到的 canOpenURL 时返回 NO,进而只进行 webview 授权或授权/分享失败。 在项目中的 info.plist 中加入应用白名单,右键 info.plist 选择 source code 打开(具体设置在 Build Setting -> Packaging -> Info.plist File 可获取 plist 路径)。 <key>LSApplicationQueriesSchemes</key> <array> <!-- 微信 URL Scheme 白名单--> <string>wechat</string> <string>weixin</string> <!-- 新浪微博 URL Scheme 白名单--> <string>sinaweibohd</string> <string>sinaweibo</string> <string>sinaweibosso</string> <string>weibosdk</string> <string>weibosdk2.5</string> <!-- QQ、Qzone URL Scheme 白名单--> <string>mqqapi</string> <string>mqq</string> <string>mqqOpensdkSSoLogin</string> <string>mqqconnect</string> <string>mqqopensdkdataline</string> <string>mqqopensdkgrouptribeshare</string> <string>mqqopensdkfriend</string> <string>mqqopensdkapi</string> <string>mqqopensdkapiV2</string> <string>mqqopensdkapiV3</string> <string>mqqopensdkapiV4</string> <string>mqzoneopensdk</string> <string>wtloginmqq</string> <string>wtloginmqq2</string> <string>mqqwpa</string> <string>mqzone</string> <string>mqzonev2</string> <string>mqzoneshare</string> <string>wtloginqzone</string> <string>mqzonewx</string> <string>mqzoneopensdkapiV2</string> <string>mqzoneopensdkapi19</string> <string>mqzoneopensdkapi</string> <string>mqqbrowser</string> <string>mttbrowser</string> <!-- 支付宝 URL Scheme 白名单--> <string>alipay</string> <string>alipayshare</string> <!-- 人人 URL Scheme 白名单--> <string>renrenios</string> <string>renrenapi</string> <string>renren</string> <string>renreniphone</string> <!-- 来往 URL Scheme 白名单--> <string>laiwangsso</string> <!-- 易信 URL Scheme 白名单--> <string>yixin</string> <string>yixinopenapi</string> <!-- instagram URL Scheme 白名单--> <string>instagram</string> <!-- whatsapp URL Scheme 白名单--> <string>whatsapp</string> <!-- line URL Scheme 白名单--> <string>line</string> <!-- Facebook URL Scheme 白名单--> <string>fbapi</string> <string>fb-messenger-api</string> <string>fbauth2</string> <string>fbshareextension</string> <!-- Kakao URL Scheme 白名单--> <!-- 注:以下第一个参数需替换为自己的kakao appkey--> <!-- 格式为 kakao + "kakao appkey"--> <string>kakaofa63a0b2356e923f3edd6512d531f546</string> <string>kakaokompassauth</string> <string>storykompassauth</string> <string>kakaolink</string> <string>kakaotalk-4.5.0</string> <string>kakaostory-2.9.0</string> <!-- pinterest URL Scheme 白名单--> <string>pinterestsdk.v1</string> </array> 3.4 调用 U-Share SDK 3.4.1 初始化设置 初始化 U-Share 及第三方平台 app 启动后进行 U-Share 和第三方平台的初始化工作,以下代码将所有平台初始化示例放出,开发者根据平台需要选取相应代码,并替换为所属注册的 appKey 和 appSecret。 在 AppDelegate.m 中设置如下代码 #import <UMSocialCore/UMSocialCore.h> - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 打开调试日志 [[UMSocialManager defaultManager] openLog:YES]; // 设置友盟 appkey [[UMSocialManager defaultManager] setUmSocialAppkey:@"57b432afe0f55a9832001a0a"]; // 获取友盟 social 版本号 // NSLog(@"UMeng social version: %@", [UMSocialGlobal umSocialSDKVersion]); // 设置微信的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_WechatSession appKey:@"wxdc1e388c3822c80b" appSecret:@"3baf1193c85774b3fd9d18447d76cab0" redirectURL:@"http://mobile.umeng.com/social"]; // 设置分享到 QQ 互联的 appKey 和 appSecret // U-Share SDK 为了兼容大部分平台命名,统一用 appKey 和 appSecret 进行参数设置, // 而 QQ 平台仅需将 appID 作为 U-Share 的 appKey 参数传进即可。 [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_QQ appKey:@"100424468" appSecret:nil redirectURL:@"http://mobile.umeng.com/social"]; // 设置新浪的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Sina appKey:@"3921700954" appSecret:@"04b48b094faeb16683c32669824ebdad" redirectURL:@"http://sns.whalecloud.com/sina2/callback"]; // 支付宝的 appKey [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_AlipaySession appKey:@"2015111700822536" appSecret:nil redirectURL:@"http://mobile.umeng.com/social"]; // 设置易信的 appKey [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_YixinSession appKey:@"yx35664bdff4db42c2b7be1e29390c1a06" appSecret:nil redirectURL:@"http://mobile.umeng.com/social"]; // 设置点点虫(原来往)的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_LaiWangSession appKey:@"8112117817424282305" appSecret:@"9996ed5039e641658de7b83345fee6c9" redirectURL:@"http://mobile.umeng.com/social"]; // 设置领英的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Linkedin appKey:@"81t5eiem37d2sc" appSecret:@"7dgUXPLH8kA8WHMV" redirectURL:@"https://api.linkedin.com/v1/people"]; // 设置 Twitter 的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Twitter appKey:@"fB5tvRpna1CKK97xZUslbxiet" appSecret:@"YcbSvseLIwZ4hZg9YmgJPP5uWzd4zr6BpBKGZhf07zzh3oj62K" redirectURL:nil]; // 如果不想显示平台下的某些类型,可用以下接口设置 // [[UMSocialManager defaultManager] removePlatformProviderWithPlatformTypes:@[@(UMSocialPlatformType_WechatFavorite), @(UMSocialPlatformType_YixinTimeLine), @(UMSocialPlatformType_LaiWangTimeLine), @(UMSocialPlatformType_Qzone)]]; ... return YES; } 设置系统回调 // 支持所有 iOS 系统 - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等 SDK 的回调 } return result; } 注:以上为建议使用的系统 openURL 回调,且 新浪 平台仅支持以上回调。还有以下两种回调方式,如果开发者选取以下回调,也请补充相应的函数调用。 1、仅支持 iOS9 以上系统,iOS8 及以下系统不会回调 - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等 SDK 的回调 } return result; } 2、支持目前所有 iOS 系统 - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等 SDK 的回调 } return result; } 3.4.2 第三方平台登录 支持登录的平台:微信、QQ、新浪微博、腾讯微博、人人网、豆瓣、Facebook、Twitter、Linkedin 领英、Kakao。 支持登录并获取用户信息的平台:微信、QQ、新浪微博、Facebook、Twitter、Linkedin 领英、Kakao。 授权并获取用户信息 // 在需要进行获取登录信息的 UIViewController 中加入如下代码 #import <UMSocialCore/UMSocialCore.h> - (void)getUserInfoForPlatform:(UMSocialPlatformType)platformType { [[UMSocialManager defaultManager] getUserInfoWithPlatform:platformType currentViewController:self completion:^(id result, NSError *error) { UMSocialUserInfoResponse *resp = result; // 第三方登录数据(为空表示平台未提供) // 授权数据 NSLog(@" uid: %@", resp.uid); NSLog(@" openid: %@", resp.openid); NSLog(@" accessToken: %@", resp.accessToken); NSLog(@" refreshToken: %@", resp.refreshToken); NSLog(@" expiration: %@", resp.expiration); // 用户数据 NSLog(@" name: %@", resp.name); NSLog(@" iconurl: %@", resp.iconurl); NSLog(@" gender: %@", resp.gender); // 第三方平台 SDK 原始数据 NSLog(@" originalResponse: %@", resp.originalResponse); }]; } 注:若在 4.x 及 5.x 版本中使用微信登录,升级后需参考说明:4.x/5.x版本升级(授权信息变化)。 3.4.3 第三方平台分享 调用分享面板 在分享按钮绑定如下触发代码 #import <UShareUI/UShareUI.h> // 显示分享面板 [UMSocialUIManager showShareMenuViewInWindowWithPlatformSelectionBlock:^(UMSocialPlatformType platformType, NSDictionary *userInfo) { // 根据获取的 platformType 确定所选平台进行下一步操作 }]; 定制自己的分享面板预定义平台 以下方法可设置平台顺序 #import <UShareUI/UShareUI.h> [UMSocialUIManager setPreDefinePlatforms:@[@(UMSocialPlatformType_Sina),@(UMSocialPlatformType_QQ),@(UMSocialPlatformType_WechatSession)]]; [UMSocialUIManager showShareMenuViewInWindowWithPlatformSelectionBlock:^(UMSocialPlatformType platformType, NSDictionary *userInfo) { // 根据获取的 platformType 确定所选平台进行下一步操作 }]; 为避免应用审核被拒,仅会对有效的平台进行显示,如平台应用未安装,或平台应用不支持等会进行隐藏。由于以上原因,在模拟器上部分平台会隐藏。 如果遇到分享面板未显示,请参考分享面板无法弹出。 设置分享内容 分享文本 - (void)shareTextToPlatformType:(UMSocialPlatformType)platformType { // 创建分享消息对象 UMSocialMessageObject *messageObject = [UMSocialMessageObject messageObject]; // 设置文本 messageObject.text = @"社会化组件 UShare 将各大社交平台接入您的应用,快速武装 App。"; // 调用分享接口 [[UMSocialManager defaultManager] shareToPlatform:platformType messageObject:messageObject currentViewController:self completion:^(id data, NSError *error) { if (error) { NSLog(@"************ Share fail with error %@ *********", error); }else{ NSLog(@"response data is %@", data); } }]; } 其他分享类型示例请参考 U-Share API 文档。 3.5 技术支持 访问:友盟开发者社区 发邮件至 social-support@umeng.com。 为了能够尽快响应您的反馈,请提供您的 appkey 及 log 中的详细出错日志,您所提供的内容越详细越有助于我们帮您解决问题。 开启友盟分享调试 log 方法: #import <UMSocialCore/UMSocialCore.h> [[UMSocialManager defaultManager] openLog:YES]; 在 console 中查看日志。 效果 3.6 简单使用 AppDelegate.m #import <UMSocialCore/UMSocialCore.h> - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 打开日志 [[UMSocialManager defaultManager] openLog:YES]; // 打开图片水印 //[UMSocialGlobal shareInstance].isUsingWaterMark = YES; // 获取友盟 social 版本号 UMSocialLogInfo(@"UMeng social version: %@", [UMSocialGlobal umSocialSDKVersion]); // 设置友盟 appkey [[UMSocialManager defaultManager] setUmSocialAppkey:@"57b432afe0f55a9832001a0a"]; // 设置微信的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_WechatSession appKey:@"wxdc1e388c3822c80b" appSecret:@"3baf1193c85774b3fd9d18447d76cab0" redirectURL:@"http://mobile.umeng.com/social"]; /* * 添加某一平台会加入平台下所有分享渠道,如微信:好友、朋友圈、收藏,QQ:QQ 和 QQ 空间 * 以下接口可移除相应平台类型的分享,如微信收藏,对应类型可在枚举中查找 */ //[[UMSocialManager defaultManager] removePlatformProviderWithPlatformTypes:@[@(UMSocialPlatformType_WechatFavorite)]]; // 设置分享到 QQ 互联的 appID // U-Share SDK为了兼容大部分平台命名,统一用 appKey 和 appSecret 进行参数设置, // 而 QQ 平台仅需将 appID 作为 U-Share 的 appKey 参数传进即可。 [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_QQ appKey:@"1105821097" /*设置 QQ 平台的 appID*/ appSecret:nil redirectURL:@"http://mobile.umeng.com/social"]; // 设置新浪的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Sina appKey:@"3921700954" appSecret:@"04b48b094faeb16683c32669824ebdad" redirectURL:@"https://sns.whalecloud.com/sina2/callback"]; // 钉钉的 appKey [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_DingDing appKey:@"dingoalmlnohc0wggfedpk" appSecret:nil redirectURL:nil]; // 支付宝的 appKey [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_AlipaySession appKey:@"2015111700822536" appSecret:nil redirectURL:@"http://mobile.umeng.com/social"]; // 设置易信的 appKey [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_YixinSession appKey:@"yx35664bdff4db42c2b7be1e29390c1a06" appSecret:nil redirectURL:@"http://mobile.umeng.com/social"]; // 设置点点虫(原来往)的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_LaiWangSession appKey:@"8112117817424282305" appSecret:@"9996ed5039e641658de7b83345fee6c9" redirectURL:@"http://mobile.umeng.com/social"]; // 设置领英的 appKey 和 appSecret [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Linkedin appKey:@"81t5eiem37d2sc" appSecret:@"7dgUXPLH8kA8WHMV" redirectURL:@"https://api.linkedin.com/v1/people"]; // 设置 Facebook 的 appKey 和 UrlString [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Facebook appKey:@"506027402887373" appSecret:nil redirectURL:@"http://www.umeng.com/social"]; // 设置 Pinterest 的 appKey [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Pinterest appKey:@"4864546872699668063" appSecret:nil redirectURL:nil]; // dropbox 的 appKey [[UMSocialManager defaultManager] setPlaform: UMSocialPlatformType_DropBox appKey:@"k4pn9gdwygpy4av" appSecret:@"td28zkbyb9p49xu" redirectURL:@"https://mobile.umeng.com/social"]; // vk 的 appkey [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_VKontakte appKey:@"5786123" appSecret:nil redirectURL:nil]; return YES; } //#define __IPHONE_10_0 100000 #if __IPHONE_OS_VERSION_MAX_ALLOWED > 100000 - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等SDK的回调 } return result; } #endif - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等SDK的回调 } return result; } - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url]; if (!result) { // 其他如支付等SDK的回调 } return result; } ViewController.m #import <UMSocialCore/UMSocialCore.h> #import <UShareUI/UShareUI.h> #pragma mark - 第三方平台登录 - (IBAction)loginButtonClick:(UIButton *)sender { [self getUserInfoForPlatform:UMSocialPlatformType_WechatSession]; } - (void)getUserInfoForPlatform:(UMSocialPlatformType)platformType { [[UMSocialManager defaultManager] getUserInfoWithPlatform:platformType currentViewController:self completion:^(id result, NSError *error) { UMSocialUserInfoResponse *resp = result; // 第三方登录数据(为空表示平台未提供) // 授权数据 NSLog(@" uid: %@", resp.uid); NSLog(@" openid: %@", resp.openid); NSLog(@" accessToken: %@", resp.accessToken); NSLog(@" refreshToken: %@", resp.refreshToken); NSLog(@" expiration: %@", resp.expiration); // 用户数据 NSLog(@" name: %@", resp.name); NSLog(@" iconurl: %@", resp.iconurl); NSLog(@" gender: %@", resp.gender); // 第三方平台 SDK 原始数据 NSLog(@" originalResponse: %@", resp.originalResponse); }]; } #pragma mark - 第三方平台分享 - (IBAction)shareButtonClick:(UIButton *)sender { [self showShareMenuView]; } - (void)showShareMenuView { // 设置平台顺序,只显示设置列表中的应用 [UMSocialUIManager setPreDefinePlatforms:@[@(UMSocialPlatformType_Sina), @(UMSocialPlatformType_QQ), @(UMSocialPlatformType_WechatSession), @(UMSocialPlatformType_AlipaySession)]]; // 设置分享面板位置,底部 默认 [UMSocialShareUIConfig shareInstance].sharePageGroupViewConfig.sharePageGroupViewPostionType = UMSocialSharePageGroupViewPositionType_Bottom; // 设置分享按钮背景形状,有图片 没有圆背景 [UMSocialShareUIConfig shareInstance].sharePageScrollViewConfig.shareScrollViewPageItemStyleType = UMSocialPlatformItemViewBackgroudType_None; // 添加自定义分享按钮 [UMSocialUIManager addCustomPlatformWithoutFilted:UMSocialPlatformType_UserDefine_Begin+2 withPlatformIcon:[UIImage imageNamed:@"icon_circle"] withPlatformName:@"演示 icon"]; // 显示分享面板 [UMSocialUIManager showShareMenuViewInWindowWithPlatformSelectionBlock:^(UMSocialPlatformType platformType, NSDictionary *userInfo) { // 根据获取的 platformType 确定所选平台进行下一步操作 [self shareTextToPlatformType:platformType]; // 在回调里面获得点击的 if (platformType == UMSocialPlatformType_UserDefine_Begin+2) { NSLog(@"点击演示添加 Icon 后该做的操作"); dispatch_async(dispatch_get_main_queue(), ^{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"添加自定义 icon" message:@"具体操作方法请参考 UShareUI 内接口文档" delegate:nil cancelButtonTitle:NSLocalizedString(@"确定", nil) otherButtonTitles:nil]; [alert show]; }); } else { } }]; } // 设置分享内容 - (void)shareTextToPlatformType:(UMSocialPlatformType)platformType { // 创建分享消息对象 UMSocialMessageObject *messageObject = [UMSocialMessageObject messageObject]; // 设置文本 messageObject.text = @"社会化组件 UShare 将各大社交平台接入您的应用,快速武装 App。"; // 调用分享接口,进行分享 [[UMSocialManager defaultManager] shareToPlatform:platformType messageObject:messageObject currentViewController:self completion:^(id data, NSError *error) { if (error) { NSLog(@"************ Share fail with error %@ *********",error); }else{ NSLog(@"response data is %@",data); } }]; }
1、UserNotifications 通知是 App 用来和用户交流的一种方式,特别是当 App 并没有在前台运行的时候。通知,正如它的名称所强调的,被用作向用户‘通知’一个事件,或者仅仅向用户提示一条重要信息。总而言之,通知在提示类型的 App 当中非常有用,甚至在一些别的类型的 App 当中也是如此。比如,当用户进入一个指定区域(这是 iOS8 的新特性),一个下载任务完成,或者当朋友给你发送一条信息的时候,一条通知就可以被显示出来。无论如何,通知的目的就是获得用户的关注,然后他们就能处理通知了。 从 iOS8 开始,本质上来说有两种推送通知 Local Notifications(本地推送) 和 Remote Notifications(远程推送)。 本地推送通知(Local Notifications):由开发者定义,App 触发,触发的时间是被事先安排好的。 地点通知(Location Notifications),iOS8 引入,但是他们只会在用户一个特定的地理或者 iBeacon 区域时,才会被触发,虽然我们看不到什么细节。 远程推送通知(Remote Notifications):这种情况下,通知可以被分成两个类别。 推送通知(The push notifications),被服务器初始化,然后通过 APNS,最终到达用户设备。 静默通知(The silent notifications),其实也是推送通知,但是他们并没有被展示给用户,而是立即被 App 处理以发起某项任务,最后当一切都完成时,一个本地通知被显示以提示用户。 iOS 10 中将之前繁杂的推送通知统一成 UserNotifications.framework 来集中管理和使用通知功能,还增加一些实用的功能——撤回单条通知、更新已展示通知、中途修改通知内容、在通知中显示多媒体资源、自定义 UI 等功能,功能着实强大。 在用户日常生活中会有很多种情形需要通知,比如:新闻提醒、定时吃药、定期体检、到达某个地方提醒用户等等,这些功能在 UserNotifications 中都提供了相应的接口。 Local Notifications(本地推送) App 本地创建通知,加入到系统的 Schedule(计划表)里,如果触发器条件达成时会推送相应的消息内容。 Remote Notifications(远程推送) 图中,Provider 是指某个 iPhone 软件的 Push 服务器。APNS 是 Apple Push Notification Service(Apple Push 服务器)的缩写,是苹果的服务器。 上图可以分为三个阶段: 第一阶段:APNS Pusher 应用程序把要发送的消息、目的 iPhone 的标识打包,发给 APNS。 第二阶段:APNS 在自身的已注册 Push 服务的 iPhone 列表中,查找有相应标识的 iPhone,并把消息发到 iPhone。 第三阶段:iPhone 把发来的消息传递给相应的应用程序, 并且按照设定弹出 Push 通知。 远程推送创建流程 从上图我们可以看到: 首先是应用程序注册消息推送。 iOS 跟 APNS Server 要 deviceToken。应用程序接受 deviceToken。 应用程序将 deviceToken 发送给 Push 服务器端程序。 Push 服务器端程序向 APNS 发送推送消息。 APNS 将推送消息发送给 iPhone 应用程序。 2、本地推送 本地推送主要流程: 1 申请本地推送 2 创建一个触发器(trigger) 3 创建推送的内容(UNMutableNotificationContent) 4 创建推送请求(UNNotificationRequest) 5 推送请求添加到推送管理中心(UNUserNotificationCenter)中 2.1 申请本地推送 1、导入头文件,且要遵守协议。这里需要注意,包含头文件我们最好写成这种形式,防止低版本找不到头文件出现问题。 // 包含头文件 #ifdef NSFoundationVersionNumber_iOS_9_x_Max #import <UserNotifications/UserNotifications.h> #endif // 遵守协议 <UNUserNotificationCenterDelegate> 2、在 application:didFinishLaunchingWithOptions: 中申请通知权限。 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; // 设置代理,必须写代理,不然无法监听通知的接收与点击事件 center.delegate = self; // 判断是否已申请通知权限 [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined || settings.authorizationStatus == UNAuthorizationStatusDenied) { // 申请通知权限 [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert ) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!error && granted) { // 用户点击允许 NSLog(@"注册成功"); } else { // 用户点击不允许 NSLog(@"注册失败"); } }]; } }]; return YES; } 上面需要注意: 1. 必须写代理,不然无法监听通知的接收与点击事件 center.delegate = self; 2. 之前注册推送服务,用户点击了同意还是不同意,以及用户之后又做了怎样的更改我们都无从得知,现在 apple 开放了这个 API,我们可以直接获取到用户的设定信息了。 注意 UNNotificationSettings 是只读对象哦,不能直接修改!只能通过以下方式获取 [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { NSLog(@"%@",settings); }]; 2.2 接收处理推送消息 iOS 10 系统更新时,苹果给了我们 2 个代理方法来处理通知的接收和点击事件。 @protocol UNUserNotificationCenterDelegate @optional // The method will be called on the delegate only if the application is in the foreground. If the method is not implemented or the handler is not called in a timely manner then the notification will not be presented. The application can choose to have the notification presented as a sound, badge, alert and/or in the notification list. This decision should be based on whether the information in the notification is otherwise visible to the user. - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __IOS_AVAILABLE(10.0) __TVOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0); // The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from applicationDidFinishLaunching:. - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __TVOS_PROHIBITED; @end 此外,苹果把本地通知跟远程通知合二为一。区分本地通知跟远程通知的类是 UNPushNotificationTrigger.h 类中,UNPushNotificationTrigger 的类型是新增加的,通过它,我们可以得到一些通知的触发条件 ,解释如下: UNPushNotificationTrigger :(远程通知)远程推送的通知类型。 UNTimeIntervalNotificationTrigger :(本地通知)一定时间之后,重复或者不重复推送通知。我们可以设置 timeInterval(时间间隔)和 repeats(是否重复)。 UNCalendarNotificationTrigger :(本地通知)一定日期之后,重复或者不重复推送通知 例如,你每天 8 点推送一个通知,只要 dateComponents 为 8,如果你想每天 8 点都推送这个通知,只要 repeats 为 YES 就可以了。 UNLocationNotificationTrigger :(本地通知)地理位置的一种通知,当用户进入或离开一个地理区域来通知。 需要注意的 1. 下面这个代理方法,只会是 app 处于前台状态下才会走,后台模式下是不会走这里的。 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler 2. 下面这个代理方法,用户点击消息时会触发,点击 Action 按钮时也会触发。点击消息时默认会打开 App。 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler 3. 不管前台后台状态下,推送消息的横幅都可以展示出来。后台状态不用说,前台时需要在前台代理方法中设置,选择是否提醒用户。 completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert); 4. 点击代理最后需要执行:completionHandler(); 不然会报错。 2016-09-27 14:42:16.353978 UserNotificationsDemo[1765:800117] Warning: UNUserNotificationCenter delegate received call to -userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: but the completion handler was never called. 接收处理本地和远程推送消息 // AppDelegate.m // UNUserNotificationCenterDelegate 协议方法,App 处于前台接收通知 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{ // 收到推送的请求 UNNotificationRequest *request = notification.request; // 收到推送的内容 UNNotificationContent *content = request.content; // 收到用户的基本信息 NSDictionary *userInfo = content.userInfo; // 收到推送消息的角标 NSNumber *badge = content.badge; // 收到推送消息 body NSString *body = content.body; // 推送消息的声音 UNNotificationSound *sound = content.sound; // 推送消息的副标题 NSString *subtitle = content.subtitle; // 推送消息的标题 NSString *title = content.title; if ([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { // 收到远程推送消息 NSLog(@"收到远程推送消息: %@", userInfo); } else { // 收到本地推送消息 NSLog(@"收到本地推送消息: %@", userInfo); } // 需要执行这个方法,选择是否提醒用户,有 Badge、Sound、Alert 三种类型可以设置 completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert); } 处理推送消息点击事件 // AppDelegate.m // UNUserNotificationCenterDelegate 协议方法 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{ // 收到推送的请求 UNNotificationRequest *request = response.notification.request; // 收到推送的内容 UNNotificationContent *content = request.content; // 收到用户的基本信息 NSDictionary *userInfo = content.userInfo; // 收到推送消息的角标 NSNumber *badge = content.badge; // 收到推送消息 body NSString *body = content.body; // 推送消息的声音 UNNotificationSound *sound = content.sound; // 推送消息的副标题 NSString *subtitle = content.subtitle; // 推送消息的标题 NSString *title = content.title; if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { // 远程推送消息 NSLog(@"点击远程推送消息: %@", userInfo); } else { // 本地推送消息 NSLog(@"点击本地推送消息: %@", userInfo); } // 系统要求执行这个方法 completionHandler(); } 2.3 创建触发器 新功能 trigger 可以在特定条件触发,有三类:UNTimeIntervalNotificationTrigger、UNCalendarNotificationTrigger、UNLocationNotificationTrigger 1、UNTimeIntervalNotificationTrigger:定时推送。 一段时间后触发。 // timeInterval:单位为秒(s) repeats:是否循环提醒 // 50s 后提醒 UNTimeIntervalNotificationTrigger *trigger1 = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:50 repeats:NO]; 2、UNCalendarNotificationTrigger:定期推送。 时间点信息用 NSDateComponents。 // 在每周一的 14 点 3 分提醒 NSDateComponents *components = [[NSDateComponents alloc] init]; components.weekday = 2; components.hour = 16; components.minute = 3; // components 日期 UNCalendarNotificationTrigger *calendarTrigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES]; 3、UNLocationNotificationTrigger:定点推送。 地区信息使用 CLRegion 的子类 CLCircularRegion,可以配置 region 属性 notifyOnEntry 和 notifyOnExit,是在进入地区、从地区出来或者两者都要的时候进行通知。 // 首先得导入 #import <CoreLocation/CoreLocation.h> 。 // 创建位置信息 CLLocationCoordinate2D center1 = CLLocationCoordinate2DMake(39.788857, 116.5559392); CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:center1 radius:500 identifier:@"经海五路"]; region.notifyOnEntry = YES; region.notifyOnExit = YES; // region 位置信息 repeats 是否重复 (CLRegion 可以是地理位置信息) UNLocationNotificationTrigger *locationTrigger = [UNLocationNotificationTrigger triggerWithRegion:region repeats:YES]; 2.4 创建推送的内容 UNNotificationContent:属性 readOnly UNMutableNotificationContent:属性有 title、subtitle、body、badge、sound、lauchImageName、userInfo、attachments、categoryIdentifier、threadIdentifier 本地消息内容 内容限制大小 展示 title NSString 限制在一行,多出部分省略号 subtitle NSString 限制在一行,多出部分省略号 body NSString 通知栏出现时,限制在两行,多出部分省略号;预览时,全部展示 注意点: body 中 printf 风格的转义字符,比如说要包含 %,需要写成 %% 才会显示, 同样 UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"本地推送通知 - title"; content.subtitle = @"本地推送通知 - subtitle"; content.body = @"本地推送通知 - body,本地推送通知本地推送通知本地推送通知本地推送通知本地推送通知"; content.badge = @666; // 推送消息的角标 content.sound = [UNNotificationSound defaultSound]; // 提醒声音 content.userInfo = @{@"key1":@"userInfoValue1", @"key2":@"userInfoValue2"}; // 收到用户的基本信息 content.categoryIdentifier = @"Dely_locationCategory"; // 用于添加 Action 的标识 2.5 完整的本地推送创建 申请本地推送 // 包含头文件 #ifdef NSFoundationVersionNumber_iOS_9_x_Max #import <UserNotifications/UserNotifications.h> #endif // 遵守协议 <UNUserNotificationCenterDelegate> - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; // 设置代理,必须写代理,不然无法监听通知的接收与点击事件 center.delegate = self; // 判断是否已申请通知权限 [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined || settings.authorizationStatus == UNAuthorizationStatusDenied) { // 申请通知权限 [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert ) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!error && granted) { // 用户点击允许 NSLog(@"注册成功"); } else { // 用户点击不允许 NSLog(@"注册失败"); } }]; } }]; return YES; } 接收处理本地推送消息 // AppDelegate.m // UNUserNotificationCenterDelegate 协议方法,App 处于前台接收通知 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{ // 收到推送的请求 UNNotificationRequest *request = notification.request; // 收到推送的内容 UNNotificationContent *content = request.content; // 收到用户的基本信息 NSDictionary *userInfo = content.userInfo; // 收到推送消息的角标 NSNumber *badge = content.badge; // 收到推送消息 body NSString *body = content.body; // 推送消息的声音 UNNotificationSound *sound = content.sound; // 推送消息的副标题 NSString *subtitle = content.subtitle; // 推送消息的标题 NSString *title = content.title; if ([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { // 收到远程推送消息 NSLog(@"收到远程推送消息: %@", userInfo); } else { // 收到本地推送消息 NSLog(@"收到本地推送消息: body:%@,title:%@, subtitle:%@, badge:%@,sound:%@, userInfo:%@", body, title, subtitle, badge, sound, userInfo); } // 需要执行这个方法,选择是否提醒用户,有 Badge、Sound、Alert 三种类型可以设置 completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert); } 处理推送消息点击事件 // AppDelegate.m // UNUserNotificationCenterDelegate 协议方法 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{ // 收到推送的请求 UNNotificationRequest *request = response.notification.request; // 收到推送的内容 UNNotificationContent *content = request.content; // 收到用户的基本信息 NSDictionary *userInfo = content.userInfo; // 收到推送消息的角标 NSNumber *badge = content.badge; // 收到推送消息 body NSString *body = content.body; // 推送消息的声音 UNNotificationSound *sound = content.sound; // 推送消息的副标题 NSString *subtitle = content.subtitle; // 推送消息的标题 NSString *title = content.title; if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { // 远程推送消息 NSLog(@"点击远程推送消息: %@", userInfo); } else { // 本地推送消息 NSLog(@"点击本地推送消息: body:%@,title:%@, subtitle:%@, badge:%@,sound:%@, userInfo:%@", body, title, subtitle, badge, sound, userInfo); } // 系统要求执行这个方法 completionHandler(); } iOS10 创建发送本地通知 // 添加头文件 #import <UserNotifications/UserNotifications.h> 创建定时推送通知 - (void)createLocalizedUserNotification1 { // 设置触发条件 UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:10.0f repeats:NO]; // 创建通知内容 UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"定时推送通知 - title"; content.subtitle = [NSString stringWithFormat:@"定时推送通知 - subtitle,%@", [NSDate date]]; content.body = @"定时推送通知 - body,定时推送通知定时推送通知定时推送通知定时推送通知定时推送通知"; content.badge = @666; // 推送消息的角标 content.sound = [UNNotificationSound defaultSound]; content.userInfo = @{@"key1":@"userInfoValue1", @"key2":@"userInfoValue2"}; // 收到用户的基本信息 content.categoryIdentifier = @"Dely_locationCategory"; // 用于添加 Action 的标识 // 创建通知标示 NSString *requestIdentifier = @"Dely.note"; // 创建通知请求 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifier content:content trigger:timeTrigger]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; // 将通知请求添加到用户通知中心 [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { NSLog(@"本地推送已添加成功 1 %@", requestIdentifier); } }]; } 创建定期推送通知 - (void)createLocalizedUserNotification2 { // 创建日期组建 NSDateComponents *components = [[NSDateComponents alloc] init]; components.weekday = 4; components.hour = 5; components.minute = 48; // 设置触发条件 UNCalendarNotificationTrigger *calendarTrigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:YES]; // 创建通知内容 UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"定期推送通知 - title"; content.subtitle = [NSString stringWithFormat:@"定期推送通知 - subtitle,%@", [NSDate date]]; content.body = @"定期推送通知 - body,定期推送通知定期推送通知定期推送通知定期推送通知定期推送通知"; content.badge = @2; // 推送消息的角标 content.sound = [UNNotificationSound defaultSound]; content.userInfo = @{@"key1":@"userInfoValue1", @"key2":@"userInfoValue2"}; // 收到用户的基本信息 content.categoryIdentifier = @"Date_locationCategory"; // 用于添加 Action 的标识 // 创建通知标示 NSString *requestIdentifier = @"Date.note"; // 创建通知请求 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifier content:content trigger:calendarTrigger]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; // 将通知请求添加到用户通知中心 [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { NSLog(@"推送已添加成功 2 %@", requestIdentifier); } }]; } 创建定点推送通知 // 添加头文件 #import <CoreLocation/CoreLocation.h> - (void)createLocalizedUserNotification3 { // 创建位置信息 CLLocationCoordinate2D center1 = CLLocationCoordinate2DMake(39.788857, 116.5559392); CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:center1 radius:500 identifier:@"经海五路"]; region.notifyOnEntry = YES; region.notifyOnExit = YES; // 设置触发条件 UNLocationNotificationTrigger *locationTrigger = [UNLocationNotificationTrigger triggerWithRegion:region repeats:YES]; // 创建通知内容 UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @"定点推送通知 - title"; content.subtitle = [NSString stringWithFormat:@"定点推送通知 - subtitle,%@", [NSDate date]]; content.body = @"定点推送通知 - body,定点推送通知定点推送通知定点推送通知定点推送通知定点推送通知"; content.badge = @1; // 推送消息的角标 content.sound = [UNNotificationSound defaultSound]; content.userInfo = @{@"key1":@"userInfoValue1", @"key2":@"userInfoValue2"}; // 收到用户的基本信息 content.categoryIdentifier = @"Address_locationCategory"; // 用于添加 Action 的标识 // 创建通知标示 NSString *requestIdentifier = @"Address.note"; // 创建通知请求 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:requestIdentifier content:content trigger:locationTrigger]; UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; // 将通知请求添加到用户通知中心 [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { NSLog(@"推送已添加成功 3 %@", requestIdentifier); } }]; } 运行结果如下 3、远程推送 远程推送主要流程: 1 应用程序注册消息推送 2 iOS 跟 APNS Server 要 deviceToken。应用程序接受 deviceToken 3 应用程序将 deviceToken 发送给 Push 服务器端程序 4 Push 服务器端程序向 APNS 发送推送消息 5 APNS 将推送消息发送给 iPhone 应用程序 3.1 申请注册远程推送 1、在苹果开发者中心,创建推送证书。 2、在 TARGETS => Capabilities 中打开 Push Notifications 开关。 在 Xcode7 中这里的开关不打开,推送也是可以正常使用的,但是在 Xcode8 中,这里的开关必须要打开,不然会报错。打开后会自动在项目里生成 .entitlements 文件。 Error Domain=NSCocoaErrorDomain Code=3000 "未找到应用程序的“aps-environment”的授权字符串" UserInfo={NSLocalizedDescription=未找到应用程序的“aps-environment”的授权字符串} 3、导入头文件,且要遵守协议。这里需要注意,包含头文件我们最好写成这种形式,防止低版本找不到头文件出现问题。 // 包含头文件 #ifdef NSFoundationVersionNumber_iOS_9_x_Max #import <UserNotifications/UserNotifications.h> #endif // 遵守协议 <UNUserNotificationCenterDelegate> 4、在 application:didFinishLaunchingWithOptions: 中申请注册通知。 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; // 设置代理,必须写代理,不然无法监听通知的接收与点击事件 center.delegate = self; // 判断是否已申请通知权限 [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined || settings.authorizationStatus == UNAuthorizationStatusDenied) { // 申请通知权限 [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert ) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!error && granted) { // 用户点击允许 NSLog(@"注册成功"); } else { // 用户点击不允许 NSLog(@"注册失败"); } }]; } }]; // 判断是否已注册远程推送 if (application.isRegisteredForRemoteNotifications == NO) { // 注册远程推送,获取 device token [application registerForRemoteNotifications]; } return YES; } 5、获取到 device Token 处理。 // 获取 Device Token 成功 - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{ // 解析 NSData 获取字符串 // 直接使用下面方法转换为 string,会得到一个 nil // 错误写法 // NSString *string = [[NSString alloc] initWithData:deviceToken encoding:NSUTF8StringEncoding]; // 正确写法 NSString *deviceString = [[deviceToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]]; deviceString = [deviceString stringByReplacingOccurrencesOfString:@" " withString:@""]; NSLog(@"获取 deviceToken 成功:%@", deviceString); } // 获取 Device Token 失败 - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{ NSLog(@"获取 deviceToken 失败:%@", error.description); } 3.2 处理远程推送消息 1、接收处理远程推送消息 // AppDelegate.m // UNUserNotificationCenterDelegate 协议方法,App 处于前台接收通知 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{ // 收到推送的请求 UNNotificationRequest *request = notification.request; // 收到推送的内容 UNNotificationContent *content = request.content; // 推送消息的声音 UNNotificationSound *sound = content.sound; // 收到推送消息的角标 NSNumber *badge = content.badge; // 推送消息的标题 NSString *title = content.title; // 推送消息的副标题 NSString *subtitle = content.subtitle; // 收到推送消息 body NSString *body = content.body; // 收到用户的基本信息 NSDictionary *userInfo = content.userInfo; // 判断收到的通知类型 if ([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { // 收到远程推送消息 NSLog(@"收到远程推送消息: body:%@,title:%@, subtitle:%@, badge:%@,sound:%@, userInfo:%@", body, title, subtitle, badge, sound, userInfo); } else { // 收到本地推送消息 NSLog(@"收到本地推送消息: %@", userInfo); } // 需要执行这个方法,选择是否提醒用户,有 Badge、Sound、Alert 三种类型可以设置 completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert ); } 2、处理推送消息点击事件 // AppDelegate.m // UNUserNotificationCenterDelegate 协议方法 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{ // 收到推送的请求 UNNotificationRequest *request = response.notification.request; // 收到推送的内容 UNNotificationContent *content = request.content; // 收到用户的基本信息 NSDictionary *userInfo = content.userInfo; // 收到推送消息的角标 NSNumber *badge = content.badge; // 收到推送消息 body NSString *body = content.body; // 推送消息的声音 UNNotificationSound *sound = content.sound; // 推送消息的副标题 NSString *subtitle = content.subtitle; // 推送消息的标题 NSString *title = content.title; if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { // 远程推送消息 NSLog(@"点击远程推送消息: body:%@,title:%@, subtitle:%@, badge:%@,sound:%@,userInfo:%@", body, title, subtitle, badge, sound, userInfo); } else { // 本地推送消息 NSLog(@"点击本地推送消息: %@", userInfo); } // 系统要求执行这个方法 completionHandler(); } 3.3 推送模拟 1、现在我们需要一个推送服务器给 APNS 发送信息。花钱买一个 APNS pusher 来模拟远程推送服务,当然可以不花钱也可以用到,例如 NWPusher、KnuffApp。 2、运行工程则会拿到设备的 Device Token,后面会用到,模拟器上无法获取到 Device Token,只有真机上才能获取到。 3、把刚刚获取的 device token 填到相应位置,同时配置好 push 证书(.p12 格式)。添加 aps 内容,然后点击 push 就 OK 了。 { "aps" : { "alert" : { "title" : "远程推送通知 - title", "subtitle" : "远程推送通知 - subtitle", "body" : "远程推送通知 - body,远程推送通知远程推送通知远程推送通知远程推送通知远程推送通知远程推送通知" }, "badge" : "6", "sound" : "default" } } 4、稍纵即逝你就收到了远程消息了。 4、推送消息的更新 1、Local Notification (本地推送)需要通过更新 request。相同的 requestIdentifier,重新添加到推送 center 就可以了,说白了就是重新创建 local Notification request(只要保证 requestIdentifier 就 ok 了),应用场景如图 Local Notification 更新前后 2、Remote Notification (远程推送)更新需要通过新的字段 apps-collapse-id 来作为唯一标示,前面用的 APNS pusher 暂不支持这个字段,不过 GitHub 上有很多这样的工具,这样 remote 也可以更新推送消息。 KnuffApp NWPusher 5、推送消息的查找和删除 推送消息的查找和删除 // 获取未送达的所有消息列表 - (void)getPendingNotificationRequestsWithCompletionHandler:(void(^)(NSArray<UNNotificationRequest *> *requests))completionHandler; // 删除所有未送达的特定 id 的消息 - (void)removePendingNotificationRequestsWithIdentifiers:(NSArray<NSString *> *)identifiers; // 删除所有未送达的消息 - (void)removeAllPendingNotificationRequests; // 获取已送达的所有消息列表 - (void)getDeliveredNotificationsWithCompletionHandler:(void(^)(NSArray<UNNotification *> *notifications))completionHandler; // 删除所有已送达的特定 id 的消息 - (void)removeDeliveredNotificationsWithIdentifiers:(NSArray<NSString *> *)identifiers; // 删除所有已送达的消息 - (void)removeAllDeliveredNotifications; // 如: UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; // 删除设备已收到的所有消息推送 [center removeAllDeliveredNotifications]; 6、通知操作 Action 早在 iOS8 和 iOS9 下,notification 增加了一些新的特性: iOS8 增加了下拉时的 Action 按钮,像微信一样; iOS9 增加了像信息一样的可以下拉直接输入; iOS10 中,可以允许推送添加交互操作 action,这些 action 可以使得 App 在前台或后台执行一些逻辑代码。如:推出键盘进行快捷回复,该功能以往只在 iMessage 中可行。在 iOS 10 中,这叫 category,是对推送功能的一个拓展,其实是独立出来的不要和创建 push 混为一谈,它只是一个扩展功能,可加可不加的。可以通过 3D-Touch 或下拉消息触发,如果你的手机不支持 3D-Touch 也没关系,左滑点击出现的 "查看" 选项来触发。 6.1 添加 Action 1、创建 Action // 按钮 Action UNNotificationAction *lookAction = [UNNotificationAction actionWithIdentifier:@"action.look" title:@"查看邀请" options:UNNotificationActionOptionForeground]; UNNotificationAction *joinAction = [UNNotificationAction actionWithIdentifier:@"action.join" title:@"接收邀请" options:UNNotificationActionOptionAuthenticationRequired]; UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"action.cancel" title:@"取消" options:UNNotificationActionOptionDestructive]; // 输入 Action UNTextInputNotificationAction *inputAction = [UNTextInputNotificationAction actionWithIdentifier:@"action.input" title:@"输入" options:UNNotificationActionOptionForeground textInputButtonTitle:@"发送" textInputPlaceholder:@"tell me loudly"]; 注意点 UNNotificationActionOptions 是一个枚举类型,是用来标识 Action 触发的行为方式,分别是: // 被执行前需要解锁屏幕。Whether this action should require unlocking before being performed. UNNotificationActionOptionAuthenticationRequired = (1 << 0), // 被用于取消按钮。Whether this action should be indicated as destructive. UNNotificationActionOptionDestructive = (1 << 1), // 会使应用程序进入到前台状态,即打开应用程序。Whether this action should cause the application to launch in the foreground. UNNotificationActionOptionForeground = (1 << 2), 2、创建 category UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"Dely_locationCategory" actions:@[lookAction, joinAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; 注意点 + (instancetype)categoryWithIdentifier:(NSString *)identifier actions:(NSArray *)actions intentIdentifiers:(NSArray *)intentIdentifiers options:(UNNotificationCategoryOptions)options; 方法中: identifier :标识符,是这个 category 的唯一标识,用来区分多个 category, 这个 id 不管是本地推送,还是远程推送,一定要有并且要保持一致。 actions :是你创建 action 的操作数组 intentIdentifiers :意图标识符,可在 <Intents/INIntentIdentifiers.h> 中查看,主要是针对电话、carplay 等开放的 API options :通知选项,枚举类型,也是为了支持 carplay 3、把 category 添加到通知中心 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center setNotificationCategories:[NSSet setWithObject:category]]; 4、处理 Action 点击事件 所有的(不管远程或者本地)Push 点击都会走到下面的代理方法,只要在此方法中判断点击的按钮做相应的处理即可。 // UNUserNotificationCenterDelegate 协议方法,处理推送消息点击事件 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __TVOS_PROHIBITED; 6.2 完整 Action 创建添加 1、创建 Action 创建按钮 Action // 创建按钮 Action UNNotificationAction *lookAction = [UNNotificationAction actionWithIdentifier:@"action.look" title:@"查看邀请" options:UNNotificationActionOptionForeground]; UNNotificationAction *joinAction = [UNNotificationAction actionWithIdentifier:@"action.join" title:@"接收邀请" options:UNNotificationActionOptionAuthenticationRequired]; UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"action.cancel" title:@"取消" options:UNNotificationActionOptionDestructive]; // 注册 category UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"Dely_locationCategory" actions:@[lookAction, joinAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; // 将 category 添加到通知中心 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center setNotificationCategories:[NSSet setWithObject:category]]; 创建输入 Action // 创建输入 Action UNTextInputNotificationAction *inputAction = [UNTextInputNotificationAction actionWithIdentifier:@"action.input" title:@"输入" options:UNNotificationActionOptionForeground textInputButtonTitle:@"发送" textInputPlaceholder:@"tell me loudly"]; // 注册 category UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:@"Dely_locationCategory" actions:@[inputAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; // 将 category 添加到通知中心 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center setNotificationCategories:[NSSet setWithObject:category]]; 2、添加 Action 本地推送或远程推送中在 Appdelegate 方法如下位置添加创建 Action 的代码。其中 [self addNotificationAction]; 方法是单独创建 Action 的代码。 远程推送的推送代码中一定要保证里面包含 category 键值对,且值一定要与代码中的创建 category 时的 Identifier 一致。 { "aps" : { "alert" : { "title" : "远程推送通知 - title", "subtitle" : "远程推送通知 - subtitle", "body" : "远程推送通知 - body,远程推送通知远程推送通知远程推送通知远程推送通知远程推送通知远程推送通知" }, "badge" : "6", "sound" : "default", "category" : "Push_remoteCategory" } } 3、处理 Action 点击事件 // UNUserNotificationCenterDelegate 协议方法,处理推送消息点击事件 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler { // 获取 Action 的 id NSString *actionID = response.actionIdentifier; // 输入 Action if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) { NSString *inputText = [(UNTextInputNotificationResponse *)response userText]; NSLog(@"actionID = %@\n inputText = %@", actionID, inputText); } // 点击 Action if ([actionID isEqualToString:@"action.join"]) { NSLog(@"actionID = %@\n", actionID); } else if ([actionID isEqualToString:@"action.look"]) { NSLog(@"actionID = %@\n", actionID); } } 效果 显示 Action 按钮 本地推送通知 远程推送通知 7、富文本推送和自定义推送界面 本地推送和远程推送同时都可支持附带 Media Attachments(媒体附件)。不过远程通知需要实现通知服务扩展 UNNotificationServiceExtension,在 service extension 里面去下载附件(Attachments),但是需要注意,service extension 会限制下载的时间(30s),并且下载的文件大小也会同样被限制。这里毕竟是一个推送,而不是把所有的内容都推送给用户。所以你应该去推送一些缩小比例之后的版本。比如图片,推送里面附带缩略图,当用户打开 app 之后,再去下载完整的高清图。视频就附带视频的关键帧或者开头的几秒,当用户打开 app 之后再去下载完整视频。 附件支持图片,音频,视频,附件支持的类型及大小 系统会在通知注册前校验附件,如果附件出问题,通知注册失败;校验成功后,附件会转入 attachment data store;如果附件是在 app bundle,则是会被 copy 来取代 move media attachments 可以利用 3d touch 进行预览和操作。 attachment data store 的位置?利用代码测试获取在磁盘上的图片文件作为 attachment,会发现注册完通知后,图片文件被移除,在 app 的沙盒中找不到该文件在哪里;想要获取已存在的附件内容,文档中提及可以通过 UNUserNotificationCenter 中方法,但目前文档中这 2 个方法还是灰的,见苹果开发者文档 // 就是这两个方法 getDataForAttachment:withCompletionHandler: getReadFileHandleForAttachment:withCompletionHandler: 7.1 富文本推送的创建 1、准备工作 附件限定 https 协议,所以我们现在找一个支持 https 的图床用来测试,具体附件格式可以查看苹果开发文档。 2、添加新的 Targe –> Notification Service 先在 Xcode 打开你的工程,File –> New –> Targe 然后添加这个 Notification Service: 这样在你工程里能看到下面目录,然后会自动创建一个 UNNotificationServiceExtension 的子类 NotificationService,通过完善这个子类,来实现你的需求。 点开 NotificationService.m 会看到 2 个方法: - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; // Modify the notification content here... self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title]; self.contentHandler(self.bestAttemptContent); } - (void)serviceExtensionTimeWillExpire { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, // otherwise the original push payload will be used. self.contentHandler(self.bestAttemptContent); } didReceiveNotificationRequest :让你可以在后台处理接收到的推送,传递最终的内容给 contentHandler。 serviceExtensionTimeWillExpire :在你获得的一小段运行代码的时间即将结束的时候,如果仍然没有成功的传入内容,会走到这个方法,可以在这里传肯定不会出错的内容,或者他会默认传递原始的推送内容。 3、设置下载附件 主要的思路就是在这里把附件下载下来,然后才能展示渲染,下面是下载保存的相关方法: - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; // 下载图片,放到本地 NSString *fileURL = [request.content.userInfo objectForKey:@"image"]; NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]]; // 需要 https 连接 UIImage *imageFromUrl = [UIImage imageWithData:data]; // 获取 documents 目录 NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; // 将所下载的图片保存到本地 NSString *localPath = [self saveImage:imageFromUrl withFileName:@"MyImage" ofType:@"png" inDirectory:path]; if (localPath && ![localPath isEqualToString:@""]) { UNNotificationAttachment * attachment = [UNNotificationAttachment attachmentWithIdentifier:@"photo" URL:[NSURL URLWithString:[@"file://" stringByAppendingString:localPath]] options:nil error:nil]; if (attachment) { self.bestAttemptContent.attachments = @[attachment]; } } self.contentHandler(self.bestAttemptContent); } // 将所下载的图片保存到本地 - (NSString *) saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath { NSString *urlStr = @""; if ([[extension lowercaseString] isEqualToString:@"png"]){ urlStr = [directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"png"]]; [UIImagePNGRepresentation(image) writeToFile:urlStr options:NSAtomicWrite error:nil]; } else if ([[extension lowercaseString] isEqualToString:@"jpg"] || [[extension lowercaseString] isEqualToString:@"jpeg"]){ urlStr = [directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", imageName, @"jpg"]]; [UIImageJPEGRepresentation(image, 1.0) writeToFile:urlStr options:NSAtomicWrite error:nil]; } else{ NSLog(@"extension error"); } return urlStr; } - (void)serviceExtensionTimeWillExpire { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, // otherwise the original push payload will be used. self.contentHandler(self.bestAttemptContent); } 4、添加新的 Targe –> Notification Content 先在 Xcode 打开你的工程,File –> New –> Targe 然后添加这个 Notification Content: 这样你在工程里同样看到下面的目录 点开 NotificationViewController.m 会看到 2 个方法: - (void)viewDidLoad; - (void)didReceiveNotification:(UNNotification *)notification; 前者渲染 UI,后者获取通知信息,更新 UI 控件中的数据。 5、自定义 UI 界面,展示推送内容 在 MainInterface.storyboard 中自定你的 UI 页面,可以随意发挥,但是这个 UI 见面只能用于展示,并不能响应点击或者手势事件,点击或者手势事件只能通过 category 来实现,下面自己添加 view 和约束。 然后把 view 拉到 NotificationViewController.m 文件中,代码如下 #import "NotificationViewController.h" #import <UserNotifications/UserNotifications.h> #import <UserNotificationsUI/UserNotificationsUI.h> @interface NotificationViewController () <UNNotificationContentExtension> @property IBOutlet UILabel *label; @property (weak, nonatomic) IBOutlet UIImageView *imageView; @end @implementation NotificationViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any required interface initialization here. } - (void)didReceiveNotification:(UNNotification *)notification { self.label.text = notification.request.content.body; UNNotificationContent *content = notification.request.content; UNNotificationAttachment *attachment = content.attachments.firstObject; if (attachment.URL.startAccessingSecurityScopedResource) { self.imageView.image = [UIImage imageWithContentsOfFile:attachment.URL.path]; } } @end 有人要有疑问了,可不可以不用 storyboard 来自定义界面?当然可以了!只需要在 Notifications Content 的 info.plist 中把 NSExtensionMainStoryboard 替换为 NSExtensionPrincipalClass,并且 value 对应你的类名!然后在 viewDidLoad 里用纯代码布局就可以了。 6、创建远程富文本推送 apes 如下 { "aps" : { "alert" : { "title" : "远程推送通知 - title", "subtitle" : "远程推送通知 - subtitle", "body" : "远程推送通知 - body,远程推送通知远程推送通知远程推送通知远程推送通知远程推送通知远程推送通知" }, "badge" : "6", "sound" : "default", "mutable-content" : "1", "category" : "Push_remoteCategory" }, "image" : "https://avatars3.githubusercontent.com/u/13508076?v=3&s=460", "type" : "scene", "id" : "1007" } 注意:mutable-content 这个键值为 1,这意味着此条推送可以被 Service Extension 进行更改,也就是说要用 Service Extension 需要加上这个键值为 1。 完成上面的工作的时候基本上可以了!然后运行工程,上面的 json 数据放到 APNS Pusher 里面点击 Push: 稍等片刻应该能收到消息,长按或者右滑查看 注意:如果你添加了 category,需要在 Notification content 的 info.plist 添加一个键值对 UNNotificationExtensionCategory 的 value 值和 category Action 的 category 值保持一致就行。 同时在推送 json 中添加 category 键值对也要和上面两个地方保持一致 就变成了下面 7、创建本地富文本推送 上面介绍了远程需要 Service Extension 的远程推送,iOS10 附件通知(图片、gif、音频、视频)。不过对图片和视频的大小做了一些限制(图片不能超过 10M,视频不能超过 50M),而且附件资源必须存在本地,如果是远程推送的网络资源需要提前下载到本地。 如果是本地的就简单了只需要在 Service Extension 的 NotificationService.m 的如下方法中拿到资源添加到 Notification Content,在 Notification Content 的控制器取到资源自己来做需求处理和展示。 - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; // 资源路径 NSURL *pictureURL = [[NSBundle mainBundle] URLForResource:@"13508076" withExtension:@"png"]; // 创建附件资源 // * identifier 资源标识符 // * URL 资源路径 // * options 资源可选操作 比如隐藏缩略图之类的 // * error 异常处理 UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"picture.attachment" URL:pictureURL options:nil error:nil]; // 将附件资源添加到 UNMutableNotificationContent 中 if (attachment) { self.bestAttemptContent.attachments = @[attachment]; } self.contentHandler(self.bestAttemptContent); } 下如果你想把 default 隐藏掉,只需要在 Notification Content 的 info.plist 中添加一个键值 UNNotificationExtensionDefaultContentHidden 设置为 YES 就可以了: 7.2 UNNotificationContentExtension 简单来说,UNNotificationContentExtension 这个类,也是 iOS10 推送的新特性,官方文档用这么一句话,简单的解释了一下,Presents a custom interface for a delivered local or remote notification.(当你收到远程或者本地通知的时候,弹出一个自定义界面)。效果如下图所示,在自定义 View 的区域,你可以放上个视频,放上个日历,放上个显示地理位置的 Label,总而言之,我们可以自定义 View。 7.2.1 如何新建一个 UNNotificationContentExtension 先在 Xcode 打开你的工程,File –> New –> Targe 然后添加这个 Notification Content: 这样你在工程里同样看到下面的目录 点开 NotificationViewController.m 会看到 2 个方法: - (void)viewDidLoad; - (void)didReceiveNotification:(UNNotification *)notification; 前者渲染 UI,后者获取通知信息,更新 UI 控件中的数据。 7.2.2 Info.plist 文件 在这个 NSExtensionAttributes 的字典下面,我们有三个属性可以添加 1、UNNotificationExtensionCategory 必须要有,系统已经创建好 解释:对应这个 key 的值,可以是一个字符串,也可以是一个数组,每一个字符串都是一个 identifier,这个 identifier 对应着每一个 UNMutableNotificationContent 的 categoryIdentifier 的属性。 简单来说,就是在收到通知的时候,我们可以让服务器把这个通知的 categoryIdentifier 带上,作用是,我们可以根据视频,音乐,图片,来分别自定义我们的通知内容。不同的分类标识符,也会在我们讲到 UNNotificationAction 的时候,帮助我们区分是什么类型的通知,方便我们对不同类型的通知做出不同的操作行为。上面的截图中,我是一个字符串的形式。下图为数组形式: 使用的时候,我们参照如下代码: // 这个方法是 UNNotificationServiceExtension 类里面的方法。 - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { self.contentHandler = contentHandler; // copy 发来的通知,开始做一些处理 self.bestAttemptContent = [request.content mutableCopy]; // Modify the notification content here... self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title]; // 重写一些东西 self.bestAttemptContent.title = @"我是标题"; self.bestAttemptContent.subtitle = @"我是子标题"; self.bestAttemptContent.body = @"来自不同"; // 附件 NSDictionary *dict = self.bestAttemptContent.userInfo; NSDictionary *notiDict = dict[@"aps"]; NSString *imgUrl = [NSString stringWithFormat:@"%@",notiDict[@"imageAbsoluteString"]]; !!!!! 这里是重点!!!!!!!!!!!! // 在这里写死了 category1,其实在收到系统推送时,每一个推送内容最好带上一个 catagory,跟服务器约定好了, // 这样方便我们根据 categoryIdentifier 来自定义不同类型的视图,以及 action self.bestAttemptContent.categoryIdentifier = @"category1"; } 这里设置 categoryIdentifier,最好让服务器的推送内容带上这个,然后我们好更加的定制化,不建议本地写死。 2、UNNotificationExtensionInitialContentSizeRatio 必须要有,系统已经创建好 解释:这个值的类型是一个浮点类型,代表的是高度与宽度的比值。系统会使用这个比值,作为初始化 view 的大小。举个简单的例子来说,如果该值为 1,则该视图为正方形。如果为 0.5,则代表高度是宽度的一半。 注意这个值只是初始化的一个值,在这个扩展添加后,可以重写 frame,展示的时候,在我们还没打开这个视图预览时,背景是个类似图片占位的灰色,那个灰色的高度宽度之比,就是通过这个值来设定。 3、UNNotificationExtensionDefaultContentHidden 可选 解释:这个值是一个 BOOL 值,当为 YES 时,会隐藏上方原本推送的内容视图,只会显示我们自定义的视图。(因为在自定义视图的时候,我们可以取得推送内容,然后按照我们想要的布局,展示出来)如果为 NO 时(默认为 NO),推送视图就会既有我们的自定义视图,也会有系统原本的推送内容视图(这里附件是不会显示的,只会显示 body 里面的文字)。 4、至于 NSExtensionMainStoryboard 以及 NSExtensionPointIdentifier,系统默认生成,大家直接用就好,如果需要更改的,只能更改使用的 storyboard 的名字。 7.2.3 MainInterface.storyboard 文件 这个就是个简单的 storyboard 文件,内部有一个 View,这个 View 就是在上面的图层中的自定义 View 视图了。它与 NotificationViewController 所绑定。 7.2.4 NotificationViewController 文件 这是是系统帮我们默认创建了一个控制器,继承 UIViewController,其实就是一个控制器啦。 遵守 UNNotificationContentExtension 的协议,我们需要用到一下的方法 // 这个方法是说,只要你收到通知,并且保证 categoryIdentifier 的设置,跟 info.plist 里面设置的一样,你就会调用这个方法。 // 注意:一个会话的多个通知,每个通知收到时,都可以调用这个方法。 - (void)didReceiveNotification:(UNNotification *)notification; 使用如下 - (void)didReceiveNotification:(UNNotification *)notification { // 这个方法,可以给自己的控件赋值,调整 frame 等等,在这里打印出来了通知的内容。 NSDictionary *dict = notification.request.content.userInfo; // 这里可以把打印的所有东西拿出来 NSLog(@"%@",dict); /**************************** 打印的信息是 ************ aps = { alert = "This is some fancy message."; badge = 1; from = "大家好"; imageAbsoluteString = "http://upload.univs.cn/2012/0104/1325645511371.jpg"; "mutable-content" = 1; sound = default; }; } *******************************************/ } 说到这里,简单的 UNNotificationContentExtension 已经说完了。在 UNNotificationContentExtension.h 中,有着这么一个枚举 typedef NS_ENUM(NSUInteger, UNNotificationContentExtensionMediaPlayPauseButtonType) { // 没有播放按钮 UNNotificationContentExtensionMediaPlayPauseButtonTypeNone, // 有播放按钮,点击播放之后,按钮依旧存在,类似音乐播放的开关 UNNotificationContentExtensionMediaPlayPauseButtonTypeDefault, // 有播放按钮,点击后,播放按钮消失,再次点击暂停播放后,按钮恢复 UNNotificationContentExtensionMediaPlayPauseButtonTypeOverlay, } 看到这么枚举,大家一定纳闷怎么使用啊。请看下面的几个属性 // 设置播放按钮的属性 @property (nonatomic, readonly, assign) UNNotificationContentExtensionMediaPlayPauseButtonType mediaPlayPauseButtonType; // 设置播放按钮的frame @property (nonatomic, readonly, assign) CGRect mediaPlayPauseButtonFrame; // 设置播放按钮的颜色 @property (nonatomic, readonly, copy) UIColor *mediaPlayPauseButtonTintColor; // 开始跟暂停播放 - (void)mediaPlay; - (void)mediaPause; 还有以下的类,这个类虽然也有开始播放跟结束播放的方法,不过要注意,这个是属于 NSExtensionContext 的,而上面我们讲的方法是 UNNotificationContentExtension 协议方法里的。大家要注意。 @interface NSExtensionContext (UNNotificationContentExtension) // 控制播放 - (void)mediaPlayingStarted // 控制暂停 - (void)mediaPlayingPaused @end 看到这些属性,想要知道如何使用,请看下面的步骤: 分析:首先这些属性都是 readonly 的,所以直接用 self.属性去修改肯定是报错的,所以我们能用的就只有 get 方法了。 其次:根据 button 的类型,我们可以联想到,如果 button 没有,这个播放开始暂停的方法也没用了。如果有 button,自然我们就有了播放的操作,联想别的 UI 空间,我们得出了一定要重写它的 frame,来确定他的位置。设置颜色,来设置它的显示颜色。设置 button 的类型,让他显示出来。 // 返回默认样式的 button - (UNNotificationContentExtensionMediaPlayPauseButtonType)mediaPlayPauseButtonType { return UNNotificationContentExtensionMediaPlayPauseButtonTypeDefault; } // 返回 button 的 frame - (CGRect)mediaPlayPauseButtonFrame { return CGRectMake(100, 100, 100, 100); } // 返回 button 的颜色 - (UIColor *)mediaPlayPauseButtonTintColor { return [UIColor blueColor]; } 通过上面的代码,我们的 button 已经可以显示出来了。如下图(请忽略下面的策略等按钮) 具体位置,大家可以通过重写 frame 来确定 button 的位置。当我们点击这个蓝色 button 的时候,便可以执行一些播放暂停操作了,如下 - (void)mediaPlay{ NSLog(@"mediaPlay, 开始播放"); } - (void)mediaPause{ NSLog(@"mediaPause,暂停播放"); } 说道这里,还少说一个地方,那就是 NSExtensionContext 类的播放暂停事件我们需要什么时候调用呢?可以这么使用,如下 - (void)mediaPlay{ NSLog(@"mediaPlay, 开始播放"); // 点击播放按钮后,4s 后暂停播放 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.extensionContext mediaPlayingPaused]; }); } - (void)mediaPause{ NSLog(@"mediaPause,暂停播放"); // 点击暂停按钮,10s后开始播放 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.extensionContext mediaPlayingStarted]; }); } 这里需要说几个注意点 1 在这个控制器中,我们可以直接电泳 self.extensionContext,来调用它的播放暂停方法。 2 调用这个播放暂停方法,并不会重新调用 - (void)mediaPlay{} 或者 - (void)mediaPause{},只能单纯的调用。 - (void)mediaPlayingStarted { NSLog(@"主动调用开始的方法"); } - (void)mediaPlayingPaused { NSLog(@"主动调用暂停的方法"); } 参考资料: https://developer.apple.com/reference/usernotifications http://www.jianshu.com/p/b74e52e866fc http://www.jianshu.com/p/b74e52e866fc http://blog.csdn.net/he317165264/article/details/52574934 http://qoofan.com/read/PnEaMEZonD.html http://www.qingpingshan.com/rjbc/ios/140921.html 8、友盟 推送 集成 U-Push 基于友盟统计的精准推送方案。帮助开发者建立与用户直接沟通的渠道,将 APP 的内容更新或者活动通知主动推送给终端用户,让用户第一时间获取到相关信息,有效提升用户活跃度和忠诚度。 U-Push 集成文档 U-Push iOS 证书配置指南 U-Push API 说明 U-Push SDK 下载
1、创建 MKMapView 地图 在 iOS6 或者 iOS7 中实现这个功能只需要添加地图控件、设置用户跟踪模式、在 mapView:didUpdateUserLocation: 代理方法中设置地图中心区域及显示范围。 在 iOS8+ 中用法稍有不同: a. 由于在地图中进行用户位置跟踪需要使用定位功能,而定位功能在 iOS8 中设计发生了变化,因此必须按照定位中提到的内容进行配置和请求。 b. iOS8+ 中不需要进行中心点的指定,默认会将当前位置设置中心点并自动设置显示区域范围。 // 包含头文件 #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h> // 遵守协议 <MKMapViewDelegate, CLLocationManagerDelegate> // 声明地图控件 @property (nonatomic, strong) MKMapView *mapView; 1、请求定位 // 实例化定位管理器 CLLocationManager *locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; // 判断系统定位服务是否开启 if (![CLLocationManager locationServicesEnabled]) { NSLog(@"%@", @"提示:系统定位服务不可用,请开启 !"); } else { // 判断应用定位服务授权状态 if([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined){ // 没有授权 // 8.0 及以上系统需手动请求定位授权 if ([UIDevice currentDevice].systemVersion.doubleValue >= 8.0) { // 前台定位,需在 info.plist 里设置 Privacy - Location When In Use Usage Description 的值 [locationManager requestWhenInUseAuthorization]; // 前后台同时定位,需在 info.plist 里设置 Privacy - Location Always Usage Description 的值 // [self.locationManager requestAlwaysAuthorization]; } // 开始定位追踪(第一次打开软件时) [locationManager startUpdatingLocation]; } else if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways) { // 允许定位授权 // 开始定位追踪 [locationManager startUpdatingLocation]; } else{ // 拒绝定位授权 // 创建警告框(自定义方法) NSLog(@"%@", @"提示:当前应用的定位服务不可用,请检查定位服务授权状态 !"); } } 2、创建地图 /* mapType: MKMapTypeStandard = 0, 标准类型 MKMapTypeSatellite, 卫星图 MKMapTypeHybrid 混合类型 userTrackingMode:用户位置追踪用于标记用户当前位置,此时会调用定位服务,必须先设置定位请求 MKUserTrackingModeNone = 0, 不跟踪用户位置 MKUserTrackingModeFollow, 跟踪并在地图上显示用户的当前位置 MKUserTrackingModeFollowWithHeading, 跟踪并在地图上显示用户的当前位置,地图会跟随用户的前进方向进行旋转 */ // 实例化地图控件 self.mapView = [[MKMapView alloc] initWithFrame:self.view.bounds]; self.mapView.delegate = self; // 设置地图类型 self.mapView.mapType = MKMapTypeStandard; // 设置跟踪模式 self.mapView.userTrackingMode = MKUserTrackingModeFollow; [self.view addSubview:self.mapView]; #pragma mark - MKMapViewDelegate 协议方法 // 更新到用户的位置 - (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation{ // 只要用户位置改变就调用此方法(包括第一次定位到用户位置),userLocation:是对用来显示用户位置的蓝色大头针的封装 // 反地理编码 [[[CLGeocoder alloc] init] reverseGeocodeLocation:userLocation.location completionHandler:^(NSArray *placemarks, NSError *error) { CLPlacemark *placemark = [placemarks firstObject]; // 设置用户位置蓝色大头针的标题 userLocation.title = [NSString stringWithFormat:@"当前位置:%@, %@, %@", placemark.thoroughfare, placemark.locality, placemark.country]; }]; // 设置用户位置蓝色大头针的副标题 userLocation.subtitle = [NSString stringWithFormat:@"经纬度:(%lf, %lf)", userLocation.location.coordinate.longitude, userLocation.location.coordinate.latitude]; // 手动设置显示区域中心点和范围 if ([UIDevice currentDevice].systemVersion.floatValue < 8.0) { // 显示的中心 CLLocationCoordinate2D center = userLocation.location.coordinate; // 设置地图显示的中心点 [self.mapView setCenterCoordinate:center animated:YES]; // 设置地图显示的经纬度跨度 MKCoordinateSpan span = MKCoordinateSpanMake(0.023503, 0.017424); // 设置地图显示的范围 MKCoordinateRegion rengion = MKCoordinateRegionMake(center, span); [self.mapView setRegion:rengion animated:YES]; } } // 地图显示的区域将要改变 - (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated { NSLog(@"区域将要改变:经度:%lf, 纬度:%lf, 经度跨度:%lf, 纬度跨度:%lf", mapView.region.center.longitude, mapView.region.center.latitude, mapView.region.span.longitudeDelta, mapView.region.span.latitudeDelta); } // 地图显示的区域改变了 - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated { NSLog(@"区域已经改变:经度:%lf, 纬度:%lf, 经度跨度:%lf, 纬度跨度:%lf", mapView.region.center.longitude, mapView.region.center.latitude, mapView.region.span.longitudeDelta, mapView.region.span.latitudeDelta); } 效果 2、添加大头针 2.1 添加大头针 自定义大头针模型 QAnnotation.h #import <MapKit/MapKit.h> @interface QAnnotation : NSObject <MKAnnotation> @property (nonatomic, copy)NSString *title; @property (nonatomic, copy)NSString *subtitle; @property (nonatomic, copy)NSString *icon; @property (nonatomic, assign)CLLocationCoordinate2D coordinate; /// 初始化大头针模型 + (instancetype)q_annotationWithTitle:(NSString *)title subTitle:(NSString *)subTitle icon:(NSString *)icon coordinate:(CLLocationCoordinate2D)coordinate; @end QAnnotation.m @implementation QAnnotation /// 初始化大头针模型 + (instancetype)q_annotationWithTitle:(NSString *)title subTitle:(NSString *)subTitle icon:(NSString *)icon coordinate:(CLLocationCoordinate2D)coordinate{ QAnnotation *annotation = [[self alloc] init]; annotation.title = title; annotation.subtitle = subTitle; annotation.icon = icon; annotation.coordinate = coordinate; return annotation; } @end 添加大头针 ViewController.m #import "QAnnotation.h" // 首先创建 MKMapView 地图 // 设置大头针显示的内容 NSString *title = @"xxx大饭店"; NSString *subtitle = @"全场一律15折,会员20折"; NSString *icon = @"category_1"; // 设置大头针放置的位置 CLLocationCoordinate2D cl2d = CLLocationCoordinate2DMake(40.1020, 116.3265); // 初始化大头针模型 QAnnotation *annotation = [QAnnotation q_annotationWithTitle:title subTitle:subtitle icon:icon coordinate:cl2d]; // 在地图上添加大头针控件 [self.mapView addAnnotation:annotation]; 效果 2.2 设置大头针样式 设置大头针样式 // MKMapViewDelegate 协议方法 - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { /* 显示大头针时触发,返回大头针视图,通常自定义大头针可以通过此方法进行。 使用遵守协议 <MKAnnotation> 的模型,写此方法时所有遵守协议 <MKAnnotation> 的大头针模型都会改变,不写时为默认样式的大头针。 */ // 判断大头针模型是否属于 QAnnotation 类 if ([annotation isKindOfClass:[QAnnotation class]]) { // 显示自定义样式的大头针 // 获得大头针控件,利用自定义的( QAnnotationView )大头针控件创建 QAnnotationView *annotationView = [QAnnotationView q_annotationViewWithMapView:mapView]; // 传递模型,更新大头针数据,覆盖掉之前的旧数据 annotationView.annotation = annotation; return annotationView; } else { // 显示系统样式的大头针 // 先从缓存池中取出可以循环利用的大头针控件,利用带针的( MKPinAnnotationView )子类大头针控件创建 MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"qianchia"]; // 缓存池中没有可以利用的大头针控件 if (annotationView == nil) { // 创建大头针控件 annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@"qianchia"]; // 设置大头针头的颜色 annotationView.pinColor = MKPinAnnotationColorGreen; // 大头针从天而降 annotationView.animatesDrop = YES; // 显示大头针标题和子标题 annotationView.canShowCallout = YES; // 设置子菜单的偏移量 annotationView.calloutOffset = CGPointMake(0, -10); // 自定义子菜单的左右视图 annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeContactAdd]; annotationView.leftCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeInfoDark]; } // 传递模型,更新大头针数据,覆盖掉之前的旧数据 annotationView.annotation = annotation; return annotationView; } } 效果 2.3 不同大头针样式的创建 1、带针的大头针 // 先从缓存池中取出可以循环利用的大头针控件,利用带针的( MKPinAnnotationView )子类大头针控件创建 MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"qianchia"]; // 缓存池中没有可以利用的大头针控件 if (annotationView == nil) { // 创建大头针控件 annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@"qianchia"]; // 设置大头针头的颜色 annotationView.pinColor = MKPinAnnotationColorGreen; // 大头针从天而降 annotationView.animatesDrop = YES; // 显示大头针标题和子标题 annotationView.canShowCallout = YES; // 设置子菜单的偏移量 annotationView.calloutOffset = CGPointMake(0, -10); // 自定义子菜单的左右视图 annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeContactAdd]; annotationView.leftCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeInfoDark]; } // 传递模型,更新大头针数据,覆盖掉之前的旧数据 annotationView.annotation = annotation; return annotationView; 效果 2、不带针的大头针 // 先从缓存池中取出可以循环利用的大头针控件 利用不带针的( MKAnnotationView )父类大头针控件创建 MKAnnotationView *annotationView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"qianchia"]; // 缓存池中没有可以利用的大头针控件 if (annotationView == nil) { // 创建大头针控件 annotationView = [[MKAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@"qianchia"]; // 显示大头针标题和子标题 annotationView.canShowCallout = YES; // 设置子菜单的偏移量 annotationView.calloutOffset = CGPointMake(0, -10); // 自定义子菜单的左右视图 annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeContactAdd]; annotationView.leftCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeInfoDark]; } // 传递模型,更新大头针数据,覆盖掉之前的旧数据 annotationView.annotation = annotation; // 设置大头针的图片,所有大头针图片相同 annotationView.image = [UIImage imageNamed:@"category_4"]; return annotationView; 效果 3、自定义类型的大头针 QAnnotationView.h @interface QAnnotationView : MKAnnotationView /// 创建大头针控件 + (instancetype)q_annotationViewWithMapView:(MKMapView *)mapView; @end QAnnotationView.m #import "QAnnotation.h" #import "UIView+Frame.h" @interface QAnnotationView () /// 自定义大头针子菜单图片视图 @property (nonatomic, strong) UIImageView *iconView; @end @implementation QAnnotationView #pragma mark - 创建大头针控件 /// 创建大头针控件 + (instancetype)q_annotationViewWithMapView:(MKMapView *)mapView { QAnnotationView *annotationView = (QAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"qianchia"]; if (annotationView == nil) { annotationView = [[self alloc] initWithAnnotation:nil reuseIdentifier:@"qianchia"]; } return annotationView; } /// 重写初始化大头针控件方法 - (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier]) { // 显示自定义大头针的标题和子标题 self.canShowCallout = YES; // 设置自定义大头针的子菜单左边显示一个图片 UIImageView *imageView = [[UIImageView alloc] init]; imageView.bounds = CGRectMake(0, 0, 40, 50); self.iconView = imageView; // 设置自定义大头针的子菜单左边视图 self.leftCalloutAccessoryView = self.iconView; } return self; } /// 重写大头针模型的 setter 方法 - (void)setAnnotation:(QAnnotation *)annotation{ [super setAnnotation:annotation]; // 设置自定义大头针图片 self.image = [UIImage imageNamed:annotation.icon]; // 设置自定义大头针的子菜单图片 self.iconView.image = [UIImage imageNamed:annotation.icon]; } @end ViewController.m // 判断大头针模型是否属于 QAnnotation 类 if ([annotation isKindOfClass:[QAnnotation class]]) { // 获得大头针控件,利用自定义的( QAnnotationView )大头针控件创建 QAnnotationView *annotationView = [QAnnotationView q_annotationViewWithMapView:mapView]; // 传递模型,更新大头针数据,覆盖掉之前的旧数据 annotationView.annotation = annotation; return annotationView; } 效果 3、地图画线 设置起点和终点 // 获取起点和终点 NSString *sourceAddress = [alertView textFieldAtIndex:0].text; NSString *destinationAddress = [alertView textFieldAtIndex:1].text; // 地理编码 起点 [[[CLGeocoder alloc] init] geocodeAddressString:sourceAddress completionHandler:^(NSArray *placemarks, NSError *error) { if (placemarks == nil || error) { return; } else { CLPlacemark *sourcePlacemark = [placemarks firstObject]; // 移除以前的大头针 if (self.sourceAnnotation) { [self.mapView removeAnnotation:self.sourceAnnotation]; } // 添加新的大头针 self.sourceAnnotation = [QAnnotation q_annotationWithTitle:sourceAddress subTitle:sourcePlacemark.name icon:nil coordinate:sourcePlacemark.location.coordinate]; [self.mapView addAnnotation:self.sourceAnnotation]; // 地理编码 终点 [[[CLGeocoder alloc] init] geocodeAddressString:destinationAddress completionHandler:^(NSArray *placemarks, NSError *error) { if (placemarks == nil || error) { return; } else { CLPlacemark *destinationPlacemark = [placemarks firstObject]; // 移除以前的大头针 if (self.destinationAnnotation) { [self.mapView removeAnnotation:self.destinationAnnotation]; } // 添加新的大头针 self.destinationAnnotation = [QAnnotation q_annotationWithTitle:destinationAddress subTitle:destinationPlacemark.name icon:nil coordinate:destinationPlacemark.location.coordinate]; [self.mapView addAnnotation:self.destinationAnnotation]; // 开始画线 [self drawLineWithSourceCLPlacemark:sourcePlacemark destinationCLPlacemark:destinationPlacemark]; } }]; } }]; 开始画线 // 自定义方法 - (void)drawLineWithSourceCLPlacemark:(CLPlacemark *)sourceCLPm destinationCLPlacemark:(CLPlacemark *)desinationCLPm{ // 初始化方向请求 MKDirectionsRequest *dRequest = [[MKDirectionsRequest alloc] init]; // 设置起点( CLPlacemark --> MKPlacemark ) MKPlacemark *sourceMKPm = [[MKPlacemark alloc] initWithPlacemark:sourceCLPm]; dRequest.source = [[MKMapItem alloc] initWithPlacemark:sourceMKPm]; // 设置终点( CLPlacemark --> MKPlacemark ) MKPlacemark *destinationMKPm = [[MKPlacemark alloc] initWithPlacemark:desinationCLPm]; dRequest.destination = [[MKMapItem alloc] initWithPlacemark:destinationMKPm]; // 根据请求创建方向 MKDirections *directions = [[MKDirections alloc] initWithRequest:dRequest]; // 执行请求 [directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) { if (error) { return; } else { // 移除所有已画的线,移除旧的线 [self.mapView removeOverlays:self.mapView.overlays]; for (MKRoute *route in response.routes) { // 添加路线,传递路线的遮盖模型数据 [self.mapView addOverlay:route.polyline]; } } }]; } 设置画线属性 // MKMapViewDelegate 协议方法 - (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay{ MKPolylineRenderer *rederer = [[MKPolylineRenderer alloc] initWithOverlay:overlay]; rederer.lineWidth = 5; // 设置线宽 rederer.strokeColor = [UIColor blueColor]; // 设置线的颜色 return rederer; } 效果 4、地图导航 4.1 创建导航 设置起点和终点 // 获取起点和终点 NSString *sourceAddress = [alertView textFieldAtIndex:0].text; NSString *destinationAddress = [alertView textFieldAtIndex:1].text; // 地理编码 起点 [[[CLGeocoder alloc] init] geocodeAddressString:sourceAddress completionHandler:^(NSArray *placemarks, NSError *error) { if (placemarks == nil || error) { return; } else { CLPlacemark *sourcePlacemark = [placemarks firstObject]; // 地理编码 终点 [[[CLGeocoder alloc] init] geocodeAddressString:destinationAddress completionHandler:^(NSArray *placemarks, NSError *error) { if (placemarks == nil || error) { return; } else { CLPlacemark *destinationPlacemark = [placemarks firstObject]; // 开始导航 [self startNavigationWithSourceCLPlacemark:sourcePlacemark destinationCLPlacemark:destinationPlacemark]; } }]; } }]; 开始导航 设置导航参数: MKLaunchOptionsDirectionsModeKey // 导航模式 Key to a directions mode MKLaunchOptionsMapTypeKey // 地图类型 Key to an NSNumber corresponding to a MKMapType MKLaunchOptionsShowsTrafficKey // 交通路况 Key to a boolean NSNumber // Directions modes MKLaunchOptionsDirectionsModeDriving // 驾驶模式 MKLaunchOptionsDirectionsModeWalking // 步行模式 // If center and span are present, having a camera as well is undefined MKLaunchOptionsMapCenterKey // 地图中心 Key to an NSValue-encoded CLLocationCoordinate2D MKLaunchOptionsMapSpanKey // 地图跨度 Key to an NSValue-encoded MKCoordinateSpan MKLaunchOptionsCameraKey // 地图相机 Key to MKMapCamera object // 自定义方法 - (void)startNavigationWithSourceCLPlacemark:(CLPlacemark *)sourceCLPm destinationCLPlacemark:(CLPlacemark *)desinationCLPm{ if (sourceCLPm == nil || desinationCLPm == nil) { return; } else { // 设置起点( CLPlacemark --> MKPlacemark ) MKPlacemark *sourceMKPm = [[MKPlacemark alloc] initWithPlacemark:sourceCLPm]; MKMapItem *sourceItem = [[MKMapItem alloc] initWithPlacemark:sourceMKPm]; // 设置终点( CLPlacemark --> MKPlacemark ) MKPlacemark *destinationMKPm = [[MKPlacemark alloc] initWithPlacemark:desinationCLPm]; MKMapItem *destinationItem = [[MKMapItem alloc] initWithPlacemark:destinationMKPm]; NSArray *items = @[sourceItem, destinationItem]; // 设置导航参数(导航模式:驾驶导航,是否显示路况:是,地图类型:标准) NSDictionary *options = @{MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving, MKLaunchOptionsShowsTrafficKey: @YES, MKLaunchOptionsMapTypeKey: @(MKMapTypeStandard)}; // 打开苹果官方的导航应用(打开苹果自带地图 App 开始导航) [MKMapItem openMapsWithItems:items launchOptions:options]; } } 效果 4.2 快速创建导航 从当前位置到指定位置导航 // 根据“北京市”进行地理编码 [_geocoder geocodeAddressString:@"北京市" completionHandler:^(NSArray *placemarks, NSError *error) { CLPlacemark *clPlacemark = [placemarks firstObject]; // 获取第一个地标 MKPlacemark *mkplacemark = [[MKPlacemark alloc] initWithPlacemark:clPlacemark]; // 定位地标转化为地图的地标 NSDictionary *options = @{MKLaunchOptionsMapTypeKey:@(MKMapTypeStandard)}; MKMapItem *mapItem = [[MKMapItem alloc] initWithPlacemark:mkplacemark]; // 调用苹果地图开始导航,从当前位置到指定位置 [mapItem openInMapsWithLaunchOptions:options]; }]; 效果 从位置 1 到位置 2 导航 // 根据“北京市”进行地理编码 [_geocoder geocodeAddressString:@"北京市" completionHandler:^(NSArray *placemarks, NSError *error) { CLPlacemark *clPlacemark1 = [placemarks firstObject]; MKPlacemark *mkPlacemark1 = [[MKPlacemark alloc] initWithPlacemark:clPlacemark1]; // 注意地理编码一次只能定位到一个位置,不能同时定位,所在放到第一个位置定位完成回调函数中再次定位 [_geocoder geocodeAddressString:@"郑州市" completionHandler:^(NSArray *placemarks, NSError *error) { CLPlacemark *clPlacemark2 = [placemarks firstObject]; MKPlacemark *mkPlacemark2 = [[MKPlacemark alloc] initWithPlacemark:clPlacemark2]; NSDictionary *options = @{MKLaunchOptionsMapTypeKey:@(MKMapTypeStandard)}; MKMapItem *mapItem1 = [[MKMapItem alloc] initWithPlacemark:mkPlacemark1]; MKMapItem *mapItem2 = [[MKMapItem alloc] initWithPlacemark:mkPlacemark2]; // 调用苹果地图开始导航,从 Item1 到 Item2 [MKMapItem openMapsWithItems:@[mapItem1, mapItem2] launchOptions:options]; }]; }]; 效果
前言 NS_CLASS_AVAILABLE(10_8, 5_0) @interface CLGeocoder : NSObject 地理编码 地名 -> 经纬度 等具体位置数据信息。根据给定的位置(通常是地名)确定地理坐标(经、纬度)。 反地理编码 经纬度 -> 地名。可以根据地理坐标(经、纬度)确定位置信息(街道、门牌等)。 1、GeoCoder 地理编码 配置 // 包含头文件 #import <CoreLocation/CoreLocation.h> 地理编码 // 声明 CLGeocoder 对象 @property (nonatomic, strong) CLGeocoder *geocoder; // 实例化 CLGeocoder 对象 self.geocoder = [[CLGeocoder alloc] init]; // 开始编码 [self.geocoder geocodeAddressString:self.addressField.text completionHandler:^(NSArray *placemarks, NSError *error) { // 判断编码是否成功 if (error || 0 == placemarks.count) { NSLog(@"erroe = %@, placemarks.count = %ld", error, placemarks.count); self.detailAddressLabel.text = @"你输入的地址找不到,可能在火星上"; } else { // 编码成功(找到了具体的位置信息) // 输出查询到的所有地标信息 for (CLPlacemark *placemark in placemarks) { NSLog(@"name = %@, locality = %@, country = %@", placemark.name, placemark.locality, placemark.country); } // 显示最前面的地标信息 CLPlacemark *firstPlacemark = [placemarks firstObject]; self.longitudeLabel.text = [NSString stringWithFormat:@"%.2f", firstPlacemark.location.coordinate.longitude]; self.latitudeLabel.text = [NSString stringWithFormat:@"%.2f", firstPlacemark.location.coordinate.latitude]; self.detailAddressLabel.text = [NSString stringWithFormat:@"%@,%@,%@", firstPlacemark.name, firstPlacemark.locality, firstPlacemark.country]; } }]; 反地理编码 // 声明 CLGeocoder 对象 @property (nonatomic, strong)CLGeocoder *geocoder; // 实例化 CLGeocoder 对象 self.geocoder = [[CLGeocoder alloc] init]; // 创建 CLLocation 对象 CLLocation *location = [[CLLocation alloc] initWithLatitude:[self.latitudeField.text doubleValue] longitude:[self.longtitudeField.text doubleValue]]; // 开始反编码 [self.geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) { // 判断反编码是否成功 if (error || 0 == placemarks.count) { NSLog(@"erroe = %@, placemarks.count = %ld", error, placemarks.count); self.reverseDetailAddressLabel.text = @"你输入的经纬度找不到,可能在火星上"; } else { // 反编码成功(找到了具体的位置信息) // 输出查询到的所有地标信息 for (CLPlacemark *placemark in placemarks) { NSLog(@"name=%@, locality=%@, country=%@", placemark.name, placemark.locality, placemark.country); } // 显示最前面的地标信息 CLPlacemark *firstPlacemark = [placemarks firstObject]; self.longtitudeField.text = [NSString stringWithFormat:@"%.2f", firstPlacemark.location.coordinate.longitude]; self.latitudeField.text = [NSString stringWithFormat:@"%.2f", firstPlacemark.location.coordinate.latitude]; self.reverseDetailAddressLabel.text = [NSString stringWithFormat:@"%@,%@,%@", firstPlacemark.name, firstPlacemark.locality, firstPlacemark.country]; } }]; 地理编码信息: placemark.name, // 地名 placemark.thoroughfare, // 街道 placemark.subThoroughfare, // 街道相关信息,例如门牌等 placemark.locality, // 城市 placemark.subLocality, // 城市相关信息,例如标志性建筑 placemark.administrativeArea, // 州 placemark.subAdministrativeArea, // 其他行政区域信息 placemark.postalCode, // 邮编 placemark.ISOcountryCode, // 国家编码 placemark.country, // 国家 placemark.inlandWater, // 水源、湖泊 placemark.ocean, // 海洋 placemark.areasOfInterest // 关联的或利益相关的地标 placemark.addressDictionary[@"City"]]; // 城市 placemark.addressDictionary[@"Country"]]; // 国家 placemark.addressDictionary[@"CountryCode"]]; // 国家编码 placemark.addressDictionary[@"FormattedAddressLines"][0]]; // 街道 placemark.addressDictionary[@"Name"]]; // 地名 placemark.addressDictionary[@"State"]]; // 州 placemark.addressDictionary[@"SubLocality"]]; // 城市相关信息
前言 NS_CLASS_AVAILABLE(10_6, 2_0) @interface CLLocationManager : NSObject 1、CoreLocation 定位 配置 1、在 iOS7 及以前的版本,如果在应用程序中使用定位服务只要在程序中调用 startUpdatingLocation 方法应用就会询问用户是否允许此应用是否允许使用定位服务,同时在提示过程中可以通过在 info.plist 中配置通过配置 Privacy - Location Usage Description 告诉用户使用的目的,同时这个配置是可选的。 但是在 iOS8 中配置项发生了变化,可以通过配置 Privacy - Location Always Usage Description (NSLocationAlwaysUsageDescription) 或者 Privacy - Location When In Use Usage Description(NSLocationWhenInUseUsageDescription) 来告诉用户使用定位服务的目的,并且注意这个配置是必须的,如果不进行配置则默认情况下应用无法使用定位服务,打开应用不会给出打开定位服务的提示,除非安装后自己设置此应用的定位服务。同时,在应用程序中需要根据配置对 requestAlwaysAuthorization 或 requestWhenInUseAuthorization 方法进行请求。 2、在需要使用 CoreLocation 的文件中 // 包含头文件 #import <CoreLocation/CoreLocation.h> // 遵守协议 <CLLocationManagerDelegate> 创建开启定位请求 // 声明定位管理器 @property (nonatomic, strong) CLLocationManager *locationManager; // 实例化定位管理器 self.locationManager = [[CLLocationManager alloc] init]; // 设置代理 self.locationManager.delegate = self; // 判断系统定位服务是否开启 if (![CLLocationManager locationServicesEnabled]) { // 创建警告框(自定义方法) [self showAlertWithTitle:@"提示" message:@"系统定位服务不可用,请开启 !"]; } else { // 判断应用定位服务授权状态 if([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined){ // 没有授权 // 8.0 及以上系统需手动请求定位授权 if ([UIDevice currentDevice].systemVersion.doubleValue >= 8.0) { // 设置前台定位,需在 info.plist 里设置 Privacy - Location When In Use Usage Description 的值 [self.locationManager requestWhenInUseAuthorization]; // 设置前后台同时定位,需在 info.plist 里设置 Privacy - Location Always Usage Description 的值 // [self.locationManager requestAlwaysAuthorization]; } // 开始定位追踪(第一次打开软件时) [self.locationManager startUpdatingLocation]; } else if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedWhenInUse || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusAuthorizedAlways) { // 允许定位授权 // 开始定位追踪 [self.locationManager startUpdatingLocation]; } else{ // 拒绝定位授权 // 创建警告框(自定义方法) [self showAlertWithTitle:@"提示" message:@"当前应用的定位服务不可用,请检查定位服务授权状态 !"]; } } 获取定位结果 // 定位到位置 // CLLocationManagerDelegate 协议方法 - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{ CLLocation *location = [locations lastObject]; // 经纬度 CLLocationDegrees longitude = location.coordinate.longitude; // 经度 CLLocationDegrees latitude = location.coordinate.latitude; // 纬度 // 海拔 CLLocationDistance altitude = location.altitude; // 路线,航向(0.0 度~359.9 度,0.0 度代表真北方向) CLLocationDirection course = location.course; // 速度(m/s) float speed = location.speed; // 停止定位(如果不关闭,会一直处在定位请求中) [manager stopUpdatingLocation]; } 2、CoreLocation 定位设置 // 设置代理 self.locationManager.delegate = self; // 获取系统定位服务开启状态 BOOL isLocationServicesEnabled = [CLLocationManager locationServicesEnabled]; // 获取应用定位服务授权状态 CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus]; // 设置每隔多少米更新位置信息 self.locationManager.distanceFilter = kCLDistanceFilterNone; // 设置定位精确度 self.locationManager.desiredAccuracy = kCLLocationAccuracyBest; // 设置前台定位,需在 info.plist 里设置 Privacy - Location When In Use Usage Description 的值 [self.locationManager requestWhenInUseAuthorization]; // 设置前后台同时定位,需在 info.plist 里设置 Privacy - Location Always Usage Description 的值 [self.locationManager requestAlwaysAuthorization]; // 开始定位 [self.locationManager startUpdatingLocation]; // 停止定位(如果不关闭,会一直处在定位请求中) [self.locationManager stopUpdatingLocation]; // 获取定位到的 经纬度 CLLocationDegrees longitude = location.coordinate.longitude; // 经度 CLLocationDegrees latitude = location.coordinate.latitude; // 纬度 // 获取定位到的 海拔 CLLocationDistance altitude = location.altitude; // 获取定位到的 路线,航向(0.0 度~359.9 度,0.0 度代表真北方向) CLLocationDirection course = location.course; // 获取定位到的 速度(m/s) float speed = location.speed; // 计算两个位置之间的距离 CLLocation *location1 = [[CLLocation alloc] initWithLatitude:40 longitude:116]; CLLocation *location2 = [[CLLocation alloc] initWithLatitude:41 longitude:116]; CLLocationDistance distance = [location1 distanceFromLocation:location2];
1、支付宝支付申请 支付宝支付官方签约集成指引 支付宝APP支付官方集成指引 蚂蚁金服开放平台 1.1 支付宝 APP 支付申请步骤 APP 支付:APP 支付是商户通过在移动端应用 APP 中集成开放 SDK 调起支付宝支付模块完成支付的模式。买家在手机、掌上电脑等无线设备的应用程序内,可通过支付宝进行付款购买特定服务或商品,资金即时到账。旧的接口叫 移动支付。 申请条件: 1) 申请前必须拥有经过实名认证的支付宝账户; 2) 企业或个体工商户可申请; 3) 需提供真实有效的营业执照,且支付宝账户名称需与营业执照主体一致; 4) 如应用开发者与支付宝账户名称不一致需提供开发合作协议; 5) 如应用已上架,需提供应用名称和下载链接;若应用未上架,需提供 demo 或产品说明文档; 6) 古玩、珠宝等奢侈品、投资类行业无法申请本产品; 费率说明: 助力中小商户,从签约日至 2017.6.30 日优惠费率为 0.55%(不包含特殊行业) 特殊行业费率:1.2%,行业范围包括:手机、通讯设备销售;家用电器;数码产品及配件;休闲游戏;网络游戏点卡、游戏渠道代理;游戏系统商;网游周边服务、交易平台;网游运营商(含网页游戏) 1.1.1 创建应用并获取 APPID 要在您的应用中使用支付宝开放产品的接口能力,您需要先去蚂蚁金服开放平台,在管理中心中创建登记您的应用,并提交审核,审核通过后会为您生成应用唯一标识 APPID,并且可以申请开通开放产品使用权限,通过 APPID 您的应用才能调用开放产品的接口能力。需要详细了解开放平台创建应用步骤请参考《开放平台应用创建指南》。 1、开发者使用支付宝账号登录开放平台(需实名认证的支付宝账号),并创建应用。 创建应用时只需填写应用名称,此时的应用状态为开发中,无法在线上正式调用接口。 2、创建应用后,点击 “修改” 可跳转到完善应用信息页面。 应用信息在开发应用过程中可以无需审核随时完善。应用名称和应用图标会在应用申请上线时进行审核,所以在配置时,建议先了解相关审核规则。 需要完善的内容 作用 应用名称 应用名称和应用图标会在授权、分享的场景中露出,请准确填写相关信息 应用图标 应用说明文档 用于审核人员了解应用覆盖场景和应用实现的功能,请准确填写 3、配置应用环境,开发者所需配置内容请参考: 字段名称 字段描述 应用网关(对应下图1) 用于接收支付宝异步通知,例如口碑开店中,需要配置此网关来接收开发者门店被动通知。 授权回调地址(对应下图2) 第三方授权或用户信息授权后回调地址。授权链接中配置的redirect_uri的值必须与此值保持一致。(如:https://www.alipay.com) RSA(SHA1)密钥(对应下图3) 开发者要保证接口中使用的私钥与此处的公钥匹配,否则无法调用接口。可参考密钥的生成与配置。 1.1.2 配置密钥 开发者调用接口前需要先生成 RSA 密钥,RSA 密钥包含应用私钥 (APP_PRIVATE_KEY)、应用公钥 (APP_PUBLIC_KEY)。生成密钥后在开放平台管理中心进行密钥配置,配置完成后可以获取支付宝公钥 (ALIPAY_PUBLIC_KEY)。详细步骤请参考《配置应用环境》。 1、生成 RSA 密钥 生成方式一(推荐):使用支付宝提供的一键生成工具(内附使用说明) Windows:下载 MAC OSX:下载 解压打开文件夹,直接运行 “支付宝RAS密钥生成器SHAwithRSA1024_V1.0.bat”(WINDOWS)或 “SHAwithRSA1024_V1.0.command”(MACOSX),点击 “生成RSA密钥”,会自动生成公私钥,然后点击 “打开文件位置”,即可找到工具自动生成的密钥。 TIPS:工具不支持含中文或空格的路径,请下载到英文目录下使用。 生成方式二:也可以使用 OpenSSL 工具命令生成 首先进入 OpenSSL 工具,再输入以下命令。 OpenSSL> genrsa -out app_private_key.pem 1024 #生成私钥 OpenSSL> pkcs8 -topk8 -inform PEM -in app_private_key.pem -outform PEM -nocrypt -out app_private_key_pkcs8.pem #Java开发者需要将私钥转换成PKCS8格式 OpenSSL> rsa -in app_private_key.pem -pubout -out app_public_key.pem #生成公钥 OpenSSL> exit #退出OpenSSL程序 经过以上步骤,开发者可以在当前文件夹中(OpenSSL 运行文件夹),看到app_private_key.pem(开发者 RSA 私钥)、app_private_key_pkcs8.pem(pkcs8 格式开发者 RSA 私钥)和 app_public_key.pem(开发者 RSA 公钥)3 个文件。开发者将私钥保留,将公钥提交给支付宝配置到开发平台,用于验证签名。以下为私钥文件和公钥文件示例。 注意:对于使用 Java 的开发者,将 pkcs8 在 console 中输出的私钥去除头尾、换行和空格,作为开发者私钥,对于 .NET 和 PHP 的开发者来说,无需进行 pkcs8 命令行操作。 标准的私钥文件示例(PHP、.NET使用) -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQC+L0rfjLl3neHleNMOsYTW8r0QXZ5RVb2p/vvY3fJNNugvJ7lo4+fdBz+LN4mDxTz4MTOhi5e2yeAqx+v3nKpNmPzC5LmDjhHZURhwbqFtIpZD51mOfno2c3MDwlrsVi6mTypbNu4uaQzw/TOpwufSLWF7k6p2pLoVmmqJzQiD0QIDAQABAoGAakB1risquv9D4zX7hCv9MTFwGyKSfpJOYhkIjwKAik7wrNeeqFEbisqv35FpjGq3Q1oJpGkem4pxaLVEyZOHONefZ9MGVChT/MNH5b0FJYWl392RZy8KCdq376Vt4gKVlABvaV1DkapL+nLh7LMo/bENudARsxD55IGObMU19lkCQQDwHmzWPMHfc3kdY6AqiLrOss+MVIAhQqZOHhDe0aW2gZtwiWeYK1wB/fRxJ5esk1sScOWgzvCN/oGJLhU3kipHAkEAysNoSdG2oWADxlIt4W9kUiiiqNgimHGMHPwp4JMxupHMTm7D9XtGUIiDijZxunHv3kvktNfWj3Yji0661zHVJwJBAM8TDf077F4NsVc9AXVs8N0sq3xzqwQD/HPFzfq6hdR8tVY5yRMb4X7+SX4EDPORKKsgnYcur5lk8MUi7r072iUCQQC8xQvUne+fcdpRyrR4StJlQvucogwjTKMbYRBDygXkIlTJOIorgudFlrKP/HwJDoY4uQNl8gQJb/1LdrKwIe7FAkBl0TNtfodGrDXBHwBgtN/t3pyi+sz7OpJdUklKE7zMSBuLd1E3O4JMzvWP9wEE7JDb+brjgK4/cxxUHUTkk592 -----END RSA PRIVATE KEY----- PKCS8处理后的私钥文件示例(Java 使用) -----BEGIN PRIVATE KEY----- MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAN0yqPkLXlnhM+2H/57aHsYHaHXazr9pFQun907TMvmbR04wHChVsKVgGUF1hC0FN9hfeYT5v2SXg1WJSg2tSgk7F29SpsF0I36oSLCIszxdu7ClO7c22mxEVuCjmYpJdqb6XweAZzv4Is661jXP4PdrCTHRdVTU5zR9xUByiLSVAgMBAAECgYEAhznORRonHylm9oKaygEsqQGkYdBXbnsOS6busLi6xA+iovEUdbAVIrTCG9t854z2HAgaISoRUKyztJoOtJfI1wJaQU+XL+U3JIh4jmNx/k5UzJijfvfpT7Cv3ueMtqyAGBJrkLvXjiS7O5ylaCGuB0Qz711bWGkRrVoosPM3N6ECQQD8hVQUgnHEVHZYtvFqfcoq2g/onPbSqyjdrRu35a7PvgDAZx69Mr/XggGNTgT3jJn7+2XmiGkHM1fd1Ob/3uAdAkEA4D7aE3ZgXG/PQqlm3VbE/+4MvNl8xhjqOkByBOY2ZFfWKhlRziLEPSSAh16xEJ79WgY9iti+guLRAMravGrs2QJBAOmKWYeaWKNNxiIoF7/4VDgrcpkcSf3uRB44UjFSn8kLnWBUPo6WV+x1FQBdjqRviZ4NFGIP+KqrJnFHzNgJhVUCQFzCAukMDV4PLfeQJSmna8PFz2UKva8fvTutTryyEYu+PauaX5laDjyQbc4RIEMU0Q29CRX3BA8WDYg7YPGRdTkCQQCG+pjU2FB17ZLuKRlKEdtXNV6zQFTmFc1TKhlsDTtCkWs/xwkoCfZKstuV3Uc5J4BNJDkQOGm38pDRPcUDUh2/ -----END PRIVATE KEY----- 公钥文件示例 -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQWiDVZ7XYxa4CQsZoB3n7bfxLDkeGKjyQPt2FUtm4TWX9OYrd523iw6UUqnQ+Evfw88JgRnhyXadp+vnPKP7unormYQAfsM/CxzrfMoVdtwSiGtIJB4pfyRXjA+KL8nIa2hdQy5nLfgPVGZN4WidfUY/QpkddCVXnZ4bAUaQjXQIDAQAB -----END PUBLIC KEY----- 2、密钥配置 开发者登录开放平台后,找到并进入应用。点击 “RSA(SHA1)密钥” 处的 “设置应用公钥”(如已设置则显示 “查看应用公钥”,可修改),将公钥文件去除头尾、换行和空格,仅需填入字符串。 例如转换前公钥 pem 文件格式: -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQWiDVZ7XYxa4CQsZoB3n7bfxLDkeGKjyQPt2FUtm4TWX9OYrd523iw6UUqnQ+Evfw88JgRnhyXadp+vnPKP7unormYQAfsM/CxzrfMoVdtwSiGtIJB4pfyRXjA+KL8nIa2hdQy5nLfgPVGZN4WidfUY/QpkddCVXnZ4bAUaQjXQIDAQAB -----END PUBLIC KEY----- 转换后得到的字符串为: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQWiDVZ7XYxa4CQsZoB3n7bfxLDkeGKjyQPt2FUtm4TWX9OYrd523iw6UUqnQ+Evfw88JgRnhyXadp+vnPKP7unormYQAfsM/CxzrfMoVdtwSiGtIJB4pfyRXjA+KL8nIa2hdQy5nLfgPVGZN4WidfUY/QpkddCVXnZ4bAUaQjXQIDAQAB 3、获取支付宝公钥 应用上线后点击 “查看支付宝公钥”,即可获取支付宝公钥,用于支付宝返回数据的验签。 对于支付宝公钥,看到的是一个字符串,如下: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDI6d306Q8fIfCOaTXyiUeJHkrIvYISRcc73s3vF1ZT7XN8RNPwJxo8pWaJMmvyTn9N4HQ632qJBVHf8sxHi/fEsraprwCtzvzQETrNRwVxLO5jVmRGi60j8Ue1efIlzPXV9je9mkjzOmdssymZkh2QhUrCmZYI/FCEa3/cNMW0QIDAQAB 如果需要使用文件方式(如使用服务端 SDK 的 PHP/.NET 版本)读取支付宝公钥,需要在头尾加入标示后保存至文件,文件内容如下: -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDI6d306Q8fIfCOaTXyiUeJHkrIvYISRcc73s3vF1ZT7XN8RNPwJxo8pWaJMmvyTn9N4HQ632qJBVHf8sxHi/fEsraprwCtzvzQETrNRwVxLO5jVmRGi60j8Ue1efIlzPXV9je9mkjzOmdssymZkh2QhUrCmZYI/FCEa3/cNMW0QIDAQAB -----END PUBLIC KEY----- 1.1.3 支付宝 APP 支付功能申请签约 1、准备签约资料 请提前准备以下资料:1)营业执照 2)APP 说明文档 3)如开发者与签约者不一致,需提供开发合作协议。 2、提交签约信息 填写商户经营信息、APP 说明文档、商户联系人信息。 3、应用创建完成后点击查看我的应用详情,功能信息中进行 APP 支付功能的签约。 或者进入蚂蚁金服商家中心 选择 APP 支付功能进行签约。 APP 说明文档相关格式。 1.2 支付宝 APP 支付集成并配置 SDK 接入移动支付需要集成两个 SDK,客户端 SDK 需要集成在商户自己的 APP 中,用于唤起支付宝 APP 并发送交易数据,并在支付宝 APP 返回商户 APP 时获得支付结果。服务端 SDK 需要商户集成在自己的服务端系统中,用于协助解析并验证客户端同步返回的支付结果和异步通知。 如何集成客户端 SDK 点击查看 iOS 集成流程详解,Android 集成流程详解。 如何集成服务端 SDK 为了帮助开发者调用开放接口,我们提供了开放平台服务端 SDK,包含 JAVA、PHP 和 .NET 三语言版本,封装了签名&验签、HTTP 接口请求等基础功能。请先下载对应语言版本的 SDK 并引入您的开发工程。 1.3 支付宝 APP 支付上线应用 应用开发完成后,请开发者自行进行验收和安全性检查(安全性检查可参考《开放平台第三方应用安全开发指南》),验收检查完成后,可申请上线,上线成功后,状态变为已上线,这个状态下的应用能够调用生产环境的接口。 应用申请上线后,预计会有1个工作日的审核时间,请耐心等待。 步骤一:确认功能 步骤二:完善应用信息 步骤三:申请上线 应用上线后可新增功能、删除功能,操作后实时生效。删除功能时请谨慎操作,如果线上已经有用户使用此功能,删除功能后会导致无法使用。 1.4 支付宝 APP 支付系统交互流程 系统交互流程: 如图,以 Android 平台为例: 第4步:调用支付接口:此消息就是本接口所描述的支付宝客户端 SDK 提供的支付对象 PayTask,将商户签名后的订单信息传进 payv2 方法唤起支付宝收银台,交易数据格式具体参见请求参数说明。 第5步:支付请求:支付宝客户端 SDK 将会按照商户客户端提供的请求参数发送支付请求。 第8步:接口返回支付结果:商户客户端在第 4 步中调用的支付接口,会返回最终的支付结果(即同步通知),参见客户端同步返回。 第13步:用户在支付宝 APP 或 H5 收银台完成支付后,会根据商户在手机网站支付 API 中传入的前台回跳地址 return_url 自动跳转回商户页面,同时在 URL 请求中附带上支付结果参数。同时,支付宝还会根据原始支付 API 中传入的异步通知地址 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统,详情见支付结果异步通知。 除了正向支付流程外,支付宝也提供交易查询、关闭、退款、退款查询以及对账等配套 API。 特别注意: 构造交易数据并签名必须在商户服务端完成,商户的应用私钥绝对不能保存在商户 APP 客户端中,也不能从服务端下发。 同步返回的数据,只是一个简单的结果通知,商户确定该笔交易付款是否成功需要依赖服务端收到支付宝异步通知的结果进行判断。 商户系统接收到通知以后,必须通过验签(验证通知中的 sign 参数)来确保支付通知是由支付宝发送的。建议使用支付宝提供的 SDK 来完成,详细验签规则参考异步通知验签。 2、支付宝 APP 支付开发 说明: 商户服务端: 负责生成订单及签名,及接受支付异步通知。 APP 客户端: 负责使用服务端传来的订单信息调用支付宝支付接口,及根据 SDK 同步返回的支付结果展示结果页。 服务端接入: 私钥必须放在商户服务端,签名过程必须放在商户服务端。 2.1 支付宝 APP 支付集成设置 1、下载 iOS 端开发工具包 AlipaySDK,并添加到创建的工程中。AlipaySDK 中有 2 个文件,分别为: AlipaySDK.bundle AlipaySDK.framework 2、添加 SDK 的依赖库和框架。在 项目设置 => TARGETS => Build Phases => Link Binary With Libraries 中依次添加以下库或框架: SystemConfiguration.framework CoreTelephony.framework QuartzCore.framework CoreText.framework CoreGraphics.framework CoreMotion.framework CFNetwork.framework AlipaySDK.framework // 导入 SDK 时已自动添加 libz.dylib libc++.tbd 其中,需要注意的是: 如果是 Xcode 7.0 之后的版本,需要添加 libc++.tbd、libz.tbd 如果是 Xcode 7.0 之前的版本,需要添加 libc++.dylib、libz.dylib 3、在 项目设置 => TARGETS => Info => URL Types 中点击加号按钮添加,在 “URL Schemes” 中输入 “alisdkdemo”。 注意:“alisdkdemo” 即为调起支付宝开始支付时使用的参数 appScheme。这里的 URL Schemes 中输入的 alisdkdemo,为测试 demo,实际商户的 app 中要填写独立的 scheme,建议跟商户的 app 有一定的标示度,要做到和其他的商户 app 不重复,否则可能会导致支付宝返回的结果无法正确跳回商户 app。 NSString *appScheme = @"alisdkdemo"; [[AlipaySDK defaultService] payOrder:orderString fromScheme:appScheme callback:^(NSDictionary *resultDic) { NSLog(@"reslut = %@", resultDic); }]; 4、iOS 9 + 系统策略更新,限制了 http 协议的访问,受此影响,当你的应用在 iOS 9 + 中需要使用支付宝 SDK 的相关能力时,需要在 “Info.plist” 里增加如下代码: <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> 5、若将 openssl 文件夹随意拉进项目中,即使添加头文件链接,也可能会出现找不到头文件的问题,在 项目设置 => TARGETS => Build Settings => Search Paths => Header Search Paths 中添加 openssl 文件夹所在的路径即可解决。 2.2 支付宝 APP 支付集成 详细代码见 GitHub Objective-C AppDelegate.m // 支付宝支付回调,当用户通过其他应用启动本应用时,会回调这个方法 // NS_DEPRECATED_IOS(2_0, 9_0) - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { if ([url.host isEqualToString:@"safepay"]) { // 支付跳转支付宝钱包进行支付,处理支付结果 [[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) { NSLog(@"result = %@",resultDic); }]; return YES; } // NS_AVAILABLE_IOS(9_0) 9.0 以后使用新 API 接口 - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options { if ([url.host isEqualToString:@"safepay"]) { // 支付跳转支付宝钱包进行支付,处理支付结果 [[AlipaySDK defaultService] processOrderWithPaymentResult:url standbyCallback:^(NSDictionary *resultDic) { NSLog(@"result = %@",resultDic); }]; return YES; } ViewController.m NSMutableDictionary *params = [NSMutableDictionary dictionary]; // 在此设置商户服务端需要的参数 params[@"totalFee"] = @"10"; params[@"bodyID"] = @"1"; // 向商户支付宝支付服务器端请求组装和签名后的请求串 orderString AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager]; [sessionManager POST:@"test 商户支付宝支付后台接口" parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { // 解析商户支付宝支付服务器端返回的数据,获得组装和签名后的请求串 orderString NSLog(@"responseObject = %@", responseObject); NSString *orderString = responseObject[@"signedString"]; if (orderString != nil) { // 应用注册的 scheme,在 Info.plist 定义 URL types NSString *appScheme = @"alisdkdemo"; // 调用支付结果开始支付 [[AlipaySDK defaultService] payOrder:orderString fromScheme:appScheme callback:^(NSDictionary *resultDic) { NSLog(@"reslut = %@", resultDic); }]; } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"向商户支付宝支付服务器端请求信息失败:%@", error.localizedDescription); }];
1、微信支付申请 微信支付官方集成指引 微信支付官方集成指导视频 微信 APP 支付开发者文档 微信公众平台 微信开放平台 微信商户平台 1.1 微信 APP 支付申请步骤 APP 支付:APP 支付又称移动端支付,是商户通过在移动端应用 APP 中集成开放 SDK 调起微信支付模块完成支付的模式。 1.1.1 第 1 阶段 1、注册微信开放平台帐号,注册成为微信开放平台开发者。 2、认证开发者资质,开发者资质认证通过后才可申请微信支付,申请审核服务费:300 元/次(年)。 3、创建 APP 并提交审核,提交你的 APP 基本信息,通过开放平台应用审核,以获得 AppID。 微信认证审核时间在 7 个工作日左右。 1.1.2 第 2 阶段 1、提交资料申请微信支付,申请成功后可以在 APP 中调用微信支付来付款。 商户在微信公众平台(申请扫码支付、公众号支付)或开放平台(申请APP支付)按照相应提示,申请相应微信支付模式。微信支付工作人员审核资料无误后开通相应的微信支付权限。微信支付申请审核通过后,商户在申请资料填写的邮箱中收取到由微信支付小助手发送的邮件,此邮件包含开发时需要使用的支付账户信息。 邮件中的账户参数与接口API参数对应关系: 2、开户成功,登录商户平台进行验证,平台帐户密码请查看收到的开户邮件,验证款项(随机金额)请查收你的结算帐户。 约 1~5 个工作日。 1.1.3 第 3 阶段 1、在线签署协议,本协议(微信支付服务协议)为线上协议,签署后立即生效,然后可以进行资金结算。 2、启动设计和开发,支付接口已可以在开发环境下调用调试。 微信公众平台支付接口在线调试工具 微信支付 API 错误码在线查询工具 微信支付 logo 及 APP 支付界面规范下载 微信 APP 支付开发者文档 官方 SDK 下载 官方 APP 支付示例下载 成功接入微信支付。 1.2 微信 APP 支付相关说明 1.2.1 支付账户 商户在微信公众平台(申请扫码支付、公众号支付)或开放平台(申请APP支付)按照相应提示,申请相应微信支付模式。微信支付工作人员审核资料无误后开通相应的微信支付权限。微信支付申请审核通过后,商户在申请资料填写的邮箱中收取到由微信支付小助手发送的邮件,此邮件包含开发时需要使用的支付账户信息。 账户参数说明: 1.2.2 协议规则 商户接入微信支付,调用API必须遵循以下规则: 1.2.3 参数规定 参数规定: 1.2.4 安全规范 安全规范 签名算法 生成随机数算法 商户证书 商户回调API安全 1.2.5 业务流程 以下是交互时序图,统一下单API、支付结果通知API和查询订单API等都涉及签名过程,调用都必须在商户服务器端完成。 商户系统和微信支付系统主要交互说明: 步骤1:用户在商户 APP 中选择商品,提交订单,选择微信支付。 步骤2:商户后台收到用户支付单,调用微信支付统一下单接口。参见【统一下单 API】。 步骤3:统一下单接口返回正常的 prepay_id,再按签名规范重新生成签名后,将数据传输给 APP。参与签名的字段名为 appId,partnerId,prepayId,nonceStr,timeStamp,package。注意:package 的值格式为 Sign=WXPay。 步骤4:商户 APP 调起微信支付。api 参见本章节【app 端开发步骤说明】 步骤5:商户后台接收支付通知。api 参见【支付结果通知 API】 步骤6:商户后台查询支付结果。,api 参见【查询订单API】 1.2.6 API 列表 API 列表 1.2.7 验收流程 验收流程 2、微信 APP 支付开发 说明: 商户服务端: 负责生成订单及签名,及接受支付异步通知。 APP 客户端: 负责使用服务端传来的订单信息调用微信支付接口,及根据 SDK 同步返回的支付结果展示结果页。 服务端接入: API 密钥必须放在商户服务端,签名过程必须放在商户服务端。 2.1 微信 APP 支付集成设置 1、下载 iOS 端开发工具包 WeChatSDK,并添加到创建的工程中。WeChatSDK 中有 5 个文件,分别为: libWeChatSDK.a // 静态库文件 WechatAuthSDK.h // 微信登陆等接口 WXApi.h // 所有 Api 接口 WXApiObject.h // Api 对象,包含所有接口和对象数据定义 README.txt // 所有版本的使用说明 2、iOS 9 + 系统策略更新,限制了 http 协议的访问,此外应用需要在 “Info.plist” 中将要使用的 URL Schemes 列为白名单,才可正常检查其他应用是否安装。 受此影响,当你的应用在 iOS 9 + 中需要使用微信 SDK 的相关能力(分享、收藏、支付、登录等)时,需要在 “Info.plist” 里增加如下代码: <key>LSApplicationQueriesSchemes</key> <array> <string>weixin</string> </array> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> 商户在微信开放平台申请开发 APP 应用后,微信开放平台会生成 APP 的唯一标识 APPID。在 Xcode 中打开项目,设置项目属性中的 URL Schemes 为您的 APPID。在 项目设置 => TARGETS => Info => URL Types 中点击加号按钮添加。 3、添加 SDK 的依赖库和框架。在 项目设置 => TARGETS => Build Phases => Link Binary With Libraries 中依次添加 README.txt 说明文档中提及的以下库或框架: SystemConfiguration.framework CoreTelephony.framework Security.framework CFNetwork.framework libz.dylib libsqlite3.0.dylib libWeChatSDK.a // 导入 SDK 时已自动添加 另外还需要添加官方没有提到的下列依赖库: libc++.tbd 4、如果接入微信的 sdk,在 delegate 里加入这句注册代码 WXApi registerApp: 运行后程序就崩溃,原因是库里用了某个类的扩展,编译时这些扩展没有编译,所以崩溃的。只需要在 项目设置 => TARGETS => Build Settings => Linking => Other Linker Flags 中添加上 -ObjC 即可,注意 O 和 C 都为大写。 2.2 微信 APP 支付集成 详细代码见 GitHub Objective-C AppDelegate.m - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 向微信终端程序注册第三方应用 APPID: wxb4ba3c02aa476ea1 [WXApi registerApp:@"wxb4ba3c02aa476ea1" withDescription:@"QWeChatPayDemo 1.0"]; return YES; } // 微信支付回调,当用户通过其他应用启动本应用时,会回调这个方法 // NS_DEPRECATED_IOS(2_0, 9_0) - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { return [WXApi handleOpenURL:url delegate:self]; } // NS_DEPRECATED_IOS(2_0, 9_0) - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { return [WXApi handleOpenURL:url delegate:self]; } // NS_AVAILABLE_IOS(9_0) 9.0 以后使用新 API 接口 - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options { return [WXApi handleOpenURL:url delegate:self]; } - (void)onResp:(BaseResp *)resp { if([resp isKindOfClass:[PayResp class]]){ // 支付返回结果,实际支付结果需要去微信服务器端查询 NSString *strMsg; switch (resp.errCode) { case WXSuccess: strMsg = @"支付成功!"; NSLog(@"支付成功:retcode = %d", resp.errCode); break; default: strMsg = [NSString stringWithFormat:@"支付失败!retcode = %d, retstr = %@", resp.errCode, resp.errStr]; NSLog(@"支付失败:retcode = %d, retstr = %@", resp.errCode, resp.errStr); break; } // 显示提示信息 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"支付结果" message:strMsg preferredStyle:UIAlertControllerStyleAlert]; [self.window.rootViewController presentViewController:alert animated:YES completion:^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [alert dismissViewControllerAnimated:YES completion:nil]; }); }]; } } ViewController.m // 判断是否安装了微信 if (![WXApi isWXAppInstalled]) { NSLog(@"没有安装微信"); return; } // 判断是否支持微信支付 if (![WXApi isWXAppSupportApi]) { NSLog(@"不支持微信支付"); return; } NSMutableDictionary *params = [NSMutableDictionary dictionary]; // 在此设置商户服务端需要的参数 params[WXTOTALFEE] = @"1"; params[WXEQUIPMENTIP] = [self fetchIPAddress]; // 向商户微信支付服务器端请求微信预支付信息 AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager]; [sessionManager POST:QCUrlUserWeChatPay parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { // 解析商户微信支付服务器端返回的数据,获得预支付信息和签名等 NSLog(@"responseObject = %@",responseObject); if (responseObject != nil) { // 发起微信支付 PayReq *request = [[PayReq alloc] init]; // 设置参数 request.openID = [responseObject objectForKey:WXAPPID]; request.partnerId = [responseObject objectForKey:WXMCHID]; request.prepayId= [responseObject objectForKey:WXPREPAYID]; request.nonceStr= [responseObject objectForKey:WXNONCESTR]; request.timeStamp= [[responseObject objectForKey:@"timestamp"] intValue]; request.package = @"Sign=WXPay"; request.sign = [responseObject objectForKey:@"sign"]; NSLog(@"%@--%@--%@--%@--%@--%d--%@",request.openID,request.partnerId,request.prepayId, request.package,request.nonceStr,request.timeStamp,request.sign); // 调用微信发起支付 [WXApi sendReq:request]; } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSLog(@"向商户微信支付服务器端请求预支付信息失败:%@", error.localizedDescription); }];
1、CoreData 数据库 CoreData 是 iOS SDK 里的一个很强大的框架,允许程序员以面向对象的方式储存和管理数据。使用 CoreData 框架,程序员可以很轻松有效地通过面向对象的接口管理数据。CoreData 框架提供了 对象 - 关系映射 (ORM) 的功能,即能够将 OC 对象转化成数据,保存在 SQLite3 数据库文件中,也能够将保存在数据库中的数据还原成 OC 对象,在数据操作过程中,无需编写任何 SQL 语句。 模型文件及实体(Entity)。要使用 CodeData,首先需要定义模型文件,描述应用程序中的所有实体(Entities),所谓实体是跟数据库进行映射的对象。 NSManagedObject:对应数据库中的一条记录。 CoreData 主要对象关系示意图类似于数据库的句柄,handle,用来操纵数据库 持久化存储调度者,是数据库与对象之间的,在开发中。只会用到一次,如果不理解,直接粘代码。 CoreData 主要对象 NSManagedObjectContext:负责应用和数据库之间的交互 (CRUD)。 NSPersistentStoreCoordinator:添加持久化存储库(如 SQLite 数据库), 是物理数据存储的物理文件和程序之间的联系的桥梁 ,负责管理不同对象上下文。 NSManagedObjectModel:被管理的对象模型。 NSEntityDescription:实体描述。 2、CoreData 的使用 配置 1、在工程中新建 Data Model 数据模型文件。 2、在 Data Model 模型文件中添加 Entity 实体,修改实体名称,并在实体中添加模型属性。 3、在模型文件右侧属性列表的 Code Generation 中设置生成 NSManagedObject subclass 子类的使用语言。 4、在模型文件右侧属性列表的 Class => Codegen 中设置 Manual/None。如果不修改此项程序编译时会报 Linker command failed with exit code 1 (use -v to see invocation) 错误。 5、基于 Data Model 数据库文件中的 Entity 创建 NSManagedObject subclass 类。Xcode8 从系统菜单的 Editor 创建,创建后文件中多出来 4 个文件。 6、在需要使用 CoreData 的文件中。 // 引入头文件 #import "Student+CoreDataClass.h" // 宏定义实体(数据表)名称 #define ENTITY_STUDENT @"Student" 搭建 CoreData 环境 // 声明目标对象上下文,CoreData 操作数据的环境 @property (nonatomic, strong) NSManagedObjectContext *moc; // 托管对象模型文件路径 NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"TestModel" ofType:@"momd"]; NSURL *modelUrl = [NSURL fileURLWithPath:modelPath]; // 创建托管对象模型,CoreData 数据模型文件 NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelUrl]; // 创建持久化存储协调器,处理数据的读写 NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; // SQLite 数据库文件路径,CoreData 使用的数据库文件后缀一般写 sqlite NSString *sqlPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:@"student.sqlite"]; NSURL *sqlUrl = [NSURL fileURLWithPath:sqlPath]; NSLog(@"sqlPath:%@", sqlPath); // 将 CoreData 文件映射到数据库,并判断操作状态 NSError *error = nil; NSPersistentStore *store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sqlUrl options:nil error:&error]; if (!store) { NSLog(@"addError = %@", error); } // 创建操作数据的对象 self.moc = [[NSManagedObjectContext alloc] init]; // 关联持久化存储协调器 self.moc.persistentStoreCoordinator = coordinator; 插入数据 // 获取插入到实体的指针 Student *std = [NSEntityDescription insertNewObjectForEntityForName:ENTITY_STUDENT inManagedObjectContext:self.moc]; // 赋值 std.name = self.nameTF.text; std.age = self.ageTF.text.intValue; NSError *error = nil; // 同步到数据库并判断 if ([self.moc save:&error]) { [self.myDataArray addObject:std]; [self.myTableView reloadData]; } else { NSLog(@"insert error = %@", error); } 删除数据 // 获取要删除的数据 Student *std = self.myDataArray[self.selectedRow]; // 从实体中删除 [self.moc deleteObject:std]; NSError *error = nil; // 同步到数据库并判断 if ([self.moc save:&error]) { [self.myDataArray removeObjectAtIndex:self.selectedRow]; [self.myTableView reloadData]; } else { NSLog(@"delete error = %@", error); } 修改数据 // 获取要修改的数据 Student *dent = self.myDataArray[self.selectedRow]; // 赋值 dent.name = self.nameTF.text; dent.age = self.ageTF.text.intValue; NSError *error = nil; // 同步到数据库并判断 if ([self.moc save:&error]) { [self.myTableView reloadData]; } else { NSLog(@"update error = %@", error); } 查询数据 // 查询请求 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:ENTITY_STUDENT]; // 查询条件(正则表达式),name 以某些字符开头 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name like %@", [NSString stringWithFormat:@"%@*", self.nameTF.text]]; // 设置请求条件,如果不设查询条件会查询所有 request.predicate = predicate; NSError *error = nil; // 执行查询 self.myDataArray.array = [self.moc executeFetchRequest:request error:&error]; if (error) { NSLog(@"fech = %@", error); } else { [self.myTableView reloadData]; } // 查询所有 CoreData 数据 // 查询请求 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:ENTITY_STUDENT]; NSError *error = nil; // 查询 NSArray *tmpArray = [self.moc executeFetchRequest:request error:&error]; if (error) { NSLog(@"fech = %@", error); } else { // 将查询结果存储到数据源数组中 [self.myDataArray setArray:tmpArray]; [self.myTableView reloadData]; } 3、MagicalRecord 的使用 CoreData 是 iOS 开发中经常使用的数据持久化的技术。但其操作过程稍微繁琐,即使你只是实现简单的存取,不涉及请求优化,也要进行许多配置工作,代码量在动辄几十行,对新手来说也需要较大时间成本。 MagicalRecord 是 OC 的一个库,协助方便 CoreData 的工作。其吸收了 Ruby on Rails 的 Active Record 模式,目标是: 简化 Core Data 相关代码 允许清晰,简单,单行获取 当需要优化请求的时候,仍然允许修改 NSFetchRequest 如果你在使用 MagicalRecord 方法的时候不想带 MR_ 前缀,直接用 findAll 代替 MR_findAll,就在引入头文件 CoreData+MagicalRecord.h 之前增加 #define MR_SHORTHAND 即可。 配置 创建 Model。创建一个 Model.xcdatamodeld ,添加一个 Person Entity,添加 age firstname lastname 三个属性。最后使用 Editor => Create NSManagedObject Subclass ORM 生成 Person 类。 在使用的文件中引入头文件 // 添加宏定义 #define MR_SHORTHAND // 引入头文件 #import "MagicalRecord.h" 初始化 - (void)viewDidLoad { [super viewDidLoad]; // 初始化 [MagicalRecord setupCoreDataStackWithStoreNamed:@"Model.sqlite"]; } - (void)dealloc { // 清理 [MagicalRecord cleanUp]; } 增 Person *person = [Person MR_createEntity]; person.firstname = @"Frank"; person.lastname = @"Zhang"; person.age = 26; [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait]; 删 Person *person = ...; // 此处略,取出的数据模型 [person MR_deleteEntity]; [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait]; 改 Person *person = ...; // 此处略,取出的数据模型 person.lastname = object; [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait]; 查 // 查找数据库中的所有 Person。 NSArray *persons = [Person MR_findAll]; // 查找所有的 Person 并按照 first name 排序。 NSArray *personsSorted = [Person MR_findAllSortedBy:@"firstname" ascending:YES]; // 查找所有 age 属性为 25 的 Person 记录。 NSArray *personsAgeEuqals25 = [Person MR_findByAttribute:@"age" withValue:[NSNumber numberWithInt:25]]; // 查找数据库中的第一条记录 Person *person = [Person MR_findFirst];
1、SQLite 数据库 SQLite 是一种轻型的嵌入式数据库,安卓和 iOS 开发使用的都是 SQLite 数据库。它占用资源非常低,在嵌入式设备中,可能需要几百 K 的内存数据就够了。他的处理速度比 Mysql、PostgreSQL 这两款著名的数据库都要快。数据库的存储和 Excel 很像,以表(table)为单位。表由多个字段(列、属性、column)组成,表里面的每一行数据称为记录。数据库操作包含打开数据库、创建表,表的增、删、改、查。 SQL(Structured Query Language)结构化查询语言,SQL 是一种对数据库中的数据进行定义和操作的语言。使用 SQL 语言编写出来的句子/代码叫 SQL 语句,在程序运行过程中,想要操作(增删改查,CRUD)数据库中的数据,必须使用 SQL 语句。SQL 语句不区分大小写,每句语句都必须以分号结尾。 SQL 中常用的关键字有 select、insert、update、delete、from、create、where、desc、orderby、table,数据库中不可以使用关键字来命名表、字段。SQL 语句中用 ?来作为占位符,不管字段是何种类型。 SQLite 语句的种类: 数据定义语句(DDL:Data Definition Language):包括 create 和 drop 等操作,在数据库中创建新表或删除表(create table 或 drop table)。 数据操作语句(DML:Data Manipulation Language):包括 insert、update、delete 等操作,上面的三种操作分别用于添加、修改、删除表中的数据。 数据查询语句(DQL:Data Query Language):可以用于查询获得表中的数据,关键字 select 是 SQL(也是所有 SQL)用的最多的操作,其他 DQL 常用的关键字有 where、order by、group by 和 having。 注意:写入数据库,字符串可以采用 char 方式,而从数据库中取出 char 类型,当 char 类型有表示中文字符时,会出现乱码。这是因为数据库默认使用 ASCII 编码方式。所以要想正确从数据库中取出中文,需要用 NSString 来接收从数据库取出的字符串。 SQLite 操作方法 sqlite3 *db 数据库句柄,跟文件句柄很类似 sqlite3_stmt *stmt 这个相当于 ODBC 的 Command 对象,用于保存编译好的 SQL 语句 sqlite3_open() 打开数据库,没有数据库时创建。 sqlite3_exec() 执行非查询的 sql 语句 sqlite3_prepare_v2 执行查询的 sql 语句 Sqlite3_step() 在调用 sqlite3_prepare 后,使用这个函数在记录集中移动。 sqlite3_free() 清空变量 Sqlite3_close() 关闭数据库文件 还有一系列的函数,用于从记录集字段中获取数据,如: sqlite3_column_text() 取 text 类型的数据。 sqlite3_column_blob() 取 blob 类型的数据 sqlite3_column_int() 取 int 类型的数据 SQLite 命令行 .help :帮助 .quit :退出 .database:列出数据库信息 .dump :查看所有的 sql 语句 .schema :查看表结构 .tables :显示所有的表 SQL 语句常用数据类型 (1)整型: bigint :整形数据,大小为 8 个字节 integer :整形数据,大小为 4 个字节 smallint:整形数据,大小为 2 个字节 tinyint :从 0 到 255 的整形数据,存储大小为 1 字节 (2)浮点型: float :4 字节浮点数 double:8 字节浮点数 real :8 字节浮点数 (3)字符型: char(n) :n 长度的字串,n 不能超过 254 varchar(n):长度不固定且其长度为 n 的字串,n 不能超过 4000 text :text 存储可变长度的非 unicode 数据,存放比 varchar 更大的字符串 注意事项: 尽量用 varchar 超过 255 字节的只能用 varchar 或 text 能用 varchar 的地方不用 text SQLite 字符串区别: char 存储定长数据很方便,char 字段上的索引效率极高,比如定义 char(10),那么不论你存储的数据是否达到了 10 个字节,都要占去 10 个字节的空间,不足的自动用空格填充。 varchar 存储变长数据,但存储效率没有 char 高,如果一个字段可能的值是不固定长度的,我们只知道它不可能超过 10 个 > 字符,把它定义为 varchar(10) 是最合算的,varchar 类型的实际长度是它的值的实际长度 +1,为什么 +1 呢 ?这个字节用于保存实际使用了多大的长度。因此,从空间上考虑,用 varchar 合适,从效率上考虑,用 char 合适,关键是根据情况找到权衡点。 text 存储可变长度的非 Unicode 数据,最大长度为 2^31-1(2147483647)个字符。 (4)日期类型: date :包含了年份,月份,日期 time :包含了小时,分钟,秒 datetime :包含了年,月,日,时,分,秒 timestamp:包含了年,月,日,时,分,秒,千分之一秒 注意:datetime 包含日期时间格式,必须写成 ‘2010-08-05’不能写为‘2010-8-5’,否则在读取时会产生错误。 (5)其他类型: null :空值 blob :二进制对象,主要用来存放图片和声音文件等 default :缺省值 primary key :主键值 autoincrement:主键自动增长 (6)什么是主键: primary key,主键就是一个表中,有一个字段,里面的内容不可以重复,一般一个表都需要设置一个主键,autoincrement 让主键自动增长。 (7)注意事项: 所有字符串必须要加 ‘ ’ 单引号 整数和浮点数不用加 ‘ ’ 日期需要加单引号 ‘ ’ 字段顺序没有关系,关键是 key 与 value 要对应 对于自动增长的主键不需要插入字段 简单基本的 SQL 语句 (1) 数据记录筛选: sql="select * from 数据表 where 字段名=字段值 order by 字段名 [desc]" sql="select * from 数据表 where 字段名 like '%字段值%' order by 字段名 [desc]" sql="select top 10 * from 数据表 where 字段名=字段值 order by 字段名 [desc]" sql="select top 10 * from 数据表 order by 字段名 [desc]" sql="select * from 数据表 where 字段名 in ('值1','值2','值3')" sql="select * from 数据表 where 字段名 between 值1 and 值2" (2) 更新数据记录: sql="update 数据表 set 字段名=字段值 where 条件表达式" sql="update 数据表 set 字段1=值1,字段2=值2 …… 字段n=值n where 条件表达式" (3) 删除数据记录: sql="delete from 数据表 where 条件表达式" sql="delete from 数据表" (将数据表所有记录删除) (4) 添加数据记录: sql="insert into 数据表 (字段1,字段2,字段3 …) values (值1,值2,值3 …)" sql="insert into 目标数据表 select * from 源数据表" (把源数据表的记录添加到目标数据表) (5) 数据记录统计函数: AVG(字段名) 得出一个表格栏平均值 COUNT(*;字段名) 对数据行数的统计或对某一栏有值的数据行数统计 MAX(字段名) 取得一个表格栏最大的值 MIN(字段名) 取得一个表格栏最小的值 SUM(字段名) 把数据栏的值相加 引用以上函数的方法: sql="select sum(字段名) as 别名 from 数据表 where 条件表达式" //set rs=conn.excute(sql) 用 rs("别名") 获取统计的值,其它函数运用同上。 查询去除重复值:select distinct * from table1 (6) 数据表的建立和删除: CREATE TABLE 数据表名称(字段1 类型1(长度),字段2 类型2(长度) …… ) 2、iOS 自带 SQLite 的使用 1、环境配置 在 TARGETS => Build Phases => Link Binary With Libraries => 中添加:libsqlite3.0.tbd 或者在 TARGETS => Build Settings => Linking => Other Linker Flags 中添加 -l< 所需 dylib 的名称 >:-lsqlite3.0 2、使用 添加头文件: #import "sqlite3.h" 配置数据库路径 // 设置数据库文件路径 NSString *databaseFilePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:@"mydb.sqlite"]; // 创建数据库句柄 sqlite3 *db; // 错误记录 char *error; 3、打开数据库 // 数据库文件不存在时,自动创建文件 if (sqlite3_open([databaseFilePath UTF8String], &db) == SQLITE_OK) { NSLog(@"sqlite dadabase is opened."); } else { NSLog(@"sqlite dadabase open fail."); } 4、创建数据表 /* sql 语句,专门用来操作数据库的语句。 create table if not exists 是固定的,如果表不存在就创建。 myTable() 表示一个表,myTable 是表名,小括号里是字段信息。 字段之间用逗号隔开,每一个字段的第一个单词是字段名,第二个单词是数据类型,primary key 代表主键,autoincrement 是自增。 */ // create table if not exists 表名(主键名 主键类型 primary key autoincrement, 键名 键类型, 键名 键类型, ...) NSString *createSql = @"create table if not exists myTable(id integer primary key autoincrement, name text, age integer, address text)"; if (sqlite3_exec(db, [createSql UTF8String], NULL, NULL, &error) == SQLITE_OK) { NSLog(@"create table is ok."); } else { NSLog(@"error: %s", error); // 每次使用完毕清空 error 字符串,提供给下一次使用 sqlite3_free(error); } 5、插入记录 // insert into 表名(键名, 键名, ...) values('键值', '键值', '...') NSString *insertSql = @"insert into myTable(name, age, address) values('小新', '8', '东城区')"; if (sqlite3_exec(db, [insertSql UTF8String], NULL, NULL, &error) == SQLITE_OK) { NSLog(@"insert operation is ok."); } else { NSLog(@"error: %s", error); // 每次使用完毕清空 error 字符串,提供给下一次使用 sqlite3_free(error); } 6、删除记录 // delete from 表名 查询条件(where id = 2) NSString *deleteSql = @"delete from myTable where id = 2"; if (sqlite3_exec(db, [deleteSql UTF8String], NULL, NULL, &error) == SQLITE_OK) { NSLog(@"delete operation is ok."); } else { NSLog(@"error: %s", error); // 每次使用完毕清空 error 字符串,提供给下一次使用 sqlite3_free(error); } 7、修改记录 // update 表名 set 键名 = '键值', 键名 = '键值', ... 查询条件(where id = 3) NSString *updateSql = @"update myTable set name = '小白', age = '10', address = '西城区' where id = 3"; if (sqlite3_exec(db, [updateSql UTF8String], NULL, NULL, &error) == SQLITE_OK) { NSLog(@"update operation is ok."); } else { NSLog(@"error: %s", error); // 每次使用完毕清空 error 字符串,提供给下一次使用 sqlite3_free(error); } 8、查询记录 sqlite3_stmt *statement; // select 键名, 键名, ... from 表名,select * from 表名:查询所有 key 值内容 NSString *selectSql = @"select id, name, age, address from myTable"; if (sqlite3_prepare_v2(db, [selectSql UTF8String], -1, &statement, nil) == SQLITE_OK) { while(sqlite3_step(statement) == SQLITE_ROW) { // 查询 id 的值 int _id = sqlite3_column_int(statement, 0); // 查询 name 的值 NSString *name = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 1)]; // 查询 age int age = sqlite3_column_int(statement, 2); // 查询 address 的值 NSString *address = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 3)]; NSLog(@"id: %i, name: %@, age: %i, address: %@", _id, name, age, address); } } else { NSLog(@"select operation is fail."); } sqlite3_finalize(statement); 9、关闭数据库 if (sqlite3_close(db) == SQLITE_OK) { NSLog(@"sqlite dadabase is closed."); } else { NSLog(@"sqlite dadabase close fail."); } 3、fmdb 的使用 iOS 中原生的 SQLite API 在使用上相当不友好,在使用时,非常不便。于是,就出现了一系列将 SQLite API 进行封装的库,例如 fmdb、PlausibleDatabase、sqlitepersistentobjects 等,fmdb 是一款简洁、易用的封装库。 fmdb 同时兼容 ARC 和非 ARC 工程,会自动根据工程配置来调整相关的内存管理代码。 fmdb 常用类: FMDatabase : 一个单一的 SQLite 数据库,用于执行 SQL 语句。 FMResultSet :执行查询一个 FMDatabase 结果集,这个和 Android 的 Cursor 类似。 FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。 除了查询操作,fmdb 数据库操作都执行 executeUpdate 方法,这个方法返回 BOOL 型。后面跟的参数类型必须是对象类型,FMDataBase 对象会将传过来的参数,转化成与数据库字段相匹配的类型,再进行后续处理。 fmdb 数据库中查询操作使用 executeQuery,并涉及到 FMResultSet,FMResultSet 提供了多个方法来获取不同类型的数据。 1、环境配置 将 第三方框架 fmdb 添加到工程中,并在 TARGETS => Build Phases => Link Binary With Libraries => 中添加:libsqlite3.0.tbd 2、使用 添加头文件: #import "FMDB.h" 配置数据库路径 // 设置数据库文件路径 NSString *databaseFilePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:@"mydb.sqlite"]; // 声明数据库管理对象 @property (nonatomic, strong) FMDatabase *db; // 实例化数据库管理对象 /* 使用数据库的路径初始化 当数据库文件不存在时,fmdb 会自己创建一个。 如果你传入的参数是空串:@"" ,则fmdb会在临时文件目录下创建这个数据库,数据库断开连接时,数据库文件被删除。 如果你传入的参数是 NULL,则它会建立一个在内存中的数据库,数据库断开连接时,数据库文件被删除。 */ self.db = [[FMDatabase alloc] initWithPath:databaseFilePath]; 3、打开数据库 if ([self.db open]) { NSLog(@"sqlite dadabase is opened."); } else { NSLog(@"sqlite dadabase open fail."); } 4、创建数据表 /* sql 语句,专门用来操作数据库的语句 create table if not exists 是固定的,如果表不存在就创建 userInfo() 表示一个表,userInfo 是表名,小括号里是字段信息 字段之间用逗号隔开,每一个字段的第一个单词是字段名,第二个单词是数据类型,primary key 代表主键,autoincrement 是自增 */ // create table if not exists 表名(主键名 主键类型 primary key autoincrement, 键名 键类型, 键名 键类型, ...) NSString *createSql = @"create table if not exists myTable(id integer primary key autoincrement, name text, age integer, address text)"; if ([self.db executeUpdate:createSql]) { NSLog(@"create table is ok."); } else { NSLog(@"create error = %@", self.db.lastErrorMessage); } 5、插入记录 // insert into 表名(键名, 键名, ...) values('键值', '键值', '...') NSString *insertSql = @"insert into myTable(name, age, address) values('小新', '8', '东城区')"; if ([self.db executeUpdate:insertSql]) { NSLog(@"insert operation is ok."); } else { NSLog(@"insert error = %@", self.db.lastErrorMessage); } 6、删除记录 // delete from 表名 查询条件(where id = 2) NSString *deleteSql = @"delete from myTable where id = 2"; if ([self.db executeUpdate:deleteSql]) { NSLog(@"delete operation is ok."); } else { NSLog(@"delete error = %@", self.db.lastErrorMessage); } 7、修改记录 // update 表名 set 键名 = '键值', 键名 = '键值', ... 查询条件(where id = 3) NSString *updateSql = @"update myTable set name = '小白', age = '10', address = '西城区' where id = 3"; if ([self.db executeUpdate:updateSql]) { NSLog(@"update operation is ok."); } else { NSLog(@"update error = %@", self.db.lastErrorMessage); } 8、查询记录 // select 键名, 键名, ... from 表名,select * from 表名:查询所有 key 值内容 NSString *selectSql = @"select id, name, age, address from myTable"; // FMResultSet 查询结果,执行查询 SQL 语句 FMResultSet *set = [self.db executeQuery:selectSql]; // 类似于数组的快速遍历,set 会依次代表所有查询出来的数据,每次取出一整条数据,根据字段名称,取出字段的值,将查询到的数据转成模型 while ([set next]) { // 查询 id 的值 int _id = [set intForColumn:@"id"]; // 查询 name 的值 NSString *name = [set stringForColumn:@"name"]; // 查询 age int age = [set intForColumn:@"age"]; // 查询 address 的值 NSString *address = [set stringForColumn:@"address"]; NSLog(@"id: %i, name: %@, age: %i, address: %@", _id, name, age, address); } 9、关闭数据库 if ([self.db close]) { NSLog(@"sqlite dadabase is closed."); } else { NSLog(@"sqlite dadabase close fail."); } 4、fmdb 多线程操作 如果应用中使用了多线程操作数据库,那么就需要使用 FMDatabaseQueue 来保证线程安全了。 应用中不可在多个线程中共同使用一个 FMDatabase 对象操作数据库,这样会引起数据库数据混乱。为了多线程操作数据库安全,fmdb 使用了 FMDatabaseQueue,使用 FMDatabaseQueue 很简单,首先用一个数据库文件地址来初使化 FMDatabaseQueue,然后就可以将一个闭包(block)传入 inDatabase 方法中。在闭包中操作数据库,而不直接参与 FMDatabase 的管理。 // 设置数据库文件路径 NSString *databaseFilePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:@"mydb.sqlite"]; // 打开数据库 FMDatabaseQueue *dbQueue = [FMDatabaseQueue databaseQueueWithPath:databaseFilePath]; // 队列 1 dispatch_async(dispatch_queue_create("queue1", NULL), ^{ for (int i = 0; i < 50; ++i) { // 对数据库进行操作,操作完成后会自动关闭数据库 [dbQueue inDatabase:^(FMDatabase *db) { NSString *insertSql= [NSString stringWithFormat: @"insert into myTable(name, age, address) values('%@', '%d', '%@')", @"小新", i, @"东城区"]; if ([db executeUpdate:insertSql]) { NSLog(@"%@ insert queue1 %d operation is ok.", [NSThread currentThread], i); } else { NSLog(@"insert error = %@", db.lastErrorMessage); } }]; } }); // 队列 2 dispatch_async(dispatch_queue_create("queue2", NULL), ^{ for (int i = 0; i < 50; ++i) { // 对数据库进行操作,操作完成后会自动关闭数据库 [dbQueue inDatabase:^(FMDatabase *db) { NSString *insertSql= [NSString stringWithFormat: @"insert into myTable(name, age, address) values('%@', '%d', '%@')", @"小白", i, @"西城区"]; if ([db executeUpdate:insertSql]) { NSLog(@"%@ insert queue2 %d operation is ok.", [NSThread currentThread], i); } else { NSLog(@"insert error = %@", db.lastErrorMessage); } }]; } }); 5、其他 SQLite 的第三方封装库 1、PlausibleDatabase 也是一个数据库操作的 objective-c 版封装库,“SQLite is the initial and primary target, but the API has been designed to support more traditional databases.” 文件较多,一般的接口与 FMDataBase 一样,此外还支持 SQL 的预编译和参数绑定。 2、sqlitepersistentobjects 这个开源库的目标是以面对对象的方式的存储和加载数据,让对象本身就有 save 和 load 的功能,屏蔽数据库的相关操作(创建、更新等),让使用者在不写 SQL 语句的状况下都可以使用 SQLite。应该说是符合 ActiveRecored 标准的。 自从 iOS3.0 支持 Core Data 之后,sqlitepersistentobjects 就停止了更新。不过单从操作数据来说,这个库还是很优秀的,如果你的数据存储工作很简单(light),使用 Core Data 比较显得凝重的话,sqlitepersistentobjects 也许是个很好的选择。
1、Base64 编码 简介: Base64 是一种基于64个可打印字符来表示二进制数据的表示方法,可打印字符包括字母 A-Z、a-z、0-9,共 62 个字符,另外两个符号在不同的系统不同 +,/。 Base 64 编码后的结果能够反算,不够安全。 Base 64 是所有现代加密算法的基础算法。 由于现代密码学是基于二进制数据进行加密的,因此经常会使用 Base64 对加密结果进行编码,以便于在网络上传输。 原理: 原本 8 bit 一组,改为 6 bit 一组,不足的补零,每两个 0 用一个 = 表示。 缺点: Base 64 编码后的结果能够反算,非常不安全。 用 base64 编码之后,结果会变大,增加了约 1/3。 用 base64 编码的结果有非常明显的特点,末尾有 = 。 终端使用: $ base64 123.png -o 123.txt 编码,将文件 123.png 编码为 123.txt $ base64 123.txt -o 321.png -D 解码,将文件 123.txt 解码为 321.png $ echo -n "A" | base64 编码,将字符串 A 编码 $ echo -n "QQ==" | base64 -D 解码,将字符串 QQ== 解码 2、对称算法 对称算法有时又叫传统密码算法,加密和解密使用相同密钥的算法,又称私钥加密、或者共享密钥。 算法公开、计算量小、加密速度快、加密效率高,可以对大数据进行加密。 双方使用相同钥匙,安全性得不到保证。秘钥的安全性非常重要,普遍采用的方法是使用 RSA 的加密算法加密给对称加密算法的秘钥进行加密。 对称加密的速度比公钥加密快很多,在很多场合都需要对称加密。 加密方法: DES :数据加密标准。 是一种分组数据加密技术,先将数据分成固定长度的小数据块,之后进行加密。 速度较快,适用于大量数据加密。 3DES:使用三组密钥做三次加密。 是一种基于 DES 的加密算法,使用 3 个不同密钥对同一个分组数据块进行 3 次加密,如此以使得密文强度更高。 AES :高级加密标准。 是美国联邦政府采用的一种区块加密标准。 相较于 DES 和 3DES 算法而言,AES 算法有着更高的速度和资源使用效率,安全级别也较之更高了,被称为下一代加密标准。 加密技术: ECB :电子代码本,就是说每个块都是独立加密的。 CBC :密码块链,使用一个密钥和一个初始化向量(IV)对数据执行加密转换。 CBC 加密可以有效地保证密文的完整性,也就是说如果有一个块在传送时丢失了(或被敌人改变了),就会导致后面所有的块无法正常解密这个特性可以用来防范一些窃听技巧。 3、非对称算法 非对称算法是指加密和解密使用不同密钥的算法,又称公钥加密。 非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey),公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。 非对称密码体制的特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快,适合对小数据加密。 对称密码体制中只有一种密钥,并且是非公开的,如果要解密就得让对方知道密钥。所以保证其安全性就是保证密钥的安全,而非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。 加密方法: RSA : 由于 RSA 算法的加密解密速度要比对称算法的速度慢很多,在实际应用中,通常采取数据本身的加密解密使用对称加密算法(AES/DES3),用 RSA 算法加密并传输对称算法所需的秘钥。 RSA 算法还在身份认证(或称鉴权)以及数字签名方面得到广泛的使用。 4、散列算法 散列算法又称散列函数,哈希(HASH)函数,该函数将数据打乱混合,重新创建一个叫做散列值的指纹。 任意二进制数据进行 "散列",即对不同长度的输入消息,产生固定长度的输出。这个固定长度的输出称为原输入消息的 “散列” 或 “消息摘要”。 对任意一个二进制数据进行加密,可以得到定长的字符串结果。相同的字符串,使用相同的算法,每次加密的结果是固定的。 散列不能逆运算,常用在用户密码上,服务器不需要知道用户的准确密码。 加密方法: MD5 :加密结果只有 32 个字符,因为数据长度不够,现在国外基本上已经不怎么用了,国内用的很普遍。 MD5 不能反算,但 MD5 已经被破解了,用碰撞算法,可以将两个不同的文件生成出相同的 MD5 结果。 MD5 在线加密解密网站 http://www.cmd5.com ,该网站破解原理:大量的常见数据被生成 md5 码,用户提交的数据与数据库中的 md5 数据进行比对查找。 终端命令: $ echo -n hello | openssl md5 $ echo -n hello | openssl sha1 $ echo -n hello | openssl sha -sha256 $ echo -n hello | openssl sha -sha512 MD5 常用加密方式: 直接使用 MD5 加密: 在 http://cmd5.com 上很容易被破解。 MD5 + 盐 加密: 早期方案。关于盐,随机添加的字符串,要够长,够复杂。 在 http://cmd5.com 不易被破解。 MD5 + HMAC 加密: HMAC 是一个结合了散列函数的加密算法。给定一个 "密钥",分别作两次加密和散列,密钥强度要求不那么高。国外用的比较多,国内还可以。 在 http://cmd5.com 无法破解。 MD5 + HMAC + 时间戳 加密: 相同的加密算法+相同的密码明文,每分钟的结果是不一样的。只有每次都不一样,黑客才不好猜。加时间戳,需要客户端和服务器端采用相同的加密算法。 在 http://cmd5.com 无法破解。 用户注册时发送 pwd.hmac 密码给服务器记录。关于用户第一次的密码安全,被黑客拦截到的几率非常非常低,增加附加安全手段,如 IP 辅助,手机绑定等。 用户登录时发送经过时间戳加密的密码给服务器端,服务器端根据用户名从数据库中取出记录的 pwd.hmac 密码,加上当前时间(时分)计算一次时间戳密码,加上当前时间(时(分-1))计算一次时间戳密码,分别与用户发过来的密码进行比对,只要有一个相同,便认为用户是合法的。 SHA1 :理论上已经被破解 SHA256:美国国家安全局、苹果等在使用的 SHA512: 5、OpenSSL OpenSSL 是一个安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及SSL协议,并提供丰富的应用程序供测试或其它目的使用。 Mac 系统自带 OpenSSL 环境。 6、钥匙串 钥匙串(英文:Keychain)是苹果公司 Mac OS 中的密码管理系统。它在 Mac OS 8.6 中被导入,并且包括在了所有后续的 Mac OS 版本中,包括 Mac OS X。一个钥匙串可以包含多种类型的数据:密码(包括网站,FTP 服务器,SSH 帐户,网络共享,无线网络,群组软件,加密磁盘镜像等),私钥,电子证书和加密笔记等。 苹果的 "生态圈",钥匙串功能可以协助记忆繁琐的个人账户信息。 iCloud 钥匙串使用 AES 256 加密算法,能够保证用户密码的安全。使用的时候,直接传递密码明文即可。 钥匙串访问 SDK,是苹果在 iOS 7.0.3 版本以后公布的。 钥匙串访问的密码保存在哪里?只有苹果知道,是为了进一步保障用户的密码安全。 钥匙串访问的接口是纯 C 语言的,钥匙串访问的第三方框架 SSKeyChain,是对 C 框架的封装,使用相当简单。 7、iOS 上 Base64 编解码 具体实现代码见 GitHub 源码 QExtension NSString+Base64.h @interface NSString (Base64) /** * 从 iOS 7.0 开始,apple 提供了 base64 的编码解码的支持。 */ /** * 对 ASCII 编码的字符串进行 base64 编码 * * 终端测试命令: * @code * echo -n "string" | base64 * base64 fileName1 -o fileName2 * @endcode * * @return base64 编码的字符串 */ - (NSString *)q_base64Encode NS_AVAILABLE(10_9, 7_0); /** * 对 base64 编码的字符串进行解码 * * 终端测试命令: * @code * echo -n "string" | base64 -D * base64 fileName2 -o fileName1 -D * @endcode * * @return ASCII 编码的字符串 */ - (NSString *)q_base64Decode NS_AVAILABLE(10_9, 7_0); /** * 生成服务器 base64 编码授权字符串 * * 示例代码格式: * @code * 输入字符串为 @"username:password" 格式。 * [request setValue:[@"username:password" q_basic64AuthEncode] * forHTTPHeaderField:@"Authorization"]; * @endcode * * @return @"BASIC (username:password).base64" 格式的字符串 */ - (NSString *)q_basic64AuthEncode NS_AVAILABLE(10_9, 7_0); @end NSString+Base64.m @implementation NSString (Base64) /// 对 ASCII 编码的字符串进行 base64 编码 - (NSString *)q_base64Encode { NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding]; return [data base64EncodedStringWithOptions:0]; } /// 对 base64 编码的字符串进行解码 - (NSString *)q_base64Decode { NSData *data = [[NSData alloc] initWithBase64EncodedString:self options:0]; return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } /// 生成服务器基本授权字符串 - (NSString *)q_basic64AuthEncode { return [@"BASIC " stringByAppendingString:[self q_base64Encode]]; } @end ViewController.m NSString *str = @"hello world"; // Base64 编码 NSString *base64Str = [str q_base64Encode]; NSLog(@"base64Str: %@", base64Str); // Base64 解码 NSString *asciiStr = [base64Str q_base64Decode]; NSLog(@"asciiStr: %@", asciiStr); // 服务器基本授权字符串编码 NSString *authStr = [str q_basic64AuthEncode]; NSLog(@"authStr: %@", authStr); 8、iOS 上 对称加密算法 。。。 9、iOS 上 非对称加密算法 具体实现代码见 GitHub 源码 QExtension 在 iOS 中使用 RSA 加密解密,需要用到 .der 和 .p12 后缀格式的文件,其中 .der 格式的文件存放的是公钥(Public key)用于加密,.p12 格式的文件存放的是私钥(Private key)用于解密. 首先需要先生成这些文件,然后再将文件导入工程使用。 首先按照本文章中的《11.2 生成 x509 格式的证书请求文件》说明生成一组公钥私钥证书文件。 接下来是创建加密解密用的类,並且把刚刚生成的证书文件添加到工程中。 QRSAEncryptor.h @interface QRSAEncryptor : NSObject /** * 使用 .der 公钥证书文件 加密字符串 * * @param string 需要加密的字符串 * @param path .der 格式的公钥文件路径 * * @return 经过 RSA 证书加密的字符串 */ + (NSString *)q_encryptWithString:(NSString *)string publicKeyFilePath:(NSString *)path; /** * 使用 .p12 私钥证书文件 解密字符串 * * @param string 需要解密的字符串 * @param path .p12 格式的私钥文件路径 * @param password 私钥文件密码 * * @return 经过 RSA 证书解密的字符串 */ + (NSString *)q_decryptWithString:(NSString *)string privateKeyFilePath:(NSString *)path password:(NSString *)password; /** * 使用 公钥字符串 加密字符串 * * <p> Xcode8+ 需要在 TARGET -> Capabitilies 中开启 Keychain Sharing 开关 <p> * * @param string 需要加密的字符串 * @param pubKey 公钥字符串,PKCS#8 格式 * * @return 经过公钥字符串加密的字符串 */ + (NSString *)q_encryptWithString:(NSString *)string publicKey:(NSString *)pubKey; /** * 使用 私钥字符串 解密字符串 * * <p> Xcode8+ 需要在 TARGET -> Capabitilies 中开启 Keychain Sharing 开关 <p> * * @param string 需要解密的字符串 * @param privateKey 私钥字符串,PKCS#8 格式 * * @return 经过私钥字符串解密的字符串 */ + (NSString *)q_decryptWithString:(NSString *)string privateKey:(NSString *)privateKey; @end QRSAEncryptor.m #import <Security/Security.h> @implementation QRSAEncryptor #pragma mark - 使用 .der 公钥证书文件 加密 /// 使用 .der 公钥证书文件 加密 + (NSString *)q_encryptWithString:(NSString *)string publicKeyFilePath:(NSString *)path { if (!string || !path) { return nil; } return [self encryptString:string publicKeyRef:[self getPublicKeyRefWithContentsOfFile:path]]; } /// 获取公钥 + (SecKeyRef)getPublicKeyRefWithContentsOfFile:(NSString *)filePath { NSData *certData = [NSData dataWithContentsOfFile:filePath]; if (!certData) { return nil; } SecCertificateRef cert = SecCertificateCreateWithData(NULL, (CFDataRef)certData); SecKeyRef key = NULL; SecTrustRef trust = NULL; SecPolicyRef policy = NULL; if (cert != NULL) { policy = SecPolicyCreateBasicX509(); if (policy) { if (SecTrustCreateWithCertificates((CFTypeRef)cert, policy, &trust) == noErr) { SecTrustResultType result; if (SecTrustEvaluate(trust, &result) == noErr) { key = SecTrustCopyPublicKey(trust); } } } } if (policy) CFRelease(policy); if (trust) CFRelease(trust); if (cert) CFRelease(cert); return key; } /// 使用公钥加密字符串 + (NSString *)encryptString:(NSString *)str publicKeyRef:(SecKeyRef)publicKeyRef { if (![str dataUsingEncoding:NSUTF8StringEncoding]) { return nil; } if (!publicKeyRef) { return nil; } NSData *data = [self encryptData:[str dataUsingEncoding:NSUTF8StringEncoding] withKeyRef:publicKeyRef]; NSString *ret = base64_encode_data(data); return ret; } #pragma mark - 使用 .p12 私钥证书文件 解密 /// 使用 .p12 私钥证书文件 解密 + (NSString *)q_decryptWithString:(NSString *)string privateKeyFilePath:(NSString *)path password:(NSString *)password { if (!string || !path) { return nil; } if (!password) { password = @""; } return [self decryptString:string privateKeyRef:[self getPrivateKeyRefWithContentsOfFile:path password:password]]; } /// 获取私钥 + (SecKeyRef)getPrivateKeyRefWithContentsOfFile:(NSString *)filePath password:(NSString*)password { NSData *p12Data = [NSData dataWithContentsOfFile:filePath]; if (!p12Data) { return nil; } SecKeyRef privateKeyRef = NULL; NSMutableDictionary * options = [[NSMutableDictionary alloc] init]; [options setObject: password forKey:(__bridge id)kSecImportExportPassphrase]; CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL); OSStatus securityError = SecPKCS12Import((__bridge CFDataRef) p12Data, (__bridge CFDictionaryRef)options, &items); if (securityError == noErr && CFArrayGetCount(items) > 0) { CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0); SecIdentityRef identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity); securityError = SecIdentityCopyPrivateKey(identityApp, &privateKeyRef); if (securityError != noErr) { privateKeyRef = NULL; } } CFRelease(items); return privateKeyRef; } /// 使用私钥解密字符串 + (NSString *)decryptString:(NSString *)str privateKeyRef:(SecKeyRef)privKeyRef { NSData *data = [[NSData alloc] initWithBase64EncodedString:str options:NSDataBase64DecodingIgnoreUnknownCharacters]; if (!privKeyRef) { return nil; } data = [self decryptData:data withKeyRef:privKeyRef]; NSString *ret = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; return ret; } #pragma mark - 使用 公钥字符串 加密 /// 使用 公钥字符串 加密 + (NSString *)q_encryptWithString:(NSString *)string publicKey:(NSString *)pubKey { NSData *data = [self encryptData:[string dataUsingEncoding:NSUTF8StringEncoding] publicKey:pubKey]; NSString *ret = base64_encode_data(data); return ret; } /// 使用公钥字符串加密数据 + (NSData *)encryptData:(NSData *)data publicKey:(NSString *)pubKey { if (!data || !pubKey) { return nil; } SecKeyRef keyRef = [self addPublicKey:pubKey]; if (!keyRef) { return nil; } return [self encryptData:data withKeyRef:keyRef]; } /// 添加公钥 + (SecKeyRef)addPublicKey:(NSString *)key { NSRange spos = [key rangeOfString:@"-----BEGIN PUBLIC KEY-----"]; NSRange epos = [key rangeOfString:@"-----END PUBLIC KEY-----"]; if (spos.location != NSNotFound && epos.location != NSNotFound) { NSUInteger s = spos.location + spos.length; NSUInteger e = epos.location; NSRange range = NSMakeRange(s, e-s); key = [key substringWithRange:range]; } key = [key stringByReplacingOccurrencesOfString:@"\r" withString:@""]; key = [key stringByReplacingOccurrencesOfString:@"\n" withString:@""]; key = [key stringByReplacingOccurrencesOfString:@"\t" withString:@""]; key = [key stringByReplacingOccurrencesOfString:@" " withString:@""]; // This will be base64 encoded, decode it. NSData *data = base64_decode(key); data = [self stripPublicKeyHeader:data]; if (!data) { return nil; } //a tag to read/write keychain storage NSString *tag = @"RSAUtil_PubKey"; NSData *d_tag = [NSData dataWithBytes:[tag UTF8String] length:[tag length]]; // Delete any old lingering key with the same tag NSMutableDictionary *publicKey = [[NSMutableDictionary alloc] init]; [publicKey setObject:(__bridge id) kSecClassKey forKey:(__bridge id)kSecClass]; [publicKey setObject:(__bridge id) kSecAttrKeyTypeRSA forKey:(__bridge id)kSecAttrKeyType]; [publicKey setObject:d_tag forKey:(__bridge id)kSecAttrApplicationTag]; SecItemDelete((__bridge CFDictionaryRef)publicKey); // Add persistent version of the key to system keychain [publicKey setObject:data forKey:(__bridge id)kSecValueData]; [publicKey setObject:(__bridge id) kSecAttrKeyClassPublic forKey:(__bridge id) kSecAttrKeyClass]; [publicKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id) kSecReturnPersistentRef]; CFTypeRef persistKey = nil; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)publicKey, &persistKey); if (persistKey != nil) { CFRelease(persistKey); } if ((status != noErr) && (status != errSecDuplicateItem)) { return nil; } [publicKey removeObjectForKey:(__bridge id)kSecValueData]; [publicKey removeObjectForKey:(__bridge id)kSecReturnPersistentRef]; [publicKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecReturnRef]; [publicKey setObject:(__bridge id) kSecAttrKeyTypeRSA forKey:(__bridge id)kSecAttrKeyType]; // Now fetch the SecKeyRef version of the key SecKeyRef keyRef = nil; status = SecItemCopyMatching((__bridge CFDictionaryRef)publicKey, (CFTypeRef *)&keyRef); if (status != noErr) { return nil; } return keyRef; } /// 去掉公钥头 + (NSData *)stripPublicKeyHeader:(NSData *)d_key { // Skip ASN.1 public key header if (d_key == nil) return(nil); unsigned long len = [d_key length]; if (!len) return(nil); unsigned char *c_key = (unsigned char *)[d_key bytes]; unsigned int idx = 0; if (c_key[idx++] != 0x30) return(nil); if (c_key[idx] > 0x80) idx += c_key[idx] - 0x80 + 1; else idx++; // PKCS #1 rsaEncryption szOID_RSA_RSA static unsigned char seqiod[] = { 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00 }; if (memcmp(&c_key[idx], seqiod, 15)) return(nil); idx += 15; if (c_key[idx++] != 0x03) return(nil); if (c_key[idx] > 0x80) idx += c_key[idx] - 0x80 + 1; else idx++; if (c_key[idx++] != '\0') return(nil); // Now make a new NSData from this buffer return ([NSData dataWithBytes:&c_key[idx] length:len - idx]); } #pragma mark - 使用 私钥字符串 解密 /// 使用 私钥字符串 解密 + (NSString *)q_decryptWithString:(NSString *)string privateKey:(NSString *)privateKey { if (!string) { return nil; } NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:NSDataBase64DecodingIgnoreUnknownCharacters]; data = [self decryptData:data privateKey:privateKey]; NSString *ret = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; return ret; } /// 使用私钥字符串解密数据 + (NSData *)decryptData:(NSData *)data privateKey:(NSString *)privKey { if (!data || !privKey) { return nil; } SecKeyRef keyRef = [self addPrivateKey:privKey]; if (!keyRef) { return nil; } return [self decryptData:data withKeyRef:keyRef]; } /// 添加私钥 + (SecKeyRef)addPrivateKey:(NSString *)key { NSRange spos = [key rangeOfString:@"-----BEGIN RSA PRIVATE KEY-----"]; NSRange epos = [key rangeOfString:@"-----END RSA PRIVATE KEY-----"]; if (spos.location != NSNotFound && epos.location != NSNotFound) { NSUInteger s = spos.location + spos.length; NSUInteger e = epos.location; NSRange range = NSMakeRange(s, e-s); key = [key substringWithRange:range]; } key = [key stringByReplacingOccurrencesOfString:@"\r" withString:@""]; key = [key stringByReplacingOccurrencesOfString:@"\n" withString:@""]; key = [key stringByReplacingOccurrencesOfString:@"\t" withString:@""]; key = [key stringByReplacingOccurrencesOfString:@" " withString:@""]; // This will be base64 encoded, decode it. NSData *data = base64_decode(key); data = [self stripPrivateKeyHeader:data]; if (!data) { return nil; } //a tag to read/write keychain storage NSString *tag = @"RSAUtil_PrivKey"; NSData *d_tag = [NSData dataWithBytes:[tag UTF8String] length:[tag length]]; // Delete any old lingering key with the same tag NSMutableDictionary *privateKey = [[NSMutableDictionary alloc] init]; [privateKey setObject:(__bridge id) kSecClassKey forKey:(__bridge id)kSecClass]; [privateKey setObject:(__bridge id) kSecAttrKeyTypeRSA forKey:(__bridge id)kSecAttrKeyType]; [privateKey setObject:d_tag forKey:(__bridge id)kSecAttrApplicationTag]; SecItemDelete((__bridge CFDictionaryRef)privateKey); // Add persistent version of the key to system keychain [privateKey setObject:data forKey:(__bridge id)kSecValueData]; [privateKey setObject:(__bridge id) kSecAttrKeyClassPrivate forKey:(__bridge id) kSecAttrKeyClass]; [privateKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id) kSecReturnPersistentRef]; CFTypeRef persistKey = nil; OSStatus status = SecItemAdd((__bridge CFDictionaryRef)privateKey, &persistKey); if (persistKey != nil){ CFRelease(persistKey); } if ((status != noErr) && (status != errSecDuplicateItem)) { return nil; } [privateKey removeObjectForKey:(__bridge id)kSecValueData]; [privateKey removeObjectForKey:(__bridge id)kSecReturnPersistentRef]; [privateKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecReturnRef]; [privateKey setObject:(__bridge id) kSecAttrKeyTypeRSA forKey:(__bridge id)kSecAttrKeyType]; // Now fetch the SecKeyRef version of the key SecKeyRef keyRef = nil; status = SecItemCopyMatching((__bridge CFDictionaryRef)privateKey, (CFTypeRef *)&keyRef); if (status != noErr) { return nil; } return keyRef; } /// 去掉私钥头 + (NSData *)stripPrivateKeyHeader:(NSData *)d_key { // Skip ASN.1 private key header if (d_key == nil) return(nil); unsigned long len = [d_key length]; if (!len) return(nil); unsigned char *c_key = (unsigned char *)[d_key bytes]; unsigned int idx = 22; //magic byte at offset 22 if (0x04 != c_key[idx++]) return nil; //calculate length of the key unsigned int c_len = c_key[idx++]; int det = c_len & 0x80; if (!det) { c_len = c_len & 0x7f; } else { int byteCount = c_len & 0x7f; if (byteCount + idx > len) { //rsa length field longer than buffer return nil; } unsigned int accum = 0; unsigned char *ptr = &c_key[idx]; idx += byteCount; while (byteCount) { accum = (accum << 8) + *ptr; ptr++; byteCount--; } c_len = accum; } // Now make a new NSData from this buffer return [d_key subdataWithRange:NSMakeRange(idx, c_len)]; } #pragma mark - 辅助方法 /// 使用公钥加密数据 + (NSData *)encryptData:(NSData *)data withKeyRef:(SecKeyRef) keyRef { const uint8_t *srcbuf = (const uint8_t *)[data bytes]; size_t srclen = (size_t)data.length; size_t block_size = SecKeyGetBlockSize(keyRef) * sizeof(uint8_t); void *outbuf = malloc(block_size); size_t src_block_size = block_size - 11; NSMutableData *ret = [[NSMutableData alloc] init]; for (int idx=0; idx<srclen; idx+=src_block_size) { //NSLog(@"%d/%d block_size: %d", idx, (int)srclen, (int)block_size); size_t data_len = srclen - idx; if (data_len > src_block_size) { data_len = src_block_size; } size_t outlen = block_size; OSStatus status = noErr; status = SecKeyEncrypt(keyRef, kSecPaddingPKCS1, srcbuf + idx, data_len, outbuf, &outlen ); if (status != 0) { NSLog(@"SecKeyEncrypt fail. Error Code: %d", status); ret = nil; break; } else { [ret appendBytes:outbuf length:outlen]; } } free(outbuf); CFRelease(keyRef); return ret; } /// 使用私钥解密数据 + (NSData *)decryptData:(NSData *)data withKeyRef:(SecKeyRef) keyRef { const uint8_t *srcbuf = (const uint8_t *)[data bytes]; size_t srclen = (size_t)data.length; size_t block_size = SecKeyGetBlockSize(keyRef) * sizeof(uint8_t); UInt8 *outbuf = malloc(block_size); size_t src_block_size = block_size; NSMutableData *ret = [[NSMutableData alloc] init]; for (int idx=0; idx<srclen; idx+=src_block_size) { //NSLog(@"%d/%d block_size: %d", idx, (int)srclen, (int)block_size); size_t data_len = srclen - idx; if(data_len > src_block_size){ data_len = src_block_size; } size_t outlen = block_size; OSStatus status = noErr; status = SecKeyDecrypt(keyRef, kSecPaddingNone, srcbuf + idx, data_len, outbuf, &outlen ); if (status != 0) { NSLog(@"SecKeyEncrypt fail. Error Code: %d", status); ret = nil; break; } else { //the actual decrypted data is in the middle, locate it! int idxFirstZero = -1; int idxNextZero = (int)outlen; for ( int i = 0; i < outlen; i++ ) { if ( outbuf[i] == 0 ) { if ( idxFirstZero < 0 ) { idxFirstZero = i; } else { idxNextZero = i; break; } } } [ret appendBytes:&outbuf[idxFirstZero+1] length:idxNextZero-idxFirstZero-1]; } } free(outbuf); CFRelease(keyRef); return ret; } /// base64 编码 static NSString * base64_encode_data(NSData *data) { data = [data base64EncodedDataWithOptions:0]; NSString *ret = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; return ret; } /// base64 解码 static NSData * base64_decode(NSString *str) { NSData *data = [[NSData alloc] initWithBase64EncodedString:str options:NSDataBase64DecodingIgnoreUnknownCharacters]; return data; } @end 1、使用秘钥证书文件进行加密解密 使用 .der 和 .p12 秘钥文件进行加密、解密。 // 原始数据 NSString *originalString = @"这是一段将要使用 '.der' 文件加密的字符串!"; NSLog(@"加密前: %@", originalString); // 秘钥证书文件 .der 和 .p12 路径 NSString *public_key_path = [[NSBundle mainBundle] pathForResource:@"public_key" ofType:@"der"]; NSString *private_key_path = [[NSBundle mainBundle] pathForResource:@"private_key" ofType:@"p12"]; // 加密 NSString *encryptStr = [QRSAEncryptor q_encryptWithString:originalString publicKeyFilePath:public_key_path]; NSLog(@"加密后: %@", encryptStr); // 解密 NSString *DencryptStr = [QRSAEncryptor q_decryptWithString:encryptStr privateKeyFilePath:private_key_path password:@"qianchia"]; NSLog(@"解密后: %@", DencryptStr); 效果 2、使用秘钥字符串进行加密解密 秘钥字符串可以来这里:http://web.chacuo.net/netrsakeypair, 这是一个在线生成 RSA 秘钥的网站, 生成公钥和秘钥后, 复制出来用于测试。 // 原始数据 NSString *originalString = @"这是一段将要使用 '秘钥字符串' 进行加密的字符串!"; NSLog(@"加密前: %@", originalString); // 加密 NSString *publicKeyStr = @"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDsQ44uzMg83T6z7/dvNn2B1KHlzGwccgo055PeimXdBbzUVBECE0nQeNGb9tkO3mVnu8R4Iu5faoX7MY/muiTVZ3NDAvtk+WBjXfNqHmWvlMfj5jwxnITosnHMLVgrqDFc9q1yfmbTLhd8cJhMXsVBlduCSYbdNitA2z4B3hKS5wIDAQAB"; NSString *encryptStr = [QRSAEncryptor q_encryptWithString:originalString publicKey:publicKeyStr]; NSLog(@"加密后: %@", encryptStr); // 解密 NSString *privateKeyStr = @"MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOxDji7MyDzdPrPv9282fYHUoeXMbBxyCjTnk96KZd0FvNRUEQITSdB40Zv22Q7eZWe7xHgi7l9qhfsxj+a6JNVnc0MC+2T5YGNd82oeZa+Ux+PmPDGchOiyccwtWCuoMVz2rXJ+ZtMuF3xwmExexUGV24JJht02K0DbPgHeEpLnAgMBAAECgYB1cuPEihJkh0t7YagsRfdASjatKOD5hwth31kXwM8Af7CuEJhf4rzIALeag6zFgnMAjUwOuLatAiRWif3SIejapMaY/DcXWM/5ugYNi1exS1U8BeBjAOyZuQf/onOn0c0eBqT912CFnjEO5iNuNDkheRQK/FBv2XuMpnAI1FbGQQJBAPmmJkXtEDoM90PxPcL/+ecoNCe2aabiN/D9JlHtOE64DJzRQG4HHpizsvzxMQ00+ItTsG089BjpZPPHuLMO3AcCQQDyRjwPri2lyRC7GHgkgjB03NFL16ENkNER5/7X6TE15uqH/kdrKwrVUNNFwq9a11CHKtJqZOSgy0iN6rKF1JohAkBihAR6d7B9l/xDnYFn4Ce35o+eVEehCYhV2zAyCFC+D7c6cwDf6oNScydg1bUrpwmlwaLPmMwiwIeMA/aJAoYlAkEAoFjJwZsHDTWRBDNCuO8NgRrwzuBs8FyLcu135pCpCELHsLAjtpMrPVmcKwyaIGZnHr7BurcB9kX0xDC0bQzz4QJBAKKCz52lxwWboqo5h4lmk0F3R17O7bUNOaSn1kauX5ADBoQ2zsfl3LrPNB2Tt+97+wRyjnF9Gkwjg2okUc4V1MY="; NSString *dencryptStr = [QRSAEncryptor q_decryptWithString:encryptStr privateKey:privateKeyStr]; NSLog(@"解密后: %@", dencryptStr); 效果 10、iOS 上 散列算法 具体实现代码见 GitHub 源码 QExtension NSString+Hash.h @interface NSString (Hash) #pragma mark - 散列函数 /** * 计算 MD5 散列结果 * * 终端测试命令: * @code * md5 -s "string" * @endcode * * <p>提示:随着 MD5 碰撞生成器的出现,MD5 算法不应被用于任何软件完整性检查或代码签名的用途。<p> * * @return 32 个字符的 MD5 散列字符串 */ - (NSString *)q_md5String; /** * 计算 SHA1 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha1 * @endcode * * @return 40 个字符的 SHA1 散列字符串 */ - (NSString *)q_sha1String; /** * 计算 SHA224 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha224 * @endcode * * @return 56 个字符的 SHA224 散列字符串 */ - (NSString *)q_sha224String; /** * 计算 SHA256 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha256 * @endcode * * @return 64 个字符的 SHA256 散列字符串 */ - (NSString *)q_sha256String; /** * 计算 SHA384 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha384 * @endcode * * @return 96 个字符的 SHA384 散列字符串 */ - (NSString *)q_sha384String; /** * 计算 SHA512 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha512 * @endcode * * @return 128 个字符的 SHA512 散列字符串 */ - (NSString *)q_sha512String; #pragma mark - HMAC 散列函数 /** * 计算 HMAC MD5 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl dgst -md5 -hmac "key" * @endcode * * @return 32 个字符的 HMAC MD5 散列字符串 */ - (NSString *)q_hmacMD5StringWithKey:(NSString *)key; /** * 计算 HMAC SHA1 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha1 -hmac "key" * @endcode * * @return 40 个字符的 HMAC SHA1 散列字符串 */ - (NSString *)q_hmacSHA1StringWithKey:(NSString *)key; /** * 计算 HMAC SHA224 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha224 -hmac "key" * @endcode * * @return 56 个字符的 HMAC SHA224 散列字符串 */ - (NSString *)q_hmacSHA224StringWithKey:(NSString *)key; /** * 计算 HMAC SHA256 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha256 -hmac "key" * @endcode * * @return 64 个字符的 HMAC SHA256 散列字符串 */ - (NSString *)q_hmacSHA256StringWithKey:(NSString *)key; /** * 计算 HMAC SHA384 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha384 -hmac "key" * @endcode * * @return 96 个字符的 HMAC SHA384 散列字符串 */ - (NSString *)q_hmacSHA384StringWithKey:(NSString *)key; /** * 计算 HMAC SHA512 散列结果 * * 终端测试命令: * @code * echo -n "string" | openssl sha -sha512 -hmac "key" * @endcode * * @return 128 个字符的 HMAC SHA512 散列字符串 */ - (NSString *)q_hmacSHA512StringWithKey:(NSString *)key; #pragma mark - 时间戳散列函数 /** * 计算时间戳的 HMAC 散列结果 * * <p>提示:同样的密码,同样的加密算法,每分钟加密的结果都不一样。<p> * * @param key 秘钥 * * @return 32 个字符的 HMAC 散列字符串 */ - (NSString *)q_timeMD5StringWithKey:(NSString *)key; #pragma mark - 文件散列函数 /** * 计算文件的 MD5 散列结果 * * 终端测试命令: * @code * md5 file.dat * @endcode * * @return 32 个字符的 MD5 散列字符串 */ - (NSString *)q_fileMD5Hash; /** * 计算文件的 SHA1 散列结果 * * 终端测试命令: * @code * openssl sha -sha1 file.dat * @endcode * * @return 40 个字符的 SHA1 散列字符串 */ - (NSString *)q_fileSHA1Hash; /** * 计算文件的 SHA256 散列结果 * * 终端测试命令: * @code * openssl sha -sha256 file.dat * @endcode * * @return 64 个字符的 SHA256 散列字符串 */ - (NSString *)q_fileSHA256Hash; /** * 计算文件的 SHA512 散列结果 * * 终端测试命令: * @code * openssl sha -sha512 file.dat * @endcode * * @return 128 个字符的 SHA512 散列字符串 */ - (NSString *)q_fileSHA512Hash; @end NSString+Hash.m #import <CommonCrypto/CommonCrypto.h> @implementation NSString (Hash) #pragma mark - 散列函数 - (NSString *)q_md5String { const char *str = self.UTF8String; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CC_MD5(str, (CC_LONG)strlen(str), buffer); return [self q_stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } - (NSString *)q_sha1String { const char *str = self.UTF8String; uint8_t buffer[CC_SHA1_DIGEST_LENGTH]; CC_SHA1(str, (CC_LONG)strlen(str), buffer); return [self q_stringFromBytes:buffer length:CC_SHA1_DIGEST_LENGTH]; } - (NSString *)q_sha224String { const char *str = self.UTF8String; uint8_t buffer[CC_SHA224_DIGEST_LENGTH]; CC_SHA224(str, (CC_LONG)strlen(str), buffer); return [self q_stringFromBytes:buffer length:CC_SHA224_DIGEST_LENGTH]; } - (NSString *)q_sha256String { const char *str = self.UTF8String; uint8_t buffer[CC_SHA256_DIGEST_LENGTH]; CC_SHA256(str, (CC_LONG)strlen(str), buffer); return [self q_stringFromBytes:buffer length:CC_SHA256_DIGEST_LENGTH]; } - (NSString *)q_sha384String { const char *str = self.UTF8String; uint8_t buffer[CC_SHA384_DIGEST_LENGTH]; CC_SHA384(str, (CC_LONG)strlen(str), buffer); return [self q_stringFromBytes:buffer length:CC_SHA384_DIGEST_LENGTH]; } - (NSString *)q_sha512String { const char *str = self.UTF8String; uint8_t buffer[CC_SHA512_DIGEST_LENGTH]; CC_SHA512(str, (CC_LONG)strlen(str), buffer); return [self q_stringFromBytes:buffer length:CC_SHA512_DIGEST_LENGTH]; } #pragma mark - HMAC 散列函数 - (NSString *)q_hmacMD5StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CCHmac(kCCHmacAlgMD5, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self q_stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } - (NSString *)q_hmacSHA1StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_SHA1_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA1, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self q_stringFromBytes:buffer length:CC_SHA1_DIGEST_LENGTH]; } - (NSString *)q_hmacSHA224StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_SHA224_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA224, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self q_stringFromBytes:buffer length:CC_SHA224_DIGEST_LENGTH]; } - (NSString *)q_hmacSHA256StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_SHA256_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA256, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self q_stringFromBytes:buffer length:CC_SHA256_DIGEST_LENGTH]; } - (NSString *)q_hmacSHA384StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_SHA384_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA384, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self q_stringFromBytes:buffer length:CC_SHA384_DIGEST_LENGTH]; } - (NSString *)q_hmacSHA512StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_SHA512_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA512, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self q_stringFromBytes:buffer length:CC_SHA512_DIGEST_LENGTH]; } #pragma mark - 时间戳散列函数 - (NSString *)q_timeMD5StringWithKey:(NSString *)key { NSString *hmacKey = key.q_md5String; NSString *hmacStr = [self q_hmacMD5StringWithKey:hmacKey]; NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; fmt.dateFormat = @"yyyy-MM-ddHH:mm"; NSString *dateStr = [fmt stringFromDate:[NSDate date]]; hmacStr = [hmacStr stringByAppendingString:dateStr]; return [hmacStr q_hmacMD5StringWithKey:hmacKey]; } #pragma mark - 文件散列函数 #define FileHashDefaultChunkSizeForReadingData 4096 - (NSString *)q_fileMD5Hash { NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self]; if (fp == nil) { return nil; } CC_MD5_CTX hashCtx; CC_MD5_Init(&hashCtx); while (YES) { @autoreleasepool { NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData]; CC_MD5_Update(&hashCtx, data.bytes, (CC_LONG)data.length); if (data.length == 0) { break; } } } [fp closeFile]; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CC_MD5_Final(buffer, &hashCtx); return [self q_stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } - (NSString *)q_fileSHA1Hash { NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self]; if (fp == nil) { return nil; } CC_SHA1_CTX hashCtx; CC_SHA1_Init(&hashCtx); while (YES) { @autoreleasepool { NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData]; CC_SHA1_Update(&hashCtx, data.bytes, (CC_LONG)data.length); if (data.length == 0) { break; } } } [fp closeFile]; uint8_t buffer[CC_SHA1_DIGEST_LENGTH]; CC_SHA1_Final(buffer, &hashCtx); return [self q_stringFromBytes:buffer length:CC_SHA1_DIGEST_LENGTH]; } - (NSString *)q_fileSHA256Hash { NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self]; if (fp == nil) { return nil; } CC_SHA256_CTX hashCtx; CC_SHA256_Init(&hashCtx); while (YES) { @autoreleasepool { NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData]; CC_SHA256_Update(&hashCtx, data.bytes, (CC_LONG)data.length); if (data.length == 0) { break; } } } [fp closeFile]; uint8_t buffer[CC_SHA256_DIGEST_LENGTH]; CC_SHA256_Final(buffer, &hashCtx); return [self q_stringFromBytes:buffer length:CC_SHA256_DIGEST_LENGTH]; } - (NSString *)q_fileSHA512Hash { NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self]; if (fp == nil) { return nil; } CC_SHA512_CTX hashCtx; CC_SHA512_Init(&hashCtx); while (YES) { @autoreleasepool { NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData]; CC_SHA512_Update(&hashCtx, data.bytes, (CC_LONG)data.length); if (data.length == 0) { break; } } } [fp closeFile]; uint8_t buffer[CC_SHA512_DIGEST_LENGTH]; CC_SHA512_Final(buffer, &hashCtx); return [self q_stringFromBytes:buffer length:CC_SHA512_DIGEST_LENGTH]; } #pragma mark - 助手方法 /** * 返回二进制 Bytes 流的字符串表示形式 * * @param bytes 二进制 Bytes 数组 * @param length 数组长度 * * @return 字符串表示形式 */ - (NSString *)q_stringFromBytes:(uint8_t *)bytes length:(int)length { NSMutableString *strM = [NSMutableString string]; for (int i = 0; i < length; i++) { [strM appendFormat:@"%02x", bytes[i]]; } return [strM copy]; } @end ViewController.m NSString *str = @"hello world"; NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Info.plist" ofType:nil]; // 散列 NSString *md5Str = [str q_md5String]; NSLog(@"md5Str: %@", md5Str); NSString *sha1Str = [str q_sha1String]; NSLog(@"sha1Str: %@", sha1Str); NSString *sha224Str = [str q_sha224String]; NSLog(@"sha224Str: %@", sha224Str); NSString *sha256Str = [str q_sha256String]; NSLog(@"sha256Str: %@", sha256Str); NSString *sha384Str = [str q_sha384String]; NSLog(@"sha384Str: %@", sha384Str); NSString *sha512Str = [str q_sha512String]; NSLog(@"sha512Str: %@\n\n", sha512Str); // hmac 散列 NSString *hmacMD5Str = [str q_hmacMD5StringWithKey:@"yourKey"]; NSLog(@"hmacMD5Str: %@", hmacMD5Str); NSString *hmacSHA1Str = [str q_hmacSHA1StringWithKey:@"yourKey"]; NSLog(@"hmacSHA1Str: %@", hmacSHA1Str); NSString *hmacSHA224Str = [str q_hmacSHA224StringWithKey:@"yourKey"]; NSLog(@"hmacSHA224Str: %@", hmacSHA224Str); NSString *hmacSHA256Str = [str q_hmacSHA256StringWithKey:@"yourKey"]; NSLog(@"hmacSHA256Str: %@", hmacSHA256Str); NSString *hmacSHA384Str = [str q_hmacSHA384StringWithKey:@"yourKey"]; NSLog(@"hmacSHA384Str: %@", hmacSHA384Str); NSString *hmacSHA512Str = [str q_hmacSHA512StringWithKey:@"yourKey"]; NSLog(@"hmacSHA512Str: %@\n\n", hmacSHA512Str); // 时间戳 MD5 散列 NSString *timeStr = [str q_timeMD5StringWithKey:@"yourKey"]; NSLog(@"timeStr: %@\n\n", timeStr); // 文件 散列 NSString *fileMD5Str = [filePath q_fileMD5Hash]; NSLog(@"fileMD5Str: %@", fileMD5Str); NSString *fileSHA1Str = [filePath q_fileSHA1Hash]; NSLog(@"fileSHA1Str: %@", fileSHA1Str); NSString *fileSHA256Str = [filePath q_fileSHA256Hash]; NSLog(@"fileSHA256Str: %@", fileSHA256Str); NSString *fileSHA512Str = [filePath q_fileSHA512Hash]; NSLog(@"fileSHA512Str: %@", fileSHA512Str); 11、iOS 上 OpenSSL 11.1 OpenSSL 生成公钥和私钥 1、进入 OpenSSL 环境 Mac 自带 OpenSSL,所以我们不需要自己装 OpenSSL,直接打开终端,输入如下命令,回车。 $ OpenSSL 2、创建私钥 输入如下命令,rsa_private_key.pem 为私钥文件名,1024 为密钥长度,觉得不够安全的话可以用 2048,但是代价也相应增大。 # genrsa -out 输出私钥文件名.文件格式 密钥长度 $ genrsa -out rsa_private_key.pem 1024 3、将把 RSA 私钥转换成 PKCS8 格式 输入如下命令,PEM 为输出的文件格式,这一步会提示给私钥文件设置密码,直接输入想要设置密码即可,然后敲回车,然后再验证刚才设置的密码,再次输入密码,然后敲回车,完毕。在解密时,私钥文件需要和这里设置的密码配合使用,因此需要牢记此密码。 # pkcs8 -topk8 -inform 输入文件格式 -in 要转换的文件名.文件格式 -outform 输出文件格式 –nocrypt $ pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM –nocrypt 4、生成公钥 输入如下命令。rsa_private_key.pem 为私钥文件名,rsa_public_key.pem 为公钥文件名。输出 writing RSA key 表示生成公钥成功。 # rsa -in 私钥文件名.文件格式 -pubout -out 公钥文件名.文件格式 $ rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem 5、退出 输入如下命令退出 OpenSSL 环境,或者直接退出终端。 $ exit ## 6、导出生成的私钥和公钥文件 打开系统下的 电脑用户名 文件夹,即可看到名为 rsa_private_key.pem 和 rsa_public_key.pem 两个文件,将文件拖拽到桌面或者你指定的文件夹内使用即可。 11.2 生成 x509 格式的证书请求文件 生成过程中会要你輸入一些地理位置信息,可以不填。如果没问题的话,在 电脑用户名 文件夹下会产生两个文件 private_key.pem 和 public_key.der,这两个分别是我们的私钥和公钥证书文件。 # openssl req -证书格式 -out 公钥文件名.文件格式 -outform 输出文件格式 -new -newkey rsa:密钥长度 -keyout 私钥文件名.文件格式 -days 有效时间 -nodes $ openssl req -x509 -out public_key.der -outform der -new -newkey rsa:2048 -keyout private_key.pem -days 3650 -nodes 分开写步骤如下,生成环境是在 Mac 系统下,使用 openssl 进行生成,首先打开终端,按下面这些步骤依次来做。 1、生成模长为 1024bit 的私钥文件 private_key.pem # openssl genrsa -out 输出的私钥文件名.文件格式 密钥长度 $ openssl genrsa -out private_key.pem 1024 2、生成证书请求文件 rsaCertReq.csr # openssl req -new -key 私钥文件名.文件格式 -out 输出的证书请求文件.文件格式 $ openssl req -new -key private_key.pem -out rsaCerReq.csr 注意:这一步会提示输入国家、省份、mail 等信息,可以根据实际情况填写,或者全部不用填写,直接全部敲回车。 3、生成证书 rsaCert.crt,并设置有效时间为 10 年 # openssl 证书格式 -req -days 有效时间 -in 证书请求文件.文件格式 -signkey 私钥文件名.文件格式 -out 输出的证书文件.文件格式 $ openssl x509 -req -days 3650 -in rsaCerReq.csr -signkey private_key.pem -out rsaCert.crt 4、生成供 iOS 使用的公钥证书文件 public_key.der # openssl 证书格式 -outform 输出文件格式 -in 证书文件.文件格式 -out 输出的公钥证书文件名.文件格式 $ openssl x509 -outform der -in rsaCert.crt -out public_key.der 5、生成供 iOS 使用的私钥证书文件 private_key.p12 # openssl 输出文件格式 -export -out 输出的私钥证书文件名.文件格式 -inkey 私钥文件名.文件格式 -in 证书文件.文件格式 $ openssl pkcs12 -export -out private_key.p12 -inkey private_key.pem -in rsaCert.crt 注意:这一步会提示给私钥文件设置密码,直接输入想要设置密码即可,然后敲回车,然后再验证刚才设置的密码,再次输入密码,然后敲回车,完毕。在解密时,private_key.p12 文件需要和这里设置的密码配合使用,因此需要牢记此密码。 6、生成供 Java 使用的公钥证书文件 rsa_public_key.pem # openssl rsa -in 私钥文件名.文件格式 -out 输出的公钥证书文件名.文件格式 -pubout $ openssl rsa -in private_key.pem -out rsa_public_key.pem -pubout 7、生成供 Java 使用的私钥证书文件 pkcs8_private_key.pem # openssl 输出文件格式 -topk8 -in 私钥文件名.文件格式 -out 输出的私钥证书文件名.文件格式 -nocrypt $ openssl pkcs8 -topk8 -in private_key.pem -out pkcs8_private_key.pem -nocrypt 全部执行成功后,会生成如下文件,其中 public_key.der 和 private_key.p12 就是 iOS 需要用到的文件,如下图。 12、iOS 上 钥匙串 GitHub 封装 SSKeychain/SAMKeychain Objective-C // 添加第三方库文件 SSKeychain // 包含头文件 #import "SSKeychain.h" // 获取所有的账户信息 // 只有同一个开发者开发的应用程序,才能够互相看到账号 NSArray *allAccounts = [SSKeychain allAccounts]; // 获取指定服务名的账户信息 NSArray *accounts = [SSKeychain accountsForService:[NSBundle mainBundle].bundleIdentifier]; // 将密码保存到钥匙串中 /* + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account; 参数: password :密码明文 serviceName:服务名,可以随便写,建议使用 bundleId,应用程序的唯一标示,每一个上架的应用程序,都有一个唯一的 bundleId account :账户名(用户名) */ // 保存 NSString 格式的密码 [SSKeychain setPassword:self.pwdText.text forService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; NSData *pwdData = [self.pwdText.text dataUsingEncoding:NSUTF8StringEncoding]; // 保存 NSData 格式的密码 [SSKeychain setPasswordData:pwdData forService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; // 将密码从钥匙串中取出 // 获取 NSString 格式的密码 self.pwdText.text = [SSKeychain passwordForService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; // 获取 NSData 格式的密码 NSData *pwdData1 = [SSKeychain passwordDataForService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text]; // 将密码从钥匙串中删除 [SSKeychain deletePasswordForService:[NSBundle mainBundle].bundleIdentifier account:self.usernameText.text];
1、Bitcode 随着 Xcode7 的发布,Apple 提供了一项新的技术来支持 App 瘦身功能,那就是 Bitcode。 1、BitCode 是什么 Bitcode is an intermediate representation of a compiled program. Apps you upload to iTunes Connect that contain bitcode will be compiled and linked on the store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the store. Xcode hides symbols generated during build time by default, so they are not readable by Apple. Only if you choose to include symbols when uploading your app to iTunes Connect would the symbols be sent to Apple. You must include symbols to receive crash reports from Apple. 上述引自Apple的文档 App Thinning (iOS, tvOS, watchOS)。 其大概意思是 Bitcode 类似于一个中间码,被上传到 AppleStore 之后,苹果会根据下载应用的用户的手机指令集类型生成只有该指令集的二进制,进行下发,从而达到精简安装包体积的目的。 2、一点编译原理 为了更好的理解什么是 Bitcode,我们简短的看一下编译器编译的过程: Lexer :读入源文件,并将其转化成字符流。 Parser :将字符流转换成 AST(抽象语法树)。 Semantic Analysis :对输入的 AST 进行语法检查。 Code Generation :代码生成,将 AST 转换成低层次的IR指令。 Optimization :分析 IR 指令,将其中潜在会拖慢运行速度的指令干掉。 AsmPrinter :通过 IR(中间码)生成特定 CPU 架构的汇编代码。 Assemble :将汇编代码转化成二进制。 Linker :通常程序会引用其他的二进制文件(.a 或者 framework),但是这些链接在程序中没有正确的地址,只是个占位符。Linker 的工作就是给这些占位符正确的地址。 更多信息可以参考:The Compiler 一般情况下,在真实的编译器构架那种,会将上述过程分成前端和后端两部分来处理: 在前后端之间传递的就是 IR(中间码),而 Bitcode 就是一种特殊形式的中间码。原本前后端的工作都是在本地 LLVM 中完成,虽然 Apple 没有给出具体的 Bitcode 实现,但是通过他们的文档可以猜测,是将一部分后端的工作移到了服务器进行。从 Xcode 上传 IR 到服务器,服务器来真对不同的机型进行后续操作。从而达到真对不同机型生成对应指令集的二进制,而减小包体积的目的。 3、Bitcode 设置 实际上在 Xcode7 + 中,我们新建一个 iOS 程序时,Bitcode 选项默认是设置为 YES 的。我们可以在 Build Settings => Build Options => Enable Bitcode 选项中看到这个设置。 不过,我们现在需要考虑的是三个平台:iOS / Mac OS / watchOS。 对应 iOS,Bitcode 是可选的。 对于 watchOS,Bitcode 是必须的。 Mac OS 不支持 Bitcode。 如果我们开启了 Bitcode,在提交包时,下面这个界面也会有个 Bitcode 选项: 但是如果其中包含第三方库,不支持 Bitcode 时候,需要将 Enable BitCode 设置成 NO。而且这个选项是只要有一个第三方库不支持,就不能开的,否则连接错误。 确保打包的时候使用的是 fembed-bitcode, 而不是 fembed-bitcode-maker You should be aware that a normal build with the -fembed-bitcode-marker option will produce minimal size embedded bitcode sections without any real content. This is done as a way of testing the bitcode-related aspects of your build without slowing down the build process. The actual bitcode content is included when you do an Archive build. fembed-bitcode-maker:只是简单的标记一下在 archive 出来的二进制中 Bitcdoe 所在的位置。 fembed-bitcode:真的会生成 Bitcode 指令,并且嵌入到二进制中,这个设置不止要在 app 中设置,同样你也必须在编译静态链接库的时候使用。而且需要主题的是该参数系统只默认在 archive 模式下会添加。 需要注意的是 Bitcode 只默认在 archive 下编译。在 debug 和 release 下并不会。 如果您开发的是 app 那么走正常的打包 archive 流程就好了。如果你正在开发 .a 静态库或者 framework,请注意打包方式设置为 archive,或者在打包脚本中加入 -fembed-bitcode 参数。如果需要的话,需要在 Build Settings 中打开 DEPLOYMENT_POSTPROCESSING=YES,设置 Strip Style 为 debugging。 4、检测是否打开 Bitcode 当打开 Bitcdoe 选项之后,我们可以使用 otool 工具来检查二进制文件中是否包含 bitcode 段。 针对于静态链接库 .a 文件 otool -arch armv7 -l xxxx.a | grep __bitcode | wc -l 如果是当前库支持 .a 文件则会输出一个数字,如果不支持 Bitcode 则不会出现该数字。 上述命令只检查了 armv7 架构,同时,也必须使用该指令检查其他的指令集是否包含 Bitcode 如:arm64,armv7s 等等 检查 app 或者 framework 中是否包含 Bitcode 由于 app 中二进制和 framework 中二进制文件与 .a 文件存在差异,因为需要检查的是 __LLVM 段,当出现该段的时候,则表示支持 Bitcdoe,否则不支持。 otool -l xxxx | grep __LLVM | wc -l 这里 otool 有个 bug,当你的 framework 使用过 lipo 命令,进行拆解和合并之后,需要指定指令集进行检查才可以。 otool -arch armv7 -l xxxx | grep __LLVM | wc -l BUT, 上述检查过了之后,也不一定是真的支持 Bitcode,在实际的测试中,发现上述检测命令通过之后,某个使用的第三方库,依然报错不支持 Bitcode。因而最终结果,还是需要以是否能够连接成功为准。重要事情说三遍,上述网上流传的检测方法只做参考,最终还是要以实际效果为准。 最终结果检查 如果您是一个 APP,可以直接进行 Archive 打包,如果是一个库,则建议建一个 Demo 工程进行打包,记得要打开 Bitcode 设置。 CheckPoint1 连接是否报错 如果有任何一个库没有打开 Bitcode 链接,将会出现类似下方的错误。只要链接过了,那么恭喜了,基本上是 OK 了。 CheckPoint2 检查最终效果 使用开发模式导出 ipa 5、选择出包的方式 这里建议使用第二种,生成针对具体机型的包 出现了,Compiling Bitcode,这个过程 在最后输出的文件中,你能够看到一个 App Thinning 的结果,里面有针对各个机型的 ipa 包。 在 App Thinning Size Report 中能够明显看到,由于使用了 Bitcode 等技术之后,所带来的收益: App Thinning Size Report for All Variants of Black Variant: Black-iPad (4th generation)-etc.ipa Supported devices: iPad (3rd generation) and iPad (4th generation) App + On Demand Resources size: 368 KB compressed, 737 KB uncompressed App size: 368 KB compressed, 737 KB uncompressed On Demand Resources size: Zero KB compressed, Zero KB uncompressed ....
Contacts 通讯录 1、访问通讯录 设置系统访问通讯录权限 1.1 iOS 9.0 及 iOS 9.0 之后获取通讯录的方法 iOS 9.0 及 iOS 9.0 之后获取通讯录的方法 // 包含头文件 #import <Contacts/Contacts.h> #import <ContactsUI/ContactsUI.h> // 获取通讯录信息,自定义方法 - (void)fetchAddressBookOnIOS9AndLater { // 创建 CNContactStore 对象 CNContactStore *contactStore = [[CNContactStore alloc] init]; // 首次访问需用户授权 if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusNotDetermined) { // 首次访问通讯录 [contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!error){ if (granted) { // 允许 NSLog(@"已授权访问通讯录"); NSArray *contacts = [self fetchContactWithContactStore:contactStore]; // 访问通讯录 dispatch_async(dispatch_get_main_queue(), ^{ // 主线程 更新 UI NSLog(@"contacts:%@", contacts); }); } else { // 拒绝 NSLog(@"拒绝访问通讯录"); } } else { NSLog(@"发生错误!"); } }]; } else { // 非首次访问通讯录 NSArray *contacts = [self fetchContactWithContactStore:contactStore]; // 访问通讯录 dispatch_async(dispatch_get_main_queue(), ^{ // 主线程 更新 UI NSLog(@"contacts:%@", contacts); }); } } // 访问通讯录,自定义方法 - (NSMutableArray *)fetchContactWithContactStore:(CNContactStore *)contactStore { // 判断访问权限 if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { // 有权限访问 NSError *error = nil; // 创建数组,必须遵守 CNKeyDescriptor 协议,放入相应的字符串常量来获取对应的联系人信息 NSArray <id<CNKeyDescriptor>> *keysToFetch = @[CNContactFamilyNameKey, CNContactGivenNameKey, CNContactPhoneNumbersKey]; // 获取通讯录数组 NSArray<CNContact*> *arr = [contactStore unifiedContactsMatchingPredicate:nil keysToFetch:keysToFetch error:&error]; if (!error) { NSMutableArray *contacts = [NSMutableArray array]; for (int i = 0; i < arr.count; i++) { CNContact *contact = arr[i]; NSString *givenName = contact.givenName; NSString *familyName = contact.familyName; NSString *phoneNumber = ((CNPhoneNumber *)(contact.phoneNumbers.lastObject.value)).stringValue; [contacts addObject:@{@"name":[givenName stringByAppendingString:familyName], @"phoneNumber":phoneNumber}]; } return contacts; } else { return nil; } } else { // 无权限访问 NSLog(@"无权限访问通讯录"); return nil; } } 效果 1.2 iOS 9.0 之前获取通讯录的方法 iOS 9.0 之前获取通讯录的方法 // 包含头文件 #import <AddressBook/AddressBook.h> // 获取通讯录信息,自定义方法 - (void)fetchAddressBookBeforeIOS9 { ABAddressBookRef addressBook = ABAddressBookCreate(); // 首次访问需用户授权 if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined) { // 首次访问通讯录 ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) { if (!error) { if (granted) { // 允许 NSLog(@"已授权访问通讯录"); NSArray *contacts = [self fetchContactWithAddressBook:addressBook]; dispatch_async(dispatch_get_main_queue(), ^{ // 主线程 更新 UI NSLog(@"contacts:%@", contacts); }); } else { // 拒绝 NSLog(@"拒绝访问通讯录"); } } else { NSLog(@"发生错误!"); } }); } else { // 非首次访问通讯录 NSArray *contacts = [self fetchContactWithAddressBook:addressBook]; dispatch_async(dispatch_get_main_queue(), ^{ // 主线程 更新 UI NSLog(@"contacts:%@", contacts); }); } } // 访问通讯录,自定义方法 - (NSMutableArray *)fetchContactWithAddressBook:(ABAddressBookRef)addressBook { if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) { // 有权限访问 // 获取联系人数组 NSArray *array = (__bridge NSArray *)ABAddressBookCopyArrayOfAllPeople(addressBook); NSMutableArray *contacts = [NSMutableArray array]; for (int i = 0; i < array.count; i++) { //获取联系人 ABRecordRef people = CFArrayGetValueAtIndex((__bridge ABRecordRef)array, i); // 获取联系人详细信息,如:姓名,电话,住址等信息 NSString *firstName = (__bridge NSString *)ABRecordCopyValue(people, kABPersonFirstNameProperty); NSString *lastName = (__bridge NSString *)ABRecordCopyValue(people, kABPersonLastNameProperty); ABMutableMultiValueRef *phoneNumRef = ABRecordCopyValue(people, kABPersonPhoneProperty); NSString *phoneNumber = ((__bridge NSArray *)ABMultiValueCopyArrayOfAllValues(phoneNumRef)).lastObject; [contacts addObject:@{@"name": [firstName stringByAppendingString:lastName], @"phoneNumber": phoneNumber}]; } return contacts; } else { // 无权限访问 NSLog(@"无权限访问通讯录"); return nil; } } 效果 2、对通讯录的操作 对通讯录的操作 // 包含头文件 #import <Contacts/Contacts.h> #import <ContactsUI/ContactsUI.h> // 遵守协议 <CNContactPickerDelegate> @property (nonatomic, strong) NSArray *contacts; @property (nonatomic, strong) NSArray *groups; 2.1 打开通讯录 打开通讯录 // 初始化 CNContactPickerViewController CNContactPickerViewController *contactPickerVC = [[CNContactPickerViewController alloc] init]; // 设置代理,需遵守 CNContactPickerDelegate 协议 contactPickerVC.delegate = self; // 显示联系人窗口视图 [self presentViewController:contactPickerVC animated:YES completion:nil]; // 取消,CNContactPickerDelegate 协议方法 - (void)contactPickerDidCancel:(CNContactPickerViewController *)picker { // 点击联系人控制器的 Cancel 按钮执行该方法,picker 联系人控制器 NSLog(@"取消"); } // 选中联系人,CNContactPickerDelegate 协议方法 - (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact { // 选中联系人时执行该方法,picker 联系人控制器,contact 联系人 NSLog(@"联系人的资料:%@", contact); [self dismissViewControllerAnimated:YES completion:nil]; // 显示联系人详细页面 CNContactViewController *contactVC = [CNContactViewController viewControllerForContact:contact]; contactVC.displayedPropertyKeys = @[CNContactGivenNameKey, CNContactPhoneNumbersKey, CNContactFamilyNameKey, CNContactInstantMessageAddressesKey, CNContactEmailAddressesKey, CNContactDatesKey, CNContactUrlAddressesKey, CNContactBirthdayKey, CNContactImageDataKey]; [self presentViewController:contactVC animated:YES completion:nil]; } 2.2 联系人操作 1、增加联系人 // 增加的联系人信息 CNMutableContact *contact = [self initializeContact]; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 添加联系人 [saveRequest addContact:contact toContainerWithIdentifier:nil]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL]; // 初始化联系人信息 - (CNMutableContact *)initializeContact { // 创建联系人对象 CNMutableContact *contact = [[CNMutableContact alloc] init]; // 设置联系人的头像 contact.imageData = UIImagePNGRepresentation([UIImage imageNamed:@"demo5"]); // 设置联系人姓名 contact.givenName = @"Qian"; // 设置姓氏 contact.familyName = @"Chia"; // 设置联系人邮箱 CNLabeledValue *homeEmail = [CNLabeledValue labeledValueWithLabel:CNLabelHome value:@"qianchia@icloud.com"]; CNLabeledValue *workEmail = [CNLabeledValue labeledValueWithLabel:CNLabelWork value:@"qianchia@icloud.com"]; CNLabeledValue *otherEmail = [CNLabeledValue labeledValueWithLabel:CNLabelOther value:@"qianchia@icloud.com"]; contact.emailAddresses = @[homeEmail,workEmail,otherEmail]; // 设置机构名 contact.organizationName = @"互联网"; // 设置部门 contact.departmentName = @"Development"; // 设置工作的名称 contact.jobTitle = @"iOS"; // 设置社会的简述 CNSocialProfile *profile = [[CNSocialProfile alloc] initWithUrlString:@"12306.cn" username:@"lily" userIdentifier:nil service:@"IT行业"]; CNLabeledValue *socialProfile = [CNLabeledValue labeledValueWithLabel:CNSocialProfileServiceGameCenter value:profile]; contact.socialProfiles = @[socialProfile]; // 设置电话号码 CNPhoneNumber *mobileNumber = [[CNPhoneNumber alloc] initWithStringValue:@"15188888888"]; CNLabeledValue *mobilePhone = [[CNLabeledValue alloc] initWithLabel:CNLabelPhoneNumberMobile value:mobileNumber]; contact.phoneNumbers = @[mobilePhone]; // 设置与联系人的关系 CNContactRelation *friend = [[CNContactRelation alloc] initWithName:@"好朋友"]; CNLabeledValue *relation = [CNLabeledValue labeledValueWithLabel:CNLabelContactRelationFriend value:friend]; contact.contactRelations = @[relation]; // 设置生日 NSDateComponents *birthday = [[NSDateComponents alloc] init]; birthday.day = 6; birthday.month = 5; birthday.year = 2000; contact.birthday = birthday; return contact; } 2、删除联系人 // 要删除的联系人 CNMutableContact *contact = [self.contacts[0] mutableCopy]; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 删除联系人 [saveRequest deleteContact:contact]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL]; 3、修改联系人 // 要修改的联系人信息 CNMutableContact *contact = [self.contacts[0] mutableCopy]; contact.givenName = @"Qianqian"; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 修改联系人 [saveRequest updateContact:contact]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL]; 4、查询联系人 // 要查询的联系人 GivenName NSString *checkName = @"Qian"; // 检索条件 NSPredicate *predicate = [CNContact predicateForContactsMatchingName:checkName]; // 提取数据 (keysToFetch:@[CNContactGivenNameKey] 是设置提取联系人的哪些数据) CNContactStore *store = [[CNContactStore alloc] init]; NSArray *contactArray = [store unifiedContactsMatchingPredicate:predicate keysToFetch:@[CNContactGivenNameKey] error:NULL]; self.contacts = contactArray; 2.3 联系人群组操作 1、增加群组 // 要增加的群组 CNMutableGroup *group = [[CNMutableGroup alloc] init]; group.name = @"QianFriend"; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 增加群组 [saveRequest addGroup:group toContainerWithIdentifier:nil]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL]; 2、删除群组 // 要删除的群组 CNMutableGroup *group = self.groups[0]; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 删除群组 [saveRequest deleteGroup:group]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL]; 3、修改群组 // 要修改的群组 CNMutableGroup *group = self.groups[0]; group.name = @"QianWork"; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 修改群组 [saveRequest updateGroup:group]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL]; 4、查询群组 CNContactStore *store = [[CNContactStore alloc] init]; // 查询所有的 group(predicate 参数为空时会查询所有的 group) NSArray *groupArray = [store groupsMatchingPredicate:nil error:NULL]; self.groups = groupArray; 5、向群组中添加联系人 // 要添加的联系人和群组 CNContact *contact = self.contacts[0]; CNMutableGroup *group = self.groups[0]; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 向群组中添加联系人 [saveRequest addMember:contact toGroup:group]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL]; 6、从群组中删除联系人 // 要删除的联系人和群组 CNContact *contact = self.contacts[0]; CNMutableGroup *group = self.groups[0]; // 创建请求 CNSaveRequest *saveRequest = [[CNSaveRequest alloc] init]; // 从群组中删除联系人 [saveRequest removeMember:contact fromGroup:group]; // 同步到通讯录 CNContactStore *store = [[CNContactStore alloc] init]; [store executeSaveRequest:saveRequest error:NULL];
1、调用电话 1.1 拨打系统电话 调用系统自带的打电话程序,要跳转到打电话程序,打完电话自动跳转回来。 在 iOS9.0 + 系统隐私控制里禁止查询设备中已安装的 App,所以在 iOS9.0 + 系统中要实现应用间跳转还需要配置协议白名单。在发起跳转的 App 的 Info.plist 文件中增加一个 LSApplicationQueriesSchemes 字段,把它设置为数组类型,并配置需要跳转的协议名单。 URL 地址:tel://电话号码 iOS 系统版本 < 10.0 NSURL *url = [NSURL URLWithString:@"tel://10086"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url]; } else { NSLog(@"没有安装应用"); } iOS 系统版本 >= 10.0 NSURL *url = [NSURL URLWithString:@"tel://10086"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { NSLog(@"没有安装应用"); } 1.2 拨打电话 在应用内部打电话,不需要跳出程序,完全在自己的程序中,打完电话自动跳转回来。 UIWebView *callWebView = [[UIWebView alloc] init]; NSURL *url = [NSURL URLWithString:@"tel:10086"]; [callWebView loadRequest:[NSURLRequest requestWithURL:url]]; [self.view addSubview:callWebView]; 2、调用短信 2.1 调用系统短信 调用系统自带的信息程序,要跳转到信息程序,发完短信后不会跳转回来。 在 iOS9.0 + 系统隐私控制里禁止查询设备中已安装的 App,所以在 iOS9.0 + 系统中要实现应用间跳转还需要配置协议白名单。在发起跳转的 App 的 Info.plist 文件中增加一个 LSApplicationQueriesSchemes 字段,把它设置为数组类型,并配置需要跳转的协议名单。 URL 地址:sms://电话号码 iOS 系统版本 < 10.0 NSURL *url = [NSURL URLWithString:@"sms://10086"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url]; } else { NSLog(@"没有安装应用"); } iOS 系统版本 >= 10.0 NSURL *url = [NSURL URLWithString:@"sms://10086"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { NSLog(@"没有安装应用"); } 2.2 带内容发送短信 在应用内部发送短信,不需要跳出程序,完全在自己的程序中,发送短信后自动跳转回来。可以在程序中设置发送的短信内容。 添加 MessageUI.framework 框架。 在发起发送短信的视图控制器中 // 引入头文件 #import <MessageUI/MessageUI.h> // 遵守协议 <MFMessageComposeViewControllerDelegate> 发送短信 if ([MFMessageComposeViewController canSendText]) { MFMessageComposeViewController *messageVC = [[MFMessageComposeViewController alloc] init]; messageVC.messageComposeDelegate = self; // 设置电话号码 messageVC.recipients = @[@"10086"]; // 设置短信内容 messageVC.body = @"话费余额"; // 调用系统发送短信界面 [self presentViewController:messageVC animated:YES completion:nil]; } 处理发送响应结果 // MFMessageComposeViewControllerDelegate 协议方法 - (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result { [self dismissViewControllerAnimated:YES completion:nil]; if (result == MessageComposeResultCancelled) { NSLog(@"Message cancelled"); } else if (result == MessageComposeResultSent) { NSLog(@"Message sent"); } else { NSLog(@"Message failed"); } } 3、调用邮件 3.1 发送系统邮件 调用系统自带的邮件程序,要跳转到邮件程序,发完邮件后不会跳转回来。 在 iOS9.0 + 系统隐私控制里禁止查询设备中已安装的 App,所以在 iOS9.0 + 系统中要实现应用间跳转还需要配置协议白名单。在发起跳转的 App 的 Info.plist 文件中增加一个 LSApplicationQueriesSchemes 字段,把它设置为数组类型,并配置需要跳转的协议名单。 URL 地址:mailto://邮件地址 iOS 系统版本 < 10.0 NSURL *url = [NSURL URLWithString:@"mailto://qq0228@163.com"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url]; } else { NSLog(@"没有安装应用"); } iOS 系统版本 >= 10.0 NSURL *url = [NSURL URLWithString:@"mailto://qq0228@163.com"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { NSLog(@"没有安装应用"); } 3.2 带内容发送邮件 在应用内部发送邮件,不需要跳出程序,完全在自己的程序中,发送邮件后自动跳转回来。可以在程序中设置发送的邮件内容。 添加 MessageUI.framework 框架。 在发起发送邮件的视图控制器中 // 引入头文件 #import <MessageUI/MessageUI.h> // 遵守协议 <MFMailComposeViewControllerDelegate> 发送邮件 if ([MFMailComposeViewController canSendMail]) { MFMailComposeViewController *mailVC = [[MFMailComposeViewController alloc] init]; mailVC.mailComposeDelegate = self; // 设置邮箱 [mailVC setToRecipients:@[@"qq0228@163.com"]]; // 设置邮件主题 [mailVC setSubject:@"Hello"]; // 设置邮件内容 [mailVC setMessageBody:@"Lorem ipsum dolor sit amet" isHTML:NO]; // 调用系统发送邮件界面 [self presentViewController:mailVC animated:YES completion:nil]; } 处理发送响应结果 // MFMailComposeViewControllerDelegate 协议方法 - (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { [self dismissViewControllerAnimated:YES completion:nil]; if (result == MFMailComposeResultCancelled) { NSLog(@"Message cancelled"); } else if (result == MFMailComposeResultSent) { NSLog(@"Message sent"); } else if (result == MFMailComposeResultSaved) { NSLog(@"Message saved"); } else { NSLog(@"Message failed"); } } 4、调用地图 4.1 调用系统地图 在 iOS9.0 + 系统隐私控制里禁止查询设备中已安装的 App,所以在 iOS9.0 + 系统中要实现应用间跳转还需要配置协议白名单。在发起跳转的 App 的 Info.plist 文件中增加一个 LSApplicationQueriesSchemes 字段,把它设置为数组类型,并配置需要跳转的协议名单。 URL 地址:maps:// iOS 系统版本 < 10.0 NSURL *url = [NSURL URLWithString:@"maps://"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url]; } else { NSLog(@"没有安装应用"); } iOS 系统版本 >= 10.0 NSURL *url = [NSURL URLWithString:@"maps://"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { NSLog(@"没有安装应用"); } 5、调用浏览器 51 调用系统浏览器 在 iOS9.0 + 系统隐私控制里禁止查询设备中已安装的 App,所以在 iOS9.0 + 系统中要实现应用间跳转还需要配置协议白名单。在发起跳转的 App 的 Info.plist 文件中增加一个 LSApplicationQueriesSchemes 字段,把它设置为数组类型,并配置需要跳转的协议名单。 URL 地址:http://网址 或:https://网址 iOS 系统版本 < 10.0 NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url]; } else { NSLog(@"没有安装应用"); } iOS 系统版本 >= 10.0 NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { NSLog(@"没有安装应用"); } 6、调用应用商店 6.1 调用系统应用商店 找到应用程序的描述链接,然后将 http:// 替换为 itms:// 或者 itms-apps://。比如: http://itunes.apple.com/gb/app/yi-dong-cai-bian/id391945719?mt=8 itms-apps://itunes.apple.com/gb/app/yi-dong-cai-bian/id391945719?mt=8 itms://itunes.apple.com/gb/app/yi-dong-cai-bian/id391945719?mt=8 URL 地址:itms-apps://网址 或:itms://网址 itms-apps:// // 调用系统 App Store 应用 itms:// // 调用系统 iTunes Store 应用 iOS 系统版本 < 10.0 NSURL *url = [NSURL URLWithString:@"itms-apps://itunes.apple.com/gb/app/yi-dong-cai-bian/id391945719?mt=8"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url]; } else { NSLog(@"没有安装应用"); } iOS 系统版本 >= 10.0 NSURL *url = [NSURL URLWithString:@"itms-apps://itunes.apple.com/gb/app/yi-dong-cai-bian/id391945719?mt=8"]; if ([[UIApplication sharedApplication] canOpenURL:url]) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { NSLog(@"没有安装应用"); } 7、保存图片到相册 设置系统访问相册权限 保存图片到相册 // 将图片存储到相册中 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); // 将图片存储到相册中,完成后调用指定的方法 UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil); // 保存完成后调用的方法,必须为这个方法 - (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo { } 8、访问通讯录 详细讲解见 iOS - Contacts 通讯录 9、获取 wifi 信息 9.1 获取 wifi 名称信息 具体实现代码见 GitHub 源码 QExtension #import <SystemConfiguration/CaptiveNetwork.h> NSString * const BSSIDKey = @"BSSID"; NSString * const SSIDKey = @"SSID"; NSString * const SSIDDATAKey = @"SSIDDATA"; // 获取当前 Wifi 信息 + (NSDictionary *)q_getCurrentWifiInfo { NSDictionary *wifiDic = [NSDictionary dictionary]; CFArrayRef arrayRef = CNCopySupportedInterfaces(); if (arrayRef != nil) { CFDictionaryRef dicRef = CNCopyCurrentNetworkInfo(CFArrayGetValueAtIndex(arrayRef, 0)); CFRelease(arrayRef); if (dicRef != nil) { wifiDic = (NSDictionary *)CFBridgingRelease(dicRef); } } return wifiDic; } // 获取当前 Wifi 信息 NSDictionary *wifiInfo = [NSDictionary q_getCurrentWifiInfo]; NSLog(@"%@", wifiInfo); NSString *bssid = wifiInfo[BSSIDKey]; NSString *ssid = wifiInfo[SSIDKey]; NSString *ssidData = [[NSString alloc] initWithData:wifiInfo[SSIDDATAKey] encoding:NSUTF8StringEncoding]; NSLog(@"%@\n %@\n %@", bssid, ssid, ssidData); 9.2 获取 IP 地址 具体实现代码见 GitHub 源码 QExtension #import <arpa/inet.h> #import <ifaddrs.h> // 获取本地 IP 地址 + (NSString *)q_getIPAddress { NSString *address = @"error"; struct ifaddrs *interfaces = NULL; struct ifaddrs *temp_addr = NULL; int success = 0; // retrieve the current interfaces - returns 0 on success success = getifaddrs(&interfaces); if (success == 0) { // Loop through linked list of interfaces temp_addr = interfaces; while (temp_addr != NULL) { if (temp_addr->ifa_addr->sa_family == AF_INET) { // Check if interface is en0 which is the wifi connection on the iPhone if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) { // Get NSString from C String address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)]; } } temp_addr = temp_addr->ifa_next; } } // Free memory freeifaddrs(interfaces); return address; } // 获取本地 IP 地址 NSString *ipStr = [NSString q_getIPAddress]; NSLog(@"%@", ipStr);
1、前言 一般 iOS 开发者做 App 开发大部分时候都是通过 Http(s) 请求跟后台服务器打交道,做一些信息展示和用户交互。很少涉及到去跟外部硬件设备连接的开发。随着近年来车联网和物联网的兴起,智能家居和智能硬件的逐步火热,越来越多的 App 被开发出来,用来跟硬件设备进行来连接,获取硬件相关信息展示或者发送指令控制硬件来提供服务。本文就针对 iOS 的 App 如何跟外部设备进行连接通信这个问题进行讲解。 如下图所示,iOS App 连接外设的常用方式可以分为三大类: 2、通过网络端口通信 建立 Socket 使用 TCP/IP 协议族进行通信,天然支持多通道,想要几个通道就建几个 socket 就行了。 关于如何使用 Socket 进行 TCP、UDP,推荐 github 上的开源项目 CocoaAsyncSocket。 通过网络端口通信主要有三种方式:Wi-Fi 连接、USB 热点共享连接、NCM 连接。 2.1 Wi-Fi 连接 优点是:简单,不需要集成 MFi 芯片,只要对应的硬件有无线网卡,然后手机和硬件连接到同一个局域网中就可以使用 socket 通过网络协议通信了。 缺点也很明显: 无线连接信号容易受到干扰,不太稳定,容易断开。 如果硬件使用的场合没有公共 wifi,就需要手机自建热点共享,硬件进行热点接入,操作步骤较多,对用户来说学习使用成本较高,并且热点共享要求手机本身的数据移动网络是稳定的,在没有移动数据网络信号的地方,热点无法建立。 2.2 USB 热点共享连接 这个其实跟 Wi-Fi 中的热点共享非常类似,也不需要集成 MFI 芯片,区别就是 USB 线共享热点,走的是有线,不容易受到干扰,更稳定,而且 iPhone 可以边使用边充电; 缺点也是操作步骤比较复杂,需要先打开个人热点共享。 2.3 NCM 连接 就是把 USB 端口虚拟成标准的网络端口,然后手机和外设就能通过有线网络直连了,可以理解成手机和外设通过一跟网线连起来了,然后就可以用 socket 通过 TCP,UDP 进行通信了。 它的优点是:有线连接,非常稳定,带宽足够;也不依赖移动网络信号。 但是它的缺点就是:需要集成 MFi 芯片并进行 MFi 认证,有一定门槛。更变态的是这么好的一种方式,苹果只允许它自己的 CarPlay 使用,如果硬件使用 NCM 跟其他 App 通信,是不能通过 MFi 认证的。 3、通过 EAP 方式通信 EAP 全拼是 External Accessory Protocol,外部设备协议。这个是苹果推荐使用的外设连接方式。需要外设集成 MFi 芯片进行 MFi 认证。 手机端开发相对简单,只要集成 iOS 系统提供的一个框架 ExternalAccessory.framework,并且在 info.plist 中配置好协议字符串(Supported external accessory protocols)。 当 iOS 设备通过 USB 线或者蓝牙连接到对应硬件时,iOS 系统会把符合 MFi 认证要求的外设抽象成了一个流对象,App 通过指定的协议字符串来创建一个 EASession 类的实例来访问到该流对象,就能通过 NSInputStream 和 NSOutputStream 跟硬件件进行通信了。 它有两种模式,一种是叫 EASession 的模式,它带宽相对较低,但是允许同时通过多个协议字符串创建多个会话,也就是说直接支持多个通道;另外一种是 Native Transport 的模式,这种模式的优点是带宽足够大,理论值是 100MB 以上,但是不支持多通道,如果业务层需要支持多数据通道的话需要 App 自己进行通道的复用与拆分,并且 Native Transport 需要 iPhone 工作在 USB host 模式,硬件需要支持 USB 模式切换。 关于如何使用 EAP 跟外部设备进行通信,可以参考苹果官方的 demo 进行入门和学习。 4、通过 BLE 方式通信 BLE 即低功耗蓝牙,是 iOS7.0 以后才支持的连接方式。 它的优点是不需要集成 MFi 芯片做认证,功耗低。手机端开发也相对简单,集成 iOS 系统提供的 CoreBluetooth.framework 就行。缺点是:带宽很低,一般适合于只需要传输少量数据的场景。比如前两年非常火爆的各种所谓智能硬件,像智能水杯,智能体重计,运动手环等,都是采用这种连接方式。 关于 BLE 的具体使用见 iOS - Bluetooth 蓝牙。
前言 1、准备 开发者账号 完工的项目 2、上架步骤 1) 创建 App ID 2) 创建证书请求文件(CSR文件) 3) 创建发布证书(CER) 4) 创建 Provisioning Profiles 配置文件(PP 文件) 5) 在 App Store 创建应用 6) 打包上架 1、创建 App ID 1、打开苹果开发者中心,点击 “Account” 登录会员中心。 2、填写信息创建 app ID 2、创建证书请求文件(CSR 文件) CSR 文件主要用于绑定你的电脑的 1、在 Mac 上,点开 LaunchPad,在其他中找到打开钥匙串访问 2、点击电脑左上角的 钥匙串访问 => 证书助理 => 从证书颁发机构请求证书 3、出现如下界面,选择存储到磁盘,点击继续 4、选择存储到桌面,存储 5、点击完成 6、在桌面上看到下面的文件,证书请求文件完成 3、创建发布证书 (CER 文件) 1、在苹果开发者中心找到 Certificates,点击 All,然后点击右上角 + 号 2、点击 App Store and Ad Hoc 发布证书和开发者证书需要分别创建,开发者证书用于真机调试,发布证书用于提交到 AppStore 3、点击 Continue 4、点击 Continue 5、点击 choose File.. 选择创建好的证书请求文件:CertificateSigningRequest.certSigningRequest 文件,点击 Generate 6、点击 Download 下载创建好的发布证书(cer 后缀的文件),然后点击 Done,你创建的发布证书就会存储在帐号中 7、双击安装。如果安装不上,可以直接将证书文件拖拽到钥匙串访问的列表中 重点:一般一个开发者帐号创建一个发布证书就够了,如果以后需要在其他电脑上上架 App,只需要在钥匙串访问中创建 p12 文件,把 p12 文件安装到其他电脑上。这相当于给予了其他电脑发布 App 的权限。 4、创建 Provisioning Profiles 文件 1、在苹果开发者中心找到 Provisioning Profiles,点击 All,然后点击右上角 + 号 2、选择 App Store,点击 Continue 该流程也需要分别创建开发用的 PP 证书和发布的 PP 证书 3、在 App ID 这个选项栏里面找到你刚刚创建的:App IDs(Bundle ID) 类型的套装,点击 Continue 4、选择你刚创建的发布证书(或者生成 p12 文件的那个发布证书),点击 Continue 5、在 Profile Name 栏里输入一个名字(这个是 PP 文件的名字,可随便输入,在这里我用工程名字,便于分别),然后点击 Generate 注意:wildCard 格式的证书没有推送,PassCard 等服务的应用,慎重选择。因为 PP 证书的开发者证书需要真机调试,所以我们需要绑定真机,这里因为之前添加过一些设备,所以这里就可以直接全选添加,如果没有的话,需要将真机的 udid 复制出来在此添加。在发布 PP 文件中,是没有这一步的。 6、Download 生成的 PP 文件,然后点击 Done 双击就添加到 Xcode 中,这样在真机调试或者发布时,就可以分别有不同的 PP 证书与其对应。其实可以不用下载保存。 5、在 App Store 创建应用 1、回到苹果开发者中心的 Account,点击 iTunes Connect 2、点击我的 App 3、点击新建 iOS App 4、依次按提示填入对应信息,然后点击创建 5、依次把不同尺寸的 App 截图拉入到对应的里面 6、填入 App 简介 7、按提示依次输入 此时这个构建版本还没有生成,我们先把基本信息填写完毕,然后再进入 Xcode 中把项目打包发送过来。注意:填写完一定要点击右上角的保存。 不要忘记填写测试账号,否则会被拒的,而且一定要跟服务器同事说好,不要删除测试账号,否则同样被拒(联系号码 一定要+ 86 如:+86 13720329661) 6、打包上架 在 Xcode 中找到你刚刚下载的发布证书(后缀为 .cer)或者 p12 文件,和 PP 文件,双击,看起来没反应,但是他们已经加入到你的钥匙串中。如果之前步骤已操作过,可省略此步。 1、打开 Xcode,配置项目环境,点击 + 可以选择 Add Apple ID;点击 View Details 可以查看该 Apple Id 下的 Certificates 和 Provisioning Profile 证书文件,在这里你可以点击下载。在项目 Targets 下的 Identity 中,Team 选择对应的 Apple ID 即可 特别注意:这里填写的 Apple ID 不是你自己手机上创建的 Apple ID 一定要是开发者账号的账号和密码 2、选择模拟器为 iOS Device,按照下图提示操作 3、修改 .plist 文件,两个 .plist 文件都要修改 4、Archive 在线打包,在真机状态下选择 Product => Archive,如果不是真机状态下,Archive 会是灰色不可用的 5、打包之后会生成一个 ipa 文件 ,然后返回苹果开发者中心 => iTunes Connect => 我的 App(在构建版本处),点击 Application Loader 就会将其下载下来,然后通过该软件把 ipa 文件上传到 appstore 上 application Loader 上传出现的错误。解决方案:ERROR ITMS-90158:"The following URL schemes found in your app are not in the current format:[XXX]." 删除 schemes 中的XXX 路径 如下图 6、发送成功后返回到我的 App,刷新页面,在构建版本处就会有个 + 号,点击 + 号把发送过来的程序添加上去就行了 7、提交审核
前言 打包 ipa 的前提 证书的申请和设置和 “App 上架” 文章的一样 从第一步到第四步都是一样的。还有第六步的 1-3 都是一样的,从第四步开始变化。 1、Archive 在线打包 1、在真机状态下选择 Product => Archive,如果不是真机状态下,Archive 会是灰色不可用的 2、打包之后会生成一个 ipa 文件 ,然后返回苹果开发者中心 => iTunes Connect => 我的 App(在构建版本处),点击 Application Loader 就会将其下载下来,然后通过该软件把 ipa 文件上传到 appstore 上 application Loader 上传出现的错误。解决方案:ERROR ITMS-90158:"The following URL schemes found in your app are not in the current format:[XXX]." 删除 schemes 中的XXX 路径 如下图 2、通过 Payload 打包 1、在打包 ipa 的前提条件都弄好之后,Command+B 编译 2、然后按图操作 3、在桌面上新建一个文件夹名字叫 “Payload”,注意一个字母也不能少。并将上面的 APP 直接拷贝到这个文件夹下面,压缩这个文件夹,并将文件夹的后缀名,改正 “.ipa”。如下图: 3、通过 iTunes 打包 1、直接把刚刚的那个 .app,拖到你的 iTunes 里面。如下图: 2、在 Finder 里面显示: 3、生成 ipa 4、通过 Alcatraz 打包 如果没有安装 Xcode 插件管理工具 Alcatraz 的可以查看 Alcatraz 工具安装教程 1、在插件 Xcode 插件管理工具 Alcatraz 之上,插件名字叫:AMAppExportToIPA 。直接 ipa 就出来了然后安装 2、找到要打包的 app 然后点击 Export IPA 3、然后在桌面找到 AM_Builds 文件夹打开就是生成好的 ipa 文件 5、手机安装 ipa 文件 如果你打包的是测试的 ipa 文件那个如何将其安装到手机里面呢? 对于以上生成的所有的 ipa 包,都需要双击打开他们,在你的 iTunes 里面,安装你的这个应用包。如下图: 6、打包时 UUID 出错的解决方案 错误描述 Your build settings specify a provisioning profile with the UUID “XXXX”, however, no such provisioning profile was found. 解决方案 1、打开工程文件夹,找到 xxx.xcodeproj 的文件,右键点击 显示包内容 2、找到 project.pbxproj 文件,双击点开 3、使用 Command+F 在输入框输入你出错的 UUID,然后把含有该 UUID 的行,全部删除,然后保存并重新打开,最后在重新打包就 ok 了
前言 1、准备 开发者账号 自从 Xcode7 出来之后,一般的真机测试不需要开发者账号,也就不需要看这篇教程,只有 app 具有 “推送” 等功能的时候,要真机测试就必须要开发者账号和设置证书。苹果只是让你体验一下它的基本功能,要深入还是要花钱的。 待测试的项目 2、真机测试步骤 1) 创建 App ID 2) 创建证书请求文件(CSR 文件) 3) 根据 CSR 创建开发者证书(CER)(开发、测试用的 Develope 证书) 4) 添加设备(Devices) 5) 根据 Devices 创建 Provisioning Profiles 配置文件(PP文件) 6) 设置 Xcode 然后真机调试 3、重点 使用 P12 文件 使多台 Mac 进行真机调试(或者发布) 1、创建 App ID 1、打开苹果开发者中心,点击 “Account” 登录会员中心。 2、填写信息创建 app ID 第一个选项:明确的 app id 与项目中的 Bundle Identifier 相对应,如果你打算将应用程序中加入 Game Center,或在应用中使用应用内购买,进行数据保护,使用 iCloud,或者想要给你的应用程序一个唯一的配置文件,你就必须申请 Explicit App ID。 第二个选项:通用 app id 可以在所有不需要明确 id 的 app 中使用,淘宝上卖的真机调试证书就是这个 2、创建证书请求文件(CSR 文件) CSR 文件主要用于绑定你的电脑的 1、在 Mac 上,点开 LaunchPad,在其他中找到打开钥匙串访问 2、点击电脑左上角的 钥匙串访问 => 证书助理 => 从证书颁发机构请求证书 3、出现如下界面,选择存储到磁盘,点击继续 4、选择存储到桌面,存储 5、点击完成 6、在桌面上看到下面的文件,证书请求文件完成 3、根据 CSR 创建开发者证书(CER) 1、在苹果开发者中心找到 Certificates,点击 All,然后点击右上角 + 号 2、点击 Developement 中的 iOS App Developement 选项 3、点击 Continue 4、点击 Continue 5、点击 choose File.. 选择创建好的证书请求文件:CertificateSigningRequest.certSigningRequest 文件,点击 Generate 6、点击 Download 下载创建好的开发证书(cer 后缀的文件),然后点击 Done,你创建的开发证书就会存储在帐号中 7、双击安装。如果安装不上,可以直接将证书文件拖拽到钥匙串访问的列表中 4、添加设备 1、在苹果开发者中心找到 Devices,点击 All,然后点击右上角 + 号添加设备到开发者账号中,为制作 PP 文件做准备 Name:设备的描述 可以随便填 方便你记忆 UDID:设备的标号 2、获取 UDID(这里随便提供一种方法获取 UDID) 将 iPhone 手机插入到电脑上 ,打开 iTunes,然后按如图操作 3、填入 UDID 就 OK 了 5、根据 Devices 创建 Provisioning Profiles 配置文件(PP 文件) 1、在苹果开发者中心找到 Provisioning Profiles ,点击 All,然后点击右上角 + 号 2、选择 iOS App Developement,点击 Continue 3、在 App ID 这个选项栏里面找到你刚刚创建的:App IDs(Bundle ID) 类型的套装,点击 Continue 4、选择你刚创建的发布证书(或者生成 p12 文件的那个发布证书),点击 Continue 5、选择设备 注意:wildCard 格式的证书没有推送,PassCard 等服务的应用,慎重选择。因为 PP 证书的开发者证书需要真机调试,所以我们需要绑定真机,这里因为之前添加过一些设备,所以这里就可以直接全选添加,如果没有的话,需要将真机的 udid 复制出来在此添加。在发布的 PP 文件中,是没有这一步的。 6、在 Profile Name 栏里输入一个名字(这个是 PP 文件的名字,可随便输入,在这里我用工程名字,便于分别),然后点击 Generate 7、然后点击下载 ,将其下载下来 双击就添加到 Xcode 中,这样在真机调试或者发布时,就可以分别有不同的 PP 证书与其对应。其实可以不用下载保存 6、设置 Xcode 真机调试 1、设置 Bundle ID 和 申请的 appid 一致 2、设置 Debug 的 CER 证书 3、配置证书描述文件(PP 文件) 4、选择真机 进行真机调试 7、使用 P12 文件多台 Mac 进行真机调试 (或者发布) 1、为什么要使用 P12 文件 当我们用大于三个 mac 设备开发应用时,想要申请新的证书,如果在我们的证书里,包含了 3 个发布证书,2 个开发证书,可以发现再也申请不了开发证书和发布证书了(一般在我们的证书界面中应该只有一个开发证书,一个发布证书,没必要生成那么多的证书,证书一般在过期之后才会重新添加。) 这时候,再点击 “+” 时,就会发现点击不了开发和发布证书,也就是添加不了开发证书和发布证书了: 2、P12 文件能解决什么问题 为了不能添加证书的问题我们有 2 个解决方案 第一种方法—— “revoke”(不推荐): 将以前的证书 “revoke” 掉,然后重新生成一个新的证书。这种方法是可以的,但是会造成相应的 ProvisioningProfiles(PP 文件)失效,这是小问题。但是又要重新申请证书甚至描述文件很浪费时间,所以不提倡这种做法。 第二种方法—— “.p12”(推荐): 我们的每一个证书都可以生成一个 .p12 文件,这个文件是一个加密的文件,只要知道其密码,就可以供给所有的 mac 设备使用,使设备不需要在苹果开发者网站重新申请开发和发布证书,就能使用。 3、P12 文件是如何使用的 注意:一般 .p12 文件是给与别人使用的,本机必须已经有一个带秘钥的证书才可以生成 .p12 文件 导出一个带有私钥的证书(这里我选择调试证书也就是调试的 CER 证书,其实也可以是发布证书,只不过那就不用于调试而是用于上架了)。然后点击导出 填好名字和储存位置,点击储存 填写该 P12 文件证书的密码,点 “好” 然后生成 P12 文件 其实 P12 文件不仅是真机测试的时候用,上架的时候也会用,P12 文件的使用方法,调试和上架是一样的。最简单的理解就是:把 P12 文件当做 CER 文件使用,调试就当调试 CER,上架就当发布 CER 使用。 使用 调试:就是把该教程的第三步创建调试证书省略,将其换成 P12 文件即可。 上架:把 “App 上架” 文章的第三步创建发布证书省略,将其换成 P12 文件即可。
1、Analyze 使用 Xcode 自带的静态分析工具 Product -> Analyze(快捷键 command + shift + B)可以找出代码潜在错误,如内存泄露,未使用函数和变量等。 Analyze 主要分析以下四种问题: 1、逻辑错误:访问空指针或未初始化的变量等; 2、内存管理错误:如内存泄漏等,比如 ARC 下,内存管理不包括 core foundation; 3、声明错误:从未使用过的变量; 4、Api 调用错误:未包含使用的库和框架。 官方文档 Xcode 执行静态代码分析视频教程 2、分析结果处理 1、user-facing text should use localized string macro 面向用户的文本应该使用本地化的字符串宏。此为代码中配置了本地化,面向用户的应该用字符串宏,而我们直接赋值为汉字,因此此提示可以忽略。 2、instance variable used while 'self' is not set to the result of '[(super or self) init...] // 此方法提示错误 - (instancetype)initWithType:(FTFFavorateType)type { if (self == [super init]) { _type = type; } return self; } 修改为如下 - (instancetype)initWithType:(FTFFavorateType)type { if (self = [super init]) { _type = type; } return self; } 3、Value stored to ‘durationValue’ during its initialization is never read 在初始化过程中存储的 “持续时间值” 的值永远不会被读取 // 此段代码提示错误 NSMutableArray *datesArray = [[NSMutableArray alloc] init]; datesArray = [_onDemandDictionary objectForKey:key]; 这是因为 [NSMutableArray alloc] init] 初始化分配了内存,而判断语句里面 [_onDemandDictionary objectForKey:key] 方法也相当于初始化分配了内存,就是把初始化的一个新的可变数组赋值给之前已经初始化过的可变数组,看似没什么大问题,其实存在一个数据源却申请了两块内存的问题,已经造成了内存泄露。 修改为如下 NSMutableArray *datesArray = nil; datesArray = [_onDemandDictionary objectForKey:key]; 4、Potential leak of an object stored into 'imageRef' imageRef 对象有内存泄漏 + (UIImage*)getSubImage:(unsigned long)ulUserHeader { UIImage * sourceImage = [UIImage imageNamed:@"header.png"]; CGFloat height = sourceImage.size.height; CGRect rect = CGRectMake(0 + ulUserHeader*height, 0, height, height); CGImageRef imageRef = CGImageCreateWithImageInRect([sourceImage CGImage], rect); UIImage* smallImage = [UIImage imageWithCGImage:imageRef]; // CGImageRelease(imageRef); return smallImage; } 5、Analyze 逻辑错误监测 这种情况在 codereview 时也较难发现,可以借助 Analyze。 如上代码,当 Tag 不等于 1、2 和 3 的时候,就会出现很问题了。 Analyze 还给出了箭头提示:len is a garbage value。建议在声明变量时,同时进行初始化。 3、内存分析 3.1 静态内存分析 所谓静态内存分析,是指在程序没运行的时候,通过 Xcode 自带的静态分析工具 Product -> Analyze(快捷键 command + shift + B)对代码直接进行分析。根据代码的上下文的语法结构,让编译器分析内存情况,检查是否有内存泄露。 缺点:静态内存分析由于是编译器根据代码进行的判断, 做出的判断不一定会准确, 因此如果遇到提示, 应该去结合代码上文检查一下。 内存泄漏提示:Potential leak of an object stored into 'imageRef' imageRef 对象有内存泄漏 + (UIImage*)getSubImage:(unsigned long)ulUserHeader { UIImage * sourceImage = [UIImage imageNamed:@"header.png"]; CGFloat height = sourceImage.size.height; CGRect rect = CGRectMake(0 + ulUserHeader*height, 0, height, height); CGImageRef imageRef = CGImageCreateWithImageInRect([sourceImage CGImage], rect); UIImage* smallImage = [UIImage imageWithCGImage:imageRef]; // CGImageRelease(imageRef); return smallImage; } 3.2 动态内存分析 动态内存分析通过 Xcode 自带的动态分析工具 Xcode -> Product -> Profile(Leaks 工具)动态的对内存进行分析,大多时候只是堆内存的分析。 3.3 动态加载图片的内存分析 imageNamed 和 imageWithContentOfFile 方法的比较。 1、imageName 加载图片 a、当 imageview 对象销毁时候,图片对象不会随着一起销毁。 b、加载的图片占据的内存比较大。 c、相同的图片只会加载一份到内存中,如果同时使用,使用的是同一个图片对象。 2、imageWithContentOfFile 加载图片 a、当 imageView 对象销毁的时候,图片对象会随着一起销毁。 b、加载的图片占用的内存比较小。 c、相同的图片对象会多次加载到内存中,如果同时使用图片,使用的是不同的对象。 总结 imageName:如果一些图片在多个界面都会使用,并且图片较小,使用频率高,(图标/小的背景图)。 imageWithContentOfFile:只在一个地方使用,并且图片比较大,使用频率不高,(相册/版本新特性)。
前言 iOS 常见的几种架构: 标签式 Tab Menu 列表式 List Menu 抽屉式 Drawer 瀑布式 Waterfall 跳板式 Springborad 陈列馆式 Gallery 旋转木马式 Carousel 点聚式 Plus 1、标签式 优点: 1、清楚当前所在的入口位置 2、轻松在各入口间频繁跳转且不会迷失方向 3、直接展现最重要入口的内容信息 缺点: 功能入口过多时,该模式显得笨重不实用 2、列表式 优点: 1、层次展示清晰 2、可展示内容较长的标题 3、可展示标题的次级内容 缺点: 1、同级内容过多时,用户浏览容易产生疲劳 2、排版灵活性不是很高 3、只能通过排列顺序、颜色来区分各入口重要程度 3、抽屉式 优点: 1、兼容多种模式 2、扩展性好 缺点: 1、隐藏框架中其他入口 2、对入口交互的功能可见性(affordance)要求高 3.1 抽屉式架构简单实现 ViewController.m #import "ViewController.h" #import "QCMainViewController.h" #import "QCDrawerViewController.h" // 设定抽屉视图的宽度 #define DRAWER_VC_WIDTH ((self.view.bounds.size.width * 3) / 4) @interface ViewController () @property (nonatomic, strong) QCMainViewController *mainVC; @property (nonatomic, strong) UINavigationController *mainNVC; @property (nonatomic, strong) QCDrawerViewController *drawerVC; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 添加主视图 self.mainVC = [[QCMainViewController alloc] init]; self.mainNVC = [[UINavigationController alloc] initWithRootViewController:self.mainVC]; [self addChildViewController:self.mainNVC]; [self.view addSubview:self.mainNVC.view]; // 添加抽屉视图 self.drawerVC = [[QCDrawerViewController alloc] init]; self.drawerVC.view.frame = CGRectMake(-DRAWER_VC_WIDTH, 0, DRAWER_VC_WIDTH, self.view.bounds.size.height); [self addChildViewController:self.drawerVC]; [self.view addSubview:self.drawerVC.view]; // 抽屉视图显示/隐藏回调 __weak typeof(self) weakSelf = self; self.mainVC.myBlock = ^(BOOL isPush){ CGRect mainNVCFrame = weakSelf.self.mainNVC.view.bounds; CGRect drawerVCFrame = weakSelf.self.drawerVC.view.bounds; mainNVCFrame.origin.x = isPush ? DRAWER_VC_WIDTH : 0; drawerVCFrame.origin.x = isPush ? 0 : -DRAWER_VC_WIDTH; [UIView animateWithDuration:0.5 animations:^{ weakSelf.mainNVC.view.frame = mainNVCFrame; weakSelf.drawerVC.view.frame = drawerVCFrame; }]; }; } @end QCMainViewController.h #import <UIKit/UIKit.h> @interface QCMainViewController : UIViewController @property (nonatomic, copy) void (^myBlock)(BOOL); @end QCMainViewController.m #import "QCMainViewController.h" @interface QCMainViewController () @property (nonatomic, assign, getter = isPush) BOOL push; @end @implementation QCMainViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor yellowColor]; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"抽屉" style:UIBarButtonItemStylePlain target:self action:@selector(pushDrawer)]; // 功能测试 for (NSUInteger i = 0; i < 5; i++) { UIButton *btn = [[UIButton alloc] init]; [self.view addSubview:btn]; btn.frame = CGRectMake(20, 200 + i * 60, 100, 50); btn.tag = i +1; [btn setTitle:[NSString stringWithFormat:@"按钮 %li", i + 1] forState:UIControlStateNormal]; [btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal]; [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside]; } } // 功能测试 - (void)btnClick:(UIButton *)btn { NSLog(@"按钮 %li", btn.tag); } // 抽屉视图显示/隐藏 - (void)pushDrawer { self.push = !self.isPush; if (self.myBlock != nil) { self.myBlock(self.isPush); } } // 触摸手势抽屉视图显示/隐藏 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { if (self.isPush) { [self pushDrawer]; } } @end QCDrawerViewController.m #import "QCDrawerViewController.h" @interface QCDrawerViewController () @end @implementation QCDrawerViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor blueColor]; // 功能测试 for (NSUInteger i = 0; i < 5; i++) { UIButton *btn = [[UIButton alloc] init]; [self.view addSubview:btn]; btn.frame = CGRectMake(20, 200 + i * 60, 100, 50); btn.tag = i +1; [btn setTitle:[NSString stringWithFormat:@"功能 %li", i + 1] forState:UIControlStateNormal]; [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside]; } } // 功能测试 - (void)btnClick:(UIButton *)btn { NSLog(@"功能 %li", btn.tag); } @end 效果 3.2 抽屉式架构第三方框架实现 第三方框架 GitHub 地址 效果 4、瀑布式 优点: 1、浏览时产生流畅体验 缺点: 1、缺乏对整体内容的体积感,容易发生空间位置迷失 2、浏览一段时间后,容易产生疲劳感 5、跳板式 优点: 1、清晰展现各入口 2、容易记住各入口位置,方便快速找到 缺点: 1、无法在多入口间灵活跳转,不适合多任务操作 2、容易形成更深的路径 3、不能直接展现入口内容 4、不能显示太多入口次级内容 6、陈列馆式 优点: 1、直观展现各项内容 2、方便浏览经常更新的内容 缺点: 1、不适合展现顶层入口框架 2、容易形成界面内容过多,显得杂乱 3、设计效果容易呆板 7、旋转木马式 优点: 1、单页面内容整体性强 2、线性的浏览方式有顺畅感、方向感 缺点: 1、不适合展示过多页面 2、不能跳跃性地查看间隔的页面,只能按顺序查看相邻的页面 3、由于各页面内容结构相似,容易忽略后面的内容 8、点聚式 优点: 1、灵活 2、展示方式有趣 3、使界面更开阔 缺点: 1、隐藏框架中其他入口 2、对入口交互的功能可见性(affordance)要求高
1、Bundle 文件 Bundle 文件,简单理解,就是资源文件包。我们将许多图片、XIB、文本文件组织在一起,打包成一个 Bundle 文件。方便在其他项目中引用包内的资源。 Bundle 文件是静态的,也就是说,我们包含到包中的资源文件作为一个资源包是不参加项目编译的。也就意味着,bundle 包中不能包含可执行的文件。它仅仅是作为资源,被解析成为特定的 2 进制数据。 2、制作 Bundle 文件 1、新建 Bundle 项目 创建名为 SourcesBundle(最后要生成的 Bundle 文件名称)的工程,注意 Bundle 默认是 macOS 系统的,Xcode 高版本中需要在 macOS => Framework & Library 选项下找到。 2、修改 Bundle 配置信息 因为 Bundle 默认是 macOS 系统的,所有需要修改他的信息,修改成 iOS 系统。 设置 Build Setting 中的 COMBINE_HIDPI_IMAGES 为 NO,否则 Bundle 中的图片就是 tiff 格式了。 3、可选配置 作为资源包,仅仅需要编译就好,无需安装相关的配置,设置 Skip Install 为 YES。同样要删除安装路径 Installation Directory 的值。 该资源包的 pch 文件和 strings 文件是可以删除的。 4、添加文件 将资源文件或文件夹拖动到工程中的 SourcesBundle 文件夹下面。 5、编译生成 Bundle 文件 我们分别选择 Generic iOS Device 和任意一个模拟器各编译一次,编译完后,我们会看到工程中 Products 文件夹下的 SourcesBundle.bundle 由红色变成了黑色。 然后 show in finder,看看生成的文件。我们看到它为真机和模拟器都生成了 .bundle 资源文件。 选中 .bundle 文件右键 显示包内容,我们可以看到之前拖拽到工程中的资源文件都在其中。 3、使用 Bundle 文件 将生成的真机(Debug-iphoneos)Bundle 资源文件拖拽到需要使用的工程中。 1、加载 Bundle 中的 xib 资源文件 // 设置文件路径 NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"SourcesBundle" ofType:@"bundle"]; NSBundle *resourceBundle = [NSBundle bundleWithPath:bundlePath]; // 加载 nib 文件 UINib *nib = [UINib nibWithNibName:@"BundleDemo" bundle:resourceBundle]; NSArray *viewObjs = [nib instantiateWithOwner:nil options:nil]; // 获取 xib 文件 UIView *view = viewObjs.lastObject; view.frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, self.view.bounds.size.width - 40); [self.view addSubview:view]; 效果 2、加载 Bundle 中的图片资源文件 指定绝对路径的形式 UIImage *image = [UIImage imageNamed:@"SourcesBundle.bundle/demo2.jpg"]; 拼接路径的形式 NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"SourcesBundle" ofType:@"bundle"]; NSString *imgPath= [bundlePath stringByAppendingPathComponent:@"demo4"]; UIImage *image = [UIImage imageWithContentsOfFile:imgPath]; 宏定义的形式 #define MYBUNDLE_NAME @"SourcesBundle.bundle" #define MYBUNDLE_PATH [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:MYBUNDLE_NAME] #define MYBUNDLE [NSBundle bundleWithPath:MYBUNDLE_PATH] NSString *imgPath= [MYBUNDLE_PATH stringByAppendingPathComponent:@"demo4"]; UIImage *image = [UIImage imageWithContentsOfFile:imgPath]; 效果
1、动态库 & 静态库 什么是库: 库是程序代码的集合,是共享程序代码的一种方式。根据源代码的公开情况,库可以分为 2 种类型: 开源库: 公开源代码,能看到具体实现。 比如 SDWebImage 、 AFNetworking 闭源库: 不公开源代码,是经过编译后的二进制文件,看不到具体实现。 主要分为:静态库、动态库 静态库和动态库: iOS 中静态库和动态库的存在形式: 静态库:.framework 和 .a 动态库:.framework 和 .tbd(之前叫 .dylib) 静态库和动态库在使用上的区别: 静态库:链接时,静态库会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝。 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。 使用注意: 需要注意的是项目中如果使用了自制的动态库,不能被上传到 AppStore,审核无法通过。 从 iOS 7.0 开始,如果程序中使用到 Framework,不再需要添加引用,只需要 import 头文件就可以了,Xcode 会在编译的时候,自动添加相关的引用。 但是 .dylib 和 .tbd 动态库还是需要手动添加引用。 2、iOS 设备的架构 模拟器: iPhone4s-iPnone5:i386 iPhone5s-iPhone7 Plus:x86_64 真机: iPhone3gs-iPhone4s:armv7 iPhone5-iPhone5c:armv7s iPhone5s-iPhone6s:Plus:arm64 支持 armv7 的静态库可以在 armv7s 上正常运行。 3、.a 静态库的制作 1、创建工程. 先创建一个新的 Xcode 工程 Test,需要选择下面 Cocoa Touch Static Library 这个模板: 创建完成后是这个样子的: 2、创建文件。 我们把默认生成的 Test.h 和 Test.m 删掉,重新创建一个类 PrintString,在这个类里面添加一个单纯打印字符串的简单方法: 3、添加公开的头文件。 为了让使用者知道有哪些方法可以用,我们需要公开头文件,这里我们公开PrintString.h,将需要公开的头文件添加到 TARGETS => Build Phases => Copy Files 中。 4、设置支持所有架构。 我们需要把 Build Active Architecture Only 修改为 NO,否则生成的静态库就只支持当前选择设备的架构。在 TARGETS => Build Setting => Architecture => Build Active Architecture Only 处修改。 5、编译生成静态库文件。 我们分别选择 Generic iOS Device 和任意一个模拟器各编译一次,编译完后,我们会看到工程中 Products 文件夹下的 libTest.a 由红色变成了黑色,然后 show in finder,看看生成的文件: 我们看到它为真机和模拟器都生成了 .a 静态库。里面都包含我们选择公开的头文件。 我们来看看静态库支持的框架,命令为: $ lipo -info 静态库名字 我们看到,Debug-iphoneos 里面的静态库支持的架构有 armv7 和 arm64 所以它只能用于真机,在模拟器上会报错。Debug-iphonesimulator 里面的静态库支持的架构有 i386 和 x86_64,所以它只能用于模拟器,在真机上会报错。 6、合并真机和模拟器静态库文件。 如果想要让模拟器和真机通用一个静态库,我们可以使用终端命令来实现。命令格式为: $ lipo -create 第一个.a文件的绝对路径 第二个.a文件的绝对路径 -output 最终的.a文件路径 我们看到生成了一个新的 libTest.a 文件。这个静态库就支持所有模拟器和所有真机了。然后我们创建一个文件夹,把 .a 和头文件都放进去,我们最终需要使用的就是这个文件夹: 注意:为了开发方便,我们可以使用生成的通用静态库,但是最终上线的使用我们可以只导入真机的,这样工程的体积也会小一些。 7、使用生成的 .a 静态库。 新建一个工程,将上面的通用静态库拖进去,导入头文件,就可以使用里面的方法了。经过试验,我们生成的静态库在真机上和模拟器上都能成功打印字符串: 4、.frameworke 静态库的制作 1、创建工程. 先创建一个新的 Xcode 工程 LibTest,需要选择下面 Cocoa Touch Frameworke 这个模板: 创建完成后是这个样子的,我们可以看到,工程本身自带一个 LibTest.h 文件和一个 Info.plist 文件。 2、创建文件。 我们创建一个类 PrintString,添加一个单纯打印字符串的简单方法: 3、添加公开的头文件。 为了让使用者知道有哪些方法可以用,我们需要公开头文件,我们需要在 LibTest.h 文件中引入需要公开的头文件,并且将 TARGETS => Build Phases => Headers 中的 Project 中要暴露的头文件拖拽到 Pulic 里面,这里我们公开 PrintString.h : 注意:暴露出来的头文件中 import 的其他类也得添加到 public 中暴露出来。如果不想将 import 的类暴露出来,那么在头文件中用 @class 然后在对应的 .m 文件中再 import。 4、设置支持所有架构。 我们需要把 Build Active Architecture Only 修改为 NO,否则生成的静态库就只支持当前选择设备的架构。在 TARGETS => Build Setting => Architecture => Build Active Architecture Only 处修改。 5、设置生成静态库。 因为动态库也可以是以 framework 形式存在,所以需要设置,否则默认打出来的是动态库。将 TARGETS => Build Setting => Linking => Mach-o Type 设为 Static Library(默认为 Dynamic Library): 6、编译生成静态库文件。 我们分别选择 Generic iOS Device 和任意一个模拟器各编译一次,编译完后,我们会看到工程中 Products 文件夹下的 LibTest.framework 由红色变成了黑色,然后 show in finder,看看生成的文件: 我们看到它为真机和模拟器都生成了 LibTest.framework 静态库。 我们来查看静态库支持的框架,命令为: $ lipo -info framework下的二进制文件名字 我们看到,Debug-iphoneos 里面的静态库支持的架构有 armv7 和 arm64 所以它只能用于真机,在模拟器上会报错。Debug-iphonesimulator 里面的静态库支持的架构有 i386 和 x86_64,所以它只能用于模拟器,在真机上会报错。 7、合并真机和模拟器静态库文件。 如果想要让模拟器和真机通用一个静态库,我们可以使用终端命令来实现。framework 静态库合并的不是 framework,而是 framework 下的二进制文件 LibTest,命令为: $ lipo -create 第一个framework下二进制文件的绝对路径 第二个framework下二进制文件的绝对路径 -output 最终的二进制文件路径 我们看到生成了一个新的 LibTest 文件,然后将任何一个 framework 中的二进制文件替换成合并后的二进制文件,然后把 framework 添加到要使用的项目中即可使用。 8、使用生成的 .framework 静态库。 新建一个工程,将静态库拖进去,导入头文件,就可以使用里面的方法了。经过试验,我们生成的静态库在真机上和模拟器上都能成功打印字符串: 注意:如果静态库中有 category 类,则在使用静态库的项目配置 TARGETS => Build Setting => Linking => Other Linker Flags 中需要添加参数 -ObjC 或者 -all_load。如果创建的 framework 类中使用了 .tbd,则需要在实际项目中导入 .tbd 动态库。 5、运行调试静态库 如果你是开发静态库的人,你会发现上面的方法只是制作静态库,并没有办法运行看效果和调试 bug,这时候我们可以这样. 1、创建工程。 新建一个专门用来开发静态库的正常工程 Test: 2、添加静态库的 target。 添加一个静态库的 target。 我们看到它生成了几样东西: 一个 framework 的 target:在这里面修改静态库的配置们,例如支持的架构、要暴露的头文件们和 Mach-O 的配置。 一个 LibTest 文件夹:静态库里面的类们都放在这里面。 Products 文件夹下面的 LibTest.framework:在这里 show in finder 找到编译后生成的静态库。 3、开发调试代码 向 LibTest 中添加文件,并设置要暴露的头文件们、支持的架构和 Mach-O 的配置。 在 Test 中引入头文件和测试代码,编译运行,我们看到程序可以正常运行,并可以在动态库里面断点运行,方便我们调试。 4、编译生成静态库文件。 确保代码没问题后,选择对应的 target 编译生成静态库文件。 5、后面的过程就与上面一样了。
前言 什么是适配: 适应、兼容各种不同的情况。 iOS 开发中,适配的常见种类: 1)系统适配, 针对不同版本的操作系统进行适配。 2)屏幕适配,针对不同大小的屏幕尺寸进行适配。 iPhone 的尺寸:3.5 inch、4.0 inch、4.7 inch、5.5 inch 。 iPad 的尺寸:7.9 inch、9.7 inch、12.9 inch 。 屏幕方向:竖屏、横屏。 1、系统适配 Objective-C // 获取系统版本 float systemVersion = [UIDevice currentDevice].systemVersion.floatValue; // 判断系统版本 if ([UIDevice currentDevice].systemVersion.floatValue > 9.0) { // iOS 9 及其以上系统运行 } else { // iOS 9 以下系统系统运行 } Swift // 获取系统版本 let systemVersion:Float = NSString(string: UIDevice.current.systemVersion).floatValue // 判断系统版本 if NSString(string: UIDevice.current.systemVersion).floatValue > 9.0 { // iOS 9 及其以上系统运行 } else { // iOS 9 以下系统系统运行 } // 判断系统版本 if #available(iOS 9.0, *) { // iOS 9 及其以上系统运行 } else { // iOS 9 以下系统系统运行 } 2、屏幕适配 2.1 屏幕适配的发展历史 iPhone3GS iPhone4 没有屏幕适配可言,全部用 frame、bounds、center 进行布局。 很多这样的现象:坐标值、宽度高度值全部写死。 UIButton *btn1 = [[UIButton alloc] init]; btn1.frame = CGRectMake(0, iPhone3GS0, 320 - b, 480 - c); iPad 出现、iPhone 横屏 出现 Autoresizing 技术,让横竖屏适配相对简单,让子控件可以跟随父控件的行为自动发生相应的变化。 前提:关闭 Autolayout 功能。 局限性:只能解决子控件跟父控件的相对关系问题,不能解决兄弟控件的相对关系问题。 iOS 6.0(Xcode 4)开始 出现了 Autolayout 技术。 从 iOS 7.0 (Xcode 5) 开始,开始流行 Autolayout。 2.2 Autoresizing 2.2.1 Storyboard/Xib 中使用 关闭 Autolayout 功能 在 SB 的 Show the File Inspector 选项卡中取消对 Use Auto Layout 和 UseSize Classes 的勾选。 关闭 Autolayout 后,SB 的 Show the Size Inspector 选项卡中将出现 Autoresizing 设置模块,如下图。 此设置模块左侧方框内为设置选项,右侧矩形为设置效果预览。 需要在子视图上设置。 小方框四周的四个设置线,选中时,子视图与父视图的边距将保持不变。 左和右、上和下,只能二选一,若同时选中,只有左和上起作用。 小方框内部的两个线,选中时,子视图的宽或高将随父视图的变化而变化。 设置示例: 示例 1: 设置子视图在父视图的右下角。 将子视图放在父视图的右下角。 设置子视图的 Autoresizing 右和下选项线。 设置效果。 子视图与父视图的右和下边距保持不变。 子视图的宽和高保持不变。 示例 2: 设置子视图在父视图的下边,且宽度与父视图的宽度相等。 将子视图放在父视图的下边。 设置子视图的 Autoresizing 下和内部小方框的宽度选项线。 设置效果。 子视图与父视图的下和左右边距保持不变。 子视图的高保持不变。 子视图的宽随父视图的变化而变化。 2.2.2 纯代码中使用 Objective-C 子视图设置选项: UIViewAutoresizingNone = 0, // 不跟随 UIViewAutoresizingFlexibleLeftMargin = 1 << 0, // 左边距 随父视图变化,右边距不变 UIViewAutoresizingFlexibleRightMargin = 1 << 2, // 右边距 随父视图变化,左边距不变 UIViewAutoresizingFlexibleTopMargin = 1 << 3, // 上边距 随父视图变化,下边距不变 UIViewAutoresizingFlexibleBottomMargin = 1 << 5 // 下边距 随父视图变化,上边距不变 UIViewAutoresizingFlexibleWidth = 1 << 1, // 宽度 随父视图变化,左右边距不变 UIViewAutoresizingFlexibleHeight = 1 << 4, // 高度 随父视图变化,上下边距不变 设置示例: 示例 1: 设置子视图在父视图的右下角。 UIView *blueView = [[UIView alloc] init]; blueView.backgroundColor = [UIColor blueColor]; CGFloat x = self.view.bounds.size.width - 100; CGFloat y = self.view.bounds.size.height - 100; blueView.frame = CGRectMake(x, y, 100, 100); // 设置父视图是否允许子视图跟随变化 /* default is YES */ self.view.autoresizesSubviews = YES; // 设置子视图的跟随效果 /* 子视图的左边距和上边距随父视图的变化而变化,即右边距和下边距保持不变 */ blueView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin; [self.view addSubview:blueView]; 设置效果。 子视图与父视图的右和下边距保持不变。 子视图的宽和高保持不变。 示例 2: 设置子视图在父视图的下边,且宽度与父视图的宽度相等。 UIView *blueView = [[UIView alloc] init]; blueView.backgroundColor = [UIColor blueColor]; CGFloat w = self.view.bounds.size.width; CGFloat y = self.view.bounds.size.height - 100; blueView.frame = CGRectMake(0, y, w, 100); // 设置父视图是否允许子视图跟随变化 /* default is YES */ self.view.autoresizesSubviews = YES; // 设置子视图的跟随效果 /* 子视图的宽度和上边距随父视图的变化而变化,即左右边距和下边距保持不变 */ blueView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; [self.view addSubview:blueView]; 设置效果。 子视图与父视图的下和左右边距保持不变。 子视图的高保持不变。 子视图的宽随父视图的变化而变化。 2.3 Autolayout 使用详情见 AutoLayout 文档。 3、App 图标和启动图片 3.1 App 图标设置 App 图标尺寸 App 图标设置 默认从 AppIcon 中加载 App 图标。 20pt 表示 20 个点,即 2x 图片的像素为 (20 * 2) * (20 * 2) 像素,3x 图片的像素为 (20 * 3) * (20 * 3) 像素。 3.2 启动图片设置 启动图片尺寸 启动图片设置 Launch Images Sources :从指定的位置加载启动图片。 Launch Screen Files :默认,从指定的文件(xib 或 sb 文件)加载启动屏幕(启动图片)。 修改为从指定的位置加载启动图片,清除 Launch Screen Files 项内容,点击 Use Asset Catalog... ,按照默认设置,点击 Migrate 。 在 Assets.xcassets 中将自动添加 LaunchImage(或者 Brand Assets)。 Retina HD 5.5 为 5.5 寸屏的设备,Retina HD 4.7 为 4.7 寸屏的设备,Retina HD 4 为 4.0 寸屏的设备;Portrait 为竖屏,Landscape 为横屏。 4、iOS 设备各种尺寸 4.1 iOS 设备尺寸 4.2 Resolutions 分辨率 4.3 Displays 显示 4.4 Dimensions App 图标尺寸 4.5 Common Design Elements 常见控件尺寸 4.6 Icons 控件图标尺寸 4.7 Default Font Sizes iPhone 5/5C/5S/6 控件字体大小 4.8 Default Font Sizes iPhone 6 Plus 控件字体大小 4.9 Size Classes For Adaptive Layout
1、国际化 开发的移动应用更希望获取更多用户,走向世界,这就需要应用国际化,国际化其实就是多语言,系统会根据当前设备的语言环境来识别 App 中使用中文还是英文。 2、应用内容国际化 1、新建一个名为 Localizable.strings 的资源文件。 2、点击 Localizable.strings 文件,在右侧属性选择器中可以看到有个按钮 Localize...。 3、点击 Localize... 按钮,如果没做过国际化处理,选项只有 Base 和 English,选择 English 点击 Localize 按钮。 4、点击工程根目录,并选择 PROJECT,然后选择 Info 选项卡,底部我们就看到了Localizations,点击 “+” 号选择一门语言(比如 Chinese(Simplified))。这时看 Localizable.strings 文件下有两个文件 Localizable.strings(English) 和 Localizable.strings(Chinese(Simplified))。 2.1 代码国际化 在国际化文件中以键值的方式定义需要国际化的字符串。 "代码中使用的字符串" = "想要显示的字符串"; 5、在 Localizable.strings(English) 文件中添加需要国际化的字符串的英文名称。 "language" = "english"; "button" = "button"; 6、在 Localizable.strings(Chinese(Simplified)) 文件中添加需要国际化的字符串的中文名称。 "language" = "中文"; "button" = "按钮"; 7、在需要使用国际化的地方使用 NSLocalizedString(key, comment) 调用名为 Localizable.strings 的文件中定义的 Key 值。系统会根据当前设备的语言环境来识别使用中文还是英文。 NSLog(@"%@\n\n", NSLocalizedString(@"language", nil)); NSLog(@"%@", NSLocalizedString(@"button", nil)); 如果你的 strings 文件名字不是 Localizable 而是自定义的话,如 qian.strings,那么你就得使用 NSLocalizedStringFromTable() 来读取本地化字符串。 NSLocalizedStringFromTable(@"language", @"qian", nil) 2.2 Storyboard/Xib 国际化 1 5、在 Storyboard 文件的 Localization 选项中选择 English 和 Chinese(Simplified),并选择 Interface Builder Storyboard 模式,去掉 Base。此时可看到 Main.storyboard 下有两个文件 Main.storyboard(Chinese(Simplified)) 和 Main.storyboard(English)。 6、在 Main.storyboard(Chinese(Simplified)) 文件中设置 Storyboard 上控件的中文名称。 7、在 Main.storyboard(English) 文件中设置 Storyboard 上控件的英文名称。 8、运行程序,系统会根据当前设备的语言环境来识别使用中文还是英文。注意使用这种方式设置完国际化后,在 Storyboard 新添加的控件不会出现在国际化文件中,建议最后进行国际化设置。 2.3 Storyboard/Xib 国际化 2 5、在 Storyboard 文件的 Localization 选项中添加 English 和 Chinese(Simplified),并选择 Localizable Strings 模式。此时可看到 Main.storyboard 下有三个文件 Main.storyboard(Base)、Main.strings(Chinese(Simplified)) 和 Main.strings(English)。 6、在 Main.strings(Chinese(Simplified)) 文件中设置 Storyboard 上控件的中文名称。 7、在 Main.strings(English) 文件中设置 Storyboard 上控件的英文名称。 8、运行程序,系统会根据当前设备的语言环境来识别使用中文还是英文。注意使用这种方式设置完国际化后,在 Storyboard 新添加的控件不会出现在国际化文件中,建议最后进行国际化设置。 3、应用名国际化 1、新建一个名为 InfoPlist.strings 的资源文件。 2、点击 InfoPlist.strings 文件,在右侧属性选择器中可以看到有个按钮 Localize...。 3、点击 Localize... 按钮,如果没做过国际化处理,选项只有 Base 和 English,选择 English 点击 Localize 按钮,再像上面一样添加上其他语言。如果做过国际化,再选中 Chinese(Simplified) 语言。 4、在 InfoPlist.strings(English) 文件中添加应用的英文名称。 CFBundleDisplayName = "App Name"; 5、在 InfoPlist.strings(Chinese(Simplified)) 文件中添加应用的中文名称。 CFBundleDisplayName = "App 名称"; 6、运行程序,系统会根据当前设备的语言环境来识别使用中文还是英文。 4、图片国际化 这里又分两种方法,第一种和国际化字符串方法类似,把中英文图片的名字分别存到中英文对应的 strings 文件,然后通过 NSLocalizedString 来获取图片名称。 1、Localizable.strings(english) 文件中加入: "BtnCancel" = "BtnCancelEn.png"; 2、Localizable.strings(chinese) 文件中加入: "BtnCancel" = "BtnCancelCn.png"; 3、然后在代码中使用 NSLocalizedString 来获取图片名称: UIImage *images = [UIImage imageNamed:NSLocalizedString(@"BtnCancel", nil)] 第二种就比较正规化了。 1、将图片文件拖到项目中(不要放到 Assets.xcassets 中),点中你要国际化的图片,如 “icon.png”,在右侧属性选择器中可以看到有个按钮 Localize...。 2、点击 Localize... 按钮,如果没做过国际化处理,选项只有 Base 和 English,选择 English 点击 Localize 按钮,再像上面一样添加上其他语言。如果做过国际化,再选中 Chinese(Simplified) 语言。 3、在图片左边就会出现一个倒三角,点开就会出现(english)和(chinese)的 2 张图,并且在项目文件夹中会出现 en.lproj 文件和 zh-Hans.lproj 文件;en.lproj 文件存放的是英文版图片,zh-Hans.lproj 存放的是中文版图片,中英文图片名字一样,我们在文件夹中直接替换图片就可以了,最后使用时直接使用正常名字就行了,如:“icon.png”。 5、其他文件国际化 国际化其他文件和国际化图片第二种方法类似,先在 Localization 中添加语言,然后把对应版本拷贝到 en.lproj 和 zh-Hans.lproj 文件夹中,最后引用就行了。
1、Git Git 是用 C 语言开发的分布版本控制系统。版本控制系统可以保留一个文件集合的历史记录,并能回滚文件集合到另一个状态(历史记录状态)。另一个状态可以是不同的文件,也可以是不同的文件内容。举个例子,你可以将文件集合转换到两天之前的状态,或者你可以在生产代码和实验性质的代码之间进行切换。文件集合往往被称作是 “源代码”。在一个分布版本控制系统中,每个人都有一份完整的源代码(包括源代码所有的历史记录信息),而且可以对这个本地的数据进行操作。分布版本控制系统不需要一个集中式的代码仓库。 当你对本地的源代码进行了修改,你可以标注他们跟下一个版本相关(将他们加到 index 中),然后提交到仓库中来(commit)。Git 保存了所有的版本信息,所以你可以转换你的源代码到任何的历史版本。你可以对本地的仓库进行代码的提交,然后与其他的仓库进行同步。你可以使用 Git 来进行仓库的克隆(clone)操作,完整的复制一个已有的仓库。仓库的所有者可以通过 push 操作(推送变更到别处的仓库)或者 Pull 操作(从别处的仓库拉取变更)来同步变更。 Git 支持分支功能(branch)。如果你想开发一个新的产品功能,你可以建立一个分支,对这个分支的进行修改,而不至于会影响到主支上的代码。 1)Git 术语: 术语 定义 Repository:仓库 一个仓库包括了所有的版本信息、所有的分支和标记信息.在 Git 中仓库的每份拷贝都是完整的。仓库让你可以从中取得你的工作副本。 Branches:分支 一个分支意味着一个独立的、拥有自己历史信息的代码线(code line)。可以从已有的代码中生成一个新的分支,这个分支与剩余的分支完全独立。默认的分支往往是叫 master。用户可以选择一个分支,选择一个分支叫做 checkout。 Tags:标记 一个标记指的是某个分支某个特定时间点的状态。通过标记,可以很方便的切换到标记时的状态。 Commit:提交 提交代码后,仓库会创建一个新的版本。这个版本可以在后续被重新获得。每次提交都包括作者和提交者,作者和提交者可以是不同的人。 URL URL 用来标识一个仓库的位置。 Revision:修订 用来表示代码的一个版本状态。Git 通过用 SHA1 hash 算法表示的 id 来标识不同的版本。每一个 SHA1 id 都是 160 位长,16 进制标识的字符串。最新的版本可以通过 HEAD 来获取。之前的版本可以通过 "HEAD~1" 来获取,以此类推。 2)Git 常用命令: 全局配置 # 告诉 git 你是谁 $ git config --global user.name "姓名" # 告诉 git 怎么联系你 $ git config --global user.email "xxx@qq.com" # 查看配置信息 $ git config -l 初始化代码仓库 # 初始化代码库 $ git init # 初始化空白的代码仓库 $ git init --bare // 协同开发使用 # 将所有变化添加到暂存区 $ git add . # 将暂存区内容提交至代码库 $ git commit -m "注释内容" # 修改最后一次提交的注释 $ git commit --amend 查看信息 # 查看所有文件状态 $ git status # 查看指定文件的状态 $ git status 文件名 # 查看版本库日志 $ git log // 按字母 q 可以退出(关闭中文输入法) # 查看指定文件的修订记录 $ git log 文件名 版本回撤 # 回撤到上一个版本 $ git reset --hard HEAD^ # 回撤到上上一个版本 $ git reset --hard HEAD^^ # 切换到任意版本 $ git reset --hard 版本号(前6位) # 撤销某一个文件当前的修改 $ git checkout 文件名 # 查看分支引用记录 $ git reflog // 能够查阅所有的版本号 本地分支操作 # 查看本地分支 $ git branch # 创建本地分支 $ git branch <name> // 不会自动切换分支 # 创建新分支并立即切换到新分支 $ git checkout -b <name> # 切换分支 $ git checkout <name> # 合并分支 $ git merge <name> # 删除已经合并过的分支 $ git branch -d <name> // 没有合并的分支不能删除,如果要强行删除分支,可以使用 -D 选项 # 删除没有与远程分支对应的本地分支 $ git fetch -p # 重命名本地分支 $ git branch -m <oldName> <newName> 远程操作 # 将远程代码库克隆到本地 $ git clone url # 将本地修改内容推送到远程代码仓库 $ git push # 将远程代码库的变化更新到本地 $ git pull # 查看远程分支 $ git branch -r $ git branch -a # 创建远程分支 $ git push origin <name> // 本质上是将本地的分支 push 到远程 # 删除远程分支 $ git push origin --delete <branchName> $ git push origin :<branchName> // 推送一个空分支到远程分支,其实就相当于删除远程分支 # 获取远程 tag $ git fetch origin tag <tagname> # 把本地 tag 推送到远程 $ git push --tags # 删除远端 tag $ git push origin --delete tag <tagName> $ git tag -d <tagname> $ git push origin :refs/tags/<tagname> // 推送一个空 tag 到远程 tag 3)Git 常见问题: UserInterfaceState.xcuserstate 文件频繁更新 1> 退出 Xcdoe,打开终端(Terminal),进入到你的项目目录下。 2> 在终端键入 git rm --cached <YourProjectName>.xcodeproj/project.xcworkspace/xcuserdata/<YourUsername>.xcuserdatad/UserInterfaceState.xcuserstate 终端返回:rm 'QFormDataExample.xcodeproj/project.xcworkspace/xcuserdata/JHQ0228.xcuserdatad/UserInterfaceState.xcuserstate' 3> 提交更新,在终端键入 git commit -m "Removed file that shouldn't be tracked" 终端返回:<master 734ff0a> Removed file that shouldn't be tracked 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 QFormDataExample.xcodeproj/project.xcworkspace/xcuserdata/JHQ0228.xcuserdatad/UserInterfaceState.xcuserstate 4> 在 .gitignore 文件中加入如下几行,或者在 GitHub Desktop 中右击 UserInterfaceState.xcuserstate 更新,选择 Ignore all .xcuserstate files *.xcuserstate project.xcworkspace xcuserdata UserInterfaceState.xcuserstate project.xcworkspace/ xcuserdata/ UserInterface.xcuserstate 5> 提交更新,重新打开 Xcode commit, push。 一次提交的文件太大 提交后提示 fatal: recursion detected in die handler 或者 出现 POST git-receive-pack (chunked) 问题原因:http.postBuffer 默认上限为 1M 所致。 在 git 的配置里将 http.postBuffer 变量改大一些即可,比如将上限设为 500M: git config --global http.postBuffer 524288000 在哪里执行以上命令呢? 打开 git bash 命令行工具。 注意要加上 --global。网上很多资料都没加这个参数。不加执行的话会报以下错误的: error:could not lock config file .git/config: no such file or directory. 1> 使用 Git 提交代码 进入到要提交的代码的目录,里面包含 .git 文件夹,输入指令 git config http.postBuffer 524288000 2> 使用 SourceTreee 提交代码 如图按照顺序依次点击在最后一步增加 [http] postBuffer = 524288000 3> 使用 TortoiseGit 右键 TortoiseGit--settings--Git--Edit systemwide gitconfig--把 postBuffer 的值修改为 524288000 git ssh失效解决办法 报错信息 git.exe clone --progress -v "https://git.duapp.com/appiddfged879rf" "F:\bae\yelp" Cloning into 'F:\bae\yelp'... fatal: unable to access 'https://git.duapp.com/appiddfged879rf/': SSL certificate problem: unable to get local issuer certificate 最简单的解决方法是执行下面的命令,然后重新执行 git clone 命令: git config --global http.sslVerify false 2、Git 桌面化管理工具 GitHub Desktop is a seamless way to contribute to projects on GitHub and GitHub Enterprise. Available for Mac and Windows. 下载地址: GitHub Desktop.app
1、CocoaPods CocoaPods 是一个负责管理 iOS 项目中第三方开源库的工具。CocoaPods 的项目源码在 Github 上管理。该项目开始于 2011 年 8 月 12 日,在这两年多的时间里,它持续保持活跃更新。开发 iOS 项目不可避免地要使用第三方开源库,CocoaPods 的出现使得我们可以节省设置和更新第三方开源库的时间,在 iOS 开发中经常会用到第三方库如 AFNetworking,ASIHttpRequest 等,在使用第三方库时,你除了要导数源码外,但是,集成这些依赖库需要我们手动去配置,还有当这些第三方库发生了更新,还需要手动去更新项目。这就显得非常麻烦。有麻烦自然有解决办法,CocoaPods 就是为了解决这个问题而生的。通过 CocoaPods,我们可以将第三方的依赖库统一管理起来,配置和更新只需要通过简单的几行命令即可完成。 1)安装: # 查看 Ruby 源 $ gem sources –l # 添加新的 Ruby 源 $ sudo gem sources -a https://ruby.taobao.org/ # 移除现有 Ruby 默认源 $ sudo gem sources -r https://rubygems.org/ # 安装 cocoapods # 苹果系统升级 OS X EL Capitan 后改为:$ sudo gem install -n /usr/local/bin cocoapods $ sudo gem install cocoapods # 设置/更新本地 cocoapods 仓库 $ pod setup 2)使用: # 新建工程,并在终端用 cd 指令打开到工程文件夹内 # 搜索第三方框架 $ pod search 第三方框架 # 新建文件,写入以下内容并保存 $ vim Podfile platform :ios, '8.0' target '项目名称' do // 最新版本的需要加该句 pod 'AFNetworking', '~> 3.1.0' // 可以直接从搜索到的第三方框架中复制 end # 上一步操作可以直接用下面的指令一步完成 $ echo -e "target '项目名称' do\npod 'AFNetworking', '~> 3.1.0'\nend" > Podfile # 安装/下载第三方框架 $ pod install # 添加/下载第三方框架 pod 'AFNetworking', '~> 3.1.0’ // 升级到最新版本 pod 'SDWebImage', '~> 3.8.1’ // 下载并添加进工程中 $ pod update 3)pod 常用命令: .podspec 文件是 用于初始化本地第三方库的 spec 描述文件,所有的 spec 文件存都存放在 ~/.cocoapods 目录中。 # 设置/更新本地 cocoapods 仓库 $ pod setup # 安装/下载第三方框架 $ pod install # 添加/下载第三方框架 $ pod update # 列出本地 cocoapods 仓库所有可用的第三方框架 $ pod list # 在本地 cocoapods 仓库中搜索名称为 query 的第三方框架 $ pod search query # 更仔细的搜索,该命令不但搜索类库的名称,同时还搜索类库的描述文本,所以搜索速度也相对慢一些。 $ pod search --full query # 更新本地 cocoapods 仓库中第三方框架的描述文件 # pod list 和 pod search 命令只搜索存在于本地 ~/.cocoapods 文件夹的所有第三方框架,并不会连接到远程服务器 $ pod repo update master $ pod repo update --verbose // 更新时显示详细信息 # 创建一个 podspec 文件 $ pod spec create Name.podspec # 注册 trunk # EmailAddr:邮箱地址,userName:用户名,--verbose:输出调试信息 $ pod trunk register EmailAddr 'userName' --verbose # 向服务器查询自己的注册信息 $ pod trunk me 输出如下信息就表示注册成功: - Name: QianChia - Email: jhqian0228@icloud.com - Since: July 17th, 06:26 - Pods: - QConnectionDownloader - QFormData - QHashString - QSessionDownloader - QWebImage - Sessions: - July 17th, 06:26 - November 23rd, 01:33. IP: 43.225.238.143 # 验证 podspec 文件是否合法 $ pod lib lint Name.podspec # 通过 trunk 推送 podspec 文件 $ pod trunk push Name.podspec # Usage: $ pod COMMAND CocoaPods, the Cocoa library package manager. # Commands: + cache Manipulate the CocoaPods cache + deintegrate Deintegrate CocoaPods from your project + env Display pod environment + init Generate a Podfile for the current directory + install Install project dependencies according to versions from a Podfile.lock + ipc Inter-process communication + lib Develop pods + list List pods + outdated Show outdated project dependencies + plugins Show available CocoaPods plugins + repo Manage spec-repositories + search Search for pods + setup Setup the CocoaPods environment + spec Manage pod specs + trunk Interact with the CocoaPods API (e.g. publishing new specs) + try Try a Pod! + update Update outdated project dependencies and create new Podfile.lock # Options: --silent Show nothing --version Show the version of the tool --verbose Show more debugging information --no-ansi Show output without ANSI codes --help Show help banner of specified command 4)常见问题: 问题 1: Error fetching http://ruby.taobao.org/: bad response Not Found 404 (http://ruby.taobao.org/specs.4.8.gz) 解决方案:把安装流程中 $ gem sources -a http://ruby.taobao.org/ 改为 $ gem sources -a https://ruby.taobao.org/ 问题 2: ERROR: While executing gem ... (Errno::EPERM) Operation not permitted - /usr/bin/pod 解决方案:苹果系统升级 OS X EL Capitan 后会出现的插件错误,将安装流程中的 $ sudo gem install cocoapods 改为 sudo gem install -n /usr/local/bin cocoapods 问题 3: ERROR: Error installing cocoapods: activesupport requires Ruby version >= 2.2.2. 解决方案:输入 $ ruby -v 查看 ruby 版本:ruby 2.0.0p648 (2015-12-16 revision 53162) [universal.x86_64-darwin15], 版本过低,使用 RVM 对 Ruby 进行升级。 问题 4: [!] Unable to satisfy the following requirements: - `AVOSCloud (~> 3.1.6.3)` required by `Podfile` Specs satisfying the `AVOSCloud (~> 3.1.6.3)` dependency were found, but they required a higher minimum deployment target. 解决方案:安装流程中的 Podfile 文件中 platform:ios, ‘6.0’ 后边的 6.0 是平台版本号 ,一定要加上。 问题 5: [!] The dependency `AFNetworking (~> 3.1.0)` is not used in any concrete target. 解决方案:百度上很多旧版本输入的类容: platform :ios, '8.0' pod 'AFNetworking', '~> 2.0' 现在版本升级官方给的文档是: platform :ios, '8.0' target '项目名称' do pod 'AFNetworking', '~> 3.1.0' end 问题 6: [!] Your Podfile has had smart quotes sanitised. To avoid issues in the future, you should not use TextEdit for editing it. If you are not using TextEdit, you should turn off smart quotes in your editor of choice. 解决方案:不要使用文本编辑去编辑 Podfile,使用 Xcode 编辑,或者使用终端敲命令去编辑。 问题 7: /usr/local/bin/pod update env: ruby_executable_hooks: No such file or directory 在 Xcode 的 CocoaPods 插件中使用 pod update 出现以上提示。 解决方案:在终端里输入 $ gem env 找到 SHELL PATH,修改 Xcode 的 cocoapods 插件里 GEM_PATH 选项为上面得到的路径, 挨着试试总会成功的。 问题 8: 有一些库编译时候会有警告。但是作为一个有洁癖的人呢不想看见这些。 解决方案:可以在 platform :ios, ‘x.0’ 的后面加入这句,这样编译这些第三方库的时候就没有那些烦人的小警告了。 inhibit_all_warnings! 问题 9: 但是有一个库 ReactiveCocoa。当关闭所有警告的时候。它就编译不过了。 解决方案:对他单独设置打开编译警告就好了。 pod 'ReactiveCocoa', '~> 2.1.8', :inhibit_warnings => true 问题 10: 如果有多个 Targets 需要 pod 的库怎么办。 解决方案:Podfile 的头部加入以下代码,AAAAA 和 BBBBB 都是你 target 的名字,这样不同的 target 都会有 pod 库了。 主要是用来解决 Unit Test 需要 pod install 一些库的问题。 link_with ['AAAAA', 'BBBBB'] 问题 11: [!] The `master` repo is not a git repo. 原因:修改了 Xcode.app 的路径后,找不到 Xcode 解决:终端执行即可 sudo xcode-select -switch /Applications/Developer/Xcode.app(Xcode 实际路径) 问题 12: [!] The specified path `QExtension.podspec` does not point to an existing podspec file. 原因:没有进入到 .podspec 文件所在的文件夹 解决:cd 进入到 .podspec 文件所在的文件夹 问题 13: 创建工程使用 cocoapods 时没有出现 xcworkspace 文件解决方法。 // 更新 cocoapods sudo gem install -n /usr/local/bin cocoapods // 在工程目录下 pod install 2、gem 常用命令 # 查看 Ruby 源 $ gem sources –l # 添加源 $ sudo gem sources -a https://ruby.taobao.org/ # 删除源 $ sudo gem sources -r https://rubygems.org/ # gem 自身升级 $ sudo gem update –system # 查看版本 $ gem --version # 清除过期的 gem $ sudo gem cleanup # 安装包 # 苹果系统升级 OS X EL Capitan 后改为:$ sudo gem install -n /usr/local/bin cocoapods $ sudo gem install cocoapods # 删除包 $ gem uninstall cocoapods # 更新包 $ sudo gem update # 列出本地安装的包 $ gem list 3、Ruby 源 “源” 相当于安装软件的服务器。 国外的源地址:https://rubygems.org/ 速度非常慢慢 国内的源地址:https://ruby.taobao.org/ 速度快 # 查看当前 ruby 版本 $ ruby -v # 列出已知的 ruby 版本,需安装 RVM 管理器 $ rvm list known # 升级 Ruby 版本,需安装 RVM 管理器 $ rvm install 2.3.0 4、RVM 管理器 RVM:Ruby Version Manager,Ruby 版本管理器,包括 Ruby 的版本管理和 Gem 库管理(gemset) # 安装 RVM: $ curl -L get.rvm.io | bash -s stable $ source ~/.bashrc $ source ~/.bash_profile # 查看 RVM 版本 $ rvm -v 5、安装 Xcode 插件管理器 curl -fsSL https://raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh | sh 安装完 Xcode 插件管理器后,在 Xcode 的菜单 Window 中多出一个 Package Manager 选项,用于管理 Xcode 插件的安装与卸载。 如果安装了 CocoaPods,在 Xcode 的菜单 Product 中多出一个 CocoaPods 选项,用于管理工程中用到的第三方框架。
前言 提前下载好相关软件,且安装目录最好安装在全英文路径下。如果路径有中文名,那么可能会出现一些莫名其妙的问题。 提前准备好的软件: apache-tomcat-6.0.45.tar.gz eclipse-jee-mars-2-macosx-cocoa-x86_64.tar.gz jdk-8u91-macosx-x64.dmg tomcat 官网 eclipse 下载 jdk 官网 提前准备好的服务器脚本程序: MJServer 文件1下载 文件2下载 1、Apache Tomcat WebServer 配置 在 Finder 中创建一个 "workspace" 的文件夹,作为 eclipse 的工作空间。 1)配置服务器: 1> 先安装 jdk。 2> 安装 eclipse,设置工作空间。 3> 把提前准备好的服务器脚本程序,拷贝到工作空间中。 4> 导入项目,导入已经存在的项目到工作空间中。 5> 导入项目之后,项目报错且格式乱码,下面进行调整。 6> 配置容器,apache-tomcat。 点击ok。创建一个新的容器 选择容器的路径 安装好后显示如下: 7> 启动服务器。以debug的方式启动,方便做一些调试 测试:server已经成功启动。 8> 部署程序 9> 在浏览器中输入服务器的地址,访问项目 至此本地服务器环境搭建完成。 访问服务器的资源 使用模拟器上的浏览器也可以访问本地服务器。输入地址192.168.1.53:8080/MJServer 10> 浏览器打开页面,文字乱码调整。
前言 Apache 服务器: Web 服务器,可以支持各种脚本(PHP)的执行,目前世界上使用最为广泛的一种 Web 服务器 WebDav 服务器: 基于 http 协议的 "文件" 服务器 实现文件的上传/下载/修改/删除 WebDav 权限: 授权信息的格式 BASIC (用户名:口令)base64 安全性并不高,密码很容易被拦截和破解。 应用场景:开发企业级的管理系统,可以用 WebDav 搭建一个内部的文件管理服务器,只是在公司内网使用。 FTP 服务器: 文件传输协议,基于 FTP 的一个文件管理服务器 可以做文件的上传/下载/修改/删除 以上三种服务器,只要 ip 地址能够访问,无论在任何位置,都能够使用。 1、Apache WebDav 配置 1)准备工作: 为了保证电脑的安全,必须设置用户密码。 put 配置脚本文件下载 2)配置服务器: 1> 配置服务器的工作: 修改了两个配置文件。 创建 web 访问用户的用户名和口令。 创建了两个目录,并且设置管理权限。 2> 配置服务器注意事项: 关闭中文输入法。 命令和参数之间需要有 "空格"。 修改系统文件一定记住 "sudo",否则会没有权限。 目录要在 /Users/JHQ0228(当前用户名)目录下。 3> 配置服务器: # 切换目录 $ cd /etc/apache2 $ sudo vim httpd.conf # 查找httpd-dav.conf /httpd-dav.conf "删除行首#" # 将光标定位到行首 0 # 删除行首的注释 x # 保存退出 :wq 注意:要在 Mac 10.10+ 配置 Web-dav 还需要在 httpd.conf 中打开以下三个模块 LoadModule dav_module libexec/apache2/mod_dav.so LoadModule dav_fs_module libexec/apache2/mod_dav_fs.so LoadModule auth_digest_module libexec/apache2/mod_auth_digest.so # 切换目录 $ cd /etc/apache2/extra # 备份文件(只要备份一次就行) $ sudo cp httpd-dav.conf httpd-dav.conf.bak # 编辑配置文件 $ sudo vim httpd-dav.conf "将 Digest 修改为 Basic" # 查找Digest /Digest # 进入编辑模式 i # 返回到命令行模式 ESC # 保存退出 :wq # 切换目录,可以使用鼠标拖拽的方式 $ cd 保存 put 脚本的目录 # 以管理员权限运行 put 配置脚本(对于 OS X 10.11 + 用户,需要关闭 SIP 安全设置) $ sudo ./put # 输入系统密码:当前用户密码 # 设置两次 WebDav 密码:adminpasswd(密码随便设置) # 当返回的文件列表中包含有如下信息时即表示配置成功。 drwxr-xr-x 2 _www _www 68 3 30 11:50 uploads -rw-r--r-- 1 root _www 44 3 30 11:50 user.passwd drwxr-xr-x 2 _www _www 68 3 30 11:50 var # 设置的用户名为:admin,密码为:adminpasswd # 点击 Finder 的菜单 前往 => 连结服务器(command + k) # 在弹出的对话框的服务器地址中输入要连结的 WebDav 服务器的 IP 地址。 # 如输入 http://192.168.88.200/uploads 点击连结。 # 或者输入本地回环地址 http://127.0.0.1/uploads 进行测试。 # 验证连结身份时,使用注册用户,名称和密码为前边设置的内容,如名称:admin,密码:adminpasswd # put 配置脚本执行的内容 # 切换目录 $ cd /usr # 设置用户 admin 的密码 $ htpasswd -c /usr/user.passwd admin # 设置密码文件的访问群组 $ chgrp www /usr/user.passwd # 建立 var 文件夹,保存 DavLockDB 相关文件 $ mkdir -p /usr/var # 修改 var 文件夹用户群组 $ chown -R www:www /usr/var # 建立上传文件夹:uploads $ mkdir -p /usr/uploads # 修改 uploads 文件夹用户群组 $ chown -R www:www /usr/uploads # 确认 $ ls -lG # 重新启动 Apache $ apachectl -k restart 3)常见问题: 1> mac root 用户 在 usr 目录下没有写权限 对于 Mac OS X 10.11 + 用户,由于系统启用了 SIP(System Integrity Protection), 导致 root 用户也没有权限修改 /usr 目录。按如下方式可恢复权限。 屏蔽方法: 重启 Mac,按住 command + R,进入 recovery 模式。选择打开 Utilities 下的终端, 输入:csrutil disable 并回车,然后正常重启 Mac 即可。 如果想想重新开启该安全设置,重复上面步骤,在终端中输入的命令更改为 csrutil enable。
前言 Apache 是目前使用最广的 Web 服务器,可以支持各种脚本的执行。 Mac 系统自带,无需单独安装,只需要修改几个配置就可以,简单,快捷。 有些特殊的服务器功能,Apache 都能很好的支持。例如:HTTP PUT/DELETE 等操作。 1、Apache WebServer 配置 1)准备工作: 为了保证电脑的安全,必须设置用户密码。 2)配置服务器: 1> 配置服务器的工作: 在 Finder 中创建一个 "ApacheWebServer" 的文件夹,直接创建在 /Users/JHQ0228(当前用户名)目录下。 修改配置文件中的 "两个路径",指向刚刚创建的文件夹。 修改一个 Options 配置项。 反注释一个文件路径配置。 拷贝一个文件。 2> 配置服务器注意事项: 关闭中文输入法。 命令和参数之间需要有 "空格"。 修改系统文件一定记住 "sudo",否则会没有权限。 目录要在 /Users/JHQ0228(当前用户名)目录下。 3> 配置服务器: 提示:$ 开头的,可以拷贝,但是不要拷贝 $ 。 "直接在当前用户目录下创建一个 ApacheWebServer 的文件夹,打开终端,按照下面的操作开始配置" # 切换工作目录 $ cd /etc/apache2 # 备份文件,以防不测,只需要执行一次就可以了,格式 cp (copy 的缩写) (源文件) (目标文件) # 如果后续操作出现错误,可以使用以下命令恢复 $ sudo cp httpd.conf.bak httpd.conf $ sudo cp httpd.conf httpd.conf.bak # 用 vim 打开 httpd.conf 文件 $ sudo vim httpd.conf # 查找 DocumentRoot,命令模式下输入 /(查找),区分大小写 * /DocumentRoot # "将光标移动到 DocumentRoot 所在行" DocumentRoot "/Library/WebServer/Documents" <Directory "/Library/WebServer/Documents"> # 进入编辑模式,将光标移动到 DocumentRoot 所在行,直接按键盘上的 i 键 * i # "修改引号中的路径为上面在 Finder 中创建的路径" DocumentRoot "/Users/JHQ0228/ApacheWebServer" <Directory "/Users/JHQ0228/ApacheWebServer"> # "往下滑动找到 Options FollowSymLinks Multiviews 行",Mac 10.10+ 的 Apache 需要修改这一行 Options FollowSymLinks Multiviews # "在 Options 与 FollowSymLinks 之间插入 Indexes" Options Indexes FollowSymLinks Multiviews # 进入命令模式,直接按键盘上的 esc 键 * esc # 查找 php,命令模式下输入 /(查找),区分大小写 * /php # "将光标移动到 #LoadModule php5_module libexec/apache2/libphp5.so 行首" #LoadModule php5_module libexec/apache2/libphp5.so # 删除行首注释 #,命令模式下直接按键盘上的 x 键 * x LoadModule php5_module libexec/apache2/libphp5.so # 保存并退出,命令模式下输入 :wq ,不保存退出为 :q! * :wq # 切换工作目录,etc 目录有点类似于 windows/system32,存放配置文件的目录 $ cd /etc # 拷贝 php.ini 文件 $ sudo cp php.ini.default php.ini # 重新启动 apache 服务器 $ sudo apachectl -k restart # "如果出现以下错误提示,表示配置完成" httpd: Could not reliably determine the server's fully qualified domain name, using JHQ0228-MacBookAir.local. Set the 'ServerName' directive globally to suppress this message # 将服务器脚本文件放到前面设置的 ApacheWebServer 文件夹中。 # 打开浏览器,在浏览器地址栏中输入服务器地址如 http://192.168.88.200 ,能够进入到 “Index of /” 页面。 # 或者输入本地地址 http://localhost 进行测试。 3)常见问题: 1> 如果点击服务器网站资源中的 info.php 文件,出现下载,或者只是显示一小段文字 解决办法: 在终端中输入以下两个命令: $ sudo apachectl -k stop // 关闭 apache 服务器 $ sudo apachectl -k start // 重新再次启动 apache 服务器 2> 每次启动计算机,Apache 服务器默认是不会自动启动的 可以启动计算机之后,打开终端,输入以下命令: $ sudo apachectl -k start // 启动 apache 服务器 设置开机启动: $ sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist 关闭开机启动: $ sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist 3> 最常见的问题 交换文件已经存在,直接按字母 d,可以删除交换文件。 4> Mac 10.10+ 的 Apache 配置略微有一些不一样 在 httpd.conf 中找到 "Options FollowSymLinks Multiviews" 加一个单词 Indexes,修改后的结果如下: "Options Indexes FollowSymLinks Multiviews" 5> 执行脚本的时候,显示没有权限,拒绝访问。用 NTFS 格式的 U 盘拷贝网络素材,会把文件本身的权限过滤掉。 以下是在终端中修改文件权限的指令: $ ls -la // 查看当前文件夹中的文件访问权限 $ chmod 644 info.php(没有权限的文件名) // 将指定的文件权限修改为 -rw-r--r-- -读写-只读-只读-,644(110 100 100) $ chmod 644 *.* // 将所有的文件权限修改为 -rw-r--r--
Xcode 插件 Xcode 插件安装目录: ~/library/Application Support/Developer/Shared/Xcode/Plug-ins Xcode 插件大全 http://www.cocoachina.com/industry/20130918/7022.html 必备插件 文档注释生成:https://github.com/onevcat/VVDocumenter-Xcode 自动检索图片名:https://github.com/ksuther/KSImageNamed-Xcode 取色:https://github.com/omz/ColorSense-for-Xcode 插件管理工具 https://github.com/mneorr/Alcatraz curl -fsSL https://raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh | sh 安装完 Xcode 插件管理器后,在 Xcode 的菜单 Window 中多出一个 Package Manager 选项,用于管理 Xcode 插件的安装与卸载。 如果安装了 CocoaPods,在 Xcode 的菜单 Product 中多出一个 CocoaPods 选项,用于管理工程中用到的第三方框架。 移除插件 可以使用上面提到的插件管理工具 Alcatraz。 可以到 ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins 文件夹中删除。 插件失效修复 http://joeshang.github.io/2015/04/10/fix-xcode-upgrade-plugin-invalid/
1、Xcode 配置 1.1 OS X 1)main 文件注释修改路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Library ▸ Xcode ▸ Templates ▸ Project Templates ▸ Base ▸ Base.xctemplate 2)main 文件中 main 函数默认配置修改路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Library ▸ Xcode ▸ Templates ▸ Project Templates ▸ Mac ▸ Application ▸ Command Line Tool.xctemplate 1.2 iOS 1)AppDelegate.m 文件中 -(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法内默认代码的修改路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Platforms ▸ iPhoneOS.platform ▸ Developer ▸ Library ▸ Xcode ▸ Templates ▸ Project Templates ▸ Application ▸ Empty Application.xctemplate 2)Xcode 中自定义代码段文件存放路径 /Users ▸ JHQ0228 ▸ Library(资源库) ▸ Developer ▸ Xcode ▸ UserData ▸ CodeSnippets 3)在 Xcode 中添加空模板 因为 Xcode5 或低于该版本的部分 Xcode 都有空模板(Empty Application.xctemplate),拷贝其中的空模板,粘贴到 Xcode6 或所需要版本的 Xcode 模版路径中即可。 1> 低于 6 版本 Xcode 的模板路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Platforms ▸ iPhoneOS.platform ▸ Developer ▸ Library ▸ Xcode ▸ Templates ▸ Project Templates ▸ Application 2> 高于 6 版本 Xcode 的模板路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Platforms ▸ iPhoneOS.platform ▸ Developer ▸ Library ▸ Xcode ▸ Templates ▸ Project Templates ▸ iOS ▸ Application 4)在 Xcode 中添加 SDK 版本 将相应版本的 SDK 文件拷贝到指定路径即可。 Xcode 中 SDK 文件存放路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Platforms ▸ iPhoneOS.platform ▸ Developer ▸ SDKs 5)在 Xcode 中添加模拟器版本 1> 模拟器添加 在 Xcode6 以前的版本,安装模拟器 SDK 就等于安装了模拟器。Xcode 中模拟器 SDK 文件存放路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Platforms ▸ iPhoneSimulator.platform ▸ Developer ▸ SDKs 在 Xcode6 和之后的版本,新版的 Xcode 并不会识别 SDKs 目录下的模拟器,需要将模拟器文件要放在这个目录下: /Library(资源库) ▸ Developer ▸ CoreSimulator ▸ Profiles ▸ Runtimes 这个目录是根目录,不在 Xcode 和 User 的目录下,而且与旧版的模拟器不同,新版模拟器是以 simruntime 为后缀打包的文件。新版的 Xcode 软件安装时仍会创建相应版本的 SDK 文件,不会创建相应的 simruntime 文件。 2> 模拟器配置(用户添加的设备) 在 Xcode6 以前的版本中,添加的设备路径为: /Users ▸ JHQ0228 ▸ Library ▸ Application Support ▸ iPhone Simulator 在 Xcode6 和之后的版本中,添加的设备路径为: /Users ▸ JHQ0228 ▸ Library ▸ Developer ▸ CoreSimulator ▸ Devices 3> 模拟器中 App 安装 在 Xcode 模拟器中,App 安装路径为: /Users ▸ JHQ0228 ▸ Library ▸ Developer ▸ Xcode ▸ DerivedData ▸ YOURPROJECTNAME_SOMETHINGSOMETHING ▸ Build ▸ Products 6)在 Xcode 中添加离线文档 将相应离线文档拷贝到指定路径即可。 Xcode 中离线文档存放路径: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Documentation ▸ DocSets 7)DeviceSupport 关于 Xcode7 真机测试出现 could not find developer disk image 问题,主要缺少了此文件夹,将其放到指定路径即可: /Applications(应用程序) ▸ Xcode.app ▸ Contents ▸ Developer ▸ Platforms ▸ iPhoneOS.platform ▸ DeviceSupport