MingleChang


  • Startseite

  • Archiv

  • Tags

iOS系统通知

Veröffentlicht am 2017-02-21

一、键盘

1.UIKeyboardWillShowNotification //将要弹出键盘
2.UIKeyboardDidShowNotification //显示键盘
3.UIKeyboardWillHideNotification //将要隐藏键盘
4.UIKeyboardDidHideNotification //键盘已经隐藏
5.UIKeyboardWillChangeFrameNotification //键盘将要改变frame
6.UIKeyboardDidChangeFrameNotification //键盘已经改变frame

二、窗口

1.UIWindowDidBecomeVisibleNotification //当window激活时并展示在界面的时候触发,返回空
2.UIWindowDidBecomeHiddenNotification //当window隐藏的时候触发,返回空
3.UIWindowDidBecomeKeyNotification //当window被设置为keyWindow时触发,返回空
4.UIWindowDidResignKeyNotification //当window的key位置被取代时触发,返回空

三、程序消息

1.UIApplicationDidEnterBackgroundNotification //程序进入后台
2.UIApplicationDidEnterBackgroundNotification //程序进入前台
3.UIApplicationDidFinishLaunchingNotification //程序加载完成
4.UIApplicationDidBecomeActiveNotification //程序变成活动状态
5.UIApplicationWillResignActiveNotification //程序变为非活动状态
6.UIApplicationDidReceiveMemoryWarningNotification //内存警告
7.UIApplicationWillTerminateNotification //程序进程停止
8.UIApplicationSignificantTimeChangeNotification //重要的时间变化(新的一天开始或时区变化)
9.UIApplicationWillChangeStatusBarOrientationNotification //将要改变状态栏方向
10.UIApplicationDidChangeStatusBarOrientationNotification //状态栏的方向改变
11.UIApplicationWillChangeStatusBarFrameNotification //将要改变状态栏的frame
12.UIApplicationDidChangeStatusBarFrameNotification //状态栏的frame改变
13.UIApplicationBackgroundRefreshStatusDidChangeNotification
14.UIApplicationProtectedDataWillBecomeUnavailable
15.UIApplicationProtectedDataDidBecomeAvailable

四、设备

1.UIDeviceOrientationDidChangeNotification //设备方向改变
2.UIDeviceBatteryStateDidChangeNotification //设备电池状态改变
3.UIDeviceBatteryLevelDidChangeNotification //设备电池电量改变
4.UIDeviceProximityStateDidChangeNotification //设备近距离传感器

五、屏幕

1.UIScreenDidConnectNotification //屏幕设备连接

firewalld命令使用

Veröffentlicht am 2017-02-08

1、firewalld的基本使用

启动: systemctl start firewalld
查看状态: systemctl status firewalld
停止: systemctl disable firewalld
禁用: systemctl stop firewalld

2.配置firewalld-cmd

查看版本: firewall-cmd --version
查看帮助: firewall-cmd --help
显示状态: firewall-cmd --state
查看所有打开的端口: firewall-cmd --zone=public --list-ports
更新防火墙规则: firewall-cmd --reload
查看区域信息: firewall-cmd --get-active-zones
查看指定接口所属区域: firewall-cmd --get-zone-of-interface=eth0
拒绝所有包:firewall-cmd --panic-on
取消拒绝状态: firewall-cmd --panic-off
查看是否拒绝: firewall-cmd --query-panic

开启或者关闭一个端口

添加
firewall-cmd --zone=public --add-port=80/tcp --permanent (--permanent永久生效,没有此参数重启后失效)
重新载入
firewall-cmd --reload
查看
firewall-cmd --zone= public --query-port=80/tcp
删除
firewall-cmd --zone= public --remove-port=80/tcp --permanent

Socket套接字API

Veröffentlicht am 2016-12-24

IPv4套接字地址结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <netinet/in.h>
/*
* Internet address (a structure for historical reasons)
*/
struct in_addr {
in_addr_t s_addr;
};
/*
* Socket address, internet style.
*/
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};

IPv6套接字地址结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <netinet6/in6.h>
/*
* IPv6 address
*/
struct in6_addr {
union {
__uint8_t __u6_addr8[16];
__uint16_t __u6_addr16[8];
__uint32_t __u6_addr32[4];
} __u6_addr; /* 128-bit IP6 address */
};
struct sockaddr_in6 {
__uint8_t sin6_len; /* length of this struct(sa_family_t) */
sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */
in_port_t sin6_port; /* Transport layer port # (in_port_t) */
__uint32_t sin6_flowinfo; /* IP6 flow information */
struct in6_addr sin6_addr; /* IP6 address */
__uint32_t sin6_scope_id; /* scope zone index */
};

通用套接字地址结构

1
2
3
4
5
6
7
8
9
#include <sys/socket.h>
/*
* [XSI] Structure used by kernel to store most addresses.
*/
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};

字节排序函数

1
2
3
4
5
6
7
#include <sys/_endian.h>
返回:网络字节序的值
#define htons(x) __DARWIN_OSSwapInt16(x)
#define htonl(x) __DARWIN_OSSwapInt32(x)
返回:主机字节序的值
#define ntohs(x) __DARWIN_OSSwapInt16(x)
#define ntohl(x) __DARWIN_OSSwapInt32(x)

