async/await への自動変換
System Framework以外でも自動変換が行われるケースに遭遇したけど自動変換されるものとされないものの違いが分からなかったので調査。
Xcode 13 Release Notes には以下のように記載がある。 https://developer.apple.com/documentation/xcode-release-notes/xcode-13-release-notes
Swift translates an Obj-C method that delivers its results asynchronously via a completion handler into an async method that directly returns the result (or throws). (78028295)
For example, Swift translates the following Objective-C method from CloudKit:
- (void)fetchShareParticipantWithUserRecordID:(CKRecordID *)userRecordID completionHandler:(void (^)(CKShareParticipant * _Nullable, NSError * _Nullable))completionHandler;
completion handler で非同期な結果を配信するObjective-Cメソッドと記載がある。
調査
調査環境 - iMac (24-inch, M1, 2021), macOS Monterey 12.3.1, Xcode 13.3.1
サンプルプロジェクトを作って確認。
- プロジェクトテンプレートからiOSアプリのSwift指定でプロジェクトを作成
- Objective-Cのクラスファイルを作成して completion handler を使ったメソッドを実装
#import <Foundation/Foundation.h> @interface MyObject : NSObject - (void)myMethod:(void (^)(NSString * _Nullable, NSError * _Nullable))compleitonHandler; @end
- Bridging-Header.h ファイルで
#import "MyObject.h"をしてSwiftから呼び出せるようにする - Swiftから
MyObject().myまで入力して Xcode の autocompletion を表示

asyncへの自動変換は行われなかった。
と次の手を考えている時にメソッド名の typo に気付く。
compleitonHandler を completionHandler に修正。
- (void)myMethod:(void (^)(NSString * _Nullable, NSError * _Nullable))completionHandler;
すると!
変換された!
引数名が completionHandler でないといけない!?
そこで引数名を completion にしてみる
OK
引数名を completionBlock にしてみる
OK
引数名を handler にしてみる
NG
あれ?最初が completion だったらなんでもいけるんじゃないのか?
引数名を completionFoo にしてみる
NG
何でもはダメなのか、、
引数名を block にしてみる
NG
とりあえず completionHandler completionBlock completion の3つはOKという事は分かった。
Nullabilityの調査
少し意地悪をして返り値とエラーの両方が _Nonnull だったら?
- (void)myMethod:(void (^)(NSString * _Nonnull, NSError * _Nonnull))completionHandler;
エラー無しで tuple で2つの値を返すasyncメソッドに変換された。
_Nullable を書いていない場合
- (void)myMethod:(void (^)(NSString *, NSError *))completionHandler;
この場合も先ほどと同じく tuple

