0%

iOS 触摸事件分类

  • 触摸事件
  • 加速事件
  • 远程事件

谁能处理触摸事件?

响应者对象

在 iOS 中不是任何对象都能处理事件,只有继承了 UIResponder 的对象才能接收并处理事件.我们称之为响应者对象.

UIApplicationUIViewControllerUIView 都继承自 UIResponder,因此它们都是响应者对象,都能够接收并处理事件.

UIResponder

UIResponder 内部提供了方法来处理事件

  1. 触摸事件

    一次完成的触摸过程,会经历 3 个状态, UIView 的触摸事件处理

    • 一根或多根手指开始触摸 view,系统会自动调用 view 下面的方法:
      1
      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;  //触摸开始
    • 一根或者多根手指在 view 上移动,系统会自动调用 view 下面的方法(随着手指的移动,会持续调用该方法)
      1
      - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;  //触摸移动
    • 一根或者多根手指离开 view,系统会自动调用 view 下面的方法
      1
      - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;  //触摸结束
    • 触摸结束前,某个系统事件(例如电话呼入 )会打断触摸过程,系统会自动调用 view 下面的方法
      1
      - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; //触摸取消(可能会经历)

    4 个触摸事件的处理方法中,都有 NSSet *touches 和 UIEvent *event 两个参数:

    • 一次完整的触摸过程,只会产生一个事件对象,4 个触摸方法都是同一个 event 参数
    • 如果两根手指同时触摸一个 view,那么 view 只会调用一次 touchesBegan:withEvent: 方法,touches 参数中装着两个 UITouch 对象;
    • 如果这两根手指一前一后分开触摸同一个 view,那么 view 会分别调用两次 touchesBegan:withEvent:方法, 并且每次调用时的 touches 参数只包含一个 UITouch 对象;
    • 根据 touches 中 UITouch 个数可以判断出使单点触摸还是多点触摸

    提示:touches 中存放的都是 UITouch 对象。

  2. 加速计事件

    1
    2
    3
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;    
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
  3. 远程控制事件

    1
    - (void)remoteControlReceivedWithEvent:(UIEvent *)event;

UITouch

当用户用一根手指触摸屏幕时,会创建一个与手指相关联的 UITouch 对象;一根手指对应一个 UITouch 对象
UITouch 的作用:

  • 保存跟手指相关的信息,比如触摸的位置、时间、阶段;
  • 当手指移动时,系统会更新同一个 UITouch 对象,使之能够一直保存该手指的触摸位置;
  • 当手指离开屏幕时,系统会销毁相应的 UITouch 对象。

提示:iPhone 开发中,要避免使用双击事件。

UITouch 的属性:
触摸产生时所处的窗口:

1
@property(nonatomic,readonly,retain) UIWindow *window;

触摸产生时所处的视图

1
@property(nonatomic,readonly,retain) UIView *view;

短时间内点按屏幕的次数,可以根据 tapCount 判断单击、双击或更多地点击

1
@property(nonatomic,readonly) NSUInteger tapCount;

记录了触摸事件产生或变化时的时间,单位是秒

1
@property(nonatomic,readonly) NSTimeInterval timestamp;

当前触摸事件所处的状态

1
2
3
4
5
6
7
8
9
10
/*
UITouchPhase 是一个枚举类型,包含:
UITouchPhaseBegan(触摸开始)
UITouchPhaseMoved(接触点移动)
UITouchPhaseStationary(接触点无移动)
UITouchPhaseEnded(触摸结束)
UITouchPhaseCancelled(触摸取消)
*/
@property(nonatomic,readonly) UITouchPhase phase;

UITouch 的方法:

1
- (CGPoint)locationInView:(UIView *)view;

1.返回值表示触摸在 view 上的位置;
2.这里返回的位置是针对 view 坐标系的,(以 view 的左上角为原点(0,0));
3.调用时传入的 view 参数为 nil 的话,返回的是触摸点在 UIWindow 的位置。

1
- (CGPoint)previousLocationInView:(UIView *)view;

该方法记录了前一个触摸点的位置;

UIEvent

每产生一个事件,就会产生一个 UIEvent 对象;
UIEvent:称为事件对象,记录事件产生的时刻和类型。

常见属性:
1.事件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@property(nonatomic,readonly) UIEventType  type;
@property(nonatomic,readonly) UIEventSubtype subtype;

typedef
NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
};

typedef
NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0, // for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,
// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104, UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106, UIEventSubtypeRemoteControlEndSeekingBackward = 107, UIEventSubtypeRemoteControlBeginSeekingForward = 108, UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

2.事件产生的时间

1
@property(nonatomic,readonly) NSTimeInterval  timestamp;

UIEvent 还提供了相应的方法可以获得在某个 view 上面的触摸对象(UITouch)。

触摸事件的产生:

  1. 发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中;
  2. UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow);
  3. 主窗口会在视图层次结构中找到一个最合适的视图控件来处理触摸事件,这也是整个事件处理过程的第一步;
  4. 找到合适的视图控件后,就会调用视图控件的 touches 方法来做具体的事件处理。

触摸事件的传递:

触摸事件的传递是从父控件传递到子控件;
如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件。
触摸事件
触摸事件 2

UIView 不接收触摸事件的三种情况:
不接受用户交互 :

  1. userInteractionEnable = NO;
  2. 隐藏 :hidden = YES;
  3. 透明:alpha = 0.0 ~ 0.01

提示:UIImageView 的 userInteractionEnable 默认就是 NO,因此 UIImageView 以及它的子控件默认是不能接收触摸事件的。

触摸事件处理的详细过程:

  1. 用户点击屏幕后产生的一个触摸事件,经过一些列的传递过程后,会找到最合适的视图控件来处理这个事件
  2. 找到最合适的视图控件后,就会调用控件的 touches 方法来作具体的事件处理
    touchesBegan…
    touchesMoved…
    touchedEnded…

这些 touches 方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理

响应者链的事件传递过程:

  1. 如果 view 的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图;
  2. 在视图层次结构最顶级的视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给 window 对象进行处理。
  3. 如果 window 对象也不处理,则其将事件或消息传递给 UIApplication 对象;
  4. 如果 UIApplication 也不能处理该事件或消息,则将其丢弃

监听触摸事件的做法

如果想监听一个 view 上面的触摸事件,之前的做法是:

  1. 自定义一个 view;
  2. 实现 view 的 touches 方法,在方法内部实现具体处理代码。
    通过 touches 方法监听 view 触摸事件,有很明显的几个缺点:
    • 必须得自定义 view;
    • 由于是在 view 内部的 touches 方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听 view 的触摸事件;
    • 不容易区分用户的具体手势行为。

iOS 3.2 之后,苹果推出了手势识别功能(Gesture Recognizer),在触摸事件处理方面,大大简化了开发者的开发难度。

UIGestureRescognizer

为了完成手势识别,必须借助于手势识别器:UIGestureRecognizer 。
利用 UIGestureRecognizer,能轻松识别用户在某个 view 上面做的一些常见手势。
UIGestureRecognizer 是一个抽象类,定义了所有的手势基本行为,使用它的子类才能处理具体的手势

  • UITapGestureRecognizer(敲击)
  • UIPinchGestureRecognizer(捏合,用于缩放)
  • UIPanGestureRecognizer(拖拽)
  • UISwipeGestureRecognizer(轻扫)
  • UIRotationGestureRecognizer(旋转)
  • UILongPressGestureRecognizer(长按)

