iOS開發系列--無限迴圈的圖片瀏覽器

KenshinCui發表於2014-08-15

--UIKit之UIScrollView

概述

UIKit框架中有大量的控制元件供開發者使用,在iOS開發中不僅可以直接使用這些控制元件還可以在這些控制元件的基礎上進行擴充套件打造自己的控制元件。在這個系列中如果每個控制元件都介紹一遍確實沒有必要,所謂授人以魚不如授人以漁,這裡會儘可能讓大家明白其中的原理,找一些典型的控制元件進行說明,這樣一來大家就可以觸類旁通。今天我們主要來看一下UIScrollView的內容:

  1. UIView
  2. UIScrollView
  3. 實戰--圖片瀏覽器

UIView

在熟悉UIScrollView之前很有必要說一下UIView的內容。在上一篇文章也簡單的對UIView進行了介紹,但是由於那篇文章的主要內容是給大家一個iOS開發的總體印象,因此並沒有系統的介紹。另外由於UIScrollView的父類是UIView,所有在討論UIScrollView之前也很有必要把UIView的知識給補充上,這樣大家在使用UIScrollView的某些方法時也不至於無從下手。

既然UIView是所有控制元件的父類,那麼對於一些常用的方法我們很有必要弄清楚,其實在Xcode中要了解一個類有哪些屬性和方法特別簡單,只要按住apple鍵點選類名就可以定位到這個類中檢視相關定義(在日後的開發中我們會經常這麼來做,畢竟要記住iOS開發中所有的API是不現實的,有些API我們可以通過這種方法來查詢),例如我們可以檢視這個類的內容:

UIViewSourceCode

當然UIView的定義檔案(.h檔案)也是相當長的,我們如果全部截圖也沒有意義。這裡列出常用的屬性和方法。

屬性 說明
@property(nonatomic) CGRect            frame; 控制元件的位置和大小,所有的控制元件必須指定這個屬性,否則即使有控制元件也無法顯示
@property(nonatomic) CGRect            bounds; 當前控制元件位置和大小,但是和frame不同的是它的位置是確定的(0,0)
@property(nonatomic) CGPoint           center; 控制元件的中心位置,一般使用者進行控制元件定位
@property(nonatomic) CGAffineTransform transform; 控制元件矩陣變化,包括平移、縮放、旋轉,預設為CGAffineTransformIdentity
@property(nonatomic) UIViewAutoresizing autoresizingMask; 控制元件旋轉時大小自動伸縮,預設為UIViewAutoresizingNone
@property(nonatomic,readonly) UIView       *superview; 當前控制元件的父控制元件
@property(nonatomic,readonly,copy) NSArray *subviews; 當前控制元件的所有一級子控制元件,注意其子控制元件的子控制元件並不包括在內
@property(nonatomic,getter=isHidden) BOOL              hidden; 是否隱藏,預設為NO
@property(nonatomic)                 UIViewContentMode contentMode;  內容模式,主要用於指定控制元件內容(注意不是子控制元件)如何填充,一般UIImageView經常使用,預設為UIViewContentModeScaleToFill
@property(nonatomic)                                 NSInteger tag; 控制元件的標示,可以儲存一些和當前控制元件有關的資訊(但是注意只能是整形),預設為0
方法   說明
- (void)addSubview:(UIView *)view; 新增子控制元件
- (void)removeFromSuperview; 從父控制元件中移除當前控制元件
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index; 在指定位置插入子控制元件
+ (void)beginAnimations:(NSString *)animationID context:(void *)context; 開始一段動畫
+ (void)commitAnimations; 結束一段動畫,注意在開始和結束之間如果控制元件的某些屬性發生變化iOS將以動畫方式進行改變
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations NS_AVAILABLE_IOS(4_0); 以block的形式執行一段動畫,注意這個方法有幾種相關的方法
- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2); 新增手勢操作
- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2); 移除手勢操作

注意上面所有的位置屬性都是相對於其父控制元件而言(不是相對於螢幕而言),多數屬性比較簡單這裡不再詳細解釋,我們重點解釋一下autoresizingMask、transform屬性。

autoresizingMask

autoresizingMask這個屬性一般我們進行螢幕旋轉的時候經常用到,它的值是一個列舉型別:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,      //不進行自動調整
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0, //自動調整與superview左側距離,右側距離保持不變
    UIViewAutoresizingFlexibleWidth        = 1 << 1, //自動調整控制元件自身寬度,保證與superview左右距離不變
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2, //自動調整與superview右側距離,左側距離保持不變
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3, //自動調整與superview頂部距離,底部距離保持不變
    UIViewAutoresizingFlexibleHeight       = 1 << 4, //自動調整控制元件自身高度,保證與superview上下距離不變
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5  //自動調整與superview底部距離,頂部距離保持不變
};