字节操纵函数

1
2
3
4
#include <strings.h>
void bzero(void *, size_t)
void bcopy(const void *, void *, size_t)
int bcmp(const void *, const void *, size_t)

bzero把目标字节串中指定数目的字节置为0,我们经常使用该函数来把一个套接字地址结构初始化为0。
bcopy将指定数目的字节从源字节串复制到目标字节串。
bcmp比较两个任意的字节串,若相同则返回值为0,否则返回值为非0。

1
2
3
4
#include <string.h>
void *memset(void *__b, int __c, size_t __len);
void *memcpy(void *__dst, const void *__src, size_t __n);
int memcmp(const void *__s1, const void *__s2, size_t __n);

memset把目标字节串指定数目的字节置为__c。
memcpy类似bcopy,不过两个指针参数的顺序是相反的,当源字节串与目标字节串重叠时,bcopy能够正确处理,但是memcpy的操作结果却不可知。
memcmp比较两个任意的字节串,若相同则返回0,否则返回一个非0,是大于0还是小于0则取决于第一个不等的字节。

inet_aton、inet_addr和inet_ntoa函数

1
2
3
4
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
char *inet_ntoa(struct in_addr inaddr);

inet_aton将strptr所指C字符串转换成一个32位的网络字节序二进制值,并通过指针addrptr来存储,若成功则返回1,否这返回0。
inet_addr进行相同的转换,返回的值为32位的网络字节序二进制值,出错时返回INADDR_NONE常值。该函数存在一个问题: 255.255.255.255不能有该函数处理,因为它的二进制值用来指示该函数失败。
inet_ntoa函数将一个32位的网络二进制IPv4地址转换成相应的点分十进制数串。

inet_pton和inet_ntop函数

1
2
3
#include <arpa/inet.h>
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, socklen_t len);

inet_pton尝试转换由strptr指针所指的字符串,并通过addrptr指针存放二进制结果。若成功则返回值为1,若输入不是有效的表达格式则为0,若出错则为-1
inet_ntop是进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。为有助于指定这个大小,在头文件中有如下定义:
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
如果len太小,不足以容纳表达格式结果(包括结尾的空字符),那么返回一个空指针,并置errno为ENOSPC。
inet_ntop函数的strptr参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小。调用成功时,这个指针就是该函数的返回值。

socket函数
为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型。

1
2
#include <sys/socket.h>
int socket(int family, int type, int protocol);

family参数指名协议族,该参数也往往被称为协议域。

family 说明
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY 密钥套接字

type参数指名套接字类型。

type 说明
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEQPACKET 有序分组套接字
SOCK_RAW 原始套接字

protocol参数应设为某个协议类型常值,或者设为0,以选择给定的family和type组合的系统默认值;

protocol 说明
IPPROTO_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

并非所有套接字family与type组合都是有效的,下表给出了一些有效的组合和对应的真正协议。其中标为“是”的项也是有效的,但还没有找到便捷的缩略词。而空白项则是无效组合。

AF_INET AF_INET6 AF_LOCAL AF_ROUTE AF_KEY
SOCK_STREAM TCP/SCTP TCP/SCTP 是
SOCK_DGRAM UDP UDP 是
SOCK_SEQPACKET SCTP SCTP 是
SOCK_RAW IPv4 IPv6 是 是

socket函数在成功时返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字描述符(socket descrptor),简称sockfd。为了得到这个套接字描述符,我们只是指定了协议族(IPv4、IPv6和Unix)和套接字类型(字节流、数据报或原始套接字)。我们并没有指定本地协议地址和远程协议地址。

connect函数
TCP客户端用connect函数来建立与TCP服务器的连接。

1
2
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小,套接字地址结构必须含有服务器的IP地址和端口号。

bind函数
bind函数把一个本地协议地址赋予一个套接字,对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。

1
2
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)

第二个参数是一个指向特定协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。

IP地址 断开 结果
通配地址 0 内核选择IP地址和端口
通配地址 非0 内核选择IP地址,进程指定端口
本地IP地址 0 进程指定IP地址,内核选择端口
本地IP地址 非0 进程指定IP地址和端口

listen函数

1
2
#include <sys/socket.h>
int listen(int sockfd, int backlog);//返回:若成功则为0,若出错则为-1

listen函数仅由TCP服务器调用,他做两件事情:

  1. 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。调用listen导致套接字从CLOSED状态转喊到LISTEN状态。
  2. 第二个参数规定了内核应该为相应套接字排队的最大连接个数。

本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。

accept函数
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。

1
2
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

返回值:如果成功返回非负描述符,若出错则为-1
参数cliaddr和addrlen用来返回已连接的对端进程的协议地址。如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP链接。

fork和exec函数

1
2
#include <unistd.h>
pid_t fork(void);

返回值:在子进程中为0,在父进程中为子进程ID,若出错则为-1
fork在子进程返回0而不是父进程的进程ID的原因在于:任何子进程只有一个父进程,而且子进程总是可以通过调用getppid取得父进程的进程ID。相反,父进程可以有许多子进程,而且无法获取各个子进程的进程ID。如果父进程想要跟踪所有子进程的进程ID,那么它必须记录每次调用fork的返回值。
父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享。
fork有两个典型用法:
1) 一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这是网络服务器的典型用法。
2) 一个进程想要执行另一个程序。既然创建新进程的唯一办法是调用fork,该进程于是首先调用fork创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec把自身替换成新的程序。这是诸如shell之类程序的典型用法。

