Dude, where is my method?

Posted by theorok on Fri, 28 Jun 2019 01:12:24 +0200

Links to the original text: Dude, Where's my Call?
The original chain of the translation: Dude, where is my method?

Imagine one day you're feeding some seemingly harmless code to the Swift compiler.

// xcrun -sdk macosx swiftc -emit-executable cg.swift

import CoreGraphics

let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 0.0, 23.0)

Then a shock wave strikes:

cg.swift:7:12: error: 'CGPathCreateMutable()' has been replaced by 'CGMutablePath.init()'
<unknown>:0: note: 'CGPathCreateMutable()' has been explicitly marked unavailable here
cg.swift:8:1: error: 'CGPathMoveToPoint' has been replaced by instance method 'CGMutablePath.moveTo(_:x:y:)'
<unknown>:0: note: 'CGPathMoveToPoint' has been explicitly marked unavailable here

Where are they? It was renamed.

A major feature of Swift 3 is proposed by Swift-Evolution. SE-0005 (Better Translation of Objective-C APIs Into Swift) and SE-0006 (Apply API Guidelines to the Standard Library) The resulting "super renaming" this time renames some of the methods in C and Objective-C API s to give them a more Swift feel. There's a porter in Xcode that converts your Swift 2 code into a new style. It will perform a lot of mechanical changes, leaving you with some tail-sweeping tasks due to other language changes, such as Remove the for loop of C.

Some renames are fairly minor, such as this one in NSView:

// Swift 2
let localPoint = someView.convertPoint(event.locationInWindow, fromView: nil)

// Swift 3
let localPoint = someView.convert(event.locationInWindow, from: nil)

Here Point is removed from the method name. You know you're dealing with a point, so there's no need to repeat that fact. fromView was renamed from because View only provides redundant type information and does not make the call clearer.

Other changes are larger, such as Core Graphics:

// Swift 2 / (Objective-C)
let path = CGPathCreateMutable()
CGPathMoveToPoint (path, nil, points[i].x, points[i].y)
CGPathAddLineToPoint (path, nil, points[i + 1].x, points[i + 1].y)
CGContextAddPath (context, path)
CGContextStrokePath (context)

// Swift 3
let path = CGMutablePath()
path.move (to: points[i])
path.addLine (to: points[i + 1])

context.addPath (path)
context.strokePath ()

Oh, oh. This has changed a lot. This API now looks like a favorite Swift style API rather than an old-fashioned C API. Apple completely changed the Core Graphics API (and GCD) in Swift to make them more usable. You can't use the old CG C style API in Swift 3, so you need to get used to the new style. I've already got GrafDemo. Core Graphics The sample program of the blog post has run through the automatic translator (twice). You can be here. pull See the changes before and after the first version of Swift 3 in the request, in this pull The request shows changes in Swift 3 versions of Xcode 8b6.

What did they do?

The Core Graphics API is a bunch of global variables and global free methods. That is, methods are not directly bound to instances such as classes or structures. It's only a tradition to use CGContext AddArcToPoint to operate CGContext, but no one will stop you from passing in a CGColor. It just explodes at runtime. It's only in C-style object-oriented that you have an obscure type passed down as the first parameter, as some kind of magic biscuit. The CGContext* method requires a CGContextRef. The CGColor* method requires a CGColorRef.

Through some compiler magic, Apple converts these obscure references into classes and adds methods to these classes to map them to the C API. When the compiler sees something like this:

let path = CGMutablePath()
path.addLines(between: self.points)
context.addPath(path)
context.strokePath()

In fact, behind the scenes, a series of calls are being made:

let path = CGPathCreateMutable()
CGPathAddLines(path, nil, self.points, self.points.count)
CGContextAddPath(context, path)
CGContextStrokePath(context)

New Class

Following are the common types of obscurity that have been treated with Swift 3.0 (ignoring specific types such as CGDisplay Mode or CGEvent), and one or two other representative approaches:

  • CGAffineTransform - translateBy(x:30, y:50), rotate(by: CGFloat.pi / 2.0)

  • CGPath / CGMutablePath - contains(point, using: evenOdd), .addRelativeArc(center: x, radius: r, startAngle: sa, delta: deltaAngle)

  • CGContext - context.addPath(path), context.clip(to: cgrectArray)

  • CGBitmapContext (folded in to CGContext) - let c = CGContext(data: bytes, width: 30, height: 30, bitsPerComponent: 8, bytesPerRow: 120, space: colorspace, bitmapInfo: 0)

  • CGColor - let color = CGColor(red: 1.0, green: 0.5, blue: 0.333, alpha: 1.0)

  • CGFont - let font = CGFont("Helvetica"), font.fullName

  • CGImage - image.masking(imageMask), image.cropping(to: rect)

  • CGLayer - let layer = GCLayer(context, size: size, auxilaryInfo: aux), layer.size

  • CGPDFContext (folded in to CGContext) / CGPDFDocument - context.beginPDFPage(pageInfo)

CGRect and CGPoint had some good extensions before Swift 3.

How did you do that?

The compiler has a built-in syntax converter that converts the explicit style of Objective-C into more Swift forms. Remove repetitive words and words that are merely repetitive type information. Some words that preceded the left parentheses of method calls were also removed and moved into parentheses as parameter labels. In this way, a large number of invocation methods are automatically cleaned up.

