Do.

WWDC23 - What's new in swift 본문

iOS

WWDC23 - What's new in swift

Hey_Hen 2023. 6. 7. 16:47

What’s new in swift

“pack”

기존에는 타입추론을 통해서 이러한 동작이 가능했습니다.

struct Request<Result> { ... }

struct RequestEvaluator {
    func evaluate<Result>(_ request: Request<Result>) -> Result
}

func evaluate(_ request: Request<Bool>) -> Bool {
    return RequestEvaluator().evaluate(request)
}

Result가 Boolean 이라서 Boolean으로 값이 출력됩니다.

만약 여러 Boolean 결과를 얻고 싶다면

let value = RequestEvaluator().evaluate(request)

let (x, y) = RequestEvaluator().evaluate(r1, r2)

let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)

Method Overloads 패턴으로 아래와 같이 작성해줄 수 있습니다.

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)

하지만 반복적으로 overload 하는 방법은 한계가있습니다. 만약 3개까지의 오버로드만 했다면 3개까지의 결과만 얻을 수 있죠

Swift5.9에서 제네릭 시스템은 인수 길이에 대한 추상화를 가능하게 합니다. 여러개의 별개의 파라미터를 함께 "Packed"할 수 있죠, 이 새로운 컨셉을 "Pack"이라고 부릅니다.

위에서 작성했던 메서드를 아래와 같이 each 키워드로 쓰게 되면

func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)

하나의 메서드만으로 오버로딩으로 작성한 모든 코드를 커버할 수 있게됩니다.

let results = RequestEvaluator.evaluate(r1, r2, r3)

swift macros

Swift Macro는 다른 언어(C,C++)에서 존재하던 그 매크로를 그대로 가져온 것으로 컴파일 타임에 지정된 매크로 코드 블럭으로 풀어서 빌드됩니다.

assert(max(a, b) == c)

예를 들어 위와 같이 기존의 assert 코드는 조건이 실패하면 앱이 중단됩니다. 하지만 구체적으로 무엇이 문제였는지 알 수 없죠. 아래처럼 단지 실패했다고만 출력됩니다.

반면 우리가 테스트에 사용하는 XCAssertEqual은 좀더 나은 결과를 보여줍니다.

하지만 여전히 a는 뭐였고 또 b는 무엇인지 그리고 c는 무엇인지 아무것도 알 수 없습니다.

요지는 우리가 무언가 실패했을 때 로그에서 더 많은 정보를 얻길 바란다는 것입니다.

swift 5.9 이전까지는 특별히 만든 기능 없이는 이 요구사항을 충족할 수 없습니다. 매크로 없이는요!

해당 예제에서는 “hash-assert”라고 불리는 문법을 사용하고 있습니다. 기존 assert문법 앞에 #을 붙이는게 다죠

그 결과 우리는 아주 세부적인 결과를 얻을 수 있습니다. “hash-assert”는 PowerAssert 패키지안에 내장되어 있습니다.

PowerAssert는 오픈소스로 Github에서 찾을 수 있습니다.

내부 코드를 보면 “hash assert”는 하나의 함수인데요

public macro assert(_ condition: Bool)

위 사진은 대부분의 매크로가 #externalMacro를 사용하고 있고 Swift Complier와 Complier Plugin끼리 어떻게 동작하는지 보여주고 있습니다.

Freestanding macro roles

asset macro는 freestanding expression 매크로의 일종입니다.

freestanding 라고 부르는 이유가 “hash” 문법을 통해 매크로 문법을 사용하기 때문에 그렇다고 합니다!

@freestanding(expression)
public macro assert(_ condition: Bool) = #externalMacro(
    module: “PowerAssertPlugin”,
    type: “PowerAssertMacro"
)

freestanding의 또다른 좋은 예제는 SwiftUI와 SwiftData에서 사용 가능한

let pred = #Predicate<Person> {
    $0.favoriteColor == .blue
}

let blueLovers = people.filter(pred)

위 freestanding 매크로는 제네릭 인풋으로 결과를 생성하는 코드입니다.

// Predicate expression macro

@freestanding(expression) 
public macro Predicate<each Input>(
    _ body: (repeat each Input) -> Bool
) -> Predicate<repeat each Input>

Reduce Boilerplate

매크로는 또한 수많은 보일러 플레이트를 줄이는데 도움이 됩니다.

예를들어 아래와 같은 연관값이 있는 열거형의 경우

enum Path {
    case relative(String)
    case absolute(String)
}

열거형의 케이스 필터링이 필요할 때 아래와 같이 쓰고 싶을 수 있습니다.

이를 위해서는 열거형의 computed property를 생성해야 합니다.

let absPaths = paths.filter { $0.isAbsolute }
extension Path {
    var isAbsolute: Bool {
        if case .absolute = self { true }
        else { false }
    }
}

