WWDC Quick Look 💓 By SwiftGGTeam
Refine Objective-C frameworks for Swift

Refine Objective-C frameworks for Swift

元の動画を見る

ハイライト

このセッションでは、Objective-C ヘッダー ファイルの注釈、名前付きマクロ、およびいくつかの Swift ラッパーを使用して、Swift を API にインポートした後、暗黙的にラップされていないオプション値、Any コレクション、エラースローの曖昧さ、および Swift の習慣に沿ったより具体的な型と呼び出しポイントでいっぱいのフレームワークを変更する方法を示します。

主要内容

多くのチームは、一度に Objective-C フレームワークを Swift に書き換えていません。古いコードは引き続き存在しますが、新しい企業はこれらの API を呼び出すために Swift を使用することが増えています。 Swift コンパイラーは、Objective-C ヘッダーを自動的にインポートします。NSStringNSDate、イニシャライザ、およびエラー処理メソッドが Swift に近いインターフェイスに書き換えられます。問題は、自動変換がヘッダーしか知らないことです。それが書かれたという事実は、フレームワークのメンテナーがそれを書かなかった意図がわかりません (00:2502:50)。

講演中のSpaceKitはその典型例だ。 Objective-C で書かれたモデル フレームワークです。 Swift 側で見られる生成されたインターフェイスには、暗黙的にラップされていないオプションおよびコレクション型が多数含まれています。Any, 特定の保存メソッドは、「保存する必要のある変更がない」場合に Swift によってスローとして扱われ、イニシャライザの継承関係もサブクラス作成者を混乱させます。根本的な原因はヘッダー情報が不十分であることです。 Swift は、宣言された事実に従ってのみ Objective-C をインポートできます (01:3303:16)。

Apple が提供する処理シーケンスは非常に実用的です。まず、Xcode 12 で Swift で生成されたインターフェイスを開いて、Swift クライアントが実際に何を表示するかを確認します。次に、Objective-C ヘッダーに戻り、null 可能性、軽量ジェネリックスを使用します。NS_STRING_ENUMNS_DESIGNATED_INITIALIZERNS_SWIFT_NAMENS_REFINED_FOR_SWIFTそしてNS_ERROR_ENUMインポート結果を修正します。最後に、インターフェイス リストが適切かどうかだけを確認するのではなく、呼び出しポイントから API をチェックします (02:0140:43)。

詳細

null 可能性を使用して、暗黙的にラップされていないオプションの値を排除します。

(04:43) Objective-C のポインタ型は当然次のようになります。nil, Swift がインポート時により正確な情報を取得できない場合、それらは暗黙的にアンラップされたオプションとしてマークされます。セッションの最初のステップは、実際のセマンティクスをヘッダーに書き込むことです。空のローカル ヘッダーは許可されます。nullable、他の場所に置いてくださいNS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END間隔。

//
// SKMission.h
//
// View the generated interface to see how Swift imports this header.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKMission : NSObject

@property (readonly, nullable) NSString *name;

- (instancetype)initWithName:(nullable NSString *)name;

@end

NS_ASSUME_NONNULL_END

キーポイント:

  • NS_ASSUME_NONNULL_BEGINそしてNS_ASSUME_NONNULL_END注釈の繰り返しを減らすために、デフォルトで中間ポインタを null 以外として扱います。
  • name保持されるプロパティnullable、ミッションに名前がない可能性があり、Swift 側がオプションになることを示します。
  • initWithName:のパラメータnullable、発信者は着信を処理する必要があります。nilセマンティクス。
  • トランスクリプト 明確な注意事項: Swift はヘッダーを信頼します。 Objective-C が実際に非 null としてマークされた値を返す場合nil, Swift は戻り点でチェックを行わず、その後空の文字列、無効なオブジェクト、または null ポインターによるクラッシュが発生する可能性があります (08:09)。

(10:31) メンテナが値が実際に常に非 null であるかどうかを確認できない場合は、未指定の null を使用して、Swift の暗黙的なアンラップをオプションに保つことができます。この選択は、問題を見つけにくい場所にドラッグするのではなく、使用時にクラッシュが発生するため、誤って非 null を記述するよりも正直です。

コレクションと文字列定数に型境界を追加する

(11:07) Objective-C の軽量ジェネリックは、Swift が認識するコレクション型に直接影響します。NSArray<SKAstronaut *> *の代わりに、明示的な要素タイプを持つ配列としてインポートされます。[Any]

//
// SKAstronaut.h
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKAstronaut : NSObject
// Stub declaration
@end

NS_ASSUME_NONNULL_END

//
// SKMission.h
//
// View the generated interface to see how Swift imports this header.
//

#import <Foundation/Foundation.h>
#import <SpaceKit/SKAstronaut.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKMission : NSObject

@property (readonly) NSArray<SKAstronaut *> *crew;

@end

NS_ASSUME_NONNULL_END

キーポイント:

  • NSArray<SKAstronaut *> *乗組員の要素タイプを Objective-C ヘッダーに書き込みます。
  • Swift からインポートする必要はありません。Any手動で変換するSKAstronaut
  • このタイプの注釈は、次の場合にも適用されます。NSSetそしてNSDictionary、モデル コレクション、キャッシュ コレクション、クエリ結果コレクションを最初に処理するのに適しています。

(13:23) 文字列定数にも型境界が必要です。SKRocketStageCount当初は特定のロケット定数のみを受信して​​いましたが、Swift saw はすべてStringNS_STRING_ENUMtypedef を Swift の生の文字列列挙型に近い構造体に再形成し、関数が受け取るだけになるようにします。SKRocket

//
// SKRocket.h
//
// View the generated interface to see how Swift imports this header.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef NSString *SKRocket NS_STRING_ENUM;

extern SKRocket const SKRocketAtlas;
extern SKRocket const SKRocketTitanII;
extern SKRocket const SKRocketSaturnIB;
extern SKRocket const SKRocketSaturnV;

NSInteger SKRocketStageCount(SKRocket);

NS_ASSUME_NONNULL_END

キーポイント:

  • typedef NSString *SKRocketまず、文字列定数のグループを同じ概念に基づいてグループ化します。
  • NS_STRING_ENUM定数が次になるように Swift インポート フォームを変更します。SKRocket静的メンバーの使用法。
  • SKRocketStageCount(SKRocket)任意の文字列は受け入れられなくなり、他の文字列を誤って表現する可能性が減りました。
  • スニペットは count の戻り値も次のように書き込みます。NSInteger。トランスクリプトはその理由を説明します: Swift の慣用句Int数えることを表し、UIntビット操作のシナリオにより適しています (11:33)。

イニシャライザ ルールを Swift に書き込む

(15:24) Objective-C で指定されたイニシャライザは規約であり、言語の必須ルールではありません。 Swift はクラスをインポートするときにこの規則に依存します。スペースキットSKAstronautマークされていない場合、Swift サブクラスの作成者は、名前は異なるがセマンティクスが似ている 2 つのイニシャライザをオーバーライドする必要があり、コード補完でもそれらが継承するものを確認することになります。NSObject高麗人参なしのinit

//
// SKAstronaut.h
//
// View the generated interface to see how Swift imports this header.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKAstronaut : NSObject

- (instancetype)initWithNameComponents:(NSPersonNameComponents *)name NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithName:(NSString *)name;
- (instancetype)init NS_UNAVAILABLE;

@property (strong, readwrite) NSPersonNameComponents *nameComponents;
@property (readonly) NSString *name;

@end

NS_ASSUME_NONNULL_END

//
// SKAstronaut.m
//

#import "SKAstronaut.h"

@interface SKAstronaut ()
@property (class, readonly, strong) NSPersonNameComponentsFormatter *nameFormatter;
@end

@implementation SKAstronaut

- (id)initWithNameComponents:(NSPersonNameComponents *)name {
    self = [super init];
    if (self) {
        _name = name;
    }
    return self;
}

- (id)initWithName:(NSString *)name {
    return [self initWithNameComponents:[SKAstronaut _componentsFromName:name]];
}

