专题:A Swift Tour

Swift初见

{%}

译者/ 梁杰

网名:numbbbbb,前端工程师,业余喜欢写Python和Swift。组织翻译了苹果官方的《Swift编程语言》Accessibility Programming Guide for iOS,独立翻译了《编程导论》,合译了《Python语言及其应用》《你不知道的JavaScript(上卷)》。除了编程和翻译,梁杰还维护着SwiftGG翻译组和Swift开发者周刊。他正在组织筹备“中国首届Swift开发者大会”,希望通过自己的努力为技术发展作出贡献。

译者/ xtymichael 2.0版译者。

{%}

作者/ Apple Developer

通常来说,编程语言教程中的第一个程序应该在屏幕上打印“Hello, world”。在 Swift 中,可以用一行代码实现:

print("Hello, world!")

如果你写过 C 或者 Objective-C 代码,那你应该很熟悉这种形式——在 Swift 中,这行代码就是一个完整的程序。你不需要为了输入输出或者字符串处理导入一个单独的库。全局作用域中的代码会被自动当做程序的入口点,所以你也不需要main()函数。你同样不需要在每个语句结尾写上分号。

这个教程会通过一系列编程例子来让你对 Swift 有初步了解。

注意

为了获得最好的体验,在 Xcode 当中使用代码预览功能。代码预览功能可以让你编辑代码并实时看到运行结果。

下载Playground

简单值

使用let来声明常量,使用var来声明变量。一个常量的值,在编译的时候,并不需要有明确的值,但是你只能为它赋值一次。也就是说你可以用常量来表示这样一个值:你只需要决定一次,但是需要使用很多次。

var myVariable = 42
myVariable = 50
let myConstant = 42

常量或者变量的类型必须和你赋给它们的值一样。然而,你不用明确地声明类型,声明的同时赋值的话,编译器会自动推断类型。在上面的例子中,编译器推断出myVariable是一个整数(integer)因为它的初始值是整数。

如果初始值没有提供足够的信息(或者没有初始值),那你需要在变量后面声明类型,用冒号分割。

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70

练习

创建一个常量,显式指定类型为Float并指定初始值为4。

值永远不会被隐式转换为其他类型。如果你需要把一个值转换成其他类型,请显式转换。

let label = "The width is"
let width = 94
let widthLabel = label + String(width)

练习

删除最后一行中的String,错误提示是什么?

有一种更简单的把值转换成字符串的方法:把值写到括号中,并且在括号之前写一个反斜杠。例如:

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

练习

使用\()来把一个浮点计算转换成字符串,并加上某人的名字,和他打个招呼。

使用方括号[]来创建数组和字典,并使用下标或者键(key)来访问元素。最后一个元素后面允许有个逗号。

var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"

var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"

要创建一个空数组或者字典,使用初始化语法。

let emptyArray = [String]()
let emptyDictionary = [String: Float]()

如果类型信息可以被推断出来,你可以用[][:]来创建空数组和空字典——就像你声明变量或者给函数传参数的时候一样。

shoppingList = []
occupations = [:]

控制流

使用ifswitch来进行条件操作,使用for-inforwhilerepeat-while来进行循环。包裹条件和循环变量括号可以省略,但是语句体的大括号是必须的。

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print(teamScore)

if语句中,条件必须是一个布尔表达式——这意味着像if score { ... }这样的代码将报错,而不会隐形地与 0 做对比。

你可以一起使用iflet来处理值缺失的情况。这些值可由可选值来代表。一个可选的值是一个具体的值或者是nil以表示值缺失。在类型后面加一个问号来标记这个变量的值是可选的。

var optionalString: String? = "Hello"
print(optionalString == nil)

var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
    greeting = "Hello, \(name)"
}

练习

optionalName改成nil,greeting会是什么?添加一个else语句,当optionalNamenil时给greeting赋一个不同的值。

如果变量的可选值是nil,条件会判断为false,大括号中的代码会被跳过。如果不是nil,会将值赋给let后面的常量,这样代码块中就可以使用这个值了。

另一种处理可选值的方法是通过使用 ?? 操作符来提供一个默认值。如果可选值缺失的话,可以使用默认值来代替。

let nickName: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fullName)"

switch支持任意类型的数据以及各种比较操作——不仅仅是整数以及测试相等。

let vegetable = "red pepper"
switch vegetable {
case "celery":
    print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
    print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
    print("Is it a spicy \(x)?")
default:
    print("Everything tastes good in soup.")
}

练习

删除default语句,看看会有什么错误?

注意let在上述例子的等式中是如何使用的,它将匹配等式的值赋给常量x

运行switch中匹配到的子句之后,程序会退出switch语句,并不会继续向下运行,所以不需要在每个子句结尾写break

你可以使用for-in来遍历字典,需要两个变量来表示每个键值对。字典是一个无序的集合,所以他们的键和值以任意顺序迭代结束。

let interestingNumbers = [
    "Prime": [2, 3, 5, 7, 11, 13],
    "Fibonacci": [1, 1, 2, 3, 5, 8],
    "Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (kind, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print(largest)

练习

添加另一个变量来记录现在和之前最大数字的类型。

使用while来重复运行一段代码直到不满足条件。循环条件也可以在结尾,保证能至少循环一次。

var n = 2
while n < 100 {
    n = n * 2
}
print(n)

var m = 2
repeat {
    m = m * 2
} while m < 100
print(m)

你可以在循环中使用..<来表示范围,也可以使用传统的写法,两者是等价的:

var firstForLoop = 0
for i in 0..<4 {
    firstForLoop += i
}
print(firstForLoop)

var secondForLoop = 0
for var i = 0; i < 4; ++i {
    secondForLoop += i
}
print(secondForLoop)

使用..<创建的范围不包含上界,如果想包含的话需要使用...

函数和闭包

使用func来声明一个函数,使用名字和参数来调用函数。使用->来指定函数返回值的类型。

func greet(name: String, day: String) -> String {
    return "Hello \(name), today is \(day)."
}
greet("Bob", day: "Tuesday")

练习

删除day参数,添加一个参数来表示今天吃了什么午饭。

使用元组来让一个函数返回多个值。该元组的元素可以用名称或数字来表示。

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
    var min = scores[0]
    var max = scores[0]
    var sum = 0

    for score in scores {
        if score > max {
            max = score
        } else if score < min {
            min = score
        }
        sum += score
    }

    return (min, max, sum)
}
let statistics = calculateStatistics([5, 3, 100, 3, 9])
print(statistics.sum)
print(statistics.2)

函数可以带有可变个数的参数,这些参数在函数内表现为数组的形式:

func sumOf(numbers: Int...) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}
sumOf()
sumOf(42, 597, 12)

练习

写一个计算参数平均值的函数。

函数可以嵌套。被嵌套的函数可以访问外侧函数的变量,你可以使用嵌套函数来重构一个太长或者太复杂的函数。

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
returnFifteen()

函数是第一等类型,这意味着函数可以作为另一个函数的返回值。

func makeIncrementer() -> (Int -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
increment(7)

函数也可以当做参数传入另一个函数。

func hasAnyMatches(list: [Int], condition: Int -> Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [20, 19, 7, 12]
hasAnyMatches(numbers, condition: lessThanTen)

函数实际上是一种特殊的闭包:它是一段能之后被调取的代码。闭包中的代码能访问闭包所建作用域中能得到的变量和函数,即使闭包是在一个不同的作用域被执行的 – 你已经在嵌套函数例子中所看到。你可以使用{}来创建一个匿名闭包。使用in将参数和返回值类型声明与闭包函数体进行分离。

numbers.map({
    (number: Int) -> Int in
    let result = 3 * number
    return result
})

练习

重写闭包,对所有奇数返回0。

有很多种创建更简洁的闭包的方法。如果一个闭包的类型已知,比如作为一个回调函数,你可以忽略参数的类型和返回值。单个语句闭包会把它语句的值当做结果返回。

let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)

你可以通过参数位置而不是参数名字来引用参数——这个方法在非常短的闭包中非常有用。当一个闭包作为最后一个参数传给一个函数的时候,它可以直接跟在括号后面。当一个闭包是传给函数的唯一参数,你可以完全忽略括号。

let sortedNumbers = numbers.sort { $0 > $1 }
print(sortedNumbers)

对象和类

使用class和类名来创建一个类。类中属性的声明和常量、变量声明一样,唯一的区别就是它们的上下文是类。同样,方法和函数声明也一样。

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

练习

使用let添加一个常量属性,再添加一个接收一个参数的方法。

要创建一个类的实例,在类名后面加上括号。使用点语法来访问实例的属性和方法。

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

这个版本的Shape类缺少了一些重要的东西:一个构造函数来初始化类实例。使用init来创建一个构造器。

class NamedShape {
    var numberOfSides: Int = 0
    var name: String

    init(name: String) {
        self.name = name
    }

    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

注意self被用来区别实例变量。当你创建实例的时候,像传入函数参数一样给类传入构造器的参数。每个属性都需要赋值——无论是通过声明(就像numberOfSides)还是通过构造器(就像name)。

如果你需要在删除对象之前进行一些清理工作,使用deinit创建一个析构函数。

子类的定义方法是在它们的类名后面加上父类的名字,用冒号分割。创建类的时候并不需要一个标准的根类,所以你可以忽略父类。

子类如果要重写父类的方法的话,需要用override标记——如果没有添加override就重写父类方法的话编译器会报错。编译器同样会检测override标记的方法是否确实在父类中。

class Square: NamedShape {
    var sideLength: Double

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 4
    }

    func area() ->  Double {
        return sideLength * sideLength
    }

    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

练习

创建NamedShape的另一个子类Circle,构造器接收两个参数,一个是半径一个是名称,在子类Circle中实现area()simpleDescription()方法。

除了储存简单的属性之外,属性可以有 getter 和 setter 。

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }

    var perimeter: Double {
        get {
            return 3.0 * sideLength
        }
        set {
            sideLength = newValue / 3.0
        }
    }

    override func simpleDescription() -> String {
        return "An equilateral triagle with sides of length \(sideLength)."
    }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)

perimeter的 setter 中,新值的名字是newValue。你可以在set之后显式的设置一个名字。

注意EquilateralTriangle类的构造器执行了三步:

1. 设置子类声明的属性值

2. 调用父类的构造器

3. 改变父类定义的属性值。其他的工作比如调用方法、getters和setters也可以在这个阶段完成。

如果你不需要计算属性,但是仍然需要在设置一个新值之前或者之后运行代码,使用willSetdidSet

比如,下面的类确保三角形的边长总是和正方形的边长相同。

class TriangleAndSquare {
    var triangle: EquilateralTriangle {
        willSet {
            square.sideLength = newValue.sideLength
        }
    }
    var square: Square {
        willSet {
            triangle.sideLength = newValue.sideLength
        }
    }
    init(size: Double, name: String) {
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size,     name: name)
    }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)

处理变量的可选值时,你可以在操作(比如方法、属性和子脚本)之前加?。如果?之前的值是nil?后面的东西都会被忽略,并且整个表达式返回nil。否则,?之后的东西都会被运行。在这两种情况下,整个表达式的值也是一个可选值。

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

枚举和结构体

使用enum来创建一个枚举。就像类和其他所有命名类型一样,枚举可以包含方法。

