2014年7月9日 星期三

Core Data programmatically using Swift language

【說明】

此份筆記是根據ISBN:978-986-201-900-9這本書的第18章節所紀錄的。

Core Data與plist的差異:
plist處理小資料量時比較快速,但是會一次把文件載入記憶體內,效率上不是很優。Core Data擁有table之間的Relationship,與SQLite相似,但是不必下語法。

Core Data就是可以儲存到磁碟的物件圖,不僅僅在磁碟上儲存資料,也把我們需要的資料物件讀取到記憶體中。   - Marcus Zarra, Core Data

Core Data主要任務是負責資料更改的管理,序列化到磁碟,最小化記憶體佔用,以及查詢資料,可以將資料儲存為XML、二進制檔案或是SQLite檔案。

Core Data堆疊(Stack):
託管物件模型(Managed Object Model):描述你在程式裡使用的Schema(綱要)。
永續性儲存協調器(Persistent Store Coordinator):負責處理不同永續性物件儲存區以及將物件儲存到儲存區中。
託管物件本文(Managed Object Context):管理被建立的物件並使用Core Data回傳。


【專案開發步驟】

建立專案:

使用Empty Application模板建立專案,專案名稱設為RecipeStore,並勾選使用Core Data。

因為使用Empty Application建立專案,所以需新增Storyboard來方便我們撰寫程式,並將名稱設為Main,如下圖所示。

新增Storyboard
點選專案,將Main Interface設為Main,如下圖所示。

設定Main Interface
將didFinishLaunchingWithOptions:這個方法更新,如下所示。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
    
    return true
}

定義託管物件模型:

在資料模型內建立一個Entity,並取名為Recipe,如下圖所示。

建立Entity
在名為Recipe的Entity內建立三個Attributes,分別為name、image、prepTime,並設三個資料型態為String,如下圖所示。

建立Attributes

設計使用者介面:

依照下圖建立好介面。

建立使用者介面
將+的Segue Identifier設為Add,如下圖所示。
設定Segue Identifier
並將Prototype Cell的Identifier設為Cell,如下圖所示。

設定Identifier為Cell
建立一個TableViewController的類別,取名為RecipeStoreTableViewController,並繼承UITableViewController,至Storyboard設定介面與自訂類別的關聯,如下圖所示。

設定頁面與自訂類別的關聯
建立一個ViewController的類別,取名為AddRecipeViewController,並繼承UIViewController,至Storyboard設定介面與自訂類別的關聯,如下圖所示。

設定頁面與自訂類別的關聯
建立AddRecipeViewController頁面上,三個TextField與兩個Bar Button的IBOutlet與IBAction,如下圖所示。

建立物件的關聯

建立託管物件:

在AppRecipeViewController.swift內建立一個func,如下所示。

func managedObjectContext() -> NSManagedObjectContext {
        
    let appDelegate: AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
    let context: NSManagedObjectContext = appDelegate.managedObjectContext
        
    return context

}
透過這個func可以管理物件,經由本文就可以取得或變更託管物件。

在AppRecipeViewController.swift實作save:方法,如下所示。

@IBAction func save(sender: UIBarButtonItem) {
        
    var context: NSManagedObjectContext = self.managedObjectContext()
        
    var newRecipe: NSManagedObject = NSEntityDescription.insertNewObjectForEntityForName("Recipe", inManagedObjectContext: context) as NSManagedObject
    newRecipe.setValue(nameTextField.text, forKey: "name")
    newRecipe.setValue(imageTextField.text, forKey: "image")
    newRecipe.setValue(prepTimeTextField.text, forKey: "prepTime")
        
    self.dismissViewControllerAnimated(true, completion: nil)

}
先取得託管物件本文,然後針對新的食譜建立一個NSManagedObject,並將託管物件跟Entity關聯在一起。NSManagedObject就像是NSDictionary一樣,一旦建立託管物件(newRecipe)後,即可將Entity的屬性當作Key一樣來設定value。最後dismissViewControllerAnimated用來解除目前的ViewController。