每一个手势识别器的用法都差不多,比如 UITapGestureRecognizer 的使用步骤如下:

  1. 创建手势识别器对象;
    1
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
  2. 设置手势识别器对象的具体属性;
    1
    2
    3
    4
    // 连续敲击 2 次
    tap.numberOfTapsRequired = 2;
    // 需要 2 根手指一起敲击
    tap.numberOfTouchesRequired = 2;
  3. 添加手势识别器到对应的 view 上
    1
    [self.iconView addGestureRecognizer:tap];
  4. 监听手势的触发
    1
    [tap addTarget:self action:@selector(tapIconView:)];

手势识别的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
// 没有触摸事件发生,所有手势识别的默认状态
UIGestureRecognizerStatePossible,
// 一个手势已经开始但尚未改变或者完成时
UIGestureRecognizerStateBegan,
// 手势状态改变
UIGestureRecognizerStateChanged,
// 手势完成
UIGestureRecognizerStateEnded,
// 手势取消,恢复至 Possible 状态
UIGestureRecognizerStateCancelled,
// 手势失败,恢复至 Possible 状态
UIGestureRecognizerStateFailed,
// 识别到手势识别
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

记录 iOS 各种重要里程碑事件

私有成员变量的实现

1.0 时代,在.h 文件采用 @private 关键词

1
2
3
4
@interface ViewController : UIViewController {
@private
NSInteger _value;
}

2.0 时代 通过在.m 文件通过 匿名Category

1
2
3
4
5
@interface ViewController ()

@property (nonatomic) NSInteger value;

@end

3.0 时代 2013 年的 WWDC 允许在 .m 的 @implementation

1
2
3
@implementation ViewController {
NSInteger _value;
}

ARC 推出

2011

自动生成 getter 和 setter 方法的 @synthesize 2012

AutoLayout 引入

iOS 6.0

Swift

  • WWDC 1.0 版本发布 – 2014.06.02
  • Swift 2.0 发布 – 2015.08.07
  • Open Source – 2015.12.04

Size Classes

iOS 8.0

Blocks

Mac OS X 10.6 “Snow Leopard” and iOS 4.0

Objective-C 2.0

At the 2006 Worldwide Developers Conference release of Objective-C 2.0

Watch OS

2015.05.21

iOS API Differences

iOS 2.1 to iOS 2.2 API Differences

Added frameworks:

  • AVFoundation

iOS 2.2 to iOS 3.0 API Differences

Added frameworks:

  • CoreData
  • ExternalAccessory
  • GameKit
  • MapKit
  • MessageUI
  • MobileCoreServices
  • StoreKit

iOS 3.2 to iOS 4.0 API Differences

Added frameworks:

  • Accelerate
  • AssetsLibrary
  • CoreMedia
  • CoreMotion
  • CoreTelephony
  • CoreVideo
  • EventKit
  • EventKitUI
  • iAd
  • ImageIO
  • QuickLook

iOS 4.3 to iOS 5.0 API Differences

Added frameworks

  • Accounts
  • CoreBluetooth
  • CoreImage
  • GLKit
  • GSS
  • NewsstandKit
  • Twitter

5.1 to iOS 6.0 API Differences

Added frameworks

  • AdSupport
  • MediaToolbox
  • PassKit
  • Social

6.1 to iOS 7.0 API Differences

Added frameworks

  • GameController
  • JavaScriptCore
  • MediaAccessibility
  • MultipeerConnectivity
  • SafariServices
  • SpriteKit

iOS 7.1 to iOS 8.0 API Differences

Added frameworks

  • Accelerate
  • Accounts
  • AddressBook
  • AddressBookUI
  • AudioToolbox
  • AudioUnit
  • AVFoundation
  • AVKit (Added)
  • CFNetwork
  • CloudKit (Added)
  • CoreAudio
  • CoreAudioKit (Added)
  • CoreAuthentication (Added)
  • CoreBluetooth
  • CoreData
  • CoreFoundation
  • CoreImage
  • CoreLocation
  • CoreMedia
  • CoreMotion
  • CoreText
  • CoreVideo
  • EventKit
  • EventKitUI
  • ExternalAccessory
  • Foundation
  • GameController
  • GameKit
  • GLKit
  • GSS
  • HealthKit (Added)
  • HomeKit (Added)
  • iAd
  • ImageIO
  • IOKit
  • JavaScriptCore
  • LocalAuthentication (Added)
  • MapKit
  • MediaAccessibility
  • MediaPlayer
  • MessageUI
  • Metal (Added)
  • MobileCoreServices
  • MultipeerConnectivity
  • NetworkExtension (Added)
  • NewsstandKit
  • NotificationCenter (Added)
  • OpenGLES
  • PassKit
  • Photos (Added)
  • PhotosUI (Added)
  • PushKit (Added)
  • QuartzCore
  • QuickLook
  • SceneKit (Added)
  • Security
  • Social
  • SpriteKit
  • StoreKit
  • UIKit
  • VideoToolbox
  • WebKit (Added)

iOS 8.3 to iOS 9.0 API Differences

Added frameworks

Objective-C

  • /usr/include
  • Accelerate
  • Accounts
  • AddressBook
  • AddressBookUI
  • AssetsLibrary
  • AudioToolbox
  • AudioUnit
  • AVFoundation
  • AVKit
  • CFNetwork
  • CloudKit
  • Contacts (Added)
  • ContactsUI (Added)
  • CoreAudio
  • CoreAudioKit
  • CoreBluetooth
  • CoreData
  • CoreFoundation
  • CoreGraphics
  • CoreImage
  • CoreLocation
  • CoreMedia
  • CoreMIDI
  • CoreMotion
  • CoreSpotlight (Added)
  • CoreTelephony
  • CoreText
  • CoreVideo
  • EventKit
  • EventKitUI
  • ExternalAccessory
  • Foundation
  • GameController
  • GameKit
  • GameplayKit (Added)
  • GLKit
  • GSS
  • HealthKit
  • HomeKit
  • iAd
  • ImageIO
  • JavaScriptCore
  • LocalAuthentication
  • MapKit
  • MediaPlayer
  • MediaToolbox
  • MessageUI
  • Metal
  • MetalKit (Added)
  • MetalPerformanceShaders (Added)
  • MobileCoreServices
  • ModelIO (Added)
  • MultipeerConnectivity
  • NetworkExtension
  • NewsstandKit
  • OpenAL
  • PassKit
  • Photos
  • PushKit
  • QuartzCore
  • QuickLook
  • ReplayKit (Added)
  • SafariServices
  • SceneKit
  • Security
  • SpriteKit
  • StoreKit
  • SystemConfiguration
  • UIKit
  • VideoToolbox
  • WatchConnectivity (Added)
  • WatchKit
  • WebKit

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

UIImag 构造方式

UIImag 构造方式大致有 4 种方式

  • 从本地 bundle 中加载 imageNamed: ,传入一个 bundle 的文件名即可
  • 从本地一个文件路径读取 imageWithContentsOfFile: ,需要传一个文件的文件路径 path
  • 通过二进制数据 NSData 来创建 imageWithData: * 通过一个 CoreGraphicsCGImageRef 来创建, initWithCGImage: * 通过一个 CoreImageCIImage 来创建 initWithCIImage 通过查阅 Apple 官网文档我们发现有 2 个这样的方法,今天就来一探究竟
1
2
3
4
5
+ (UIImage *)imageWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(4_0);
+ (UIImage *)imageWithCIImage:(CIImage *)ciImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(6_0);

- (instancetype)initWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(4_0);
- (instancetype)initWithCIImage:(CIImage *)ciImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(6_0);

2 个类方法 2 个实例方法都是类似,这里以 CGImageRef 为例