enum Rank: Int {
    case Ace = 1
    case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
    case Jack, Queen, King
    func simpleDescription() -> String {
        switch self {
        case .Ace:
            return "ace"
        case .Jack:
            return "jack"
        case .Queen:
            return "queen"
        case .King:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}
let ace = Rank.Ace
let aceRawValue = ace.rawValue

练习

写一个函数,通过比较它们的原始值来比较两个Rank值。

在上面的例子中,枚举原始值的类型是Int,所以你只需要设置第一个原始值。剩下的原始值会按照顺序赋值。你也可以使用字符串或者浮点数作为枚举的原始值。使用rawValue属性来访问一个枚举成员的原始值。

使用init?(rawValue:)初始化构造器在原始值和枚举值之间进行转换。

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}

枚举的成员值是实际值,并不是原始值的另一种表达方法。实际上,以防原始值没有意义,你不需要设置。

enum Suit {
    case Spades, Hearts, Diamonds, Clubs
    func simpleDescription() -> String {
        switch self {
        case .Spades:
            return "spades"
        case .Hearts:
            return "hearts"
        case .Diamonds:
            return "diamonds"
        case .Clubs:
            return "clubs"
        }
    }
}
let hearts = Suit.Hearts
let heartsDescription = hearts.simpleDescription()

练习

Suit添加一个color()方法,对spadesclubs返回“black”,对heartsdiamonds返回“red”。

注意,有两种方式可以引用Hearts成员:给hearts常量赋值时,枚举成员Suit.Hearts需要用全名来引用,因为常量没有显式指定类型。在switch里,枚举成员使用缩写.Hearts来引用,因为self的值已经知道是一个suit。已知变量类型的情况下你可以使用缩写。

使用struct来创建一个结构体。结构体和类有很多相同的地方,比如方法和构造器。它们之间最大的一个区别就是结构体是传值,类是传引用。

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}
let threeOfSpades = Card(rank: .Three, suit: .Spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()

练习

Card添加一个方法,创建一副完整的扑克牌并把每张牌的 rank 和 suit 对应起来。

一个枚举成员的实例可以有实例值。相同枚举成员的实例可以有不同的值。创建实例的时候传入值即可。实例值和原始值是不同的:枚举成员的原始值对于所有实例都是相同的,而且你是在定义枚举的时候设置原始值。

例如,考虑从服务器获取日出和日落的时间。服务器会返回正常结果或者错误信息。

enum ServerResponse {
    case Result(String, String)
    case Error(String)
}

let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Error("Out of cheese.")

switch success {
case let .Result(sunrise, sunset):
    let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
case let .Error(error):
    let serverResponse = "Failure...  \(error)"
}

练习

ServerResponseswitch添加第三种情况。

注意如何从ServerResponse中提取日升和日落时间并用得到的值用来和switch的情况作比较。

协议和扩展

使用protocol来声明一个协议。

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

类、枚举和结构体都可以实现协议。

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription

练习

写一个实现这个协议的枚举。

注意声明SimpleStructure时候mutating关键字用来标记一个会修改结构体的方法。SimpleClass的声明不需要标记任何方法,因为类中的方法通常可以修改类属性(类的性质)。

使用extension来为现有的类型添加功能,比如新的方法和计算属性。你可以使用扩展在别处修改定义,甚至是从外部库或者框架引入的一个类型,使得这个类型遵循某个协议。

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}
print(7.simpleDescription)

练习

Double类型写一个扩展,添加absoluteValue功能。

你可以像使用其他命名类型一样使用协议名——例如,创建一个有不同类型但是都实现一个协议的对象集合。当你处理类型是协议的值时,协议外定义的方法不可用。

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// print(protocolValue.anotherProperty)  // Uncomment to see the error

即使protocolValue变量运行时的类型是simpleClass,编译器会把它的类型当做ExampleProtocol。这表示你不能调用类在它实现的协议之外实现的方法或者属性。

泛型

在尖括号里写一个名字来创建一个泛型函数或者类型。

func repeatItem<Item>(item: Item, numberOfTimes: Int) -> [Item] {
    var result = [Item]()
    for _ in 0..<numberOfTimes {
        result.append(item)
    }
    return result
}
repeatItem("knock", numberOfTimes:4)

你也可以创建泛型函数、方法、类、枚举和结构体。

// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
    case None
    case Some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .None
possibleInteger = .Some(100)

在类型名后面使用where来指定对类型的需求,比如,限定类型实现某一个协议,限定两个类型是相同的,或者限定某个类必须有一个特定的父类。

func anyCommonElements <T: SequenceType, U: SequenceType where T.Generator.Element: Equatable, T.Generator.Element == U.Generator.Element> (lhs: T, _ rhs: U) -> Bool {
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false
}
anyCommonElements([1, 2, 3], [3])

练习

修改anyCommonElements(_:_:)函数来创建一个函数,返回一个数组,内容是两个序列的共有元素。

<T: Equatable><T where T: Equatable>是等价的。

阅读The Swift Programming Language(中文版)原文

阅读The Swift Programming Language(英文版)原文

从Objective-C到Swift

作者/ 关东升

国内知名iOS技术作家,iOS技术顾问,高级培训讲师,移动开发专家。担任51CTO社区iOS技术顾问,精通iOS、Android和Windows Phone及HTML5等移动开发技术,擅长移动平台的应用和游戏类项目开发。除《Swift开发指南》外,还著有《iOS开发指南》《品味移动设计》《iOS网络编程与云端应用最佳实践》《iOS传感器应用开发最佳实践》《iOS多媒体编程技术最佳实践》《iPhone与iPad开发实战——iOS经典应用剖析》等畅销书。

或许,你现在就是一个iOS程序员,你对Objective-C很熟悉,对iOS开发也很熟悉,然而,苹果公司在iOS 8之后推出了Swift语言。那么,如何才能快速地从Objective-C过渡到Swift呢?

在本文中,我们将重点讲解如何从Objective-C过渡到Swift,如何利用现有的Objective-C工程调用Swift代码,以及如何利用Swift工程调用以前写好的Objective-C代码。

选择语言

在苹果公司的Swift语言出现之前,开发iOS或Mac OS X应用主要使用Objective-C语言,此外还可以使用C和C++语言,但是UI部分只能使用Objective-C语言。

Swift语言出现后,iOS程序员有了更多的选择。在苹果社区里,有很多人在讨论Swift语言以及Objective-C语言的未来,人们关注的重点是Swift语言是否能够完全取代Objective-C语言。然而在我看来,苹果公司为了给程序员提供更多的选择,会让这两种语言并存。既然是并存,我们就有4种方式可以选择:

  • 采用纯Swift的改革派方式;

  • 采用纯Objective-C的保守派方式;

  • 采用Swift调用Objective-C的左倾改良派方式;

  • 采用Objective-C调用Swift的右倾改良派方式。

无论是Swift调用Objective-C,还是Objective-C调用Swift,我们都需要做一些工作。

Swift调用Objective-C

Swift调用Objective-C需要一个名为“<工程名>-Bridging-Header.h”的桥接头文件,如图1所示。桥接头文件的作用是为Swift调用Objective-C对象搭建一个桥,它的命名必须是“<工程名>- Bridging-Header.h”,我们需要在桥接头文件中引入Objective-C头文件,而且桥接头文件是需要管理和维护的。

{%}

图1 Swift调用Objective-C与桥接头文件

创建Swift的iOS工程

为了能够更好地介绍混合搭配调用,我们首先创建一个基于Swift的iOS工程。创建一个main.swift文件,注意在文件创建过程中,Xcode会弹出一个是否创建桥接头文件对话框,如图2所示,这里要选择Yes,这样就会在创建swift文件的同时也创建一个桥接头文件,如果我们选择No也可以在以后创建,就是有些麻烦。

{%}

图2 创建桥接头文件

完成上述操作的Xcode界面如图3所示。

{%}

图3 Xcode界面

在Swift工程中添加Objective-C类

我们刚刚创建了Swift的工程,还需要调用其他Objective-C类来实现某些功能,需要添加Objective-C类到Swift工程中。具体过程是,右键选择HelloWorld组,然后选择菜单中的“New File…”弹出新建文件模板对话框,如图4所示,选择“iOS →Source→Cocoa Touch Class”。

{%}

图4 新建文件模板

接着单击“Next”按钮,随即出现如图5所示的界面。在Class中输入“ObjCObject”,在Language中选择Objective-C,其他的选项保持默认值就可以了。

{%}

图5 新建Objective-C类

相关选项设置完成后,单击“Next”按钮,进入保存文件界面,根据提示选择存放文件的位置,然后单击“Create”按钮创建Objective-C类。

调用代码

Objective-C的ObjCObject创建完成后,我们会在Xcode工程中看到新增加的两个文件ObjCObject.h和ObjCObject.m。我们并不打算过多地介绍Objective-C语言,但为了更好地理解Swift与Objective-C的互相调用,我们还是简单地解释一下Objective-C代码。

ObjCObject.h代码如下:

#import <Foundation/Foundation.h>                                         ①

@interface ObjCObject : NSObject                                          

-(NSString*)sayHello🙁NSString*)greeting withName: (NSString*)name;       

@end

ObjCObject.h文件是Objective-C的头文件,我们在这里定义类,声明类的成员变量和方法。第①行代码引入Foundation框架的头文件。第②行代码定义类ObjCObject,它继承自NSObject父类。NSObject类是所有Objective-C的根类。第③行代码声明了实例方法sayHello: withName:,它有两个参数greetingname

ObjCObject.m代码如下:

#import "ObjCObject.h"                                                     ①

@implementation ObjCObject

-(NSString*)sayHello🙁NSString*)greeting withName: (NSString*)name         
{
    NSString *string = [NSStringstringWithFormat:@"Hi,%@ %@.",name,greeting];
    return string;
}

@end

上述代码第①行引入ObjCObject.h头文件,第②行代码定义sayHello: withName:方法。

下面我们再来看看Swift调用文件main.swift的代码:

import Foundation                                                          

var obj : ObjCObject = ObjCObject()                                        
var hello = obj.sayHello("Good morning", withName:"Tony")                  

println(hello)                                                             

上述代码第①行的import Foundation语句是引入Foundation框架,类似于Objective-C的#import <Foundation/Foundation.h>语句。

第②行代码声明并实例化ObjCObject类的实例objObjCObject就是Objective-C里定义的ObjCObject

第③行代码调用ObjCObject类的sayHello: withName:方法。要注意Swift调用时的方法名和参数与Objective-C中该方法的方法名和参数的对应关系,如图6所示。

{%}

图6 Objective-C与Swift调用方法和参数对应关系

第④行代码打印输出结果,输出结果如下:

Hi,Tony Good morning.

要使Swift能够调用Objective-C,还必须在桥接头文件中引入Objective-C头文件。HelloWorld- Bridging-Header.h代码如下:

#import "ObjCObject.h"

很简单,只有一行代码,如果还有其他的Objective-C头文件,都需要在此引入。

这样就实现了在Swift中调用Objective-C代码,我们可以借助于这样的调用充分地利用已有的Objective-C文件,减少重复编写代码,提供工作效率。

至此,整个工程创建完毕。点击左上角的“运行”按钮{%}即可查看运行结果,运行结果输出到日志窗口。

Objective-C调用Swift

如果已经有了一个老的iOS应用,它是使用Objective-C编写的,而它的一些新功能需要采用Swift来编写,这时就可以从Objective-C调用Swift。

Objective-C调用Swift时不需要桥接头文件,而是需要Xcode生成的头文件。这种文件由Xcode生成,不需要我们维护,对于开发人员也是不可见的。如图7所示,它能够将Swift中的类暴露给Objective-C,它的命名是:<工程名>-swift.h。我们需要将该头文件引入到Objective-C文件中,而且Swift中的类需要声明为@objc

{%}

图7 Objective-C调用Swift与Xcode生成头文件

创建Objective-C的iOS工程

为了能够更好地介绍混合搭配调用,我们首先创建一个Objective-C的iOS工程。

启动Xcode 6,然后单击File→New→Project菜单,在打开的Choose a template for your new project界面中选择“iOS→Application→Single View Application”工程模板(如图8所示)。

{%}

图8 选择工程模板

接着单击“Next”按钮,随即出现如图9所示的界面。

{%}

图9 新工程中的选项

在Product Name中输入“HelloWorld”,在Language中选择Objective-C,其他的项目可以结合自己的实际情况输入内容。相关的工程选项设置完成后,单击“Next”按钮。