在AppRecipeViewController.swift實作cancel:方法,如下所示。

@IBAction func cancel(sender: UIBarButtonItem) {
        
    self.dismissViewControllerAnimated(true, completion: nil)

}
解除目前的ViewController。

取得託管物件:

在RecipeStoreTableViewController.swift內建立一個名為recipes的可變陣列,如下所示。

var recipes: NSMutableArray = []
資料型態為NSMutableArray的陣列變數,名為recipes。

在RecipeStoreTableViewController.swift內建立一個func,取得託管物件文本,與AppRecipeViewController.swift內的func相同。

在RecipeStoreTableViewController.swift內增加viewDidAppear方法,如下所示。

override func viewDidAppear(animated: Bool)  {
    super.viewDidAppear(true)

    var managedObjectContext = self.managedObjectContext()
    let fetchRequest = NSFetchRequest(entityName: "Recipe")
    recipes = NSMutableArray(array: managedObjectContext.executeFetchRequest(fetchRequest, error: nil))
        
    self.tableView.reloadData()

}
為了要取得資料,必須取得本文(Context)。建立一個提取需求(fetch request),並指定要去提取的Entity。利用Recipe entity建立一個NSFetchRequest的實體,並用executeFetchRequest:這個方法取得資料庫的所有食譜。最後讓tableView重新載入。
viewDidAppear方法是在View確實被顯示出來時才會呼叫的,利用這個方法讓新食譜被建立後重新載入資料,但這不是好的方法,因為若使用者不想新建食譜時按下取消也會被呼叫。

在TableView中填入食譜:

在RecipeStoreTableViewController.swift內實作以下方法。

override func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
    return 1

}
回傳有幾個Section在TableView裏面。

override func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
    return recipes.count

}
告訴ViewController要顯示多少個Cell。

override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell? {
        
    var cellIdentifier: String = "Cell"
    var cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as UITableViewCell
        
    if cell == nil {
        cell = UITableViewCell(style: .Default, reuseIdentifier: cellIdentifier)
    }

    var recipe: NSManagedObject = recipes.objectAtIndex(indexPath.row) as NSManagedObject
    
    cell.textLabel.text = recipe.valueForKey("name") as String
    var imageString: String = recipe.valueForKey("image") as String
    var prepTimeString: String = recipe.valueForKey("prepTime") as String
    cell.detailTextLabel.text = "\(imageString) - \(prepTimeString)"
        
    return cell

}
在TableView內顯示資料,可參考這篇。NSManagedObject與NSDictionary非常像,使用valueForKey方法就可以取得屬性值。將食譜名稱作為標題,圖檔名與準備時間以子標題方式呈現。


【執行結果】

    

    


【專案範例】

2014年7月3日 星期四

UIWebView programmatically using Swift language

【說明】

此份筆記是根據ISBN:978-986-201-900-9這本書的第11章節所紀錄的。

【片段程式碼】

var url: NSURL = NSURL.fileURLWithPath(NSBundle.mainBundle().pathForResource("about.html", ofType: nil))
建立一個名為url的變數,資料型態為NSURL,存放html在檔案裡的路徑。

var url: NSURL = NSURL.URLWithString("http://yishenghsu.blogspot.tw")
若欲要顯示的html不是在專案裡面而是在遠端,可以透過這樣程式碼將遠端的網頁載入。

var request: NSURLRequest = NSURLRequest(URL: url)
建立一個名為request的變數,資料型態為NSURLRequest,要求將html載入request內。

webView.loadRequest(request)
請UIWebView顯示畫面。

【執行結果】

載入本地端的網頁內容
載入遠端的網頁內容

UITableView programmatically using Swift language(Send data by Segue)

【說明】

此份筆記是根據ISBN:978-986-201-900-9這本書的第10章節所紀錄的。

此份筆記記錄如何透過Storyboard的Segue將資料從A畫面傳送到B畫面顯示。