通過註釋大家應該大概瞭解每個列舉值的意義,但是我們知道列舉經常進行按位或操作(“|”),例如如果autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleLeftMargin那麼iOS如何處理呢?此時會自動調整左邊的距離和控制元件自身寬度,右側距離不變,同時保證左側距離和控制元件寬度同等比例的調整(延長或縮短)。例如在iPhone 5中,如果一個按鈕假設自身寬度為200,左右側距離均為60(左側和寬度比例3:10),當從豎屏旋轉到橫屏的時候(此時寬度由320變為568,注意如果有狀態列則寬度變為568-20=548),由於右側邊距不變為60,根據比例左側邊距應該是(568-60)*(3/13)=117,寬度為:(568-60)*(10/13)=391。

請看下面的程式碼(下面例子通過純程式碼方式建立iOS應用,並且自定義一個KCMainViewController):

AppDelegate.m

//
//  AppDelegate.m
//  UIViewAndUIScrollView
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "AppDelegate.h"
#import "KCMainViewController.h"
#import "KCTransformViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate
            

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    self.window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
    
    KCMainViewController *mainController=[[KCMainViewController alloc]init];
    self.window.rootViewController=mainController;
    self.window.backgroundColor=[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1];

    [self.window makeKeyAndVisible];
    
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

@end

KCMainViewController.m

//
//  KCMainViewController.m
//  UIViewAndUIScrollView
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"

@interface KCMainViewController (){
    UIButton *_btn; //私有變數
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //新增一個Button
    _btn=[[UIButton alloc]initWithFrame:CGRectMake(60, 100, 200, 50)];
    _btn.backgroundColor=[UIColor orangeColor];
    [_btn setTitle:@"Hello,world!" forState:UIControlStateNormal];
    _btn.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleLeftMargin;
    [self.view addSubview:_btn];
    
}

#pragma mark 螢幕旋轉事件
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
    NSLog(@"%@",NSStringFromCGRect(_btn.frame));
}
@end

在上面的程式碼中設定了window的背景為灰色,雖然上面有一個UIView但是我們可以看到最終效果是灰色的,這說明UIView預設是透明的。另外定義了一個私有成員變數_btn,這種定義方式大家以後會經常用到。

執行效果:

豎屏

autoSize_portrait

橫屏

autoSize_landscape

 

注意上面執行前請先隱藏iOS狀態列目,全域性隱藏iO狀態列的方法:

1.在info.plist 中設定Status bar is initially hidden為YES

2.在info.plist中設定View controller-based status bar appearance 為NO

 

transform

transform我們一般稱為形變屬性,其本質是通過矩陣變化改變控制元件的大小、位置、角度等,這裡我們通過一個例子來看一下具體的操作,在下面的例子中我們也會看到UIImageView控制元件的常用操作。

KCTransformViewController.m

//
//  KCTransformViewController.m
//  UIViewAndUIScrollView
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCTransformViewController.h"
//定義rgb顏色
#define NORMAL_COLOR [UIColor colorWithRed:75/255.0 green:160/255.0 blue:253/255.0 alpha:1]
#define HIGHLIGHTED_COLOR [UIColor colorWithRed:197/255.0 green:221/225.0 blue:249/225.0 alpha:1]
//按鈕操作
typedef void(^ ButtonHandle)();

@interface KCTransformViewController (){
    UIImageView *_imageView;//圖片控制元件
    UIButton *_btnRotation;//旋轉按鈕
    UIButton *_btnScale;//縮放按鈕
    UIButton *_btnTranslate;//移動按鈕
}

@end

@implementation KCTransformViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self addImageView];
    [self addRotationButton];
    [self addScaleButton];
    [self addTranslateButton];
    
}

#pragma mark 新增圖片控制元件
-(void)addImageView{
    //直接使用圖片名稱,系統會自動到資原始檔中找到對應的檔案
    UIImage *image=[UIImage imageNamed:@"promo_ios8.png"];
    //如果使用initWithImage進行初始化則控制元件大小會自動設定成圖片大小
    _imageView=[[UIImageView alloc]initWithImage:image];
    _imageView.frame=CGRectMake(20, 20, 280, 154);
    //設定內容填充模式為等比例填充
    _imageView.contentMode=UIViewContentModeScaleAspectFit;
    //self.view就是每個檢視控制器中的view屬性
    [self.view addSubview:_imageView];
}