创建成功后的界面如图10所示,然后选择图10中的5个文件删除,文件删除后工程中的main.m文件会有编译错误,如图11所示,这个错误我们先不用考虑它,我们会在下一节修改main.m文件。

{%}

图10 新建的Objective-C工程

{%}

图11 main.m编译错误

在Objective-C工程中添加Swift类

我们刚刚创建了Objective-C的工程,需要添加Swift类到工程中。具体过程是,右键选择HelloWorld组,选择菜单中的“New File…”弹出新建文件模板对话框。如图12所示,选择iOS X→Source→Cocoa Touch Class。

注意 这里我们并没有选择Swift File,是因为需要创建的Swift类是基于Cocoa Touch框架的。

{%}

图12 新建文件模板

接着单击“Next”按钮,随即出现如图13所示的界面。在Class中输入“SwiftObject”,在Subclass of中选择NSObject,这个选项可以让生成的Swift类继承于NSObject。在Language中选择Swift。

{%}

图13 新建Swift类

相关选项设置完成后,单击“Next”按钮,进入保存文件界面,根据提示选择存放文件的位置,然后单击“Create”按钮创建Swift类。如果工程中没有桥接头文件,在创建过程中,Xcode也会提示我们是否添加桥接头文件,我们需要选择添加。

以上操作成功后在Xcode工程中生成了SwiftObject.swift文件,Xcode界面如图14所示。

{%}

图14 Xcode界面

调用代码

Swift的SwiftObject创建完成后,我们会在Xcode工程中看到新增加的SwiftObject.swift文件。我们在SwiftObject.swift中编写如下代码:

import Foundation                                                         

@objc class SwiftObject: NSObject {                                       

    func sayHello(greeting : String, withName name : String) -> String {  

        var string = "Hi," + name
        string += greeting

        return string
    }
}

上述代码第①行引入了Foundation框架的头文件。第②行代码定义SwiftObject类,SwiftObject类继承自NSObject类。另外,我们在类前面声明为@objc@objc所声明的类能够被Objective-C访问,@objc还可以修饰属性。

第③行代码定义了sayHello方法,它有两个参数,第一个参数不需要指定外部参数名,第二个参数(除了第一个以后所有的参数)需要指定外部参数名,例如withNamename参数的外部参数名。这是为了方便在Objective-C中调用。

下面看Objective-C端的代码,main.m文件代码如下:

#import <Foundation/Foundation.h>
#import "HelloWorld-swift.h"                                             ①

int main(intargc, const char * argv[]) {

    SwiftObject *sobj = [[SwiftObject alloc] init];                      
    NSString *hello = [sobj sayHello:@"Good morning" withName:@"Tony"];  

    NSLog(@"%@",hello);                                                  

    return 0;
}

上述代码第①行引入头文件HelloWorld-swift.h,它是Objective-C调用Swift对象所必需的,它的命名规则是“<工程名>-swift.h”。

第②行代码实例化SwiftObject对象,SwiftObject是Swift中定义的类。第③行代码调用SwiftObjectsayHello方法,它在Objective-C中被调用时的方法和参数命名与SwiftObject的方法和参数之间的对应关系如图15所示。

{%}

图15 Swift与Objective-C调用方法和参数对应关系

第④行代码NSLog(@"%@",hello)用于输出结果,输出结果如下:

2014-07-05 14:25:43.879 HelloWorld[3266:303] Hi,TonyGood morning

这样就实现了在Objective-C中调用Swift代码,我们可以借助于这样的调用充分利用已有的Swift文件,减少重复编写代码,提高工作效率。

{%}

《Swift开发指南(修订版)》通过大量案例全面讲解了Swift语言开发,包含常量与变量、运算符和表达式、数据类型、数据类型转换、字符串类型、语言的元组类型、可选类型、数组、字典、控制语句、函数、闭包、类与结构体、属性、方法、下标、继承、构造器、析构器、扩展、协议、内存管理等,每章均配有同步习题,由浅入深,循序渐进,最后通过iPhone计算器应用的开发带领读者进行了开发实战。本文节选自《Swift开发指南(修订版)》

Boisy Pitre:Objective-C很不错,但它已经是过去时

{%}

Boisy G. Pitre是一位资深苹果开发专家,曾任Siri语音识别技术提供方Nuance公司Mac产品小组的资深软件工程师,参与开发了语音识别软件Dragon Dictate。MacTech杂志“Developer to Developer”月度专栏作家,曾多次在MacTech和CocoaConf等会议上发表技术演讲。现任情绪识别公司Affectiva的移动远景规划师,领导团队致力于将公司的表情分析技术迁移到移动平台。Boisy著有《Swift基础教程》一书。

问:你写作《Swift基础教程》的目的是什么?

当苹果引入Swift时,我就知道这门语言会在几年之内成为计算机语言版图中的重要一块。我的目标是以有趣而轻松的方式向读者们介绍这门新语言,让读者们抢先起步,并在之后的学习中迅速加速。写作这本书很具有挑战性,因为这门语言可以讨论的东西实在是太多了,而我不想让读者们感到不知所措。这本书是我为初学者设计的,从这个角度上说,这本书很成功。

问:你能否向读者们介绍一下你在Affectiva从事的工作?

当然。Affectiva是一家在数字体验中引入情绪智力的科技公司。我们的技术根据实时面部分析来判断一个人的情绪状态。我是这家公司的移动远景规划师,我的责任是把技术和移动世界连接起来。我们已经实现了这个目标,而我现在正在努力提高移动平台的技术,这些平台包括iOS和Android。我们有一个可以供开发者使用的SDK,用于把情绪智能整合到他们的应用中。

问:在你看来,开源Swift会改变iOS的生态环境以及iOS和Android之间的关系吗?

很难预测作为开源语言的Swift会如何影响Android开发。Google在语言初创方面有Go语言,它对于Android来说就像是Swift之于iOS。我期待会有人用Swift写Android应用,就像是有人用Go写iOS应用一样。一种语言会比另一种语言更成功吗?各自平台的语言会依附在它们本来的平台上吗?只有时间能告诉我们。但是话说回来,苹果开源Swift语言的决定肯定会让各种变化朝更好的方向发展。

问:Swift从其他语言中整合了哪些特性?其中你最喜欢的特性是什么?

所有计算机语言都会从其他语言身上借鉴一些东西。对于Swift来说也是如此。从语法和存储模型的角度上说,我在Swift上能看到很多Rust的影子。很明显,Swift对于安全的强调使其与C和C++保持了一定距离,所以它们之间的共同点比较少。我相信苹果很努力想让Swift遵从自己的主张,成为自己的语言。对于未来,我们拭目以待。

到目前为止,我最喜欢的Swift特性是语法的清晰度。用关键字‘let’建立一个常量变量,和用关键字‘var’建立一个可变变量相比,很大程度上简化了C/C++风格的语法。用执行命名参数可以清晰地写函数(如Objective-C),而创建和使用闭包也变得更简单。

问:Swift在未来有可能会取代Objective-C吗?

当然。但是没人知道会用多长时间,但是我认为应该是在5年之内。我们很容易就忘记计算机语言的生命有多长(现在C语言已经40岁了)。Objective-C不会彻底消失,但是我能预见在未来的某一个时刻,它在语言世界中的份额将变得非常小。

问:iOS中的很多应用框架都还是Objective-C的,目前调用的方式是使用桥接头文件,今后的调用是否有可能变为更加简洁的方式?未来是否会出现更多基于Swift的框架?

几乎可以肯定是这样的,而且我相信苹果正在努力实现这件事。但是我们得记住,这是一个艰巨的任务,需要转换的框架可不在少数。但是我相信这些工作不会花费太久的时间,而且我们很快就会看到专属于Swift的新框架,这样的框架在Objective-C中甚至都找不到同类。这种情况的发生肯定会对Objective-C的后续使用造成很大的影响。

问:你建议初学者应该先学Objective-C还是Swift?

肯定是Swift。Objective-C是一门很不错的语言,但是它带有C语言的遗留问题和包袱。对于编写应用来说,Objective-C在未来会变得越来越脱节。事实上,我现在不会用Objective-C来开启任何一个新的iOS或OS X项目。对于新项目来说我只使用Swift来开发。

问:对于已经掌握一两门编程语言的程序员来说,你认为他们怎样才能快速有效地学习一门新语言?

这取决于已有的一两种语言是什么。如果这两种语言是Objective-C和Swift,或者C和C++,或者其中任何组合,为了挑战思维,我会推荐你学习一种完全不同的语言。像C, C++, Objective-C,以及Swift这样的命令式语言都遵循着相同的模型,所以要学习同类语言很简单。你一定要让自己多接触不同的语言泛型,所以我会推荐你学一种函数式语言,比如Scheme。虽然你不会用这种语言来写应用,但是它会全面打开你对计算机语言的理解。

问:在学习编程语言的过程中,你认为什么时候接触大量的库和API比较合适?对于某个平台的开发者来说,他有必要掌握所有可能用到的API吗?

我经常会用木匠的工具腰带做类比。他的腰带中会装有所有想象得到的工具吗?不……只有那些经常使用的工具。当他需要特殊工具的时候,他会走到他的卡车前,打开放在里面的大工具箱,然后使用某种工具一次到两次,然后把它放起来。

对于编程语言和API来说也是如此。你无法完整地了解所有API,但是你知道得越多,你就越能更好地解决问题。我会建议你先比较完整地学习一门语言,然后再继续研究这种语言的细微之处,也就是开始接触这门语言中可以用来创建有用应用的框架和API。然后你可以每周一次,选择一个新的API,投入3-5个小时来学习它的功能。你可能并不会经常使用这个API,但是你知道它就在那,当遇到适当的时机时,你就会准备好用它来完成特定的任务了。

问:对于一位iOS开发专家来说,他的知识框架是什么样的?

一个全面的iOS专家必须掌握这四样东西:

  • (1) 用来写应用的语言(Swift / Objective-C);

  • (2) 对创建软件的工具的控制(Xcode);

  • (3) 关于iOS应用基础框架和API的强大知识储备;

  • (4) 鉴别好的UI设计的能力。

要经历很多应用和上百小时的编程才能达到这个水平。每个应用都会带来自身的要求和需要的API,所以你写的应用越多,就能越广地接触到各式苹果框架的,而你的UI设计技巧也会越来越好。

{%}

关于Swift内存管理你不知道的7件事

{%}

作者/ Boisy G. Pitre

资深苹果开发专家,曾任Siri语音识别技术提供方Nuance公司Mac产品小组的资深软件工程师,参与开发了语音识别软件Dragon Dictate。MacTech杂志“Developer to Developer”月度专栏作家,曾多次在MacTech和CocoaConf等会议上发表技术演讲。现任情绪识别公司Affectiva的移动远景规划师,领导团队致力于将公司的表情分析技术迁移到移动平台。

无论是小如Int类型还是大如BLOB,每个对象都需要占用内存资源。即便你的Mac、iPhone或iPad有很多内存,也是有限的稀缺资源,必须妥善管理。系统并非只运行你的应用程序,要成为良好的“应用程序市民”,就得行为得当,妥善而明智地使用内存。

使用其他编程语言时,开发人员通常必须了解内存处理细节,但Swift让这种细节尽可能透明。

在Swift中,好像不需要考虑内存管理,其中的秘密武器就是苹果的高级编译器LLVM。LLVM不仅将Swift语句转换为机器代码,还跟踪代码路径,确定对象何时不再在作用域内,可回收它们占用的内存。

虽然内存管理看起来是透明的,你在Swift开发中依然需要注意一些有趣的细节。

值和引用

IntString、结构和枚举都属于值类型,例如,将值类型传递给方法时,将在内存中创建其副本,并传递个副本。这让内存管理相对简单。通过使用String值的副本,方法可随心所欲地修改它,而不用担心这会修改传入的原始值。

另一方面,传递引用类型时不会复制它,而将其地址提供给可能使用它们的函数或方法。闭包以及从类实例化得到的对象都属于引用类型。将闭包或对象传递给方法时,不会创建其副本,而是传递引用(内存地址)。

由于传递引用类型时不会创建其副本,因此需要特别小心,确保在正确的时间妥善地释放它们。如果将引用类型占用的内存过早地归还到系统内存池,将导致崩溃;相反,如果在引用类型不再需要时没有将其占用的内存归还到系统内存池,这些内存相当于被“拘禁”,无法再分配。这被称为内存泄露

为何要同时支持值类型和引用类型呢?为何不让一切都是值类型呢?在包含可执行代码的类和闭包等结构中,特殊的内存保护约束禁止复制代码。从资源分配和执行时间的角度看,按引用(而不是值)传递对象的效率也更高。别忘了,值需要复制,而复制数据需要占用处理器时间。另外,存储副本需要占用额外的内存。

引用计数

Swift使用独特的方式为引用类型管理内存。这种内存管理方法被称为ARC(Automatic Reference Counting,自动引用计数),由苹果公司设计并由LLVM编译器提供支持。

ARC的基本假设是,每个引用类型都有一个被称为引用计数的数字。引用的对象被创建时,这个数字被设置为1。随着这个对象在应用程序中被传递,它可能有一个或多个“所有者”,这些所有者将以这样或那样的方式使用它。对象的所有者获得所有权时,必须将引用计数加1,而在放弃所有权时必须将引用计数减1。引用计数变成0(即最初的所有者放弃所有权)后,对象将被销毁,而它占用的内存将归还到可用内存池,供其他对象重用。

在Objective-C中,这种保留/放弃内存管理模型最初由应用程序开发人员手工实现。即开发人员编写使用引用对象的代码时,必须负责保留对象(将其引用计数加1),并在使用完毕后放弃对象(将其引用计数减1)。

不久后引入了ARC,将保留和放弃的负担从开发人员的肩上移交给了编译器。Swift是这种内存管理模型的受益者,让你完全不用操心何时该保留对象,何时该放弃对象这种乏味的工作。

然而,在Swift开发中,你并非可以完全不考虑内存管理。有时候,使用引用内存可能让你陷入困境,因此对ARC的功能有大致了解很重要。

引用循环

在很大程度上说,要在代码中使用类和闭包,只需声明并创建它们即可。然后,你就可以根据需要完成的工作传递变量。虽然不太明显,但当你声明引用类型的变量并将对象赋给它时,你实际上创建了指向该对象的(strong)引用,这意味着该对象的引用计数将加1。

然而,两个对象彼此引用对方(这种情况在Swift开发中很常见)时,一种独特的问题可能悄然而至,这就是引用循环。可将这种问题视为致命拥抱。对象A有一个指向对象B的引用,而对象B有一个指向对象A的引用,如图1所示。它们彼此抱得很紧,需要分开时却分不开了。

{%}

图1 引用循环

引用循环的后果是,只要应用程序还在运行,涉及的对象就不会释放,它们占用的内存也不会归还给系统。

来看一个演示这种问题的代码示例。如果还没有启动Xcode,现在就启动它,再选择菜单File > New > Project新建一个OS X项目。请务必选择Swift语句,并确保复选框未被选中。将这个项目命名为ReferenceCycleExample。

演示引用循环

为演示引用循环,你将创建两个Swift类:一个Letter类,表示要邮寄给人的信函;一个MailBox类,表示要将信函投入其中的邮箱。

class Letter {
    let addressedTo: String
    var mailbox : MailBox?

    init(addressedTo: String) {
        self.addressedTo = addressedTo
    }

    deinit {
        println("The letter addressed to \(addressedTo) is being discarded")
    }
}

这个类包含一个名为addressedTo的常量,它是收信人的姓名;还有一个变量,是指向可选MailBox对象的引用。

init方法接受一个String参数,并将其赋给Letter类的成员变量addressedTo。还有一个以前没介绍过的deinit方法,它在对象即将被释放,且该对象占用的内存被归还给系统前被调用。这个方法显示一条消息,指出正在将信函销毁。

现在来看MailBox类:

class MailBox {
    let poNumber: Int
    var letter: Letter?

    init(poNumber: Int) {
        self.poNumber = poNumber
    }

    deinit {
        println("P.O. Box \(poNumber) is going away")
    }
}

MailBox类的结构与Letter类相似——包含类型为Int的成员常量poNumber,表示邮箱的编号;还有成员变量letter,是指向可选Letter对象的引用。

编写测试代码

下面编写使用这两个类来演示引用循环的代码。

在下面的代码片段中,声明了两个变量:一个名为firstClassLetter的可选Letter变量,以及一个名为homeMailBox的可选MailBox变量。

    var firstClassLetter: Letter?
    var homeMailBox: MailBox?

接下来,使用合适的参数创建了两个对象,并将它们赋给前面声明的变量。

    // 初始化对象
    firstClassLetter = Letter(addressedTo: "John Prestigiacomo")
    homeMailBox = MailBox(poNumber: 355)

下面的代码将对象homeMailBox赋给对象firstClassLetter的成员变量mailbox,并将对象引用firstClassLetter赋给对象homeMailBox的成员变量letter

    firstClassLetter!.mailbox = homeMailBox
    homeMailBox!.letter = firstClassLetter

最后,将对象firstClassLetterhomeMailBox都设置为nil。之所以可以这样做,是因为这两个对象变量都被声明为可选类型。将nil赋给包含可选引用的变量时,它指向的对象将被销毁,该对象的deinit方法将被调用,而它占用的内存将交还给系统。

    // 销毁对象
    firstClassLetter = nil
    homeMailBox = nil

图2显示了这些代码在新建项目的文件AppDelegate.swift中的位置。LetterMailBox类位于这个文件末尾的第41~65行,而测试代码位于方法applicationDidFinishLaunching中的第19~31行。

{%}

图2 引用循环示例代码在文件AppDelegate.swift中的位置

在这个应用程序中,两个类都在方法deinit中使用方法println来指出对象被销毁,因此调试区域必须是可见的,因为方法println的输出将显示到这里。运行该应用程序前,确保在Xcode窗口底部能够看到调试区域。如果看不到,可单击调试区域图标,如图3所示。

{%}

图3 显示/隐藏调试区域的调试区域图标

代码准备就绪后,在下述行设置断点,方法是单击编辑器区域左边的行号(别忘了,蓝箭头表示设置了断点)。

第30行:firstClassLetter = nil

第50行:println("The letter address to \(addressedTo) is being discarded")

第63行:println("P.O. Box \(poNumber) is going away")

设置断点后,选择菜单Product > Run在Xcode中运行该应用程序。将在执行到30行时暂停,如图4所示。

{%}

图4 遇到第30行的断点

对象firstClassLetter即将被设置为nil。这种操作将导致Letter类的方法deinit被调用,从而遇到第二个断点。会遇到吗?为核实这一点,单击调试区域顶部的继续执行程序按钮,如图5所示。没有遇到第二个断点,程序继续往下执行。

{%}

图5 继续执行程序按钮

出现了一个空窗口,但这个简单应用程序的焦点是查看显示的信息,你可以完全不管这个窗口。按Command+Q退出应用程序。

至此,变量firstClassLetterhomeMailBox都被设置为nil,但相应类的方法deinit并没有被调用。这清晰地表明,存在引用循环,因此即便对象不再被引用,它们依然没有被销毁,还占用着内存。

断开引用循环

既然存在引用循环,该如何断开它呢?

问题的根源在于,指向对象的变量默认为强引用。解决方案是让变量之一——Letter类中的letterMailBox类中的mailbox——不要将引用计数加1。这被称为(weak)引用

将引用变量声明为weak,意味着它不会“拥有”被引用的对象,而只是引用它。赋值不会导致引用计数加1,从而解除两个对象之间的致命拥抱。

在前面的代码示例中,将哪个变量声明为weak无关紧要,只要对其中一个这样做就行。将第43行的成员变量mailbox修改成下面这样。

weak var mailbox : MailBox?

只需在关键字var前面加上关键字weak,就能让Swift知道mailbox是一个弱变量。执行修改后,再次运行这个应用程序。

与以前一样,执行到第30行的断点处将暂停。现在继续执行时,你将发现遇到了第63行的断点。这是MailBox对象的deinit方法,在该对象销毁时被调用。单击继续执行程序按钮,将在执行到第50行的断点处暂停,这行是Letter对象的deinit方法。引用循环断开后,两个对象都被销毁。方法println的输出也出现在调试区域中,表明这两个对象都已销毁。

闭包中的引用循环

闭包也是引用类型,因此可能成为引用循环的受害者,虽然受害的方式稍有不同。请看下面的类,它创建一个MailBox对象和一个Letter对象。

class MailChecker {
    let mailbox: MailBox
    let letter: Letter

    lazy var whoseMail: () -> String = {
        return "Letter is addressed to \(self.letter.addressedTo)"
    }

    init(name: String) {
        self.mailbox = MailBox(poNumber: 311)
        self.letter = Letter(addressedTo: name)
    }

    deinit {
        println("class is being deinitialized")
    }
}

除属性mailboxletter外,这个类还包含以前没介绍过的东西:一个名为whoseMail延迟(lazy)属性,它检查函件的收信人。关键字lazy用于推迟属性的计算,直到属性在代码中被使用。在这个示例中,由于计算属性whoseMail的闭包引用了self.letter.addressedTo,因此必须使用关键字lazy,否则将导致编译器错误。

由于这个闭包使用了self来引用包含它的MailChecker对象,Swift将以强引用的方式捕获这个对象。同样,MailChecker对象也以强引用的方式拥有这个闭包,这也导致了致命拥抱,即两个对象都以强引用的方式引用对方。(在这里,这两个对象分别是类和闭包。)

为证明这一点,在文件AppDelegate.swift末尾输入MailChecker类的代码,在方法application DidFinishLaunching中的第32行插入一个空行,再从第33行开始输入如下代码行(如图6所示)。

        // 创建并销毁一个MailChecker对象
        var checker : MailChecker? = MailChecker(name: "Mark Marlette")
        var result : String = checker!.whoseMail()
        println(result)
        checker = nil

这些代码实例化一个MailChecker对象并将其赋给可选变量checker,再显示该对象的成员变量whoseMail(该变量为前面讨论的闭包)。最后,将变量checker设置为nil,这应该导致MailChecker对象的方法deinit被调用,从而将其占用的内存还给系统。

{%}

图6 在方法applicationDidFinishLaunching中添加代码

代码准备就绪后,在MailChecker类的deinit方法所在的行(即第87行,如图7所示)设置一个断点,再在Xcode中运行这个应用程序。

{%}

图7 在MailChecker类的方法deinit所在的行设置一个断点

该应用程序运行时,将遇到前面设置的断点,单击继续执行程序按钮往下执行即可。然而,并没有遇到第87行的断点,这是引用循环导致的。

要断开这种引用循环,必须在MailChecker类中的闭包声明中添加一种特殊表示法。为此,将第77行修改成下面这样:

lazy var whoseMail: () -> String = { [unowned self] in

通过在闭包定义中添加[unowned self],让Swift知道不应保留self对象,从而断开了引用循环。

添加上述代码后,再次在Xcode运行这个应用程序。将执行到第87行的断点处并暂停,这证明不再存在引用循环,而MailChecker对象也被妥善地销毁。

感恩

除需要注意引用循环等问题外,Swift内存管理几乎是隐藏起来的细节,由LLVM编译器在幕后处理。这是使用Swift进行开发如此简单而直观的众多原因之一——你可以将重点放在应用程序上,其他的事情都交给Swift和编译器去操心。

{%}

《Swift基础教程》完全针对初学者,既适合接触过其他编程语言的有经验程序员,也适合有足够学习欲望的新手。本书包含大量屏幕截图和示例代码,让初学者全面了解Swift和Xcode工具集。基于概念和交互式环境快速扎实掌握苹果开发技能,真正学会开发完整App。本文节选自《Swift基础教程》

如何用Parse和Swift搭建你的Instagram?

{%}

作者/ Reinder de Vries

Reinder既是一名企业家,也是优秀的程序员,发表多篇应用程序的博客。本篇文章中,作者主要介绍了如何基于Parse特点,打造一款类似Instagram的应用,完整而清晰的步骤,为开发者提供一次绝佳的学习体验。本文系OneAPM工程师编译整理。

Parse 是一个移动应用开发平台,旗下有个很有意思的产品:Parse Core。它的特色之一是允许应用开发者直接将数据存储在云端,而无需担心设置服务器或重新设计一个 REST API。Parse Core 是本地备份(比如核心数据),这使得它能轻松解决线上线下等后端问题。

{%}

本篇教程主要介绍如何创建一个基于 Parse 的应用。我们将打造一个类似 Instagram 的应用,它包括以下特征:

1. 从 Parse 加载数据,存储在本地;

2. 保存数据到 Parse,并写回云端;

3. 为 Cat 的图片点赞。

这款应用将完全使用 Swift 语言开发,Swift 是苹果最新的编程语言,用于打造 iOS 应用。Parse 并不用完全重写 Swift,所以我们需要创建一个桥接头来处理它俩的兼容性。

通过这篇文章你将学到以下技能:

  • 用 Parse 实现检索,存储数据到云端;

  • Cocoapods 整合一个调用 Objective-C 框架的 Swfit 程序;

  • 建立视图和有接口的自定义表视图单元;

  • 从零开始,用 Swift 编写一个完整的 App;

  • 使用自动布局和约束;

  • 使用手势识别、可选类型、条件、闭包、属性、出口和动作。

那开始吧!

首先,你得有一个 Parse 帐户。这可以通过你的 Facebook、Google+、GitHub 帐号或邮件在 Parse.com 注册。然后,登录 Parse,通过 https://www.parse.com/apps 可以到你的 apps Dashboard。

通过点击「创建新应用」 按钮,创建一个新的应用程序,输入「Paws」作为应用名。接着,打开新应用,确保你能看到下图的的核心选项卡。

{%}

创建数据存储区

从技术上说,Parse 只是在线数据库。数据被存储为具有一个名称和多个字段的对象,如电子表格。这样的对象被称为一个类,它的功能是数据结构的蓝图。我们将要使用的类名为 Cat。

在核心选项卡单击该按钮添加一个类。在下拉框中选择自定义,然后键入类的名称:Cat。然后,单击创建类。

{%}

这样,我们就创建了新的类,接着我们还可以添加一系列的标准字段,如 ObjectId、createdAt、updatedat和ACL。

+Col按钮翻到最上,添加下列字段,名称和类型:

  • Name: name, type: String.

  • votes, type Number.

  • url, type String.

  • cc_by, type String.

这些字段将为 Cat 数据库提供基本信息。

导入数据

现在我们已经设置好基础结构,可以导入数据了!将该文件保存到:cat.json。然后,回到核心选项卡和数据库,左键点击「导入」按钮。选中你刚保存的文件并上传。确保类别集是「自定义」 ,并重命名为 Cat(而不是rs1_cat)。再单击「完成导入」。如果导入是完整的,Parse 会及时提示。点击「Got it」并重新加载页面。如果一切顺利,你现在应该能看到数据库中有10个 Cat。它们都有一个名字、一个 URL、一些得票数和一些为原则这预留的空间。

{%}

至此,我们在 Parse 中的全部工作已经完成。下面开始构建 Swift Xcode 程序。

构建 Xcode

打开 Xcode 并创建一个新项目,从开始界面选择「菜单→新建→工程」。选择类别「iOS→应用模板」的单一视图的应用。在下一屏上输入以下字段:

  • 产品名称:Paws

  • 结构名称:随意

  • 结构标识符:随意,比如com.appcoda

  • 语言:Swift

  • 设备:通用

单击「下一步」 ,选择工程目录文件夹,再单击「创建」 。

我们不打算使用 Storyboards,所以单击左上的 Paws、2 targets、 iOS SDK,打开工程设置。在左侧的列表中单击 Target 下方的 Paws,然后找到屏幕中主区域的主界面设置。将 textMain 从框中移除。

{%}

用 Cocoapods 将 Parse 库添加到 Xcode 项目

在程序代码中使用 Parse 之前,我们必须将其添加依赖关系。因此我们选择 Cocoapods,它是一个软件包管理器。许多应用项目依赖于第三方库,比如 Parse。CocoaPods 是方便加载库的工具,并确保其实时更新。在终端执行以下命令安装 Cocoapods。它会要求你输入你的 Mac 用户密码。但不允许包含「$」符号。这标志意味着 shell 命令!

$ sudo gem install cocoapods

中途如果一两分钟没有进展也无需担心,那是因为 Cocoapods 正在安装中。安装完成后你可以看到一堆线条,最终…安装完成。

{%}

接下来,在 Xcode 项目的根目录下创建一个空文件,并调用 Podfile 文件。用你喜欢的文本编辑器打开,并粘贴下面代码到该文件:

pod Parse’, ‘~> 1.7.1
pod ParseUI’, ‘~> 1.1.3

Podfile 会告知 Cocoapods 哪些库是我们需要的。这样的话,Parse 的版本是1.7.1,而ParseUI版本是1.1.3。现在,关闭 Xcode,并使用终端找到程序项目的根目录。写入终端 cd,然后在 Paws 目录中查找,并将其拖至终端。

接下来,在命令行输入以下代码:

$ pod install

CocoaPods 会查找 Podfile,并尝试安装我们设置的依赖关系。这个步骤大概会花上几分钟。结果应该是这样:

{%}

CocoaPods 已经下载并编译 Parse,并把它添加到一个新的工作区。从现在开始,我们不再使用原来的应用项目,而会使用 CocoaPods 创建的工作区。它包含了我们的原始项目和 CocoaPods 项目。

这里要注意的是:通过搜索浏览找到程序的根目录,打开 Xcode 中的新工作区,再打开其中的 Paws.xcworkspace。验证下左侧的导航项目,会看到:Pods 和 Paws。

在我们直接编写应用前,需要建立 Parse 和项目之间的连接。Parse 是用 Objective-C 搭建的,而我们的项目则是用 Swift 语言,两者之间需要适当的设置才能兼容。

在Swift项目中使用Objective-C

任何 Objective-C 库、项目或类都可以通过设置桥接头才能与 Swift 兼容。从技术上讲,这样的桥接将 Objective-C 的头文件转换成 Swift 语言。

创建一个桥接头需要执行以下操作:

1. 在 Paws 目录添加一个新文件,选择 Paws 工程下的 Paws 目录,单击右键,然后单击「新建文件」。

2. 从「iOS→源」类别中选择Objective-C文件模板并单击「下一步」。

3. 将类命名为「Paws」(或其它你喜欢的名字),然后继续进行并保存文件。出现提示时,单击「是」来配置 Objective-C 桥接头。

{%}

Xcode 创建两个新文件:Paws.m 和 Paws-Bridging-Header.h。Paws.m 文件没什么用,你可以直接将它删除。在 Paws-Bridging-Header.h 文件中写入以下代码:

#import <Parse/Parse.h>
#import <ParseUI/ParseUI.h>
#import <Bolts/Bolts.h>

看到这里,你不禁会问难道这一大堆工作只是为了建立一个编程项目么?别担心,我们接下来就来搞点有趣的。请记住:Parse 提供一个现成的在线后端,能节省大量时间!

验证Parse是如何工作的

通过https://parse.com/apps,返回 Parse 的 Dashboard。将鼠标悬停在你帐户名的右上角,单击帐户,再单击应用键标签最上面一栏。你还可以直接访问 https://parse.com/account/keys。确定应用的 Parse 网络服务后,该页面将显示你的应用键。应用键由一串字母数字或字符组成,基本上这就是应用的密码,要注意保密。接下来,在 Xcode 中打开文件 AppDelegate.swift。找到应用程序的 didFinishLaunchingWithOptions 方法。添加以下代码到方法中,确保它在该方法的首行。完整的是这样:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
    Parse.setApplicationId("...", clientKey: "...")

return true
}

用 Parse 的应用键分别替换「…」

1. 第一个,应用 ID

2. 第二个,客户端密钥

之后,尝试运行应用程序:

1. 确保选择正确的设备,通过确认PLAY按钮的右上方,确保设备选择,比如 iPhone 或 iPhone6。 2. 然后,点击Play按钮或按下 Command-R 运行程序。

项目在构造、运行过程中没有错误。一旦运行,你可以看到 iPhone 会出现有一个黑色的窗口,但窗口上却什么都没有——这是因为我们移除了主页面脚本,还没有替换。值得高兴的是:你已经成功用 Parse 和 CocoaPods 配置好你的项目了!

创建表视图控制器

为了显示出 Paws 应用中的 Cat 图片,我们需要使用一个表视图控制器。这是非常常见的 iOS 接口元件,能在垂直列表中展示各行数据。显而易见的例子就是 iPhone 上的联系人应用中人和号码的垂直列表。在 Objective-C 和 Swift 中,一个表视图控制器本身就是 UITableViewController 类。

注:陌生的术语类?把它当作原型,也就是你在铁铸件时用的模具。你把铁倒入模具,就出来一个副本。这种复制被称作该类的一个实例。

Parse 有个很棒的对应叫 ParseUI,是 UI 元素集合,能与 Parse 产品紧密结合。我们即将使用 PFQueryTableViewController 类。它用 Parse 数据扩展 UITableViewController 类的功能。这简直是完美的结合。创建一个名为 catstableviewcontroller 的 Swift 新类。在项目浏览器中,右键单击 Paws 目录并选择新文件。从「iOS→源」,选择 Cocoa Touch 类模板。输入以下设置:

  • 类:CatsTableViewController

  • 继承:PFQueryTableViewControlle

  • 语言:Swift

  • 确保创建 XIB 文件处于未选中状态

{%}

保存文件到 Paws 目录。选择目录时,请确保 Paws 作为目标被选中。

打开新类文件,可以看到的基本结构:一个名为 viewDidLoad;另一个名为 didReceiveMemoryWarning。需要注意的是,CatsTableViewController 扩展 PFQueryTableViewController,是它的子类。反过来,PFQueryTableViewController 类是 UITableViewController 的扩展,所以我们的表视图控制器将继承所有表视图功能,同时可用 ParseUI 添加代码和功能。

编码表视图控制器

首先,我们必须重写类的构造方法来配置基础设置。

将以下两种方法添加到类的顶部,在文件的第一个大括号之后:

override init(style: UITableViewStyle, className: String!)
{
    super.init(style: style, className: className)

    self.pullToRefreshEnabled = true
    self.paginationEnabled = false
    self.objectsPerPage = 25

    self.parseClassName = className
}

required init(coder aDecoder:NSCoder)
{
    fatalError("NSCoding not supported")
}

你刚添加了两个方法:

  • 指定初始化的 init,这需要两个参数:表视图的风格和我们要使用的 Parse 类名(在这里是 C)

  • 必需的初始化,需要一个参数:NSCoder 的一个实例。现在为止,它的内容是不相关的,坚持做下去,但不要求你创造性地使用该方法。

在最初的初始化中,完成了下列事项:

1. 当 super.init()调用时,初始化父类 PFQueryTableViewController,从而完成自身初始化。

2. 接着,pullToRefreshEnabled 设置为 true,继承 PFQueryTableViewController 类。特殊变量 self 指当前作用域,为该类的实例。

3. 随后,我们禁用分页,并设置表对象的最大数目为25。

4. 最后,在实例属性 parseClassName 中存储参数 className。之后,当我们创建 CatsTableViewController 类的实例,这个构造(或指定初始化)将是表示图控制器被调用和设置的基础。

用 queryForTable 编码数据检索

通过 PFQueryTableViewController 实现 Parse 表视图的核心是方法 queryForTable。我们继承该方法时,继承的实际上是 PFQueryTableViewController,所以需要对其进行重写:需要连接表视图控制器到 Parse 数据存储区时,PFQueryTableViewController 会调用它。它从表中查询数据,因此该方法名为 queryForTable。在该方法中,我们可以自定义检索。添加此方法到 CatsTableViewController 类(在 viewDidLoad 方法下)。注意这些括号匹配!

override func queryForTable() -> PFQuery {
    var query:PFQuery = PFQuery(className:self.parseClassName!)

    if(objects?.count == 0)
    {
        query.cachePolicy = PFCachePolicy.CacheThenNetwork
    }

    query.orderByAscending("name")

    return query
}

来看看这个新方法:

override func queryForTable() -> PFQuery

其中包含了什么?编写新方法 queryForTable,告知编译器以下事项:

1. 用相同的名字覆盖父类方法(识别标志),使用语句覆盖。

2. 用 func 和名字声明该方法,queryfortable。

3. 声明 methsignatureod 之间的参数。本例中没有参数。

4. 最后选择「Write→ PFQuery」,返回方法类型。

在方法内发生了下列变化:

1. 声明一个新变量调用查询,需要一个命名参数的类名,用于实例化构造函数的方法,被分配的 self.parseclassname 值。换句话说,表的类名是 Cat,利用 Cat 实例创建查询。

2. 然后,如果查询是空,在查询中设置 CachePolicy 属性。它的值是连续的 PFCachePolicy.CacheThenNetwork,这意味着该查询,首先在脱机缓存中寻找对象,如果没有找到,它会从在线 Parse 数据存储中下载对象。当表视图终于显示在应用界面上,这时 if 语句便成功执行。

3. 然后,我们以「名称」列为查询对象。

最后,返回查询结果。

将数据展现在屏幕上

让我们看看下面这个类的最终方法。它会把数据放在表视图中:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {

    let cellIdentifier:String = "Cell"

    var cell:PFTableViewCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? PFTableViewCell

    if(cell == nil) {
         cell = PFTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: cellIdentifier)
    }

    if let pfObject = object {
        cell?.textLabel?.text = pfObject["name"] as? String
    }

    return cell;
}

该方法的特征是这样:用带参数的 tableView、indexPath 和对象,重写方法 tableView 的 cellForRowAtIndexPath,返回一个显示展开的 PFTableViewCell 实例。换句话说:这里有一个表视图、一个对象以及它的索引(行号),现在请返回一个可用的单元格视图。

然后,我们首先声明单元格的文本标识符。每个单元类型具有不同的标识符。我们这里只使用一种单元类型,为「单元格」指定字符串类型值。该语句声明的是一个常数,而不是变量。

然后,我们要声明 PFTableViewCell 类型的可选变量单元吗?我们尝试将 tableView 参数出列。出列是一个机制,重新使用旧表视图单元加速表视图。实例方法 dequeueReusableCellWithIdentifier 需要一个参数,即我们这种类型的单元集的标识符。该方法的返回值类型是可选的,我们要将它转成 PFTableViewCell 类,转换是从一种类型转成另一种兼容的类型。这样,我们可从 UITableViewCell 类转成 PFTableViewCell 类。为什么是可选的?如果没有单元出列,方法将返回空值。

当单元格为空,我们可以创建 PFTableViewCell 类的新单元。用标识符表征这种单元类型,并赋给它 UITableViewCellStyle.Default 的风格。

在接下来的 if 语句中,我们尝试做一些很酷的东西。通常情况下,当你使用一个选项,你需要先将它打开。在打开之前,你必须检查可选是否为空值。你不能打开值为零的可选项。在本例中,我们使用可选的绑定(if-let),来验证可选是否为空。如果它包含一个值,我们可以将这个值作为一个临时常数(pfObject)。

然后,我们分配对象「名称」填写到 textLabel 的 Text 属性中。显而易见,textLabel 是表视图中按行显示的文本标签。PFObject 类型的变量对象是继承 NSObject 的,所以我们可以使用 「…」符号,用属性名称「name」检索对象。然后将其转换成可选字符串,因为对象的名称属性可有可无,也可能是空值。

最终,我们返回该单元。

CatsTableViewController小结

在 CatsTableViewController 类里,我们需要做以下三件事情:

1. 通过几个基本设置来初始化类的实例。

2. 通过覆盖 queryForTable 集成 Parse 后端:这是我们要用的类,也是我们的缓存策略。

3. 通过创建或重新使用一个单元格并填充,将数据按行置于屏幕上。

在屏幕上放置表视图

那么现在,在运行应用前什么事情都不会发生。我们并没有连接应用和 theCatsTableViewController!等等再做。回到 AppDelegate 类,并调整应用的 didFinishLaunchingWithOptions 方法来反映以下内容:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
    Parse.setApplicationId("...", clientKey: "...")

    var tableVC:CatsTableViewController = CatsTableViewController(className: "Cat")
    tableVC.title = "Paws"

    UINavigationBar.appearance().tintColor = UIColor(red: 0.05, green: 0.47, blue: 0.91, alpha: 1.0)
    UINavigationBar.appearance().barTintColor = UIColor(red: 0.05, green: 0.47, blue: 0.91, alpha: 1.0)
    UINavigationBar.appearance().titleTextAttributes = [NSForegroundColorAttributeName: UIColor.whiteColor()]
    UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.LightContent

    var navigationVC:UINavigationController = UINavigationController(rootViewController: tableVC)

    let frame = UIScreen.mainScreen().bounds
    window = UIWindow(frame: frame)

    window!.rootViewController = navigationVC
    window!.makeKeyAndVisible()

    return true
}

首先,确保你仍有正确的 Parse 应用 ID 和客户端密钥。

其次,我们创建 CatsTableViewController 的新实例,并将其分配给变量 tableVC。我们使用初始化程序,只需要一个参数类名「Cat」,之前写的初始化就会被内部调用。也给 tableVC 一个名字 ——「Paws」,它可用于任何 UIViewController 类(表视图继承该类)的属性,之后通过导航控制器被调用。

然后,我们改变 UINavigationBar 的外观。该类被导航控制器使用,即许多的应用程序顶部的粗条栏。设置外观可以确保类的一切实例附着到某些样式规则。比如 tintColor 和 barTintColor。它们均设置为略带蓝色的样子,并直接分配为 UIColor 的实例。导航栏文本颜色被设置为白色,然后设置 iPhone 状态栏的主题(也是白色)。

注意:它不是必需的,但要使状态栏颜色变化生效,你需要添加一行到 Info.plist 文件。这时,你会发现目录中支持哪些文件。打开支持的文件,插入一个新行(右键菜单),要么粘贴基于控制器的状态栏外观属性,要么将 UIViewControllerBasedStatusBarAppearance 放置其中。确保该行值是 NO 还是 False。

接下来,我们终于要创建 UINavigationController 实例本身,并将其分配给变量 navigationVC。我们用 RootViewController 来初始化,第一个视图控制器显示着:tableVC。这是我们的视图层次:

UIWindow → UINavigationController → CatsTableViewController

因此,我们显示导航控制器内部的表视图控制器,并将它放入应用程序的 UIWindow 顶层根视图控制器。

最后,就是一些常规步骤了:创建一个新的 UIWindow 实例,为它指定完整的框架,指定根视图控制器,使之成为应用程序的关键窗口。

运行应用程序

哦耶!按下 command-R 运行应用程序或点击 Play按钮的左上方。一切顺利的话,你的应用程序应该会出现基本的蓝色视图显示着10个猫的名字。

你可以下拉表视图并刷新,然后它从 Parse 下载新的数据并重新加载表视图。之前奠定的基础,能让我们的应用程序更加丰富,这是我们接下来要做的。来看看我们的成果!你已经成功将 Parse 加入了一个构建中的应用原型。太棒了!

{%}

在 Interface Builder 中创建一个自定义的 UITableViewCell

可以用自定义表视图单元格,让应用更加丰富。我们将不会再使用纯文本的单元格,取而代之的是 image-name-votes 单元格。

首先,我们创建一个新的类,命名为 CatsTableViewCell。在 Xcode 文件导航中右击 Paws,选择 「新建文件」 。从「iOS → 源」添加可触摸类的模板。命名为 CatsTableViewCell,继承 UITableViewCell 类。然后,勾选并创建 XIB 文件。语言是依然是 Swift。在 Paws 目录中创建文件。

然后,打开 CatsTableViewCell.swift 文件,更改类定义如下:

class CatsTableViewCell: PFTableViewCell

看看我们做了什么?此类扩展(继承) PFTableViewCell 类,而不是 UITableViewCell 类。还记得表视图单元格的 ForRowAtIndexPath 方法吗?它返回 PFTableViewCell 类的单元格实例,我们正是我们做出修改的原因。

添加以下新 CatsTableViewCell 的出口,在该类的首行,第一个大括号之后。

@IBOutlet weak var catImageView:UIImageView?
@IBOutlet weak var catNameLabel:UILabel?
@IBOutlet weak var catVotesLabel:UILabel?
@IBOutlet weak var catCreditLabel:UILabel?

我们需要这四个出口来显示四个 Parse 数据:猫的图片、猫的名字、它得到的票数、图片原作者。

接下来,从工程导航中打开 CatsTableViewCell.xib(它会在 Interface Builder 中打开)。Xcode 里有一种工具,对于设置应用的用户界面很有帮助。这是一个基础工具,它不是创建一个功能性的应用界面,只是定义它。与汽车收音机相比,Interface Builder 会创建无线控制台,并用Swift来编写线路。

首先我们要做的是:

1. 单击文档大纲(左)的主要单元元素。

2. 选择 Inspector 选项卡(右)的大小。

3. 更改行高为350,宽度为320,并再次将高度设为350。

主视图需要调整大小。

{%}

现在,增加四个视图:

1. 在对象库的右下,找到 UIImageView 类。

2. 将库中图像拖动到单元视图查看。

3. 调整图像视图,水平居中,并从顶部、左侧和右边与它相隔15点。选择图像视图时,你也可以通过 Size Inspector 来实现。其 X 和 Y 位置都是15,而宽度和高度均为290。

重复上述步骤建立3个新视图,都是 UILabel 的实例。一个左对齐,另一个右对齐。参照上面的截图作为参考。左标签的位置(317,25,209,21),四个数值分别为X值、Y值、宽度和高度。右标签的位置(225,69,317,21)。信用标签被定位在(199,285,106,21)。

{%}

下一步,配置所有4个视图。打开右侧的属性检查器,为各个视图进行如下设置:

1. 图像视图:填充模式,检查剪辑子视图。

2. 左标签:字体为粗体17.0号,设置为黑色。

3. 右标签:字体为14.0号,颜色为浅灰色。

4. 信用标签:字体为14.0号,设置为白色。

现在,让我们连接多个视图到出口。首先,在左边的文档大纲,再次选择猫的表格视图单元格。然后,切换到右边的连接 Inspector 选项卡。

然后,在检查器中找到 Outlets 下的四个出口。看到空心圆了吗?拖动 Cat ImageView 的右圈到单元格的图像视图中。一条蓝线会出现,在检查器中,出口都有选择框。其他三个标签也重复这一步骤。

{%}

为自定义单元格设置自动布局约束

注意:你其实可以跳过设置自动布局限制,但程序在不同 iPhone 屏幕中,显示效果可能不好。如果你不熟悉自动布局或约束,那花些力气来学学。之后你会觉得大有裨益。

为了让我们的应用在 iPhone4、4S、5、5S、6 和 6Plus 上完美运行,我们必须为 Interface Builder 的 UI 元素添加一些规则。Interface Builder 中有一个叫自动布局的功能,使用约束来管理视图位置、对准和调整。自动布局是一个很给力的工具,因为它既直观,又非常合乎逻辑。总的来说,这个工具能非常有效地管理复杂的大小调整,为你节省大量编写自定义布局代码的时间。

从技术上讲,约束只是我们一直遵行的视图规则。

  • 图像视图:中心水平,高度固定为290点,宽度任意,但距离单元格的顶部、左侧和右侧边缘均15点。

  • 左侧标签:宽度任意,固定高度为21点,距离单元格左边缘25点,右边缘11点。

  • 右侧标签:宽度任意,固定高度为21点,距离单元格左边缘25点,右边缘11点。

  • 信用标签:宽度任意,固定高度为21点,距离右侧15点,顶部285点(固定该标签在图像视图的右下方)

有四种办法可以建立约束,我们这里就演示一下最简单的:通过顶部的编辑目录。

为了配置图像视图,按下列目录选项进行选择:

1. Editor → Align → Horizontal Center In Container

2. Editor → Pin → Height

3. Editor → Pin → Leading Space To Superview

4. Editor → Pin → Trailing Space To Superview

5. Editor → Pin → Top Space To SuperView

然后,设置左标签:

1. Editor → Pin → Height

2. Editor → Pin → Leading Space To Superview

3. Editor → Pin → Bottom Space To Superview

然后,设置右标签:

1. Editor → Pin → Height

2. Editor → Pin → Trailing Space To Superview

3. Editor → Pin → Bottom Space To Superview

最后设置信用标签:

1. Editor → Pin → Height

2. Editor → Pin → Trailing Space To Superview

3. Editor → Pin → Bottom Space To Superview

看见没?我们只是将视图置于边界,并将其固定到相应的几个位置。现在你的屏幕应该类似下面的屏幕截图。

{%}

使用Swift和自定义表视图单元格

现在让我们再次回归代码——已经有足够的接口。打开 CatsTableViewController.swift 并找到指定初始化 init(风格:类名:)。

在这个方法中,我们可以在 self.parseClassName = className;下添加以下两行代码:

self.tableView.rowHeight = 350
self.tableView.allowsSelection = false

第一行设置合适的行高,第二行禁止单元格选择。 然后添加下列代码到 viewDidLoad just above super.viewDidLoad(): 方法

tableView.registerNib(UINib(nibName: "CatsTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier)

该行很可能引发错误。为了尽量不出错,将下面代码从 tableView 的 cellForRowAtIndexPat 方法移动到类的顶部,并重新将其值命名为「CatCell」。

let cellIdentifier:String = "Cell"

类的定义应该类似这样:

 class CatsTableViewController: PFQueryTableViewController
{
    let cellIdentifier:String = "CatCell"

    override init!(style: UITableViewStyle, className: String!)
}

我们刚刚将 cellIdentifier 常量从局部方法范围扩展成类范围,使得它在整个类中均可用,包括 tableView 的 cellForRowAtIndexPath 和 viewDidLoad。接下来,我们用下面代码替换 tableView 的 cellForRowAtIndexPath 的内容:

var cell:CatsTableViewCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? CatsTableViewCell

if(cell == nil) {
     cell = NSBundle.mainBundle().loadNibNamed("CatsTableViewCell", owner: self, options: nil)[0] as? CatsTableViewCell
}

if let pfObject = object {
    cell?.catNameLabel?.text = pfObject["name"] as? String

    var votes:Int? = pfObject["votes"] as? Int
    if votes == nil {
        votes = 0
    }
    cell?.catVotesLabel?.text = "\(votes!) votes"

    var credit:String? = pfObject["cc_by"] as? String
    if credit != nil {
        cell?.catCreditLabel?.text = "\(credit!) / CC 2.0"
    }
}

return cell

你不禁疑惑,这与我们以前使用的旧代码相比有什么区别?主要体现在:

1. 单元格类型从 PFTableViewCell 改为 CatsTableViewCell。

2. 当单元格为空,新单元格从我们刚才创建的 XIB 文件中得到。我们将从集合中检索,赋予所有当前类所有权,然后将它转换为 catstableviewcell。

3. 然后,我们检查对象是否存在,并尝试将 Parse 对象的列名赋给它的文本属性,就像之前那样。

4. 然后,catVotesLabel 和文本属性也一样。Parse 每列的票数是 String 类型,但不是 int,所以要转换成 int 类型吗?如果票数恰好是空值,那么我们就将其设置为零。然后,使用一种称为字符串插值的骚亮技术,设置标签文本。

最后,我们返回单元格。

让我们再次运行应用程序。一切看上去太完美了!没有 Bug 和死机!但是…图像在哪儿?

从 Parse 异步下载图像

图像不见了,这怎么可以!让我们加上它。在 TableView 的 cellForRowAtIndexPath 添加如下代码「在最后一个 if 语句(用于信用标签)之后,在返回语句之前」。

var cell:CatsTableViewCell? = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? CatsTableViewCell

if(cell == nil) {
     cell = NSBundle.mainBundle().loadNibNamed("CatsTableViewCell", owner: self,
options: nil)[0] as? CatsTableViewCell
}

if let pfObject = object {
    cell?.catNameLabel?.text = pfObject["name"] as? String

    var votes:Int? = pfObject["votes"] as? Int
    if votes == nil {
        votes = 0
    }
    cell?.catVotesLabel?.text = "\(votes!) votes"

    var credit:String? = pfObject["cc_by"] as? String
    if credit != nil {
        cell?.catCreditLabel?.text = "\(credit!) / CC 2.0"
    }
}

return cell

哇!这里发生了什么?我们将 Parse 的 URL 列转成了 NSURL 类型的实例。

我们用它在主操作队列中启动异步 NSURLConnection,其中下载图像作为 NSData 对象。当下载完成时关闭执行。它分配下载得到 UIImage 的数据,分配到 catImageView 的图像属性中。

在这里无需钻研太深,因为上面代码的复杂性与我们的应用程序无关。但是,请注意以下几点:

  • 使用 NSURLConnection 很方便,但有点枯燥。当你使用互联网上的数据源做更复杂的事情,请选择优秀的 AFNetworking (Objective-C)或 alamofire (Swift)库。

  • Parse 允许你将图像存储在云端,并能直接使用。它是 ParseUI 的组成部分,但它不允许外部 URL(猫图片源自 Flickr)的调用。

  • 在开始另一个异步连接之前,我们首先要明确主队列中的所有操作。这是有点枯燥:它从队列中移除 pending-and-unfinished 下载。尝试删除该行并运行程序,你会看到所有图像混合成一堆。当它重新使用单元格时,出列机制不会重置任何挂起连接,因此图像可以加载成功。

让我们再运行应用,看看是否有效。

加码 : Instagram 的类似功能

进行到这一步了,真不容易!还有一些终极功能有待完善。接下来就让我们来添加这些功能:类似「Instagram」的功能——你在图片上双击,一个「赞+1」被添加到该猫的图片上,并显示一个干净的小猫爪动画。

首先,为爪子图像到表视图单元格添加出口。添加下面一行代码到 CatsTableViewCell 类(在其他四个出口的下面):

@IBOutlet weak var catPawIcon:UIImageView?

在 Interface Builder 中添加一个 UIImageView 到 CatsTableViewCell.xib。还记得怎么做的吗?

1. 在对象库中查找 UIImageView 类。

2. 将它从对象库中拖放到表视图单元格。

确保将其向右拖动到其他图像视图的中心。调整新图像,宽高均为 100 点,它的 X 和 Y 均为大约110点。然后,当图像视图已选中时,添加以下限制。

1. Editor → Pin → Width

2. Editor → Pin → Height

3. Editor → Pin → Top Space To Superview

4. Editor → Align → Horizontal Center In Container

正如下图所示,使图像视图水平居中,固定宽度和高度为100点,并保持它与顶部有固定的空间,有效地将其居中的猫图像的放在正中心。

{%}

现在,通过从文档的顶部选择猫的表格视图单元格,创建出口连接。再选择 Connections Inspector 选项卡,从 catpawicon 单元格图像视图中绘出一条蓝色的线。

接下来,下载 paw.zip。该文件包含三个图形文件,是一个图像的三种分辨率。在使用之前需要将它们导入。

首先,解压缩文件;然后,打开 Xcode 中 Images.xcassets 文件;接着右键单击左侧列表(一个写着 APPICON 的列表),然后单击新建图像集,或使用左下方的「加号」按钮。重命名刚才创建的图像集,打开其属性。

现在,将刚才解压的文件从 Finder 拖至打开的文件集。确保文件匹配:

  • paw.png 是 1x.

  • paw@2x.png 是 2x.

  • paw@3x.png 是 3x.

看不到文件也不用担心,因为它们都是白色。

{%}

然后,返回 CatsTableViewCell.xib 并选择小图像视图。找到属性检查器,然后从在图像下拉列表中选择合适的爪子图像。白色的爪子应该像下图这样显示在单元格视图。

{%}

最后,请记住连接与 catPawIcon 出口和小图像视图。现在,让我们回到编码。打开 Xcode 中的 CatsTableViewCell。将下面的代码添加到 awakeFromNib 方法中(在super.awakeFromNib() 之前)。

let gesture = UITapGestureRecognizer(target: self, action:Selector("onDoubleTap:"))
gesture.numberOfTapsRequired = 2
contentView.addGestureRecognizer(gesture)
catPawIcon?.hidden = true

这里会发生两种情况。

  • 第一,我们建立一个 UITapGestureRecognizer,这样我们便可以跟任何视图互动。在这种情况下,我们将其添加到 contentView 查看,这个视图包括单元格的两个标签和两个图像视图。它为 onDoubleTap: 初始化一个 target、self、一个动作和一个选择器。所以,当检测到连续双击时,方法 onDoubleTap:of self(当前类)被执行。此外,我们设置连续数目为 2,使得它为双击响应。

  • 第二,我们隐藏 catPawIcon 出口。

其次,添加 onDoubleTap 方法到当前类(在 awakeFromNib():函数之后)。

func onDoubleTap(sender:AnyObject) {
    catPawIcon?.hidden = false
    catPawIcon?.alpha = 1.0

    UIView.animateWithDuration(1.0, delay: 1.0, options:nil, animations: {

        self.catPawIcon?.alpha = 0

        }, completion: {
            (value:Bool) in

            self.catPawIcon?.hidden = true
    })
}

这种方法被称为一个动作,始终需要一个参数:AnyObject。在该方法中,可以实现以下动画代码:

1. 首先,通过设置隐藏为 false,使 catPawIcon 可见。

2. 然后,将 alpha 即透明度设置为1.0,完全可见。需要重置图像状态,也就是当动画完成时 alpha 通道为0。

3. 动画的设置需要编程。UIView 的类方法被使用,这需要五个参数:动画时间、动画前延迟、基本选项、动画属性的关闭,以及动画完成时关闭的指令。

这时你会看到:

1. 为了使图像可见,我们可以设置它的 alpha 通道为可见。

2. 稍等一下动画延迟。

3. 动画 alpha 通道从1到0的时间不到一秒,这就是动画周期。

4. 动画完成,隐藏图像。

这个解决方案的最大好处在于它易于使用:代码将完全管理动画。我们只需要设置它的初始状态、结束状态、持续时间,以及动画框架插补状态和动画步骤。从技术上来讲,我们使用两个属性:一个连续的值 α,一个用来隐藏管理爪子图像可见性的布尔值。

最后,运行应用,看看新功能能否正常适用。你可以双击一个单元格,简要展示爪子图标,双击然后淡出消失。

{%}

能运行了吗?太棒了!

用 Parse 整合投票

剩下要做的事就是给 Parse 猫对象增加投票数列,通过双击响应投票动作。

那么我们怎么去实现呢?

首先,我们要改变的对象叫做 PFObject 类型的对象,在 CatsTableViewController 的 tableView 的 cellForRowAtIndexPath 方法中。我们不能从表视图单元访问它,因为它在双击动作的方法内。

我们不能移动 onDoubleTap 方法,所以我们需要在表视图对象和表视图单元之间创造引用。

我们采取以下步骤来实现:

1.在 CatsTableViewCell 中,在类的顶部和网点下,编写下列代码创建一个新的属性:

var parseObject:PFObject?

2.然后,在 tableView 里的 cellForRowAtIndexPath,编写下面代码(就在单元格 == nill 语句结束的大括号后面),如下:

cell?.parseObject = object

现在,我们已建立一个机制,将 cellForRowAtIndexPath 的对象复制到我们的表视图单元,使得在 CatsTableViewCell 类的对象实例可用。然后,调整 CatsTableViewCell的onDoubleTap 方法。在该方法的开始处添加下面代码:

if(parseObject != nil) {
    if var votes:Int? = parseObject!.objectForKey("votes") as? Int {
        votes!++

        parseObject!.setObject(votes!, forKey: "votes");

        parseObject!.saveInBackground();

        catVotesLabel?.text = "\(votes!) votes";
    }
}

这段代码可以实现以下工作:

1. 检查 parseObject 是否为空;

2. 从 parseObject 得到票数,并将它放在可选的 Int 中;

3. 如果票数不为空,用 ++ 操作增加票数变量,与 votes = votes! + 1 有相同功能;

4. 用 setObject 函数将票数变量返回给 parseObject 集;

5. 调用 parseObject 的 saveInBackground() 方法!它在后台将保存当前对象,可能的时候将其写入 Parse 云端;

6. 更新文本以反馈新的票数,一切就是这么简单,用 Command-R 或 Play 按钮运行程序,验证新功能是否实现。

运行成功了吗?太赞了!

小结

通过本篇文章,我们学习了以下内容:

  • 用 Parse 实现检索,存储数据到云端;

  • Cocoapods整合一个调用 Objective-C 框架的 Swfit 程序;

  • 建立视图和有接口的自定义表视图单元;

  • 从零开始,用 Swift 编写一个完整的 App;

  • 使用自动布局和约束;

  • 使用手势识别、可选类型、条件、闭包、属性、出口和动作。

你可以在这里下载整个爪子项目文件。使用 Xcode6.3(或以上)版本运行项目。请注意,你必须改变 AppDelegate.swift 中的应用程序键和客户端密钥。另外也要记住,如果你自己动手编写这个完整的 App,对自己是个很好的提升机会。

图灵社区原文

英文原文:Building an Instagram-Like App with Parse and Swift

iOS分层架构设计

作者/ 关东升

国内知名iOS技术作家,iOS技术顾问,高级培训讲师,移动开发专家。担任51CTO社区iOS技术顾问,精通iOS、Android和Windows Phone及HTML5等移动开发技术,擅长移动平台的应用和游戏类项目开发。除《iOS开发指南》外,还著有《Swift开发指南》《品味移动设计》《iOS网络编程与云端应用最佳实践》《iOS传感器应用开发最佳实践》《iOS多媒体编程技术最佳实践》《iPhone与iPad开发实战——iOS经典应用剖析》等畅销书。

设计模式只是解决某一特定问题的策略,是面向局部的。而一个架构设计是宏观地、全面地、有机地将这些设计模式组织起来解决整个应用系统的方案。衡量一个软件架构设计好坏的原则是:可复用性和可扩展性。因为可复用性和可扩展性强的软件系统能够满足用户不断变化的需求。为了能够使我们的软件系统具有可复用性和可扩展性,我主张采用分层架构设计,层(Layer)就是具有相似功能的类或组件的集合。例如,表示层就是在应用中负责与用户交互的类和组件的集合。

在讨论iOS平台上的应用分层设计之前,我们先讨论一下一个企业级系统是如何进行分层设计的。

低耦合企业级系统架构设计

首先,我们来了解一下企业级系统架构设计。软件设计的原则是提高软件系统的“可复用性”和“可扩展性”,系统架构设计采用层次划分方式,这些层次之间是松耦合的,层次内部是高内聚的。图10-1是通用低耦合的企业级系统架构图。

{%}

图1 通用低耦合的企业级系统架构图

  • 表示层。用户与系统交互的组件集合。用户通过这一层向系统提交请求或发出指令,系统通过这一层接收用户请求或指令,待指令消化吸收后再调用下一层,接着将调用结果展现到这一层。表示层应该是轻薄的,不应该具有业务逻辑。

  • 业务逻辑层。系统的核心业务处理层。负责接收表示层的指令和数据,待指令和数据消化吸收后,再进行组织业务逻辑的处理,并将结果返回给表示层。

  • 数据持久层。数据持久层用于访问信息系统层,即访问数据库或文件操作的代码应该只能放到数据持久层中,而不能出现在其他层中。

  • 信息系统层。系统的数据来源,可以是数据库、文件、遗留系统或者网络数据。

图1所示看起来像一个多层“蛋糕”,蛋糕师们在制作多层“蛋糕”的时候先做下层再做上层,最后做顶层。没有下层就没有上层,这叫做“上层依赖于下层”。信息系统层是最底层,它是所有层的基础,没有信息系统层就没有其他层。其次是数据持久层,没有数据持久层就没有业务逻辑层和表示层。再就是业务逻辑层,没有业务逻辑层就没有表示层,最后是表示层。也就是说,我们开发一个企业级系统的顺序应该是,先是信息系统层,其次是数据持久层,再次是业务逻辑层,最后是表示层。

iOS分层架构设计

iOS(也可以说移动平台)的应用也需要架构设计吗?答案是肯定的,但是并不一定采用分层架构设计。一般情况下,有关信息处理的应用应该采用分层架构设计,而游戏等应用不会这种采用分层架构设计。

提示 游戏开发一般都会采用某个引擎,游戏引擎事实上包含了架构设计解决方案,游戏引擎的架构一般不是分层的而是树形结构的。

图2所示是iOS分层架构设计,其中各层内容说明如下。

  • 表示层。它由UIKit Framework构成,包括我们前面学习的视图、控制器、控件和事件处理等内容。

  • 业务逻辑层。采用什么框架要根据具体的业务而定,但一般是具有一定业务处理功能的Swift、Objective-C和C++等语言封装的类,或者是C封装的函数。

  • 数据持久层。提供本地或网络数据访问,它可能是访问SQLite数据的API函数,也可能是Core Data技术,或是访问文件的NSFileManager,或是网络通信技术。采用什么方式要看信息系统层是什么。

  • 信息系统层。它的信息来源分为本地和网络。本地数据可以放入文件中,也可以放在数据库中,目前iOS本地数据库采用SQLite3。网络可以是某个云服务,也可以是一般的Web服务。

{%}

图2 iOS平台中信息处理应用的分层架构设计图

在iOS平台中,分层架构设计有多种模式,在介绍这些模式之前我们要熟悉一些重要的概念:工程与工作空间、静态链接库和框架。

工程与工作空间

使用Xcode不仅可以创建工程(Project),还可以创建工作空间(Workspace)。出于方便管理等目的,我们也可以将多个相互管理的工程放到一个工作空间中,工作空间是多个工程的集合。工程文件名的后缀为.xcodeproj,工作空间文件名的后缀是.xcworkspace。

图3所示是具有3个工程的工作空间,它们之间可以互相建立依赖关系。要创建工作空间,可以通过菜单File→New→Workspace来实现。需要说明的是,此时创建的工作空间是空的,没有工程,我们可以为其添加工程,具体操作将在后面详细介绍。

{%}

图3 Xcode工作空间

静态链接库

有时候,我们需要将某一层复用给其他的团队、公司或者个人,但由于某些原因,我们不能提供源代码,此时就可以将业务逻辑层和数据持久层编写成静态链接库(static library或statically-linked library)或框架(Framework)。

库是一些没有main函数的程序代码的集合。除了静态链接库,还有动态链接库,它们的区别是:静态链接库可以编译到你的执行代码中,应用程序可以在没有静态链接库的环境下运行;动态链接库不能编译到你的执行代码中,应用程序必须在有链接库文件的环境下运行。

在Xcode中可以创建静态链接库工程,具体创建过程如下:在Xcode中选择菜单File→New→ Project…,在打开的对话框中选择Framework & Library→Cocoa Touch Static Library工程模板,如图4所示。

{%}

图4 创建静态链接库工程

提示 静态链接库中不能有Swift代码模块,只能是Objective-C代码模块。

框架

由于静态链接库比较麻烦,需要给使用者提供.a.h文件,使用的时候还要配置很多的环境变量。事实上,苹果提供的API(如UIKit、QuartzCore和CoreFoundation)都是框架。为了方便使用,框架会将.a.h等文件打包在一起。

在Xcode 6之前,苹果没有提供开发框架的工程模板。直到Xcode 6,开发人员才可以创建自己的框架。图5所示的Cocoa Touch Framework工程模板可以帮助开发者创建自己的框架。

{%}

图5 创建框架

提示 自定义框架没有静态链接库的限制,Swift代码可以在框架工程中使用。

10种分层模式

由于iOS 8之后应用开发可以使用Swift和Objective-C两种语言,开发人员都有4种方式来选择开发语言:

  • 采用纯Swift的改革派方式;

  • 采用纯Objective-C的保守派方式;

  • 采用Swift调用Objective-C的左倾改良派方式;

  • 采用Objective-C调用Swift的右倾改良派方式。

从技术上讲,无论是否采用分层架构设计,都可以用上述4种方式来选择语言。也就是说,可以在同一层中采用单一语言和混合搭配,也可以在不同层之间采用单一语言和混合搭配。但是从设计规范上讲,一般不会在同一层中使用混合搭配,在不同层之间可以混合搭配。基于图2进行分层,如果只考虑业务逻辑层和数据持久层采用相同语言的情况下,那么可以混合搭配出4种模式,具体如下。

  • ObjC-ObjC-ObjC,缩写为OOO:用Objective-C语言实现表示层,用Objective-C语言实现业务逻辑层,用Objective-C语言实现数据持久层。

  • Swift-Swift-Swift,缩写为SSS:用Swift语言实现表示层,用Swift语言实现业务逻辑层,用Swift语言实现数据持久层。

  • Swift-ObjC-ObjC,缩写为SOO:用Swift语言实现表示层,用Objective-C语言实现业务逻辑层,用Objective-C语言实现数据持久层。

  • ObjC-Swift-Swift,缩写为OSS:用Objective-C语言实现表示层,用Swift语言实现业务逻辑层,用Swift语言实现数据持久层。

另外,如果考虑到代码的组织形式,可以分为如下3种组织方式:

  • 同一工程的分层,用P(Project)表示;

  • 基于静态链接库实现的同一个工作空间不同工程的分层,用WL(Workspace Library)表示;

  • 基于自定义框架实现的同一个工作空间不同工程的分层,用WF(Workspace framework)表示。

因此,经过组合,可以得到表1所述的结果。

表1 10种分层模式

{%}

根据表1得出10种分层模式。由于框架实现要先进于静态链接库实现,所以WFOOO、WFSSS、WFSOO和WFOSS是我们优先考虑的,如果从技术和设计上无法实现,可以考虑WLOOO和WLSOO,最后是POOO、PSSS、PSOO和POSS实现。

{%}

《iOS开发指南》以Swift和Objective-C语言为基础进行讲解,共分4篇:第一篇为基础篇,主要讲解iOS的一些基础知识;第二篇为网络篇,主要讲解iOS网络开发的相关知识;第三篇为进阶篇,介绍了iOS高级内容、商业思考等;最后一篇是实战篇,通过两个真实的项目,带领读者身临其境地完成一个项目的开发,使读者了解整个开发过程以及开发中的技巧。本文节选自《iOS开发指南》

阅读详情 -> 专题:A Swift Tour.

未经允许不得转载:前端头条 » 专题:A Swift Tour
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

前端开发相关广告投放 更专业 更精准

联系我们