善假于物,利用工具2天开发一款完整新闻类iOS app

来源:互联网 发布:软件测试原理与实践 编辑:程序博客网 时间:2024/05/17 03:59

题外话:
此篇文章以一个iOS新手的角度解释一款新闻类iOS APP诞生的过程,详细介绍在这过程中碰到的问题和我的解决思路。欢迎大家指正。

菜单界面:

主界面:

详细页面:

关于我们页面:

初期的想法

具体项目背景跳过,我们着重看如何实现一款新闻类APP。

在开始项目计划前,我下载了大量的新闻类APP进行研究,不论是android还是iOS,充斥着大量的此类APP,比如网易新闻,一点咨询,新浪微博(暂归位新闻类),知乎,知乎日报等,然后我从零开始点开每个应用,思考它的流程以及布局和要点。发现核心不外乎以下两点:

  • 一个可以下拉刷新的tableview(即一栏一栏的视图)
  • 点击每一栏可以进入一个详细的页面

可是我们的应用不可能就这个样子,需要有不同的新闻模块,所以又有以下几点:

  • 两种菜单布局:底部或顶部导航栏和侧边栏(抽屉导航)
  • 除了新闻模块还要添加丰富类模块,比如我这的地图显示和校车查询(随意拓展)。

当然,有了APP后我们的客户端从哪请求数据呢?于是有了以下问题:

  • 如何一个人快速开发移动端的后台,而且安全可靠

OK,暂先我们只想到这,至于优化本地存储和用户友好展示等,我们后面会涉及到。

开始实现

(一)绘画草图

一开始我们只是个想法,即便想了很多,依然还是觉的不踏实,没有清晰的项目概念,所以我把项目的草图画了下来,如下:

这下心里踏实多了,起码知道自己想让它长个什么样子了。

(二)开始布局

ok,草图已经有了,那想必大家已经知道我选用了流行的侧边栏(抽屉导航)布局。大家都知道盖一栋大楼最重要的是先把整栋大楼的骨架先搭起来,同样一个APP首先要做的是把自己的架构搭起来,而这就是我们的布局。

iOS有着众多开源的项目,这是我以前就知道的(PS:这些都是经验才能得到的,你可以通过看书,看博客,浏览官网,与大牛交流等各种渠道获取这些信息),而对于类似我们这种侧边栏的布局,开源界有着众多的解决方案,我选用了SWRevealViewController **这套开源库,地址是:
https://github.com/John-Lluch/SWRevealViewController (fork: **440 star: 1660)

通过内部的示例,相信大家很快就能搭建好项目的基本骨架。这里有一个另外的小源码,希望对你掌握这块开源框架提供帮助。附链接:http://mexiqq.qiniudn.com/VReaderSidebarDemo.zip

案例效果:

(三)构建核心页面

核心页面是一个主页面和一个详细页面:

  • 主页面当然是新闻资讯的界面,因为我们的目标是一款新闻资讯类的框架,所以页面就是一个新闻的列表。自然而然我们用了一个UItableView作为主页面的控件。
  • 详细页面是展示详细新闻内容的页面,暂时我们不考虑复杂的涉及,只需要在里面放置一textview
  • 通过segue将主页面和详细页面联系起来,实现跳转和消息传递(基础)

OK,核心界面有了(PS:我这里使用的是Xcode的storboard构建页面,不使用旧式的xib)

效果如下:

(四)构建服务器

终于到了这个移动开发人员头疼的问题了,由于项目需要,我们必须有可以提供数据的接口,对于前端移动开发人员来说如果可以自己构建后台无疑减少了大量的沟通问题,然而人的精力有限,无法同时兼顾,咋么办?

OK,现在这个问题得到了不错的解决,越来越多的云服务开始出现,小编今天给大家推荐一款移动端后台开发平台Bomb(当然同类型的不少,不如facebook的Parse,不过国内大家都懂的),它提供了一套后台开发所使用的SDK,方便的为你的前端产品搭建后台,适合中小企业或者独立开发者使用。

附官方链接:http://www.bmob.cn/
附我的推荐链接:
http://www.mexiqq.com/2014/09/09/Bomb%E4%BA%91%E6%9C%8D%E5%8A%A1%E4%BB%8B%E7%BB%8D%E6%8A%95%E7%A5%A8/

给出我的平台数据库图片:

