iOS Apprentice中文版-從0開始學iOS開發-第十七課

編輯已有的待辦事項名稱

在app中添加新的待辦項目到列表中對你是一個巨大的進步,但是通常伴隨著新增還有其他兩個操作需要實現,那就是:

1、刪除待辦項目(通過輕掃某一行刪除,我們已經在前面實現了)

2、編輯已存在的待辦項目

編輯已存在的項目是非常有用的,比如你想對待辦項目重命名或者你打錯字了需要修改,都需要用到編輯功能。

你也許可以做一個全新的界面來完成這個功能,但是你會發現這個功能和新增待辦事項界面沒有什么區別,唯一的不同就是一開始文本框中存在字符,而新增時一開始文本框是空的。

所以我們來重用新增待辦事項界面,讓它具備修改已存在項目的名稱的能力。

實現后的預覽

當用戶點擊Done按鈕后你做的并不是創建一個新的CheclistItem對象,而是簡單的修改已存在的CheclistItem對象的文本。

你同時也要通知委托這里做出了修改,這樣委托就會更新列表中相應的table view cell。

練習:為了實現這個功能,我們都需要做哪些改造,你能做個列表出來嗎?

答案:1、編輯項目時界面需要被重命名為Edit Item(現在是Add Item)
2、你必須能讀取到一條存在的ChecklistItem對象。
3、你必須把這個ChecklistItem對象的文本放入文本框。
4、當用戶點擊Done按鈕時,你不應該添加新的ChecklistItem對象,而是更新被編輯的那個。

這里有一些用戶界面問題需要處理,比如用戶如何打開編輯項目的界面?多數app都是通過直接點擊某一行完成這個動作,但是在我們的這個app中,點擊動作已經被占用了,它用來控制對勾符號的開關。

為了解決這個問題,你需要先修改一下UI設計。

當一行具備兩個功能的時候,標準的做法是使用‘詳細信息按鈕(detail disclosure button)’來完成第二個功能。

詳細信息按鈕

點擊某一行時,還是執行原來的動作,在我們的app中就是控制對勾符號的開關,而當點擊詳細信息按鈕時,打開編輯項目界面。

??:還有一種可選擇的方法是,只有點擊左邊對勾符號的區域時,用于開關對勾符號,而點擊這一行的其他地方時,打開編輯項目界面。
還有一些app是這樣做的,你可以把整個屏幕觸發為可編輯狀態,然后可以逐條編輯項目。具體使用哪種方法依賴于哪種方法最有利于你的數據模型。

打開故事模版選定table view cell,然后進入到屬性檢查器,找到Accessory選項并且選擇Detail Disclosure(注意:是Detail Disclosure,不是Detail)選項。

這時對勾符號將被一個藍色感嘆號+一個大于號到按鈕圖標替代掉。這就意味著你要重新選擇一個地方放置對勾符號。

拖拽一個新的label到cell中去,并且做出如下設置:

文本 :√(option+v可以輸入這個符號)

字體:Helvetica Neue,bold,大小22

Tag:1001

如果option+v沒有這個符號,你可以從Xcode頂部菜單中選擇Edit->Emoji & Symbols。然后在彈出窗口的搜索欄中輸入“check”,選擇一個對勾符號,或者選擇任何你喜歡的圖形。(注意,在進行此操作時,先雙擊標簽,處于可以輸入文本的狀態下再進行,另外,這些特殊符號也許在部分手機上無法正確顯示)

插入Emoji符號

重新調整兩個標簽的位置和大小,不要互相覆蓋,也不要覆蓋到右邊的藍色感嘆號按鈕上去。

重新設計后的prototype cell應該是這種樣子的:

新的cell設計

打開ChecklistViewController.swift,將configureCheckmark(for:with:)改變為:

func configureCheckmark(for cell: UITableViewCell,with item: ChecklistItem) {
        let label = cell.viewWithTag(1001) as! UILabel
        
        if item.checked {
            label.text = "√"
        } else {
            label.text = ""
        }
    }

現在我們不修改cell的accessory屬性了,而是修改新增的label的文本。

運行app,你會看到對勾符號從右邊轉移到左邊了,同時這里多了一個藍色感嘆號的詳細信息按鈕在右邊。點擊某一行會開關對勾符號,而點擊詳細信息按鈕則不會。

運行后的效果

現在,我們開始著手處理點擊詳細信息按鈕后打開編輯界面的事情。這非常簡單,因為界面建造器允許你為詳細信息按鈕添加一個轉場。

