马上着手开发 iOS 应用程序 (七) - 定义数据模型

重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。

翻译自苹果官网:

https://developer.apple.com/library/ios/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson6.html#//apple_ref/doc/uid/TP40015214-CH20-SW1

在本课中,你将为 FoodTracker app 定义一个数据模型。这个数据模型代表 app 的信息结构。

学习目标

在课程的最后,你将能够:

  • 创建一个数据模型
  • 在自定义类中编写可失败构造器
  • 理解可失败和不可失败构造器的区别
  • 通过编写和运行单元测试来测试一个数据模型

创建一个数据模型

现在创建一个数据模型来存储食物场景需要展示的信息。定义一个简单的拥有 name 属性、 photo 属性和 rating 属性的类来表示这个模型。

创建一个数据模型类
  1. 选择 File > New > File (或者按 Command-N)。

  2. 在出现的对话框的左边,选择 iOS 下面的 Source。

  3. 选择 Swift 文件,然后点击 Next。
    你正在使用一种不同与先前创建 RatingControl 类的步骤来创建类 (iOS > Source > Cocoa Touch Class),因为你为你的数据模型定义了一个基类,这意味着它不需要继承任何其他类。

  4. 在 Save As 区域,输入 Meal。

  5. 保存位置默认是你的项目目录。
    Group 选项默认是你的 app 名字,FoodTracker。
    在 Targets 区域,确保 app 和 test 都选中了。

    [图片上传失败...(image-2a95c3-1608214873246)]

  6. 点击 Create。
    Xcode 创建一个叫做 Meal.swift 的文件。

在 Swift 中,使用一个 String 来表示名字,使用一个 UIImage 来表示照片,使用一个 Int 来表示评分。因为食物经常会拥有名字和评分,但是可能没有照片,所以让 UIImage 为可选类型。

为食物定义一个数据模型
  1. 如果辅助编辑器是打开状态,通过点击 Standard 按钮来返回标准编辑器。

    [图片上传失败...(image-204b91-1608214873246)]

  2. 打开 Meal.swift。

  3. 修改 import 语句来引入 UIKit 代替 Foundation:

     import UIKit
    

    默认 Swift 文件引入 Foundation 框架所以可以直接使用其中的数据结构。因为要使用 UIKit 的类,所以需要 import UIKit。导入 UIKit 同时让你能访问 Foundation,所以可以移除多余的 import Foundation。

  4. 在 import 语句的下面,添加如下代码:

     class Meal {
         // MARK: Properties
         
         var name: String
         var photo: UIImage?
         var rating: Int
     }
    

    代码为需要储存的数据定义了基础属性。使用变量(var)而不是常量(let)因为它们在食物对象的生命周期过程中需要修改。

  5. 在属性的下面,添加代码来定义一个构造器:

     // MARK: Initialization
    
     init(name: String, photo: UIImage?, rating: Int) {
     }
    

    回忆一下构造器是准备一个类的实例的方法,它为每个属性设置初始值并执行任何其他的初始化和构造过程。

  6. 设置属性等于参数值。

     // Initialize stored properties.
     self.name = name
     self.photo = photo
     self.rating = rating
    

    但是当你尝试创建一个不正确的食物会发生什么,例如一个空的名字或者一个负的评分?你需要添加代码来检查这些情况并返回 nil 来表示构造失败了。

  7. 在构造器的最后,添加 if 语句来检查不正确的值并且当一个条件不满足就返回 nil。

     // Initialization should fail if there is no name or if the rating is negative.
     if name.isEmpty || rating < 0 {
         return nil
     }
    
  8. 点击错误 fix-it 来添加一个问号(?)到构造器的 init 关键字后面。

    [图片上传失败...(image-52b767-1608214873246)]

    这被称为可失败构造器,这意味着构造器有可能返回 nil 值。

此刻,init?(name:photo:rating:) 构造器应该像下面这样:

// MARK: Initialization
 
init?(name: String, photo: UIImage?, rating: Int) {
    // Initialize stored properties.
    self.name = name
    self.photo = photo
    self.rating = rating
    
    // Initialization should fail if there is no name or if the rating is negative.
    if name.isEmpty || rating < 0 {
        return nil
    }
}

检验:通过选择 Product > Build(或者按 Command-B)来编译项目,不要使用你新创建的类做任何事情,只是编译它给编译器一次机会来验证你没有犯任何的输入错误,像问号(?)。如果你犯错了,通过阅读编译器提供的警告和错误来解决问题,之后回顾过去课程的说明来确保一切都看起来像这里描绘的样子。

测试你的数据

尽管你的数据模型代码完成了,但你并没有把它结合到你的 app 中。所以很难说明是否已经正确实现一切了,同时可能在运行时遇到并没有考虑到的临界情况。

可以编写单元测试来定位这些不确定因素。它用于测试少量且独立的代码来确保它们功能正确。食物类是个完美的单元测试用例。

Xcode 已经创建一个单元测试文件作为 Single View Application 模板的一部分。

为 FoodTracker 查看单元测试文件
  1. 通过点击项目导航中 FoodTrackerTests 文件夹旁边的三角形打开它。

    [图片上传失败...(image-c70d3a-1608214873246)]

  2. 打开 FoodTrackerTests.swift。

花一点时间理解目前的代码;

class FoodTrackerTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    
    func testExample() {
        // This is an example of a functional test case.
        XCTAssert(true, "Pass")
    }
    
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measureBlock() {
            // Put the code you want to measure the time of here.
        }
    }
    
}

这个文件引入了 XCTest 框架。单元测试定义在一个叫 FoodTrackTests 的类中,它继承自 XCTestCase。代码注释解释了 setUp() 和 tearDown() 方法。

你可以写的主要类型的测试是功能测试(为了检查一切正在产生你期望的值)和性能测试(为了检查你的代码执行的是否像你预期的那么快)。因为你没有编写任何很影响性能的代码,你现在仅仅需要编写功能测试方法。

使用 test 作为标题开始任一你想要用作测试的方法,给它一个特殊的标题这样之后就更容易识别了,例如,一个测试方法或许检查食物是否得到正确的初始化,你可以把它命名为 testMealInitialization。

为食物对象构造过程编写单元测试
  1. 删除 FoodTrackerTests.swift 中模板创建的测试方法。

     import UIKit
     import XCTest
      
     class FoodTrackerTests: XCTestCase {
         
     }
    
  2. 在最后的大括号(})前面,添加如下注释:

     // MARK: FoodTracker Tests
    

    它帮助你(或其他任何读你的代码的人)在测试方法中间导航和标识所对应的模块。

  3. 在注释下面,添加一个新的单元测试方法:

     // Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
     func testMealInitialization() {
     }
    
  4. 首先,添加一个能通过的测试用例。向 testMealInitialization() 测试方法中添加如下注释和代码行:

     // Success case.
     let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
     XCTAssertNotNil(potentialItem)
    

    XCAssertNotNil 测试食物对象在初始化后不为空,这意味着使用提供的参数成功地初始化并创建了一个食物对象。

  5. 现在添加一个食物对象初始化失败的测试用例。向 testMealInitialization() 测试方法添加如下注释和代码行。

     // Failure cases.
     let noName = Meal(name: "", photo: nil, rating: 0)
     XCTAssertNil(noName, "Empty name is invalid")
    

    XCTAssertNil 断言一个对象是 nil。在这个例子中,意味着 noName 对象是 nil,说明构造失败。你预期构造失败因为名字是空的字符串,这是你明确设置用于测试的构造器。

  6. 现在添加一个食物对象初始化失败的测试用例,但是这次,尝试断言初始化成功。向 testMealInitialization() 测试方法添加如下代码:

     let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
     XCTAssertNotNil(badRating)
    

    你预期这个测试用例会失败因为评分是负的,这是你在构造方法中明确设置用于测试的。

testMealInitialization() 单元测试方法应该像这样:

// Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
func testMealInitialization() {
    // Success case.
    let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
    XCTAssertNotNil(potentialItem)
    
    // Failure cases.
    let noName = Meal(name: "", photo: nil, rating: 0)
    XCTAssertNil(noName, "Empty name is invalid")
    
    let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
    XCTAssertNotNil(badRating)
}

你可以按 Command-U 同时运行你的所有单元测试方法,或者你可以运行一个单独的测试方法。最后的测试用例预期会失败因为尽管实际上是 nil 但你断言对象是非空的。

运行 testMealInitialization() 单元测试方法
  1. 找到 FoodTrackerTests.swift 中的 testMealInitialization() 单元测试方法。

  2. 在测试方法名字左边,找到一个菱形形状。

    [图片上传失败...(image-93d313-1608214873246)]

  3. 悬停你的鼠标在菱形上面会露出一个小的 Run 按钮。

    [图片上传失败...(image-5dbe27-1608214873246)]

  4. 点击 Run 按钮来运行单元测试方法。

检验:运行刚才写的单元测试方法。前两个测试用例应该能通过,最后一个应该失败。

[图片上传失败...(image-ba12eb-1608214873246)]

你会看到,单元测试帮助在代码中捕捉错误。如果你实际上期望最后测试用例中的食物对象非空,你在测试中不会捕捉到这个错误。(在这个例子中,因为你故意编写一个失败的测试用例,你仅仅只需返回你的代码来修复你的测试用例。)

修复测试用例
  1. 找到 FoodTrackerTests.swift 中的 testMealInitialization() 单元测试方法。

  2. 修改最后一行为这个:

     XCTAssertNil(badRating, "Negative ratings are invalid, be positive")
    

testMealInitialization() 单元测试方法应该像这样:

// Tests to confirm that the Meal initializer returns when no name or a negative rating is provided.
func testMealInitialization() {
    // Success case.
    let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
    XCTAssertNotNil(potentialItem)
    
    // Failure cases.
    let noName = Meal(name: "", photo: nil, rating: 0)
    XCTAssertNil(noName, "Empty name is invalid")
    
    let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
    XCTAssertNil(badRating, "Negative ratings are invalid, be positive")
}

检验:运行刚才写的单元测试方法。所有的测试用例应该能通过。

[图片上传失败...(image-78e1b3-1608214873247)]

单元测试是编写代码中一个必要的环节因为它能帮助你捕捉一些你或许会忽略的错误。就像它们的名字所表达的,保持单元测试模块化非常重要。每个测试应该检查一个特别而且基础的一类行为。如果你编写的单元测试非常长或者复杂,这会很难追踪到底发生了什么的。

注意:

为了看到本课的完整示例项目,下载文件并在 Xcode 中查看它。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容