如图所示我的云端数据库有7个表,User是用户的注册表,由Bomb自动帮我们建立,因为我们是简单的新闻资讯类APP,所以未使用。接下来有四个module表,分别对应我新闻内容的四个模块(我添加了title,publishTime,content三个字段)。GameScore暂时未用。Question用于用户提交问题反馈的表(我添加了Question 和 Reply两个字段)。

OK,对于一个简单的新闻咨询类APP这个后台我想已经足够了。

(五)连接后台与前端

虽然这里用不着,但我说些题外话,在iOS开发中,对于web service的访问是重中之重,主要涉及以下三个方面:

  • Json数据格式的解析:iOS SDK自带了最快的解析方式
  • 访问web Service:推介使用第三方的开源库(ASIHTTPRequest,AFNetworking,MKNetworkKit)
  • 定位服务与地图应用

上面的看起来对于初学者有些恐怖,不过好消息是Bomb提供了封装好的SDK,使用自带的BombQuery去查询数据库。现面给出我的一个例子:
(PS:在使用之前记得将应用秘钥加入项目,方法见Bomb官网)

<code style="font-family: Consolas, Menlo, Monaco, 'Courier New', monospace; font-size: 1em; padding: 0px; color: inherit; background-color: transparent;"> <span class="hljs-comment" style="color: rgb(136, 136, 136);">//请求数据</span>    BmobQuery   *bquery = [BmobQuery queryWithClassName:module];    [bquery selectKeys:@[<span class="hljs-string" style="color: rgb(136, 0, 0);">@"title"</span>,<span class="hljs-string" style="color: rgb(136, 0, 0);">@"publishTime"</span>]];    [bquery setLimit:<span class="hljs-number" style="color: rgb(0, 136, 0);">15</span>];    [bquery orderByDescending:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"createdAt"</span>];    [bquery findObjectsInBackgroundWithBlock:^(<span class="hljs-built_in" style="font-weight: bold;">NSArray</span> *array, <span class="hljs-built_in" style="font-weight: bold;">NSError</span> *error) {        <span class="hljs-keyword" style="font-weight: bold;">if</span>(!error){            local = <span class="hljs-literal" style="color: rgb(0, 136, 0);">false</span>;(步骤<span class="hljs-number" style="color: rgb(0, 136, 0);">1</span>)            myarray = array;(步骤<span class="hljs-number" style="color: rgb(0, 136, 0);">2</span>)            [<span class="hljs-built_in" style="font-weight: bold;">NSThread</span> detachNewThreadSelector:<span class="hljs-keyword" style="font-weight: bold;">@selector</span>(saveContent:) toTarget:<span class="hljs-keyword" style="font-weight: bold;">self</span> withObject:array];(步骤<span class="hljs-number" style="color: rgb(0, 136, 0);">3</span>)            [<span class="hljs-keyword" style="font-weight: bold;">self</span> performSelector:<span class="hljs-keyword" style="font-weight: bold;">@selector</span>(reloadData) withObject:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span> afterDelay:<span class="hljs-number" style="color: rgb(0, 136, 0);">0</span>];(步骤<span class="hljs-number" style="color: rgb(0, 136, 0);">4</span>)        }<span class="hljs-keyword" style="font-weight: bold;">else</span>{            [<span class="hljs-keyword" style="font-weight: bold;">self</span><span class="hljs-variable">.refreshControl</span> endRefreshing];            UIAlertView *alertView = [[UIAlertView alloc]                                      initWithTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"友情提示"</span>                                      message:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"无法获取数据,请检查网络(☆_☆)"</span>                                      delegate:<span class="hljs-keyword" style="font-weight: bold;">self</span> cancelButtonTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"关闭"</span>                                      otherButtonTitles:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>, <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>];            [alertView show];        }    }];</code>

上述例子查询了数据库的15条例子,采用异步访问的方式返回数据。发生错误时给出提示,如果成功,会有四步操作:

  • 步骤一我指定一个bool值,表示数据非本地存储
  • 步骤二我将新加载的数据赋值给tableview绑定的数据
  • 步骤三开启一个线程,将新加载的数据本地化
  • 步骤四跳转到执行更新列表的方法

后面会详细涉及到数据持久话操作,在这里我们不必细究。
OK,我们使用Bomb自己提供的查询方法访问Bomb服务,成功的将应用与服务器连接起来。

这是我访问数据后展示新闻列表的页面:

