1. WWDC2025で発表されたSwiftの新機能・変更点
2025年6月10日 ~ 2025年6月14日(日本時間)にかけて、WWDC 2025が開催されました。
本記事では、「Swiftの新機能」で発表されたライブラリの変更内容について備忘録的にまとめていきます。
本編につきましては以下の動画をご参照ください。
今回発表されたライブラリの新機能と変更点の一覧は以下です。
No. | New or Update | 対象ライブラリ名 | 概要 |
---|---|---|---|
1 | New | Subprocess | Swiftから外部コマンドを非同期的に、かつ安全に実行できるようになる |
2 | Update | Foundation | UIKitからの通知を受け取るObserver登録時のコードをより安全かつ簡潔に書ける様になる |
3 | Update | Observation | Observationsの登場によって、Swiftらしい構文で、宣言的、非同期、型安全なコードを書ける様になる。Observationsにより監視対象のプロパティ更新時のパフォーマンスが向上する |
4 | Update | Testing | カスタム添付ファイルの導入でテスト時の状態やコンテキストの可視化、レポートに記録がより便利になる。終了テストが可能になり、テスト後の状態を保証することが可能となり、テストケースの漏れが防ぎやすくなる |
以下では、上記についてもう少し細かく見ていこうと思います。
2. Subprocess
2.1. 概要
Subprocess
というライブラリが追加されます。
イメージとしてはPython
のsubprocess.run(...)
と同様のものかと思っております。
外部コマンドを実行するためのライブラリ
2.2. サンプルコード
import Subprocess // Subprocessライブラリをインポート
// 🟢 シェルコマンド "pwd(current directory表示)" を非同期的に実行し、結果が"result"に格納される
let result = try await run(
.name("pwd")
)
result.standardOutput // 🟢 成功した時の結果を参照する(ex: Users/hoge/fuga/)
シンプルに外部コマンドを実行できるようになりました。
- 非同期処理となっている
-
do-try-catch
など不要
2.3. 従来手法との比較: Foundation.Process
import Foundation
let process = Process() // Foundation.Processを使用して外部コマンドを実行する
let pipe = Pipe() // ❌ Pipeを使用して非同期処理として実行する
process.executableURL = URL(fileURLWithPath: "/bin/pwd")
process.standardOutput = pipe
do { // ❌ do-try-catchでrunを実行する
try process.run()
process.waitUnitlExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) {
print("current directory: \(output)")
}
} catch {
print("running process has failed: \(error)")
}
Foundation.Processでの外部コマンド実行
- 独自で非同期処理として実装するためにひと工夫する必要がある
-
do-try-catch
でrun()
を実行する必要がある
2.4. 参照情報
- runメソッド
- 結果の構造体
- https://github.com/swiftlang/swift-subprocess/blob/main/Sources/Subprocess/Result.swift#L37
-
TerminationStatus
で成功/失敗も判定やエラーハンドリングができるようになっている
3. Foundation
3.1. 概要
UIKitからの通知を受け取るためのObserverについて改善がされました。
3.2. サンプルコード
import UIKit
@MainActor
class KeyboardObserver {
func registerObserver(screen: UIScreen) {
let center = NotificationCenter.default
let token = center.addObserver(
of: screen,
for: .keyboardWillShow // 🟢 コンパイラで正しい通知の種類かチェックされる
) { keyboardState in
let startFrame = keyboardState.startFrame // 🟢 通知の情報をシンプルに参照することが可能
let endFrame = keyboardState.endFrame // 🟢 通知の情報をシンプルに参照することが可能
self.keyboardWillShow(startFrame: startFrame, endFrame: endFrame) // 🟢 通知の種類によって通知の投稿先が指定される。ここでは通知が MainActor に投稿されることが担保されるため、MainActorに隔離されたメソッドを同期的に呼び出すことが可能となる。
}
}
func keyboardWillShow(startFrame: CGRect, endFrame: CGRect) {}
}
extension UIResponder {
public struct KeyboardWillShowMessage: NotificationCenter.MainActorMessage // MainActorMessageに適合させると通知が常に MainActor に同期的に投稿されることが担保される。
}
extension HTTPCookieStorage {
public struct CookiesChangedMessage: NotificationCenter.AsyncMessage // AsyncMessageに適合させると通知は任意のスレッドに非同期的に投稿される
}
通知のObserver登録時の処理がシンプルかつ安全になる
- 通知の種類設定時に具象型で設定するのでコンパイラでチェックでき安全性が向上する
- 通知の種類の引数がシンプルに設定できるようになる
- 通知の情報をシンプルに参照できるようになる
- MainActorMessageに適合させると通知が常にMainActorに同期的に投稿されるようになるため、通知受信時のコールバック処理の中からMainActor隔離されたメソッドを実行することが可能になる
3.3. 従来手法との比較
import UIKit
@MainActor
class KeyboardObserver {
func registerObserver(screen: UIScreen) {
let center = NotificationCenter.default
let token = center.addObserver(
forName: UIResponder.keyboardWillShowNotification, // ❌ 通知の名前を登録する際に注意が必要。ミスすると通知のコールバックが実行されない。
object: screen,
queue: .main // ❌ 通知はメインスレッドに投稿されることが確定するが、MainActorのAPIへのアクセスでエラーとなる
) { notification in
guard let userInfo = notification.userInfo else { return } // ❌ 通知に関する情報は型指定のない辞書に格納されている
let startFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect // ❌ 正しいキーを使用して手動で対象データを参照し、取得結果を正しい型にキャストする必要がある。
let endFrame = userInfo[UIResponsder.keyboardFrameEndUserInfoKey] as? CGRect
guard let startFrame, let endFrame else { return }
self.keyboardWillShow(startFrame: startFrame, endFrame: endFrame) // ❌ error: Call to main actor-isolated instance method 'keyboardWillShow' in a synchronous nonisolated context
// ❌ MainActor隔離されたkeyboardWillShowを非同期処理の中身から同期的に呼び出そうとしているため、Actorの隔離違反となり、ビルドエラーとなる
}
}
}
通知のObserver登録の際、気を遣わなければならない点が多い
- 通知の名前を文字列ベースで設定するため、正しい値が設定されていなくてもコンパイラのチェックにひっかからない。
- 通知に関する情報は
userInfo
に格納されており、参照する際は正しいkey名で参照し、正しい型にキャストしなければならない - 通知がメインスレッドに投稿されることを担保することは可能だが、通知受信時の非同期処理からMainActor隔離されたメソッドを同期的に呼び出そうとするとコンパイルエラーとなる
4. Observation
4.1. 概要
Observations
の登場で状態変化の処理がトランザクションとして実行されることで、複数のプロパティの変更が同時に発生しても1度の更新で済む様になり、更新処理の効率が向上されました。
4.2. サンプルコード
import Observation
@Observable
class Player {
let name: String
var score: Int = 0
var item: Item = .none
}
let player = Player(name: "Holy")
let values = Observations {
// 🟢 player.score か player.itemのいずれかの`willSet`が実行された時に更新処理が実行される
let score = "(\(player.score) points)"
let item =
switch player.item {
case .none: "no item"
case .banana: "a banana"
case .star: "a star"
}
return "\(score) and \(item)"
}
// 以下が同期的に変更されたという前提:
// 🟢 2points and a banana(printが実行されるのは1回のみ)
player.score += 2
player.item = .banana
// 🟢 AsyncSequence型に適合しているため、for-awaitで更新された都度、反復処理できる
for await value in values { print(value) }
状態変化時の更新処理がトランザクションで実行される
- 複数のプロパティの変更が同期的に発生した場合は1回だけ更新処理が実行される
- 効率が良い
4.3. 従来手法との比較
import Combine
class Player: ObservableObject {
@Published var score = 0
@Published var item: String? = nil
}
let player = Player()
let cancellable = Publishers.CombineLatest(player.$score, player.$item)
.map { score, item in
let scoreString = "\(score) points"
let itemString = item.map { "a \($0)" } ?? "no item"
return "\(scoreString) and \(itemString)"
}
.sink { print($0) }
player.score += 2
// ❌ print結果:2 points and no item
player.item = "banana"
// ❌ print結果:2 points and a banana
各プロパティの状態変化時にそれぞれの更新処理が実行される
- 効率が悪い
- 非同期処理のバグの発生率が高い
5. Testing
5.1. 概要
カスタム添付ファイルが導入されました。
従来のXCTestでもテスト実行時にファイルを参照することは可能でしたが、Testingを用いるとよりシンプルで安全な方法で実装することができるようになりました。
TearDownTestが導入されました。
TearDownTestによって後処理が正しく実行されたかどうかを明示的にテスト可能になりました。
テスト対象となる変数の値が初期化されたかどうかなど検証できる様になるため、テストコード作成時の不備に気づきやすくなりました。
5.2 カスタム添付ファイル
5.2.1 サンプルコード
import Testing
import Foundation
import EvoulutionMetadataModel
@Test
func validateProposalID() async throws {
let (data, _) = try await URLSession.shared.data(from: evolutionJSONMetadataURL)
// 🟢 カスタム添付ファイルの追加
Attachment.record(data, named: "evolution-metadata.json")
let jsonDecoder = JSONDecoder()
let metadata = try jsonDecoder.decode(EvolutionMetadata.self, from: data)
for proposal in metadata.proposals {
#expect(proposal.id.starts(with: "SE")) // 仮定の検証
}
}
ファイル読み込みの処理がシンプルに書けるようになりました。
-
Attachment.record(...)
によって添付ファイルがログに残る - 独自型をそのまま Attachment.record(...)に渡せる
5.2.2 従来手法との比較
import XCTest
final class EvolutionTests: XCTestCase {
func testValidateProposalID() async throws {
let (data, _) = try await URLSession.shared.data(from: evolutionJSONMetadataURL)
// ❌ カスタム添付ファイルの追加
let attachment = XCTAttachment(data: data)
attachment.name = "evolution.json"
attachment.lifetime = .keepAlways
add(attachment)
let jsonDecoder = JSONDecoder()
let metadata = try decoder.decode(EvolutionMetadata.self, from: data)
for proposal in metadata.proposals {
XCTAssertTrue(proposal.id.starts(with: "SE"), "Proposal ID \(proposal.id) is invalid")
}
}
}
カスタム添付ファイルの追加の際に気を遣うポイントが多かった
- XCTAttachmentはdata型から生成される
- 独自型をXCTAttachmentにしたい場合はData型に変換する必要がある
5.3 TearDownTest
5.3.1 サンプルコード
import Testing
import EvolutionMetadataModel
extension Proposal {
public var number: Int {
let components = id.split(separator: "-")
// 🟢 前提条件「-(ハイフン)でidを2つのに分けたうちの後半部分は全て数字である必要がある」
precondition(
components.count == 2 && components[1].allSatisfy(\.isNumber),
"Invalid proposal ID format \(id); expected SE-<Number>"
)
return Int(components[1])!
}
}
@Test
func invalidProposalPrefix() async throws {
// 🟢 前提条件に合致しないため、プロセスが失敗することを期待している
await #expect(processExitsWith: .failure) {
let proposal = Proposal(id: "SE-NNN")
_ = proposal.number
}
}
意図した通りに失敗したかどうかを検証できる
- プロダクトコードを無理やり
throws
にしなくて済む - 従来のXCTestでは「失敗(クラッシュすること)を期待するテスト」は難しかった
6. まとめ
今回は、WWDC2025 の「Swiftの新機能」からライブラリの変更内容についてまとめました。
他にもまだまだ改善された点がありますので、随時まとめていければと思っています。
従来手法のコードにつきまして、検証が不十分な箇所もあるかもしれません。
大変申し訳ないのですが、コメントにてご指摘いただけましたら幸いです。
お目通し頂きまして、ありがとうございました。