首先必須設定Segue的Identifier,主要是用來判定是從哪一個Segue到哪一個畫面,如下圖所示,將Identifier設定為showRecipeDetial。

設定Segue的Identifier
在A畫面的ViewController內實作prepareForSegue這個方法即可傳遞想要顯示的資料。

【片段程式碼】

override func prepareForSegue(_segue: UIStoryboardSegue!, sender: AnyObject!) {
        
}
實作prepareForSegue這個方法,因為A畫面是繼承UITableViewController,所以func會以override的方式實作。

if (_segue.identifier == "showRecipeDetial") { }
確認Segue的identifier是不是先前我們設定的字串。

var indexPath = tableView.indexPathForSelectedRow()
建立一個名為indexPath的變數,存放的是使用者所點選行。

var destViewController = _segue.destinationViewController as RecipeDetailViewController
建立一個名為destViewController的變數,資料型態為RecipeDetailViewController,存放的是Segue所連接的目的地ViewController,如此一來我們就可以存取RecipeDetailViewController內所擁有的變數。

destViewController.recipeName = recipesNames[indexPath.row]
將A畫面的recipesNames傳給B畫面的recipeName。


【完整的prepareForSegue程式碼】

override func prepareForSegue(_segue: UIStoryboardSegue!, sender: AnyObject!) {
    if (_segue.identifier == "showRecipeDetial") {
        var indexPath = tableView.indexPathForSelectedRow()
        var destViewController = _segue.destinationViewController as RecipeDetailViewController
        destViewController.recipeName = recipesNames[indexPath.row]
    }
}

【執行結果】

    

2014年7月2日 星期三

UITableView programmatically using Swift language(Property List)

【說明】

此份筆記是根據ISBN:978-986-201-900-9這本書的第7章節所紀錄的。

將此篇的資料改為以Property List來儲存,Property List是一種提供儲存結構化資料的方式。

建立一個Property List的檔案,如下圖所示。

新增Property List檔案
將先前在CustomTableViewController內建立的資料轉換成Property List,如下圖所示。

將資料轉換為Property List

【片段程式碼】

var recipesNames: String[] = []
var recipesImages: String[] = []
var recipePrepTimes: String[] = []
將三個變數設為空值,並設為全域變數。

let path = NSBundle.mainBundle().pathForResource("recipes", ofType: "plist")
設一個名為path的變數,儲存Property List存放的位置。

let dict = NSDictionary(contentsOfFile: path)
設一個名為dict的變數,將Property List以NSDictionary的方式存在dict裡面。

recipesNames = dict.objectForKey("Name") as Array<String>
recipesImages = dict.objectForKey("Image") as Array<String>
recipePrepTimes = dict.objectForKey("PrepTime") as Array<String>
將Dictionary內的key給變數做存放。

若單獨以這幾行程式碼放在CustomTableViewController的class內會出現ViewController沒有path這個成員的錯誤訊息,所以解決方法就是將程式塞進awakeFromNib()的函式裡。

override func awakeFromNib() {
    super.awakeFromNib()
        
    let path = NSBundle.mainBundle().pathForResource("recipes", ofType: "plist")
    let dict = NSDictionary(contentsOfFile: path)
    recipesNames = dict.objectForKey("Name") as Array<String>
    recipesImages = dict.objectForKey("Image") as Array<String>
    recipePrepTimes = dict.objectForKey("PrepTime") as Array<String>
}
如此一來就可以解決錯誤的訊息。

【執行結果】


【專案範例】

UITableView programmatically using Swift language(Delete Cell)

【說明】

此份筆記是根據ISBN:978-986-201-900-9這本書的第6章節所紀錄的,沿用這篇的範例,新增刪除資料的功能。

【片段程式碼】

