스위프트가 4번째 버전이 나왔습니다. 하지만 문법적으로 변경되는 부분이 크게 많지 않아서 서 3 버전과 크게 달라진 문법은 많지 않습니다.
내부적으로 수정되거나 추가된 문법이 많고, 삭제된 부분은 거의 없으니까요.
달라진 문법을 살펴보겠습니다.
단방향 범위 연산자
이제 범위연산자에서 양 쪽 끝을 모두 신경쓸 필요가 없습니다.
var numbers = [1, 2, 3, 4, 5]
// Swift 3
numbers[2..<numbers.endIndex] // [3, 4, 5]
numbers[0...2] // [1, 2, 3]
numbers[0..<2] // [1, 2]
// Swift 4
numbers[2...] // [3, 4, 5]
numbers[...2] // [1, 2, 3]
numbers[..<2] // [1, 2]
numbers = []
// Swift 3
//numbers[0...numbers.endIndex] // out of range - error
// Swift 4
numbers[0...] // []
let number: Int = 100
// Swift 3
switch number {
case Int.min..<0:
print("negative")
case 0:
print("zero")
case 1...Int.max:
print("positive")
default:
print("unknwon")
}
// Swift 4
switch number {
case ..<0:
print("negative")
case 0:
print("zero")
case 1...:
print("positive")
default:
print("unknwon")
}
String의 많은 변화
Swift 4에서 String은 정말 많은 변화가 있는 부분 중 하나입니다. 그 중 눈에 띌만한 내용을 위주로 정리했습니다. 더 많은 사항은 [String Processing For Swift 4] 문서를 참고해보시기 바랍니다.
String 구조체가 다시 Collection 프로토콜을 따릅니다. [SE-0163]
var greeting: String = "Hello, Swift!"
// Swift 3
greeting.characters.forEach { print($0) }
print(greeting.characters.count) // 13
// Swift 4
greeting.forEach { print($0) }
print(greeting.count) // 13
Unicode 9이 적용되었습니다. [String Processing For Swift 4 - Unicode 9 Conformance]
이전에 문자열 길이 결과가 제각각이었던 유니코드 문자들이 사람이 인지할 수 있는 단위로 세어집니다.
여러 줄 리터럴 문법이 생겼습니다. [SE-0168]
큰따옴표 세 개를 연결하여 표현하면 여러 줄 문자열을 표현하는 리터럴 문법입니다.
반드시 """ 다음 줄부터 문자열을 입력해야하며, 문자열 마지막 줄 다음줄에 """로 닫아줘야 합니다.
// Swift 3
greeting = "Hello\nHow are you?\nI'm fine thanks, and you?"
// Swift 4
greeting = """
Hello
How are you?
I'm fine thanks, and you?
"""
Substring 타입과 StringProtocol 프로토콜이 생겼습니다.
기존에 String 타입에 구현되어있던 주요 기능을 StringProtocol에 기본구현 하였습니다.
let subIndex = greeting.index(greeting.startIndex, offsetBy: 4)
// Swift 3
let subGreeting = greeting[greeting.startIndex...subIndex]
print(type(of: subGreeting)) // String
// Swift 4
let subGreeting = greeting[...subIndex]
print(type(of: subGreeting)) // Substring
let stringFromSubstring: String = String(subGreeting)
Character 타입에 unicodeScalars 프로퍼티가 추가되었습니다. [SE-0178]
Swift 3에서는 Character 인스턴스의 유니코드 스칼라 값을 알아내려면 String 타입으로 변환하여야 했지만, 이제는 바로 Character 인스턴스의 unicodeScalars 프로퍼티를 이용해 알아낼 수 있습니다.
let character: Character = ""
// Swift 3
print(String(character).unicodeScalars)
// Swift 4
print(character.unicodeScalars)
Dictionary와 Set
자주 사용하는 콜렉션 타입인 Dictionary와 Set의 기능이 한층 강화되었습니다.
[SE-0154], [SE-0165]
키와 값의 시퀀스를 통해 새로운 딕셔너리를 생성할 수 있습니다. 또, 키와 값을 기존의 딕셔너리에 병합할 수 있습니다.
let zipSequence = zip("abcdefghijklmnopqrstuvwxyz", 97...)
let asciiTable = Dictionary(uniqueKeysWithValues: zipSequence)
// ["w": 119, "n": 110, "u": 117, "v": 118, "x": 120, "q": 113, ...]
let vegetables = ["tomato", "carrot", "onion", "onion", "carrot", "onion"]
let vegetableZipSequence = zip(vegetables, repeatElement(1, count: Int.max))
var vegetableCounts = Dictionary(vegetableZipSequence, uniquingKeysWith: +)
vegetableCounts.merge([("tomato", 1)], uniquingKeysWith: +)
// ["tomato": 2, "carrot": 2, "onion": 3]
Dictionary와 Set의 filter 결과가 이제 Array대신 각각의 원래 타입으로 반환됩니다. 또, 딕셔너리는 mapValues(_:) 메서드를 통해 값이 변형된 새로운 딕셔너리를 만들 수 있습니다.
let vowels: Set<Character> = ["a", "e", "i", "o", "u"]
let asciiVowels = asciiTable.filter({ vowels.contains($0.key) })
// Swift 3
print(type(of: asciiVowels)) // Array<(key : Character, value : Int)>
// Swift 4
print(type(of: asciiVowels)) // Dictionary<Character, Int>
asciiVowels["a"] // 97
asciiVowels["b"] // nil
let filteredSet: Set<Character> = vowels.filter{ $0 < "h" }
// Swift 3
print(type(of: filteredSet)) // Array<Character>
// Swift 4
print(type(of: filteredSet)) // Set<Character>
// Swift 3
let strangeMap = asciiTable.map { [$0: "0x" + String($1, radix: 16)] }
print(type(of: strangeMap)) // Array<Dictionary<Character, String>>
// [["u": "0x75"], ["v": "0x76"], ["w": "0x77"], ["x": "0x78"], ["q": "0x71"], ["n": "0x6e"], ...]
// Swift 4
let asciiHexTable = asciiTable.mapValues({ "0x" + String($0, radix: 16) })
// ["w": "0x77", "n": "0x6e", "u": "0x75", "v": "0x76", "x": "0x78", ...]
딕셔너리 서브스크립트 문법이 추가되었습니다. 원하는 키에 해당하는 값이 없으면 nil 대신 기본값을 돌려줍니다.
기본값이 있다는 것은 값이 항상 존재한다는 의미이므로, 해당 서브스크립트의 반환값은 옵셔널이 아닌 값이라는 뜻입니다. 딕셔너리 옵셔널 전쟁에서 해방될 수도!
let favoriteSubject: [String: String] = ["yagom": "swift", "hana": "communication", "jisu": "swift"]
// Swift 3
type(of: favoriteSubject["minji"]) // Optional<String>
// Swift 4
type(of: favoriteSubject["minji"]) // Optional<String>
type(of: favoriteSubject["minji", default: "unknown"]) // String
var favoriteCount = [String: Int]()
favoriteSubject.forEach { favoriteCount[$0.value, default: 0] += 1 }
print(favoriteCount) // ["swift": 2, "communication": 1]
init(grouping:by:) 이니셜라이저를 통해 특정 조건에 따라 Array 또는 다른 시퀀스를 딕셔너리 형태로 그룹지을 수 있습니다. 그룹 기준값은 딕셔너리의 키가 됩니다.
struct Person: CustomStringConvertible {
enum Gender {
case male, female, unknwon
}
let name: String
var gender: Gender
var description: String { return name }
}
let yagom = Person(name: "yagom", gender: .male)
let hana = Person(name: "hana", gender: .female)
let jisu = Person(name: "jisu", gender: .unknwon)
let eric = Person(name: "eric", gender: .male)
let mike = Person(name: "mike", gender: .male)
let friends = [yagom, hana, jisu, eric, mike]
let friendsByGender = Dictionary(grouping: friends, by: { $0.gender })
print(type(of: friendsByGender)) // Dictionary<Gender, Array<Person>>
print(friendsByGender)
// [Person.Gender.unknwon: [jisu], Person.Gender.male: [yagom, eric, mike], Person.Gender.female: [hana]]
reserveCapacity(_:) 메서드를 통해 Array 처럼 예약된 공간을 가질 수 있습니다.
var emptyDictionary = [String: String]()
print(emptyDictionary.capacity) // 0
emptyDictionary.reserveCapacity(100)
print(emptyDictionary.capacity) // 192
// 최소 100개의 요소가 들어갈 수 있도록 공간을 확보하기 때문에 꼭 100이지 않을 수 있습니다
emptyDictionary.reserveCapacity(10)
print(emptyDictionary.capacity) // 192
emptyDictionary.reserveCapacity(1000)
print(emptyDictionary.capacity) // 1536
Key path
Cocoa에서 단순한 문자열로 표현하던 key path를 스위프트에서는 정확한 타입(KeyPath 라는 제네릭 클래스) 방식으로 표현합니다. 조금 더 안전한 동적 프로그래밍을 할 수 있지 않을까 기대됩니다. [SE-0161]
key path를 통하여 프로퍼티에 접근할 수 있도록 모든 타입에 자동으로 [keyPath:] 서브스크립트 메서드가 추가됩니다.
key path는 백슬래시(\)를 통해 표현합니다.
struct Person {
var name: String
}
struct Stuff {
var name: String
var owner: Person
}
var yagom = Person(name: "yagom")
let macbook = Stuff(name: "macbook pro", owner: yagom)
print(type(of: \Person.name)) // WritableKeyPath<Person, String>
var result: Any = macbook[keyPath: \Stuff.name]
print(result) // macbook pro
let keyPath = \Stuff.owner
let nameKeyPath = keyPath.appending(path: \.name)
result = macbook[keyPath: nameKeyPath]
print(result) // yagom
result = macbook[keyPath: \Stuff.owner.name]
print(result) // yagom
프로토콜 혼합 타입
Objective-C 에서 특정 클래스와 프로토콜을 동시에 따르고 있는 타입이라는 의미로 사용되었던 표현이 있습니다.
SomeClass<SomeProtocol, AnotherProtocol> *name
// SomeProtocol과 AnotherProtocol을 준수하는 SomeClass 타입의 변수 name
이 표현이 이제 스위프트에서도 가능해졌습니다. [SE-0156]
class ClassA { }
class ClassB: ClassA { }
protocol ProtocolA { }
extension ClassB: ProtocolA { }
var someVariable: ClassA & ProtocolA
// someVariable = ClassA()
// 오류발생 : ProtocolA를 충족하지 않음
someVariable = ClassB() // 모든 조건 충족
where 확장
프로토콜과 그 연관 타입에 where 절을 사용하여 타입 제약을 줄 수 있습니다. [SE-0142]
protocol StringRepresentable: RawRepresentable
where RawValue == String { }
protocol RawStringWrapper {
associatedtype Wrapped: RawRepresentable
where Wrapper.RawValue == String
}
제네릭 서브스크립트
서브스크립트가 이제 제네릭 매개변수와 반환 타입을 사용할 수 있게 되었습니다. [SE-0148]
struct CustomModel<Key: Hashable, Value> {
var dictionary: [Key: Value]
subscript<T>(key: Key) -> T? {
return dictionary[key] as? T
}
}
let information = CustomModel(dictionary: ["name": "yagom", "age": 100, "height": 183.0])
let name: String? = information["name"] // yagom
Private 접근수준의 변경
기존의 private 접근수준은 같은 파일인 여부와 상관없이 private 요소를 해당 범위를 벗어나면 사용할 수 없었습니다. 대체제로 fileprivate를 사용하였는데, 이는 문제를 야기할 수 있는 가능성을 가지고 있습니다. 같은 파일이라도 접근을 원치 않는 요소가 있는데, extension 등으로 타입을 확장하여 요소를 사용려면 fileprivate로 지정해 주어야 하는 문제가 있었기 때문이죠. 아래 Swift 3의 예를 먼저 보겠습니다.
// Swift 3
struct Person {
var name: String
// private로 지정하면 extension에서 접근할 수 없기 때문에 fileprivate로 선언
fileprivate var age: Int = 0
init(name: String) {
self.name = name
}
}
extension Person {
mutating func passedYear() {
self.age += 1
}
}
var yagom = Person(name: "yagom")
yagom.passedYear()
yagom.age // 접근가능! 원하던 시나리오가 아님!
나이라는 민감한 부분은 숨기고 싶었기 때문에 private로, 어디서든 접근을 막고 싶었지만, 같은 파일의 익스텐션에서 조차 접근할 수 없기 때문에 fileprivate로 접근수준을 지정했습니다. 그 결과, 엉뚱하게 같은 파일의 다른 소스에서도 접근할 수 있게 되었습니다.
// Swift 4
struct Person {
var name: String
private var age: Int = 0
init(name: String) {
self.name = name
}
}
extension Person {
mutating func passedYear() {
// private라도 같은 파일의 extension에서 접근 가능
self.age += 1
}
}
var yagom = Person(name: "yagom")
yagom.passedYear()
// yagom.age // 접근불가
Swift 4에서는 이 문제를 개선하여 같은 파일의 익스텐션에서는 접근할 수 있도록 변경되었습니다.
아카이브와 시리얼라이제이션 / JSON 인코딩
NSCoding과 NSObject를 손쉽게 사용할 수 있는 클래스 타입을 제외하고, 구조체나 열거타입에서는 아카이빙이 참으로 난해했습니다. 물론 NSObject를 상속받지 않는 스위프트 고유 클래스도 마찬가지였습니다. 그러나 이제 타입이 스스로 어떻게 아카이브하고 시리얼라이즈 할지 정의할 수 있게 되었습니다. 그저 타입과 그 타입의 하위 타입이 모두 Codable 프로토콜을 준수하면 아카이브하고 그것을 풀어낼 수 있도록 할 수 있습니다. 기존에 사용하던 NSKeyedArchiver 클래스도 Codable 프로토콜을 완벽히 지원합니다. Codable과 함께 JSONEncoder, JSONDecoder로 인해 JSON 인코딩과 디코딩이 엄청나게 편해졌습니다!! 으아아아아아아아~!! [SE-0166], [SE-0167]
struct Person: Codable {
enum Gender: String, Codable {
case male, female, unknown
}
var name: String
var age: Int
var gender: Gender
var friends: [Person]
}
let yagom = Person(name: "yagom", age: 20, gender: .male, friends: [])
let hana = Person(name: "hana", age: 22, gender: .female, friends: [yagom])
let eric = Person(name: "eric", age: 25, gender: .male, friends: [yagom, hana])
var encoder = JSONEncoder()
let jsonData = try encoder.encode(eric)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString)
// "{\"age\":25,\"gender\":\"male\",\"friends\":[{\"age\":20,\"gender\":\"male\",\"friends\":[],\"name\":\"yagom\"},{\"age\":22,\"gender\":\"female\",\"friends\":[{\"age\":20,\"gender\":\"male\",\"friends\":[],\"name\":\"yagom\"}],\"name\":\"hana\"}],\"name\":\"eric\"}"
let decoder = JSONDecoder()
let decoded: Person = try decoder.decode(Person.self, from: jsonData)
print(decoded.name) // eric
NSNumber 브릿징
예상치 못한 결과를 낼 수 있는 NSNumber 브릿징 결과가 수정되었습니다.
// Swift 3
let numberOne = NSNumber(value: Int64.max)
if numberOne is Int16 {
print("numberOne == Int16")
} else if numberOne is Int64 {
print("numberOne == Int64")
} // numberOne == Int16
let numberTwo = NSNumber(value: UInt32(777))
if let value = numberTwo as? Int8 {
print(value)
} // 9
// Swift 4
let numberOne = NSNumber(value: Int64.max)
if numberOne is Int16 {
print("numberOne == Int16")
} else if numberOne is Int64 {
print("numberOne == Int64")
} // numberOne == Int64
let numberTwo = NSNumber(value: UInt32(777))
if let value = numberTwo as? Int8 {
print(value)
} // 아무 출력 없음
그외 기타 변경 사항
* Core Foundation 타입들은 Hashable과 Equatable 프로토콜을 CFHash와 CFEqual 함수구현을 통해 준수합니다. Swift 3 모드(Swift 3.2 버전)에서도 적용되는 사항입니다. [SR-2388]
* 암시적인 @objc 선언을 이제 더욱 명확히 표현해 주어야 합니다. [SE-0160]
* raw buffer를 슬라이스 한 결과는 더이상 같은 타입으로 반환되지 않습니다. [SE-0138]
* 재정의(override)가 더 완벽하게 지원됩니다. [SR-1529]
* 제네릭 매개변수를 갖는 서브스크립트를 정의할 수 있습니다. [SE-0148]
* 스위프트 타입 시스템이 이제는 하나의 튜플 전달인자를 가지는 함수와 여러 매개변수를 가지는 함수를 구분합니다. [SE-0110]
* 여러 연산자를 수반한 정수형 상수를 정의하는 C 언어의 매크로의 형태를 더 다양하게 임포트 할 수 있습니다.
* inout 매개변수를 사용하는 reduce 함수가 추가되었습니다. [SE-0171]
* 컬렉션 타입의 두 요소를 서로 바꿔주는 swapAt(_:_:) 메서드가 추가됐습니다. [SE-0173], [SE-0176]