- (id)init {
  [self doesNotRecognizeSelector:_cmd];
  return nil;
}

- (NSString *)name {
    return [SKAstronaut.nameFormatter stringFromPersonNameComponents:self.nameComponents];
}

+ (NSPersonNameComponents*)_componentsFromName:(NSString*)name {
    return [self.nameFormatter personNameComponentsFromString:name];
}

+ (NSPersonNameComponentsFormatter *)nameFormatter {
    static NSPersonNameComponentsFormatter *singleton;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        singleton = [NSPersonNameComponentsFormatter new];
    });
    return singleton;
}

@end

キーポイント:

  • initWithNameComponents:電話super init, したがって、次のようにマークされていますNS_DESIGNATED_INITIALIZER
  • initWithName:電話self initWithNameComponents:、つまり便利なパスです。
  • 高麗人参不使用init実装時に呼び出されるdoesNotRecognizeSelector:、ヘッダーで使用されますNS_UNAVAILABLESwift および Objective-C クライアントに、このルートを選択しないように明示的に指示します。
  • アノテーションの後、Swift は、Swift クラス自体の初期化モデルに近い継承とオーバーライド要件を表現できます。

Objective-C のエラー処理規則を修正

(20:00) Swift は、Objective-C のエラー規則に従うメソッドをスローメソッドとしてインポートします。この合意の重要な点は、次のとおりです。 false を返すと失敗を意味します。NSError **エラー オブジェクトは書き込まれませんでした。スペースキット古いsaveToURL:error:「変更を保存する必要がない」場合は false を返します。これにより、Swift は非公開の Foundation エラー タイプをスローします。

//
// SKMission.h
//
// View the generated interface to see how Swift imports this header.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKMission : NSObject

/// \returns \c YES if saved; \c NO with non-nil \c *error if failed to save;
///          \c NO with nil \c *error` if nothing needed to be saved.
- (BOOL)saveToURL:(NSURL *)url error:(NSError **)error NS_SWIFT_NOTHROW DEPRECATED_ATTRIBUTE;

/// @param[out] wasDirty If provided, set to \c YES if the file needed to be
///   saved or \c NO if there weren’t any changes to save.
- (BOOL)saveToURL:(NSURL *)url wasDirty:(nullable BOOL *)wasDirty error:(NSError **)error;

@end

NS_ASSUME_NONNULL_END

キーポイント:

  • 旧メソッドプラスNS_SWIFT_NOTHROW、Swift に自動的にインポートしないように指示します。throws
  • DEPRECATED_ATTRIBUTEObjective-C クライアントが古いインターフェイスを引き続き参照できるように、移行用のパスを残しておきます。
  • 新しいメソッドは、「ファイルが本当に書き込まれたかどうか」を判定します。wasDirtyoutパラメータ、みましょうBOOL戻り値は成功または失敗のみを示します。
  • この変更は、単なる名前の変更ではなく、トランスクリプトのエラー規約の説明から来ています。

Swift ラッパーを使用して、Objective-C では実行できないインターフェイスを表現する

(22:40) Objective-Cのエラー規約は戻り値を占有しており、「エラーをスローする可能性がある」と「保存するかどうかを返す」を同時に表現することは自然にはできません。セッションの解決策は、フレームワークに Swift ファイルを追加し、拡張機能を使用して Swift API の層をラップし、次に使用することです。NS_REFINED_FOR_SWIFT基礎となる Objective-C メソッドを通常の補完から非表示にします。

//
// SKMission.h
//
// View the generated interface to see how Swift imports this header.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKMission : NSObject

/// \returns \c YES if saved; \c NO with non-nil \c *error if failed to save;
///          \c NO with nil \c *error` if nothing needed to be saved.
- (BOOL)saveToURL:(NSURL *)url error:(NSError **)error NS_SWIFT_NOTHROW DEPRECATED_ATTRIBUTE;

