用Swift實現一款天氣預報APP(三)


這個系列的目錄:

用Swift實現一款天氣預報APP(一)

用Swift實現一款天氣預報APP(二)

用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關聯好了UITableViewDelegateUITableViewDataSource

到目前為止,我們在Storyboard中創建了一個Controller(Scene),在上面放了一個UITableView。創建了一個UIViewController代碼,並且和剛剛創建的Controller關聯到了一起。並且把UITableView也關聯到了一起,同時關聯的還有這個UITableView的UITableViewDelegateUITableViewDataSourceUITableViewDelegateUITableViewDataSource在Swift的語法上來說都是protocol,也就是其他的語言,如Java、C#的接口,哪里繼承了哪里就要給出實現。既然UITableView的這delegate和datasource都制定在了他所在的Controller上,那么我們代碼里的CityListViewController就需要繼承UITableViewDelegateUITableViewDataSource並實現這兩個protocol。

class CityListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource

下面是UITableViewDelegateUITableViewDataSource中部分必要的方法的實現。注意,這些只是一個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,並在其中繼承和實現UITableViewDelegateUITableViewDataSource。這一部分需要多練習並且熟記。因為你會發現沒有一個應用不用到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,並把這個UINavigationControllerRootViewController和我們剛剛創建的城市列表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方法updateWeatherInfoOver Load就是定義一個和某個別的方法同名但是參數不同的方法。

func updateWeatherInfo(cityName: String)

以后的事情就是在前文中關於HTTP請求的url字符串問題了。這里不多做敘述。

還有一個功能沒有完成。你很快會想到:刷新(refresh)。用戶在選擇了其他的城市的時候,需要很快回到用戶點擊刷新時所在位置的天氣預報。點擊Refresh的時候獲取用戶的最新地理位置數據,並請求天氣預報數據。這個功能已經實現。在用戶進入主界面的時候就會自動獲取用戶的位置,並請求天氣預報。刷新功能只需要在在refreshAction方法中重新獲取用戶的地理位置就可以,請求天氣預報會在獲得用戶位置后自動執行。這里需要提到的一點是,當成功獲取了天氣預報之后,就應該停止獲取用戶地理位置。因為這樣會給用戶省電!省電也是用戶體驗的一部分。作為一個開發者,如果你的APP太多費電,而且還不是一個很好玩的游戲的話實在是說不過去的。

本系列文章的代碼在這里

延伸閱讀:AFNetworking是cocoaPods加載進來的。了解更多cocoaPods,請看這里

本文所使用的代碼的原始版本是來自Github的這里。我們現在的代碼已經比原作者的豐富的多了。不過還是要感謝原作者。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM