要格式化日期洽洁,你將使用DateFormatter對象团秽。 你在上一個教程中看過這個類疯趟。 它將Date對象封裝的日期和時間轉(zhuǎn)換為人類可讀的字符串澎剥,同時考慮到用戶的語言和區(qū)域設(shè)置锡溯。
在上一個教程中,你每次要將Date轉(zhuǎn)換為字符串時哑姚,都會創(chuàng)建一個DateFormatter的新實(shí)例祭饭。 不幸的是,創(chuàng)建DateFormatter對象是一個比較費(fèi)時的事叙量。 換句話說倡蝙,初始化這個對象需要很長時間。 如果你這么做绞佩,你的app會變慢(并且更多的消耗手機(jī)電池)悠咱。
更好的辦法是只創(chuàng)建一次DateFormatter對象蒸辆,然后反復(fù)調(diào)用它。 就是直到應(yīng)用程序?qū)嶋H需要之前析既,我們不會創(chuàng)建DateFormatter對象躬贡。 這個原理被稱為延遲加載(lazy loading),它是開發(fā)iOS應(yīng)用程序的一個非常重要的模式眼坏。 可以極大程度的避免系統(tǒng)開銷拂玻。
此外,我們只會創(chuàng)建一個DateFormatter的實(shí)例宰译。 下次需要使用DateFormatter時檐蚜,我們不會創(chuàng)建一個新的實(shí)例,而是重新使用現(xiàn)有的實(shí)例沿侈。
你將使用一個私有的全局常量闯第。 這是一個常駐于LocationDetailsViewController類(全局global)之外的常量,但它僅在LocationDetailsViewController.swift文件(私有private)中可見缀拭。
打開LocationDetailsViewController.swift咳短,在import和class語句之間添加以下代碼:
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter
}()
這段代碼是什么意思?你創(chuàng)建了一個名為dateFormatter的常量蛛淋,它的類型是DateFormatter咙好。這個常量是私有(private)的,在LocationDetailsViewController.swift文件之外你無法使用它褐荷。
你同時給dateFormatter了一個初始值勾效,但是等于號的后面并不是一個值,而是由一對花括號括起來的代碼叛甫,說明這是一個閉包(closure)层宫。
通常,你創(chuàng)建一個新的對象是像下面這個樣子:
private let dateFormatter = DateFormatter()
但是要初始化日期格式其监,僅僅要創(chuàng)建一個DateFormatter實(shí)例是不夠的卒密,你還要設(shè)置這個實(shí)例的dateStyle和timeStyle屬性。
創(chuàng)建一個對象并且同時設(shè)置它的屬性棠赛,你可以通過閉包的方式實(shí)現(xiàn):
private let dateFormatter: DateFormatter = {
//這里寫上設(shè)置屬性的代碼
return formatter
}()
閉包內(nèi)部是創(chuàng)建和初始化新的DateFormatter對象的代碼哮奇,然后將它們放入dateFormatter并且返回。
注意末尾的一對圓括號睛约,這是必須的鼎俘。
??: 如果你忘記了末尾的這對圓括號(),Swift會認(rèn)為你是想要把閉包本身分配給dateFormatter辩涝,換而言之贸伐,dateFormatter的值將是一段代碼,而不是實(shí)際的DateFormatter對象怔揩。
這對圓括號的作用就是執(zhí)行閉包中的代碼捉邢,并且將返回DateFormatter對象給到dateFormatter常量脯丝。
使用閉包來同時創(chuàng)建并且設(shè)置對象是非常常見的技巧,你在Swift編程中會經(jīng)常遇到這種情況伏伐。
在Swift中宠进,全局變量始終以惰性的方式創(chuàng)建,這就是說創(chuàng)建和設(shè)置DateFormatter對象的代碼將不會立即執(zhí)行藐翎,而是在應(yīng)用程序中第一次使用dateFormatter全局常量時材蹬,才會執(zhí)行這段代碼。
而我們使用dateFormatter的地方吝镣,就是在format(date)方法中堤器。
我們來創(chuàng)建format(date)方法,注意末贾,它應(yīng)該在class的內(nèi)部闸溃,不要寫到外面去了:
func format(date: Date) -> String {
return dateFormatter.string(from:date)
}
是不是看上去很簡單?它僅僅是向DateFormatter請求結(jié)果拱撵,并且把結(jié)果放到一個字符串里辉川。
練習(xí):你怎么確認(rèn)date formatter確實(shí)就是只被創(chuàng)建了一次呢?
答案:添加一個print()方法裕膀,就在閉包中的return formatter這一行前面。這個打印內(nèi)容在調(diào)試區(qū)域中勇哗,應(yīng)該只出現(xiàn)一次昼扛。
運(yùn)行app。在模擬器的調(diào)試菜單中選擇Apple Location欲诺。等到地址信息可見的時候抄谐,點(diǎn)擊Tag Location按鈕。
你會看到坐標(biāo)扰法,地址和日期標(biāo)簽都會顯示出相應(yīng)的值了:
等等蛹含,Address標(biāo)簽好像不太對勁...
我們之前將這個標(biāo)簽設(shè)置為多行顯示的模式了,記得嗎塞颁,但是table view對此還一無所知浦箱,所以它就不給你好好顯示。
打開LocationDetailsViewController.swift祠锣,添加下面的方法進(jìn)去酷窥,注意,下面的注釋是必須的伴网,否則不會生效蓬推。
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 && indexPath.row == 0 {
return 88
} else if indexPath.section == 2 && indexPath.row == 2 {
addressLabel.frame.size = CGSize(width: view.bounds.size.width - 115, height: 10000)
addressLabel.sizeToFit()
addressLabel.frame.origin.x = view.bounds.size.width - addressLabel.frame.size.width - 15
return addressLabel.frame.size.height + 20
} else {
return 44
}
}
當(dāng)table view讀取cell的時候會調(diào)用這個委托方法。你可以利用它來通知table view每個cell的高度是多少澡腾。
通常沸伏,所有的cell高度都是相同的糕珊,如果你需要改變cell的高度的話,你只需要簡單的設(shè)置cell的高度屬性就可以了(通過storyboard中的Row Height屬性或者tableView.rowHeight屬性)毅糟。
對于我們這個tableView红选,它的cell具備三種不同的高度:
1、最上面的Description cell留特。你已經(jīng)在storyboard中設(shè)置了它的高度為88纠脾。
2、Address cell蜕青。這個cell的高度是動態(tài)的苟蹈。它取決于得到的address字符串多大。
3右核、其他cell慧脱。都是標(biāo)準(zhǔn)的44點(diǎn)高度。
tableView(heightForRowAt)方法中的if語句對應(yīng)于上述三種情況贺喝。我們來詳細(xì)看一下Address Label的情況:
//1
addressLabel.frame.size = CGSize(width: view.bounds.size.width - 115, height: 10000)
//2
addressLabel.sizeToFit()
//3
addressLabel.frame.origin.x = view.bounds.size.width - addressLabel.frame.size.width - 15
//4
return addressLabel.frame.size.height + 20
這里用了一點(diǎn)小技巧來調(diào)整UILabel的大小菱鸥,使得其中的文本適合cell 的寬度(使用word-wrapping),然后你使用了新計算出的高度躏鱼,來決定這個cell的高度氮采。
frame屬性的類型是CGRect,用于描述視圖的位置和大小染苛。
CGRect是一個結(jié)構(gòu)(struct)鹊漠,定義了一個矩形。這個矩形的起點(diǎn)坐標(biāo)(X茶行,Y)為CGPoint值躯概,高度和寬度為CGSize值。
所有的UIView對象畔师,以及它們的子類比如UILabel娶靡,都有frame屬性。改變這個屬性看锉,就可以改變它們的大小和位置姿锭。
我們來逐句看下代碼:
1、改變label的寬度為正好比界面的寬度少115點(diǎn)伯铣,這樣在iPhone SE上就正好是200點(diǎn)寬度艾凯。
這條代碼同時使得高為10000。這樣就足夠容納任何長度的字符串了懂傀。
因為你改變了frame屬性趾诗,所以現(xiàn)在UILable中的多行文本會以換行的形式來適應(yīng)label的寬度。因為你已經(jīng)在viewDidLoad()中對標(biāo)簽的文本進(jìn)行了設(shè)置。
2恃泪、使標(biāo)簽適應(yīng)文本的大小郑兴,你必須使label自動適應(yīng)文本的大小,否則每次這個cell都會是10000的高度贝乎。為了達(dá)到這個目的可以使用菜單中的Size to Fit情连,也可以使用方法sizeToFit()。
3览效、調(diào)用sizeToFit()會移除掉label右側(cè)和底部的多余的空間却舀。它同時也可能會改變label的寬度,以便label內(nèi)部的文本盡可能和和label貼近锤灿,所以label的x位置可能會變得不再正確挽拔。
所以我們需要重新擺放它的位置,正好和界面邊緣有15點(diǎn)的空隙但校。我們通過改變frame的origin.x屬性來實(shí)現(xiàn)這個目的螃诅。
4、然后你在label的高度上加上20點(diǎn)的余量(頂部10點(diǎn)和底部10點(diǎn))状囱,就是最后cell的高度了术裸。
??:如果你覺得用這種方式來制定多行文本的大小太可怕了,我完全同意你的意見亭枷,但是重要的是袭艺,這種方法非常有效。
也許你想知道叨粘,能不能用自動布局來解決這個問題猾编,答案是肯定的,你可以使用自動布局來自動計算address cell的高度宣鄙,使用所謂的自定義大小的table view cell來自動計算address cell的高度袍镀。
然而默蚌,對多行文本的label使用自動布局會很麻煩冻晤。我覺得還是手動計算來的簡單些。
運(yùn)行app绸吸,現(xiàn)在地址信息應(yīng)該能夠正常顯示了鼻弧,即使是在iPhone 6或者7上:
Frame and bounds(邊框和范圍)
在上面的代碼中,有這樣一段:
addressLabel.frame.size = CGSize(width: view.bounds.size.width - 115, height: 10000)
你使用了視圖的范圍來計算address標(biāo)簽的邊框锦茁。邊框和范圍的類型都是CGRect攘轩,這種類型描述了一個矩形。那么邊框和范圍的區(qū)別是什么呢码俩?
邊框表述的是一個視圖在它的父視圖中的大小和位置度帮。如果你想把一個150*50的label放到X:100,Y:30的位置,那么它的邊框就是(100笨篷,30瞳秽,150,50)率翅。把一個視圖從一個位置移動到另一個位置练俐,你需要改變它的frame屬性。
范圍是描述視圖內(nèi)部的大小冕臭。在范圍中X和Y始終是(0,0)腺晾,寬度和高度則和邊框一致。對于上面的例子而言辜贵,它的范圍就是(0悯蝉,0,150念颈,50)泉粉。
當(dāng)你用自動布局為一個視圖添加約束的時,這些約束通常是由視圖的邊框計算得出的榴芳,同時嗡靡,如果你一個視圖具有約束,你就不應(yīng)該手動去調(diào)整它的邊框或者范圍窟感,這會把一切都弄糟讨彼。
分類選擇器(The category picker)
當(dāng)用戶點(diǎn)擊Category(分類)cell時,app會展示一個列表顯示分類的名稱:
這是一個新的界面柿祈,所以你需要創(chuàng)建一個新的視圖控制器哈误。這和上個課程中的圖標(biāo)選擇界面很像。所以我下面會講快一些躏嚎。
添加一個新的文件蜜自,命名為CategoryPickerViewController.swift.
刪掉該文件中的原有內(nèi)容,替換為下面的代碼:
import UIKit
class CategoryPickerViewController: UITableViewController {
var selectedCategoryName = ""
let categories = [
"No Category",
"Apple Store",
"Bar",
"Bookstore",
"Club",
"Grocery Store",
"Historic Buliding",
"House",
"Icecream Vendor",
"Landmark",
"Park"]
var selectedIndexPath = IndexPath()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0 ..< categories.count {
if categories[i] == selectedCategoryName {
selectedIndexPath = IndexPath(row: i,section: 0)
break
}
}
}
//MARK: - UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let categoryName = categories[indexPath.row]
cell.textLabel!.text = categoryName
if categoryName == selectedCategoryName {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
//MARK - UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row != selectedIndexPath.row {
if let newCell = tableView.cellForRow(at: indexPath) {
newCell.accessoryType = .checkmark
}
if let oldCell = tableView.cellForRow(at: selectedIndexPath) {
oldCell.accessoryType = .none
}
selectedIndexPath = indexPath
}
}
}
這里沒有新的東西卢佣。你創(chuàng)建了一個table view controller重荠,用來展示分類的名稱。它有table view數(shù)據(jù)源以及委托方法虚茶。數(shù)據(jù)源從categories數(shù)組中讀取數(shù)據(jù)戈鲁。
唯一值得注意的事情是實(shí)例變量selectedIndexPath。當(dāng)這個界面打開時嘹叫,它會在目前被選擇的分類的旁邊顯示一個對勾符號婆殿。具體在哪一條上顯示,取決于轉(zhuǎn)場時selectCategoryName屬性罩扇。
當(dāng)用戶點(diǎn)擊某一行婆芦,你需要把對勾符號從之前的行上移除,并且在新選定的這一行上顯示。
為了直線這個目的消约,你需要知道目前被選定的是哪一行癌压。你不能用selectCategoryName來判斷,因為它是一個字符串荆陆,不是一個行號滩届。因此,你首先要找到當(dāng)前被選定的這一行的行號或者indexPath被啼。
你可以在viewDidLoad()中做這件事帜消。你歷遍categories數(shù)組并且用selectCategoryName和數(shù)組中每一個對象做比較。如果比對成功浓体,你就創(chuàng)建一個indexPath對象泡挺,并且存儲到selectedIndexPath變量中,然后中斷循環(huán)命浴。
現(xiàn)在你知道了行號娄猫,就可以在另一行被點(diǎn)擊時,移除當(dāng)前行的對勾符號了生闲,我們是在tableView(didSelectRowAt)中實(shí)現(xiàn)了這個目的媳溺。