03.實戰(zhàn)技術(shù) 網(wǎng)易新聞,開發(fā)中常見問題,bounds深入研究

03.實戰(zhàn)技術(shù) 網(wǎng)易新聞,開發(fā)中常見問題,bounds深入研究

@(iOS Study)[實戰(zhàn)技術(shù)]


目錄

  • 03.實戰(zhàn)技術(shù) 網(wǎng)易新聞,開發(fā)中常見問題,bounds深入研究
  • 1.通知
    • 通知的概念
    • 通知的簡單使用
    • 通知的注意點
    • 通知在多線程的使用
  • 2.枚舉中的位運算
    • 位運算的使用
  • 3.assign和weak的使用
    • assign和weak的區(qū)別
  • 4.網(wǎng)易新聞
    • 1.基本界面搭建
    • 2.監(jiān)聽標(biāo)題按鈕的點擊
    • 3.監(jiān)聽contentScrollView的滾動,
    • 4.標(biāo)題居中處理
    • 5.標(biāo)題文字縮放,顏色漸變(較難理解)
    • 網(wǎng)易新聞抽取框架
  • 5.bounds的深入研究
    • bounds和frame的簡介
    • UIScrollView底層實現(xiàn)


1.通知

通知的概念

  • 通知與代理的區(qū)別

通知支持一對多,代理支持一對一

  • 通知的用法

凡是要發(fā)送或監(jiān)聽通知,都必須使用通知中心[NSNotificationCenter defaultCenter]