간단한 코드지만 케이스가 많아질 수록 보일러 플레이트가 증가하게 되는 구조입니다.

Swift Macro는 아래와 같은 문법으로 보일러 플레이트를 줄여줍니다.

@CaseDetection
enum Path {
    case relative(String)
    case absolute(String)
}

let absPaths = paths.filter { $0.isAbsolute }

@CaseDetection propertyWrapper 매크로를 사용하게 되면 깔끔하게 해결되죠

CaseDetection의 경우는 member에 해당되는 attached입니다.

Observation in SwiftUI

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)")
    }
}

SwiftUI에서 Class타입의 변경을 감지하기 위해 ObservableObject를 채용하고 방출이 필요한 이벤트를 Published Wrapper로 감쌉니다. 하지만 속성이 늘어나면 이 또한 보일러 플레이트이죠

이 또한 Swift5.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)")
    }
}

ObservableObject 프로토콜도 쓸 필요 없고 속성마다 Published를 선언할 필요도 없죠

Observable은 아래와 같은 3가지 매크로 기능을 사용한 것입니다.

@attached(member, names: ...)
@attached(memberAttribute)
@attached(conformance)
public macro Observable() = #externalMacro(...).

실제로는 매크로를 통해 아래와 같은 코드가 완성됩니다.

@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)
    }
}

Foundation performance improve

calendar calculations: 20% faster

date formatting: 150% faster

json coding: 200-500% faster

swift copyable

example 1

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)
  }
}

FileDescriptor는 파일을 열고 쓰는 모델인데 실수의 여지가 있다.

작업이 끝난 이후에 명시적으로 close를 호출해야 하는데 코드를 누락할 확률이 있음

그래서 다음과 같이 수정할 수 있는데

example 2

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 {
    close()
  }
}

클래스로 변경 후 메모리에서 해제되는 시점에 close를 호출하는 방법이 있다. 하지만 클래스로 변경되면서 발생하는 여러 오버헤드가 있음!

Copyable은 구조체에서도 클래스 처럼 deinit 호출이 가능하도록 함

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)
  }
}

또 다른 방법은 호출 시점에 강제하는 것인데

메서드에 consuming이라는 코드를 부착합니다.

  consuming func close() {
    Darwin.close(fd)
  }

이렇게 되면 호출할 때 호출 순서를 강제할 수 있습니다.

let file = FileDescriptor(fd: descriptor)
file.write(buffer: data)
file.close()

위 코드는 정상일 때의 예제이고 아무 문제가 발생하지 않습니다.

let file = FileDescriptor(fd: descriptor)
file.close() // Compiler will indicate where the consuming use is
file.write(buffer: data) // Compiler error: 'file' used after consuming

write전에 close를 쓰게 되면 컴파일 에러가 발생하게 됩니다.

Using C++ from swift / C++ interop

swift에서 c++를 다이렉트로 쓸 수 있는 방법이 새로 들어왔습니다.

// Person.h
struct Person {
  Person(const Person &);
  Person(Person &&);
  Person &operator=(const Person &);
  Person &operator=(Person &&);
  ~Person();

  std::string name;
  unsigned getAge() const;
};
std::vector<Person> everyone();

// Client.swift
func greetAdults() {
  for person in everyone().filter { $0.getAge() >= 18 } {
    print("Hello, \\(person.name)!")
  }
}

swift 컴파일러는 기본적으로 c++ 언어를 대다수 이해합니다. 예를들면 위 코드에서처럼 move constructors, assignment operators, destructor 등을 사용할 수 있고 vector나 map을 사용할 수 있습니다.

Using Swift from C++

반대로 C++ 에서도 Swift코드를 호출할 수 있습니다.

// Geometry.swift
struct LabeledPoint {
  var x = 0.0, y = 0.0
  var label: String = “origin”
  mutating func moveBy(x deltaX: Double, y deltaY: Double) { … }
  var magnitude: Double { … }
}

// C++ client
#include <Geometry-Swift.h>

void test() {
  Point origin = Point()
  Point unit = Point::init(1.0, 1.0, “unit”)
  unit.moveBy(2, -2)
  std::cout << unit.label << “ moved to “ << unit.magnitude() << std::endl;
}

추가로 Objective-C처럼 objc attribute를 추가할 필요도 없으며, 별도의 브리징 오버헤드 없이 Swift 타입의 모든 프로퍼티들, 메서드들, 생성자를 추가할 수 있습니다.

FoundationDB

FoundationDB는 분산 Database로 확장 가능한 거대한 key-value 솔루션입니다. macOS, Linux, Windows 환경을 지원하죠

C++로 작성된 오픈 소스이며 뭔 내용인지 잘 모르겠으므로 생략합니다. ㅎ

Comments