override func tableView(_tableView: UITableView!, commitEditingStyle editingStyle: 
         UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath!) {
        
    recipesNames.removeAtIndex(indexPath.row)
    recipesImages.removeAtIndex(indexPath.row)
    recipePrepTimes.removeAtIndex(indexPath.row)
        
    _tableView.reloadData()
}
在UITableViewDataSource協定裡有這樣方法,當使用者滑過某一列時,UITableView就會去檢查這個方法有沒有被實作,如果已有被實作就會自動顯示出Delete的按鈕,接著只要將資料移除再重新載入資料即可。

【執行結果】

    

【專案範例】

2014年7月1日 星期二

UITableView programmatically using Swift language(Prototype Cell)

【說明】

此份筆記是根據ISBN:978-986-201-900-9這本書的第4~5章節所紀錄的。

使用Prototype Cell呈現TableView的內容。

首先建立一個只有Single View Controller的專案,將自動產生的View Controller刪除,並拖曳Table View Controller進入Storyboard,如下圖所示。

手動拖曳進Storyboard的Table View Controller
將Table View Controller的Custom Class設為CustomTableViewController,如下圖所示。

設定Table View Controller與CustomTableViewController的關聯
將Table View Cell的Style設為Custom,如下圖所示。

將Table View Cell的Style設為Custom
將Table View的Row Height設為71,如下圖所示。

將Table View的Row Height設為71
將Table View Cell的Row Height設為71,並將Custom打勾,如下圖所示。

將Table View Cell的Row Height設為71,並將Custom打勾
拖曳一個Image View至Table View Cell內,並設定X:20、Y:7、Width與Height:58,如下圖所示。

設定Image View的位置與大小
拖曳一個Label至Table View Cell內,並設定X:92、Y:7、Width:215、Height:23,名稱為RecipeName,字型的樣式改為Helvetica Neue Condensed Bold,大小為21,如下圖所示。

設定Label的位置與大小
設定Label的名稱、自行樣式與大小
拖曳一個Label至Table View Cell內,並設定X:92、Y:32、Width:186、Height:33,名稱為PrepTime,如下圖所示。

設定Label的位置與大小
完成的Prototype Cell,如下圖所示。

自訂的Prototype Cell
為自訂的Cell建立一個類別,並宣告一個UIImageView與兩個UILabel的IBOutlet,如下圖所示。

新建自訂的Class
將Prototype Cell的Class設為CustomTableCell,如下圖所示。

設定Prototype Cell的Class
設定Prototype Cell得Identifier為CustomTableCell,如下圖所示。

設定Prototype Cell的Identifier
建立類別屬性及介面中UIImageView與UILabel的連接,如下圖所示,僅以一個Label作為代表。

類別屬性與介面做連接

【片段程式碼】

tableView.contentInset = UIEdgeInsetsMake(20, tableView.contentInset.left, tableView.contentInset.bottom, tableView.contentInset.right)
當Cell View超出至Status bar時可以使用此行程式將View往下調整。

override func tableView(_tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
    return recipesNames.count

}
TableVeiw必須實作的函式,告訴ViewController要顯示多少個Cell。