という事で Nullability を適切に記載しているというのも条件になる。
Swiftコードの調査
Objective-Cの実装を削除してSwiftのclassを作成する
class MyObject { }
メソッドを実装
func myMethod(_ completionHandler: @escaping (String?, Error?) -> Void) { }
これを呼び出してみる
NG
@objc を付けてみる
@objc func myMethod(_ completionHandler: @escaping (String?, Error?) -> Void) { }
NG
NSObject を継承してみる
class MyObject: NSObject {
NG
NSString NSError にしてみる
@objc func myMethod(_ completionHandler: @escaping (NSString?, NSError?) -> Void) { }
NG
Resultにしてみる
func myMethod(_ completionHandler: @escaping (Result<String, Error>) -> Void) { }
NG
ドキュメントにはObjective-Cメソッドと記載があるので、Swiftでの実装だろうと @objc 宣言でObjective-Cメソッドとして扱えば自動変換してくれそうに思えるけどSwiftで実装されたものは自動変換してくれないみたい。Swiftコードのasync/awaitへの自動変換があると移行に面倒がなくて良いんだけど残念。
async/await への自動変換の条件まとめ
- Objective-Cで書かれている
- Nullabilityが正しく設定されている
- 引数名は
completionHandler,completionBlock,completionのいずれか
Task.init と Task.detached の書き方バリエーション
以下5つは同じ
Task {
}
Task.init {
}
Task.init() {
}
// deprecated
async {
}
// deprecated
async() {
}
以下6つは同じ
Task.detached {
}
Task.detached() {
}
// deprecated
Task.runDetached {
}
// deprecated
Task.runDetached() {
}
// deprecated
detach {
}
// deprecated
detach() {
}
いずれも引数の priority を省略して operation を Trailing Closure で省略表記した形。
WWDCでの発表から正式リリースまでの間に書かれた記事やコードはこの辺ばらつきがあるの読む時は注意。*1
まとめ
シンプルに以下2つを記憶に残して他は忘れて良し
Task {
}
Task.detached {
}
*1:注意というか軽く読み流す感じで
Swift の関数の override で引数のデフォルト値を変更すると何が起こるか
Xcode engineer の人がこんなツイートをしていた
Overriding functions with default arguments is fun in Swift! Guess what this prints. pic.twitter.com/CBtZ6am6R9
— Louis D'hauwe (@LouisDhauwe) 2021年7月13日
早速 Playground で確認
まずクラスとメソッドを定義*1
import Foundation class Base { func test(i: Int = 1) { print("Base class i: \(i)") } } class Sub: Base { override func test(i: Int = 2) { print("Sub class i: \(i)") } }
通常のインスタンス化と呼び出し
let base = Base() base.test() // Base class i: 1 let sub = Sub() sub.test() // Sub class i: 2
ここまでは普通
問題は次
サブクラスでインスタンス化しつつ親クラスを型として宣言
そしてメソッドを呼ぶと...
let a: Base = Sub() a.test() // Sub class i: 1
インスタンスはサブクラスの Sub でメソッドもサブクラスの Sub.test() が呼ばれるけど、デフォルト引数は親クラス Base で指定されている値が使われる!
以下を試してみる
let b = Sub() b.test() // Sub class i: 2 let c: Base = b c.test() // Sub class i: 1 print(b === c) // true
同一インスタンスの参照であるため === で比較すると true になる。
つまり実体は1つ。
でも宣言型によってメソッドを呼んだ時に適用されるデフォルト引数が変化している。
個人的には直感に反した挙動でいつか落とし穴になってしまいそう。*2
ちなみに Kotlin だと親クラスでデフォルト引数が指定されていようがいまいが override された関数ではデフォルト引数を指定できない仕様になっているらしく、不意の事故が起こる心配が無い。
This is a really curious effect!
— rolgalan (@rolgalan_) 2021年7月13日
I was intrigued about what would happen in #Kotlin and it turns out that this is solved by disallowing default parameters in any overridden function, even if the base doesn't have any default param. https://t.co/aeYBhYqTmB pic.twitter.com/2y2RcC1Tor
ドキュメントでの記述が見当たらなかったので Swift Forum を当たってみたところ、2018年3月に話題に上がっていた模様。*3
https://forums.swift.org/t/pitch-allow-default-parameter-overrides/10673
*1:シンプルにするため super.test() は省いた
*2:ドキュメントを探したけどこの挙動の仕様について明記されている場所は見つからず
https://docs.swift.org/swift-book/LanguageGuide/Functions.html#ID169
https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html#ID196
*3:内容は確認できていないけど C++ と同じ挙動だそう
SwiftUI でアプリ全体のアクセントカラーを指定する Accent color for the entire SwiftUI app
新規アプリで Xcode のテンプレートからプロジェクトを作成すると Assets.xcassets に AccentColor という名称のカラーアセットが用意されている。これに色を設定するとアプリ全体の AccentColor として反映される。

import SwiftUI struct ContentView: View { var body: some View { NavigationView { Button("Button") { } .toolbar { ToolbarItem(placement: .automatic) { Button(action: {}, label: { Text("Button") }) } } } } }

既存プロジェクトの場合
既存プロジェクトの場合は同名のカラーアセット AccentColor を用意するだけでは反映されない。
プロジェクトテンプレートのビルド設定を見ると Global Accent Color Name という項目がありそこに AccentColor の名称が指定されていた。
設定キーは ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME らしい。
ビルド時にはこの値を見てコンパイルされる。

なのでカラーアセットの名称も AccentColor 固定ではなくてここで自由に設定する事ができる。*1
*1:記事タイトルに SwiftUI と入れたけどこの設定は UIKit でも同じ
Swift での範囲内の数値の判定バリエーション
Comparison operator + logical AND operator
他の言語と同じ < > <= >= && を使うパターン
| 条件 | 比較演算子+論理和演算子 |
|---|---|
| 0以上1以下 | if 0.0 <= value && value <= 1.0 |
| 0以上1未満 | if 0.0 <= value && value < 1.0 |
| 0超え1以下 | if 0.0 < value && value <= 1.0 |
| 0超え1未満 | if 0.0 < value && value < 1.0 |
Comparison Operator
https://docs.swift.org/swift-book/LanguageGuide/BasicOperators.html#ID70
Logical AND Operator
https://docs.swift.org/swift-book/LanguageGuide/BasicOperators.html#ID78
Comparison operator + comma
Swift ではカンマ , で条件を列挙できる *1
| 条件 | 比較演算子+論理演算子 |
|---|---|
| 0以上1以下 | if 0.0 <= value, value <= 1.0 |
| 0以上1未満 | if 0.0 <= value, value < 1.0 |
| 0超え1以下 | if 0.0 < value, value <= 1.0 |
| 0超え1未満 | if 0.0 < value, value < 1.0 |
Range + nextUp, nextDown
もう一つ Swift で Range ClosedRange を使ったパターン
肝は FloatingPoint の nextUp と nextDown
nextUp はその値より大きい最も小さな値
nextDown はその値より小さい最も大きな値
| 条件 | Range+nextUp(Down)+パターンマッチング |
|---|---|
| 0以上1以下 | if case 0.0...1.0 = value |
| 0以上1未満 | if case 0.0..<1.0 = value |
| 0超え1以下 | if case 0.0.nextUp...1.0 = value |
| 0超え1未満 | if case 0.0.nextUp..<1.0 = value |
| 0超え1未満 | if case 0.0.nextUp...1.0.nextDown = value |
ちなみに、「対象の値」と「対象の値より大きい最も小さな値」の差分って何?
nextUp nextDown によってどう値が変化するの?
という疑問への答えは以下。
0.0.nextUp == .leastNonzeroMagnitude // true
覚えてしまえば1つの条件式で書けてしまうので楽かも?
Range (half-open range)
https://developer.apple.com/documentation/swift/range
ClosedRange
https://developer.apple.com/documentation/swift/closedrange
nextUp
https://developer.apple.com/documentation/swift/floatingpoint/1848104-nextup
nextDown
https://developer.apple.com/documentation/swift/floatingpoint/3017979-nextdown
leastNonzeroMagnitude
https://developer.apple.com/documentation/swift/floatingpoint/1848591-leastnonzeromagnitude
追記
Range + nextUp, nextDown + ~=
記事を見たiOSエンジニアの人から他のパターンも教えてもらった
if case 0.0...1.0 = value
これを演算子 ~= を使って
if 0.0...1.0 ~= value
と置き換え可能との事。
置き換え版は以下
| 条件 | Range+nextUp(Down)+パターンマッチング |
|---|---|
| 0以上1以下 | if 0.0...1.0 ~= value |
| 0以上1未満 | if 0.0..<1.0 ~= value |
| 0超え1以下 | if 0.0.nextUp...1.0 ~= value |
| 0超え1未満 | if 0.0.nextUp..<1.0 ~= value |
| 0超え1未満 | if 0.0.nextUp...1.0.nextDown ~= value |
Appleのドキュメントによれば ~= はパターンマッチング演算子 (pattern-matching operator) で
case statement の書き方のパターンマッチングは内部でこの演算子を使用しているらしい。
勉強になった。
~=(::)
https://developer.apple.com/documentation/swift/1539154
*1:ドキュメントの記載場所は見当たらなかった。どこかに書いてあるんだろうけど。。
UIAction と UIControlEvent
少し調査部分が長くなってしまったのでざっと読みたい人は「まとめ」の段をどうぞ
UIButton でメソッドの実行を設定するには親クラスの UIControl で定義されている以下のメソッドを使用する。
func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event)
https://developer.apple.com/documentation/uikit/uicontrol/1618259-addtarget
実装例
let button = UIButton(type: .system) button.addTarget(self, action: #selector(someMethod), for: .touchUpInside)
iOS 14 では UIAction を使った代替メソッドが UIControl に追加されている。
func addAction(_ action: UIAction, for controlEvents: UIControl.Event)
実装例
let button = UIButton(type: .system) let action = UIAction(title: "title") { action in // do something } button.addAction(action, for: .touchUpInside)
暗黙的なトリガータイミングの指定
UIControl には addAction メソッドの他に以下のイニシャライザが追加されている。
convenience init(frame: CGRect, primaryAction: UIAction?)
https://developer.apple.com/documentation/uikit/uicontrol/3600494-init
このイニシャライザを使用した場合 UIControlEvent の指定がされていない。
ではどのタイミングで処理が発生するのか。
以下のコードでイニシャライザを使って UIAction を指定した時の UIControlEvent を確認してみる。
let action = UIAction(title: "title") { action in // do something } let button = UIButton(frame: frame, primaryAction: action) print(button.allControlEvents) // UIControlEvents(rawValue: 8192)
結果は UIControlEvents(rawValue: 8192) となった。
これは UIControlEvent の primaryActionTriggered の値と一致する。
@available(iOS 9.0, *) public static var primaryActionTriggered: UIControl.Event { get }
https://developer.apple.com/documentation/uikit/uicontrol/event/1618222-primaryactiontriggered
ドキュメントには以下のように書かれているが説明が短い。
A semantic action triggered by buttons.
調べてみたところ以下のメソッドのヘッダコメントにもう少し説明があった。
@available(iOS 14.0, *) public convenience init(frame: CGRect, primaryAction: UIAction?)
Initializes the control and adds primaryAction for the UIControlEventPrimaryActionTriggered control event. Subclasses of UIControl may alter or add behaviors around the usage of primaryAction, see subclass documentation of this initializer for additional information.
ちゃんと UIControlEventPrimaryActionTriggered を使うという記載があったが、じゃあそのトリガーのタイミングがいつかという具体的な説明はされていない。
サブクラスのドキュメントを見ろと書いてあるので見てみる。
UIButton
convenience init(frame: CGRect, primaryAction: UIAction?)
https://developer.apple.com/documentation/uikit/uibutton/3600349-init
The action to perform when the button is selected. The button registers this action for the primaryActionTriggered control event and sets the title and image properties to the action’s title and image.
という事で、ボタンが選択された時に実行すると書かれている。
UISegmentedControl
convenience init(frame: CGRect, actions: [UIAction])
https://developer.apple.com/documentation/uikit/uisegmentedcontrol/3600580-init
No overview available.
ドキュメント無し。
ヘッダコメントの方に記述があった。
Initializes the segmented control with the given frame and segments constructed from the given UIActions. Segments will prefer images over titles when both are provided. Selecting a segment calls UIAction.actionHandler as well as handlers for the ValueChanged and PrimaryActionTriggered control events.
選択すると valueChanged と PrimaryActionTriggered の両方がトリガーされるらしい。
調査結果
結局 UIControlEventPrimaryActionTriggered の詳細な説明は見つからなかった。
コードを書いて試してみたところ実際の動作としては
UIButton は選択時、おそらく touchUpInside に相当。
UISegmentedControl UISwitch UIStepper UIDatePicker も選択時で valueChanged に相当。
UISlider はスライダー操作時にトリガーで、挙動を見た感じでは valueChanged 以外に touchDown touchUpInside touchUpOutside あたりも入っていそうな動きをしていた。
最後に UITextField はキーボードのリターンキーを押したタイミングでトリガーされ、テキスト入力のフォーカスは外れないという挙動だった。UITextField + editingDidEnd の場合はフォーカスが外れた時にトリガーされ、editingDidEndOnExit はフォーカスが外れただけではトリガーされずにリターンキーを押すとトリガーされつつフォーカスが外れるという挙動になるので primaryActionTriggered はこれらとは異なった挙動になる事が分かった。
まとめ
iOS 14.5で確認
primaryAction を使ったイニシャライザで UIAction を設定すると暗黙的に UIControlPrimaryActionTriggered が設定される。
Implicitly using UIControlEventPrimaryActionTrigg…
UIControlPrimaryActionTriggered はクラスによって挙動が異なる。
| Class | UIControlPrimaryActionTriggered と同じような挙動の UIControlEvent |
|---|---|
| UIButton | touchUpInside |
| UISegmentedControl, UISwitch, UIStepper, UIDatePicker | valueChanged |
| UISlider | [valueChanged, touchDown, touchUpInside, touchUpOutside] |
| UITextField | None |
UITextField の場合 UIControlPrimaryActionTriggered と同じ挙動になる UIControlEvent は無い。
| UITextField の UIControlEvent | リターンキーを押した時の挙動 | トリガーのタイミング |
|---|---|---|
| editingDidEnd | フォーカスが外れない | フォーカスが外れた時 |
| editingDidEndOnExit | フォーカスが外れる | リターンキーを押してフォーカスが外れた時(リターンキー以外でフォーカスが外れた場合はトリガーされない) |
| primaryActionTriggered | フォーカスが外れない | リターンキーを押した時 |
UISlider と UITextField で UIControlPrimaryActionTriggered を使用する時は単純に単一の UIControlEvent を用いた時とは挙動が異なる事を頭の片隅に置いておいた方が良いかも知れない。
また、これらの挙動の違いを理解した上で初期化済みのインスタンスに対して UIControlPrimaryActionTriggered でアクションを設定したいという場合は以下の様に実装する事も可能。
let action = UIAction(title: "title") { action in // do something } let textField = UITextField(frame: frame) textField.addAction(action, for: .primaryActionTriggered)
以上
Xcode 12 の SwiftUI + Core Data のプロジェクトテンプレートが不完全
新規プロジェクト作成でプロジェクトテンプレートから iOS App を選択

Interface SwiftUI, Life Cycle SwiftUI App, Language Swift を選択して Use Core Data をチェック

作成される初期画面 ContentView の body とビルド結果は以下
var body: some View { List { ForEach(items) { item in Text("Item at \(item.timestamp!, formatter: itemFormatter)") } .onDelete(perform: deleteItems) } .toolbar { #if os(iOS) EditButton() #endif Button(action: addItem) { Label("Add Item", systemImage: "plus") } } }

画面が真っ白で何も表示されない。
1つ目の原因は NavigationView の指定が無いこと。toolbar のコンテンツを表示するには NavigationView が必要なのでまず body の中身のルートの List を NavigationView に入れる。
var body: some View { NavigationView { List { // 省略 } .toolbar { // 省略 } } }

編集ボタンが表示された。でもまだ項目追加のプラスボタンが表示されていない。
次に toolbar のコンテンツの各ボタンを ToolbarItem に入れる。引数の placement は今回は navigationBarLeading navigationBarTrailing を使用。
var body: some View { NavigationView { List { // 省略 } .toolbar { #if os(iOS) ToolbarItem(placement: .navigationBarLeading) { EditButton() } #endif ToolbarItem(placement: .navigationBarTrailing) { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } } }

プラスボタンを押すと後はテンプレートの実装通り動作しリストが表示される。

これから初めて Swift/SwiftUI をやるぞって時にテンプレートがこの有様だとスタートでつまづいてしまうのでヘルプになればと思いシェアです*1
*1:このテンプレートの不具合が気になった人は Feedback Assistant にレポートしてくれると幸い