API с обратным развертыванием, больше неявных самостоятельных обновлений, улучшенные построители результатов и многое другое!
Несмотря на то, что многие важные изменения Swift в настоящее время просачиваются через Swift Evolution, сам Swift 5.8 представляет собой скорее чистую версию: да, есть дополнения, но есть и другие улучшения, улучшающие функциональность, которая уже широко использовалась. Надеюсь, это должно значительно облегчить внедрение, особенно после гигантского набора изменений, которые были втиснуты в Swift 5.7!
В этой статье я расскажу вам о наиболее важных изменениях на этот раз, предоставив примеры кода и пояснения, чтобы вы могли попробовать все сами. Вам понадобится Xcode 14.3 или более поздняя версия, чтобы использовать это, хотя некоторые изменения требуют определенного флага компилятора, прежде чем Swift 6 наконец появится.
- Совет: Вы также можете загрузить это как playground Xcode, если хотите сами попробовать примеры кода.
Снять все ограничения на переменные в построителях результатов
SE-0373 ослабляет некоторые ограничения на переменные при использовании внутри построителей результатов, позволяя нам писать код, который ранее был бы запрещен компилятором.
Например, в Swift 5.8 мы можем использовать ленивые переменные непосредственно внутри построителей результатов, например так:
struct ContentView: View {
var body: some View {
VStack {
lazy var user = fetchUsername()
Text("Hello, \(user).")
}
.padding()
}
func fetchUsername() -> String {
"@twostraws"
}
}
Это показывает концепцию, но не дает никакой пользы, потому что всегда используется ленивая переменная — нет никакой разницы между использованием 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() вызывалась бы во всех трех случаях state, даже если она не используется ни в одном из них.
- Если бы мы удалили lazy и поместили вызов fetchUsername() в два случая, то мы бы дублировали код — не такая уж большая проблема для простого однострочного кода, но вы можете себе представить, как это вызовет проблемы в более сложном коде.
- Если мы переместим user в вычисляемое свойство, оно будет вызвано во второй раз, когда пользователь нажмет кнопку «Подписаться сейчас».
Это изменение также позволяет нам использовать обертки свойств и локальные вычисляемые свойства внутри построителей результатов, хотя я подозреваю, что они будут менее полезными. Например, сейчас разрешен такой код:
struct ContentView: View {
var body: some View {
@AppStorage("counter") var tapCount = 0
Button("Count: \(tapCount)") {
tapCount += 1
}
}
}
Однако, хотя это приведет к изменению базового значения UserDefaults при каждом касании, использование @AppStorage таким образом не приведет к повторному вызову свойства body при каждом изменении tapCount — наш пользовательский интерфейс не будет автоматически обновляться, чтобы отразить изменение.
Обратное развертывание функции
SE-0376 добавляет новый атрибут @backDeployed, который позволяет разрешить использование новых API в более старых версиях фреймворков. Он работает, записывая код функции в двоичный файл вашего приложения, а затем выполняя проверку во время выполнения: если ваш пользователь использует подходящую новую версию операционной системы, тогда будет использоваться собственная версия функции системы, в противном случае версия, скопированная в ваш вместо этого будет использоваться двоичный файл приложения.
На первый взгляд это звучит как фантастический способ для Apple сделать некоторые новые функции задним числом доступными в более ранних операционных системах, но я не думаю, что это какая-то серебряная пуля — @backDeployed применяется только к функциям, методам, индексам и вычисляемым свойствам. , поэтому, хотя он может отлично работать для небольших изменений API, таких как модификатор fontDesign(), представленный в iOS 16.1, он не будет работать для любого кода, требующего использования новых типов, такого как новый модификатор scrollBounceBehavior(), который зависит от новой структуры ScrollBounceBehavior.
Например, в iOS 16.4 появился monospaced(_ isActive:) вариант для Text. Если бы это использовало @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 и аналогичную. На практике, несмотря на то, что новые типы не раскрываются публично и поэтому кажется простым выбором для обратного развертывания, мы не знаем, какие типы SwiftUI использует внутри, поэтому непросто предсказать, что можно, а что нельзя обратно развернуть.
Разрешить неявное self для слабых захватов self после того, как self развернуто
SE-0365 делает еще один шаг к тому, чтобы позволить нам удалить self из замыканий, разрешив неявное self в местах, где был развернут weak 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
}
}
}
Этот код не был бы скомпилирован до Swift 5.8, потому что оба экземпляра fireCount в замыкании должны были быть записаны как self.fireCount.
Лаконичные волшебные имена файлов
SE-0274 настраивает магический идентификатор #file для использования формата Module/Filename, например. MyApp/ContentView.swift. Раньше #file содержал полный путь к файлу Swift, например /Users/twostraws/Desktop/WhatsNewInSwift/WhatsNewInSwift/ContentView.swift, который излишне длинный и также может содержать вещи, которые вы не хотели бы раскрывать.
Важно: сейчас это поведение не включено по умолчанию. SE-0362 добавляет новый флаг компилятора -enable-upcoming-feature, предназначенный для того, чтобы разработчики могли выбирать новые функции до того, как они будут полностью включены в язык, поэтому, чтобы включить новое поведение #file, вы должны добавить -enable-upcoming-feature ConciseMagicFile в Другие флаги Swift в Xcode.
Если вы хотите сохранить старое поведение после включения этого флага, вам следует вместо этого использовать #filePath:
// New behavior, when enabled
print(#file)
// Old behavior, when needed
print(#filePath)
Предложение Swift Evolution по этому изменению стоит прочитать, потому что в нем упоминаются удивительно большие улучшения в размере двоичного файла и производительности выполнения, а также из-за этого довольно блестящего абзаца, объясняющего, почему использование полного пути часто является плохой идеей:
«Полный путь к исходному файлу может содержать имя пользователя разработчика, подсказки о конфигурации фермы сборки, проприетарные версии или идентификаторы или Sailor Scout, в честь которого вы назвали внешний диск».
Открытие экзистенциальных аргументов для необязательных параметров
SE-0375 расширяет функцию Swift 5.7, которая позволяла нам вызывать универсальные функции с использованием протокола, устраняя небольшое, но раздражающее несоответствие: Swift 5.7 не допускал такого поведения с необязательными параметрами, тогда как Swift 5.8 разрешает.
Например, этот код отлично работал в Swift 5.7, потому что он использует необязательный параметр T:
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 Numeric’ не может соответствовать ‘Numeric’», поэтому приятно видеть, что это несоответствие устранено.
Теперь поддерживаются понижения коллекции в шаблонах приведения.
Это устраняет еще одно небольшое, но потенциально раздражающее несоответствие в Swift, когда понижение коллекции — например. приведение массива ClassA к массиву другого типа, наследуемого от ClassA, в некоторых случаях не допускается.
Например, этот код теперь действителен в 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 это приводило к сообщению об ошибке: “Collection downcast in cast pattern is not implemented; use an explicit downcast to ‘[Dog]’ instead.”. На практике такой синтаксис, как если бы собаки = домашние животные как? [Собака] { работала нормально, так что я полагаю, что эта ошибка встречается редко. Однако это изменение означает, что несоответствие другого языка устранено, что всегда приветствуется.
И это еще не все!
Есть два дополнительных изменения, о которых стоит кратко упомянуть.
Во-первых, в SE-0368 представлен новый тип StaticBigInt, который должен облегчить добавление новых, более крупных целочисленных типов в будущем.
И, во-вторых, SE-0372 корректирует документацию функций сортировки Swift, чтобы пометить их как stable, что означает, что если два элемента в массиве считаются равными, они останутся в том же относительном порядке в отсортированном массиве — они останутся вместе в отсортированного массива, а также в исходном порядке, который у них был. Сортировка Swift некоторое время была стабильной, но теперь она официальная.
Кроме того, в Swift 5.8 запланированы две дополнительные функции, которые в настоящее время недоступны в текущих бета-версиях Xcode — вполне вероятно, что они будут включены в финальной версии Xcode 14.3, но нам нужно немного подождать, чтобы узнать наверняка.
Вот они:
- SE-0369: добавьте соответствие CustomDebugStringConvertible к AnyKeyPath, что означает, что выходные данные отладки для пути к ключу будут записаны как \ParentTypeName.PropertyName, а не текущий KeyPath<ParentTypeName, PropertyTypeName>.
- SE-0381: DiscardingTaskGroups, который создает группы задач, которые отбрасывают свои дочерние задачи, как только они завершаются, вместо того, чтобы нам приходилось ждать их явно.