(六)基本的业务逻辑

OK,其实我们几乎不涉及业务逻辑,因为我们只是把最新的新闻数据展示给用户看,不过仍然有一些逻辑需要我们注意:

  • 用户如何获取最新的消息: 下拉刷新
  • 用户想保存一条新闻消息到本地: 我的收藏
  • 用户发现问题咋么办: 消息反馈

解决方案:

  • iOS sdk自带了下拉刷新的解决方案refreshControl

示例代码:

<code style="font-family: Consolas, Menlo, Monaco, 'Courier New', monospace; font-size: 1em; padding: 0px; color: inherit; background-color: transparent;"><span class="hljs-keyword" style="font-weight: bold;">self</span><span class="hljs-variable">.refreshControl</span> = [[UIRefreshControl alloc] init];<span class="hljs-keyword" style="font-weight: bold;">self</span><span class="hljs-variable">.refreshControl</span><span class="hljs-variable">.backgroundColor</span> = [<span class="hljs-built_in" style="font-weight: bold;">UIColor</span> lightGrayColor];<span class="hljs-keyword" style="font-weight: bold;">self</span><span class="hljs-variable">.refreshControl</span><span class="hljs-variable">.tintColor</span> = [<span class="hljs-built_in" style="font-weight: bold;">UIColor</span> whiteColor];[<span class="hljs-keyword" style="font-weight: bold;">self</span><span class="hljs-variable">.refreshControl</span> addTarget:<span class="hljs-keyword" style="font-weight: bold;">self</span>                        action:<span class="hljs-keyword" style="font-weight: bold;">@selector</span>(getLatestLoans)              forControlEvents:UIControlEventValueChanged];</code>

效果图案:


(PS:以上代码和示例只是用于展示,不推荐直接使用,建议先找几篇文章系统学习以下)

  • 使用CoreData保存用户想要保存的信息

在这里我推荐两篇教程,很简明的讲述了Core Data的基本使用,附链接:

http://www.appcoda.com/introduction-to-core-data/
http://www.appcoda.com/core-data-tutorial-update-delete/

再给出我处理保存动作时执行的代码:

