Se-0005 can better convert Objective-C APIs into swift version

Time:2021-11-26

translator:Poxue

Required before submitting review

As part of the following three documents, their contents are related to each other:

The contents of these three documents are interrelated (for example, the adjustment of an API in the standard library corresponds to an API guideline, or the clang importer rule formulated according to a design guide, etc.). Because of these overlapping contents, in order to ensure that the discussion is maintainable, we hope you:

  • Before submitting the review, have a basic understanding of all the contents of the above three documents

  • When submitting the review of the above three documents, please refer to the review statement of each document。 When you submit a review, if cross references between documents help you explain your ideas, you should include them (which is also advocated).

brief introduction

This proposal describes how we can improve Swift’sClang Importer, it has two functions: first, map the APIs of C and Objective-C to swift version; Secondly, translate the names of functions, types, methods and attributes in Objective-C to meet their requirementsAPI design guideThese requirements are the principles we established when designing swift 3.

Our approach focuses on the Objective-C versionCocoa coding guideAnd swift versionAPI design guideDifferences between. Some simple linguistic analysis methods are used to help us automatically convert the names in Objective-C into more “original” names.

The conversion results can be displayed inSwift 3 API Guidelines ReviewView in this repository. This repository containsSwift 2andSwift 3Written Objective-C APIs project, and some sample code that has been migrated to swift 3 version. You can also passCompare the two branchesTo see all the changes.

motivation

Objective-C versionCocoa coding guideIt provides a complete framework for creating simple and consistent APIs using Objective-C. But swift is a different programming language. In particular, it is a strongly typed language that supports language features such as type derivation, generic programming and overloading. Therefore, Apis written based on Objective-C are a little acclimatized to swift, and these APIs are very wordy in swift. For example:


let content = 
    listItemView.text.stringByTrimmingCharactersInSet(
        NSCharacterSet.whitespaceAndNewlineCharacterSet())

This is obviously an Objective-C style function call. If we write it in swift, the result should look like this:


let content = 
    listItemView.text.trimming(.whitespaceAndNewlines)

This is obviously more followedSwift API design guideIn particular, we ignore the type names that the compiler can force us to use (for example, view, string, character set, etc.). The purpose of this proposal is to make the API introduced from Objective-C more “original swift”, so that swift developers can have a more consistent development experience when using objective API and using swift “native code”.

The solution in this proposal is the same for the Objective-C framework (e.g. cocoa and cocoa touch) and any Objective-C API that can be used in the swift hybrid project. To illustrate,Swift core libraryThe API in Objective-C framework is re implemented, so the changes to these APIs (names) will be reflected in the implementation of swift 3 core library.

Proposed solution

The proposed solution introduces a definition of Objective-CCocoa coding guideandSwift API design guideThis method can help us change the former into the latter by setting a series of rules and referring to the cocoa coding guide and the established customs in Objective-C. This is a heuristic extension for name translation with clang importer. For example, put the global in Objective-CenumThe constants are changed into cases in swift (this needs to remove the prefix set by Objective-C for the global enum constant name) and the factory methods in Objective-C (for example:+[NSNumber numberWithBool:])Mapping to initialization methods in swift(NSNumber(bool: true))。

The heuristic described in this proposal needs to be iterated, tuned, and tested by covering a large number of Objective-C APIs to ensure that it eventually works. However, it is still impossible to work perfectly. There must be some APIs. After “translation”, they will not be as clear as the original meaning. Therefore, our goal is to ensure that most objective C APIs can be more original in swift after translation. It also allows the author of the Objective-C API to explain the problem through API comments in the Objective-C header file for those unsatisfied translations.