1
+ (UIImage *)imageWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation NS_AVAILABLE_IOS(4_0);
  1. 新建的 Xcode 工程选择 single Application

  2. 在 storyboard 中拖一个 UIImageView 设置它水平垂直居中对齐,宽带高度随便设一个值不要太大就行,设置 UIImageViewcontentModeAspect Fit 方便查看以免变形

  3. UIImageView 下发放一个 UIButton 控件方便后面好对图片进行旋转操作

  4. 在 viewController 中建立一个 UIImageView 引用,拉出一个 rotate 按钮的 IBAction 现在大概界面大概这样
    UIImageOrientation 效果图

  5. 下面我们实现
    - (IBAction)rotateImage:(id)sender {} 这个方法

在这里我们想通过点击按钮实现图片旋转
为了方便使用我们使用 Category 的方式实现
新建一个 UIImage 的分类取名叫 Rotate

这里需要传一张要处理的图片和一个待处理成的图片方向

1
2
+ (UIImage *)rotateImage:(UIImage *)oldImage
orientation:(UIImageOrientation)orientation;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    + (UIImage *)rotateImage:(UIImage *)oldImage orientation:(UIImageOrientation)orientation{

UIImage *newImage = [UIImage imageWithCGImage:oldImage.CGImage scale:1 orientation:orientation];

NSString *orientationStr = nil;
switch (orientation) {
case UIImageOrientationUp: {
orientationStr = @"UIImageOrientationUp";
break;
}
case UIImageOrientationDown: {
orientationStr = @"UIImageOrientationDown";
break;
}
case UIImageOrientationLeft: {
orientationStr = @"UIImageOrientationLeft";
break;
}
case UIImageOrientationRight: {
orientationStr = @"UIImageOrientationRight";
break;
}
case UIImageOrientationUpMirrored: {
orientationStr = @"UIImageOrientationUpMirrored";
break;
}
case UIImageOrientationDownMirrored: {
orientationStr = @"UIImageOrientationDownMirrored";
break;
}
case UIImageOrientationLeftMirrored: {
orientationStr = @"UIImageOrientationLeftMirrored";
break;
}
case UIImageOrientationRightMirrored: {
orientationStr = @"UIImageOrientationRightMirrored";
break;
}

}

NSLog(@"current orientation: %@",orientationStr);

return newImage;
}

在 button 点击事件触发时的这样使用

1
2
3
4
5
6
7
8
- (IBAction)rotateImage:(id)sender {

UIImage *oldImage = self.imgView.image;

UIImage *rotatedImage = [UIImage rotateImage:oldImage orientation:UIImageOrientationLeft];

self.imgView.image = rotatedImage;
}

点击按钮测试发现第一次没问题,但是重逢点击无效
原来 + (UIImage *)imageWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale orientation:(UIImageOrientation)orientation 方法执行原理是执行前通过 @property(nonatomic,readonly) UIImageOrientation imageOrientation; 接口先判断当前图片的方向是否为将要旋转的方向,如果是就直接返回不做处理,如果不是再作旋转处理,也就是说这个方法并没有实际上旋转 image 的数据,只是用一个枚举标记旋转的状态

如果我们想每次旋转需要直接改变原始 image 的数据该怎么办呢?
在这里我们通过 CGBitmapContext ,使用 CGContextRotateCTM 来设置旋转,再把 UIImage 通过 drawInRect 重新绘制出来,通过 UIGraphicsGetImageFromCurrentImageContext 获得处理后的图片

下面是具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
- (UIImage *)fixedRotation{
if (self.imageOrientation == UIImageOrientationUp) return self;
CGAffineTransform transform = CGAffineTransformIdentity;

switch (self.imageOrientation) {
case UIImageOrientationDown:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, self.size.width, self.size.height);
transform = CGAffineTransformRotate(transform, M_PI);
break;

case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
transform = CGAffineTransformTranslate(transform, self.size.width, 0);
transform = CGAffineTransformRotate(transform, M_PI_2);
break;

case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, 0, self.size.height);
transform = CGAffineTransformRotate(transform, -M_PI_2);
break;
case UIImageOrientationUp:
case UIImageOrientationUpMirrored:
break;
}

switch (self.imageOrientation) {
case UIImageOrientationUpMirrored:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, self.size.width, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;

case UIImageOrientationLeftMirrored:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, self.size.height, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
case UIImageOrientationUp:
case UIImageOrientationDown:
case UIImageOrientationLeft:
case UIImageOrientationRight:
break;
}

// Now we draw the underlying CGImage into a new context, applying the transform
// calculated above.
CGContextRef ctx = CGBitmapContextCreate(NULL, self.size.width, self.size.height,
CGImageGetBitsPerComponent(self.CGImage), 0,
CGImageGetColorSpace(self.CGImage),
CGImageGetBitmapInfo(self.CGImage));
CGContextConcatCTM(ctx, transform);
switch (self.imageOrientation) {
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
// Grr...
CGContextDrawImage(ctx, CGRectMake(0,0,self.size.height,self.size.width), self.CGImage);
break;

default:
CGContextDrawImage(ctx, CGRectMake(0,0,self.size.width,self.size.height), self.CGImage);
break;
}

// And now we just create a new UIImage from the drawing context
CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
UIImage *img = [UIImage imageWithCGImage:cgimg];
CGContextRelease(ctx);
CGImageRelease(cgimg);
return img;

}

现在再优化一下原来 + (UIImage *)rotateImage:(UIImage *)oldImage orientation:(UIImageOrientation)orientation 方法,修改成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
+ (UIImage *)rotateImage:(UIImage *)oldImage orientation:(UIImageOrientation)orientation{

UIImage *newImage = [UIImage imageWithCGImage:oldImage.CGImage scale:1 orientation:orientation];

//fix original Image with gived orientation.
UIImage *fixedRotationImage = [newImage fixedRotation];

NSString *orientationStr = nil;
switch (orientation) {
case UIImageOrientationUp: {
orientationStr = @"UIImageOrientationUp";
break;
}
case UIImageOrientationDown: {
orientationStr = @"UIImageOrientationDown";
break;
}
case UIImageOrientationLeft: {
orientationStr = @"UIImageOrientationLeft";
break;
}
case UIImageOrientationRight: {
orientationStr = @"UIImageOrientationRight";
break;
}
case UIImageOrientationUpMirrored: {
orientationStr = @"UIImageOrientationUpMirrored";
break;
}
case UIImageOrientationDownMirrored: {
orientationStr = @"UIImageOrientationDownMirrored";
break;
}
case UIImageOrientationLeftMirrored: {
orientationStr = @"UIImageOrientationLeftMirrored";
break;
}
case UIImageOrientationRightMirrored: {
orientationStr = @"UIImageOrientationRightMirrored";
break;
}

}

NSLog(@"current orientation: %@",orientationStr);

return fixedRotationImage;
}

现在再测试一下,well,It‘s OK。

have fun!!!

参考资料

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

这是什么?

此文将讲解通过形状图层 CAShaperLayerstrokeStartstrokeEnd 来实现动画绘制路径 CGPath ,此文是By Ole Begemann创建于 December 18, 2010,当时是发布 iOS SDK 4.2 时 CAShapeLayer 新增加的两个属性strokeStartstrokeEnd,这两个值是两个浮点数取值范围 0.0~1.0,用来表明形状图层所指向的路径在绘制开始和结束路径中的相对位置。

strokeStart 默认值是0.0, strokeEnd 默认值是1.0,显然这会导致形状图层的路径将一整个被绘制。假如,你想说,如果设置了layer.strokeEnd = 0.5f,只让她绘制前半部分,那该多好。

真正有趣的事情是这些接口都是可动画的。通过动画绘制 strokeEnd 从 0.0 到 1.0 在几秒内,我们就能很容易自己绘制路径像下面这样写:

1
2
3
4
5
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = 10.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[self.pathLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];

最后,再添加第二个图层包含一个铅笔图片,使用关键帧动画 CAKeyframeAnimation 来让它随着这个路径以相同的速度绘制,就可以达到完美的错觉效果:

1
2
3
4
5
CAKeyframeAnimation *penAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
penAnimation.duration = 10.0;
penAnimation.path = self.pathLayer.path;
penAnimation.calculationMode = kCAAnimationPaced;
[self.penLayer addAnimation:penAnimation forKey:@"penAnimation"];

绘制普通路径效果视频

下载地址

这个对文本一样有效;我们只需要把字符转化成 CGPathCore Text 提供了那样的功能的函数,CTFontCreatePathForGlyph( )。为了使用它,我们需要创建一个带属性的字符串用我们想要渲染的文本,先把它们分割成行在分割成一个个字符。在把所有的字符转换成路径后,我们以子路径方式把它添加到一个单个的 CGPath 路径中。更多细节可以查看Ohmu写的Low-level text rendering这篇文章。结果看以来非常的炫酷:

绘制文字路径效果视频

下载地址

从 Github 上获得iPad 版的样品工程

你将学到的知识点

  • 使用 CAShapeLayerstrokeStartstrokeEnd 来实现路径动画,比较高级复杂的效果像 google 的下拉刷新转圈就可以从这里引申去实现。
  • CABasicAnimationCABasicAnimation 使用
  • 深入理解 CAShapeLayerCALayer * 通过文本创建路径,核心函数 CTFontCreatePathForGlyph()

补充说明

1
2
3
4
5
6
7
8
9
10
11
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.animationLayer.bounds;
pathLayer.bounds = pathRect;
pathLayer.geometryFlipped = YES;
pathLayer.path = path.CGPath;
pathLayer.strokeColor = [[UIColor blackColor] CGColor];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 10.0f;
pathLayer.lineJoin = kCALineJoinBevel;

[self.animationLayer addSublayer:pathLayer];

有一点非常重要,CALayer 在 iOS 系统中相对坐标系是以屏幕左上 top-left 为坐标原点的,在 Mac OS X 上以坐下 bottom-left 为坐标原点,但是可以通过 CALayer 的接口 geometryFlipped 垂直翻转坐标系,这个值默认是 NO ,设置成 YES 就可以把坐标系转换成左下 bottom-left 了,这里作者使用的左下 bottom-left 的坐标系。

1
@property(getter=isGeometryFlipped) BOOL geometryFlipped;

关于这个属性使用时需要特别注意

  1. 翻转会同时作用于它的子图层
  2. 即使这个属性设置成 YES ,图片的 orientation 仍然是不变的(也就是说当设置 flipped=YESflipped=NO 时一个 CGImageRef 储存在 contents 接口中的内容将会显示一致,赋值并不会真正变换底层的图层)

pathLayer 动画实现原理

  1. 先创建一个动画用的图层 animationLayer 类型 CALayer ,用来充当动画的画布。
  2. 创建真正的路径图层 pathLayer 类型为 CAShapeLayer ,让它的坐标系垂直翻转,并且让图层宽高同时向内收缩 100 个点,通过 CGRectInset(CGRect rect, CGFloat dx, CGFloat dy) 实现
  3. pathLayer 添加到 animationLayer 的子图层中去
  4. 创建一个铅笔图层 penLayer 类型为 CALayer ,把它添加到 pathLayer
  5. pathLayer 添加 CABasicAnimation 动画,动画属性为 strokeEnd 6. 对 penLayer 添加 CAKeyframeAnimation 动画,动画属性为 position #### textLayer 动画实现原理
  6. 先创建一个动画用的图层 animationLayer 类型 CALayer ,用来充当动画的画布
  7. Create path from text,See:http://www.codeproject.com/KB/iPhone/Glyph.aspx,最终保存到一个类型为 CGMutablePathRef 的 letter 中
  8. 通过 letter 来创建文字 UIBezierPath 类型的 path 4. 通过 path 再创建 CAShapeLayer pathLayer,并且把 pathLayer 添加到 animationLayer 中去
  9. 创建一个铅笔图层 penLayer 类型为 CALayer ,把它添加到 pathLayer
  10. pathLayer 添加 CABasicAnimation 动画,动画属性为 strokeEnd 6. 对 penLayer 添加 CAKeyframeAnimation 动画,动画属性为 position

修复一处 bug

重复点击 UISegmentedControl 导致铅笔消失,这是设置了 penAnimation.delegate = self; 在代理方法里面没有判断结束直接将设置 self.penLayer.hidden = YES ,导致连续切换时铅笔不见了,要修复这个 bug 只需加一个判断if (flag) self.penLayer.hidden = YES; 即可,这样的意思是只有当动画完成时才设置 self.penLayer.hidden的值,好了现在已经非常完美了,快去动手自己试试吧!🍺

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

简书

国内个人

国外个人

  • mikeash–白天飞行员,晚上程序员 :] just this guy, you know?
  • shinobicontrols–iOS 最新 API 以及新功能用法
  • iosdevelopertips–教程配合 Demo 让你学习成长立竿见影
  • iosdevweekly–很棒的个人技术博客网,已发表 230 篇文章
  • ios-goodies–iOS,UI,UX,Objective-c,Swift,Xcode
  • mattt-thompson – 不用解释
  • subjc –Subjective-C is a study of innovative iOS interfaces.
  • thinkandbuild–《Introduction To 3D Drawing in CoreAnimtion》作者
  • robb.is–《How to build a nice Hamburger transition in swift》作者
  • commandshift–不多说,质量极高
  • indieambitions–raywenderlich 常驻作者之一的 blog,非常赞。
  • nvie–国外一个大神的 blog,讲得比较杂,git,ios,python 都有涉猎,但是每篇都很精彩。
  • stuartkhall–很多关于 app 上线运营之类的 blog,值得一看。
  • ittybittyapps–神器 Reveal 的作者的 blog。
  • adoptioncurve–更新极快,当时 iOS8 还没出几个周,作者就写了篇 sizeclass 解析。非常棒
  • ciechanowski–各种数学上几何变化
  • bignerdranch
  • cocoawithlove–2008-2011 的老文章,现在没怎么跟新了
  • codinghorrorJeff Atwood 主站
  • g8production
  • lucida – 我比较佩服一名程序员,想学算法找他推荐咯😁

团体 blog

优质 iOS 学习资源

中文网站

Swift:

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

__autoreleasing 修饰符

将对象赋值给附有 __autoreleasing 修饰符的变量等同于 ARC 无效时调用对象的 autorelease 方法。我们通过以下源代码来看一下

1
2
3
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}

该源代码主要将 NSObject 类对象注册到 autoreleasepool 中,可作如下变换:

1
2
3
4
5
6
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

这与苹果的 autorelease 实现中的说明(参考 1.2.7 节)完全相同。虽然 ARC 有效和无效时,其在源代码上的表现有所不同,但 autorelease 的功能完全一样。

在 alloc/new/copy/mutableCopy 方法群之外的方法中使用注册到 autoreleasepool 中的对象会如何呢?下面我们来看看 NSMutableArray 类的 array 类方法。

1
2
3
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}

这与前面的源代码有何不同呢?