1
2
3
4
5
6
7
8
#include <unistd.h>
int execl(const char * __path, const char * __arg0, ...);
int execv(const char * __path, char * const * __argv);
int execle(const char * __path, const char * __arg0, ...);
int execve(const char * __file, char * const * __argv, char * const * __envp);
int execlp(const char * __file, const char * __arg0, ...);
int execvp(const char * __file, char * const * __argv);

返回值:若成功则不返回,若出错则为-1
这些函数只在出错时才返回到调用者。否则,控制将被传递给新程序的起始点,通常就是main函数。

close函数

1
2
#include <unistd.h>
int close(int sockfd);

返回值:若成功则为0,若出错则为-1
close函数用来关闭套接字,并终止TCP连接。
close一个TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。

getsockname和getpeername函数
这两个函数返回与某个套接字关联的本地协议地址(getsockname),或者返回与某个套接字关联的外地协议地址(getpeername)。

1
2
3
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr * localaddr, socklen_t * addrlen);
int getpeername(int sockfd, struct sockaddr * localaddr, socklen_t * addrlen);

iOS自动打包

Veröffentlicht am 2016-12-24

做iOS开发这么久,一直给测试打包都是使用Xcode先进行Archive,然后再将对应的xcarchive文件Export为一个ipa文件。当然做了这么久感觉也没有什么不对的,但是前段时间我们老大希望能让我准备一个脚本,然后以后就让运维通过脚本打包,这样一想也的确省去不少麻烦,就算不让运维打包,我们自己用脚本也省去了不断点下一步的麻烦操作,于是我就开始通过谷歌和度娘查找相关资料,根据网上资料,网上流传的打包脚本无外两种:

第一种是先通过xcodebuild编译工程得到一个XXX.app文件,脚本如下:

1
2
xcodebuild -project XXXXXX.xcodeproj -scheme XXXXXX -configuration Release -sdk iphoneos build CODE_SIGN_IDENTITY="XXXXXX" PROVISIONING_PROFILE="XXXXXXX"
xcodebuild -workspace XXXXXX.xcworkspace -scheme XXXXXX -configuration Release -sdk iphoneos build CODE_SIGN_IDENTITY="XXXXXX" PROVISIONING_PROFILE="XXXXXXX"

这里有两句脚本,这里的两句是二选一的,如果你是需要打包的是一个普通的project就是用第一句,如果你需要打包的是一个workspace(当你使用了CocoaPods添加第三方库,你就需要使用第二句)就用第二句。
然后在使用xcrun将XXX.app文件转为XXX.ipa,脚本如下:

1
xcrun -sdk iphoneos -v PackageApplication ./Build/Products/Release-iphoneos/XXXXXX.app -o bin/XXXXXX.ipa

这里CODE_SIGN_IDENTITY等号后面的就是对应的开发者证书,PROVISIONING_PROFILE等号后面对应的是PROVISIONING_PROFILE的uuid。
这种方式打包相当于先在Xcode进行build,然后将Products目录下的app文件直接拖入iTunes所得,但是我并不建议该种方法,因为这种方法并不完全同于我们以前的打包流程。

第二种就是先通过xcodebuild archive将项目工程打包为xcarchive文件,脚本如下:

1
2
xcodebuild archive -project XXXX.xcodeproj -scheme XXXX -archivePath bin/XXXX.xcarchive
xcodebuild archive -workspace XXXX.xcworkspace -scheme XXXX -archivePath bin/XXXX.xcarchive

这里同样是两句选一句,分别针对project和workspace
然后使用xcodebuild -exportArchive将xcarchive文件export为对应该ipa文件,脚本如下:

1
xcodebuild -exportArchive -archivePath bin/XXXX.xcarchive -exportPath bin/$XXXX -exportFormat IPA -exportProvisioningProfile "[PROVISION_PROFILE]"

这里就是将的xcarchive通过使用PROVISION_PROFILE文件打包为ipa文件,这里[PROVISION_PROFILE]代表项目对应的PROVISION_PROFILE文件的文件名。
这种方式的打包是最接近于我之前手动打包的方式。

这里附上完整脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
PROJECT_NAME="XXXXX"
WORKSPACE_NAME="XXXXX"
PROVISION_PROFILE="XXXXXX"
rm -rf bin
# 如果是打包xcodeproj就使用这句
# xcodebuild archive -project ${PROJECT_NAME}.xcodeproj -scheme ${PROJECT_NAME} -archivePath bin/${PROJECT_NAME}.xcarchive
# 如果是打包xcworkspace就使用这句
xcodebuild archive -workspace ${WORKSPACE_NAME}.xcworkspace -scheme ${WORKSPACE_NAME} -archivePath bin/${WORKSPACE_NAME}.xcarchive
xcodebuild -exportArchive -archivePath bin/${WORKSPACE_NAME}.xcarchive -exportPath bin/${WORKSPACE_NAME} -exportFormat IPA -exportProvisioningProfile "${PROVISION_PROFILE}"

