[IOS] Swift functional APIs
In the past, people have summarized many common models and best practices for designing APIs. In general, we can always summarize some development examples from Apple's Foundation, Cocoa, Cocoa Touch and many other frameworks. There is no doubt that different people have different opinions on the question of "how to design an API in a specific situation" and there is a lot of room for discussion. However, many Objective-C developers are familiar with common models.
With the emergence of Swift, designing APIs has caused more problems. In most cases, we can only continue to do the work at hand, and then translate the existing methods into the Swift version. However, this is unfair to Swift because Swift has added many new features compared with Objective-C. Here is a reference to Chris Lattner, the founder of Swift:
Swift introduces the idea of generic and functional programming, which greatly extends the design space.
In this article, we will focus onCore Image
This is an example to explore how to use these new tools in API design.Core Image
It is a powerful image processing framework, but its API is sometimes a bit bulky.Core Image
The API is of the weak type-it uses key-value pairs to set the image filter. In this way, errors are easily made when setting the parameter type and name, resulting in running errors. The new API will be very secure and modular, and such runtime errors will be avoided by using types instead of key-value pairs.
Target
Our goal is to build an API that allows us to easily and securely assemble custom filters. For example, at the end of the article, we can write as follows:
let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)let result = myFilter(image)
A custom filter is built above. First, blur the image and then add a color mask. To achieve this goal, we will make full use of the Swift function as a first-class citizen. The project source code can be downloaded from this example project on Github.
Filter Type
CIFilter
YesCore Image
Is used to create Image filters. When instantiatingCIFilter
After the object, you (almost) always passkCIInputImageKey
Input the image, and thenkCIOutputImageKey
Obtain the returned image. The returned result can be input as a parameter of the next filter.
In the APIS we are about to develop, we will extract the actual content corresponding to these key-value pairs to provide users with a secure and strong-type API. We have defined our own filter type.Filter
It is a function that can input images as parameters and return a new image.
typealias Filter = CIImage -> CIImage
Here we usetypealias
Keyword:CIImage -> CIImage
The Type defines our own name. This type is a function and its parameter isCIImage
, The return value is alsoCIImage
. This is the basic type required for subsequent development.
If you are not familiar with functional programming, you may name a function typeFilter
It is a bit strange. Generally, we use this name to define a class. If we really want to express this type of functional features in some way, we can name itFilterFunction
Or some other similar names. However, we have made a conscious choice.Filter
This name, because in the core philosophy of functional programming, a function is a value, and there is no difference between a function and a struct, integer, multivariate group, or class. At first, I was not very comfortable with it, but after a while I found that it really makes sense to do so.
Build Filter
Now we have definedFilter
Type, then you can define the function to build a specific filter. These functions require parameters to set specific filters and return a typeFilter
. These functions look like this:
func myFilter(/* parameters */) -> Filter
Note that the returned valueFilter
In itself, it is a function. In the future, we can combine multiple filters to achieve the desired processing effect.
To make subsequent development easier, we have extendedCIFilter
Class, added a convenience initialization method, and a computing attribute used to obtain the output image:
typealias Parameters = Dictionary
extension CIFilter { convenience init(name: String, parameters: Parameters) { self.init(name: name) setDefaults() for (key, value : AnyObject) in parameters { setValue(value, forKey: key) } } var outputImage: CIImage { return self.valueForKey(kCIOutputImageKey) as CIImage }}
This convenience initialization method has two parameters: the first parameter is the filter name, and the second parameter is a dictionary. The key-value pairs in the dictionary are set as parameters of the new filter. The convenience initialization method first calls the specified initialization method, which complies with the Swift development specifications.
Calculation attributeoutputImage
You can easily obtain the output image from the filter object. It findskCIOutputImageKey
And convert it intoCIImage
Object. By providing this attribute, API users no longer need to manually convert the returned results.
Fuzzy
With these things, we can now define our own simple filters. Gaussian Blur filters only require a blur radius as a parameter. We can easily complete a blur filter:
func blur(radius: Double) -> Filter { return { image in let parameters : Parameters = [kCIInputRadiusKey: radius, kCIInputImageKey: image] let filter = CIFilter(name:CIGaussianBlur, parameters:parameters) return filter.outputImage }}
This is simple. This fuzzy function returns a function. The parameter of the new function is of the type:CIImage
Image, return value (filter.outputImage
) Is a new image. The format of this fuzzy function isCIImage -> CIImage
To meet the requirements we have previously definedFilter
Type format.
This example is justCore Image
You can repeat the same pattern multiple times to create our own filter function.
Color Mask
Now let's define a color filter that adds a color mask to an existing image.Core Image
This filter is not provided by default, but we can assemble it with an existing filter.
We use two modules to do this. One is to generate a color filter (CIConstantColorGenerator
), And the other is the resource merging filter (CISourceOverCompositing
). Let's first define a filter that generates a constant Color panel:
func colorGenerator(color: UIColor) -> Filter { return { _ in let filter = CIFilter(name:CIConstantColorGenerator, parameters: [kCIInputColorKey: color]) return filter.outputImage }}
This code looks similar to the previous blur filter, but there is a significant difference: the color generation filter does not detect the input image. Therefore, we do not need to name the Input Image Parameters in the function. We use an anonymous parameter._
To emphasize that the image parameters of the filter are ignored.
Next, let's define the merging filter:
func compositeSourceOver(overlay: CIImage) -> Filter { return { image in let parameters : Parameters = [ kCIInputBackgroundImageKey: image, kCIInputImageKey: overlay ] let filter = CIFilter(name:CISourceOverCompositing, parameters: parameters) return filter.outputImage.imageByCroppingToRect(image.extent()) }}
Here we crop the output image to the same size as the input image. This is not strictly required. It depends on how we want filters to work. However, we can see in our examples below that this is a wise move.
func colorOverlay(color: UIColor) -> Filter { return { image in let overlay = colorGenerator(color)(image) return compositeSourceOver(overlay)(image) }}
Once again, we return a function with the parameter image,colorOverlay
Called at the beginningcolorGenerator
Filter.colorGenerator
The filter requires a color as the parameter and returns a filter. ThereforecolorGenerator(color)
YesFilter
Type. HoweverFilter
The type itself isCIImage
DirectionCIImage
For the conversion function, we cancolorGenerator(color)
Add a typeCIImage
To obtainCIImage
. This is the definitionoverlay
What happened: we usecolorGenerator
The function creates a filter and transmits the image as a parameter to the filter to obtain a new image. Return ValuecompositeSourceOver(overlay)(image)
Similar to this, it consists of a filter.compositeSourceOver(overlay)
And an image Parameterimage
.
Filter combination
Now we have defined a blur filter and a color filter. We can combine them when using them: first we will blur the image, then put a red masked layer on the top. Let's load an image first:
let url = NSURL(string: http://tinyurl.com/m74sldb);let image = CIImage(contentsOfURL: url)
Now we can combine the filters and apply them to an image:
let blurRadius = 5.0let overlayColor = UIColor.redColor().colorWithAlphaComponent(0.2)let blurredImage = blur(blurRadius)(image)let overlaidImage = colorOverlay(overlayColor)(blurredImage)
We again assembled the image through a filter. For example, in the last row, we first got the blur filter.blur(blurRadius)
And then apply the filter to the image.
Function assembly
However, we can do better than above. We can simply combine the two filters into one line. This is the first thing that can be improved in my mind:
let result = colorOverlay(overlayColor)(blur(blurRadius)(image))
However, these parentheses make this line of code completely unreadable. A better way is to define a function to complete this task:
func composeFilters(filter1: Filter, filter2: Filter) -> Filter { return { img in filter2(filter1(img)) }}
composeFilters
Both parameters of the function are filters, and a new Filter is returned. The assembled filter requiresCIImage
Type parameters, and the parameters are passedfilter1
Andfilter2
. Now we can usecomposeFilters
To define our own filter combination:
let myFilter = composeFilters(blur(blurRadius), colorOverlay(overlayColor))let result = myFilter(image)
We can further define a filter operator to make the code more readable,
infix operator >|> { associativity left }func >|> (filter1: Filter, filter2: Filter) -> Filter { return { img in filter2(filter1(img)) }}
Operatorinfix
Keyword definition, indicating that the operator hasLeft
AndRight
Two parameters.associativity left
This operation satisfies the left combination law, that is, f1 >|> f2 >|> f3 is equivalent to (f1 >|> f2 >|> f3. The left combination law is satisfied by this operation, and the filter on the left is applied in the operation. Therefore, the filter sequence is from left to right, just like a Unix pipeline.
The rest is a function, content andcomposeFilters
Basically the same, but the function name is changed>|>
.
Next we will apply this combined filter splitter to the previous example:
let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)let result = myFilter(image)
Operators make the code easier to read and understand the order in which filters are used, making it easier to call filters. It's like1 + 2 + 3 + 4
Aspect Ratioadd(add(add(1, 2), 3), 4)
Clearer and easier to understand.
Custom Operators
Many Objective-C developers are skeptical about custom operators. When Swift was just released, it was not very popular. Many people have experienced (or even abused) User-Defined operators in C ++, some of which are personal experience and some are heard by others.
You may>|>
I have the same skeptical attitude. After all, if everyone defines their own operators, isn't the code hard to understand? Fortunately, there are many operations in functional programming. Defining an operator for these operations is not uncommon.
The filter combination operator we define is an example of a function combination, which is widely used in functional programming. In mathematics, two functionsf
Andg
The combination is sometimes writtenf ° g
In this way, a new function is definedx
Mapf(g(x))
. This happens to be ours.>|>
The work done (except the reverse call of the function ).
Generic
If you think about it, there is no need to define an operator to specifically assemble filters. We can use a generic operator to assemble functions. Currently, our>|>
Yes:
func >|> (filter1: Filter, filter2: Filter) -> Filter
After this definition, the input parameters can only beFilter
Type Filter.
However, we can use the common features of Swift to define a generic function combination operator:
func >|> (lhs: A -> B, rhs: B -> C) -> A -> C { return { x in rhs(lhs(x)) }}
It may be hard to understand at the beginning-at least for me. But after reading each part separately, everything becomes clearer.
First, let's look at the angle brackets behind the function name. Angle brackets define the generic type that this function applies. In this example, we define three types: A, B, and C. Because these types are not specified, they can represent anything.
Next let's take A look at the function parameters: the first parameter: lhs (left-hand side abbreviation) is A function of type A-> B. This indicates that the parameter of A function is A, and the type of the returned value is B. The second parameter: rhs (short for right-hand side) is a function of type B-> C. The parameters are named lhs and rhs because they correspond to the values on the left and right of the operators respectively.
NoFilter
After the filter combination operator, we soon discovered that the previously implemented combination operator is only a special case in generic functions:
func >|> (filter1: CIImage -> CIImage, filter2: CIImage -> CIImage) -> CIImage -> CIImage
Replace all generic types A, B, and C in our mindsCIImage
In this way, we can clearly understand how useful it is to replace the filter combination operator with a common operator.
Conclusion
So far, we have successfully encapsulated it using functional APIs.Core Image
. We hope this example can be well illustrated. For Objective-C developers, there is a completely different world outside of the well-known API design patterns. With Swift, we can now explore new areas and make full use of them.