通知的簡單使用

  • 監(jiān)聽通知的兩種方式

    • 監(jiān)聽通知方式一: 普通的addObserver方法監(jiān)聽
      // 監(jiān)聽通知第一種方式
      - (void)test
      {
          // 監(jiān)聽通知
          // Observer;觀察者
          // selector;通知發(fā)出的時候,觀察者調(diào)用方法
          // name:通知名稱
          // object:誰發(fā)出通知,nil:匿名發(fā)送
          [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(note) name:@"wxNote" object:nil];
      }
      // 當(dāng)一個對象即將銷毀的時候就會調(diào)用
      - (void)dealloc
      {
          // 移除通知
          [[NSNotificationCenter defaultCenter] removeObserver:self];
      }
    
    • 監(jiān)聽通知方式二: block方式監(jiān)聽
      只要是系統(tǒng)管理對象(通知是由系統(tǒng)管理的),都不需要自己手動管理,可以用weak.
      @interface ViewController ()
      @property (nonatomic, weak) id observer;
      @end
      - (void)test2
      {
          // name:通知名稱
          // object:誰發(fā)出通知
          // queue:決定block在哪個線程執(zhí)行,nil:表示在發(fā)送通知的線程
          // block:只要有通知發(fā)出,就會調(diào)用block
          // 返回值: 通知對象
          self.observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"wxNote" object:nil queue:[[NSOperationQueue alloc] init] usingBlock:^(NSNotification * _Nonnull note) {
                  
              NSLog(@"%@",[NSThread currentThread]);
          }];
      
      }
      // 當(dāng)一個對象即將銷毀的時候就會調(diào)用
      - (void)dealloc
      {
          // 移除通知
          [[NSNotificationCenter defaultCenter] removeObserver:self.observer];
      }
    
  • 發(fā)送/移除通知的的幾種方法

    • 發(fā)送通知方法
      // Notification: 通知對象
      - (void)postNotification:(NSNotification *)notification;
      // Name: 通知名稱
      // object: 發(fā)送者
      // userInfo:傳遞參數(shù)(字典)
      - (void)postNotificationName:(NSString *)aName object:(nullable id)anObject;
      - (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
    
    • 移除通知方法
      - (void)removeObserver:(id)observer;
      - (void)removeObserver:(id)observer name:(nullable NSString *)aName object:(nullable id)anObject;
    
  • 在對象銷毀時移除移除通知

    // 當(dāng)一個對象即將銷毀的時候就會調(diào)用
    - (void)dealloc
    {
        // 移除通知
        [[NSNotificationCenter defaultCenter] removeObserver:self.observer];
    }
    

通知的注意點

  • 通知注意點
    • 一定要記得移除通知.(通知中心調(diào)用removeObserver方法移除)
    • 一定要先監(jiān)聽通知,再發(fā)出通知.

通知在多線程的使用

  • 通知在多線程中注意點

    • 監(jiān)聽通知方法調(diào)用:跟發(fā)送通知線程有關(guān)系.
      如果沒指定通知在哪個線程執(zhí)行,則通知監(jiān)聽調(diào)用的方法在發(fā)送通知的線程執(zhí)行.
  • 通知在多線程的使用

    • 子線程發(fā)送通知,主線程監(jiān)聽通知.
      // 發(fā)出通知:子線程 監(jiān)聽通知:主線程 方法調(diào)用:子線程
      - (void)viewDidLoad {
          [super viewDidLoad];
          
          // 監(jiān)聽通知:主線程監(jiān)聽通知,
          [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(note) name:@"wxNote" object:nil];
          
          // 發(fā)出通知
          // name:通知名稱
          // object:誰發(fā)出通知
          // 開啟子線程條件:1.異步 2.并發(fā)隊列(全局并發(fā)隊列)
          // 2.異步 串行 也會開啟子線程
          dispatch_async(dispatch_get_global_queue(0, 0), ^{
              // 異步任務(wù)
              [[NSNotificationCenter defaultCenter] postNotificationName:@"wxNote" object:nil];
          });
      }
      // 監(jiān)聽通知發(fā)出,就會調(diào)用
      - (void)note
      {
          NSLog(@"%@",[NSThread currentThread]);
      }
      
      // 當(dāng)一個對象即將銷毀的時候就會調(diào)用
      - (void)dealloc
      {
          // 移除通知
          [[NSNotificationCenter defaultCenter] removeObserver:self];
      }
    
    • 主線程發(fā)送通知,子線程監(jiān)聽通知(如果監(jiān)聽通知沒指定block在哪個線程執(zhí)行(queue: nil)時,則表示block在發(fā)送通知的線程中執(zhí)行)
      以下方法實際返回的是通知對象,需拿到通知對象,用于移除通知.
      
      // 發(fā)送通知: 主線程 監(jiān)聽通知: 子線程 方法調(diào)用: 主線程
      - (void)viewDidLoad
      {
          [super viewDidLoad];
          
          // 子線程監(jiān)聽通知
          dispatch_async(dispatch_get_global_queue(0, 0), ^{
              [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(note) name:@"wxNote" object:nil];
          });
      }
      
      - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
      {
          // 主線程發(fā)送通知
          [[NSNotificationCenter defaultCenter] postNotificationName:@"wxNote" object:nil];
      }
      
      // 監(jiān)聽通知發(fā)出,就會調(diào)用
      - (void)note
      {
          NSLog(@"%@",[NSThread currentThread]);
      }
      
      // 當(dāng)一個對象即將銷毀的時候就會調(diào)用
      - (void)dealloc
      {
          // 移除通知
          [[NSNotificationCenter defaultCenter] removeObserver:self];
      }
    

2.枚舉中的位運算

位運算的使用

  • 位運算的簡單使用
#import "ViewController.h"

// 定義位移枚舉
typedef NS_OPTIONS(NSUInteger, WXDirection) {
    WXDirectionTop      = 1 << 0,
    WXDirectionBottom   = 1 << 1,
    WXDirectionLeft     = 1 << 2,
    WXDirectionRight    = 1 << 3,
};

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self testDirection:WXDirectionTop | WXDirectionRight];
}

- (void)testDirection:(WXDirection)directions {
    
    // 用位與運算判斷
    if (directions & WXDirectionTop) {
        NSLog(@"上");
    }
    if (directions & WXDirectionBottom) {
        NSLog(@"下");
    }
    if (directions & WXDirectionLeft) {
        NSLog(@"左");
    }
    if (directions & WXDirectionRight) {
        NSLog(@"右");
    }
}


3.assign和weak的使用

