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 にレポートしてくれると幸い