Of course, humans like to make subtle and complex words, so there is a mechanism in the Swift compiler that allows manual rewriting of the translation part of the automatic translator. This is a concrete implementation (don't rely on them when exporting products), but they provide an opportunity to gain insight into the work that existing API s do in Swift.

One of the mechanisms involved is "overlay", which is the second library referenced by the compiler when you introduce a framework or C library. Swift Lexicon Overay is described as "enhancing and expanding the library in the system when it is not modified in the system". Some of the great CGRect and CGPoint extensions that have always existed, such as someRect. divide (30.0, fromEdge:.MinXEdge), how did they come about? They come from overlay. The tool chain thought, "Oh, I saw you linking Core Graphics. Let me add a little more convenience. "

There is another mechanism. apinotes In particular CoreGraphics.apinotes Word by word controls naming and visibility in Core Graphics.

For example, calls to initialize infrastructure in Swift, such as CGRectMake, do not work because their initialization methods already exist. So make these call methods unavailable:

# The below are inline functions that are irrelevant due to memberwise inits
- Name: CGPointMake
  Availability: nonswift
- Name: CGSizeMake
  Availability: nonswift
- Name: CGVectorMake
  Availability: nonswift
- Name: CGRectMake
  Availability: nonswift

Then there are other mappings -- if you see this in Swift, call that method:

# The below are fixups that inference didn't quite do what we wanted, and are
# pulled over from what used to be in the overlays
- Name: CGRectIsNull
  SwiftName: "getter:CGRect.isNull(self:)"
- Name: CGRectIsEmpty
  SwiftName: "getter:CGRect.isEmpty(self:)"

If the compiler sees something like rect.isEmpty(), it sends a request to CGRectIsEmpty.

The following are some renaming of methods and functions:

# The below are attempts at providing better names than inference
- Name: CGPointApplyAffineTransform
  SwiftName: CGPoint.applying(self:_:)
- Name: CGSizeApplyAffineTransform
  SwiftName: CGSize.applying(self:_:)
- Name: CGRectApplyAffineTransform
  SwiftName: CGRect.applying(self:_:)

When the compiler sees rect. apply (transform), it knows to call CGRectApplyAffineTransform.

The compiler can only rename the Objective-C API automatically because it follows good system naming. C APIs (such as Core Graphics) need to be implemented through overlay and apinote.

What can you do?

You can do something similar to the apinote mechanism through NS_SWIFT_NAME. You can use this macro to annotate the C/Objective-C header file to indicate which name to use in Swift. The compiler will replace your NS_SWIFT_NAME with the same ("If you see X, call Y").

For example, this is a call in an Intents(Siri) framework:

- (void)resolveWorkoutNameForEndWorkout:(INEndWorkoutIntent *)intent
                         withCompletion:(void (^)(INSpeakableStringResolutionResult *resolutionResult))completion
     NS_SWIFT_NAME(resolveWorkoutName(forEndWorkout:with:));

Calling it from Objective-C looks like this:

NSObject<INEndWorkoutIntentHandling> *workout = ...;

[workout resolveWorkoutNameForEndWorkout: intent  withCompletion: ^(INSpeakableStringResolutionResult) {
     ...
}];

In Swift, this is the case:

let workout: INEndWorkoutIntentHandling = ...
workout.resolveWorkoutName(forEndWorkout: workout) {
    response in
    ...
}

NS_SWIFT_NAME, along with lightweight generics in Objective-C, nullability annotations, and automatic Objective-C API renaming in Swift compiler, gives you a sense of Instant Return of interfaces to the Swift world.

It's possible to use self-made overlay and apinote, but those were originally used when Swift and Apple's SDK were combined. You can distribute apinotes in your own framework, but overlay needs to be compiled from the Swift compiler tree.

To create a more Swift API for yourself, you have to listen to the header file as well as you can (such as adding nullability annotations and NS_SWIFT_NAME), and then put some Swift files in your project to forge overlay to cover any redundancy. These "overlay" files need to be transmitted as source files before they have ABI stability.

Skipping through the 10 headers of iOS, it seems that the new API prefers NS_SWIFT_NAME, while the older API uses apinote. This makes sense because these headers are shared in different Swift versions, and adding new NS_SWIFT_NAME to older headers may damage the current code without changing the compiler. Furthermore, apinotes can be added by compiler teams or community members, and changes to header files require the attention of the team that owns the header file. And that team might be ready to release their functionality.

Is it good?

Swift 3 Core Graphics is definitely better and more Swift-like. To be honest, I also want to use it on Objective-C. You may lose some Google-enabled features, and you need to do some brain-switching when you see existing CG code in Stack Overflow articles or online tutorials. But that doesn't necessarily require a lot of mental activity for normal Swift code these days.

There are some inconsistencies in the API due to CG's OO-like nature and how it enters Swift. In this CoreGraphics.apinotes:

- Name: CGBitmapContextGetWidth
  SwiftName: getter:CGContext.width(self:)
- Name: CGPDFContextBeginPage
  SwiftName: CGContext.beginPDFPage(self:_:)

Both CGBitmapContext and CGDFContext methods were stolen by CGContext. This means that you can ask for the width of any CGContext, or call it to start a PDF page. If you look for a non-bitmap context to ask for its width, you will get such runtime errors:

<Error>: CGBitmapContextGetWidth: invalid context 0x100e6c3c0.
If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.

So even if the API is very Swift, the compiler can't catch some types of API misuse. Xcode will happily give you a way to make up for what is actually inappropriate. In a sense, the C API is safer because CGBitmapContext GetWidth clearly tells you that it wants a bitmap context even though the first parameter is technically a CGContextRef. I hope it's just a bug.( rdar://27626070).

If you want to learn more about super renaming and tools like NS_SWIFT_NAME, take a look at this. WWDC 2016 Session 403 - iOS API Design Guidelines.

Topics: iOS Swift xcode SDK