这里只需修改PROJECT_NAME,WORKSPACE_NAME和PROVISION_PROFILE即可。
脚本地址:ios_auto_build

iOS10 Notification Content Extension

Veröffentlicht am 2016-12-24

iOS10对推送通知的增加了两个扩展框架,之前介绍了Notification Service Extension,允许在收到推送之后,通知展示之前对推送信息进行二次处理;而另一个就是Notification Content Extension,允许开发者对推送信息自定义一个展示界面,在这个界面里你可以自定义任何视图,但是有一个限制,这个界面不能有用户交互,也就是这个界面用户不能点击它。但是对于整个通知我还是可以继续使用actions进行交互。
首先我们介绍下这个推送界面的组成,当我收到一条新的推送,我们下拉这条推送就会出现下面如图界面:

Header是属于系统默认的部分,所有推送都有这个Header且不可修改

Custom Content就是我们要介绍的Notification Content Extension的内容,在这里你可以自定义为任何样式。

Default Content系统默认的推送展示样式,不可修改,但可以选择不显示(这个我们后面会说到)。

Actions就是我们之前介绍UserNotification.Framework是介绍过的,这里用户可以进行一些操作并体现到Custom Content中。

现在我们来创建一个Notification Content Extension的Target,Xcode会自动创建如下图的几个文件:

NotificationViewController就是我们Custom Conten的代码部分,MainInterface.storyboard就是布局部分,Info.plist就是配置文件。

打开Info.plist文件,我们可以看到如下内容:

这里的UNNotificationExtensionCategory就是响应这个Content Extension的通知的categoryId,那如果有多个categoryId都对应的是这个Content Extension怎么办呢?那么我们可以将UNNotificationExtensionCategory改为一个数组,数组中包含多个categoryId,如图:

UNNotificationExtensionInitialContentSizeRatio这个是Content Extension初始化时候的高宽比。

除了这两个我之前说过Default Content可以选择是否显示,那么如果我们希望不显示Default Content,我们可以在NSExtensionAttributes增加UNNotificationExtensionDefaultContentHidden并设置为YES,如图:

然后我们在MainInterface.storyboard中的ViewController中增加一些子视图:

一个imageView和两个Label,并将其关联为NotificationViewController的属性:

1
2
3
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *contentLabel;

现在我们开始处理NotificationViewController,NotificationViewController实际上就是一个继承于UIViewController的一个视图控制器,但是他实现了UNNotificationContentExtension协议。
UNNotificationContentExtension协议主要有两个方法:

1
2
3
- (void)didReceiveNotification:(UNNotification *)notification;
@optional
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion;

第一个方法是当NotificationContentExtension收到指定categoryId的推送时,那么就将会响应这个方法,然后我们可以根据通知内容设置我们的界面。
第二个方法是一个可选方法,这个就是当用户进行Actions的操作是,NotificationContentExtension会响应的方法,我们可以根据对应的Action修改NotificationContentExtension界面或者进行网络请求等操作。
我先根据我们收到推送修改对应的Content Extension界面,那么我们在didReceiveNotification:中添加如下代码:

1
2
3
4
5
- (void)didReceiveNotification:(UNNotification *)notification {
self.imageView.image=[UIImage imageNamed:@"push_image"];
self.titleLabel.text=notification.request.content.title;
self.contentLabel.text=notification.request.content.body;
}

那么当我们收到推送之后,我们的Content Extension界面界面将会展示如下:

之前在介绍Service Extension时,我们可以根据推送请求一些图片等并设置到attachments,那么当我们在Content Extension中收到一个包含attachments的推送时,我们要如何展示,这里我们将代码如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)didReceiveNotification:(UNNotification *)notification {
if (notification.request.content.attachments.count==0) {
self.imageView.image=[UIImage imageNamed:@"push_image"];
}else{
UNNotificationAttachment *lAttachment=notification.request.content.attachments.firstObject;
if (lAttachment) {
if ([lAttachment.URL startAccessingSecurityScopedResource]) {
self.imageView.image = [UIImage imageWithContentsOfFile:lAttachment.URL.path];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[lAttachment.URL stopAccessingSecurityScopedResource];
});
}
}
}
self.titleLabel.text=notification.request.content.title;
self.contentLabel.text=notification.request.content.body;
}

这里如果我们收到的推送包含attachments内容,那么我们就将图片赋值给imageView,但是attachment是由系统管理的,系统会把它们单独的管理,这意味着它们存储在我们sandbox之外。所以这里我们要使用attachment之前,我们需要告诉iOS系统,我们需要使用它,并且在使用完毕之后告诉系统我们使用完毕了。对应上述代码就是-startAccessingSecurityScopedResource和-stopAccessingSecurityScopedResource的操作。
这是如果我们收到一个带图片的推送,Content Extension展示如下:

这里的imageView展示的就是在Service Extension下载的图片。

然后我们来介绍UNNotificationContentExtension协议的第二个方法,- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion
该方法有两个参数,第一个response就是点击Actions传递过来的参数,我们可以根据response来判断是点击了哪个Actions,并对其做对应的处理,第二个completion是一个block,当我处理完response后需要回调的block,系统根据该block做后续处理,这个block有一个参数UNNotificationContentExtensionResponseOption,这个是UNNotificationContentExtensionResponseOption是一个枚举,定义如下:

1
2
3
4
5
typedef NS_ENUM(NSUInteger, UNNotificationContentExtensionResponseOption) {
UNNotificationContentExtensionResponseOptionDoNotDismiss,
UNNotificationContentExtensionResponseOptionDismiss,
UNNotificationContentExtensionResponseOptionDismissAndForwardAction,
} __IOS_AVAILABLE(10_0) __TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE __OSX_UNAVAILABLE;

UNNotificationContentExtensionResponseOptionDoNotDismiss代表不关闭Content Extension
UNNotificationContentExtensionResponseOptionDismiss代表关闭Content Extension,这里需要注意如果点击的action类型为UNNotificationActionOptionForeground,Content Extension仍然不会关闭
UNNotificationContentExtensionResponseOptionDismissAndForwardAction代表关闭Content Extension,且打开App。
这里我们修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response completionHandler:(void (^)(UNNotificationContentExtensionResponseOption option))completion{
if ([response.actionIdentifier isEqualToString:kCategoryTestInputKey]) {
UNTextInputNotificationResponse *inputResponse=(UNTextInputNotificationResponse *)response;
NSString *lString=inputResponse.userText;
self.contentLabel.text=lString;
}else if ([response.actionIdentifier isEqualToString:kCategoryTestConfirmKey]) {
self.contentLabel.text=@"Confirm";
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
completion(UNNotificationContentExtensionResponseOptionDismiss);
});
}

这里我们根据对应的Action修改contentLabel的显示,并在1.5s后关闭Content Extension,当然你也可以在这里进行一些网络请求后在执行block。
那么我们在收到推送点击了Confirm后,Content Extension将修改为如下:

对于Content Extension的介绍就如下了。

代码下载:UserNotificationsTest

iOS10 Notification Service Extension

Veröffentlicht am 2016-12-21

在iOS10之前,iOS的推送逻辑是服务器想苹果的APNS服务器发送一条消息,然后由APNS服务器推送到手机,然后由操作系统处理后直接展示给用户,这个过程如下:

服务器 → APNS → 操作系统 → 用户

可以看出,这个过程跟我们的App没有任何关系(除了注册推送,获取Token),推送来的任何信息我们都无法对其展示做处理,iOS10苹果推出了Notification Service Extension,使得推送来的的信息可以通过Service Notification进行二次处理,那么现在我们推送发送到展示的过程就变成了:

服务器 → APNS → 操作系统 → Service Extension → 用户

通过Notification Service Extension,我们能在收到推送一条新的推送之后的30s(据说是30s,未测试)之内对推送信息进行二次处理,然后再展示,当然如果我们在规定时间之内未能成功进行二次处理,系统还是会按照当前的推送信息进行展示。

首先我对我们的项目创建一个Notification Service Extension,Notification Service Extension跟以前的Today Extension一样都属于一个应用扩展,那么就需要创建一个Target:

然后如图选择:

点击Next,输入名称,我们在项目中就多出一个新的target:

这里Bundle Identifier就是项目的bundle id加上.扩展名称,所以我这里的Bundle Identifier为mingle.chang.joke.NotificationService,这里我们就创建一个Notification Service Extension。

接下来我来看下Notification Service Extension中的处理逻辑,Notification Service Extension为我们创建三个文件:

Info.plist就是Notification Service Extension的配置文件,而NotificationService.h和NotificationService.m则是我们对通知进行二次处理的地方,打开NotificationService.m可以看到,已经系统已经默认帮我们写好了两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
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);
}

而我们需要做的也就是在这两个方法中进行处理,第一个方法- (void)didReceiveNotificationRequest:(UNNotificationRequest )request withContentHandler:(void (^)(UNNotificationContent _Nonnull))contentHandler就是当Service Extension收到推送时首先执行的方法,第二个方法- (void)serviceExtensionTimeWillExpire就是如果我们对推送的二次处理超时或者处理出现异常情况将会默认执行这个方法,所以对于我们来说主要对第一个方法进行修改,在之前如果我们收到这样一条推送:

1
2
3
4
5
6
7
8
9
10
11
{
"aps": {
"alert": {
"title": "这是一个标题",
"subtitle": "这是一个副标题",
"body": "你收到一个内容"
},
"badge": 1,
"sound": "default"
}
}

那么我们收到的推送将会是如下:

我们说过Notification Service Extension可以对推送进行二次处理之后在进行展示,那么需要我们做哪些处理呢?
首先我们需要后台在推送的JSON中增加一个mutable-content字段,且该字段的值为1,那么我们服务器发出的推送就会是下面这个JSON:

1
2
3
4
5
6
7
8
9
10
11
12
{
"aps": {
"alert": {
"title": "这是一个标题",
"subtitle": "这是一个副标题",
"body": "你收到一个内容"
},
"badge": 1,
"sound": "default",
"mutable-content": 1
}
}

