Swift 6 で起きるランタイムクラッシュを警告に留めて移行作業をする
Swift 6 言語モードでは、エグゼキュータの等価性チェックでクラッシュが発生する可能性があります。例えば、暗黙的に MainActor なクロージャがバックグラウンドスレッドから呼ばれてクラッシュする、のようなケースです。
移行作業の最中では、クラッシュされるとアプリケーションのビルドをし直したりしないといけないので面倒なのです。
そこで便利なのが、 SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE というランタイム環境変数です。
Xcode では Product > Scheme > Edit Scheme... から設定できます。

例えば legacy を設定すると、ランタイムクラッシュから以下のようなランタイム警告になります。MainActorな関数がメインスレッド以外から呼ばれていることがわかるようになるので、stacktraceを見るよりも対応が容易になります。
warning: data race detected: @MainActor function at {ファイル名}.swift:{行番号} was not called on the main thread
SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE では、Swift 6.2.4 時点で以下を設定できます。
lagacy:- Swift 5 系の挙動。isolation が違ってもクラッシュせず、ランタイム警告に留まる
swift6:- Swift 6.0-6.1の挙動をする。
SerialExecutor.checkIsolated()を実行して isolation が違うとクラッシュ
- Swift 6.0-6.1の挙動をする。
isIsolatingCurrentContext:- Swift 6.2の挙動。
SerialExecutor.checkIsolated()ではなくSerialExecutor.isIsolatingCurrentContext()を強制的に使う場合に設定する
- Swift 6.2の挙動。
移行フェーズに応じて活用してみてはいかがでしょうか。ちなみにランタイム環境変数なので、設定してクラッシュを握りつぶした状態でリリースすることはできないので注意が必要です。
Swift 6.2+ の isolated deinit が嬉しい
@MainActor に隔離されたプロパティや non-Sendable なクラスのプロパティを deinit で触ろうとすると、コンパイラに怒られてしまいます。
- Main actor-isolated property 'xxx' can not be mutated from a nonisolated context
- Cannot access property 'xxx' with a non-Sendable type 'XXX' from nonisolated deinit
Swift 6.2 からは、 isolated deinit を使うことで安全にアクセスすることができるようになりました。
[SE-0371] Isolated synchronous deinit
アクターに分離されたクラスのデイニシャライザを分離済みとしてマークする機能が導入されたことで、デイニシャライザはクラス内の他の場所にあるデータに安全にアクセスできるようになりました。少し具体例をあげてみます。
安全にキャンセル処理をする
@MainActor 隔離されたプロパティはもちろんですが、プロパティがnon-Sendableなクラスの場合にも役立ちます。例えば、 GraphQL の watcher をキャンセルするときに便利です。外部のサードパーティー側で、Sendable 対応が進んでいたとしても、 プロジェクト側の事情で non-Sendable なままのバージョンで開発を進めていることなどがあると思います。
以下のように、 GraphQLQueryWatcher が non-Sendable だった場合、 deinit でキャンセル処理をすることができません。 Swift 6に移行する過程で ViewModel を @MainActor に隔離した場合、その時に View の onDisappear 経由でキャンセルするようにして回避することもできますが、別画面をモーダルで表示した時に、 watcher を生存させておきたいというケースでは困ってしまいます。(表示されてない時にVMがwatchし続けている必要は基本的にはないと思いますが...)
@MainActor final class ViewModel: ObservableObject { private var watcher: GraphQLQueryWatcher<SomeQuery>? deinit { watcher?.cancel() } }
そういったケースでは isolated deinit を使うことで、 onDisappear でキャンセルするようにしなくても済むようになったりします。
@MainActor final class ViewModel: ObservableObject { private var watcher: GraphQLQueryWatcher<SomeQuery>? isolated deinit { watcher?.cancel() } }
そのほか、deinit で触りたいために、 無理に nonisolated なプロパティにしてしまっている箇所なんかも見直したりできそうです。
2026/02/04 追記
isolated deinit を使ったクラスを含むテストが同期なものの場合、 ランタイムクラッシュするようでした。その場合、テストを非同期にするとこれは回避できました。
SwiftUI.SectionのFooterの末端要素を画面下端にalignさせるテク
これは はてなエンジニア Advent Calendar 2025 の20日目の記事です。
SwiftUI での List や Form で使われる Section の Footer の中で、ある要素を下端に寄せたくなったことはありませんか?要素をVStackに入れてSpacerを挟めばできるでしょうと思ってやってみると、予想以上に複雑になります。
まず問題となるSectionの画面はこちらです。

