반응형

Hacking with Swift 사이트의 강좌 번역본입니다.

[원문 : https://www.hackingwithswift.com/articles/256/whats-new-in-swift-5-8]

What’s new in Swift 5.8

결과 빌더 내에서의 변수에 대한 모든 제한 해제 (Lift all limitations on variables in result builders)

SE-0373 결과 빌더 내에서 사용될때 변수에 대한 일부 제한을 완화해서, 컴파일러에서 허용하지 않았던 코드들을 작성할 수 있도록 합니다.

예를들어, Swift 5.8에서는 다음과 같이 결과 빌더 내에서 직접 lazy 변수를 사용할 수 있습니다.

struct ContentView: View {
    var body: some View {
        VStack {
            lazy var user = fetchUsername()
            Text("Hello, \(user).")
        }
        .padding()
    }

    func fetchUsername() -> String {
        "@twostraws"
    }
}

이는 개념을 보여주지만, lazy한 변수가 항상 사용되기 때문에 어떤 장점도 없습니다 - lazy var과 let 코드 간에 다른게 없습니다. 실제로 유용한 부분을 확인하기 위해서 다음과 같이 더 긴 코드를 봅시다. 

// The user is an active subscriber, not an active subscriber, or we don't know their status yet.
enum UserState {
    case subscriber, nonsubscriber, unknown
}

// Two small pieces of information about the user
struct User {
    var id: UUID
    var username: String
}

struct ContentView: View {
    @State private var state = UserState.unknown

    var body: some View {
        VStack {
            lazy var user = fetchUsername()

            switch state {
            case .subscriber:
                Text("Hello, \(user.username). Here's what's new for subscribers…")
            case .nonsubscriber:
                Text("Hello, \(user.username). Here's why you should subscribe…")
                Button("Subscribe now") {
                    startSubscription(for: user)
                }
            case .unknown:
                Text("Sign up today!")
            }
        }
        .padding()
    }

    // Example function that would do complex work
    func fetchUsername() -> User {
        User(id: UUID(), username: "Anonymous")
    }

    func startSubscription(for user: User) {
        print("Starting subscription…")
    }
}

이 접근방식은 둘중 하나로 나타날수 있는 문제를 해결합니다.

  • lazy를 사용하지 않는 경우에, fetchUserName()는 하나도 사용되지 않는 경우에도, 3가지 케이스에서 모두 호출됩니다.
  • lazy를 제거하고 fetchUsername()을 2가지 케이스 내부에 배치하도록 코드를 복사합니다 - 단순히 한 줄로는 큰 문제가 되지 않지만, 이보다 더 복잡한 코드에서는 문제가 될 수 있습니다
  • user를 계산된 프로퍼티로 변경하면, 지금 구독(Subscribe now) 버튼을 클릭할때 두번 호출됩니다. 

이 변경으로 인해 결과 빌더 내에서 프로퍼티 래퍼와 로컬 계산 프로퍼티를 사용할 수 있지만 유용하지는 않을 것입니다. 예를들어, 다음과 같은 종류의 코드가 이제 허용됩니다.

struct ContentView: View {
    var body: some View {
        @AppStorage("counter") var tapCount = 0

        Button("Count: \(tapCount)") {
            tapCount += 1
        }
    }
}

하지만, 이렇게 하면, UserDefaults 값이 변할때마다 기본 값이 변경되지만 @AppStorage을 이런식으로 사용하면 매번 tapCount를 변경할때마다 body 프로퍼티가 다시 호출되지 않습니다. - UI는 변경사항을 반영하도록 자동으로 업데이트 되지 않습니다.

함수 다시 배포 (Function back deployment)

SE-0376에서 이전 버젼의 프레임워크에서 새 API를 사용할소 있도록 하는 @backDeployed 속성을 새로 추가했습니다. 앱의 바이너리에 함수에 대한 코드를 작성하고나서 런타임 검사를 수행합니다: 사용자가 적절한 새로운 운영체제를 사용하는 경우에 시스템 자체 버젼의 함수가 사용되며, 그렇지 않으면 앱의 바이너리에 있는 버젼이 복사되어 사용될 것입니다.

표면적으로는 Apple이 이전 운영체제에서 사용할 수 있는 몇가지 새로운 기능을 만드는 멋진 방법으로 들리지만 묘책이라고 생각하지 않습니다 - @backDeployed는 함수(functions), 메소드(methods), 첨자(subscripts), 계산 프로퍼티(computed properties)에만 적용되므로, iOS 16.1 에서 도입된 fontDesign() 수정자와 같은 소규모 API 변경에는 잘 동작할 수 있지만, ScrollBounceBehavior 구조체에 의존하는 새로운 scrollBounceBehavior() 수정자 처럼 새로운 타입을 사용해야 하는 코드에는 동작하지 않습니다. 

예를들어, iOS 16.4는 Text의 변형에 대한 monospaced(_ isActive:)를 토입했습니다. @backDeployed를 사용하는 경우에, SwiftUI 팀은 다음과 같이 실제로 필요한 구현 코드를 지원하는 가장 초기 버젼의 SwiftUI에서 수정자를 사용할수 이는지 확인 할 수 있습니다.

extension Text {
    @backDeployed(before: iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4)
    @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
    public func monospaced(_ isActive: Bool) -> Text {
        fatalError("Implementation here")
    }
}

수정자가 이처럼 구현된 경우에, 런타임에 Swift는 해당 수정자를 이미 가지고 있는 경우에 시스템의 SwiftUI의 복사본을 사용하고, 그렇지 않으면 iOS 14.0 용으로 다시 배포된 버젼을 사용합니다. 실제로는 새로운 타입을 공객적으로 노출하지 않으므로 다시 배포(back deployment)를 위한 쉬운 선택처럼 보이지만 SwiftUI가 내부적으로 어떤 타입을 사용하는지 모르기 때문에, 다시 배포(back deployment)할수 있는 것과 아닌 것을 예측하기가 쉽지 않습니다.

self가 래핑해제되고나서, 약한 self 캡쳐에 대한 암시적인 self 허용(Allow implicit self for weak self captures, after self is unwrapped)

SE-0365는 클로져에서 weak self 캡쳐가 언래핑된 곳에서 self를 암시적으로 허용함으로써 self를 제거할수 있게 해줍니다. 

예를들어, 아래 코드에는 클로져가 self가 약하게 캡쳐하고 있지만, self를 바로 언래핑합니다.

class TimerController {
    var timer: Timer?
    var fireCount = 0

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
            guard let self else { return }
            print("Timer has fired \(fireCount) times")
            fireCount += 1
        }
    }
}