The proposed solution introduces the following changes to the clang importer:

  1. generalizationswift_nameApplication scope of property: clang’sswift_nameNow it can only be used for renamingenumCases and factory methods. When it is introduced into swift, it should be generalized to allow renaming of arbitrary C or Objective-C language elements, so as to facilitate the author of C or Objective-C API to better adjust the renaming process.

  2. Remove redundant type names: Objective-C cocoa coding guidelines require method declarations with descriptions of each parameter. When this description restates the type of parameter, the name of the method violates the design requirement of “ignoring unnecessary characters” in swift coding guidelines. Therefore, when performing translation, we should remove those parts that describe the type.

  3. Add default parameters: if the declaration of the Objective-C API strongly implies that parameters need parameter default values, you should add parameter default values for such APIs when introducing swift. For example, a parameter representing a set of options can be set to [].

  4. Add label for the first parameter: if the first parameter of the method has a default value,You should set a parameter label for this parameter

  5. Prefix Boolean semantic attributes with “is”Bool attribute should express the semantics of assertion when readingHowever, in the Objective-C cocoa coding guide,The word “is” is prohibited in attribute names。 Therefore, when such attributes are introduced, they are prefixed with “is”.

  6. A name that expresses the semantics of a value, starting with a lowercase letter: in the swift API design guide, lowercase letters are required for “non type declarations”. include,enumMediumcaseAnd the declaration of a property or function. Therefore, when introducing values without prefixes in Objective-C, make the initials of these names lowercase (for example, a name calledURLHandlerThe property of should becomeurlHandler)。

  7. Let realizecompare(_:) -> NSComparisonResultThe class complies with the comparable protocol: in Objective-C, the comparison results of class objects are judged by “sorting” (Note: for example:NSOrderedDescendingandNSOrderedAscending)。 During the import process, by making these classes comply withComparableProtocol, which can make the implementation of comparison operation more formal (Note: throughComparableProvided operator (method).

In order to feel the actual effect of these conversion rules, take a lookUIBezierPathFrom swift 2:


class UIBezierPath : NSObject, NSCopying, NSCoding {
    convenience init(ovalInRect: CGRect)
    func moveToPoint(_: CGPoint)
    func addLineToPoint(_: CGPoint)
    func addCurveToPoint(_: CGPoint, 
        controlPoint1: CGPoint, controlPoint2: CGPoint)
    func addQuadCurveToPoint(_: CGPoint, 
        controlPoint: CGPoint)
    func appendPath(_: UIBezierPath)
    func bezierPathByReversingPath() -> UIBezierPath
    func applyTransform(_: CGAffineTransform)
    var empty: Bool { get }
    func containsPoint(_: CGPoint) -> Bool
    func fillWithBlendMode(_: CGBlendMode, alpha: CGFloat)
    func strokeWithBlendMode(_: CGBlendMode, alpha: CGFloat)
    func copyWithZone(_: NSZone) -> AnyObject
    func encodeWithCoder(_: NSCoder)
}

Changes after migration to swift 3:


class UIBezierPath : NSObject, NSCopying, NSCoding {
    convenience init(ovalIn rect: CGRect)
    func move(to point: CGPoint)
    func addLine(to point: CGPoint)
    func addCurve(to endPoint: CGPoint, 
        controlPoint1 controlPoint1: CGPoint, 
        controlPoint2 controlPoint2: CGPoint)
    func addQuadCurve(to endPoint: CGPoint, 
          controlPoint controlPoint: CGPoint)
    func append(_ bezierPath: UIBezierPath)
    func reversing() -> UIBezierPath
    func apply(_ transform: CGAffineTransform)
    var isEmpty: Bool { get }
    func contains(_ point: CGPoint) -> Bool
    func fill(_ blendMode: CGBlendMode, alpha alpha: CGFloat)
    func stroke(_ blendMode: CGBlendMode, alpha alpha: CGFloat)
    func copy(with zone: NSZone = nil) -> AnyObject
    func encode(with aCoder: NSCoder)
}

It can be seen that in the swift 3 version, many parts describing type information in the original API have been removed. The converted result is closer to the requirements in swift API design guide. Now, swift developers canfoo.copy()In this way, copy any complianceNSCopyingInstead of looking like the originalfoo.copyWithZone(nil)In this way.

Implementation process

A pilot implementation of this proposal is in swift main repository. The swift compiler provides switches to help us see the Objective-C API introduced as described in this proposal and the conversion results of the swift code itself (for example, through the utils / omit-needless-words.py script). These switches are:

  • --enable-omit-needless-words: this switch enables most changes to the clang importer (1, 2, 4, 5 mentioned in the previous section). It is mainly suitable for printing on master andSwift 2.2 branchOn, swift provides an interface to the Objective-C module. staySwift 3 API guidelines branchOn, it is on by default;

  • --enable-infer-default-arguments: this switch enables interference with the default value of the parameter in the clang importer (3 in the previous section);

  • --swift-migration: only inSwift 2.2 branchThis option performs the basic transformation of migrating names from swift 2 to swift 3 by adding “fix its”. By using it with other compiler switches (e.g. – fixit code, – fixit all) and a script for collecting and applying “fix its” (utils / apply fixit edits. Py), the basic migration work provided by this switch can help us understand the appearance of swift code after conversion according to this proposal in various declaration and call scenarios;

In order to use the converted name and really compile swift 3 code, you can useSwift 3 API GuidelinesBranch. The compiler enables the above functions by default, with a standard library changed with them.

Design details

This section describes the experimental implementation details of articles 2-5 of the above change rules. The real implementation is in the swift code tree, mainlylib/Basic/StringExtras.cppMediumomitNeedlessWordsFunction.

The following description is closely related to the Objective-C API involved in name translation. For example:startWithQueue:compeltionHandler:It’s a method with two selector fragments,startWithQueueandcompletionHandler。 The translation of this selector into swift should bestartWithQueue(_:completionHandler:)

Remove redundant names

Objective-C APIs often contain type names of parameters or return values, which should be removed from swift APIs. The following rules are used to identify and remove these words representing type names. [[see API design principles ignore unnecessary words]].

Define type name

The matching process is to search for specific information in the selector fragment of the old swift APIType nameSuffix, these type names are defined as follows:

  • For most Objective-C types, type names are ignorednullableThe name introduced by swift, for example:

    Objective-C type Type name
    float Float
    nullable NSString String
    UIDocument UIDocument
    nullable UIDocument UIDocument
    NSInteger NSInteger
    UIUInteger NSUInteger
    CGFloat CGFloat
  • When the Objective-C type is a block, the type name in swift isBlock

  • When the type of Objective-C is a function pointer or reference, the type name in swift isFunction

  • When the type of Objective-C is a divisionNSIntegerNSUIntegerorCGFloatFor typedefs other than, the type name in swift is the name of the type actually represented by these typedefs. For example, the type in Objective-C isUILayoutPriorityActually, it’s afloatFor typedef of type, we will try to match the string (in the old version of swift API)Float。 [see API design principles for information compensation for weak types].

Match type name

In order to delete redundant type information in the selector fragment, we need to match the string containing the above type information in the selector.

All matches are managed by the following two basic rules:

  • Match at the beginning and end of the word: whether inside the selector fragment or the type name, the word boundary position is the beginning or end of a string and before each uppercase letter. Taking each capital letter as a word boundary allows us to match “capital acronyms” without maintaining a special list of acronyms or prefixes;

For example, the followingURLIs a match:

func documentForURL(_: NSURL) -> NSDocument?

However, the following view cannot be matched because it is in the middle of the word.

var thumbnailPreview : UIView  // not matched
  • The matching character extends to the end of the type name: because we support the suffix matching a type name, so:

    func constraintEqualToAnchor(anchor: NSLayoutAnchor) -> NSLayoutConstraint?

Can be simplified to:

func constraintEqualTo(anchor: NSLayoutAnchor) -> NSLayoutConstraint?

Based on the above two principles, we can perform the following matching processes:

  • Basic matching

    • The string in the selector fragment matches the string of the type name, for example:appendStringMediumStringmatchingNSStringMediumStringfunc appendString(_: NSString)

    • In the selector fragmentIndexMatch in type nameIntFor example:func characterAtIndex(_: Int) -> unichar

  • Collection matches

    • In the selector fragmentIndexesorIndicesMatch in type nameIndexSetFor example:func removeObjectsAtIndexes(_: NSIndexSet)

    • If the singular form of the plural noun in the selector fragment matches the type of the element in the set, the plural noun in the selector matches the set type name, for example:func arrangeObjects(_: [AnyObject]) -> [AnyObject]

Special suffix matching

  • In the selector fragment, match the in the type name with an empty string suffixTypeor_tFor example:

    //Note: type is matched with an empty string, so saveoperation plus
    //The empty string matches the saveoperationtype,
    //Then the part of saveoperation in the selector can be deleted.
    func writableTypesForSaveOperation(
        _: NSSaveOperationType) -> [String]
    //Note: type is matched with an empty string.
    func objectForKey(_: KeyType) -> AnyObject
    //Note: match with an empty string_ t。
    func startWithQueue(_: dispatch_queue_t, 
        completionHandler: MKMapSnapshotCompletionhandler)
  • The empty string in the selector fragment can match the suffix in the form of “number + D” in the type name, for example:

    //Coordinate + empty string matches to 2D
    func pointForCoordinate(_: CLLocationCoordinate2D) -> NSPoint

Name constraints

(Note: restrictions on deleting type names)

If any of the following restrictions are violated when deleting the selector name, the deletion will not occur:

  • Don’t delete all selectors

  • Do not convert the first segment of the selector to the swift keyword

In swift, the first selector fragment in the Objective-C method becomesThe basis for building a method nameperhapsThe name of an attribute

None of them can be swift keywords, otherwise, users need to use backquotes to use them.

For example, the following usage is reasonable:

extension NSParagraphStyle {
    class func defaultParagraphStyle() -> NSParagraphStyle
}
let defaultStyle = 
    NSParagraphStyle.defaultParagraphStyle()  // OK

If we deleteParagraphStyle, it will be bad to use:

extension NSParagraphStyle {
    class func `default`() -> NSParagraphStyle
}
let defaultStyle = 
    NSParagraphStyle.`default`()    // Awkward

Other selector fragments in the Objective-C method name will become the parameter label of the swift method. This label is allowed to use the swift keyword, for example:

receiver.handle(someMessage, for: somebody)  // OK
  • Do not convert the method name to “get”, “set”, “with”, “for” or “using”, the meaning of the name formed by these words is very empty;

  • Do not delete suffixes in method names that describe parameters unless the suffix is directly preceded by a preposition, verb, or gerund

This heuristic transformation helps us avoid breaking noun phrases that introduce parameters. Deleting noun phrase suffixes directly usually does not bring the expected ideographic results. For example:

func setTextColor(_: UIColor)
...
button.setTextColor(.red())  // clear

If we delete theColor, onlyText, the semantics expressed when calling a method can be confusing:

func setText(_: UIColor)
...
button.setText(.red())      // appears to be setting the text!
  • If the name of a method matches an attribute in its type, do not convert the method name

This heuristic transformation allows us to avoid converting too general names to methods that modify class properties, such as:

var gestureRecognizers: [UIGestureRecognizer]
func addGestureRecognizer(_: UIGestureRecognizer)

If we delete the in the methodGestureRecognizer, just leaveadd, for one is actually modifyinggesturerecognizersThe name is obviously too generic for the method of property.

var gestureRecognizers: [UIGestureRecognizer]
func add(_: UIGestureRecognizer) // should indicate that we're adding to the property

To delete a type name

We delete redundant names as follows:

1. Delete the result type information of the header。 In particular, when:

  • When a method returns a type of its own;

  • And the name of this type matches the first selector fragment in the method;

  • The matched noun is followed by a preposition;

    You can delete the match.

    Generally, the properties and methods that match these conditions are used to change their own type into some other equivalent form of value.

    For example:

extension NSColor {
  func colorWithAlphaComponent(_: CGFloat) -> NSColor
}
let translucentForeground = 
    foregroundColor.colorWithAlphaComponent(0.5)

Can be simplified to:

extension NSColor {
    func withAlphaComponent(_: CGFloat) -> NSColor
}
let translucentForeground = 
    foregroundColor.withAlphaComponent(0.5)

2. Delete the redundant preposition by。 In particular, when:

  • In the first step, delete the beginning noun;

  • In the method name, the rest isBy+The form of gerund;

    You can delete the superfluousByYes.

    This heuristic allows us to use similar methodsa = b.frobnicating(c)Method call form. For example:

extension NSString {
    func stringByApplyingTransform(_: NSString, 
        reverse: Bool) -> NSString?
}
let sanitizedInput = 
    rawInput.stringByApplyingTransform(
        NSStringTransformToXMLHex, reverse: false)

Through the first and second steps, it can be simplified into:

extension NSString {
    func applyingTransform(
        _: NSString, reverse: Bool
    ) -> NString?
}
    
let sanitizedInput = 
    rawInput.applyingTransform(NSStringTransformToXMLHex, 
        reverse: false)

3. In the last selector fragment of the method signature, delete any matching type name。 In particular, the following types:

The tail of the method name is Delete matched
Selector fragment for introducing parameters Parameter type name
An attribute name The type name of the property
A method without parameters The type name of the return value

For example, the following situations:

extension NSDocumentController {
    func documentForURL(
        _ url: NSURL) -> NSDocument? // parameter introducer
}
extension NSManagedObjectContext {
    var parentContext: NSManagedObjectContext?  // property
}
extension UIColor {
    class func darkGrayColor() -> UIColor  // zero-argument method
}
...
    
myDocument = self.documentForURL(locationOfFile)
if self.managedObjectContext.parentContext != changedContext { 
    return 
}
    
foregroundColor = .darkGrayColor()

Can be simplified to:

extension NSDocumentController {
    func documentFor(_ url: NSURL) -> NSDocument?
}
extension NSManagedObjectContext {
    var parent : NSManagedObjectContext?
}
extension UIColor {
    class func darkGray() -> UIColor
}
...
myDocument = self.documentFor(locationOfFile)
if self.managedObjectContext.parent != changedContext { 
    return 
}
foregroundColor = .darkGray()

4. As long as the verb is directly connected in front of the matched type name, even if the type name is in the middle of the method name, it can be deletedFor example:

extension UIViewController {
    func dismissViewControllerAnimated(
        flag: Bool, 
        completion: (() -> Void)? = nil)
}

Can be simplified to:

extension UIViewController {
    func dismissAnimated(
        flag: Bool, 
        completion: (() -> Void)? = nil)
}
Why must deletion be performed in order?

Some of the following simplifications match in the first selector fragment of the method name, and some match at the end of the method name. WhenName constraintsWhen preventing us from deleting from the beginning and end, deleting names from the head can maintain the consistency of the method family, for example, forNSFontDescriptorFor example:

func fontDescriptorWithSymbolicTraits(
    _: NSFontSymbolicTraits) -> NSFontDescriptor
func fontDescriptorWithSize(
    _: CGFloat) -> UIFontDescriptor
func fontDescriptorWithMatrix(
    _: CGAffineTransform) ->  UIFontDescriptor
...

According to the header matching rules, they can become as follows:

func withSymbolicTraits(
    _: UIFontDescriptorSymbolicTraits) ->  UIFontDescriptor
func withSize(
    _: CGFloat) -> UIFontDescriptor
func withMatrix(
    _: CGAffineTransform) -> UIFontDescriptor
...

If we insist on deleting from the tail, delete the in the first methodSymbolicTraitsAfter that, we can no longer delete the headerfontDescriptorBecause it goes against ourName constraintsThere is only one principle defined inwithMeaning is not enough.

func fontDescriptorWith(
    _: NSFontSymbolicTraits) -> NSFontDescriptor // inconsistent
func withSize(_: CGFloat) -> UIFontDescriptor
func withMatrix(_: CGAffineTransform) -> UIFontDescriptor
...

Note: in this way, we destroy the name consistency of the original family method.

Add parameter defaults

Except for setter methods with only one parameter, default values should be added to the parameters in the following cases:

  • Give WayThe trailing closure parameter can be nullValue isnil

  • Give WayCan be emptyNSZoneparameterDefault tonil。 Zones are rarely used in swift, they should always benilof

  • Give WayParameters with “options” in the parameter nameThe default value is[], representing an empty set of options;

  • Nsdictionary parameter related to options, properties, or information. The default value is[:]

By collecting these rules, this heuristic method can put the following code:

rootViewController.presentViewController(
     alert, animated: true, completion: nil)
    
UIView.animateWithDuration(0.2, delay: 0.0, 
    options: [], 
    animations: { self.logo.alpha = 0.0 }) {
    _ in self.logo.hidden = true 
}

Become like this:

rootViewController.present(alert, animated: true)

UIView.animateWithDuration(0.2, delay: 0.0, 
    animations: { self.logo.alpha = 0.0 }) { 
    _ in self.logo.hidden = true 
}

Add label for the first parameter

If the first selector fragment in the method name contains a preposition,Just separate the selector fragment from the last preposition, turn the part after the preposition in the selector into the label of the first parameter.

In addition to adding labels for the first parameter of a large number of APIs, when the first parameter has a default value, this heuristic method can also eliminate the fuzzy semantics expressed by the method name when the method is called. For example:

extension UIBezierPath {
    func enumerateObjectsWith(
        _: NSEnumerationOptions = [], 
        using: (AnyObject, UnsafeMutablePointer) -> Void)
}

array.enumerateObjectsWith(.Reverse) { // OK
   // ..
}

array.enumerateObjectsWith() { // ?? With what?
   // ..
}

become:

extension NSArray {
    func enumerateObjects(
      options _: NSEnumerationOptions = [], 
      using: (AnyObject, UnsafeMutablePointer) -> Void)
}

array.enumerateObjects(options: .Reverse) { // OK
   // ..
}

array.enumerateObjects() { // OK
   // ..
}

Prefix the attribute of bool semantics with “is”

In Objective-C, the attribute expressing bool semantics uses the corresponding getter method as the name of the attribute in swift. For example:

objective-c
@interface NSBezierPath : NSObject
@property (readonly,getter=isEmpty) BOOL empty;

Will become:

extension NSBezierPath {
  var isEmpty: Bool
}

if path.isEmpty { ... }

Guidelines for implementing comparison methods

Nowadays, in order to realize object comparison, for exampleNSDateIn general, developers often extendNSDateLet it complyComparableOr useNSDateofcompare(_:) -> NSComparisonResultmethod. In these scenes, rightNSDateUsing table operators can effectively improve code readability, such assomeDate < todayThansomeDate.comare(today) == .OrderedAscendingMuch clearer. Since the conversion process can determine whether a class implements the comparison method in Objective-C, all classes that implement this method can comply with itComparableProtocol.

not onlyNSDate, some other classes in foundation will also be affected by this change, for example:

func compare(other: NSDate) -> NSComparisonResult
func compare(decimalNumber: NSNumber) -> NSComparisonResult
func compare(otherObject: NSIndexPath) -> NSComparisonResult
func compare(string: String) -> NSComparisonResult
func compare(otherNumber: NSNumber) -> NSComparisonResult

Impact on existing code

The proposed changes introduce a number of breaking changes to existing swift code using the Objective-C framework. So, we need a migration tool to migrate swift 2 code to swift 3. stayImplementation processDescribed in-swift3-migrationSwitches provide basic information for such conversion tools. In addition, the compiler needs to provide good error information (with modification suggestions) for swift code that references the old version Objective-C name. In addition, it should also provide an auxiliary query mechanism through the old version name.

statement

In order to finally formSwift API design guide, this automatic name conversion proposal was developed by Dmitri hrybenko, Ted kremenek, Chris LATTNER, Alex migicovsky, Max moiseev, Ali ozer and Tony Parker.

Supplement the comparable part added byChris AmanseSubmitted tocore-librariesIn the mailing list. Philippe hausler added it to this proposal after reviewing it.