iOS面试问题
- Ball *ball = [[[[Ball alloc] init] autorelease] autorelease];
感受科技之美
iOS 13系统在今天凌晨疯狂弹窗,这影响了不少用户使用。从用户的反馈来看,这次iOS 13的弹窗影响范围非常的广,而弹窗的信息是:“The iTunes Store is unable to process purchases at this time. Please try again later(iTunes Store目前无法执行购买请求。请稍后再试。)”
除了国外用户外,从一些国内网友反馈的情况看,他们也遇到了这个情况。当然,这个问题显然并不只限于iOS 13才会发生,已经有用户发现在iOS 11和iOS 12也会弹出相同的弹窗,部分iPad机型也没能幸免。
1. 点击“设置”—打开“与App Store”点击id账号退出登录
2. 设置—通用—还原—还原网络设置—关机重启—断开Wifi—启用4G网络—重新登录id即可
不过有部分用户反映自己的手机通过以上的操作之后仍然还是出现弹窗的问题,那么我觉得下一种方法可能更适合你。
卸载iTunes Store即可解决,删除又清爽,又干净,心情也美美的,当然卸载完毕我们还能再次下载回来哦,所以我们也没必要太担心。
目前,苹果还没有对此事进行说明,有遇到这个情况的粉丝吗,欢迎留言讨论。
iOS13.1关闭验证
说重点:三天前,苹果公司正式关闭了 iOS12.4.1 、13.0 和 13.1.1 系统验证,唯独iOS 13.1没有关闭系统验证,为什么跳版本关闭,原因未知,但!今天,苹果公司正式关闭了iOS13.1系统,意味着你无法升降该系统版本,你只能刷iOS13.1.2系统。
iOS7可以下载没有任何问题,iOS8发现挂在官网上的企业版的app点击了提示是否安装应用程序,但是确认以后没有反应,找了很久,都没有发现问题。后来查看了的device console发现安装的时候出现
<span style="font-size: 24px;"><code><span style="color: rgb(255, 0, 0);"><span class="typ">LoadExternalDownloadManifestOperation</span><span class="pun">:</span><span class="pln"> </span><span class="typ">Ignore</span><span class="pln"> manifest download</span><span class="pun">,</span><span class="pln"> already have bundleID</span><span class="pun">:</span><span class="pln"> com</span><span class="pun">.</span><span class="pln">mycom</span><span class="pun">.</span><span class="typ">MyApp</span></span></code></span>
后来查资料外国开发者推测是iOS8的一个bug:
The biggest issue for us is that we can not reproduce this onany of our devices. Our suspicion is that iOS 8 has some internalcache with bundle IDs and just doesn’t install a build if it thinksthat an app with this bundle ID is already installed. As theinstallation doesn’t even start, we think that iOS is matching thebundle identifier from the manifest plist against this cache.
它会寻找是否ios缓存的identifier与bundle identifier在plist文件中匹配,如果匹配,它会认为已经安装了,就不会有反应。 上面解释的很清楚。所以解决办法就是在plist文件中修改bundle Identifier。
比如你的plist文件的BundleID是com.mycom.MyApp,则修改成com.mycom.MyApp.fixios8。(创建一个假的bundleID,可以随便取,这样ios就不会认为你已经安装。记住是修改plist文件的bundleID,不是应用程序的bundleID)
发布以后就发现可以了。只是如果你已经安装了app,则会出现一个新的下载的空白icon,因为这个app的bundleID与你plist的bundleID不一致,当下载完成后,会覆盖原本app,因为它会检测到下载安装的app的bundleID已经存在并且覆盖。
完美解决。
有两种方法
1、注册手势
用 UIGestureRecognizer 的好处在于有现成的手势,开发者不用自己计算手指移动轨迹。 UIGestureRecognizer 的衍生类別有以下几种:
• UITapGestureRecognizer
• UIPinchGestureRecognizer
• UIRotationGestureRecognizer
• UISwipeGestureRecognizer
• UIPanGestureRecognizer
• UILongPressGestureRecognizer
从命名上不难了解這些类別所对应代表的手势,分別是 Tap (点一下)、 Pinch (二指往內或往外拨动)、 Rotation (旋转)、 Swipe (滑动,快速移动)、 Pan (拖移,慢速移动)以及 LongPress (长按)。這些手势別在使用上也很简单,只要在使用前定义并添加到对应的视图上即可。
2、重新UIView触摸响应事件
– ( void )touchesBegan:( NSSet *)touches withEvent:( UIEvent *)event
{
[ super touchesBegan :touches withEvent :event];
}
– ( void )touchesMoved:( NSSet *)touches withEvent:( UIEvent *)event
{
[ super touchesMoved :touches withEvent :event];
}
– ( void )touchesEnded:( NSSet *)touches withEvent:( UIEvent *)event
{
[ super touchesEnded :touches withEvent :event];
}
当手指接触屏幕时,就会调用 touchesBegan:withEvent 方法;
当手指在屏幕上移时,动就会调用 touchesMoved:withEvent 方法;
当手指离开屏幕时,就会调用 touchesEnded:withEvent 方法;
//
注意:采用重写方式,不要用手势否则会获取不到touchesEnded事件。
项目中有个需求是想拿到app里所有在屏幕上的点击坐标
解决方案创建一个子类继承自UIApplication
,然后在sendEvent
方法中获取并判断
#import “MRApplication.h”
#include <CommonCrypto/CommonCrypto.h>
@interface MRApplication()
@property(nonatomic,assign) BOOL isMoved;
@end
@implementation MRApplication
– (void)sendEvent:(UIEvent *)event{
if (event.type==UIEventTypeTouches) {
UITouch *touch = [event.allTouches anyObject];
if (touch.phase == UITouchPhaseBegan) {
self.isMoved = NO;
}
if (touch.phase == UITouchPhaseMoved) {
self.isMoved = YES;
}
if (touch.phase == UITouchPhaseEnded) {
if (!self.isMoved && event.allTouches.count == 1) {
UITouch *touch = [event.allTouches anyObject];
CGPoint locationPointWindow = [touch preciseLocationInView:touch.window];
NSLog(@”TouchLocationWindow:(%.1f,%.1f)”,locationPointWindow.x,locationPointWindow.y);
}
self.isMoved = NO;
}
}
[super sendEvent:event];
}
@end
其实在touch
对象中已经有了View的信息,如果想获取在view中的相对坐标也可以.使用touch.view
即可
CGPoint locationPointWindow = [touch preciseLocationInView:touch.view];
注意:这个MRApplication需要在main.m
中引入,然后就可以拦截整个app所有的点击事件了,其中我对滑动和多点触控做了处理,不加if
判断是会拿到滑动和多点触控时的UIEvent
的
pointInside和hitTest区别:
hitTest和pointInside是UIView提供的触摸事件处理方法。
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event: 用来判断触摸点是否在控件上
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event: 用来判断控件是否接受事件以及找到*合适的view
事件处理流程:
(1)当用户点击屏幕时,会产生一个触摸事件,系统会将该事件加入到一个由UIApplication管理的事件队列中
(2)UIApplication会从事件队列中取出*前面的事件进行分发以便处理,通常,先发送事件给应用程序的主窗口(UIWindow)
(3)主窗口会调用hitTest:withEvent:方法在视图(UIView)层次结构中找到一个*合适的UIView来处理触摸事件
(hitTest:withEvent:其实是UIView的一个方法,UIWindow继承自UIView,因此主窗口UIWindow也是属于视图的一种)
hitTest:withEvent:方法处理流程:
(1)首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内:
若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil
若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕:
(2)
若*次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束
若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self)
(4)*终,这个触摸事件交给主窗口的hitTest:withEvent:方法返回的视图对象去处理
使用示例:
(1)扩大Button的点击区域(上下左右各增加20)
重写自定义Button的pointInside方法:
– (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (CGRectContainsPoint(CGRectInset(self.bounds, -20, -20), point)) {
return YES;
}
return NO;
}
(2)子view超出了父view的bounds响应事件
正常情况下,子View超出父View的bounds的那一部分是不会响应事件的,要解决这个问题,需要重写父View的pointInside方法:
– (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL flag = NO;
for (UIView *view in self.subviews) {
if (CGRectContainsPoint(view.frame, point)){
flag = YES;
break;
}
}
return flag;
}
(3)如果Button被View盖住了,在点击View时,希望该Button能够响应事件
方案1:点击View及View的非交互子View(例如UIImageView),则该Button可以响应事件
– (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL next = YES;
for (UIView *view in self.subviews) {
if ([view isKindOfClass:[UIControl class]]) {
if (CGRectContainsPoint(view.frame, point)){
next = NO;
break;
}
}
}
return !next;
}
方案2:
– (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint btnP = [self convertPoint:point toView:self.btn];
if ([self.btn pointInside:btnP withEvent:event]) {
//如果触点在后面按钮上,可以返回按钮,让按钮响应事件
return self.btn;
}else{
//如果不在就按照系统默认做法
return [super hitTest:point withEvent:event];
}
}
1、创建线程
detach方法:直接创建并启动一个线程,由于没有返回值,如果需要获取新创建的线程,需要在执行的selector方法中调用-[NSThread currentThread]获取。
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
init方法:初始化线程,并返回NSThread对象,线程创建出来之后需要手动调用-start方法启动。
– (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
– (instancetype)initWithBlock:(void (^)(void))block;
2、线程操作
创建好线程之后需要对线程进行操作,NSThread给线程提供的主要操作方法有启动、睡眠、取消、退出
1、启动
我们使用init方法将线程创建出来之后, 线程并不会立即运行, 只有我们手动调用-start方法之后才会启动线程(detachNewThread创建的线程会自动启动)。
– (void)start;
注意: 部分线程属性需要在启动前设置 , 线程启动之后再设置会无效. 如qualityOfService属性。
2、睡眠
NSThread提供了2种让线程睡眠的方法,一个是根据NSDate传入睡眠时间,一个是直接传入NSTimeInterval(睡眠时间)。
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
这两个方法作用一样,相当于执行一个sleep的任务。在执行过程中,即使有其他任务传入runloop,runloop也不会立即响应,必须sleep任务完成之后,才会响应其他任务。
调用睡眠方法后,线程会立即让出当前时间片,让出CPU资源,进入阻塞状态。
3、取消
对于线程的取消,NSThread提供了一个取消的方法和一个属性。
– (void)cancel;
@property (readonly, getter=isCancelled) BOOL cancelled;
调用-cancel方法并不会立刻取消线程,它仅仅是将cancelled属性设置为YES。cancelled也仅仅是一个用于记录状态的属性。线程取消的功能需要我们在main函数中自己实现。
要实现取消的功能,我们需要自己在线程的main函数中定期检查isCancelled状态来判断线程是否需要退出,当isCancelled为YES的时候,我们手动退出。如果我们没有在main函数中检查isCancelled状态,那么调用cancel将没有任何意义。
– (void)onThread {
for (int i = 0; i < 5; i++) {
//睡眠1秒
[NSThread sleepForTimeInterval:1];
//判断是否已经取消
if (NSThread.currentThread.isCancelled) {
return;
}
}
}
4、退出
与充满不确定性的-cancel相比,+exit函数可以让线程立即退出。
+ (void)exit;
+exit 调用之后会立即终止线程,即使任务还没有执行完成也会中断,这就非常有可能导致内存泄漏等严重问题,所以一般不推荐使用。
– (void)onThread {
for (int i = 0; i < 5; i++) {
//睡眠1秒
[NSThread sleepForTimeInterval:1];
if (i == 3) {
[NSThread exit];
}
}
}
对于有runloop的线程,可以使用CFRunLoopStop()结束runloop配合-cancel结束线程
– (void)onThread {
// 给线程设置名字
[[NSThread currentThread] setName:@”myThread”];
// 给线程添加runloop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 给runloop添加数据源
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 检查isCancelled
While(![[NSThread currentThread] isCancelled]) {
// 启动runloop
[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10];
}
}
5、线程状态
//判断线程是否正在执行
@property (readonly, getter=isExecuting) BOOL executing;
//判断线程是否结束
@property (readonly, getter=isFinished) BOOL finished;
//判断线程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
3、线程通讯
线程准备好之后,经常需要从主线程把耗时的任务丢给辅助线程,当任务完成之后辅助线程再把结果传回主线程,这些线程通讯一般用的都是perform方法:
// 1、将selector丢给主线程执行, 可以指定runloop mode
– (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
// 2、将selector丢给主线程执行, runloop mode默认为common mode
– (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// 3、创建一个后台线程执行selector
– (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
// 4、将selector丢给指定线程执行, 可以指定runloop mode
– (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AV;
// 5、将selector丢给指定线程执行, runloop mode默认为default mode
– (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
所以我们一般用3、4、5方法将任务丢给辅助线程,任务执行完成之后再使用1、2方法将结果传回主线程;
注意: perform方法只对拥有runloop的线程有效,如果创建的线程没有添加runloop,perform的selector将无法执行;
4、线程优先级
NSThread有4个优先级的API:
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
@property double threadPriority;
@property NSQualityOfService qualityOfService
前两个是类方法, 用于设置和获取当前线程的优先级。
threadPriority属性可以通过对象来设置和获取优先级。
由于线程优先级是一个比较抽象的东西,没人知道0.5和0.6到底有多大区别,所以iOS8之后新增了qualityOfService枚举属性,大家可以通过枚举值设置优先级。
typedef NS_ENUM(NSInteger, NSQualityOfService) {
// *高优先级, 主要用于直接参与提供交互式UI的工作, 例如: 处理点击事件或绘制图像到屏幕上
NSQualityOfServiceUserInteractive = 0x21,
// 次高优先级, 用于执行用户明确请求的工作, 并且必须立即显示结果, 以便进行进一步的用户交互
NSQualityOfServiceUserInitiated = 0x19,
// 默认优先级, 当没有设置优先级的时候, 线程默认优先级
NSQualityOfServiceUtility = 0x11,
// 普通优先级,主要用于不需要立即返回的任务
NSQualityOfServiceBackground = 0x09,
// 后台优先级, 用于完全不紧急的任务
NSQualityOfServiceDefault = -1
};
一般主线程和没有设置优先级的线程都是默认优先级。
5、主线程和当前线程
NSThread提供了非常方便的获取和判断主线程的API:
// 判断当前线程是否是主线程
@property (readonly) BOOL isMainThread;
@property (class, readonly) BOOL isMainThread;
// 获取主线程的thread
@property (class, readonly, strong) NSThread *mainThread;
// 获取当前线程
@property (class, readonly, strong) NSThread *currentThread;
6、线程通知
NSThread有三个线程相关的通知:
// 由当前线程派生出*个其他线程时发送, 一般一个线程只发送一次
FOUNDATION_EXPORT NSNotificationName const NSWillBecomeMultiThreadedNotification;
// 这个通知目前没有实际意义, 可以忽略
FOUNDATION_EXPORT NSNotificationName const NSDidBecomeSingleThreadedNotification;
// 线程退出之前发送这个通知
FOUNDATION_EXPORT NSNotificationName const NSThreadWillExitNotification;
7、解决线程冲突
同步代码块
使用@synchronized包围的代码即为同步代码块,它可以防止不同的线程同时执行同一段代码。
– (void)onThread {
@synchronized (self) {
self.money += 10;
[NSThread sleepForTimeInterval:0.1];
}
}
锁机制
创建一个NSLock类的锁对象,lock方法用于加锁,如果锁被其他对象占用则线程被阻塞,unlock方法用于解锁,以便其他线程加锁。
self.lock = [[NSLock alloc] init];
– (void)onThread {
[self.lock lock];
[NSThread sleepForTimeInterval:0.1];
self.money -= 10;
[self.lock unlock];
}
线程间通信
线程的调度对于开发者来说是透明的,我们不能也无法预测线程执行的顺序,但有时我们需要线程按照一定条件来执行,这时就需要线程间进行通信,NSCondition提供了线程间通信的方法。
@interface NSCondition : NSObject <NSLocking> {
@private
void *_priv;
}
/*
调用NSCondition对象wait方法的线程会阻塞,直到其他线程调用该对象的signal方法或broadcast方法来唤醒
唤醒后该线程从阻塞态改为就绪态,交由系统进行线程调度
执行wait方法时内部会自动执行unlock方法释放锁,并阻塞线程
*/
– (void)wait;
//同上,只是该方法是在limit到达时唤醒线程
– (BOOL)waitUntilDate:(NSDate *)limit;
/*
唤醒在当前NSCondition对象上阻塞的一个线程
如果在该对象上wait的有多个线程则随机挑选一个,被挑选的线程则从阻塞态进入就绪态
*/
– (void)signal;
/*
同上,该方法会唤醒在当前NSCondition对象上阻塞的所有线程
*/
– (void)broadcast;
@property (nullable, copy) NSString *name;
@end
NSCondition实现了NSLocking协议,所以NSCondition同样具有锁的功能,与NSLock一样可以加锁与解锁的操作。
self.condition = [[NSCondition alloc] init];
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(onThread1) object:nil];
[thread1 start];
NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(onThread2) object:nil];
[thread2 start];
– (void)onThread1 {
while (true)
{
//首先使用condition上锁,如果其他线程已经上锁则阻塞
[self.condition lock];
if (self.haveMoney)
{
self.haveMoney = NO;
//唤醒其他在condition上等待的线程
[self.condition broadcast];
}
else
{
//如果没钱则等待,并阻塞
[self.condition wait];
//如果阻塞的线程被唤醒后会继续执行代码
}
//释放锁
[self.condition unlock];
}
}
– (void)onThread2 {
while (true)
{
//首先使用condition上锁,如果其他线程已经上锁则阻塞
[self.condition lock];
if (!self.haveMoney)
{
self.haveMoney = YES;
//唤醒其他在condition上等待的线程
[self.condition broadcast];
}
else
{
//如果有钱则等待,并阻塞
[self.condition wait];
//如果阻塞的线程被唤醒后会继续执行代码
}
//释放锁
[self.condition unlock];
}
}
方案1:
使用self.tabBarController.tabBar.hidden来控制当前页面tabBar的显示和隐藏
self.tabBarController.tabBar.hidden = YES;
方案2:
使用hidesBottomBarWhenPushed来控制push出来页面tabBar的显示和隐藏
UIViewController *ctrl = [[UIViewController alloc] init];
ctrl.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController: ctrl animated:YES];
问题:UITabBarController从根页面push到下级页面后,确认下级页面的hidesBottomBarWhenPushed已经设置为YES,当下级页面的输入框收起键盘时,tabBar会再次出现。
解决方案1:(iOS12)
[[UITabBar appearance] setTranslucent:NO];
解决方案2:
当使用self.tabBarController.tabBar.hidden后,再调用hidesBottomBarWhenPushed,push出来的页面键盘隐藏时tabBar会再次出现,避免同时使用这两种隐藏tabbar的方案。
本文主要介绍使用UIBezierPath,CAShapeLayer和CABasicAnimation 来自定义具有动画效果的弹出提示框(AlertView)。
1.在这里alertView继承的是UIWindow,
(1)基本思路是:在自定义的window添加一个子视图UIView,
(2)在view的层上添加一个子层CAShapeLayer,在子层上绘制UIBezierPath路径,往层上添加CABasicAnimation动画,动画按路径运行
(3)往window上添加两个UILabel显示消息标题和内容,添加一个取消按钮和一个确定按钮
2.实现步骤
(1)XSAlertView.h
<span style=”font-size:18px;”>typedef enum : NSInteger{
/**
* 点击确定按钮
*/
XSAlertViewClickOk,
/**
* 点击取消按钮
*/
XSAlertViewClickCancel
} XSAlertViewClickType;
/**
* 提示框的样式
*/
typedef enum : NSInteger {
XSAlertviewStyleDefault, // 默认样式—-成功
XSAlertviewStyleSuccess, // 成功
XSAlertviewStyleFail, // 失败
XSAlertviewStyleWaring // 警告
} XSAlerTviewStyle;
typedef void(^Completion)(XSAlertViewClickType buttonIndex);
@interface XSAlertView : UIWindow
{
}
// 按钮单击回调block
@property (nonatomic,strong)Completion completeBlock;
/**
* 单例
*
* @return 返回XSAlertView对象
*/
+ (id)shared;
/**
* 创建AlertView并展示
*
* @param style 绘制的图片样式
* @param title 警示标题
* @param message 警示内容
* @param cancel 取消按钮标题
* @param ok 确定按钮标题
* @param completion 按钮点击时回调
*
* @return 返回XSAlertView
*/
+ (id)showAlertWithStyle:(XSAlerTviewStyle)style title:(NSString *)title message:(NSString *)message cancelButtonTitle:(NSString *)cancel okButtonTitle:(NSString *)ok completion:(Completion)completion;</span></span>
(2)创建单例,节约内存
<span style=”font-size:18px;”>static XSAlertView *alertView = nil;
+(id)shared {
// 同步锁
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
alertView = [[XSAlertView alloc]init];
});
return alertView;
}</span>
(3)创建UIView,用于绘制logo
<span style=”font-size:18px;”>- (void)_logoInit {
// 移除上一次绘制
[_logoView removeFromSuperview];
_logoView = nil;
// 创建新画布
_logoView = [[UIView alloc]init];
_logoView.center = CGPointMake(self.center.x, self.center.y);
_logoView.bounds = CGRectMake(0.f, 0.f, kScreenWidth / 1.5f, kScreenHeight / 2.5f);
_logoView.backgroundColor = [UIColor colorWithWhite:1.f alpha:0.7f];
_logoView.layer.cornerRadius = 10.f;
_logoView.layer.shadowOffset = CGSizeMake(0.f, 5.f);
_logoView.layer.shadowColor = [UIColor blackColor].CGColor;
_logoView.layer.shadowOpacity = 0.3f; // 不透明度:0表示完全透明
_logoView.layer.shadowRadius = 10.f;
// 保证画布在*底层
if (_titleLabel != nil) {
[self insertSubview:_logoView belowSubview:_titleLabel];
}
else {
[self addSubview:_logoView];
}
}
</span>
(4)往window上添加按钮和标签,此处比较简单,代码简单繁琐,就不列出了。
(5)往_logoView的层上添加层,绘制路径,此处给出绘制提示正确的弹出框的图片代码
<span style=”font-size:18px;”>- (void)_drawRight {
// 初始化画布
[self _logoInit];
// 圆的中心
CGPoint pathCenter = CGPointMake(_logoView.frame.size.width / 2.f, _logoView.frame.size.height / 2.f – 50);
// 画圆(360度的弧线)
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:pathCenter radius:40.f startAngle:0 endAngle:M_PI * 2 clockwise:YES];
// 勾的起点坐标
CGFloat x = _logoView.frame.size.width / 2.5f + 5.f;
CGFloat y = _logoView.frame.size.height / 2.f – 45.f;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineCapRound;
// 画勾的起点
[path moveToPoint:CGPointMake(x, y)];
[path addLineToPoint:CGPointMake(x + 10.f, y + 10.f)];
[path addLineToPoint:CGPointMake(x + 35.f, y – 20.f)];
// 新建层
CAShapeLayer *layer = [[CAShapeLayer alloc]init];
layer.fillColor = [UIColor clearColor].CGColor;
layer.strokeColor = [UIColor greenColor].CGColor;
layer.lineWidth = 5.f;
layer.path = path.CGPath;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@”strokeEnd”];
animation.fromValue = @0;
animation.toValue = @1;
animation.duration = 0.5f;
[layer addAnimation:animation forKey:@”strokeEnd”];
[_logoView.layer addSublayer:layer];
</span>
在这里值得一提的是,CAshapeLayer的strokeStart,strokeEnd与CABasicAnimation 的fromValue,toValue的用法和做出的效果下面是我个人对这个的总结:
<span style=”font-size:18px;”>1 keyPath = strokeStart 动画的fromValue = 0,toValue = 1
strokeEnd默认为1,strokeStart从 0 到 1 ,strokeStart = 0 时有一条完整的路径,strokeStart = 1 时 路径消失,效果就是一条从路径起点到终点慢慢的消失
2 keyPath = strokeStart 动画的fromValue = 1,toValue = 0
strokeEnd默认为1,strokeStart从 1 到 0 ,strokeStart = 1 时无路径,strokeStart = 0 时 画出一条完整的路径,效果就是一条从路径终点到起点慢慢的出现
3 keyPath = strokeEnd 动画的fromValue = 0,toValue = 1
strokeStart默认为0,strokeEnd从 0-1,strokeEnd=0 时,无路径,strokeEnd=1 时,一条完整路径。效果就是一条从路径起点到终点慢慢的出现
4 keyPath = strokeEnd 动画的fromValue = 1,toValue = 0
效果就是反方向路径慢慢消失</span>
(6)对类方法 的实现
<span style=”font-size:18px;”>/**
* 创建AlertView并展示
*
* @param style 绘制的图片样式
* @param title 警示标题
* @param message 警示内容
* @param cancel 取消按钮标题
* @param ok 确定按钮标题
* @param completion 按钮点击时回调
*
* @return 返回XSAlertView
*/</span>
<pre name=”code” class=”objc”><span style=”font-size:18px;”>+(id)showAlertWithStyle:(XSAlerTviewStyle)style title:(NSString *)title message:(NSString *)message cancelButtonTitle:(NSString *)cancel okButtonTitle:(NSString *)ok completion:(Completion)completion {
switch (style) {
// 成功、默认
case XSAlertviewStyleDefault:
case XSAlertviewStyleSuccess:
[[self shared] _drawRight];
break;
// 失败
case XSAlertviewStyleFail:
[[self shared] _drawWrong];
break;
// 警告
case XSAlertviewStyleWaring:
[[self shared] _drawWaring];
default:
break;
}
[[self shared] _addButtonTitleWithCancle:cancel OK:ok];
[[self shared] _addTitle:title message:message];
[[self shared] setCompleteBlock:nil];//释放掉之前的Block
[[self shared] setCompleteBlock:completion];
[[self shared] setHidden:NO];//设置为不隐藏
return [self shared];
</span>
}
(7) *后调用我们自己写的弹出框
<span style=”font-size:18px;”>- (void)show:(UIButton *)button {
NSString * title = nil;
NSString * message = nil;
NSString * cancle = @”取消”;
NSString * ok = @”确定”;
switch (button.tag+1) {
// 成功
case XSAlertviewStyleSuccess:
case XSAlertviewStyleDefault:
title = @”温馨提示”;
message = @”登录成功”;
cancle = nil;
break;
// 失败
case XSAlertviewStyleFail:
title = @”错误提示”;
message = @”您输入的号码有误。”;
break;
// 警告
case XSAlertviewStyleWaring:
title = @”警告”;
message = @”您确定要开抢吗!!”;
default:
break;
}
//为成员变量Window赋值则立即显示Window
_alertWindow = [XSAlertView showAlertWithStyle:button.tag+1 title:title message:message cancelButtonTitle:cancle okButtonTitle:ok completion:^(XSAlertViewClickType buttonIndex) {
if (buttonIndex == 0) {
NSLog(@”确定”);
// …
}
else {
NSLog(@”取消”);
// …
}
//Window隐藏
_sheetWindow.hidden = YES;
//置为nil,释放内存
_sheetWindow = nil;
}];
}
</span>
*终的效
昨天提交苹果商店被拒
Guideline 5.1.1 – Legal – Privacy – Data Collection and Storage
We noticed that your app requests the user’s consent to access their AppTracking Transparency but does not clarify the use of the AppTracking Transparency in the applicable purpose string.
Next Steps
Please revise the relevant purpose string in your app’s Info.plist file to specify why the app is requesting access to the user’s AppTracking Transparency. You can modify your app’s Info.plist file using the property list editor in Xcode.
To help users understand why your app is requesting access to their personal data, all permission request alerts in your app should specify how your app will use the requested feature.
Resources
For additional information and instructions on requesting permission, please review the Requesting Permission section of the iOS Human Interface Guidelines and the Information Property List Key Reference. You may also want to review the Technical Q&A QA1937: Resolving the Privacy-Sensitive Data App Rejection page for details on how to provide a usage description for permission request alerts.
Please see attached screenshot for details.
从邮件中看,原因是使用了用户追踪(IDFA)但是NSUserTrackingUsageDescription的文案不符合规则,不能明确表明使用用户追踪的意图。
经测试,NSUserTrackingUsageDescription中一定要包含数据(data)和广告(ads)两个字段,修改后提交即可通过。
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速 |