override func tableView(_tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {

}
這個函式也是必須實作的,告訴ViewController每一列要顯示什麼。

var cellIdentifier: String = "CustomTableCell"
var cell = _tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? CustomTableCell
if cell == nil {
    cell = CustomTableCell(style: .Default, reuseIdentifier: cellIdentifier)

}
這幾段程式是在cellForRowAtIndexPath函式發面,要判別Cell是否已經建立過了,因為TableView受螢幕的限制,一次顯示的Cell不多,所以不可能每一筆資料都建立一個UITableViewCell的物件,而是改變Cell中的Image和Label的文字而已,如此一來可以提升程式的效率。透過dequeueReusableCellWithIdentifier去檢查是否已建立,若回傳Null則新建一個Cell。

override func tableView(_tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
}
這個函式是當Cell被按下時會觸發的,與Button的概念相似。

_tableView.deselectRowAtIndexPath(indexPath, animated: true)
當Cell被按下時會以反灰顯示,若要點完後回復原來的樣子可以使用deselectRowAtIndexPath來恢復。

【完整程式碼】

CustomTableViewController.swift:
//
//  ViewController.swift
//  CustomTable
//
//  Created by Hsu,Yi-Sheng on 2014/7/1.
//  Copyright (c) 2014年 yisheng. All rights reserved.
//

import UIKit

class CustomTableViewController: UITableViewController {
                            
    //Data的初始設定
    var recipesNames: String[] = ["Egg Benedict", "Mushroom Risotto", "Full Breakfast", "Hamburger", "Ham and Egg Sandwich", "Creme Brelee", "White Chocolate Donut", "Starbucks Coffee", "Vegetable Curry", "Instant Noodle with Egg", "Noodle with BBQ Pork", "Japanese Noodle with Pork", "Green Tea", "Thai Shrimp Cake", "Angry Birds Cake", "Ham and Cheese Panini"]
    
    var recipesImages: String[] = ["egg_benedict.jpg", "mushroom_risotto.jpg", "full_breakfast.jpg", "hamburger.jpg", "ham_and_egg_sandwich.jpg", "creme_brelee.jpg", "white_chocolate_donut.jpg", "starbucks_coffee.jpg", "vegetable_curry.jpg", "instant_noodle_with_egg.jpg", "noodle_with_bbq_pork.jpg", "japanese_noodle_with_pork.jpg", "green_tea.jpg", "thai_shrimp_cake.jpg", "angry_birds_cake.jpg", "ham_and_cheese_panini.jpg"]
    
    var recipePrepTimes: String[] = ["30 min", "30 min", "20 min", "30 min", "10 min", "1 hour", "45 min", "5 min", "30 min", "8 min", "20 min", "20 min", "5 min", "1.5 hour", "4 hours", "10 min"]
    
    var recipeChecked: Bool[] = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        //當cell超過status bar時可以利用此行程式將顯示的地方縮小。
        tableView.contentInset = UIEdgeInsetsMake(20, tableView.contentInset.left, tableView.contentInset.bottom, tableView.contentInset.right)
    }
    
    //設定有幾行
    override func tableView(_tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
        return recipesNames.count
    }
    
    //設定每行的顯示
    override func tableView(_tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
        
        var cellIdentifier: String = "CustomTableCell"
        var cell = _tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? CustomTableCell
        if cell == nil {
            cell = CustomTableCell(style: .Default, reuseIdentifier: cellIdentifier)
        }
        
        cell!.thumbnailImageView.image = UIImage(named: recipesImages[indexPath.row])
        cell!.nameLabel.text = recipesNames[indexPath.row]
        cell!.prepTimeLabel.text = recipePrepTimes[indexPath.row]
        
        if recipeChecked[indexPath.row] {
            cell!.accessoryType = .Checkmark
        } else {
            cell!.accessoryType = .None
        }
        
        return cell
    }
    
    override func tableView(_tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
        
        var alertView = UIAlertController(title: recipesNames[indexPath.row], message: recipePrepTimes[indexPath.row], preferredStyle: .Alert)
        alertView.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
        self.presentViewController(alertView, animated: true, completion: nil)
        
        var cell: UITableViewCell = _tableView.cellForRowAtIndexPath(indexPath)
        cell.accessoryType = .Checkmark
        
        if (recipeChecked[indexPath.row]) {
            recipeChecked[indexPath.row] = false
            cell.accessoryType = .None
        } else {
            recipeChecked[indexPath.row] = true
            cell.accessoryType = .Checkmark
        }
        
        _tableView.deselectRowAtIndexPath(indexPath, animated: true)
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

CustomTableCell.swift:
//
//  CustomTableCell.swift
//  CustomTable
//
//  Created by Hsu,Yi-Sheng on 2014/7/1.
//  Copyright (c) 2014年 yisheng. All rights reserved.
//

import UIKit

class CustomTableCell: UITableViewCell {
    
    @IBOutlet var nameLabel: UILabel
    @IBOutlet var prepTimeLabel: UILabel
    @IBOutlet var thumbnailImageView: UIImageView
    

}

【執行結果】

        

【專案範例】