assign和weak的區(qū)別

  • assign關(guān)鍵字的介紹

    • 用assign關(guān)鍵字描述對象成員屬性,會報壞內(nèi)存訪問錯誤
    • 用assign關(guān)鍵字描述的成員屬性,在敲(_屬性名)會提示__unsafe_unretained`.
    • __unsafe_unretained關(guān)鍵字表示: 引用計數(shù)器不會+1,但是對象被銷毀,指針不會清空,所以assign描述對象時,對象釋放后,在用assign描述的成員屬性,會導(dǎo)致壞內(nèi)存訪問錯誤.所以對象不能用assign描述;
    // 錯誤寫法,assign不能描述對象
    @property (nonatomic, assign) UIView *blueView;
    
    • weak關(guān)鍵字的介紹

      • 用weak關(guān)鍵字描述的成員屬性,在敲(_屬性名)會提示__weak.weak:弱指針,不會讓引用計數(shù)器+1,當(dāng)對象被銷毀,這個弱指針會被清空,nil.所以weak關(guān)鍵字可以描述對象成員屬性.
      @property (nonatomic, weak) UIView *redView;
      

4.網(wǎng)易新聞

1.基本界面搭建

  • 搭建基本界面

    • 1.搭建標(biāo)題滾動視圖
    • 2.搭建內(nèi)容滾動視圖
    • 3.添加子控制器
    • 4.添加TitleScrollView中所有button,設(shè)置標(biāo)題(標(biāo)題可以從子控制器中獲取)
  • 注意: 如果已經(jīng)設(shè)置了按鈕的標(biāo)題,但是titleScrollView的按鈕并沒有顯示,可以設(shè)置嘗試self.automaticallyAdjustsScrollViewInsets = NO;,因為導(dǎo)航控制器下的ScrollView,系統(tǒng)會默認(rèn)為所有ScrollView添加額外的滾動區(qū)域64.

2.監(jiān)聽標(biāo)題按鈕的點擊

  • 監(jiān)聽標(biāo)題按鈕的點擊

    • 1.切換選中狀態(tài),修改當(dāng)前選中的按鈕文字顏色
      /** 切換選中狀態(tài),修改當(dāng)前選中的按鈕文字顏色 */
      - (void)selectedBtn:(UIButton *)btn {
          
          // 還原上一個按鈕的字體顏色和字體形變大小
          [self.selectedBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
          self.selectedBtn.transform = CGAffineTransformIdentity;
          
          // 設(shè)置當(dāng)前選中的按鈕的字體顏色和字體形變大小
          self.selectedBtn = btn;
          [self.selectedBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
          self.selectedBtn.transform = CGAffineTransformMakeScale(1.3, 1.3);
      }
    
    • 2.將子控制器的view添加到_contentScrollView對應(yīng)的位置
      /** 將子控制器的view添加到_contentScrollView對應(yīng)的位置 */
      - (void)setupOneViewControllerView:(NSInteger)index {
          
          UIViewController *vc = self.childViewControllers[index];
          // 判斷該子控制器的view是否已經(jīng)添加到當(dāng)前的控制器view上,如果已添加,則直接退出
          if (vc.view.superview) {
              return;
          }
          // 添加到contentScrollView對應(yīng)的位置上
          vc.view.frame = CGRectMake(index * WXScreenW, 0, WXScreenW, self.contentScrollView.frame.size.height);
          [self.contentScrollView addSubview:vc.view];
          
      }
    
    • 3.設(shè)置_contentScrollView的偏移量.如果沒偏移,則還是之前的view,因為添加子控制器的view是添加到contentScrollView指定的位置上.
    self.contentScrollView.contentOffset = CGPointMake(btn.tag * WXScreenW, 0);
    

3.監(jiān)聽contentScrollView的滾動,

  • 監(jiān)聽contentScrollView的滾動

    • 設(shè)置_contentScrollView和titleScrollView的contentSize
    self.contentScrollView.contentSize = CGSizeMake(count * WXScreenW, 0);
    self.titleScrollView.contentSize = CGSizeMake(count * btnW, 0);
    
    • 在添加按鈕的setupAllTitle方法中設(shè)置contentScrollView和titleScrollView的初始配置信息
      - (void)setupAllScrollViewSetting {
          
          // 設(shè)置contentScrollView的代理
          self.contentScrollView.delegate = self;
          
          // 設(shè)置contentScrollView和titleScrollView不添加額外滾動區(qū)域,隱藏滾動條,開啟分頁功能
          self.automaticallyAdjustsScrollViewInsets = NO;
          self.contentScrollView.showsHorizontalScrollIndicator = NO;
          self.titleScrollView.showsHorizontalScrollIndicator = NO;
          self.contentScrollView.pagingEnabled = YES;
      }
    
    • 監(jiān)聽contentScrollView減速完成.在scrollViewDidEndDecelerating方法中切換刷新按鈕顯示和切換contentScrollView中顯示的view
      /** ScrollView減速完成調(diào)用 */
      - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
          
          // 獲取按鈕的角標(biāo)
          NSInteger index = scrollView.contentOffset.x / WXScreenW;
          
          // 通過角標(biāo)從存放按鈕的數(shù)組中取出按鈕
          UIButton *btn = self.buttons[index];
          
          // 切換按鈕的狀態(tài)
          [self selectedBtn:btn];
          
          // 添加對應(yīng)子控制器的view到contentScrollView
          [self setupOneViewControllerView:index];
          
      }
    

4.標(biāo)題居中處理

  • 標(biāo)題居中計算圖解
1.標(biāo)題居中計算圖解.png
  • 選中標(biāo)題居中處理

    • 在點擊按鈕時調(diào)用的selectedBtn方法中讓標(biāo)題居中
    • 因為點擊按鈕和拖動contentScrollView都會調(diào)用selectedBtn方法,所以在該方法的最后調(diào)用setupTitleButtonCenter方法,讓按鈕居中.
    • setupTitleButtonCenter方法中需對最大最小偏移量進行判斷,實現(xiàn)代碼如下
      /** 設(shè)置當(dāng)前選中按鈕居中 */
      - (void)setupTitleButtonCenter:(UIButton *)selectedBtn {
          
          // 偏移量計算 偏移量 = selectedBtn.center.x - self.contentScrollView.frame.size.width * 0.5
          CGFloat offSetX = selectedBtn.center.x - self.contentScrollView.frame.size.width * 0.5;
          NSLog(@"%0.2f", offSetX);
          
          // 如果計算出來的偏移量小于0,則不偏移,將偏移量置0
          if (offSetX < 0) {
              offSetX = 0;
          }
          
          // 獲取最大的x偏移量,如果超過最大偏移量,則偏移到最大偏移量的位置
          CGFloat maxOffsetX = self.titleScrollView.contentSize.width - WXScreenW;
          if (offSetX > maxOffsetX) {
              offSetX = maxOffsetX;
          }
          
          // 開始偏移titleScrollView
          [self.titleScrollView setContentOffset:CGPointMake(offSetX, 0) animated:YES];
      }
    
  • 標(biāo)題居中初步運行效果

2.網(wǎng)易新聞標(biāo)題居中.gif

5.標(biāo)題文字縮放,顏色漸變(較難理解)

  • 實現(xiàn)標(biāo)題文字縮放,顏色漸變功能

    • 在拖動過程中計算左右按鈕的形變比例和顏色漸變比例
    • 實現(xiàn)標(biāo)題文字縮放,顏色漸變參考代碼
      /** ScrollView滾動的時候調(diào)用 */
      - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
      
          // ------------------------------------------------------------------------
          // 滾動過程中實現(xiàn)標(biāo)題的文字大小變化(形變),標(biāo)題文字顏色漸變
          
          // 1.獲取左右兩個按鈕
          // 1.1 獲取左右按鈕的角標(biāo)
          NSInteger leftIndex = scrollView.contentOffset.x / WXScreenW;
          NSInteger rightIndex = leftIndex + 1;
          
          // 1.2 獲取左右按鈕
          UIButton *leftBtn = self.buttons[leftIndex];
          NSUInteger count = self.childViewControllers.count;
          UIButton *rightBtn = nil;
          if (rightIndex < count) {
              rightBtn = self.buttons[rightIndex];
          }
          
          // 2.計算左右按鈕縮放的比例
          // 2.1 計算右邊按鈕的縮放比例
          CGFloat rightScale = scrollView.contentOffset.x / WXScreenW;
          // 2.2 處理左邊和右邊的比例范圍0.0 ~ 1.0
          // 假設(shè)rightScale: 0.22 ,leftScale: 0.78
          rightScale = rightScale - leftIndex;
          CGFloat leftScale = 1 - rightScale;
          
          // 3.改變文字的大小,不是修改字體,修改按鈕的形變屬性transform
          // 3.1 計算最終要縮放的比例, 將縮放比例0.0 ~ 1.0變成1.0 ~ 1.3
          CGFloat lastRightScale = (rightScale * 0.3) + 1;
          CGFloat lastLeftScale = (leftScale * 0.3) + 1;
          // 3.2 開始縮放左右按鈕
          leftBtn.transform = CGAffineTransformMakeScale(lastLeftScale, lastLeftScale);
          rightBtn.transform = CGAffineTransformMakeScale(lastRightScale, lastRightScale);
          
          // 4.設(shè)置按鈕顏色漸變
          [leftBtn setTitleColor:[UIColor colorWithRed:leftScale green:0 blue:0 alpha:1] forState:UIControlStateNormal];
          [rightBtn setTitleColor:[UIColor colorWithRed:rightScale green:0 blue:0 alpha:1] forState:UIControlStateNormal];
      }
    
  • 網(wǎng)易新聞最終運行效果

3.網(wǎng)易新聞最終運行效果.gif

網(wǎng)易新聞抽取框架

  • 抽取框架步驟

    • 將添加子控制器的操作移到外部,由子類自己決定要添加什么子控制器.
    • 將設(shè)置titleBtn標(biāo)題的方法放在viewWillAppear方法中調(diào)用,而且只需調(diào)用一次.
      /** view即將顯示的時候調(diào)用 */
      - (void)viewWillAppear:(BOOL)animated {
          [super viewWillAppear:animated];
          
          // 設(shè)置titleBtn的標(biāo)題
          if (self.isInitial == NO) {
              
              [self setupAllTitle];
              self.isInitial = YES;
          }
          
      }
    
  • 抽取框架的好處

    • 只要繼承NewsViewController控制器, 就能實現(xiàn)功能.
    • 子類繼承NewsViewController控制器,并添加子控制器就能實現(xiàn)功能.

5.bounds的深入研究

bounds和frame的簡介

frame:以父控件左上角為原點
bounds:以自己內(nèi)容左上角為原點
frame和bounds都是描述一塊區(qū)域

frame描述的這塊區(qū)域:可視范圍
bounds描述的區(qū)域:可視范圍在內(nèi)容范圍顯示的區(qū)域

區(qū)域和點一樣,不同參照物,描述的區(qū)域不同
frame:參照父控件一直不變
frame:參照內(nèi)容,位置會變動

其實bounds:x,y可以為其他值
疑問:bounds的y++,為什么內(nèi)容往上走?
bounds的y++表示要看下面的內(nèi)容,內(nèi)容就會往上移走.

  • bounds:以自己內(nèi)容的左上角為原點,描述可視范圍相對內(nèi)容范圍的位置

  • bounds和frame圖解分析

4.bounds和frame圖解分析.png

UIScrollView底層實現(xiàn)

  • UIScrollView的實現(xiàn)原理是用bounds實現(xiàn)滾動
#import "ViewController.h"

@interface ViewController ()<UIScrollViewDelegate>
@property (weak, nonatomic)  UIView *scrollView;
@property (nonatomic, assign) CGPoint offsetX;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *scrollView = [[UIView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:scrollView];
    _scrollView = scrollView;
    
    // 添加拖動手勢pan
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [scrollView addGestureRecognizer:pan];
    
    // 添加子控件觀察效果
    UISwitch *switchV = [[UISwitch alloc] init];
    [scrollView addSubview:switchV];
}

// 拖動的時候調(diào)用
- (void)pan:(UIPanGestureRecognizer *)pan
{
    // 獲取偏移量
    CGPoint transP = [pan translationInView:pan.view];
    
    _offsetX.x += -transP.x;
    _offsetX.y += -transP.y;

    // 修改其bounds值就能實現(xiàn)滾動效果
    _scrollView.bounds = CGRectMake(_offsetX.x, _offsetX.y, self.view.bounds.size.width, self.view.bounds.size.height);
    // 清0,復(fù)位
    [pan setTranslation:CGPointZero inView:pan.view];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSLog(@"%f %f",scrollView.contentOffset.y,scrollView.bounds.origin.y);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容