只有推送中包含该字段,系统才会将推送发送给Service Extension进行二次处理,然后我们修改- (void)didReceiveNotificationRequest:(UNNotificationRequest )request withContentHandler:(void (^)(UNNotificationContent _Nonnull))contentHandler中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// 根据收到的推送request修改推送显示的信息
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.title];
self.bestAttemptContent.subtitle = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.subtitle];
self.bestAttemptContent.body = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.body];
self.contentHandler(self.bestAttemptContent);
}

首先我们介绍这个方法中的两个参数:
request:就是我们收到的推送请求
contentHandler:是我们对推送进行二次处理完成后的推送信息回调给系统或者通知中心的block
接下介绍该方法中的代码:
首先我们通过:

1
2
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];

将contentHandler和request.content赋值给属性contentHandler和bestAttemptContent;
然后我们通过:

1
2
3
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.title];
self.bestAttemptContent.subtitle = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.subtitle];
self.bestAttemptContent.body = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.body];

修改bestAttemptContent的title,subtitle和body;
最后我们通过:

1
self.contentHandler(self.bestAttemptContent);

将修改后的bestAttemptContent回调给系统或者通知中心,这样当我们收到推送信息:

1
2
3
4
5
6
7
8
9
10
11
12
{
"aps": {
"alert": {
"title": "这是一个标题",
"subtitle": "这是一个副标题",
"body": "你收到一个内容"
},
"badge": 1,
"sound": "default",
"mutable-content": 1
}
}

之后,系统展示的就会是:

可以看到,这里我们将收到的推送的title,subtitle和body都增加了[NotificationService]进行展示。
之前说过系统留给我们处理推送的时间是30s,而我们上面的处理估计连1s都不到,那么我们在这30s还能干点其他什么吗?
当然,这里留给我们处理足够长,我们能够处理很多东西,比如可以让服务器推送一段加密的信息,我们将信息解密之后在进行展示;又比如可以让服务器推送一条信息的唯一标识,然后我们通过唯一标识向服务器获取需要展示的信息;我们也可以在收到推送后向服务器下载图片,视频,语音进行展示,当然这些文件也有一些要求规定,如图:

这里我们以下载图片为例:
首先我们修改推送JSON,在推送JSON增加一个自定义的字段image,这个字段就是对应的图片的地址,这里我们收到推送JSON则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"aps": {
"alert": {
"title": "这是一个标题",
"subtitle": "这是一个副标题",
"body": "你收到一个内容"
},
"badge": 1,
"sound": "default",
"mutable-content": 1,
"image": "xxxxx"
}
}

然后修改- (void)didReceiveNotificationRequest:(UNNotificationRequest )request withContentHandler:(void (^)(UNNotificationContent _Nonnull))contentHandler中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.title];
self.bestAttemptContent.subtitle = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.subtitle];
self.bestAttemptContent.body = [NSString stringWithFormat:@"%@ [NotificationService]", self.bestAttemptContent.body];
NSDictionary *lApsDic = self.bestAttemptContent.userInfo[@"aps"];
NSString *lImageUrl=lApsDic[@"image"];
if (lImageUrl.length>0) {
[self loadAttachmentForUrlString:lImageUrl withType:@"png" completionHandle:^(UNNotificationAttachment *attach) {
if (attach) {
self.bestAttemptContent.attachments = [NSArray arrayWithObject:attach];
}
self.contentHandler(self.bestAttemptContent);
}];
}else{
self.contentHandler(self.bestAttemptContent);
}
}

这里我们在之前修改body的后增加了图片下载和加入通知信息的代码,首先我们从推送信息中获取image字段,得到图片的链接地址,然后使用loadAttachmentForUrlString:withType:completionHandle:下载我们的图片,该方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)loadAttachmentForUrlString:(NSString *)urlStr withType:(NSString *)type completionHandle:(void(^)(UNNotificationAttachment *attach))completionHandler{
__block UNNotificationAttachment *attachment = nil;
NSURL *attachmentURL = [NSURL URLWithString:urlStr];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDownloadTask *lTask=[session downloadTaskWithURL:attachmentURL completionHandler:^(NSURL *temporaryFileLocation, NSURLResponse *response, NSError *error) {
if (error != nil) {
NSLog(@"Error:%@", error.localizedDescription);
} else {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *localURL = [NSURL fileURLWithPath:[temporaryFileLocation.path stringByAppendingPathExtension:type]];
[fileManager moveItemAtURL:temporaryFileLocation toURL:localURL error:&error];
NSError *attachmentError = nil;
attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:localURL options:nil error:&attachmentError];
if (attachmentError) {
NSLog(@"Error:%@", attachmentError.localizedDescription);
}
}
completionHandler(attachment);
}];
[lTask resume];
}

这个方法就是用于图片下载,并将下载的图片生成一个UNNotificationAttachment对象,然后通过block回调给- (void)didReceiveNotificationRequest:(UNNotificationRequest )request withContentHandler:(void (^)(UNNotificationContent _Nonnull))contentHandler中,然后使用self.bestAttemptContent.attachments = [NSArray arrayWithObject:attach];对bestAttemptContent.attachments进行赋值,最后执行self.contentHandler(self.bestAttemptContent);通过这样,当我们收到上面的推送之后展示给用户的就会是如图所示:

下拉推送信息将会展示为:

这样我们就成功的推送了一条图片信息。