<code style="font-family: Consolas, Menlo, Monaco, 'Courier New', monospace; font-size: 1em; padding: 0px; color: inherit; background-color: transparent;">- (<span class="hljs-keyword" style="font-weight: bold;">IBAction</span>)saveNews:(<span class="hljs-keyword" style="font-weight: bold;">id</span>)sender {NSManagedObjectContext *managedObjectContext = [<span class="hljs-keyword" style="font-weight: bold;">self</span> managedObjectContext];NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"CollectionNews"</span>];NSPredicate *predicate = [NSPredicate predicateWithFormat: <span class="hljs-string" style="color: rgb(136, 0, 0);">@"newsTitle == %@"</span>,titleName];[fetchRequest setPredicate:predicate];<span class="hljs-built_in" style="font-weight: bold;">NSArray</span> *news = [[managedObjectContext executeFetchRequest:fetchRequest error:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>] mutableCopy];    <span class="hljs-keyword" style="font-weight: bold;">if</span> (news<span class="hljs-variable">.count</span> == <span class="hljs-number" style="color: rgb(0, 136, 0);">0</span>) {        NSManagedObject *newRecentlyNews = [NSEntityDescription insertNewObjectForEntityForName:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"CollectionNews"</span> inManagedObjectContext:managedObjectContext];        [newRecentlyNews setValue:titleName forKey:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"newsTitle"</span>];        [newRecentlyNews setValue:publishTime forKey:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"newsPublishTime"</span>];        [newRecentlyNews setValue:newsContent<span class="hljs-variable">.text</span> forKey:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"newsContent"</span>];        <span class="hljs-comment" style="color: rgb(136, 136, 136);">//[newRecentlyNews setValue:[obj objectForKey:@"playerName"] forKey:@"newsContent"];</span>        <span class="hljs-built_in" style="font-weight: bold;">NSError</span> *error = <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>;        <span class="hljs-keyword" style="font-weight: bold;">if</span> (![managedObjectContext save:&error]) {            UIAlertView *alertView = [[UIAlertView alloc]                                      initWithTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"友情提示"</span>                                      message:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"发生意外(☆_☆)"</span>                                      delegate:<span class="hljs-keyword" style="font-weight: bold;">self</span> cancelButtonTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"关闭"</span>                                      otherButtonTitles:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>, <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>];            [alertView show];        }<span class="hljs-keyword" style="font-weight: bold;">else</span>{            UIAlertView *alertView = [[UIAlertView alloc]                                      initWithTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"友情提示"</span>                                      message:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"保存成功Y(^_^)Y"</span>                                      delegate:<span class="hljs-keyword" style="font-weight: bold;">self</span> cancelButtonTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"关闭"</span>                                      otherButtonTitles:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>, <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>];            [alertView show];        }    }<span class="hljs-keyword" style="font-weight: bold;">else</span>{        UIAlertView *alertView = [[UIAlertView alloc]                                  initWithTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"友情提示"</span>                                  message:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"重复保存哦,亲(☆_☆)"</span>                                  delegate:<span class="hljs-keyword" style="font-weight: bold;">self</span> cancelButtonTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"关闭"</span>                                  otherButtonTitles:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>, <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>];        [alertView show];    }}</code>

首先查询表中有无重复的信息,如果有提示重复保存,如果没有则新建一个NSManageObject保存信息。

效果图:



- 使用Bomb提供的服务提交问题到Question表

Bomb不仅给我提供了查询的API,同样提供了增删改查一整套API,所以我们能使用Bomb提供的服务将用户的问题进行提交,给出我的代码和运行效果:

<code style="font-family: Consolas, Menlo, Monaco, 'Courier New', monospace; font-size: 1em; padding: 0px; color: inherit; background-color: transparent;">- (<span class="hljs-keyword" style="font-weight: bold;">IBAction</span>)submitQuestion:(<span class="hljs-keyword" style="font-weight: bold;">id</span>)sender {    <span class="hljs-keyword" style="font-weight: bold;">if</span>(![_myQuestion<span class="hljs-variable">.text</span> isEqualToString:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"在此输入:"</span>]){        BmobObject *gameScore = [BmobObject objectWithClassName:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"Questions"</span>];        [gameScore setObject:_myQuestion<span class="hljs-variable">.text</span> forKey:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"question"</span>];        [gameScore saveInBackgroundWithResultBlock:^(<span class="hljs-built_in" style="font-weight: bold;">BOOL</span> isSuccessful, <span class="hljs-built_in" style="font-weight: bold;">NSError</span> *error) {            <span class="hljs-comment" style="color: rgb(136, 136, 136);">//进行操作</span>            <span class="hljs-keyword" style="font-weight: bold;">if</span>(error ==<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>){                UIAlertView *alertView = [[UIAlertView alloc]                                          initWithTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"友情提示"</span>                                          message:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"提交成功Y(^_^)Y"</span>                                          delegate:<span class="hljs-keyword" style="font-weight: bold;">self</span> cancelButtonTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"关闭"</span>                                          otherButtonTitles:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>, <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>];                [alertView show];            }<span class="hljs-keyword" style="font-weight: bold;">else</span>{                UIAlertView *alertView = [[UIAlertView alloc]                                          initWithTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"友情提示"</span>                                          message:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"提交失败,请检查网络(☆_☆)"</span>                                          delegate:<span class="hljs-keyword" style="font-weight: bold;">self</span> cancelButtonTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"关闭"</span>                                          otherButtonTitles:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>, <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>];                [alertView show];            }        }];    }<span class="hljs-keyword" style="font-weight: bold;">else</span>{        UIAlertView *alertView = [[UIAlertView alloc]                                  initWithTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"友情提示"</span>                                  message:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"请输入有效文字(☆_☆)"</span>                                  delegate:<span class="hljs-keyword" style="font-weight: bold;">self</span> cancelButtonTitle:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"关闭"</span>                                  otherButtonTitles:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>, <span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>];        [alertView show];    }}</code>


(七)优化用户体验

我们本着主要功能去优化用户的体验,我想到以下两点,其实也是最基本的两点:

  • 当用户进入详细页面后加载数据期间,如何显示才能不让用户觉得突兀?
  • 用户访问过一次数据之后,再进入到此详细页面时难道要再访问一次后台吗?

好伐,以上两点应该是我们应用中用户体验感最强的两点,下面给出我的解决思路:

  • 对于需要用户等待的时间段内我会展示一个旋转的进度条,告诉用户我在加载数据,这个进度条可以使用iOS 提供的UIActivityIndicatorView类,只需要通过以下三行代码即可改善用户体验:

    <code style="font-family: Consolas, Menlo, Monaco, 'Courier New', monospace; font-size: 1em; padding: 0px; color: inherit; background-color: transparent;"><span class="hljs-title" style="color: rgb(136, 0, 0); font-weight: bold;">[myprogress setHidesWhenStopped:YES]</span><span class="hljs-comment" style="color: rgb(136, 136, 136);">;//设置进度条停止时隐藏</span><span class="hljs-title" style="color: rgb(136, 0, 0); font-weight: bold;">[myprogress startAnimating]</span><span class="hljs-comment" style="color: rgb(136, 136, 136);">;//设置进度条开始旋转</span><span class="hljs-title" style="color: rgb(136, 0, 0); font-weight: bold;">[myprogress stopAnimating]</span><span class="hljs-comment" style="color: rgb(136, 136, 136);">;//设置进度条停止旋转</span></code>

只需要在加载前设置进度条开始旋转,异步加载完数据后设置进度条停止旋转即可。

  • 对于第二点,很自然就想到了数据持久化。当用户访问完某条新闻内容时,将数据内容本地化,下一次访问时直接从本地获取,无需延迟。同样,我使用core data进行数据存储。

代码如下:

<code style="font-family: Consolas, Menlo, Monaco, 'Courier New', monospace; font-size: 1em; padding: 0px; color: inherit; background-color: transparent;">-(<span class="hljs-built_in" style="font-weight: bold;">BOOL</span>)isLocalData{    <span class="hljs-comment" style="color: rgb(136, 136, 136);">//NSLog(@"titlename%@",titleName);</span>    NSManagedObjectContext *managedObjectContext = [<span class="hljs-keyword" style="font-weight: bold;">self</span> managedObjectContext];    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:myEntimty];    NSPredicate *predicate = [NSPredicate predicateWithFormat: <span class="hljs-string" style="color: rgb(136, 0, 0);">@"newsTitle == %@"</span>,titleName];    [fetchRequest setPredicate:predicate];    <span class="hljs-built_in" style="font-weight: bold;">NSArray</span> *news = [[managedObjectContext executeFetchRequest:fetchRequest error:<span class="hljs-literal" style="color: rgb(0, 136, 0);">nil</span>] mutableCopy];    <span class="hljs-keyword" style="font-weight: bold;">if</span>(news<span class="hljs-variable">.count</span> > <span class="hljs-number" style="color: rgb(0, 136, 0);">0</span>)    {        NSManagedObject *entiy = [news objectAtIndex:<span class="hljs-number" style="color: rgb(0, 136, 0);">0</span>];        <span class="hljs-keyword" style="font-weight: bold;">if</span>(![entiy valueForKey:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"newsContent"</span>]){            <span class="hljs-keyword" style="font-weight: bold;">return</span> <span class="hljs-literal" style="color: rgb(0, 136, 0);">false</span>;        }<span class="hljs-keyword" style="font-weight: bold;">else</span>{            [newsContent setText:[entiy valueForKey:<span class="hljs-string" style="color: rgb(136, 0, 0);">@"newsContent"</span>]];            [<span class="hljs-keyword" style="font-weight: bold;">self</span> changeFrameSizeOfTextView];            <span class="hljs-keyword" style="font-weight: bold;">return</span> <span class="hljs-literal" style="color: rgb(0, 136, 0);">true</span>;        }    }    <span class="hljs-keyword" style="font-weight: bold;">return</span> <span class="hljs-literal" style="color: rgb(0, 136, 0);">false</span>;}</code>

上述代码在用户进入详细页面后执行,检测本地是否已经拥有此条新闻内容,如果已经存在,则从本地获取,若不存在,则远程获取数据。(PS:由于我本地最多存取15条新闻,每次刷新时会清空原先的数据,所以直接通过新闻标题查取,不推荐)

(八)丰富功能模块

OK,经过以上七步,基本已经搭建起一个简单的新闻类APP了,但是我们的应用会不会太单调了呢?我们可以给自己的应用增加一些额外的模块,具体选择什么要视自己的情况。这里因为是给学校做了一个就业信息发布的APP,所以添加了校车查询,地图显示的富类模块。

(九)开源

https://github.com/mexiQQ/VReader-iOS.git

欢迎各位大牛们指点一二 Y(^_^)Y

0 0
原创粉丝点击