/// @param[out] wasDirty If provided, set to \c YES if the file needed to be
///   saved or \c NO if there weren’t any changes to save.
- (BOOL)saveToURL:(NSURL *)url wasDirty:(nullable BOOL *)wasDirty error:(NSError **)error NS_REFINED_FOR_SWIFT;

@end

NS_ASSUME_NONNULL_END

//
// SwiftExtensions.swift
//

import Foundation

extension SKMission {
    public func save(to url: URL) throws -> Bool {
        var wasDirty: ObjCBool = false
        try self.__save(to: url, wasDirty: &wasDirty)
        return wasDirty.boolValue
    }
}

キーポイント:

  • NS_REFINED_FOR_SWIFTSwift 名の前に 2 つのアンダースコアを追加します。 Xcode は通常、この低レベルのエントリを補完や生成されたインターフェイスから隠します。
  • Swift ラッパーが公開されましたsave(to:) throws -> Bool、エラーと戻り値を別々に表現します。
  • ObjCBool必要な詳細です。トランスクリプトの説明 SwiftBoolとObjective-CBOOL一部のプラットフォームではメモリ表現が異なるため、Swift は使用できません。Boolポインタは Objective-C に直接渡されます。
  • __save(to:wasDirty:)ラッパーからは引き続き呼び出すことができますが、Objective-C API は完全には封印されていません。

最終仕上げには名前付きマクロとエラー列挙型を使用する

(31:35) Swift は Objective-C セレクターを自動的に書き換えますが、命名判断には美的要素があります。NS_SWIFT_NAMEメンテナが Swift 側に表示されるベース名と引数ラベルを直接書き出すことができます。

//
// SKMission.h
//
// View the generated interface to see how Swift imports this header.
//

#import <Foundation/Foundation.h>
#import <SKAstronaut/SKAstronaut.h>

NS_ASSUME_NONNULL_BEGIN

@interface SKMission : NSObject

- (NSSet<SKMission *> *)previousMissionsFlownByAstronaut:(SKAstronaut *)astronaut NS_SWIFT_NAME(previousMissions(flownBy:));

@end

キーポイント:

  • Objective-C selector previousMissionsFlownByAstronaut:デフォルトの分割結果は Swift の読み取りに準拠していない可能性があります。
  • NS_SWIFT_NAME(previousMissions(flownBy:))明らかにflownByパラメータラベルに入れてください。
  • トランスクリプトには次のことも記載されていますNS_SWIFT_NAMEこれは型、定数、変数、グローバル関数に使用でき、グローバル関数を静的メソッド、インスタンス メソッド、またはプロパティに再構成できます (33:12)。

(37:02) NSError ドメインが追加されましたNS_ENUMSwiftではまだ使いにくいです。呼び出し側はまず次のように変換する必要があります。NSError、ドメインを確認してから、コードを enum に変換します。に変更しますNS_ERROR_ENUM最後に、Swift は直接一致できるエラー タイプを合成します。

//
//  SKError.h
//  SpaceKit
//

#import <Foundation/Foundation.h>

extern NSString *const SKErrorDomain;

typedef NS_ERROR_ENUM(SKErrorDomain, SKErrorCode) {
    SKErrorLaunchAborted = 1,
    SKErrorLaunchOutOfRange,
    SKErrorRapidUnscheduledDisassembly,
    SKErrorNotGoingToSpaceToday
};

キーポイント:

  • NS_ERROR_ENUM(SKErrorDomain, SKErrorCode)エラー コードをエラー ドメインにバインドします。
  • Swift はそれを中心に構成されますSKErrorに準拠させます。Error
  • トランスクリプトでは、Swift が合成も行うと説明されています。catchそしてcase動作ロジックのマッチング、マッチング時にドメインとコードを同時にチェックします。
  • これにより、呼び出しサイトを、手書きのドメイン、コード、型変換ではなく、SpaceKit エラーを捕捉したものとして表現できるようになります。

重要ポイント