打開故事模版。選定table view cell并且按住ctrl拖拽到旁邊的Navigation Controller上,在彈出窗口中選擇Accessory Action分節下的Present Modally。

為詳細信息按鈕添加轉場

現在從Checklists界面到導航欄有兩個轉場了,一個是用于?按鈕,一個用于cell中的詳細信息按鈕。

兩個轉場

為了使app能夠區分兩個轉場,我們必須給它們不同的身份id。

選擇新的轉場箭頭,然后打開屬性檢查器,在identifier中輸入EditItem。

如果你運行app的話,點擊藍色的感嘆號按鈕,會打開新增待辦事項界面,但是此時你點擊Cancel按鈕的話,不會有任何作用。

練習:想一想為什么?

答案:你還沒有配置委托。一定要記得你是在prepare(for:sender:)中設置委托的,但是只有?號按鈕的AddItem轉場設置委托,你還需要對EditItem轉場做同樣的事情。

在你修復這個問題前,你需要先使新增待辦事項界面擁有編輯ChecklistItem對象的能力。

打開AddItemViewController.swift,添加一個新的實例變量:

var itemToEdit: ChecklistItem?

這個變量用于包含用戶準備編輯的ChecklistItem對象。但是當新增一個待辦項目時,itemToEdit會是nil,這就是視圖控制器如何區分新增和編輯的。

因為itemToEdit會為nil,所以它必須是可選型的,這就是問號的作用。

還是在AddItemViewController.swift中,添加viewDidLoad()方法:

override func viewDidLoad() {
        super.viewDidLoad()
        
        if let item = itemToEdit {
            title = "Edit Item"
            textField.text =  item.text
        }
    }

回憶一下viewDidLoad()是黨視圖控制器從故事模版中被加載時由UIKit調用,此時界面還沒有展現在屏幕上。這就給了你時間配置用戶界面。

在編輯模式下,也就是當itemToEdit不為nil時,你改變導航欄的名稱為“Edit Item”。你通過改變導航欄的title屬性來完成這一功能。

每個視圖控制器都有幾個內建的屬性,title正是其中之一。導航控制器讀取到title的值后自動改變導航欄的名稱。

你同時也設置了文本框的值為item的text屬性。

if let
你不能像使用普通變量那樣使用可選型變量。例如,如果你直接這樣寫語句的話:

textField.text =  item.text

這樣Xcode的編譯器會給出一個報錯,“Value of optional type ChecklistItem?not unwarped(類型為ChecklistItem的可選型變量的值沒有解包)”
這是因為itemToEdit是ChecklistItem的可選型變量。
要使用它你首先要進行解包。通過一個特殊的語法完成這一功能:

if let temporaryConstant = optionalVariable {
  // temporaryConstant now contains the unwrapped value
  // of the optional variable
}

如果這個可選型不是nil,if的條件為真,if語句體內的代碼才會被執行。
還有一些其他方法可以讀取可選型變量的值,但是使用if let是最安全的:如果可選型沒有值,或者為nil,那么if語句體內的代碼會被自動略過。
可選型讓你頭暈腦脹了嗎?多做練習,熟能生巧,每個人都是這樣過來的。可選型是swift的特色,多數主流語言沒有這個功能,許多開發者都會在這里付出大量精力去理解它。
盡管難以理解,但是可選型可以避免空指針錯誤并且防止你的app掛掉。

現在AddItemViewController有能力識別什么時候進入編輯界面了。如果itemToEdit擁有一個ChecklistItem對象,那么界面會魔法般的變成編輯界面。

但是你在哪里給itemToEdit賦值呢?當然是在轉場的時候了!這里最理想的地方用于給變量賦值,在界面即將顯示在屏幕上前把一切都配置好。

打開ChecklistViewController.swift修改prepare(for: sender:)為下面這個樣子:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "AddItem" {...
} else if segue.identifier == "EditItem" {
            let navigationController = segue.destination as! UINavigationController
            let controller = navigationController.topViewController as! AddItemViewController
            controller.delegate = self
            if let indexPath = tableView.indexPath(for: sender as! UITableViewCell) {
                controller.itemToEdit = items[indexPath.row]
            }
        }
    }

和以前一樣,你從故事模版中讀取導航控制器,并且使用topViewController屬性調用AddItemViewController。

你同時也設置了視圖控制器的委托屬性,這樣用戶點擊Cancel或者Done按鈕時,你就可以得到通知了,這里做的事和“AddItem”轉場一模一樣。

比較有意思的是新增加的這一部分:

if let indexPath = tableView.indexPath(for: sender as! UITableViewCell) {
                controller.itemToEdit = items[indexPath.row]

prepare(for:sender:)方法有一個叫做sender的參數。這個參數包含一個控制轉場的引用,在我們的這個情況中,就是table view cell的詳細信息按鈕被點擊時。

你設置了一個UITableViewCell對象用于定位被點擊的那一行的行號相應的index-path,通過使用tableView.indexPath(for:)。

tableView.indexPath(for:)的返回類型為IndexPath?,是一個可選型,這就意味著它可能返回nil。這就是為什么在你使用它前需要用if let來解包的原因。

一旦你有了一個行號,你就可以獲得需要被編輯的ChecklistItem對象,并且你同時將它分配給了AddItemViewController的itemToEdit屬性。

視圖控制器之間互相發送數據
我們講過界面B(Add/Edit Item screen)回傳數據給界面A(Checklists screen)是通過委托完成的。
但是這里你是從界面A向界面B傳遞數據,也就是說傳遞一個ChecklistItem對象用于編輯。
數據在視圖控制器間傳遞有兩種方法:
1、從A到B。當界面A打開界面B時,A可以給B需要的數據。你簡單的在B的視圖控制器中創建一個實例變量,然后A轉場到B時給這個變量賦值就可以了,這一工作通常都是在prepare(for:sender:)中完成。
2、從B到A。B回傳數據給A則需要使用委托。
下圖說明了A發送數據給B時,是如何對B的變量賦值的,以及B是如何通過委托向A回傳數據的:



我希望你能初步的了解了視圖控制器間的數據傳遞方式。在我們的這個課程中還會出現幾次這樣的場景,請務必掌握這一知識。
制作iOS app的本質就是創建視圖控制器并且在其中傳遞數據,你要把這件事變成自己的第二本能。

做完這些步驟后,你可以運行一下app,點擊?按鈕會打開新增待辦事項界面,而點擊某一行上的藍色感嘆號按鈕后會彈出編輯界面,界面上已經存在了待編輯的條目:

編輯待辦事項

有個小問題:導航欄上的Done按鈕一開始是被禁用的,這是因為你最初的時候在故事模版中設置禁用了它。

打開AddItemViewController.swift,添加一行代碼進去:

override func viewDidLoad() {
  super.viewDidLoad()
  if let item = itemToEdit {
    title = "Edit Item"
    textField.text = item.text
    doneBarButton.isEnabled = true  //添加這一行
} 
}

在編輯模式下,可以安全的在一開始就將Done按鈕置為可用狀態,因為文本被刪至沒有時,Done會自動被禁用。

真正的問題不在這里,眼下你運行app,編輯某一行后點擊Done按鈕,你會發現原先的一行并沒有被修改,取而代之是,新增了一行上去。

你還沒有寫代碼來復制并且更新數據模型,所以委托會以為你的目的是新增一行。

為了解決這個問題,你需要在委托協議中新增一個方法。

打開AddItemViewController.swift,在協議中添加如下代碼:

func addItemController(_ controller: AddItemViewController,didFinishEditing item: ChecklistItem)

現在整個協議內容看起來是這個樣子的:

protocol AddItemViewControllerDelegate: class {
    func addItemControllerDidCancel(_ controller: AddItemViewController)
    func addItemController(_ controller: AddItemViewController,didFinishAdding item: ChecklistItem)
    func addItemController(_ controller: AddItemViewController,didFinishEditing item: ChecklistItem)
}

現在用戶點擊Done按鈕后一共有兩個方法用于響應。

當新增一行后調用didFinishAdding,而當編輯一行后調用didFinishEditing。

通過調用不同的委托方法就可以分別處理這兩種不同的情景了。

打開AddItemViewController.swift,將done()方法修改為:

@IBAction func done() {
        if let item = itemToEdit {
            item.text = textField.text!
            delegate?.addItemController(self, didFinishEditing: item)
        } else {
        let item = ChecklistItem()
        item.text = textField.text!
        item.checked = false
        delegate?.addItemController(self, didFinishAdding: item)
        }
    }

首先我們檢查itemToEdit屬于是否包含一個對象,你應該明白if let的作用是解包可選型變量。

如果這個可選型變量不為空,你就在文本框中放入一條已存在的ChecklistItem對象并且調用新的didFinishEditing方法。

如果為空,則添加一條新的紀錄進去,就像我們之前做的一樣。

試著運行app,你會看到一個驚喜,app掛了。

Xcode說道:“Build Failed”,但是在AddItemViewController.swift中看不到任何報錯,這是為什么?

你可以在Xcode的問題導航器(Issue navigator)中看到一個報錯信息:

報錯信息

這個報錯信息是發生在ChecklistViewController中的,因為它沒有執行協議中的方法。這并不奇怪,因為你剛把didFinishEditing方法添加到協議中,但是你還沒有告訴視圖控制器,誰來扮演委托的角色。

??:在我的這個版本的Xcode中報錯信息為:Method 'addItemController(_:didFinishAdding:)' has different argument names from those required by protocol...,這是個比較奇怪的報錯信息。它并沒有準確的描述什么地方出了問題,僅僅是反應了Swift對目前的情況困惑不解。
當你制作自己的app時,你也許會面對Swift不可描述的奇怪報錯信息。隨著經驗的積累,你會變得熟悉這些情況。Swift的編譯器剛問世不久,它也需要被完善。

在ChecklistViewController.swift中添加以下代碼,就可以把剛才的報錯化為歷史了:

func addItemController(_ controller: AddItemViewController, didFinishEditing item: ChecklistItem) {
        if let index = items.index(of: item) {
        let indexPath = IndexPath(row: index, section: 0)
        if let cell = tableView.cellForRow(at: indexPath) {
            configureText(for: cell, with: item)
            }
        }
        dismiss(animated: true, completion: nil)
    }

這樣ChecklistItem就有了新的文本,cell也是已經存在在table view中的,你只是需要更新table view cell中的標簽。

這個新的方法就是你用來尋找cell對應的ChecklistItem對象,然后通過configureText方法來更新標簽。

第一行語句對我們而言比較新鮮:

if let index = items.index(of: item)

你需要從cell中讀取所需的IndexPath,首先你就需要尋找到ChecklistItem對象的行號。行號和ChecklistItem在items數組中的索引值是一致的,然后你通過index(of)方法來返回這個index。

雖然在我們的這個app中不會發生,但是理論上數組中的某個對象會不存在,這個時候index(of)會返回nil,所以它的返回值是可選型的,所以我們用if let對它進行解包。

試著運行app,噢,我想我太心急了。Xcode給出了另一個報錯:Cannot invoke index with an argument list of type blah blah blah。這是怎么回事?

這是因為你不能在任意對象上使用index(of),只能在“相同”的對象上使用它。index(of)以某種方式對你在數組中尋找的對象進與調用它的對象行比較,看看它們是否相等。

你的ChecklistItem對象現在并不具備這個功能。這里有好幾種方式處理這個問題,我們來用最簡單的一種。

在ChecklistItem.swift的類聲明的那一行添加一點東西:

class ChecklistItem: NSObject {

如果你之前使用過Object-C,那么你應該對NSObject非常熟悉。

幾乎所有Object-C中的對象都是基于NSObject的。這是由iOS提供的最基本的代碼塊,它可以提供Swift對象所沒有的大量有用的基礎功能。

你以Swift寫代碼時候大多數時候并不需要搭理NSObject,但是眼下是必須的。

將ChecklistItem建立在NSObject之上,就可以使它安全的進行比較了。后面我們在學習如何存儲checklist對象時,你也必須將它轉換為NSObject。

運行app,試試功能,現在一切都正常了。

重構代碼

現在你已經有了一個app,可以實現新增待辦事項以及編輯事項的功能,非常不錯。

但是我發現AddItemViewController這個名字起的不太恰當了,畢竟這個界面現在承載著新增待辦事項和編輯已有項目兩個功能,我們應該將它重命名為ItemDetailViewController。

現在我們有一個好消息和一個壞消息,你想先聽哪個?

好消息是Xcode有一個特殊的菜單用于重構源代碼,包含重命名的工具,你可以在Edit->Refactor中找到它。

壞消息是在Xcode 8.0中這個功能并不支持Swift語言,僅支持Object-C,所以你不能使用它,差評。

所以我們的唯一選擇就是手工完成這件事,幸運的是,Xcode有一個非常便利的搜索和替換功能。我們接下來一步步的做完這個工作。

1、打開搜索導航起,工程導航界面上的第三個按鈕。

就是放大鏡圖標的那個

2、點擊Find,切換為Replace(替換)。

3、將上圖中的Ignoring Case,切換為Matching Case。

4、在搜索框中輸入AddItemViewController。重要:確保你的拼寫無誤。

5、在替換框中輸入ItemDetailViewController

填寫完畢的樣子

6、點擊鍵盤上的回車鍵開始搜索,這一操作不會進行替換。

搜索結果會返回所有相關的匹配項。你應該可以看到兩個swfit源文件和Main.storyboard。

搜索結果

7、點擊Preview按鈕。Xcode會打開一個界面里面包含各個文件中將被替換掉部分。

替換結果預覽

仔細的逐條對比每一對替換,確保沒有替換掉不應該替換掉東西。這次替換僅僅是把名為AddItemViewController的字樣替換為ItemDetailViewController,包含故事模版內的。

8、點擊Replace按鈕并且祈禱。如果Xcode需要你確認,點擊Continue。

9、在工程導航器中,選定AddItemViewController.swift,然后再點擊一次,兩次點擊不要太快,然后你就可以對這個文件進行重命名了。

新的名字是:ItemDetailViewController.swift

還沒有結束,你還要為之前的委托協議進行重命名。

1、再次切換到搜索導航器

2、確定處于Replace模式下(如果不是點擊Find切換到Replace)

3、確保處于Matching Case模式下(如果不是則點擊Ignoring Case切換)

4、輸入搜索文本addItemViewController,注意開頭是小寫的a

5、輸入替換文本itemDetailViewController,注意開頭是小寫的i

6、敲擊鍵盤上的回車鍵。

得到搜索結果

搜索結果應該只包含協議委托方法的名稱,分別在ChecklistViewController.swift和ItemDetailViewController.swift兩個文件中。

7、點擊Replace All進行替換。

替換完畢后,可以再進行一次搜索確認替換結果,此時應該搜不到任何東西了,因為都被替換完了。

現在ItemDetailViewController.swift中的協議方法應該是下面這個樣子:

protocol ItemDetailViewControllerDelegate: class {
    func itemDetailViewControllerDidCancel(_ controller: ItemDetailViewController)
    func itemDetailViewController(_ controller: ItemDetailViewController,didFinishAdding item: ChecklistItem)
    func itemDetailViewController(_ controller: ItemDetailViewController,didFinishEditing item: ChecklistItem)
}

用command+B鍵來重新編譯以下代碼,如果順利,編譯會成功通過。

??:如果編譯報錯,那么一定是搜索和替換的關鍵字錯了,重新檢查一下,尤其注意大小寫,在swift中ItemDetailViewController和itemDetailViewController是兩個完全不同的對象,它們開頭的字母大小寫不一樣。

如果你運行app崩潰的話,檢查一下故事模版中Add Item這個界面的身份檢查器中的Custom Class此時應該為ItemDetailViewController。有時在搜索替換時,Xcode會漏掉這個地方。

因為你做了很多改動,此時最好清理一下緩存先,通過Xcode的菜單Product->Clean來完成緩存清理。

如果沒有問題,你就可以重新運行app,好好的測試下各種場景了。確保一切工作正常。(如果Xcode編譯正常但是仍然顯示有報錯,那么就整個關掉Xcode再重新打開試試)

迭代開發(Iterative development)
如果你覺得我們這個課程里的開發過程太啰嗦了,為什么不一步到位的講呢?非要弄出這么多彎路。這種想法是不正確的。
當你從一個設計開始實現具體功能的時候,你總會發現很多問題沒有考慮清楚,所以你需要在實踐的過程中不斷的重構自己的代碼,最終達到理想效果。
軟件開發就是這樣的一種工作。
一開始你只是完成一小塊功能,并且看上去好像沒有什么問題。然后你又添加另外一小部分進去,突然間意料之外的問題就出現了。這時候也許你就不得不推翻之前的設計一切從頭開始了。直到最終完成為止你都不得的頻繁的重復這個行為。
軟件開發就是一個不斷的細致化的過程。所以在我們的課程中,我不會給你最完美的結局方案,而是通過不斷的修改至臻完善,并且詳細的展示每一小塊的細節。因為真實的軟件開發就是這樣一個工作。
而你則會從0出發直到完成一個完整的app,并且在這個過程中持續的解決問題,就像哪些專業人士做的一樣。
在軟件的領域不存在高屋建瓴的設計,像建筑圖紙那樣的東西,我并不相信這些設計。當然提前做好計劃是沒錯的,但是不能把計劃當成實現方式。在寫這些教程的時候,我會畫一些草圖來想象這些app應該如何運作。這個工作的效果顯著的,但是經常性的,在開發的過程中會遇到問題,使我不得不改變當初的想法,這還僅僅是這樣一個小的app而已。
這并不是說你不要去做提前的規劃,只是不要在這個上面浪費太多時間而已。
你要做的是簡單的從某個步驟開始做起,直到遇到問題卡住,然后來回溯并且找到問題的解決辦法。這就叫做迭代開發,這種方法比提前規劃好藍圖的效果和效率要快的多了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容