需要注意的地方:
一、是生成UNNotificationAttachment的时候,对应的图片文件名必须有正确的文件后缀名,否则在生成UNNotificationAttachment时将会抛出如下错误:
2016-10-30 23:08:17.135250 NotificationService[1581:190750] Error:Unrecognized attachment file type
二、如果我们需要对Notification Service Extension进行调试,需要选中Notification Service Extension的Target进行调试,如图:

三、如果在Notification Service Extension中的网络请求不是HTTPS,那么必须该Target的Info.plist中添加App Transport Security Settings说明。

代码下载:UserNotificationsTest

iOS10 UserNotification.Framework

Veröffentlicht am 2016-12-21

作为一个App推送功能基本是每个App都会有的功能,尤其是国内应用,推送功能基本达到了滥用的地步,但是随着苹果公司对推送功能不断的加强,我们能通过推送实现更多的功能,尤其是这次iOS10的发布,增加了UserNotification.Framework,Notification Content和Notification Service Extension,推送功能变得更加强大。

这里主要是介绍UserNotification.Framework。

在iOS10之前我们注册通知的方式有两种,在iOS8之前我们使用

1
[application registerForRemoteNotificationTypes:UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound];

在iOS8之后我们使用

1
2
3
4
5
6
7
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound categories:nil];
[application registerUserNotificationSettings:settings];
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)settings
{
[application registerForRemoteNotifications];
}

然后使用下面的方法获取token

1
2
3
4
5
6
7
8
9
10
11
12
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSLog(@"Registration successful, bundle identifier: %@, device token: %@",[NSBundle.mainBundle bundleIdentifier], deviceToken);
NSString *pushToken = [[[[deviceToken description]
stringByReplacingOccurrencesOfString:@"<" withString:@""] stringByReplacingOccurrencesOfString:@">" withString:@""]
stringByReplacingOccurrencesOfString:@" " withString:@""];
NSLog(@"device token: %@",pushToken);
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
NSLog(@"Failed to register: %@", error);
}

然后只需要后台通过APNS发送一条JSON:

1
2
3
4
5
6
7
8
9
{
"aps": {
"alert": {
"body": "你收到一个内容"
},
"badge": 1,
"sound": "default"
}
}

在App端就能收到一条如下的推送:

以上就是在iOS10我们实现基本推送功能的方式,至于之前推送的一些其他功能可以自行百度或者谷歌。

在iOS10,苹果引入了一个新的Framework:UserNotification.Framework,将之前的RemoteNotification和LocalNotification的进行了统一处理,这里主要是对于RemoteNotification。

首先是注册通知:

1
2
3
4
5
6
7
8
9
10
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted==YES) {
NSLog(@"request authorization succeeded!");
[application registerForRemoteNotifications];
}else{
NSLog(@"request authorization failed!");
NSLog(@"Error:%@",error);
}
}];

UNUserNotificationCenter是用于专门管理推送通知的类,通过requestAuthorizationWithOptions向系统请求推送权限,请求完成后会有一个block的回调,如果granted为YES则代表获取权限成功,然后通过[application registerForRemoteNotifications]注册通知,获取token方式与之前,然后使用后台发起推送,就能收到跟之前一样的推送了。

在iOS10之前,一条推送上只能显示一句话,但是在iOS10之后如果我们推送下面这条JSON:

1
2
3
4
5
6
7
8
9
10
11
{
"aps": {
"alert": {
"title": "这是一个标题",
"subtitle": "这是一个副标题",
"body": "你收到一个内容"
},
"badge": 1,
"sound": "default"
}
}

那么你收到的推送将会是如下:

这里除了我们之前能看到的消息外,还额外增加了title(标题)和subtitle(副标题)的显示,当然如果还想有更复杂的推送显示,在iOS10中也可以通过Notification Service Extension实现,但不在这篇文章介绍。

有时我们需要在点击了推送进入app时根据推送内容进行对应的操作,在iOS10之前在UIApplicationDelegate提供了对应的处理方法,那么如果我们使用UserNotification.Framework又该如何实现这个功能,这里我们需要使用到UNUserNotificationCenterDelegate,UNUserNotificationCenterDelegate提供了两个代理方法,分别为:

1
2
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler

第一个代理方法是当应用处于前台时,收到推送时响应的代理方法,第二个代理方法是在点击了推送或者点击了推送的Action会响应的代理方法。

当然我们首先需要在注册推送的时候添加如下代码:

1
[[UNUserNotificationCenter currentNotificationCenter]setDelegate:self];

首先介绍第一个方法,当我们的程序正处于前台运行时候,这时候如果服务端向app发送了一个推送,那么我们的app将会响应到- (void)userNotificationCenter:(UNUserNotificationCenter )center willPresentNotification:(UNNotification )notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler,这里我们可以看到该方法有三个参数:

第一个center就是我们注册推送使用的UNUserNotificationCenter。

第二个参数notification就是我们收到的推送对象UNNotification,这里系统将推送信息整理成一个对象给我们处理,比以前直接传递一个Dictionary要有好很多,UNNotification中的个字段的含义就不一一说明了。

