我的位置APP(MyLocations App)
在本次课程中将介绍来自iOS SDK中的更加诱人的技术,并且你将掌握更加高级的Swift技能。
你将要制作一个“我的位置”app,它将使用Core Location框架来获取GPS坐标来定位用户的位置,你还要将用户常去的地点用Map Kit展示出来,还可以使用户通过iPhone的摄像头拍摄上传照片,或者从照片库里获取照片上传到app,最后,你要用Core Data将所有东西存储到数据库中。听起来工作好多啊。
app完成后将会是下面这个样子:
我的位置app可以让你将你觉得有趣的风景拍照,保存下来,以列表的形式展现出来。当你带着iPhone去旅游的时候,你可以通过点击“Get My Location(获取我的位置)”按钮来启动GPS功能,并且得到相应的街道名称。然后你可以再拍一张照片,并且添加一些描述,将这条信息保存到我的收藏中,以便将来拿出来回忆。把这个app想象为一个旅游相册吧!
和往常一样,为了能正确的掌握工作量,你要讲这个大工程分解为许多小块:
1、首先你要指出如何通过Core Location框架获取GPS坐标,以及如何将坐标转换为街道信息,这就叫做坐标转换(reverse geocoding)。使用Core Location可以轻易实现这一点,但是对于那些无法直接转换的地址,我们就要用点手段了。
2、一旦你成功获取了坐标,你就要创建一个Tag Location(标注位置)界面,让用户输入一些关于这个地点的描述。这个界面是一个table view controller,并且将使用静态cell,和上一个课程的内容非常相似。
3、然后你要将这个位置信息存储到Core Date中。之前的课程你是将数据保存到了plist文件中,对于简单的小型app是没问题的,但是对于比较复杂的app,专业的手段就是使用Core Data。它并没有听上去那么可怕,不用担心。
4、紧接着,你要使用Map Kit框架将这个位置展示在地图上。
5、Tag Location界面上应该有一个Add Photo(新增照片)的按钮,你可以通过这个按钮连接到iPhone的摄像头或者照片库,进行拍照或者选择照片。
6、最后,给整个app来个大整容。同时添加一些音效和动画效果来充实app。
在你得到上述功能前,我们要先讲一些理论知识。你需要先熟悉一下Swift语言和面向对象编程概念。
复习一下之前遇到的Swift语法
在之前的课程中,我已经给你展示了一些Swift的语法,但是那只是Swift的一小部分。之前我或多或少的对那些代码进行了一点讲解和标注,是你不会完全陷入迷茫。
这样做对之前的课程是没问题的,通过一些简单的讲解可以使你勉强跟上思路,但是现在,我们要仔细的讲一下这些内容了。
首先,我们来复习一下目前遇到过的东西。
变量、常量和类型
一个变量是一个指定类型的值的临时容器:
var count: Int
var shouldRemind: Bool
var text: String
var list: [ChecklistItem]
数据类型,或者简称为类型,用来决定变量可以存储那种类型的值。有些变量仅存储一些简单的类型,比如Int(整型)、Bool(布尔型),而有些则存储复杂一些的,比如String(字符串)、Array(数组)。
到目前为止,你用过的基础类型为:Int,用于存放整数;Float,用于存放小数,以及Bool,用于存放逻辑值(true和false)。
还有一些基础类型是目前为止你没有见过的:
Double,和Float类似,只是精度更高。在本次课程中你讲会使用Double来存储一个地理位置的经度和纬度。
Character,仅仅存放一个字符。可以把String当作是Character的集合。
UInt,Int的一个变形,你偶尔会遇到。U是代表“unsigned(无符号的)”,这种类型只能存储正整数。它被称作是无符号的就是因为不能在数组前放置(-)负号。UInt的存储范围是0到18万亿,不包含负数。
Int8,Uint8,Int16,UInt16,Int32,UInt32,Int64,UInt64。它们都是Int家族中的成员。它们的区别在于能够存储的字节数不同。字节越多,可以存储的值也越大。实际上你很少会遇到他们,一般都是用Int,默认使用8个字节,可以存储正整数和负整数,其中最大的数为19位数。这已经是个天文数字了。
CGFloat,这并不是一个Swift类型,而是iOS SDK定义的类型。它和Float以及 Double类似。由于历史原因,它的使用贯穿UIKit。(CG前缀代表Core Graphics框架)
Swift对类型的要求非常严格,比其他语言要严格的多。如果变量的类型是Int,你就不能将Float型的值放进去。反过来也不行。(有些语言可以把Int放入Float)
即使Int和Float都是存放数字,Swift也不会自动对它们进行转换,你需要手动执行类型转换。
例如:
var i = 10
var f: Float
f = i //❌
f = Float(i) //✅
当你创建一个变量时,你并不需要总是指定它的类型。如果你给一个变量初始值的话,Swift会进行类型推断,自己判定它的类型:
var i = 10 //Int
var d = 3.14 //Double
var b = true //Bool
var s = "Hello, world" //String
Swift会根据初始值自动判定类型。
注意一下,对于小数3.14,Swift会将其的类型判定为Double,而不是Float。如果你要使用Float,那么你必须指明类型:
var f: Float = 3.14
上面的: Float,叫做类型注释。使用了类型注释的变量,Swift不再对其进行类型判定。
同样的,如果你想创建一个名称为i的Doubel型变量,你可以这样写:
var i: Double = 10
或者:
var i = 10.0
上面的10,3.14以及“Hello World”称作字面值,它们在创建基础类型(Int,Double,String等)时非常有用。但是对于复杂的类型,你就需要先实例化一个对象。
当你写出下面的语句时,
var item: ChecklistItem
仅仅是通知了Swift,你想要存储一个ChecklistItem对象到item变量中,但是此时不会创建ChecklistItem对象,为此你又写了一句:
item = ChecklistItem()
这是一个预留内存,用来保存对象的数据,在调用init()后将对象准备好,随时可以使用。预留内存也称为资源配置,然后使用对象的初始值填满它就叫做初始化。
这整个过程就叫做对象的实例化,你创建了一个对象的实例。实例就是一块保存着对象中的变量的值的内存。(这就是为什么对象中的变量被称为实例变量,get到了吗?)
当然,你可以将上面的两行合并为一行:
var item = ChecklistItem()
这里你省略了“: ChecklistItem”这一类型注释,因为Swift非常聪明,它可以识别出item的类型就是ChecklistItem。
然而,你并不能把()这对括号省略掉,这对括号的作用就是通知Swift,你要创建一个新的ChecklistItem实例。
有些对象允许你在这对括号内传递参数,例如:
var item = ChecklistItem(text: "Charge my iPhone", checked: false)
这里是调用了相应的init(text,checked)方法来分配新的空间存储ChecklistItem对象。
目前为止,你已经见过了两种类型的变量:局部变量(local variables),生命期仅在方法调用期间;实例变量,属于整个对象,因此可以在任何方法内被调用。
变量的生命期也称为变量的作用范围。局部变量的作用范围比实例变量小的多。一旦方法调用结束,局部变量就被释放掉了。
class MyObject {
var count = 0 // 一个实例变量
func myMethod() {
var temp: Int // 一个局部变量
temp = count // 方法内调用了实例变量
// 局部变量temp在方法外不存在
}
}
当实例变量与局部变量重名时,局部变量会覆盖实例变量,你需要避免这种情况的发生,因为这样可能会因为调用错变量而引发bug,比如:
class MyObject {
var count = 7 // 一个实例变量
func myMethod() {
var count = 42 // 局部变量覆盖了实例变量
print(count) // 打印值为42
}
}
有些开发者会在实例变量的名称前面放一个下划线来避免这个情况,比如:_count。另外一个方法就是使用关键字self来调用实例变量。
class MyObject {
var count = 7 // 一个实例变量
func myMethod() {
var count = 42 // 局部变量覆盖了实例变量
print(self.count) // 打印值为 7
}
}
变量不仅仅是存储值而已。一个变量是用来存放值的容器,它可以被正在运行中app改变。
例如:记事本类型的app可以允许用户随意的改变其中的文本,所以你要将这些文本存储到String型变量中,每次用户编辑这些文本的时候,变量的值都会被更新。
通常,你仅仅是想存储一个计算结果或者一个方法的返回值,并且不想再改变它们。这种情况,你最好就不要使用变量,而是使用常量。
以下值在设置好以后,永远不会发生变化:
let pi = 3.141592
let difference = abs(targetValue - currentValue)
let message = "You scored \(points) points"
let image = UIImage(named: "SayCheese")
如果一个常量是属于某个方法的局部常量,那么每次这个方法被调用时,允许给这个常量一个新的值,只是在方法的内部,这个值不允许被改变,因为方法被调用结束后,这个常量就被释放掉了,所以每次调用方法时,这个常量可以被给予一个不同的值。
小贴士:我的建议时,你始终使用let,尽量不要使用var,直到编译器报错,通知你需要修改为var的时候再用var。
基础的变量类型,比如整数和字符串,被称为值类型,使用let创建的常量,仅可以被赋值一次:
let pi = 3.141592
pi = 3 // 不允许这样操作
然而,对象都是引用类型的,使用常量实例化对象时,仅仅是将到这个对象的引用常量化了,而对象本身还是可以发生变化的。
let item = ChecklistItem()
item.text = "Do the laundry"
item.checked = false
item.dueDate = yesterday
但是不允许像下面这样操作:
let anotherItem = ChecklistItem()
item = anotherItem //不允许这样操作
那么你怎么知道哪些东西是值类型,哪些东西是引用类型呢?
使用class关键字定义的都是引用类型,而struct或者enum定义的对象都是值类型。实际上,大多数来自iOS SDK中的对象都是引用类型,而Swift内建的Int,String,Array等都是值类型。
一个变量仅能存储一个值。要存储多个对象的话,需要使用一种叫做集合对象的东西。比如数组(Array)或者字典(Dictionary),这些之前你都见过了。
一个数组存储一列对象。存储的对象在数组中有序的排列着,你可以用过数组的索引访问它们。
// an array of ChecklistItem objects:
var items: Array
// using shorthand notation:
var items: [ChecklistItem]
// making an instance of the array:
items = [ChecklistItem]()
// accessing an object from the array:
let item = items[3]
数组可以写作Array
字典可以存储配对的“键-值”。通常键是String型的,而值是其他类型。
// a dictionary that stores (String, Int) pairs, for example a
// list of people’s names and their ages:
var ages: Dictionary
// using shorthand notation:
var ages: [String: Int]
// making an instance of the dictionary:
ages = [String: Int]()
// accessing an object from the dictionary:
var age = dict["Jony Ive"]
从数组中检索对象的符号和数组非常类似,都是使用[]方括号。但是检索数组中的对象时,使用的是正整数,而字典则使用的是字符串。
还有一些其他类型的集合类型,但是数组和字典是用的最多的。
数组和字典也被称为泛型,这意味这它们独立于你存储到其中的对象的类型。
你可以用一个数组存储Int型对象,String对象,甚至另一个数组。
这就是为什么你在使用是一个数组前,要指明存储到这个数组中的对象的类型,你不能像下面这样存储数组:
var items: Array // error: should be Array
var items: [] // error: should be [TypeName]
(最近了,翻译的进度就慢一些,后面会补上,希望大家耐心等待)