Swift project update
Expressivew code
if-else와 switch문을 expression으로 사용 가능해졌다.
// as-is
let bullet =
isRoot && (count == 0 || !willExpand) ? ""
: count == 0 ? "- "
: maxDepth <= 0 ? "▹ " : "▿ "
// to-be
let bullet =
if isRoot && (count == 0 || !willExpand) { "" }
else if count == 0 { "- " }
else if maxDepth <= 0 { "▹ " }
else { "▿ " }
전역변수 등을 초기화 할 때도 클로져를 만들어서 즉석에서 실행하는 등의 테크닉을 쓸 필요가 줄었다.
let attributedName =
if let displayName, !displayName.isEmpty {
AttributedString(markdown: displayName)
} else {
"Untitled"
}
resultBuilder 개선
기존에는 타입 체커가 올바르지 않은 경로도 일일이 탐사하느라 검사가 오래 걸렸다.
struct ContentView: View {
enum Destination { case one, two }
var body: some View {
List {
NavigationLink(value: .one) { //In 5.9, Errors provide a more accurate diagnostic
Text("one")
}
NavigationLink(value: .two) {
Text("two")
}
}.navigationDestination(for: Destination.self) {
$0.view // Error occurs here in 5.7
}
}
}
Generic
기존에 Generic인자를 여러개 쓰기 위해서는 일일이 정의해줘야 했다.
struct Request<Result> { ... }
struct RequestEvaluator {
func evaluate<Result>(_ request: Request<Result>) -> Result
}
func evaluate(_ request: Request<Bool>) -> Bool {
return RequestEvaluator().evaluate(request)
}
let value = RequestEvaluator().evaluate(request)
let (x, y) = RequestEvaluator().evaluate(r1, r2)
let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
// 위 코드를 위해서는 일일이 정의
func evaluate<Result>(_:) -> (Result)
func evaluate<R1, R2>(_:_:) -> (R1, R2)
func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3)
func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4)
func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5)
func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6)
이러면 정의되지 않은 인자 갯수가 들어오면 컴파일 에러가 난다. 이는 임의적으로 생기는 API의 한계다.
//This will cause a compiler error "Extra argument in call"
let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7)
5.9에서는 제네릭 인자의 길이도 추상화가 가능해졌다. 이는 여러 타입 인자를 함께 묶어서(packed)받는 게 가능해진 덕분이다.
<each Result>
이제 인자별로 여러개 정의 안해도 된다.
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
쓰는 쪽에서는 기존처럼 쓰면 된다.
struct Request<Result> { ... }
struct RequestEvaluator {
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
}
let results = RequestEvaluator.evaluate(r1, r2, r3)
매크로
매크로를 통해서 보일러플레이트를 줄이면서 언어의 표현력을 높일 수 있다.
예시: assert
assert(max(a, b) == c) // max_assert.swift:5 Assertion failed
XCAssertEqual(max(a, b), c) //XCTAssertEqual failed: ("10") is not equal to ("17")
#assert(max(a, b) == c) // 소스코드를 읽어서 좀 더 풍부한 정보를 제공해줄 수 있다.
Swift에서의 매크로는 타입과 함수와 동일한 API기 때문에 패키지를 통해서 배포되고 이를 정의한 모듈을 import함으로써 사용할 수 있다.
import PowerAssert
#assert(max(a, b) == c)
모듈내에서 정의하는 부분을 보면 함수랑 크게 다를 게 없다.
반환값이 필요하면 함수처럼 →을 사용하면 된다.
public macro assert(_ condition: Bool)
타입 체킹도 된다.
import PowerAssert
#assert(max(a, b)) //Type 'Int' cannot be a used as a boolean; test for '!= 0' instead
매크로 정의 및 동작 원리
매크로가 정의된 모듈과 해당 매크로를 정의한 타입을 string으로 명시한다.
public macro assert(_ condition: Bool) = #externalMacro(
module: “PowerAssertPlugin”,
type: “PowerAssertMacro"
)
매크로는 별도 프로그램으로 정의되어 컴파일러의 플러그인으로 들어간다.
컴파일러가 소스코드를 넘기면, 이 프로그램이 새로운 코드를 반환해서 컴파일러가 이를 통합하는 방식
// input
#assert(a == b)
// output
PowerAssert.Assertion(
"#assert(a == b)"
) {
$0.capture(a, column: 8) == $0.capture(b, column: 13)
}
매크로 선언에는 해당 매크로의 역할도 명시한다.
ex. assert는 freestanding(expression)이다. 이는 독립적으로 expression으로 쓰인다는 뜻이다.
// Freestanding macro roles
@freestanding(expression)
public macro assert(_ condition: Bool) = #externalMacro(
module: “PowerAssertPlugin”,
type: “PowerAssertMacro"
)
ex. Foundation의 새로운 Predicate API
let pred = #Predicate<Person> {
$0.favoriteColor == .blue
}
let blueLovers = people.filter(pred)
// Predicate expression macro
@freestanding(expression)
public macro Predicate<each Input>(
_ body: (repeat each Input) -> Bool
) -> Predicate<repeat each Input>
ex. caseDetection. enum에서 특정 케이스인지를 확인하는 기능
// as-is
enum Path {
case relative(String)
case absolute(String)
}
let absPaths = paths.filter { $0.isAbsolute }
extension Path {
var isAbsolute: Bool {
if case .absolute = self { true }
else { false }
}
}
extension Path {
var isRelative: Bool {
if case .relative = self { true }
else { false }
}
}
// to-be
@CaseDetection
enum Path {
case relative(String)
case absolute(String)
}
let absPaths = paths.filter { $0.isAbsolute }
// 위 매크로는 다음코드로 expantion 된다.
enum Path {
case relative(String)
case absolute(String)
//Expanded @CaseDetection macro integrated into the program.
var isAbsolute: Bool {
if case .absolute = self { true }
else { false }
}
var isRelative: Bool {
if case .relative = self { true }
else { false }
}
}
attached macro의 역할
attached macro는 여러개가 합쳐질 수 있다.
ex. observation
// Observation in SwiftUI(5.9 이전)
final class Person: ObservableObject {
@Published var name: String
@Published var age: Int
@Published var isFavorite: Bool
}
struct ContentView: View {
@ObservedObject var person: Person
var body: some View {
Text("Hello, \\(person.name)")
}
}
// Observation in SwiftUI(5.9 이후)
@Observable final class Person {
var name: String
var age: Int
var isFavorite: Bool
}
struct ContentView: View {
var person: Person
var body: some View {
Text("Hello, \\(person.name)")
}
}
Obsrevation 매크로 정의
// Observation 매크로 정의
@attached(member, names: ...)
@attached(memberAttribute)
@attached(conformance)
public macro Observable() = #externalMacro(...).
매크로는 순서대로 펼쳐진다.
안 펼쳐진 상태
@Observable final class Person {
var name: String
var age: Int
var isFavorite: Bool
}
member 매크로를 펼친 상태
@Observable final class Person {
var name: String
var age: Int
var isFavorite: Bool
internal let _$observationRegistrar = ObservationRegistrar<Person>()
internal func access<Member>(
keyPath: KeyPath<Person, Member>
) {
_$observationRegistrar.access(self, keyPath: keyPath)
}
internal func withMutation<Member, T>(
keyPath: KeyPath<Person, Member>,
_ mutation: () throws -> T
) rethrows -> T {
try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
}
}
memberAttribute 매크로 펼친 상태
@Observable final class Person {
@ObservationTracked var name: String
@ObservationTracked var age: Int
@ObservationTracked var isFavorite: Bool
internal let _$observationRegistrar = ObservationRegistrar<Person>()
internal func access<Member>(
keyPath: KeyPath<Person, Member>
) {
_$observationRegistrar.access(self, keyPath: keyPath)
}
internal func withMutation<Member, T>(
keyPath: KeyPath<Person, Member>,
_ mutation: () throws -> T
) rethrows -> T {
try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
}
ObservationTracked 매크로에 의해서 accessor가 추가된 형태
@Observable final class Person {
@ObservationTracked var name: String { get { … } set { … } }
@ObservationTracked var age: Int { get { … } set { … } }
@ObservationTracked var isFavorite: Bool { get { … } set { … } }
internal let _$observationRegistrar = ObservationRegistrar<Person>()
internal func access<Member>(
keyPath: KeyPath<Person, Member>
) {
_$observationRegistrar.access(self, keyPath: keyPath)
}
internal func withMutation<Member, T>(
keyPath: KeyPath<Person, Member>,
_ mutation: () throws -> T
) rethrows -> T {
try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
}
}
comformance 매크로에 의해 Observable 프로토콜 채택 추가
@Observable final class Person: Observable {
@ObservationTracked var name: String { get { … } set { … } }
@ObservationTracked var age: Int { get { … } set { … } }
@ObservationTracked var isFavorite: Bool { get { … } set { … } }
internal let _$observationRegistrar = ObservationRegistrar<Person>()
internal func access<Member>(
keyPath: KeyPath<Person, Member>
) {
_$observationRegistrar.access(self, keyPath: keyPath)
}
internal func withMutation<Member, T>(
keyPath: KeyPath<Person, Member>,
_ mutation: () throws -> T
) rethrows -> T {
try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
}
}
이렇게 만들어진 코드는 일반적인 Swift코드다.
Xcode에서는 매크로를 자동으로 펼쳐줄 수 있는 기능을 제공한다.
Swfit everywhere
Swift-Foundation 프로젝트 시작 및 오픈소스화
애플 플랫폼과 비애플 플랫폼이 코드를 공유한다.
그와 동시에 엄청난 양의 Objective-C와 C코드를 Swift로 재작성한다는 뜻이기도 하다.
macOS sonoma와 iOS 17부터 Date, Calendar, Locale, AttributedString, JSON 인코딩 및 디코딩 등이 Swift로 돌아간다.
이러한 향상은 브릿징 비용 뿐 아니라 Swift 구현체 자체가 더 효율적으로 작성되어서 그렇다.
ownership 개념 추가
low-level에서 좀 더 최적화를 잘 해주기 위한 opt-in 기능
ex. fileDescriptor
struct FileDescriptor {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func write(buffer: [UInt8]) throws {
let written = buffer.withUnsafeBufferPointer {
Darwin.write(fd, $0.baseAddress, $0.count)
}
// ...
}
func close() {
Darwin.close(fd)
}
}
다만 위처럼 만들면 close가 불리지 않을 위험이 있다.
그래서 class로 만들어서 deinit을 써볼 수 있다.
class FileDescriptor {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func write(buffer: [UInt8]) throws {
let written = buffer.withUnsafeBufferPointer {
Darwin.write(fd, $0.baseAddress, $0.count)
}
// ...
}
func close() {
Darwin.close(fd)
}
deinit {
self.close(fd)
}
}
다만 이러면 다른 단점이 있다.
그래서 struct를 쓰되, 복사만 막고 싶다. 그래서 5.9에서는 NonCopyable type이 추가 되었다.
struct FileDescriptor: ~Copyable {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func write(buffer: [UInt8]) throws {
let written = buffer.withUnsafeBufferPointer {
Darwin.write(fd, $0.baseAddress, $0.count)
}
// ...
}
func close() {
Darwin.close(fd)
}
deinit {
Darwin.close(fd)
}
}
추가적으로 close 이후에는 fileDescriptor를 사용하지 못하게 막을 수도 있다.
struct FileDescriptor {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func write(buffer: [UInt8]) throws {
let written = buffer.withUnsafeBufferPointer {
Darwin.write(fd, $0.baseAddress, $0.count)
}
// ...
}
consuming func close() {
Darwin.close(fd)
}
deinit {
Darwin.close(fd)
}
}
let file = FileDescriptor(fd: descriptor)
file.write(buffer: data)
file.close()
file.write(buffer: data) // Compiler error: 'file' used after consuming
다만 아직 초기 단계고, 이후에는 제네릭 코드에도 이 NonCopyable 기능을 쓸 수 있게 해줄 것이다.
C++ 상호운용