#pragma mark 新增旋轉按鈕
-(void)addRotationButton{
    _btnRotation=[self getButton];
    _btnRotation.frame=CGRectMake(20, 400, 280, 30);
    [_btnRotation setTitle:@"旋轉" forState:UIControlStateNormal];
    //新增按鈕點選事件
    [_btnRotation addTarget:self action:@selector(rotation:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_btnRotation];
}

#pragma mark 新增縮放按鈕
-(void)addScaleButton{
    //在上面一個按鈕位置的基礎上確認當前位置
    CGRect scaleButtonFrame=_btnRotation.frame;
    scaleButtonFrame.origin.y+=40;
    _btnScale =[self getButton];
    _btnScale.frame=scaleButtonFrame;
    [_btnScale setTitle:@"縮放" forState:UIControlStateNormal];
    //新增按鈕點選事件
    [_btnScale addTarget:self action:@selector(scale:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_btnScale];
}

#pragma mark 新增移動按鈕
-(void)addTranslateButton{
    CGRect translateButtonFrame=_btnScale.frame;
    translateButtonFrame.origin.y+=40;
    _btnTranslate =[self getButton];
    _btnTranslate.frame=translateButtonFrame;
    [_btnTranslate setTitle:@"移動" forState:UIControlStateNormal];
    [_btnTranslate addTarget:self action:@selector(translate:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_btnTranslate];
    
}

#pragma mark 圖片旋轉方法,注意引數中的btn表示當前點選按鈕
-(void)rotation:(UIButton *)btn{
    [self animation:^{
        //注意旋轉角度必須是弧度,不是角度
        CGFloat angle=M_PI_4;//M開頭的巨集都是和數學(Math)相關的巨集定義,M_PI_4表示四分之派,M_2_PI表示2派
        //使用CGAffineTransformMakeRotation獲得一個旋轉角度形變
        //但是需要注意tranform的旋轉不會自動在原來的角度上進行疊加,所以下面的方法旋轉一次以後再點選按鈕不會旋轉了
        //_imageView.transform=CGAffineTransformMakeRotation(angle);
        //利用CGAffineTransformRotate在原來的基礎上產生一個新的角度(當然也可以定義一個全域性變數自己累加)
        _imageView.transform=CGAffineTransformRotate(_imageView.transform, angle);
        
    }];
}

#pragma mark 圖片縮放方法
-(void)scale:(UIButton *)btn{
//    [self animation:^{
//        CGFloat scalleOffset=0.9;
//        //_imageView.transform=CGAffineTransformMakeScale(scalleOffset, scalleOffset);
//        _imageView.transform= CGAffineTransformScale(_imageView.transform, scalleOffset, scalleOffset);
//    }];
    //通常我們使用UIView的靜態方法實現動畫而不是自己寫一個方法
    [UIView animateWithDuration:0.5 animations:^{
        CGFloat scalleOffset=0.9;
        //_imageView.transform=CGAffineTransformMakeScale(scalleOffset, scalleOffset);
        _imageView.transform= CGAffineTransformScale(_imageView.transform, scalleOffset, scalleOffset);
    }];
}

#pragma mark 圖片移動方法
-(void)translate:(UIButton *)btn{
    [self animation:^{
        CGFloat translateY=50;
        //_imageView.transform=CGAffineTransformMakeTranslation(0, translateY);
        _imageView.transform=CGAffineTransformTranslate(_imageView.transform, 0, translateY);
    }];
}

#pragma mark 動畫執行方法,注意這裡可以使用UIView的animateWithDuration方法代替這裡只是為了演示
-(void)animation:(ButtonHandle)handle{
    //開始動畫
    [UIView beginAnimations:@"animation" context:nil];
    //設定動畫執行時間
    [UIView setAnimationDuration:0.5];
    
    handle();
    
    //執行動畫操作
    [UIView commitAnimations];
    
}

#pragma mark 取得一個按鈕,統一按鈕樣式
-(UIButton *)getButton{
    UIButton *button =[[UIButton alloc]init ];
    //設定正常狀態下字型顏色
    [button setTitleColor:NORMAL_COLOR forState:UIControlStateNormal];
    //設定高亮狀態下的字型顏色
    [button setTitleColor:HIGHLIGHTED_COLOR forState:UIControlStateHighlighted];
    return button;
}

@end
  • 獲得CGAffineTransform有多種方法,例如使用CGAffineTransformMake,但是對於矩陣操作相對比較麻煩,事實上iOS已經為我們準備好了三個方法:CGAffineTransformMakeRotation(旋轉)、CGAffineTransformMakeScale(縮放)、CGAffineTransformMakeTranslation(移動);
  • transform進行旋轉、縮放、移動的時候不是在原來的基礎上增量形變的,因此如果需要持續在原來的基礎上旋轉、縮放、移動那麼每次需要在原來的基礎上增加或減少。當然,我們可以定義一個全域性變數進行累加,但是事實上iOS已經為我們提供好了三個對應的方法,分別用於在原來的角度、縮放、移動位置的基礎上做出修改:CGAffineTransformRotate、CGAffineTransformScale、CGAffineTransformTranslate;
  • Objc語法規定不允許直接修改一個物件的結構體屬性的成員,只能給這個屬性直接賦值為一個結構體型別,例如上面的程式碼中如果寫成“_btnRotation.frame.origin.x=380;”是不正確的;
  • 上面的程式碼中我們用到了UIView的動畫相關方法,在iOS開發中動畫開發異常簡單,而且動畫和邏輯處理是完全分離的,只要在兩個動畫方法之間修改一個控制元件的屬性那麼當程式碼執行時就會自動新增動畫效果,為了複習前面的block這裡我們實現了一個類似於animation方法,類似於UIView的animateWithDuration靜態方法的功能,僅僅為了說明它的實現原理,實際開發中可以直接呼叫animateWithDuration即可(或相關方法);

執行效果:

transform

注意在iOS開發中推薦使用png圖片,iOS會對png圖片進行優化。

UIScrollView

通過上面的介紹相信大家對於UIView的基本操作應該比較熟悉了,那麼下面就看一下UIView的子控制元件UIScrollView 。顧名思義,這是一個可以處理滾動操作的檢視,UIScrollView在開發過程中使用很頻繁,而且它也經常作為其他控制元件的子控制元件,例如UITableView就繼承自UIScrollView。 我們還是先看一下UIScrollView的常用屬性和方法:

屬性 說明
@property(nonatomic)         CGPoint                      contentOffset; 內容偏移量,當前顯示的內容的頂點相對此控制元件頂點的x、y距離,預設為CGPointZero
@property(nonatomic)         CGSize                       contentSize; 控制元件內容大小,不一定在顯示區域,如果這個屬性不設定,此控制元件無法滾動,預設為CGSizeZero
@property(nonatomic)         UIEdgeInsets                 contentInset; 控制元件四周邊距,類似於css中的margin,注意邊距不作為其內容的一部分,預設為UIEdgeInsetsZero
@property(nonatomic,assign) id<UIScrollViewDelegate>      delegate; 控制元件代理,一般用於事件監聽,在iOS中多數控制元件都是通過代理進行事件監聽的
@property(nonatomic)         BOOL                         bounces; 是否啟用彈簧效果,啟用彈簧效果後拖動到邊緣可以看到內容後面的背景,預設為YES
@property(nonatomic,getter=isPagingEnabled) BOOL          pagingEnabled; 是否分頁,如果分頁的話每次左右拖動則移動寬度是螢幕寬度整數倍,預設為NO
@property(nonatomic,getter=isScrollEnabled) BOOL          scrollEnabled;   是否啟用滾動,預設為YES
@property(nonatomic)         BOOL                         showsHorizontalScrollIndicator; 是否顯示橫向滾動條,預設為YES
@property(nonatomic)         BOOL                         showsVerticalScrollIndicator; 是否顯示縱向滾動條,預設為YES
@property(nonatomic) CGFloat minimumZoomScale; 最小縮放倍數,預設為1.0
@property(nonatomic) CGFloat maximumZoomScale; 最大縮放倍數(注意只有maximumZoomScale大於minimumZoomScale才有可能縮放),預設為1.0
@property(nonatomic,readonly,getter=isTracking)     BOOL tracking; (狀態)是否正在被追蹤,手指按下去並且還沒有拖動時是YES,其他情況均為NO
@property(nonatomic,readonly,getter=isDragging)     BOOL dragging; 是否正在被拖拽
@property(nonatomic,readonly,getter=isDecelerating) BOOL decelerating; 是否正在減速
@property(nonatomic,readonly,getter=isZooming)       BOOL zooming; 是否正在縮放
方法 說明
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; 設定滾動位置,第二個參數列示是否啟用動畫效果
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated; 滾動並顯示指定區域的內容,第二個參數列示是否啟用動畫效果
代理方法 說明
- (void)scrollViewDidScroll:(UIScrollView *)scrollView; 滾動事件方法,滾動過程中會一直迴圈執行(滾動中…)
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView; 開始拖拽事件方法
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; 拖拽操作完成事件方法
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView; 即將停止滾動事件方法(拖拽鬆開後開始減速時執行)
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; 滾動停止事件方法(滾動過程中減速停止後執行)
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view NS_AVAILABLE_IOS(3_2); 開始縮放事件方法
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2); 縮放操作完成事件方法
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView; 返回縮放檢視,注意只有實現這個代理方法才能進行縮放,此方法返回需要縮放的檢視

contentSize、contentInset、contentOffset在開發中會經常使用,為了幫助大家理解這裡以圖形的形式展現三者之間的關係:

UIScrollViewLayout

關於上面列出的幾個方法,我們有必要說一下它們的執行順序:

a.如果我們拖動一個UIScrollView中的子控制元件移動的時候它的執行順序如下:開始拖拽,滾動,滾動…,停止拖拽,將要停止滾動,滾動,滾動…,停止滾動

紅色部分有可能執行也有可能不執行,關鍵看你拖拽的停止的時候是突然停止還是有一段慣性讓他繼續執行(就好像剎車一樣,如果是急剎車就沒有後面的慣性滑動了,如果是慢慢踩剎車可能會有一段滑動距離)。但是不管怎麼樣滾動事件會一直執行,因此如果在這個事件中進行某種操作一定要注意效能。

b.如果我們縮放UIScrollView的子控制元件的時候它的執行順序如下:開始縮放,滾動,滾動…,停止縮放。同樣在這個過程中滾動事件會一直呼叫(當然如果縮放過程中手指有別的動作也可能會觸發其他事件,這個大家可以自己體會一下)。

下面我們簡單做一個例子

KCScrollViewController.h

//
//  KCScrollViewController.h
//  UIViewAndUIScrollView
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface KCScrollViewController : UIViewController

@property (nonatomic,strong) UIScrollView *scrollView;

@property (nonatomic,strong) UIImageView *imageView;

@end

KCScrollViewController.m

//
//  KCScrollViewController.m
//  UIViewAndUIScrollView
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCScrollViewController.h"

//實現UIScrollView代理
@interface KCScrollViewController ()<UIScrollViewDelegate>

@end

@implementation KCScrollViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //新增scrollView控制元件
    //注意UIScreen代表當前螢幕物件,其applicationFrame是當前螢幕內容區域
    _scrollView =[[UIScrollView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
    //_scrollView.backgroundColor=[UIColor redColor];
    _scrollView.contentMode=UIViewContentModeScaleToFill;
    [self.view addSubview:_scrollView];
    
    
    //新增圖片控制元件
    UIImage *image=[UIImage imageNamed:@"wwdc14-labs-hero-background.jpg"];
    _imageView=[[UIImageView alloc]initWithImage:image];
    [_scrollView addSubview:_imageView];
    
    
    
    //contentSize必須設定,否則無法滾動,當前設定為圖片大小
    _scrollView.contentSize=_imageView.frame.size;
    
    //實現縮放:maxinumZoomScale必須大於minimumZoomScale同時實現viewForZoomingInScrollView方法
    _scrollView.minimumZoomScale=0.6;
    _scrollView.maximumZoomScale=3.0;
    //設定代理
    _scrollView.delegate=self;
    

    //邊距,不屬於內容部分,內容座標(0,0)指的是內容的左上角不包括邊界
    //_scrollView.contentInset=UIEdgeInsetsMake(10, 20, 10, 20);
    
    //顯示滾動內容的指定位置
    //_scrollView.contentOffset=CGPointMake(10, 0);
    
    //隱藏滾動條
    _scrollView.showsHorizontalScrollIndicator=NO;
    _scrollView.showsVerticalScrollIndicator=NO;
    
    //禁用彈簧效果
    //_scrollView.bounces=NO;
}

#pragma mark 實現縮放檢視代理方法,不實現此方法無法縮放
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
    return _imageView;
}
-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
    NSLog(@"scrollViewWillBeginDecelerating");
}
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    NSLog(@"scrollViewDidEndDecelerating");
}
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    NSLog(@"scrollViewWillBeginDragging");
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    NSLog(@"scrollViewDidEndDragging");
}
-(void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view{
    NSLog(@"scrollViewWillBeginZooming");
}
-(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale{
    NSLog(@"scrollViewDidEndZooming");
}
//-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
//    NSLog(@"scrollViewDidScroll");
//}

#pragma mark 當圖片小於螢幕寬高時縮放後讓圖片顯示到螢幕中間
-(void)scrollViewDidZoom:(UIScrollView *)scrollView{
    CGSize originalSize=_scrollView.bounds.size;
    CGSize contentSize=_scrollView.contentSize;
    CGFloat offsetX=originalSize.width>contentSize.width?(originalSize.width-contentSize.width)/2:0;
    CGFloat offsetY=originalSize.height>contentSize.height?(originalSize.height-contentSize.height)/2:0;

    _imageView.center=CGPointMake(contentSize.width/2+offsetX, contentSize.height/2+offsetY);
}

@end

執行效果如下:

scroll

預設情況下縮放後的內容會放到UIScrollView的內容起始位置,所以如果要想縮放後內容放到中間我們必須自己維護它的位置,上面已經給出了設定方法。

擴充套件—ARC

iOS5之後引入了ARC特性,程式中不用自己retain、release、autorelease操作,編譯器會自動為你管理記憶體,編譯時自動加上記憶體釋放的程式碼,使用起來很方便。ARC是編譯器特性,而不是iOS執行時特性,其實質還是手動管理記憶體,只是相應記憶體管理的程式碼編譯器會自動生成而已。由於ARC是編譯器特性,因此它管理記憶體的規則和之前ObjC記憶體管理是類似的,只要有一個物件引用(強引用)指向這個物件,那麼這個物件就不會被釋放。

在開啟ARC之後我們可以使用四個關鍵字修飾我們的成員變數、區域性變數和屬性:

  • strong(修飾變數用__strong):強引用,相當於原來的retain,每次賦值引用計數器加1,只要指標引用這個物件,這個物件就不會被銷燬;
  • weak(修飾變數用__weak):弱引用,相當於assign,和assign不同的是當物件釋放後該變數會設定為nil防止野指標(雖然之前講過的內容中assign都是應用於基本資料型別,其實它也完全可以修飾物件型別的屬性);
  • unsafe_unretained(修飾變數用__unsafe_unretained):和weak類似,區別就是如果物件釋放後它不會像weak一樣自動將指標設定為nil,有可能出現野指標;
  • __autoreleasing(只能修飾變數不能修飾屬性):修飾一個物件在使用完之後自動釋放,通常用於延遲釋放記憶體,同在MRC下呼叫物件的autorelease方法是等效的;

注意:

  1. 除了weak(注意不是__weak)之外其他的修飾符在非ARC(MRC)下使用也不會報錯,但是這麼做並沒有什麼意義,因為在編譯時會被忽略。舉例來說:在MRC下使用__autoreleasing修飾一個物件也不會自動釋放,而是應該使用autorelease方法。同樣的,在MRC下使用__strong來修飾一個變數也同樣是會直接忽略這個關鍵字;
  2. unsafe_unretained(或者__unsafe_unretained)和weak(或__weak)的區別不大,只是weak(或__weak)做釋放之後會將變數設定為nil避免野指標,之所以目前兩個關鍵字還存在主要是因為後者在在iOS5.0及lion之後才出現,出於相容性考慮,因此推薦使用weak或__weak;
  3. __autoreleasing主要用於函式引數是ObjC物件指標的情況下(也就是引數”NSObject **obj”型別,指標的指標),典型的應用就是NSError的使用。在這種情況下,經常需要在函式內部重新建立一個物件給傳入的引數賦值(修改引數內容),如果使用__autorelesing引數編譯器在處理內部函式時會使用自動釋放池,即保證內部物件能夠正常釋放又可以修改外部變數。之所以很多時候使用NSError作為引數傳遞到一些方法中沒有將變數宣告為__autoreleasing是因為編譯器已經自動做了處理(因此,如果考慮到效能推薦還是加上此引數);

strong和weak在iOS開發過程中經常使用,這裡簡單看一個例子(注意這兩個引數仍然可以修飾屬性,意義是完全一樣的在此不再演示)

KCPerson.h

//
//  KCPerson.h
//  ARC
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface KCPerson : NSObject

@property (nonatomic,assign) int no;

@end

KCPerson.m

//
//  KCPerson.m
//  ARC
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCPerson.h"

@implementation KCPerson

-(NSString *)description{
    return [NSString stringWithFormat:@"no=%i",_no];
}

@end

main.m

//
//  main.m
//  ARC
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "KCPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //strong
        __strong KCPerson *person1=[[KCPerson alloc]init];
        __strong KCPerson *person2=person1;
        person1.no=1;
        NSLog(@"%@",person2); //結果:no=1
        person1=nil;
        NSLog(@"%@",person2); //結果:no=1
        
        
        //weak
        __strong KCPerson *person3=[[KCPerson alloc]init];
        __weak KCPerson *person4=person3;
        person3.no=3;
        NSLog(@"%@",person4); //結果:no=3
        person3=nil;
        NSLog(@"%@",person4); //結果:(null)

    }
    return 0;
}