해당 코드는 fireCount의 인스턴스 모두 self.fireCount로 작성해야 했기에, Swift 5.8 이전에는 컴파일되지 않을 것입니다. 

마법같은 간결한 파일이름(Concise magic file names)

SE-0274는 MyApp/ContentView.swift처럼Module/Filename 포멧을 사용하도록 #file마법 식별자를 변경했습니다. 이전에는 #file은 Swift 파일의 전체 경로를 포함했습니다.

예를들어, /Users/twostraws/Desktop/WhatsNewInSwift/WhatsNewInSwift/ContentView.swift. 불필요하고 길고 공개하고 싶지 않는 내용이 포함될수 있습니다. 

중요: 현재 이 동작은 기본적으로 활성화 되지 않습니다. SE-0362 는 개발자가 언어에서 전부 활성화하기 전애 새로운 기능을 선택할 수 있도록 하는-enable-upcoming-feature라는새로운 컴파일러 플래그를 설계 했으므로, -enable-upcoming-feature ConciseMagicFile을 Xcode에 있는 Other Swift Flags에 추가하면 새로운 #file 동작을 활성화합니다.

해당 플러그를 활성화 하고 이전 동작을 사용하려면 대신 #filePath를 사용해야 합니다.

// New behavior, when enabled
print(#file)

// Old behavior, when needed
print(#filePath)

이러한 변경사항에 대한 Swift Evolution 제안은 바이너리 크기와 실행 선능에 놀랄만큼의 큰 개선사항을 언급하고 전체 경로를 갖는 것이 왜 안 좋은 생각인지를 설명하는 매우 훌륭한 내용이 있기에 읽을 만합니다.

소스 파일의 전체 경로에는 개발자의 사용자이름, 빌드 환경에 대한 도움말, 특정 버젼이나 식별자 또는 외부 디스크의 이름을 따서 붙인 Sailor Scout가 포함될 수 있습니다.

옵셔널 매개변수에 대한 실제 인자값 열기 (Opening existential arguments to optional parameters)

SE-0375는 프로토콜을 사용해서 제네릭 함수를 호출할 수 있도록 하는 Swift 5.7의 기능을 확장해서 조금 작지만 성가신 모순을 수정합니다: Swift 5.7은 해당 동작을 옵셔널로 허용하지 않았지만, Swift 5.8은 허용합니다.

예를들어, 다음 코드는 옵셔널이 아닌 T 매개변수를 사용하기 때문에 Swift 5.7에서 훌륭하게 동작했습니다.

func double<T: Numeric>(_ number: T) -> T {
    number * 2
}

let first = 1
let second = 2.0
let third: Float = 3

let numbers: [any Numeric] = [first, second, third]

for number in numbers {
    print(double(number))
}

Swift 5.8에서서, 동일한 매개변수가 다음과 같이 옵셔널일 수가 있습니다.

func optionalDouble<T: Numeric>(_ number: T?) -> T {
    let numberToDouble = number ?? 0
    return  numberToDouble * 2
}

let first = 1
let second = 2.0
let third: Float = 3

let numbers: [any Numeric] = [first, second, third]

for number in numbers {
    print(optionalDouble(number))
}

Swift 5.7에서 any Numberic 타입은 Numberic을 준수할 수 없습니다. 라는 당황스러운 오류 메시지를 발생되었을 것이므로, 이러한 불일치가 해결되는 것이 좋습니다. 

형변환 패턴에서 컬렉션 하위형변환이 허용됩니다 (Collection downcasts in cast patterns are now supported)

컬력센을 하위형변환(downcasting) 하는 것은 어떤 상황에서는 허용되지 않는 Swift의 또 다른 작지만 성가진 모순을 해결합니다 - 예를들어, ClassA의 배열을 ClassA로 부터 상속받은(inherits) 또 다른 타입의 배열로 형변환합니다. 

예를들어, 다음 코드는 이제 Swift 5.8에서는 유효하지만 이전에는 동작하지 않을 것입니다.

class Pet { }
class Dog: Pet {
    func bark() { print("Woof!") }
}

func bark(using pets: [Pet]) {
    switch pets {
    case let pets as [Dog]:
        for pet in pets {
            pet.bark()
        }
    default:
        print("No barking today.")
    }
}

Swift 5.8 이전에는 형변환 패턴에서 컬렉션 하위형변환이 구현되지 않았습니다. 대신 [Dog]에 대한 명시적인 하위형변완을 사용합니다. 실제로 다음과 같은 구문이 if let dogs = pets as? [Dog] {은 잘 동작하므로 오류가 거의 발생하지 않을거라 생각합니다. 하지만 이러한 변경은 다른 언어의 불일치가 해결되는 것을 의미하며, 언제는 환영합니다.

그리고 더! (And there’s more!)

간단하게 언급할만한 가치가 있는 2가지 변경사항이 있습니다.

첫째, SE-0368은 향후에 새롭고 더 큰 정수 타입을 쉽게 추가할수 있도록 하는 새로운 StaticBigInt 타입을 도입합니다. 

두번째, SE-0372은 Swift의 정렬 함수들의 문서를 수정해서 안정적인(stable) 것으로 표시합니다. 배열의 두 요소가 동일한 것으로 간주되면 정렬된 배열에서 동일한 상대적인 순서(정렬된 배열과 원래 순서)로 유지됩니다. Swift의 정렬은 한동안 안정적이었지만 이제는 공식화 되었습니다.

 

 

(추후 업데이트)

반응형

'Swift > Tip' 카테고리의 다른 글

What’s new in Swift 5.9  (0) 2023.06.12
Array Extension  (0) 2023.05.08
What’s new in Swift 5.7  (0) 2022.08.08
Type의 문자열 이름 사용하기  (0) 2022.04.01
What’s new in Swift 5.6  (0) 2022.03.21
Swift version과 Xcode version  (0) 2022.02.09
What’s new in Swift 5.5  (0) 2022.01.25
What’s new in Swift 5.4  (0) 2021.04.15
Posted by 까칠코더
,