Skip to content
/ NewsAPP Public

一款新闻与娱乐的聚合iOS应用。An aggregating iOS app for news and entertainment.

License

Notifications You must be signed in to change notification settings

Peyet/NewsAPP

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 

Repository files navigation

NewsAPP

platform Lines of code GitHub last commit GitHub releases License Twitter Follow

NewsAPP

一款新闻与娱乐的聚合iOS应用。

开发平台 Xcode 13.1 iOS 15.0

应用演示

SplashNewsVideoRecommend

如果不能正常显示,请点击加载失败的图片跳转查看

TODO List

  • 推送通知

  • 开屏广告

  • 加载新闻、视频、图片列表

  • 查看新闻、图片详情

  • 已读新闻标记

  • 删除页面内容

  • 离线缓存

  • 分享功能

  • 优化离线缓存

  • 个人中心(使用后端云服务存储用户数据)

API

新闻 视频 图片

技术实现

  • 开屏广告

    ​ iOS系统在App启动前,也就是AppDelegate的- application: didFinishLaunchingWithOptions: 前,会加载一个从LaunchScreen中提前准备好的启动图片,在加载完成App UI后,这张图片就会消失。由于现在手机手机处理器速度很快,这张图片的展示事件非常短,所以需要在展示完系统启动图后衔接一个和系统启动图一样的界面。

    ​ 这里选择了在SceneDelegate中 - scene: willConnectToSession options: 即将把Scene绑定到UIWindow的时候,配置我们的自定义广告界面。这样就实现了无缝切换 。

  • 标题栏

    CMPageTitleView

    ​ 标题栏采用了GitHub上现有的轮子:CMPageTitleView, 这里自定义了选中状态下的标题的样式。

  • 网络请求

    ​ 网络请求使用了AFNetWorking框架,并进行了一定的封装。请求回正确的数据后开启新的线程处理数据。

    __weak typeof(self) weakSelf = self;
    [[AFHTTPSessionManager manager] GET:requestURL parameters:nil headers:nil progress:^(NSProgress * _Nonnull downloadProgress) {
            
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            __strong typeof(weakSelf) strongSelf = weakSelf;
            // 请求数据失败,返回缓存数据
            if ([responseObject[kZPJModelKeyNewsRespondResult] isKindOfClass:[NSNull class]]) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (finishBlock) {
                        finishBlock(YES, listData);
                    }
                });
                return;
            }
            // 请求成功,返回网络数据
            NSArray *dataArray = [(responseObject[kZPJModelKeyNewsRespondResult]) objectForKey:kZPJModelKeyNewsGetData];
            NSMutableArray *listItemArray = [NSMutableArray arrayWithCapacity:30];
            for (NSDictionary *info in dataArray) {
                ZPJListItem *listItem = [[ZPJListItem alloc] initWithConfig:info];
                [listItemArray addObject:listItem];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                if (finishBlock) {
                    finishBlock(YES, listItemArray);
                }
            });
            // 缓存数据
            [strongSelf _archiveListDataWithArray:listItemArray.copy];
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            // 请求数据失败
            dispatch_async(dispatch_get_main_queue(), ^{
                if (finishBlock) {
                    finishBlock(NO, listData);
                }
            });
        }];
  • 读取/缓存 新闻数据

    缓存新闻数据

- (void)_archiveListDataWithArray:(NSArray<ZPJListItem *> *)array {
    NSArray *pathArray =  NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *cachePath = [pathArray firstObject];
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    // 初始化文件
    NSString *dataPath = [cachePath stringByAppendingPathComponent:kZPJLocalCachePath];
    if (![fileManager fileExistsAtPath:dataPath]) {
        NSError *createError;
        [fileManager createDirectoryAtPath:dataPath withIntermediateDirectories:YES attributes:nil error:&createError];
    }
    
    // 缓存新闻数据
    NSString *listDataPath = [dataPath stringByAppendingPathComponent:kZPJLocalCacheNewsFileName];
    NSData *listData = [NSKeyedArchiver archivedDataWithRootObject:array requiringSecureCoding:YES error:NULL];
    [fileManager createFileAtPath:listDataPath contents:listData attributes:nil];
}    

​ 读取新闻数据

