用Swift实现一款天气预报APP(三)
这个系列的目录:
通过前面的学习,一个天气预报的APP已经基本可用了。至少可以查看现在当前的天气情况和未来几个小时的天气预报了。但是,还不够完善。如果用户想要知道他要去的地方的天气怎么办。明显我们的APP在目前来说无法满足用户的这个需求。而我们的APP需要获取其他城市的天气却非常的简单。通过查看天气的API,发现只要把城市的名称作为参数就可以获得当地城市的天气预报。API:
api.openweathermap.org/data/2.5/find?q=London&type=like&mode=xml
q=London就是在API中指明地点的参数。但是,从这里也可以单出。这个城市的名称显示是需要英文的,不是“北京”这样的汉字,也就是说在城市列表中显示的是汉字,但是传给API的url使用的时这个城市的英文名称,或者可以说是拼音字母。
在这一篇中,我们主要要实现的功能是切换城市,和刷新天气预报数据。首先在Storyboard中添加一个叫做城市列表的Controller。我们需要在Controller中显示一个城市列表,这里需要用到一个在iOS的开发中很常用的控件:UITableView。添加一个UIViewController之后,拖动一个UITableView到这个ViewController上。这样,在界面上来说就齐全了。
下面,创建代码,选择Source,先在Subclass of选择你的超类为UIViewController,然后命名你的类的名称。这里是"CityListViewController"。最后在语言一项选择Swift。你应该不会选择Objective-C的。如图:
之后一路的Next一直到Done。
很多的教程在讲到UITableView的时候总是喜欢用UITableViewController,这个是对用包含一个UITableView的UIViewController的封装。有的时候,系统封装了过多的东西对于开发者来说并不是什么好事。尤其是开发者单独处理UITableView也不会耗费太多的时间。所以,这里使用的是UIViewController和UITableView的组合。
在你的ViewController文件中添加准备绑定的UITableView对象的属性:
@IBOutlet weak var tableView: UITableView!
之后在Controller里选择之后,在右边栏里选择左数第三个选项,然后在下面的Class里选择你刚刚创建的CityListViewController。一般,在你选择完了
Controller之后,Class下面的Module会自动设定为Current-Swift_Weather。也就是会自动选择你的项目名称。如果没有选择的话,你需要手动添加你的项目名称到Module里。否则,这个ViewController是不可用的。Swift中引入了Module(模块)这个概念。默认的你的APP就是一个Module。类都是从你的应用的Module里查找的。如果没有这个Module名称的话,应用无法找到你给这个ViewController关联的代码。
这些操作完成之后,你已经把Storyboard的ViewController和对应的代码关联在了一起。下面还需要关联上前文提到的UITableView控件。点击你刚刚选中的controller然后在右边栏中选择最右边的箭头按钮你会看到里面会出现一个tableView,他后面的小圆圈还没有和Storyboard的TableView关联起来。鼠标放在小圆圈上,按下Ctrl同时移动鼠标到Storyboard的UITableView上。
到目前为止都很完美,但是这个TableView还不能用。TableView需要知道有多少个TableViewCell要显示出来,每一个Cell上面显示什么内容。每一个Cell有多高,有多少个Section。哪个Cell被选中,哪个Cell从被选中变成了么有被选中等等。。。这些都是通过TableView的代理实现的。这样的代理一共有两个,一个叫做UITableViewDelegate,一个叫做UITableViewDataSource。
所以,还需要把UITableView的两个代理和UITableView所在的Controller关联。选中Storyboard的UITableView,然后选择右边栏的最后一个选项。就是最后的那个选项。你会看到
把鼠标放在小圆圈上,同时按下Ctrl键,移动鼠标到这个UITableView所在的Controller上,准确的说是移动到这里:。两个都是这样的操作。完成之后就给这个UITableView关联好了UITableViewDelegate和UITableViewDataSource。
到目前为止,我们在Storyboard中创建了一个Controller(Scene),在上面放了一个UITableView。创建了一个UIViewController代码,并且和刚刚创建的Controller关联到了一起。并且把UITableView也关联到了一起,同时关联的还有这个UITableView的UITableViewDelegate和UITableViewDataSource。UITableViewDelegate和UITableViewDataSource在Swift的语法上来说都是protocol,也就是其他的语言,如Java、C#的接口,哪里继承了哪里就要给出实现。既然UITableView的这delegate和datasource都制定在了他所在的Controller上,那么我们代码里的CityListViewController就需要继承UITableViewDelegate和UITableViewDataSource并实现这两个protocol。
class CityListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource
下面是UITableViewDelegate和UITableViewDataSource中部分必要的方法的实现。注意,这些只是一个UITableView正常显示的必要方法,还有很多的方法暂时没有用到。
第一行,是指定这个UITableView中有多少个section的,section分区,一个section里会包含多个Cell。这里,是只有一个section。
第二行,是指定每一个section里面有多少个Cell的。因为我们只有一个section,所以,有多少个城市可选就有多少个Cell。这个是视不同情况定的。
第三行,初始化每一个Cell。一个Cell长什么样子就由这个方法决定。
第四行,是选中一个Cell后执行的方法。当用户选择了一个Cell的时候,我们需要知道是哪一个,并把这个Cell的城市的英文名称(或者是拼音的字母)发送到主界面中用于获取该城市的天气数据。
这些,就是使用一个UITableView时的全部了。首先创建一个放UITableView的Controller(Scene)然后拖动一个UITableView在上面。二,创建一个对应于这个Scene的Controller的Swift代码,并在代码中添加UITableView属性。关联Scene和Swift的Controller,关联代码的UITableView属性和Storyboard中的UITableView。三,关联UITableView的delegate和datasource到Swift代码的Controller,并在其中继承和实现UITableViewDelegate和UITableViewDataSource。这一部分需要多练习并且熟记。因为你会发现没有一个应用不用到UITableView的。如果你找不出UITableView那也可能是开发者对于UITableView的定制比较深,直观上看不出来而已。
这里必须强调的一点,就是第三行的创建Cell的方法。UITableView的Cell不是每次用到都去创建的。手机如今的内存已经有2G的了,CPU也是几核心的。但是,其资源还是相对比较紧缺。如果,每个Cell都新建一个。那么,用户在上下滑动UITableView的时候会非常的卡顿。这对于一个好的APP时绝对不允许的。所以苹果也推荐了一种使用Cell的方法,如果Cell还没有被创建的话就创建一个。如果Cell已经创建了,那么就对这个Cell重新赋值。这里一点很关键,如果一个Cell已经创建了,只重新赋值而不再创建!参见下面的代码:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier") as? UITableViewCell if cell == nil { cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "CellIdentifier") } cell?.textLabel.text = self.cityList.values.array[indexPath.row] return cell! }
首先按照Cell的Identifier从UITableView的Cell重用队列获取Cell。如果为空则创建一个Cell,并指定这个Cell的Identifier。如果Cell不为空的话给这个Cell的textLabel的text属性重新赋值。但是,既然我们用了Storyboard了,就用的彻底一点。Cell为什么不用Storyboard来创建呢。这样又会省去很多的代码,比如我们这里的重用Cell的部分。在右边栏中找到UITableViewCell,并拖动到UITableView上。给这个Cell的Identifier起个名字就叫"cityCell"。Style选择Custom,表示我们要自定义这个Cell。如图:
接下来,添加为这个Cell添加新的Swift代码文件。首先,source->Cocoa Touch Class。之后选择
给Cell绑定Swift代码类。选中Storyboard的这里
之后->
这个Cell需要一个UILabel来显示城市的中文名称(这个UILabel只是为了表明在Storyboard中自定义一个Cell的时候该如何处理,一般来说Cell中有一个textLabel可以显示文本)。那么先在代码中添加一个label的属性:
@IBOutlet weak var cityNameLabel: UILabel!
添加完属性之后,关联这cityNameLabel和Storyboard中的Cell中刚刚添加的Label。重复上面说到的第一步,选中这个cell。然后选择第二步中最上面选项中的最后一项。你就会看到cityNameLabel和他后面是小圆圈。你应该知道怎么办了。关联属性和Storyboard的Label之后,回到前面说道的创建Cell的方法。
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cityListCell = tableView.dequeueReusableCellWithIdentifier("cityCell", forIndexPath: indexPath) as CityListTableViewCell cityListCell.cityNameLabel.text = self.cityList.values.array[indexPath.row] return cityListCell }
看到有什么不同的么。是的,这里不用再从TableView的Cell的复用队列中获取Cell了。因为,这些都在Storyboard中处理好了。我们只要每次给Cell重新赋值就可以了。
到目前为止,这个城市列表还是不能用的。因为,我们还没有把这个列表和天气预报的主界面关联起来。是的,用户从哪里进入这个城市列表呢。现在我们就把主界面和城市列表关联起来。首先,在Storyboard上拖入一个UINavigationController。删掉后面的RootViewController,并把这个UINavigationController的RootViewController和我们刚刚创建的城市列表Controller关联起来。这一类似操作在第一部分中讲过。不清楚的话可以重新看看第一部分。
之后,找到主界面的City按钮,连接到新添加的UINavigationController上,在弹出的Action Segue中选择modal。这个时候运行APP,点击City就会出现刚刚创建的CityListController了,用户可以点这上面的某一行,但是。。。回不去了。现在就来处理这个问题。开发这个工作就是“逢山开路,遇水搭桥”。在MainViewController中添加如下代码
@IBAction func dismissCityListController(segue: UIStoryboardSegue){
println("dismiss controller")
}
名字可以任意起,但是参数必须是UIStoryboardSegue类型的。然后,在Storyboard的天气预报主界面中右击Exit,在出现的菜单中你会看到刚刚添加的方法dismissCityListController。在这个方法有个小圆圈。Storyboard里就是充满了这样的小圆圈。把这个小圆圈连接到CityListController的Cell上,在弹出的小菜单中选择selection。也就是说在用户选择了UITableView的一个Cell的时候(selection)CityListController就会“Exit”退出。
这个方法就是传说中的“unwind segue”给这个segue设定一个identifier为“backToMain”。连接好以后再运行APP。在用手指点了一个City以后CityListController就会隐藏起来。用户可以在选择了城市以后返回继续操作。
但是,选择了城市以后主界面显示的还是原来的城市,并没有更换。那时因为,我们并没有添加相关的代码。上面添加的只是让界面可以在segue的引导之下跳转,但是没有更换城市后重新请求天气预报数据。下面就完成这一部分功能。
在UIViewController之间传递数据,我们这里是从CityListController传递选择好的城市给MainViewController。方法有很多,比如,以后你会经常用到的Notification的方法。用户选择一个城市之后发出一个Notification,在MainviewController中捕获这个Notification并做相应的处理。看似很简单把。不过我们这里不用这个方法。用Notification的方法会给代码的维护造成一定的困扰。哪里发送,哪里接受都是分开写的,不容易维护代码。我们这里要将的时用代理的方式传递数据。这个中方法在自定义控件,和ViewController之间都会经常用到。具体到我们的天气APP这里,我们需要从CityListController传递数据给MainViewController,那么就在CityListController文件中定义一个接口(protocol)。苹果一般的命名规则是你的Controller的名字后面加Delegate。这样非常好辨认哪里是定义Delegate的,哪里是用到这个Delegate的。
@objc protocol CityListViewControllerDelegate{
func cityDidSelected(cityKey: String)
}
这个@objc不是一定要加的,一般是和其他的Objective-C代码共用的时候加。在CityListViewController中添加一个叫做delegate的属性。这个属性会在CityListViewController的UITableView里的某个Cell选中时被执行。
@IBOutlet weak var delegate: CityListViewControllerDelegate?
这里用weak时因为,我们不希望这个代理强引用(strong)其他的Controller。这样会造成循环引用,使得连个Controller的引用计数不减少,从而无法在不用的时候从内存清除。在选中的时候执行
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { println("did select row \(indexPath.row)") // set current selected index of city list self.selectedIndex = indexPath.row if self.selectedIndex != nil && self.delegate != nil { var cityKey = self.cityList.keys.array[self.selectedIndex!] self.delegate?.cityDidSelected(cityKey) } tableView.deselectRowAtIndexPath(indexPath, animated: true) }
在CityListViewController中执行代理的方法的话,就需要在MainViewController中实现这个protocol。
class MainViewController: UIViewController, CLLocationManagerDelegate, CityListViewControllerDelegate{
//MARK: city changed delegate func cityDidSelected(cityKey: String){ println("selected city \(cityKey)") }
}
上面的代码是一个大概是实现。具体的代码可以在示例工程中查看。
运行APP,并选择一个其他的城市的时候,这段代码就会执行。在Console中会出现选择的城市的英文名称。现在我们需要来真的了。查看之前的代码,有一个方法func updateWeatherInfo(latitude: CLLocationDegrees, longitude: CLLocationDegrees)会在获取用户的地理位置后请求服务器获得天气预报。我们现在是需要根据城市的名称获取天气预报了。那么,我们就来Over Load方法updateWeatherInfo。Over Load就是定义一个和某个别的方法同名但是参数不同的方法。
func updateWeatherInfo(cityName: String)
以后的事情就是在前文中关于HTTP请求的url字符串问题了。这里不多做叙述。
还有一个功能没有完成。你很快会想到:刷新(refresh)。用户在选择了其他的城市的时候,需要很快回到用户点击刷新时所在位置的天气预报。点击Refresh的时候获取用户的最新地理位置数据,并请求天气预报数据。这个功能已经实现。在用户进入主界面的时候就会自动获取用户的位置,并请求天气预报。刷新功能只需要在在refreshAction方法中重新获取用户的地理位置就可以,请求天气预报会在获得用户位置后自动执行。这里需要提到的一点是,当成功获取了天气预报之后,就应该停止获取用户地理位置。因为这样会给用户省电!省电也是用户体验的一部分。作为一个开发者,如果你的APP太多费电,而且还不是一个很好玩的游戏的话实在是说不过去的。
本系列文章的代码在这里。
延伸阅读:AFNetworking是cocoaPods加载进来的。了解更多cocoaPods,请看这里。
本文所使用的代码的原始版本是来自Github的这里。我们现在的代码已经比原作者的丰富的多了。不过还是要感谢原作者。