1
2
3
4
5
6
/* 编译器的模拟代码 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);

虽然持有对象的方法从 alloc 方法变为 objc_retainAutoreleasedReturnValue 函数, 但注册 autoreleasepool 的方法没有改变,仍是 objc_autorelease 函数。

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

重复发布这个主题已经说明了同编译器保存健康关系的重要性,像任何草稿一样,作为一个实践者的效率取决于他们怎样对待他们的工具,你照顾好它们,它们反过来也会对你有好处。

__attribute__ 是一个编译器的指令在声明的时候指明了一些参数,这些参数允许更多的检查错误和高级的优化。

语法关键字是 __attribute__ 紧跟 2 套圆括号(双圆括号让出现的宏更容易辨认,特别是在有多个属性的时候)。在括号内部是一个以逗号分隔的属性列表, __attribute__ 指令被放在函数,变量和类型声明后面。

1
2
3
4
5
6
7
8
9
10
 // Return the square of a number
int square(int n) __attribute__((const));

// Declare the availability of a particular API
void f(void)
__attribute__((availability(macosx,introduced=10.4,deprecated=10.6)));

// Send printf-like message to stderr and exit
extern void die(const char *format, ...)
__attribute__((noreturn, format(printf, 1, 2)));

假如这个让你想起 ISO C 语言的 #pragma ,你就不会感到孤单了。

实际上,当 __attribute__ 被第一次引入到 GCC 编译器时,它面临一些阻力,有人建议使用专用的 #pragma 因为相同的目的。

这里,然而,有 2 个非常好的理由为什么 __attribute__ 被添加进来

  • 从一个宏中产生 #pragma 命令几乎是不能的(在 C99 _Pragma 预算符以前)。
  • 这里没人知道相同的 #pragma 在另一个编译器中可能的意思。

引用 GCC Documentation for Function Attributes

  • 这里有 2 个原因被应用到几乎所有的应用推荐使用 #pragma ,这犯了一个低级错误就是把 #pragma 使用到任何地方。

确实,假如你在苹果的框架中和牛逼工程师的开源项目中的头文件看一下现代的 Objective-c__attribute__ 被大量使用。(相反, #pragma 的主要声明名声来着这些天是装饰: #pragma mark

所以为了以后不费力,我们还是先看一下最重要的属性:


GCC

format

format 属性指定了一个函数像 printf , scanf , strftime 或者 strfmo 风格的参数,这个参数应该是可以进行类型检查的一个格式化字符串。

1
2
3
extern int
my_printf (void *my_object, const char *my_format, ...)
__attribute__((format(printf, 2, 3)));

Objective-C 程序员也可使用 __NSString__ 来格式化来做到相同的格式化规则,像在 NNString 中通过 +stringWithFormat:NSLog() 格式化字符串一样。

nonnull

这个 nonnull 属性指定了某些函数的参数必须是非空的指针。

1
2
3
extern void *
my_memcpy (void *dest, const void *src, size_t len)
__attribute__((nonnull (1, 2)));

使用 nonnull 编码期望这个值遵守一个明确的约定中,这样能帮助捕获潜伏在任何代码调用的 NULL 指针 bugs,请记住:
编译时的错误 >> 运行时的错误。 noreturn

一些标准库函数,例如 abortexit ,是不能返回的。GCC 自动知道这些东西,这个 noreturn 属性用于指定任何其他函数永远不会返回的情况。

例如,AFNetworking 使用 noreturn 属性在它的 网络请求线程进入点的方法 里面,这个方法用在当大量产生专用的网络的线程里用来保证分离的线程持续执行在应用的整个生命周期中。

pure/const

pure 属性指定了一个函数除了返回值没有副作用,例如它的返回值仅仅依赖参数和/或者全局变量。这样的函数可以用公共子表达式消除并且循环优化就像一个算数操作符那样。

pure 属性指定了一个函数不会检查任何值除了它们的参数,并且返回值没有副作用。注意到一个函数有一个指针参数并且需呀检查数据的指向不能声明成 const 。同样的,一个函数调用一个非 nonst 函数通常不能为 const ,一个 const 函数返回 void 并没有什么意义。

1
int square(int n) __attribute__((const));

pureconst 是两个执行在一个函数式编程惯例中的参数为了允许有效性能优化。 const 可以被认为是严格形式的 pure 因为它不依赖全局变量或者指针。

例如,因为一个函数声明为 const 的结果并不依赖任何东西除了传进来的参数。函数的结果能够缓存那个结果并且当函数被调用时返回,这样的函数叫做相同的组合参数(也就是说,我们知道一个数字的平方是一个常量,所以我们仅仅需要只计算它一次)。

unused

这个属性,附着在一个函数后面,意味着那个函数很可能不会被使用,GCC 不会对这个函数产生警告。

__unused 关键词可以获得相同的效果,声明这个在方法实现中不会被使用的参数中。知道那以后一些上下文就可以允许编译器来做相应的优化。你很可能喜欢在 delegate 方法实现李勉使用 __unused ,因为协议频繁的提高更多的上下文比通常必要的情况,为了满足大量的潜在使用案例。

LLVM

像 GCC 的很多特征一样,Clang 也支持 __attribute__ ,添加到它自己的小范围的扩展。为了检查某个属性的可用性,你可以直接使用 __has_attribute 属性。

availability

Clang 引进了 availability 属性,这个可以被取代在声明描述的生命周期中声明相对于操作系统的版本。思考对一个简单函数 f:的函数声明

1
void f(void) __attribute__((availability(macosx,introduced=10.4,deprecated=10.6,obsoleted=10.7)));

availability 属性声明 f 在 OS X 老虎系统中被引入,在 OS X 雪豹系统中被弃用,在 OS X 山狮系统中被废弃。

这个信息被 Clang 用来决定什么时候使用 f:函数式安全的,例如,假如 Clang 在 OS X 美洲豹系统上编译,调用 f()函数将成功。假如 Clang 在 OS X 雪豹系统中编译,函数调用将成功但是 Clang 会发出一个警告指明这个函数被弃用了。最后,假如 Clang 被引进编译 OS X 山狮系统的代码,函数调用将失败,因为 f()函数已经不再可用了。

availability 属性是一个逗号分隔的列表以平台名开始然后引入一些定语列举出生命周期内的重要里程碑事件附加额外的信息(以任何顺序)。

  • introduced:声明被引入的第一个版本
  • deprecated:声明被弃用的第一个版本,这意味着用户应该把这个 API 移走
  • obsoleted: 声明被废弃的第一个版本,这意味着它将被完全移除并且不能再使用
  • unavailable:声明在这个平台上将永远不可用
  • message:额外的消息将被 Clang 提供当忽略一个警告或者一个错误在使用一个被弃用或者被废弃的声明。对引导用户替换 APIs 很有用。

在声明时可以使用多个 availability 属性,每个对应不同的平台,仅当 availability 属性对应相应的目标平台被使用的时候,任何其他才将被忽略。假如没有 availability 属性指定可用性对现在的目标平台,availability 属性将被忽略。

支持的平台

  • ios:苹果的 iOS 操作系统。最小的部署目标被指定通过 -mios-version-min=*version* 或者 -miphoneos-version-min=*version* 命令行参数。
  • macosx:苹果的 OS X 操作系统,最小的部署目标被指定通过 -mmacosx-version-min=*version* 命令行参数

overloadable

Clang 提供对 C++函数在 C 中重载的支持。在 C 中函数重载被引进使用 overloadable 属性。例如,一个可能提供一个重载版本的 tgsin 函数来精确执行相关的标准函数计算 float , double , long double 的正弦值:

1
2
3
4
#include <math.h>
float __attribute__((overloadable)) tgsin(float x) { return sinf(x); }
double __attribute__((overloadable)) tgsin(double x) { return sin(x); }
long double __attribute__((overloadable)) tgsin(long double x) { return sinl(x); }

请注意 overloadable 只对函数起作用。你可以重载方法声明在某种范围内通过使用通用的返回值和参数类型,想 id 或者 void * .


上下文是国王当它遇到编译器优化时。通过提供限制在怎样解析你的代码,增加你参数尽可能高效代码的可能性。遇到编译器把你打断,这将是一项奖励。

还有 __attribute__ 并不仅仅对编译器有用:下一个人看代码也将感谢这些额外的上下文。所以多走几英尺远将对你的合作中和接替者或者从现在算 2 年以后的你(那个时候你已经忘记了所以的事情关于这份代码)自己有用

你付出了多少爱,最终你会得到多少爱。

译者注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

NSRunLoop 是什么?

在 Cocoa 中,每个线程( NSThread )对象中内部都有一个 RunLoop( NSRunLoop ) 对象用来循环处理输入事件.

NSRunloop 并不真的是一个 loop,在 Apple 的文档中也提到了需要自己写 while 或者 for 语句来实现,类似下面:

1
2
3
while(running){
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

何为 RunLoop 事件源

从字面翻译来看,RunLoop 就是一个运行循环,的确它就是一个处理输入时间的运行循环,为什么需要这样处理,难道没有事件发生的时候让线程空转浪费资源?很明显在有事件发生的时候唤醒线程,没有事件发生的时候让其 sleep 更好。

下面我还是拿这张百看不厌的图来说事:

RunLoop

处理的事件包括两类

  • 来自 Timer sources 的同步事件
  • 来自 Input sources 的异步事件
  1. Time Source. Timer sources deliver synchronous events, occurring at a scheduled time or repeating interval. 苹果文档中有句话需要注意,

Timer sources deliver events to their handler routines but do not cause the RunLoop to exit.*

创建 NSTimer 添加到 RunLoop 中的时候,这里需要注意的是, NSTimer 默认是处于 NSDefaultRunloopMode ,这也就可以解释为什么如果你在你的控制器中添加了一个 timer 定时刷新你的界面,而你在拖动视图的时候 timer 不回 fire,因为这个时候你的 runloop 是 NSEventTrackingRunloopMode ,在这个 mode 下 timer 不会 fire。

  1. input source input source 主要是一些异步的事件,比如来自其它线程或者其它 app 的消息。

input source 传递异步事件到其对应的处理函数,并且使 runUntilDate(与线程相关联的 RunLoop 对象调用)返回

为了能够处理 input source, RunLoop 产生 notifications。通过注册成 RunLoop observers 可以接受到这些通知(通过 Core Foundation 来注册 observers)。

RunLoopMode 有哪些?

RunLoop 在处理输入事件时会产生通知,可以通过 Core Foundation 向线程中添加 RunLoop observers 来监听特定事件,以在监听的事件发生时做附加的处理工作。

每个 RunLoop 可运行在不同的模式下,一个 RunLoop mode 是一个集合,其中包含其监听的若干输入事件源,定时器,以及在事件发生时需要通知的 RunLoop observers。运行在一种 mode 下的 RunLoop 只会处理其 RunLoop mode 中包含的输入源事件、定时器事件、以及通知 RunLoop mode 中包含的 observers。

Cocoa 中的预定义模式有:

  1. Default 模式
    1. 定义 NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation)
    2. 描述:默认模式中几乎包含了所有输入源(NSConnection 除外),一般情况下应使用此模式,这是最常用的 RunLoop mode。
  2. Connection 模式
    1. 定义: NSConnectionReplyMode (Cocoa)
    2. 描述:处理 NSConnection 对象相关事件,系统内部使用,这个 mode 表明 NSConnection 对象等待 reply,用户基本不会使用。
  3. Modal 模式
    1. 定义: NSModalPanelRunLoopMode (Cocoa)
    2. 描述:处理 modal panels 事件,需要等待处理的 input source 为 modal panel 时设置,比如 NSSavePanel 和 NSOpenPanel。
  4. Event tracking 模式
    1. 定义: UITrackingRunLoopMode (iOS) NSEventTrackingRunLoopMode (cocoa)
    2. 描述:使用该模式来处理用户界面相关的事件,例如在拖动 loop 或其他 user interface tracking loops 时处于此种模式下,在此模式下会限制输入事件的处理。例如,当手指按住 UITableView 拖动时就会处于此模式。
  5. Common 模式
    1. 定义: NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation)
    2. 描述:这是一个伪模式,其为一组 RunLoop mode 的集合,将输入源加入此模式意味着在 Common Modes 中包含的所有模式下都可以处理。在 Cocoa 应用程序中,默认情况下 Common Modes 包含 default modes,modal modes,event Tracking modes, 可使用 CFRunLoopAddCommonMode 方法向 Common Modes 中添加自定义 modes。

注意这个并不是一个特定的 mode,而是一个 mode 的集合,而 runloop 必须运行在一个特定的 mode 下

获取当前线程的 runloop mode

1
NSString *runLoopMode = [[NSRunLoop currentRunLoop] currentMode];

NSTimer、NSURLConnection 与 UITrackingRunLoopMode

NSTimer 与 NSURLConnection 默认运行在 default mode 下,这样当用户在拖动 UITableView 处于 UITrackingRunLoopMode 模式时,NSTimer 不能 fire,NSURLConnection 的数据也无法处理。

NSTimer 的例子: 在一个 UITableViewController 中启动一个 0.2s 的循环定时器,在定时器到期时更新一个计数器,并显示在 label 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {

label =[[UILabel alloc]initWithFrame:CGRectMake(10, 100, 100, 50)];
[self.view addSubview:label];
count = 0;

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 0.2
target: self
selector: @selector(incrementCounter:)
userInfo: nil
repeats: YES];
}

- (void)incrementCounter:(NSTimer *)theTimer
{
count++;
label.text = [NSString stringWithFormat:@"%zd",count];
}

在正常情况下,可看到每隔 0.2s,label 上显示的数字 +1,但当你拖动或按住 tableView 时,label 上的数字不再更新,当你手指离开时,label 上的数字继续更新。当你拖动 UItableView 时,当前线程 RunLoop 处于 UIEventTrackingRunLoopMode 模式,在这种模式下,不处理定时器事件,即定时器无法 fire,label 上的数字也就无法更新。 解决方法,一种方法是在另外的线程中处理定时器事件,可把 Timer 加入到 NSOperation 中在另一个线程中调度;还有一种方法时修改 Timer 运行的 RunLoop 模式,将其加入到 UITrackingRunLoopMode 模式或 NSRunLoopCommonModes 模式中。 即

1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

另外一种是放到 NSThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)viewDidLoad{
[super viewDidLoad];
NSLog(@"主线程 %@", [NSThread currentThread]);

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
}

- (void)newThread{
@autoreleasepool{

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] run];
}
}

- (void)timer_callback{
NSLog(@"Timer %@", [NSThread currentThread]);
}

NSURLConnection 也是如此,见 SDWebImage 中的描述,以及 SDWebImageDownloader.m 代码中的实现。修改 NSURLConnection 的运行模式可使用 scheduleInRunLoop:forMode: 方法。

1
2
3
4
5
6
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];

self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];

参考资料:

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com

为什么需要出现 tintColor ?

解决以前不方便统一设置视图颜色风格的通点,方便自定义系统控件外观
UIAppearance 协议设计有点类似, UIAppearance 是为了方便统一 设置一类控件的外观,而 tintColor 是为方便设置某个控件的外观,或者 说某个容器内所有控件的风格。

像在 UIViewController 中,通过这段代码可以实现容器内,所有的子 view 风格统一化,这样在这个控制器中的所有子 view 都会以 tintColor 显示

1
self.view.tintAdjustmentMode = UIViewTintAdjustmentModeNormal

UIViewtintAdjustmentMode需要说明一下,这是一个UIViewTintAdjustmentMode枚举

  • UIViewTintAdjustmentModeAutomatic(着色调整模式自动)
  • UIViewTintAdjustmentModeNormal(着色调整模式正常)
  • UIViewTintAdjustmentModeDimmed(着色调整模式变暗,打开控风格会变成灰白模式)

先看看官方 API 说明

https://developer.apple.com/documentation/uikit/uiview/1622467-tintcolor?language=objc

iOS7 以后所有的 UIView 以及它的子类都新增了一个叫 tintColor 的接口,方便我们对视图进行颜色调整

注意事项

UIImageView 需要设置 renderingModeUIImageRenderingModeAlwaysTemplate 才能生效。
renderingMode是一个类型为UIImageRenderingMode的枚举

  • UIImageRenderingModeAutomatic (默认渲染模式,自动模式)
  • UIImageRenderingModeAlwaysOriginal(总是绘制原来的图片,不把它当成临时图片来处理)
  • UIImageRenderingModeAlwaysTemplate (总是绘制临时图片,会忽略它原本的颜色信息,也就是根据 tintColor 生产图片)

UIImageView 的使用

1
2
UIImage *image = [UIImage imageNamed:@"xxx.png"];
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];

tintColor 更新

在子类中重写 - (void)tintColorDidChange 方法,就可以实现每次更新 tintColor 的时候调用相关配置

1
2
3
4
5
- (void)tintColorDidChange
{
_tintColorLabel.textColor = self.tintColor;
_tintColorBlock.backgroundColor = self.tintColor;
}

序言经过一晚上的折腾,终于从 Hexo 成功转入 Octopress ,那么本文就来细说一下如何使用 Octopress + GitHub Pages 搭建个人博客

为什么选择 Octopress & Github Pages?

  • 免费且独立。把 Octopress 博客系统搭建到 Github Pages 虽是免费,但不失独立性,即便 Github 全站关闭,你也将有一份本地全站备份,随时可以重新恢复。不必受托管商之气,而且还免费,如果你愿意,甚至可以自行插入广告挣钱。
  • 版本控制。写文章,建网站,做软件都需要修改,但有时候改完了又会后悔,如果有时光机就好了,Git 就是你的时光机。当然如果你不想了解这些看上去很唬人的 IT 名词,只是想写博客的话,请在需要的时候再研究这条的内容。
  • 相对其他托管到 Github 上的博客程序,Octopress 更加成熟易上手。打个比方,Jekyll 可以说是毛坯房,HexoOctopress 算是简装修,但相比 Hexo,Octopress 有更多装修范例和更多熟练的装修工人,更容易获取帮助。当然如果你只想住精装修的房子,那不得不花点钱上 WordPress
  • 使用 Markdown。Markdown 是现在最为流行的轻量级标记语言,也是已故的天才 Aaron Swartz 留给世人最好的礼物,窃以为每个在互联网上发布文章的人都该掌握。
  • 按照官方的说法,Octopress 是个 A blogging framework for hackers (「为黑客设计的博客框架」),这很酷,你不觉得吗?

如果你之前没有写过博客,打算开始搭建自己第一个博客的话,其实也不妨试试 Octopress,免费还能学到东西,何乐而不为?

本文是在 OS X EI Capitan 系统上搭建一个基于 Octopress 的个人博客系统,记录搭建过程的各种坑,希望对有想搭建个人博客的朋友有所帮助。

本文是建立在你有 Shell 指令基础和 Git 操作基础之上,如果不了解的话,需要查阅相关资料。

先解释几个专业术语:

  • Ruby
    • Ruby 是一种编程语言。Octopress 是用 Ruby 语言 实现的。我们不需要对它有太多了解,只需要正确安装 Ruby 的环境(Ruby 版本必须不低于 1.9.3-p0,后面会详细介绍)及按步骤执行指令即可。
  • RubyGems
    • RubyGems(简称 gems)是一个用于对 Ruby 组件进行打包的 Ruby 打包系统。它可以用来查找、安装、升级和卸载软件包。我们也是通过它安装 Octopress 包的
  • RVM
    • RVM 是 Ruby Version Manager 的简称,是一款 Ruby 语言安装、管理的工具。我们对 Ruby 的操作是通过它的指令完成的。
  • Jekyll
    • Jekyll 一个简单的博客形态的静态站点生产机器。 Jekyll 有一套模板目录,可以将 Markdown 文件(或者 Textile)转换为静态网页,并生成一个完整的可发布的静态网站。
    • 同时,我们可以将产生的静态网站布置到 GitHub Pages 上,生成个人博客站点。
    • 想了解更多内容可以查看中文文档
  • Octopress
    • Octopress 是基于 Jekyll 的博客框架。他们的关系就像 jQuery 与 js 的关系一样。
    • 它为我们提供了现成的美观的主题模板,并且配置简单,使用方便,大大降低了我们建站的门槛。
  • Git
    • 分布式版本控制工具,跟它有点类似的是 SVN,只是两者使用场景不一样。
  • GitHub
    • GitHub 是全球最热的开源社区,程序界的 Facebook。它为我们提供代码托管服务,以及我们搭建博客所需要的 Pages 服务。
  • GitHub Pages
    • GitHub Pages 是 GitHub 提供的一项服务。它用于显示托管在 GitHub 上的静态网页。所以我们可以用 Github Pages 搭建博客,当然我们也可以把项目的文档和主页放在上面。

通过以上内容,我们大概能够明白 Octopress 建站的原理:
我们使用基于 Jekyll 的 Octopress 站点生成工具,生成本地的静态网站。然后将静态网站托管到 GitHub 为我们提供的 GitHub Pages 服务上。访问 username.github.io 即可显示你的个人博客站了


明白了上面这些内容,下面进行具体的搭建工作:

第一步:安装 Ruby

Mac 自带 Ruby 环境打开终端,安装 RVM ,终端执行指令:

1
$ curl -L https://get.rvm.io | bash -s stable --ruby

接下来我们要查看自己的 Ruby 环境

1
$ ruby -v

如果你的 Ruby 版本不低于 1.9.3-p0 可以忽略 Ruby 的安装(或升级),直接跳到安装 RubyGems 。 否则,我们执行之后的操作,终端执行指令:

1
2
$ rvm install 1.9.3
$ rvm use 1.9.3

然后安装 RubyGems, 终端执行指令:

1
$ rvm rubygems latest

到这里第一步完成。我们可以再执行一次第一条指令 ruby -v 来查看当前 Ruby 的版本了。

第二步:安装 Octopress

因为 Mac 系统自动 git 环境,所以我们不需要考虑 git 的安装。直接将 Octopress 的项目 clone 到本地,在终端执行指令:

1
$ git clone git://github.com/imathis/octopress.git octopress

完成后进入 octopress 的目录

1
$ cd octopress

接下来,安装依赖:

1
2
3
4
5
$ gem install bundler
# 这时你可能会遇到没有权限的问题,那么我们需要加上 sudo 重新执行,并输入密码。
$ sudo gem install bundler
# 接下来执行:
$ bundle install

这时你可能还会遇到问题如下:

1
2
3
4
5
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...

Gem::RemoteFetcher::FetchError: SocketError: getaddrinfo: Name or service not known (https://rubygems.org/gems/rake-10.4.2.gem)
An error occurred while installing rake (10.4.2), and Bundler cannot continue.

这是因为被墙了,解决办法有两个: 一个是,可以使用自己的翻墙工具; 另一个,淘宝做了一个 gem 的镜像。我们需要在 Octopress 的文件目录下找到 Gemfile 文件,将其中的 source 'https://rubygems.org/' 改为 source 'https://ruby.taobao.org/' 再重新运行 bundle install 就可以了。 这段内容可以参考 bundle install 提示如下,是需要翻墙解决么

下面就可以安装 Octopress 的默认主题了,终端执行指令:

1
rake install

这样一个最基本的个人博客站就产生了。

Octoress init

安装 octostrap3 主题(可选)

1
2
3
4
$ cd octopress
$ git clone https://github.com/kAworu/octostrap3.git .themes/octostrap3
$ rake "install[octostrap3]"
$ rake generate

会提示以下信息

1
A theme is already installed, proceeding will overwrite existing files. Are you sure? [y/n]

输入 y 继续,然后开始安装…

1
2
3
4
5
6
7
## Copying octostrap3 theme into ./source and ./sass
mkdir -p source
cp -r .themes/octostrap3/source/. source
mkdir -p sass
cp -r .themes/octostrap3/sass/. sass
mkdir -p source/_posts
mkdir -p public

显示上面的内容完成后没有错误,就安装主题完成


可以看出,现在显示得都是预设值,并不是我们想要的,所以需要修改 Octopress 目录下的_config.yml 文件。

文件目录 _config.yml 描述:保存配置数据。很多配置选项都会直接从命令行中进行设置,但是如果你把那些配置写在这儿,你就不用非要去记住那些命令了。

_config.yml 文件共分为 3 个部分内容

  • Main Configs
  • Jekyll & Plugins
  • 3rd Party Settings

目前,我们只需要关注第一部分 Main Configs。

# ----------------------- #
# Main Configs #
# ----------------------- #

#网站地址
url: http://wangruofeng.github.io

#网站标题
title: 王若风的技术博客

#网站副标题
subtitle: 天天向上.

#网址作者,通常显示在页尾和每篇文章的尾部
author: Ace

#搜索引擎
#simple_search: http://google.com/search

#网站的描述,出现在 HTML 页面中的 meta 中的 description
#description:

对应填入你的个人信息,其中 url 为必填的,一般填 GitHub 仓库对应的连接,其内容大致就是 username.github.io ,这个地址我们会在后面步骤中获得。

第三步:集成 GitHub Pages

  1. 注册 Github 账号

这个没什么好说的,早晚需要,去 http://github.com 注册吧。

  1. 在 GitHub 上创建一个代码参考,项目名称命名规则为 username.github.io,username 必须与用户名称一致。

  2. 域名指向(可选)
    如果你有自己的域名可用,可以在这时就配置好,毕竟解析起来需要一段时间,不如在我们搭建博客的时候让它开始,这样我们搭建完成后,基本上就可以直接用自有域名访问了。

我的是在万网申请的,原价一年 100 多点现在有活动便宜的几块的都有,想要个性域名的可以去注册一个。

  • 如果你用的是顶级域名,比如 wangruofeng007.com, 请创建两个 A 记录 (A Record) 分别指向 192.30.252.153192.30.252.154 .

  • 如果你使用二级域名,比如 blog.wangruofeng007.com, 请将该域名的 CNAME 指向 [your_username].github.io, 把其中的 [your_username]换成你自己在 Github 上的用户名。

  • 如果你暂时没有域名,这一步可以暂时不用管

Ps. 在创建过程中最好不要 添加忽略文件README 文件。因为我们要把本地的 git 仓库同步到 GitHub 远程仓库中。如果再远程仓库中添加了其他文件,需要我们执行 pull 操作。除非你能非常熟练的使用 git ,否则不建议你制造不必要的麻烦。
接下来将本地代码仓库同步到 GitHub 上,执行终端指令:

1
$ rake setup_github_pages

它会要求你绑定远程仓库的地址,此时只需要输入即可:

1
$ git@github.com:username/username.github.com.git

这样就会将 Octopress 生成的静态站点与 GitHub 进行绑定了。

之后我们创建第一篇文章:

1
rake new_post["title"]

然后会有一个名为 yyyy-mm-dd-Post-Title.markdown 的文件在 octopress/source/_posts 目录下生成,其中 yyyy-mm-dd 是你当时的日期。然后执行以下命令:

1
2
cd source/_posts/
vim yyyy-mm-dd-Post-Title.markdown

即可用 vim 编辑器编辑的刚才的文章了,好吧我知道你作为这篇文章的读者并不是一个能熟练使用 vim 的人,那么请在命令行输入 q!退出这个编辑器。如果你不想假装是个黑客的话,其实发布文章并不需要这么麻烦。

我们直接打开 octopress/source/_posts 文件夹,找到刚才生成的文件,用你喜欢的 Markdown 编辑器(免费的我推荐 Mou或者Atom)或者文本编辑器打开,对文章内容进行编辑。

打开文件后,你会发现文章开头有这么一段信息:

1
2
3
4
5
6
7
---  
layout: post
title: "Post Title"
date: yyyy-mm-dd hh:mm:ss
comments: true
categories: ""
---

这其实是这篇文章的元数据:layout 暂时不要理会;title 是这篇文章显示在最终网页上的标题;date 部分是详细的文件生成时间,如 2014-01-28 03:35:00;comment 部分表示是否允许评论,目前显示是允许,如果想关闭评论,请改为 false;categories 指这篇文章的分类目录,请在后面引号中输入,不用引号也可以,多个分类用 空格 隔开,如果没有该目录,则会自动生成。请不要删除这段信息,在这段信息下面开始你的文章内容

这件事情给我们的启发是,以后发布文章,其实并不需要使用终端命令行生成文件。可以直接将自己写好的文章放到这个文件夹下面,当然请按照 yyyy-mm-dd-Post-Title.markdown 这样的文件格式命名,同时记得在文章前面添加元数据信息。这种做法生成的文章与上面的方法无异

生成的新文章在 source/_post/目录下,文件名构成为时间和标题的拼音。我们可以用 Markdown 编辑器对文章进行修改。
之后生成静态站点,终端执行指令:

1
2
# 生成静态站点
$ rake generate

如果你想预览本地的站点,可以执行终端指令:

1
2
# 预览静态站点
$ rake preview

此时,可以使用浏览器打开 localhost:4000 查看效果。如果没有问题可以将静态站点同步到 GitHub 远程仓库中,终端执行指令:

1
2
# 同步内容
$ rake deploy

你会发现我们的静态站点已经被 push 到 GitHub 仓库的 master 分支上。稍等几分钟,访问 username.github.io (或者 username.github.com ),就会发现你的个人博客站已创建成功了。
如果你还想给自己的本地资源文件(如 Markdown 文件等内容)也同步到 GitHub 中,可以执行以下指令:

1
2
3
$ git add .
$ git commit -m "comment"
$ git push origin source

这样我们的资源文件就会同步到 GitHub 的 source 分支了。

使用自己的域名(可选)

如果你有自己的域名,并且想指向这个新博客的话,请首先确保执行了第三步的 域名指向(可选) 的内容。如果没有执行,可以随时执行。
然后执行下面的命令,注意把 your-domain.com 换成你自己的域名。

1
echo 'your-domain.com' >> source/CNAME

这句话的意思是在 source 分支下创建一个 CNAME 的文件并且将 your-domain.com 写入文件

然后再次执行以下命令:

1
2
rake generate
rake deploy

或者二合一

1
rake gen_deploy

这样你就可以使用自己的域名了。域名解析需要一段时间,如果没有马上生效,请不要着急。如果长时间没有生效,请确保完整执行了 域名指向(可选)使用自己的域名(可选) 的内容。

现在我们完成了个人博客的初级搭建,足够满足我们的基本需求。

错误处理

出现下面的错误:

1
2
Pushing generated _deploy website Permission denied (publickey).
fatal: Could not read from remote repository.

这个错误是因为缺少 SSH keys ,只有拥有这个 key 才有权限 push 到远程仓库,通过这种方式实现权限安全控制。
解决方案:Generating SSH keys

出现下面错误:

1
2
3
4
5
6
7
8
## Pushing generated _deploy website
To git@github.com:wangruofeng/wangruofeng.github.io.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'git@github.com:wangruofeng/wangruofeng.github.io.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

原因,是修改了远程仓库,导致本地本地版本落后于远程仓库版本。
最佳解决方案:

1
2
3
4
cd octopress/_deploy
git pull origin master
cd ..
rake deploy

参考链接:rake-gen-deploy-rejected-in-octopress

参考资料:

备注:欢迎转载,但请一定注明出处! http://blog.wangruofeng007.com