お題はこの Bottom Aligned Footer というテキストを画面下端に寄せることです。
ただし、この末端の要素はあくまでスクロールビューの中の要素の一つとしたいので、List や Form のスクロールと連動し、表示領域が足りなければ画面から見えなくなるものとします。そのため、画面下端に固定するために overlay(alignment: .bottom) { ... } を使うことは適していません。overlayの場合、キーボードを表示した時などにテキストフィールドに要素が重なってしまうからです。
そのためFooter要素は VStack で構成し Text の間に Spacer を挟みます。先ほどのスクリーンショットは以下のようなコードになっています。
struct ContentView: View { var body: some View { Form { Section { TextField("TextField 1", text: Binding.constant("")) TextField("TextField 2", text: Binding.constant("")) TextField("TextField 3", text: Binding.constant("")) TextField("TextField 4", text: Binding.constant("")) } header: { Text("Header") } footer: { VStack(alignment: .leading) { Text("Footer") Spacer(minLength: 16) Text("Bottom Aligned Footer") } } } } }
ただ、これだけでは Spacer は広がりません。テキストを画面下端(SafeArea)に寄せるには Spacer に必要な距離を設定するか、 VStack 自体に高さを設定する必要があります。
Footerの1番上から画面下端までの高さを算出できれば良さそうですが、 Spacer には minLength が設定されていたことを考慮すると、 VStack 自体に必要な高さを求めることは、テキストを含めた子Viewそれぞれの高さも求める必要がでてきてしまい、やりたいことに対してやや複雑すぎる印象です。
そのため、末端の要素が画面下端に寄るように Spacer に必要な距離を設定するアプローチを紹介します。必要な距離は、FooterのbottomのY座標から、ルートのViewのbottomのY座標までの距離の差分です。
Viewの高さを算出する
まず Form 自体の高さを算出できるようにします。 View のサイズを取得できるモディファイアを作ります。これは SwiftUI ではよく知られた手法です。サブビューから親コンテナビューへの情報の送信には Preferences を使います。
extension View { func viewSize(onChange: @escaping (CGSize) -> Void) -> some View { overlay { GeometryReader { geometry in Color.clear.preference(key: ViewSizePreferenceKey.self, value: geometry.size) } } .onPreferenceChange(ViewSizePreferenceKey.self, perform: onChange) } } private struct ViewSizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } }
これで View の高さを取得することができます。今回のFormのケースの場合、高さをbottomのY座標として扱うことができます。
struct ContentView: View { @State var formBottomY: CGFloat = 0 var body: some View { Form { } .viewSize { formBottomY = $0.height } } }
要素の座標の取得する
サブビューの座標の取得にはまずサブビューの Anchor(レイアウト参照)を取得するので、 そのための PreferenceKey を用意します。
struct AnchorPointKey: PreferenceKey { static var defaultValue: Anchor<CGPoint>? static func reduce(value: inout Anchor<CGPoint>?, nextValue: () -> Anchor<CGPoint>?) { value = nextValue() } }
GeometryReader から得られるジオメトリ情報と、 anchorPreference(key:value:transform:)) から得られる Anchor を使って、Footer の bottom のY座標を取得することができます。
GeometryReader { geometry in
Form {
Section {
...
} header: {
...
} footer: {
VStack(alignment: .leading) {
...
}
.anchorPreference(key: AnchorPointKey.self, value: .bottom) { $0 }
}
.onPreferenceChange(AnchorPointKey.self) { anchor in
print(geometry[anchor].y)) // FooterのbottomのY座標が取得できる
}
}
}
差分を算出して Spacer に設定する
ここまで取得した Form のbottomのY座標とFooterのbottomのY座標を使うことで、 Spacer に必要な距離を算出できます。Anchorはスクロール毎に更新されるので、一度 Spacer の距離を設定したらそれを維持するようにしましょう。そして、 Form のサイズが切り替わったタイミングで Spacer の距離が再計算された値に更新されるように State は Optional で定義します。
struct ContentView: View { @State var spacerLength: CGFloat? @State var formBottomY: CGFloat = 0 { didSet { // 画面回転やキーボード表示でFormの高さが変わるのでSpacerを再計算する spacerLength = nil } } var body: some View { GeometryReader { geometry in Form { Section { TextField("TextField 1", text: Binding.constant("")) TextField("TextField 2", text: Binding.constant("")) TextField("TextField 3", text: Binding.constant("")) TextField("TextField 4", text: Binding.constant("")) } header: { Text("Header") } footer: { VStack(alignment: .leading) { Text("Footer") Spacer(minLength: spacerLength ?? 16) Text("Bottom Aligned Footer") } .anchorPreference(key: AnchorPointKey.self, value: .bottom) { $0 } } .onPreferenceChange(AnchorPointKey.self) { anchor in // FooterのbottomYからFormのbottomYまでの長さを計算して必要なSpacerの長さを算出する if let anchor, spacerLength == nil { let length = abs(formBottomY - geometry[anchor].y) spacerLength = max(16, length) } } } .viewSize { formBottomY = $0.height } } } }
これで Bottom Aligned Footer というテキストを画面下端に寄せることができました 🎉

List や Form は制約が多いので、今回のケースのように一見シンプルでも複雑なアプローチを強いられることがあります。実装したいUIが素朴に実装できるのかどうかまず試してみることが重要ですね...。
みなさんもこのお題にチャレンジしてみてください!もっとスマートな方法がきっとあるはずです。
明日は
id:yujiorama さんです!
Swift 6の `sending` キーワードについて調べた
Swift 6言語モードに移行を進めるにあたって、引数に sending がついているから Sendable にしようみたいな対応がある。 ただ sending のことをちゃんと説明できる自信がなかったので自分で理解できる範囲で調べた。
sending キーワード
- 提案自体は SE-0430: sending parameter and result values で行われた
- SE-0414: Region based Isolation がベースにあって、
sendingはその拡張。SE-0414の提案で region isolation (領域分離)の概念が入った- データが属する isolation の追跡ができるようになり
- Sendableじゃない型でもactor boundaryを越えて送信可能になったという理解
- Region based Isolation についてはインターネットにいろいろ書いてある
- データが属する isolation の追跡ができるようになり
- SE-0414: Region based Isolation がベースにあって、
- 関数の引数/戻り値に
sendingという明示的なアノテーションを付けることで、その値がactor boundaryを越えて送信可能( disconnected )であることを保証する挙動を定義できる- 別のactorに渡せるという言い方もできる?
- 要はSendableであることを保証した上で、その値を安全を送信する
- region が完全に分離されている(disconnected)なら、non-Sendableでも大丈夫ということがコンパイラで判断できる
触ってみないとわからない
Swift 6.0.3で確認した。
// swift --version swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1) Target: arm64-apple-macosx15.0
まず、non-Sendableなclassを用意する。
class NonSendable { var value: Int init(_ value: Int) { self.value = value } }
NonSendableを sending で受け取る関数を作る。
actor SomeActor {
func doSomething(_ nonSendable: sending NonSendable) async {
print("nonSendable value:", nonSendable.value)
}
}
簡単な確認だけど、他参照なしであればnon-Sendableなclassでも引数に渡すことができるし、他の参照がある(regionを超えている)とnon-Sendableを渡すことができない。
struct SendingCheck { static func main() async { let actor = SomeActor() // その場で新しいインスタンスを渡す(他参照なし)はOK await actor.doSomething(NonSendable(1)) let nonSendable = NonSendable(2) await actor.doSomething(nonSendable) // sendingで渡した後に参照していると `Sending 'nonSendable' risks causing data races` エラーになる print("nonSendable value:", nonSendable.value) } }
戻り値につけることもできる。それはそうかという感じがする。内部でプロパティとして持ちつつ sending をつけたら怒られた。その返り値がどこからもアクセスされないことを意図したい時に使えるということだろう。
actor SomeActor {
var nonSendable: NonSendable?
func getSending(value: Int) -> sending NonSendable {
nonSendable = NonSendable(value)
// Sending 'self.nonSendable' risks causing data races
return nonSendable!
}
}
よく出くわすのは CheckedContinuation の func resume(returning value: sending T) でProposalでも例としてあげられている。あとは AsyncThrowingStream の func yield(_ value: sending Element) 。だいたいはSendableにして渡すようにすれば警告(Swift 5言語モードなら)やエラーは解消するが、なぜそうなるかをわかっていると少しわかった気になれる。
まとめ
ぐっと考えると理解できるような気がするが、完全に理解した上でここでは sending つけたいな〜と思いながらコードを書くのは難しい気がする。そのうち領域展開できそう。
参考
ScrollView内のButtonのハイライト挙動の変化
SwiftUI.ScrollView 内にある SwiftUI.Button のインタラクションの挙動がXcode/OSバージョンによって変わっています。調べた限りXcode 16以降からのiOS 18*1で挙動の違いがありました。
何が起きるか
Xcode 16以降のiOS 18では、SwiftUI.ScrollView 内に Button を配置している場合、タップした瞬間にタッチイベントは発火してもハイライトされません。通常は軽くタップするとボタンが一瞬透明になりますよね?あの状態です。
import SwiftUI struct ContentView: View { var body: some View { ScrollView { Button("Button") { print("action") } } .padding() } }
ButtonStyle を明示的に指定して値の変化を監視すると何が起きているかわかりやすいです。厳密には ButtonStyleConfiguration.isPressed が軽くタップするだけでは反映されない状態なので、ボタンの opacity などが変化しません。タップする時間が0.5秒くらいあれば isPressed が反映されます。自前の ButtonStyle を作ってスケールエフェクトなどをつけている場合、この挙動はユーザー体験を悪くしてしまう可能性があります。
import SwiftUI struct TestButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .onChange(of: configuration.isPressed) { print("configuration.isPressed: \(configuration.isPressed)") // 軽くタップするだけでは変化しない } } } struct ContentView: View { var body: some View { ScrollView { Button("Button") { print("action") } .buttonStyle(TestButtonStyle()) } .padding() } }
iOS 18での対応策
UIScrollView には delaysContentTouches というプロパティがあります。
この delaysContentTouches を無効にしてあげると従来の挙動に戻すことができることがわかっています。
SwiftUI.ScrollView の場合、この挙動を変更する術がないため、 swiftui-introspectを使って制御する必要があります。
import SwiftUI import SwiftUIIntrospect struct ContentView: View { var body: some View { ScrollView { Button("Button") { print("action") } } .introspect(.scrollView, on: .iOS(.v18)) { scrollView in scrollView.delaysContentTouches = false } .padding() } }
各地の ScrollView につけてまわる必要があるので素敵な解決策ではないですね...。他にもいい方法があれば教えてください。
Xcode/OSバージョンごとやUIButtonとの違い
XcodeやOSバージョンごとに違うのはもちろんそうですが、 UIButton かどうかでも違いがあります。
| UIButton | SwiftUI.Button | ScrollView内のUIButton | ScrollView内のSwiftUI.Button | |
|---|---|---|---|---|
| Xcode 15/iOS 17 | ⚪︎ | ⚪︎ | × | ⚪︎ |
| Xcode 15/iOS 18 | ⚪︎ | ⚪︎ | × | ⚪︎ |
| Xcode 16/iOS 17 | ⚪︎ | ⚪︎ | × | ⚪︎ |
| Xcode 16/iOS 18 | ⚪︎ | ⚪︎ | × | × |
| Xcode 16/iOS 26 | ⚪︎ | ⚪︎ | × | ⚪︎ |
| Xcode 26/iOS 17 | ⚪︎ | ⚪︎ | × | ⚪︎ |
| Xcode 26/iOS 18 | ⚪︎ | ⚪︎ | × | × |
| Xcode 26/iOS 26 | ⚪︎ | ⚪︎ | × | ⚪︎ |
UIButtonの場合は一貫してハイライトが効かないので、Xcode 16/iOS 18で挙動を合わせたのかな〜と思ったのですが、iOS 26で元の挙動に戻っているため不具合だったのかもしれません。ちなみにこの挙動をFeedback(FB16848138)済みでした。
検証コード
適当にコピペして試してみてください。
import SwiftUI struct TestButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 44) .background(configuration.isPressed ? .blue.opacity(0.5) : .blue) .cornerRadius(10) .onChange(of: configuration.isPressed) { print("configuration.isPressed: \(configuration.isPressed)") } } } struct ContentView: View { var body: some View { VStack { switftUIButton("SwiftUI/Outside") uikitButton("UIKit/Outside") ScrollView { switftUIButton("SwiftUI/Inside") uikitButton("UIKit/Inside") } } .padding() } private func switftUIButton(_ title: String) -> some View { Button(title) { print("action") } .buttonStyle(TestButtonStyle()) } private func uikitButton(_ title: String) -> some View { UIKitButton(title: title) { print("action") } .frame(height: 44) } } struct UIKitButton: UIViewRepresentable { let title: String let action: (() -> Void)? init(title: String, action: (() -> Void)? = nil) { self.title = title self.action = action } func makeUIView(context: Context) -> some UIButton { _UIKitButton(title: title, action: action) } func updateUIView(_ uiView: UIViewType, context: Context) { } } final class _UIKitButton: UIButton { init(title: String, action: (() -> Void)?) { super.init(frame: .null) translatesAutoresizingMaskIntoConstraints = false setTitle(title, for: .normal) setTitleColor(.white, for: .normal) backgroundColor = .systemBlue layer.cornerRadius = 10 addAction(.init { _ in action?() }, for: .touchUpInside) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var isHighlighted: Bool { didSet { print("isHighlighted: \(isHighlighted)") backgroundColor = isHighlighted ? .systemBlue.withAlphaComponent(0.5) : .systemBlue } } }
*1:正確には18.1 ~ 18.5で発生します。18.0は体感ほとんど発生しない
iOS16 から使える SwiftUI の機能をおさらいする
はてなのマンガアプリチームで GigaViewer for Apps を作っています、
id:fxwx23 です。
タイトルを見て今更?と思ったかもしれませんが、iOSのアップデートで追加される機能の多くはバックポートされていないため、アプリ側のサポートする下限のOSバージョンが追いついて初めて利用可能になります。その結果、WWDCで発表された機能が実際の日々のプロダクト開発に取り入れられるまでには、2〜3年かかることがあります。
2022年に発表された iOS16 で SwiftUI に追加された機能は、ユーザー体験の向上やコードをシンプルにするために役立つものが多くあります。このタイミングであらためて見直すことで鮮度を保ったまま実践投入することができるのではと思い、今回はその中から個人的におさらいしておきたい機能をピックアップして紹介します。
lineLimit(_:)
View の中で Text が占有できる最大行数を設定するモディファイアです。iOS 15までは Int? を受け取る形のみでしたが、細かい進化を遂げています。
範囲型のサポート
iOS16 で TextField に複数行入力サポートが入ったため、範囲型を受け付けるようになっています。指定した範囲よりもスペースが必要になるとスクローラブルになってくれます。チャットUIの入力部分などがイメージしやすいと思います。
lineLimit(_ limit: ClosedRange<Int>)A...BlineLimit(_ limit: PartialRangeFrom<Int>)A...lineLimit(_ limit: PartialRangeThrough<Int>)...A
Form {
TextField("Bio", text: $bio, axis: .vertical)
.lineLimit(3...)
}

余白空間を保持
指定した行数に満たなくても想定されるスペース分を確保してくれるようになっています。Grid レイアウトで各要素に表示する説明文は最大行数は指定したいけど、要素自体の高さは均一にしたい時などに使えますね。地味に嬉しい...
Text("description here") .font(.subheadline) .lineLimit(2, reservesSpace: true)
contentTransition(_:)
View のコンテンツの変更をアニメーション化させることができるモディファイアです。また一部をアニメーションさせないということも可能です。
例えば、数値のカウントアップやカウントダウンの演出のためににいい感じのアニメーションをつけることができます。コンテンツのカウント数が上限するタイミングでアニメーションがあるとおしゃれですね。
VStack {
Text("\(count)")
.contentTransition(.numericText())
Button("Count Up") {
withAnimation {
count += 1
}
}
}

defersSystemGestures()
OS独自の組み込みジェスチャー(コントロールセンターや通知センター)よりもジェスチャーが優先されるように要求できるモディファイアです。これはユーザーが頻繁にスワイプする可能性のあるゲームや、画面の端に独自のジェスチャーを置く場合などに良さそうです。例えば Edge.Set.vertical で有効にすると Home Indicator がグレーアウトします。
ちなみに完全に非表示にしたりできる persistentSystemOverlays(_:)) も iOS16+ です。UIKit でできたことも SwiftUI で完結するようになってきています。
ViewThatFits
ViewThatsFits は複数の View の中から親 View のレイアウト条件に最も適した View を選択して表示してくれます。レスポンシブなデザインや、画面サイズやレイアウトの制約に応じた View の選択を簡素化するのに役立ちますね。
例えば、 View 内に収まるテキスト量なら Text で、収まらない場合はスクローラブルに表示したいというシーンを考えます。ViewThatsFits を使うと振り分けたい要素を優先順位順に書いておけばOKです。楽ちん!
struct ContentView: View { let text = String(repeating: "Long text ...", count: 100) var body: some View { ViewThatFits(in: .vertical) { // 収まる場合はText Text(text) // 収まらない場合はScrollView ScrollView { Text(text) } } .padding() } }
デフォルトでは ViewThatFits にテキストの水平軸と垂直軸の両方を気にするようになっているため、常に ScrollView が選択されてしまいます。 この例の場合は垂直軸のみを測定するように制限する必要があるので注意が必要です。


AnyLayout
最後は AnyLayout です。AnyLayout は異なるレイアウト( HStack や VStack など)を動的に切り替えるための型消去ラッパーです。SwiftUI では通常レイアウトを静的に決定しますが、AnyLayout を使用すると条件に応じてレイアウトを動的に変更することができます。
horizontalSizeClass や、ダイナミックフォントに応じてレイアウトを切り替えたい時に便利です。AnyLayout なら subview の状態を破壊せずにレイアウトコンテナの型を動的に変更できるのでいいですね。 if 分岐がなくなるのも良い。
struct ContentView: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass var body: some View { let layout = horizontalSizeClass == .compact ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout()) VStack { layout { Rectangle() .fill(.red) .frame(maxWidth: .infinity) .frame(height: 200) VStack(spacing: 0) { Rectangle() .fill(.blue) .frame(maxWidth: .infinity) .frame(height: 100) Rectangle() .fill(.green) .frame(maxWidth: .infinity) .frame(height: 100) .background(.blue) } } Spacer() } .padding() } }


Layout プロトコルに準拠したものなら利用できるので、自前のカスタムレイアウトと組み合わせることもできます。早く使いたいですね!
ここまでいくつかの機能を紹介しましたが、もちろんこれがすべてではありません!まだまだ活用しきれていない面白い機能がたくさんあるのでぜひ調べて活用してみてください。iOS 16 以降の機能を再発見する良いきっかけになれば幸いです。
この記事は はてなエンジニア Advent Calendar 2024 の23日目の記事でした。明日は
id:masawada さんです!
SwiftPM 6 は同時ビルド/テストを回避するために Package の .build ディレクトリをロックする
Swift version
swift-driver version: 1.115 Apple Swift version 6.0 (swiftlang-6.0.0.9.10 clang-1600.0.26.2) Target: arm64-apple-macosx14.0
挙動について
SwiftPM 6 (release/6.0 以降)は、同時ビルド/テストを回避するために Package の .build ディレクトリをロックをするようです。残念ながら、このロックする挙動は一部のケースでプラグイン実行やテストの動作を止めてしまう可能性があります。 その影響を受けた場合以下のような warning が発生します。
Another instance of SwiftPM is already running using '/path/to/your/package-directory/.build', waiting until that process has finished execution...
プロセスが終了するまでロックされ続けるので、CIなどの実行で処理が完了せずにタイムアウトするようになってしまうなどの影響があります。
どのようなパッケージや実装が対象になるかというと、実行プロセスの中でさらに dump-package や build コマンドを実行しているケースです。厳密には try swiftCommandState.getActiveWorkspace() の箇所で起こりうるので、他のコマンドを同プロセスの中で実行していれば起きると思います。
Swift の VSCode Extension ではパッケージの依存解決をよしなにやってくれますが、従来の挙動だとユーザーがビルドしている最中にも依存解決が実行されたりしてリポジトリを破損されることが起きていたので同時にビルドが起きないようにこの対応が入ったようです。(という理解)
対応
escape-hatch として従来の挙動に戻せるコマンドオプション --ignore-lock が同時に追加されています。プロセスの中で実行している SPM コマンドに Swift 6 以上での実行かをチェックした上で --ignore-lock オプションを追加することで対処が可能です。
// swift --version などで Swift 6 以上での実行かを判断する(Regex) if swift6Plus { let output = try sh("swift package dump-package --ignore-lock") ... } else { ... }
問題なくオプションを渡せていれば Another instance of SwiftPM is already running using '/path/to/your/package-directory/.build', but this will be ignored since --ignore-lock has been passed というwarningと共に処理がロックされずに後続に進むようになります。
参考
- https://github.com/swiftlang/swift-package-manager/issues/6643
- https://github.com/swiftlang/swift-package-manager/pull/7291
- https://github.com/swiftlang/swift-package-manager/pull/7335
- https://github.com/swiftlang/swift-package-manager/pull/7338
- https://github.com/swiftlang/swift-package-manager/pull/7384