- (NSArray<ZPJListItem *> *)_readDataFromLocal{
    
    NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *cachePath = [pathArray firstObject];
    NSString *listDataPath = [cachePath stringByAppendingPathComponent:kZPJLocalCacheNewsPath];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    NSData *readListData = [fileManager contentsAtPath:listDataPath];

    id unarchiveObj = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSArray class],[ZPJListItem class], nil]  fromData:readListData error:nil];
    
    if ([unarchiveObj isKindOfClass:[NSArray class]] && [unarchiveObj count] > 0) {
        return (NSArray<ZPJListItem *> *)unarchiveObj;
    }
    return nil;;
}
  • 加载上下拉数据

    ​ 使用了MJRefresh框架来实现。

    - (void)loadNewmodels {
        __weak typeof(self)wself = self;
        [self.listLoader loadListDataWithChannelInfo:self.channelInfo FinishBlock:^(BOOL success, NSArray<ZPJListItem *> * _Nonnull dataArray) {
            __strong typeof(wself) strongSelf = wself;
            if (success) {
                strongSelf.dataArray = [dataArray mutableCopy];
                [strongSelf.tableView.mj_header endRefreshing];
                [strongSelf.tableView reloadData];
            } else {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [strongSelf.tableView.mj_header endRefreshing];
                    [strongSelf.tableView reloadData];
                });
            }
        }];
    }
  • 视频播放器使用AVFoundation类库实现。

    - (void)playVideoWithvideoUrl:(NSString *)videoUrl attachView:(UIView *)attachView {
        // 停止上次播放
        [self _stopPlay];
        
        AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:videoUrl]];
        _videoItem = [AVPlayerItem playerItemWithAsset:asset];
        
        // 监听视频资源状态、缓冲进度
        [_videoItem addObserver:self forKeyPath:kVideoObserverStatus options:NSKeyValueObservingOptionNew context:nil];
        [_videoItem addObserver:self forKeyPath:kVideoObserverTimeRanges options:NSKeyValueObservingOptionNew context:nil];
        
        CMTime duration = _videoItem.duration;
        CGFloat videoDuration = CMTimeGetSeconds(duration);
        
        // 创建播放器
        _avPlayer = [AVPlayer playerWithPlayerItem:_videoItem];
        [_avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            NSLog(@"播放进度:%@", @(CMTimeGetSeconds(time)));
        }];
        
        // 展示播放画面
        _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_avPlayer];
        _playerLayer.frame = attachView.bounds;
        [attachView.layer addSublayer:_playerLayer];
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handlePlayEnd) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    
    }
  • 推荐界面瀑布流

    ​ 用继承自UICollectionViewFlowLayout的ZPJRecommendLayout来实现瀑布流样式。

    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
        UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
        // 计算cell的frame
        CGFloat cellX = 0, cellY = 0, cellWidth = 0, cellHeight = 0;
        // cell的宽度
        cellWidth = (self.collectionView.frame.size.width - kDefaultUIEdgeInsets.left - kDefaultUIEdgeInsets.right - kDefaultColumnMargin) / kDefaultColumnCount;
        // cell的高度
        cellHeight = kDefaultCellHeight;
        CGFloat cellPictureHeight;
        CGFloat cellExcerptHeight;
        if ([self.delegate respondsToSelector:@selector(cellImageSizeWithIndexPath:InFlowLayout:)]) {
            MyRecommendListItem *model = [self.delegate cellImageSizeWithIndexPath:indexPath InFlowLayout:self];
            // 默认字体大小配置
            CGFloat excerptFontSize = 15;
            UIFont *excerptFont = [UIFont systemFontOfSize:excerptFontSize];
            cellPictureHeight = cellWidth * [[model.images firstObject][kZPJModelKeyRecommendImageHeight] floatValue] / [[model.images firstObject][kZPJModelKeyRecommendImageWidth] floatValue];
            cellExcerptHeight = [model.excerpt sizeOfTextWithMaxSize:CGSizeMake(cellWidth, 36) Font:excerptFont].height;
            cellHeight = cellPictureHeight + cellExcerptHeight + 5 ;
        }
        
        // cell应该拼接到第几列
        NSInteger destColumn = 0;
        // 高度最小的列数高度
        CGFloat minColumnHeight = [self.columnHeights[0] floatValue];
        // 获取高度最小的列数
        for (int i = 0; i < kDefaultColumnCount; i++) {
            CGFloat columnHeight = [self.columnHeights[i] floatValue];
            if (minColumnHeight > columnHeight) {
                minColumnHeight = columnHeight;
                destColumn = i;
            }
        }
        
        // 计算cell的x
        cellX = kDefaultUIEdgeInsets.left + destColumn * (cellWidth + kDefaultColumnMargin);
        // 计算cell的y
        cellY = minColumnHeight;
        if (cellY != kDefaultUIEdgeInsets.top) {
            cellY += kDefaultRowMargin;
        }
    
        // 保存列表的高度
        if ([self.delegate respondsToSelector:@selector(setCellImageSizeWithIndexPath:cellSize:InFlowLayout:)]) {
            NSValue *pictureSize = [NSValue valueWithCGSize:CGSizeMake(cellWidth, cellPictureHeight)];
            NSValue *excerptSize = [NSValue valueWithCGSize:CGSizeMake(cellWidth - 15, cellExcerptHeight)];
            NSDictionary *cellSize = @{kZPJModelKeyRecommendPictureSize:pictureSize, kZPJModelKeyRecommendExcerptSize:excerptSize};
            [self.delegate setCellImageSizeWithIndexPath:indexPath cellSize:cellSize InFlowLayout:self];
        }
        attribute.frame = CGRectMake(cellX, cellY, cellWidth, cellHeight);
        self.columnHeights[destColumn] = @(CGRectGetMaxY(attribute.frame));
        return attribute;
    }

About

一款新闻与娱乐的聚合iOS应用。An aggregating iOS app for news and entertainment.

Resources

License

Stars

Watchers

Forks

Packages

No packages published