第三个参数completionHandler是一个block,在iOS10以前如果当系统App处于前台时收到推送,系统只会向App发出推送信息,但不会在界面上弹出推送提示语,但是现在我们可以使用completionHandler使我们处于前台时系统也会弹出推送提示信息,我们需要做的就是在这个代理方法中针对想要弹出提示框的推送执行如下代码:

1
completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert);

那么当我们App处于前台时系统也会弹出提示信息,如图:

这样我们App处于前台时也能让系统弹出推送的提示信息。
UNNotificationPresentationOptionBadge,UNNotificationPresentationOptionSound,UNNotificationPresentationOptionAlert分别代表红点,声音和提示语,大家可以自行测试其功能。

第二个代理方法- (void)userNotificationCenter:(UNUserNotificationCenter )center didReceiveNotificationResponse:(UNNotificationResponse )response withCompletionHandler:(void(^)())completionHandler,当我们点击了推送的提示信息后,App将启动(如果App不处于前台)并执行该方法,这个代理方法也有三个参数:

第一个center就是我们注册推送使用的UNUserNotificationCenter。

第二个response我们点击推送后收到的UNNotificationResponse对象,UNNotificationResponse有两个变量,一个notification就是我们对应的UNNotification,另一个actionIdentifier是一个字符串,功能在后面说明。

第三个completionHandler是一个block,具体功能还在研究。
在该代理中我们添加如下代码:

1
2
3
4
5
6
7
8
9
10
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{
NSString *lString=@"点击了通知";
UIAlertController *lAlertController=[UIAlertController alertControllerWithTitle:lString message:nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *lOKAction=[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}];
[lAlertController addAction:lOKAction];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:lAlertController animated:YES completion:nil];
completionHandler();
}

当我们点击了推送之后,App将启动,并弹出一个Alert,效果如下:

在iOS8的时候苹果提供了UIUserNotificationCategory,UIUserNotificationAction等相关类,可以在当程序在后台收到推送,对应的按钮或者输入文字后进入App进行不同的操作,效果如下:
当我们后台发送如下的推送信息:

1
2
3
4
5
6
7
8
9
10
11
12
{
"aps": {
"alert": {
"title": "这是一个标题",
"subtitle": "这是一个副标题",
"body": "你收到一个内容"
},
"badge": 1,
"sound": "default",
"category":"category.test"
}
}

在收到推送后下拉这个推送:

点击confirm按钮:

如果点击text按钮:

点击send:

那么我们使用UserNotification.Framework,要如何实现该功能呢?
首先我们需要在注册通知的时候同时注册NotificationCategoriy:

1
[self registerNotificationCategory];

registerNotificationCategory方法如下:

1
2
3
4
5
6
7
8
9
10
11
static NSString *kCategoryTestKey=@"category.test";
static NSString *kCategoryTestInputKey=@"category.test.input";
static NSString *kCategoryTestConfirmKey=@"category.test.confirm";
-(void)registerNotificationCategory{
UNTextInputNotificationAction *lTextAction=[UNTextInputNotificationAction actionWithIdentifier:kCategoryTestInputKey title:@"text" options:UNNotificationActionOptionForeground textInputButtonTitle:@"send" textInputPlaceholder:@"please"];
UNNotificationAction *lConfirmAction=[UNNotificationAction actionWithIdentifier:kCategoryTestConfirmKey title:@"Confirm" options:UNNotificationActionOptionForeground];
UNNotificationCategory *lCategory=[UNNotificationCategory categoryWithIdentifier:kCategoryTestKey actions:@[lTextAction,lConfirmAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone];
[[UNUserNotificationCenter currentNotificationCenter]setNotificationCategories:[NSSet setWithObjects:lCategory, nil]];
}

这里我们为UNUserNotificationCenter设置了一个identifier为category.test的category,这个category包含一个标题为text的文字输入的action(identifier为category.test.input),一个标题为confirm的按钮action(identifier为category.test.confirm)。

那么我们该如何处理各种action操作后需要执行的行为,这里我就要用到之前我们提到的UIApplicationDelegate中的第二个代理方法- (void)userNotificationCenter:(UNUserNotificationCenter )center didReceiveNotificationResponse:(UNNotificationResponse )response withCompletionHandler:(void(^)())completionHandler,之前我们提到第二个参数response还有一个actionIdentifier属性,这个属性就代表我们注册的category中各自action的identifier,那么我修改代理方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{
NSString *lString=@"点击了通知";
if ([response.actionIdentifier isEqualToString:kCategoryTestInputKey]) {
UNTextInputNotificationResponse *inputResponse=(UNTextInputNotificationResponse *)response;
lString=[NSString stringWithFormat:@"点击了input,输入内容为:%@",inputResponse.userText];
}else if([response.actionIdentifier isEqualToString:kCategoryTestConfirmKey]){
lString=@"点击了confirm";
}
UIAlertController *lAlertController=[UIAlertController alertControllerWithTitle:lString message:nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *lOKAction=[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}];
[lAlertController addAction:lOKAction];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:lAlertController animated:YES completion:nil];
completionHandler();
}

就可以实现前面同样的功能。

代码下载:UserNotificationsTest

MingleChang

MingleChang

7 Artikel
8 Tags
© 2017 MingleChang
Erstellt mit Hexo
Theme - NexT.Pisces