Objective-C フレームワークの Swift インポート チェックを実行する

  • 何をすべきか: Swift で生成されたパブリック ヘッダーのインターフェイスを定期的に開きます。レコードはオプションです。Anyコレクション、奇妙な初期化子、メソッドのスロー、および名前付けの問題。
  • 実行する価値がある理由: セッションでは、Swift クライアントの最終評価がコール ポイントであることを繰り返し強調しています。生成されたインターフェイスは、問題を見つけるためのエントリ ポイントです。
  • 開始方法: Xcode 12 の [関連項目] メニューで Swift 5 インターフェイスを開き、暗黙的にラップされていない各オプションの値をAnyコレクションは、対応するヘッダーまで遡ります。

null 可能性と古い API の汎用移行計画を追加

  • 何をすべきか: まず最初の null 可能性アノテーションをヘッダーに追加し、次に Xcode 警告を押して残りの位置を埋めます。
  • 実行する価値がある理由: Swift は Objective-C ヘッダーを信頼します。 false の非 null は、後で Swift 側で予期しない動作に変わる可能性があります。
  • 開始方法: を使用しますNS_ASSUME_NONNULL_BEGINほとんどの非 null 領域をラップし、実際に null が許可される場所のみを書き込みます。nullable; コレクション属性が最初に補足されますNSArray<Element *>NSSet<Element *>NSDictionary<Key, Value>

文字列定数 API を型付きインターフェイスに照合します。

  • 内容: 許可される固定定数のみを検索しますNSString *パラメータを変更して取得しますNS_STRING_ENUMtypedef。
  • 実行する価値がある理由: SpaceKitSKRocketStageCountこの例は、純粋な文字列パラメータにより、Swift 呼び出し元が任意の文字列を渡すことができることを示しています。
  • 開始方法: 最初に定義しますtypedef NSString *YourKind NS_STRING_ENUM;、次に定数宣言と関数パラメータをこの typedef に変更し、最後に Swift で生成されたインターフェイスの呼び出し形式を確認します。

Swift クライアント用の小さくて正確なラッパーを作成する

  • 内容: Objective-C で自然に表現できないインターフェイスを非表示にします。NS_REFINED_FOR_SWIFT後で、Swift 拡張機能を使用して、ユーザーに実際に呼び出してほしいメソッドを公開します。
  • 実行する価値がある理由:save(to:) throws -> Boolこの例ではエラーと戻り値が分離されており、元の Objective-C メソッドは基礎となる呼び出し用に予約されています。
  • 開始方法: まず、Objective-C メソッドがアンブレラ ヘッダーに表示されていることを確認し、フレームワークの Swift ファイルに拡張子を書き込みます。合格する必要がありますBOOL *ときに使用されますObjCBool

NSError コードを Swift が一致できるエラーに移行します。

  • 内容: エラー ドメインとエラー ドメインのペアを結合します。NS_ENUMエラーコードを次のように変更しますNS_ERROR_ENUM
  • 実行する価値がある理由: Swift はドメインを認識したマッチング ロジックを合成するため、呼び出し元が手動でロジックを記述する必要はありません。NSErrorドメインとコードのチェック。
  • 開始方法: 最も人気のあるものから始めますcatchエラー列挙の移行が開始され、変更後、予期されるエラー列挙が Swift 呼び出しポイントに書き込まれます。catch形態を調べ、テストで動作を修正します。

関連セッション

  • Swift の新機能 — ランタイム、診断、言語機能、システム ライブラリ、およびパッケージ エコシステムに関する Swift 5.3 の年次更新。
  • Swift の型推論を採用する — Swift コンパイラーがコンテキストから型を推論する方法を説明し、Xcode 12 でのエラー ローカリゼーションの改善を示します。
  • Swift でポインターを安全に管理する — 混合言語境界の根本的な安全性の背景を補うために、安全でないポインター、生のポインター、メモリ バインディングについて詳しく説明します。
  • 安全でない Swift — 強制アンラップ、範囲外のコレクション、ポインター API から、Swift における安全でない操作の境界について説明します。
  • バイナリ フレームワークを Swift パッケージとして配布 — フレームワーク API を保守するチームに適した、Swift パッケージを使用してバイナリ フレームワークを配布します。続きを読んでください。

コメント

GitHub Issues · utterances