由於person1和person2都指向一個物件並且都是強引用,因此當person1設定為nil時物件仍然不會釋放,所以此時person2還是指向這個物件,可以正常輸出;person3和它指向的物件是強引用,而person4是弱引用,因此當person3設定為nil後,物件沒有了強引用就會釋放,此時再列印person4自然就是null。為了說明strong和weak的使用,下面使用圖形方式描繪上面的情況:

strong--person1和person2的關係

strong2

weak--person3和person4的關係

weak2

由此得出如下結論:

  1. 不管是怎麼管理記憶體都是針對物件型別而言(無論是strong,weak都不能應用到基本資料型別),對於基本資料型別直接宣告為assign就可以了,它不需要我們自己管理記憶體;
  2. 所有的指標變數預設都是__strong型別,因此我們通常省略不寫__strong;
  3. 如果一個物件沒有強引用之後即使存在弱引用它也會被釋放,與此同時弱引用將被設定為nil;

回過頭來我們看一下前面UIScrollView部分的幾個屬性都設定成了strong,如果設定成weak行不行呢?答案是否定的。如果我們設定成weak,Xcode首先就會給出提出“Assigning retained object to weak variable; object will be released after assignment”,就是說ObjC物件賦值給一個弱引用變數,賦值之後物件會立即被銷燬。其實根據前面介紹的內容很容易理解,就拿上面的scrollView屬性來說,如果設定為weak,當使用“_scrollView =[[UIScrollView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];”給這個變數賦值,根據前面的知識如果一個物件沒有了強引用就會被銷燬,賦值完成後執行時看到這個物件只有一個弱引用_scrollView自然就會銷燬這個物件,因此如果執行上面的程式就達不到之前看到的效果了。

但是如果使用storyboard來設計介面的時候,我們會發現系統預設生成的屬性就是weak,此時為什麼不會銷燬呢?那是因為它的頂層物件保持了一個強引用strong,因此這個物件不會被銷燬。這樣一來我們得出如下結論:

  1. 在iOS開發中使用strong、weak代替之前的retain、assign(基本型別使用assign);
  2. 如果一個屬性使用IBOutlet修飾(也就是此屬性時strongboard中元件)那麼使用weak;
  3. 如果一個屬性不是storyboard元件(一般純程式碼編寫介面時),使用strong;

UIScrollView實戰

前面介紹了iOS中UIKit的一些簡單知識,這裡我們一起利用前面的知識做一個例子--圖片無限迴圈滾動。在這個例子中我們需要解決如下兩個問題:

如何無限迴圈?

我們知道在UIScrollView中如果放置其他控制元件後,只要設定contentSize之後這些圖片就可以滾動。如果要讓圖片無限迴圈那麼只有兩種辦法,一種是無限迴圈疊加圖片,另一種就是如果最後一張圖片瀏覽完立即顯示第一張圖片。很明顯第一種方法是不現實的,我們考慮使用第二種方式。其實使用第二種方式實現原理比較簡單,只要在圖片前後各放一張圖片即可(此時共有n+2個圖片在UIScrollView中)。例如我們有5張圖片,只要使用7個UIImageView依次存放:圖片5,圖片1,圖片2,圖片3,圖片4,圖片5,圖片1。當從圖片1滾動到圖片5時由於最後一張是圖片1就給使用者一種無限迴圈的感覺,當這張圖完全顯示後我們迅速將UIScrollView的contentOffset設定到第二個UIImageView,也就是圖片1,接著使用者可以繼續向後滾動。當然向前滾動原理完全一樣,當滾動到第一張圖片(圖片5)就迅速設定UIScrollView的contentOffset顯示第6張圖(圖片5)。為了方便說明請看下面的示意圖(注意示意圖由於寬度有限只描述了3張圖片顯示的情景):

loopScroll2

如何優化效能?

無限迴圈實現了,但是我們知道如果圖片過多這些圖片勢必全部載入到記憶體,這是我們不願意看到的,此時我們需要優化上面的方案。其實從上面的方案我們也可以看出端倪,我們完全沒必要建立n+2個UIImageView,其實3個已經足夠(事實上也可以用兩個實現,大家不妨自己思考一下),只要一直保持顯示中間的UIImageView,滾動時動態更改三個UIImageView的圖片即可。例如三個UIImageView預設放圖片5、圖片1、圖片2,當前顯示中間的UIImageView,也就是圖片1,。如果向後滾動那麼就會顯示圖片2,當圖片2顯示完整後迅速重新設定三個UIImageView的內容為圖片1、圖片2、圖片3,然後通過contentOffset設定顯示中間的UIImageView,也就是圖片2。繼續向後看到圖片3,當圖片3滾動完成迅速重新設定3個UIImageView的內容為圖片2、圖片3、圖片4,然後設定contentOffset顯示中間的UIImageView,也就是圖片3。當然,向前滾動原理完全一樣,如此就給使用者一種迴圈錯覺,而且不佔用過多記憶體。

下面給出具體的實現,在這個程式中除了UIscrollView我們還可以看到UIPageControl的使用,實現並不複雜。首先我們需要將圖片資訊儲存到plist檔案中(日後方便擴充套件),並且設定plist的key表示圖片的名稱,value代表對應的圖片描述,這個描述我們需要展示在介面上方。具體內容如下:

imageInfo.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>0.jpg</key>
    <string>iphone 5s</string>
    <key>1.jpg</key>
    <string>iphone 5c</string>
    <key>2.jpg</key>
    <string>ipad min with retain</string>
    <key>3.jpg</key>
    <string>ipad air</string>
    <key>4.jpg</key>
    <string>ipod</string>
    <key>5.jpg</key>
    <string>ipod touch</string>
    <key>6.jpg</key>
    <string>mac book pro</string>
    <key>7.jpg</key>
    <string>mac book air</string>
    <key>8.jpg</key>
    <string>imac</string>
</dict>
</plist>

在程式中我們需要讀取plist檔案並載入對應的圖片,這裡我們將圖片按順序依次命名:0.jpg、1.jpg…8.jpg。我們的程式主要集中於自定義的KCMainViewController.m中,在這裡我們宣告1個UIScrollView和3個UIImageView用於顯示圖片,同時宣告一個UILable顯示圖片描述資訊,宣告一個UIPageControl來顯示當前圖片頁數,具體程式碼如下:

//
//  KCMainViewController.m
//  ImageViewer
//
//  Created by Kenshin Cui on 14-2-23.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import "KCMainViewController.h"
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 568
#define IMAGEVIEW_COUNT 3

@interface KCMainViewController ()<UIScrollViewDelegate>{
    UIScrollView *_scrollView;
    UIImageView *_leftImageView;
    UIImageView *_centerImageView;
    UIImageView *_rightImageView;
    UIPageControl *_pageControl;
    UILabel *_label;
    NSMutableDictionary *_imageData;//圖片資料
    int _currentImageIndex;//當前圖片索引
    int _imageCount;//圖片總數
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //載入資料
    [self loadImageData];
    //新增滾動控制元件
    [self addScrollView];
    //新增圖片控制元件
    [self addImageViews];
    //新增分頁控制元件
    [self addPageControl];
    //新增圖片資訊描述控制元件
    [self addLabel];
    //載入預設圖片
    [self setDefaultImage];
}

#pragma mark 載入圖片資料
-(void)loadImageData{
    //讀取程式包路徑中的資原始檔
    NSString *path=[[NSBundle mainBundle] pathForResource:@"imageInfo" ofType:@"plist"];
    _imageData=[NSMutableDictionary dictionaryWithContentsOfFile:path];
    _imageCount=(int)_imageData.count;
}

#pragma mark 新增控制元件
-(void)addScrollView{
    _scrollView=[[UIScrollView alloc]initWithFrame:[UIScreen mainScreen].bounds];
    [self.view addSubview:_scrollView];
    //設定代理
    _scrollView.delegate=self;
    //設定contentSize
    _scrollView.contentSize=CGSizeMake(IMAGEVIEW_COUNT*SCREEN_WIDTH, SCREEN_HEIGHT) ;
    //設定當前顯示的位置為中間圖片
    [_scrollView setContentOffset:CGPointMake(SCREEN_WIDTH, 0) animated:NO];
    //設定分頁
    _scrollView.pagingEnabled=YES;
    //去掉滾動條
    _scrollView.showsHorizontalScrollIndicator=NO;
}

#pragma mark 新增圖片三個控制元件
-(void)addImageViews{
    _leftImageView=[[UIImageView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    _leftImageView.contentMode=UIViewContentModeScaleAspectFit;
    [_scrollView addSubview:_leftImageView];
    _centerImageView=[[UIImageView alloc]initWithFrame:CGRectMake(SCREEN_WIDTH, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    _centerImageView.contentMode=UIViewContentModeScaleAspectFit;
    [_scrollView addSubview:_centerImageView];
    _rightImageView=[[UIImageView alloc]initWithFrame:CGRectMake(2*SCREEN_WIDTH, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    _rightImageView.contentMode=UIViewContentModeScaleAspectFit;
    [_scrollView addSubview:_rightImageView];

}
#pragma mark 設定預設顯示圖片
-(void)setDefaultImage{
    //載入預設圖片
    _leftImageView.image=[UIImage imageNamed:[NSString stringWithFormat:@"%i.jpg",_imageCount-1]];
    _centerImageView.image=[UIImage imageNamed:[NSString stringWithFormat:@"%i.jpg",0]];
    _rightImageView.image=[UIImage imageNamed:[NSString stringWithFormat:@"%i.jpg",1]];
    _currentImageIndex=0;
    //設定當前頁
    _pageControl.currentPage=_currentImageIndex;
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",_currentImageIndex];
    _label.text=_imageData[imageName];
}

#pragma mark 新增分頁控制元件
-(void)addPageControl{
    _pageControl=[[UIPageControl alloc]init];
    //注意此方法可以根據頁數返回UIPageControl合適的大小
    CGSize size= [_pageControl sizeForNumberOfPages:_imageCount];
    _pageControl.bounds=CGRectMake(0, 0, size.width, size.height);
    _pageControl.center=CGPointMake(SCREEN_WIDTH/2, SCREEN_HEIGHT-100);
    //設定顏色
    _pageControl.pageIndicatorTintColor=[UIColor colorWithRed:193/255.0 green:219/255.0 blue:249/255.0 alpha:1];
    //設定當前頁顏色
    _pageControl.currentPageIndicatorTintColor=[UIColor colorWithRed:0 green:150/255.0 blue:1 alpha:1];
    //設定總頁數
    _pageControl.numberOfPages=_imageCount;
    
    [self.view addSubview:_pageControl];
}

#pragma mark 新增資訊描述控制元件
-(void)addLabel{
    
    _label=[[UILabel alloc]initWithFrame:CGRectMake(0, 10, SCREEN_WIDTH,30)];
    _label.textAlignment=NSTextAlignmentCenter;
    _label.textColor=[UIColor colorWithRed:0 green:150/255.0 blue:1 alpha:1];

    [self.view addSubview:_label];
}

#pragma mark 滾動停止事件
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    //重新載入圖片
    [self reloadImage];
    //移動到中間
    [_scrollView setContentOffset:CGPointMake(SCREEN_WIDTH, 0) animated:NO];
    //設定分頁
    _pageControl.currentPage=_currentImageIndex;
    //設定描述
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",_currentImageIndex];
    _label.text=_imageData[imageName];
}

#pragma mark 重新載入圖片
-(void)reloadImage{
    int leftImageIndex,rightImageIndex;
    CGPoint offset=[_scrollView contentOffset];
    if (offset.x>SCREEN_WIDTH) { //向右滑動
        _currentImageIndex=(_currentImageIndex+1)%_imageCount;
    }else if(offset.x<SCREEN_WIDTH){ //向左滑動
        _currentImageIndex=(_currentImageIndex+_imageCount-1)%_imageCount;
    }
    //UIImageView *centerImageView=(UIImageView *)[_scrollView viewWithTag:2];
    _centerImageView.image=[UIImage imageNamed:[NSString stringWithFormat:@"%i.jpg",_currentImageIndex]];
    
    //重新設定左右圖片
    leftImageIndex=(_currentImageIndex+_imageCount-1)%_imageCount;
    rightImageIndex=(_currentImageIndex+1)%_imageCount;
    _leftImageView.image=[UIImage imageNamed:[NSString stringWithFormat:@"%i.jpg",leftImageIndex]];
    _rightImageView.image=[UIImage imageNamed:[NSString stringWithFormat:@"%i.jpg",rightImageIndex]];
}

@end

在上面的程式碼中需要提醒大家的是一定要謹慎在滾動時進行相關操作,前面我們說過滾動事件會迴圈執行十分消耗效能,因此如果能不在其中操作的話儘可能不要在這個方法中進行相關操作,例如在上面的程式碼中我們的核心邏輯主要集中在滾動停止事件中,這個事件在一次滾動操作中只需要執行一次。

執行效果:

loopScrollTuning

相關文章