源码分享:iOS高仿喜马拉雅FM

Raleigh31U 8年前
   <p>最新用空闲时间高仿了一下喜马拉雅FM这款APP,目前主要完成了发现栏目中的推荐页面。</p>    <h2>效果演示</h2>    <p style="text-align:center"><img src="https://simg.open-open.com/show/3ab48a4cc8897a58ff43579ebf45e8e8.gif"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d1a4f311d369672fef45f8a4587206c3.gif"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ee918a0aba8294b6b0c20f092d48d0e2.gif"></p>    <h2>分析</h2>    <p>+发现tab中有五个小分类,分别对应五个页面,所有在“发现”的控制器中使用了UIPageViewController来控制五个子控制器。</p>    <p>+从Charles抓出来的接口来看,“推荐”页面一共调用了三个接口,分别请求了推荐、热门、直播的内容,所以在这里选择了Reactivecocoa来实现接口的并发访问</p>    <pre>  <code class="language-objectivec">- (void)refreshDataSource {               @weakify(self);      RACSignal *signalRecommend = [RACSignal createSignal:^RACDisposable *(id                        subscriber) {          @strongify(self);          [self requestRecommendList:^{              [subscriber sendNext:nil];          }];          return nil;      }];            RACSignal *signalHotAndGuess = [RACSignal createSignal:^RACDisposable *(id                    subscriber) { @strongify(self); [self requestHotAndGuessList:^{ [subscriber sendNext:nil]; }]; return nil; }]; RACSignal *signalLiving = [RACSignal createSignal:^RACDisposable *(id               subscriber) { @strongify(self); [self requestLiving:^{ [subscriber sendNext:nil]; }]; return nil; }]; [[RACSignal combineLatest:@[signalRecommend,signalHotAndGuess,signalLiving]] subscribeNext:^(id x) { @strongify(self); [(RACSubject *)self.updateContentSignal sendNext:nil]; }]; } 文章转自 耐心_朱迪的简书                       </code></pre>    <p>+在“推荐”页面中有几个轮播图,仔细观察会发现它的轮播图一直想左转换,所以这里的轮播图片需要做一下特殊处理。以实现无限轮播的效果</p>    <pre>  <code class="language-objectivec">- (void)setModel:(XMLYFindFocusImagesModel *)model  {            _model = model;          [self.adverScrollView removeAllSubViews];          self.adverScrollView.contentSize = CGSizeMake(kScreenWidth * _model.list.count, 150);           //1.向scrollView中增加UIImageView的时候,需要在最后一张图片后面将第一张图片添加上去          for(NSInteger index = 0; index <= _model.list.count; index++)   {                //2.如果是最后一张图片,则放置第一张图片          XMLYFindFocusImageDetailModel \*detail = index == _model.list.count ? _model.list.firstObject : [_model.list objectAtIndex:index];          UIImageView \*imageView = [[UIImageView alloc] init];          imageView.frame = CGRectMake(kScreenWidth \* index, 0, kScreenWidth, 150);          [imageView yy_setImageWithURL:[NSURL URLWithString:detail.pic] options:YYWebImageOptionSetImageWithFadeAnimation];          [self.adverScrollView addSubview:imageView];      }  }</code></pre>    <p>在轮播图滚动动画结束后需要做一下判断,如果当前滚动到了最后一张图片,则立即将scrollView的偏移调整到初始位置,这样一个无限轮播就完成了。</p>    <pre>  <code class="language-objectivec">- (void)scrollViewDidScroll:(UIScrollView \*)scrollView {         NSInteger curPage = self.adverScrollView.contentOffset.x / kScreenWidth;          if(curPage == self.model.list.count) {              [self.adverScrollView setContentOffset:CGPointMake(0, 0) animated:NO];            }  }</code></pre>    <p>在有轮播图的地方肯定少不了定时器,如果将定时器直接放在cell中,就会因为cell的复用导致定时器出现问题,所有一般是将定时器放在控制器中。但是这样的话也带来一个问题,就是由于定时器的存在,如果要求定时器的生命周期和控制器相同(也就是在控制器dealloc的时候才取消定时器).这样的控制器是无法调用dealloc的,会造成控制器虽然已经退出但是定时器依然在正常工作。所以这里专门为控制器设计了一个定时器的单例帮助类,这样的话就可以在dealloc中去销毁所有的定时器。</p>    <pre>  <code class="language-objectivec">@interface XMLYFindRecommendHelper : NSObject      #pragma mark - Common      //生成帮助类单例  + (instancetype)helper;        //销毁所有的定时器      - (void)destoryAllTimer;    #pragma mark - Live        //  开启为直播设置的定时器        - (void)startLiveTimer;        //销毁直播的定时器      - (void)destoryLiveTimer;        #pragma mark - Header        //开启头部的定时器      - (void)startHeadTimer;        //销毁头部的定时器      - (void)destoryHeaderTimer;      @end</code></pre>    <p>在广播页面中,有一个根据当前时间显示不同的问候语的小功能。比如现在是早上6点钟,应该显示“早安*北京”。这里就需要用到NSDateFormatter,但是NSDateFormatter的比较消耗性能,所以我专门写了一个XMLYTimeHelper类来管理所有的时间转换操作。在这个类中对NSDateFormatter做了缓存处理,并使用dispatch_semaphore_t保证了线程安全。</p>    <pre>  <code class="language-objectivec">//根据字符串生成相应的NSDateFormatter,比如"yyyy-MM-dd HH:mm:ss"  static force_inline NSDateFormatter *XMLYDataCreateFormatter(NSString *string) {      NSDateFormatter *formatter = [[NSDateFormatter alloc] init];      formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];      formatter.dateFormat = string;      return formatter;  }    //用户直接调用此方法,传入"yyyy-MM-dd HH:mm:ss"这样的字符串生成NSDateFormatter  static force_inline NSDateFormatter *XMLYDateFormatter(NSString *string) {      //1.检查输入的合法性      if(!string || ![string isKindOfClass:[NSString class]] || string.length == 0) return nil;     //2.初始化单例参数      static CFMutableDictionaryRef cache;      static dispatch_semaphore_t lock;      static dispatch_once_t onceToken;      dispatch_once(&onceToken, ^{          cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);          lock = dispatch_semaphore_create(1);      });        //3.加锁      dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);      //4.查询当前字符串是否已经存在相应的NSDateformatter      NSDateFormatter *formatter = CFDictionaryGetValue(cache, (__bridge const void *)(string));     //5.解锁      dispatch_semaphore_signal(lock);       //6.如果缓存中没有,则需要重新生成      if(!formatter) {          formatter = XMLYDataCreateFormatter(string);          //7.重新生成成功,存入缓存          if(formatter) {              dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);              CFDictionarySetValue(cache, (__bridge const void *)(string), (__bridge const void *)(formatter));              dispatch_semaphore_signal(lock);          }      }      return formatter;  }</code></pre>    <p> </p>    <p> </p>