I just want a screen

Presumably the use of iPhone users, we all know that in accordance with the Home key + power button can be screenshots. Screenshots for product managers, engineers, designers are more important. Then the iOS code is used but the screenshot function. So in the research and development of iOS, how to effectively screen? The author in the last week it took 2 days to write a Swift version of the open source library – SwViewCapture.

I have a little idea how to intercept the entire web page or even the entire scroll view content? Make a support of the function of the source database will not be popular? Share ideas of CocoaChina+ App and the author’s own ideas based on a little, I decided to write a convenient Swift developers use screenshots of the library support vector, interception of all the contents of the database pages.

  • The idea comes from the origin of an open source – App – CocoaChina+, in which the user can share the contents of the page is being browsed, that is, the content of WebView App.

We may be curious, so a screenshot, you need to write 2 days? At the beginning of the author’s idea is very simple: nothing more than to write a screenshot library. I really really write up, only to find the original light a screenshot also has so many Hom waiting for me to step on. The author of the difficulties encountered in the screenshot of the code here to sort out, so that we can also repeat the pit.

Simplified screenshot API

At the beginning of writing SwViewCapture, I want to achieve a simple point, the first to achieve the basis of the screenshot function, you can convert any View directly into a UIImageView.

Screen based implementation

This function is estimated that most of the iOS developers have been involved in the past, iOS7 has been using CoreGraphic way to Draw out of the corresponding map. The key code is as follows:

UIGraphicsBeginImageContextWithOptions (bounds.size, false), UIScreen.mainScreen (.Scale) self.layer.renderInContext (context!) let = UIGraphicsGetImageFromCurrentImageContext (capturedImage) (UIGraphicsEndImageContext)

The key code of the above figure renderInContext is the CALayer method, CALayer is the bottom layer of CoreGraphic, composed of UIView. UIGraphic and other related operations Context is the Quartz 2D framework in API, and Quartz 2D is one of the components of CoreGraphic.

Only 4 lines of code have been able to meet the most basic needs of the most ~ is because I in this screenshot screenshot in addition to the current method on the WKWebView failed, temporarily and screenshots in the other View on failure, need to continue to check. So what’s wrong with WKWebView?

Screenshot meets WKWebView

When I write SwViewCapture, try to intercept the WKWebView map. The result of the screenshot returned to me is just a background map, apparently the screenshot failed. Through the search StackOverflow and Google, I found that WKWebView can not simply use the layer.renderInContext method to draw graphics.

If direct call layer.renderInContext needs to obtain the corresponding Context, but UIGraphicsGetCurrentContext in WKWebView (NIL) result (specific principle still unknown, after I know will supplement)

StackOverflow provides a solution is to use the UIView drawViewHierarchyInRect method to intercept the screen view.

By directly calling the drawViewHierarchyInRect WKWebView method (afterScreenUpdates parameter must be true), you can successfully intercept the contents of the screen WKWebView.

Page nesting problem

In the search for information to try to solve the problem of WKWebView screenshot, inadvertently search Chrome open source project chromium screenshot source SnapshotManager. I read the source code found that he missed a big scene:

  • UIView based on the scene contains WKWebView screenshot

Refer to the solution in SnapshotManager, define a recursive function to determine whether the WKWebView:

Public func swContainsWKWebView (-> Bool) {if self.isKindOfClass (WKWebView) {return} true {if (in for subView self.subviews subView.swContainsWKWebView (return)) {true}}} return false

The ultimate screenshot is:

  • View any sub View contains WKWebView, then use drawViewHierarchyInRect to intercept the view
  • Any view in the View does not contain WKWebView, then the use of renderInContext way to screenshot

You may wonder why don’t all use the way of drawViewHierarchyInRect well, also to make an unnecessary move judgments, reference chromium source code comments in SnapshotManager to explain why

-drawViewHierarchyInRect:afterScreenUpdates:YES is buggy as of iOS 8.3. Using it afterScreenUpdates:YES creates unexpected GPU glitches screen redraws, during animations, broken pinch to dismiss on tablet, etc. For now only using this with WKWebView, which depends on -drawViewHierarchyInRect. TODO (justincohen): Remove this (and always use drawViewHierarchyInRect once the iOS) 8 bugs have been fixed.

PS: write iOS over the years, more or less have encountered a lot of iOS system native BUG, is drunk

When I write a blog by chromium master on the project, there are still some of the comments.

The author will be based on the above screenshot function package, in the SwViewCapture library, just a line of code can be achieved screenshot function:

{view.swCapture (capturedImage) -> Void in Do something with; / / capturedImage (UIImage)}

Interception content implementation

Ordinary screenshot achieved, then began to want to achieve the full content of the screenshot. The development of a complex function, the first step is to simplify the realization of the function, then the author took UIWebView as an experimental object to achieve the interception of the content. So the question is, how to achieve it?

By printing the size of the UIWebView inside the UIScrollView, you can initially understand the content of the UIWebView is actually carried inside the UIScrollView. Then a simple way to achieve the idea of memory consumption came out:

The length and width of the UIWebView to modify the content of the size of the UIScrollView, and then the UIWebView screenshot with the ordinary way out.

This simple idea is based on the author can immediately think of the UIWebView’s internal UIScrollView length width modify operation and screenshots, if feasible, can be directly extended use in UITableView and UIScrollView based on.

The basic implementation code is as follows:

UIGraphicsBeginImageContext (scrollView.contentSize) let savedContentOffset = scrollView.contentOffset let savedFrame = scrollView.frame scrollView.contentOffset = CGPointZero scrollView.frame = CGRectMake (0, 0, scrollView.contentSize.width, scrollView.contentSize.height) scrollView.layer.renderInContext (UIGraphicsGetCurrentContext) (image = let!) (UIGraphicsGetImageFromCurrentImageContext); scrollView.contentOffset = savedContentOffset; scrollView.frame = savedFrame; (UIGraphicsEndImageContext)

Under this scenario, UIScrollView can be a normal screenshot, then the extension of the application on the UIWebView should not be difficult, right?

PS: UIWebView has a get method can get the corresponding UIScrollView

IOS8 strange BUG

In the basic scenario, the method can intercept UIScrollView normally, but in the iOS8 environment, the tail visual region will be black abnormal BUG. (this BUG may be generated with UIScrollView under UINavigationController hosted by VC)

Through a burst of Stackoverflow and CocoaChina+ source code, there is a more appropriate solution:

Add Scrollview another temp and render view to it.

Separate the UIScrollView and put it in a separate UIView. By this method, the iOS8 rendering problem can be blocked.

Synthesis of I just want a screen
improvement scheme

Asynchronous rendering rough solution

The previously described shots used in the actual scene, the author found that some web page elements are loaded asynchronously, that only the page scroll to the corresponding part, will render the loading (the author’s blog page is the theme of this scene). In addition, UIScrollView and UITableView do not lack this scenario.

There is no perfect solution for this asynchronous approach, the author can only solve a violent way to solve some cases:

  • Scroll to the bottom of the screen to scroll to the bottom, scroll back to ScrollView

Screenshot flash problem

The Stackovetflow a Screenshot of a – Getting UIScrollView including offscreen parts in a way to repair the iOS8 screenshot multiple strange black area BUG, also found a problem in the actual screenshot:

  • The screen is displayed in the View carried out in the other View in the rendering, after the completion of the restoration of the recovery, there may be a flash over the situation.

The reason for the above phenomenon is estimated that everyone knows: because View leave the current view, triggering the interface rendering, display interface view is not in the display interface, naturally becomes a background color.

Since it is necessary to do a screenshot library, then this problem is also need to be resolved, the total can not let people call screenshots API flash when it?

I think about it, < font color= “orange” &gt decided to introduce a screenshot of the current view and cover the view on this view, so that we have a visual illusion, to cover up the real view of the operation. < /font>

IOS7 based on View provided by API – snapshotViewAfterScreenUpdates, you can directly produce a screenshot view View, the rest of the work is as follows:

  1. False cover snapshotViewAfterScreenUpdates generated by View
  2. Select the parentView of the target view, and the target view at the parentView level
  3. Add view to the same level of parentView in the target view
  4. Perform real screenshot logic
  5. Remove false cover View from view
I just want a screen
pseudo graph cover hint

Through the above method, the screen flash screen problem is solved perfectly

SetFrame damage

We note that in the implementation of the full content of the screenshot, it will dynamically modify the UIScrollView frame, and then perform the corresponding logical content. In the implementation of the screenshot logic function, often involving asynchronous operation, then in the following scenarios may appear abnormal screenshot:

  1. Users in the corresponding layoutSubView set to modify the need to take screenshots of the view frame.
  2. Screenshot of the user in the process of the need to capture the view frame operation

In fact, the scene 2 contains the scene 1, summary is in the screenshot process, any frame operation will cause damage to the screenshot behavior, but the frame operation may be triggered by layoutSubView and other system functions

Since the emergence of this problem, then I would like to solve this problem, I can think of is the process of the screenshot of the invalid View arbitrary frame operation

Now that you have thought of a solution, then the design code. The author is a try to bind a UIView attribute on the isCapturing by running, then override frame set target view and get method, the set method by judging whether the screenshot to realize whether to call the super frame operation. But in the actual operation, set and get target view in frame operation and simple to be read and write operations, and the operation of the system logic view, so &lt font; color=’red’> not by the way to disable the frame&lt /font>.

After the failure of the override frame program, the author has tried the following scheme:

  • The use of Runtime in front of the screenshot to replace the target view setFrame method, and then after the end of the screenshot, use the run-time recovery

Technical realization as follows:

Let method: Method = class_getInstanceMethod (object_getClass (self), Selector ("setFrame:") swizzledMethod: Method class_getInstanceMethod (let) = object_getClass (self), Selector ("swSetFrame:")) method_exchangeImplementations (method, swizzledMethod) / / capturing / / capturing / /... Method_exchangeImplementations (swizzledMethod, method)

The PS: swSetFrame method is an empty method used to invalidate the setting of frame

Here is a question: do you have the same problem?

SwViewCapture library on the constraints of the treatment is one of the future optimization of the author

Intercepted content and met WKWebView

Based on the screenshot function and BUG have been resolved almost, then try the most troublesome WKWebView bar. After a simple test, through the front of the package UIScrollView screenshots way indeed as expected < font color=’red’> WKWebView can not be full content screenshots < /font>.

I am curious about the composition of the structure of the WKWebView, by scanning the way subview print WKWebView:

Print mode:

For subView in (webView?.subviews)! {print ("V / name: (subView.dynamicType)") if String (subView.dynamicType) = = "WKScrollView" Do something for subSubView {/ / in} subView.subviews {print ("sub / name: (subSubView.dynamicType)") if String (subSubView.dynamicType) {/ / Do = = "WKContentView" something}}}

Print result:

  • WKWebView WKScrollView WKContentView

The base class of WKScrollView is UIWebScrollView, the base class of UIWebScrollView is UIScrollView, and the WKContentView contains the same width and height as the actual content. So is it possible to get a screenshot of the WKWebView below WKScrollView?

I am happy to find a breakthrough, try the screenshot. As a result, whether it is a screenshot of the WKWebView itself or any of its subclasses screenshot, the results of the screenshot is still a blank.

In the above two ways have failed helpless situation, the author did not have a particularly good solution, the absolute first temporary use of violent rendering to synthesize a large screenshot:

  • Capture the screen to display the scope of the size of the view, scroll, according to page screenshots, scroll, according to the page screenshots, loop operation until the last page to scroll, and finally all the interception of the image into a large picture.
I just want a screen
screenshot synthesis schematic

Through the above screenshot program to capture the image still exist < font color=’red’> imperfect < /font> part, especially for the label position: fixed; div element. The tag is position: fixed; the div element will appear on the larger image.

In this scenario, the author do not plan to deal with, because this screenshot WKWebView are not the ideal way of taking a screenshot, need urgently the children can use the business logic to handle:

  1. Scanning position: fixed; element
  2. Calculate the height of the top and tail of the elements of the postion: fixed
  3. According to the distance between the top and tail, respectively, the corresponding display and hidden operations

< font color= > WKWebView “orange” on the screenshot, if the big people who have better screenshot program, please let us know and learn from each other to improve; I will continue to try new ways to see whether the interception of WKWebView and UIWebView as perfect; I think WKWebView is absolutely perfect way to intercept all content no, just temporarily find < /font>

Based on the realization of the contents of the previous screenshot description, the author will simplify the package API, only need to use SwViewCapture, call the following code can be used:

{view.swContentCapture (capturedImage) -> Void in Do something with; / / capturedImage (UIImage)}

About performance

Drawing performance problem

The preparation of the client program may be aware that only the main thread can operate view. Because the iOS and Android system to do the protection under non main thread operation view behavior can have unpredictable consequences, so the system when operating in a non view of the main thread, will often take the initiative to throw an exception. CoreGraphic (draw) way to draw the view is to support multi-threaded operation, rendering process will not be displayed in the view.

In writing SwViewCapture, I tried to use the asynchronous (other threads) to manipulate the drawing process, renderInContext and drawViewHierarchyInRect have tried.

  • RenderInContext support for asynchronous rendering quite good, and there is no screenshot failure and system flash back situation.
  • DrawViewHierarchyInRect multi thread does not support friendly, may be because drawViewHierarchyInRect is the UIView method because of the image rendering in drawViewHierarchyInRect using GCD asynchronous thread will be lost and the failure of the drawing.

In view of the drawViewHierarchyInRect method is not friendly to the thread support, the author in the first version of SwViewCapture does not consider to use multiple threads to optimize performance, is the need to further improve and try to place.

Excessive memory problem

I do not have to consider the memory problems in the first versions of SwViewCapture, so if a very long UIScrollView to the interception of images may appear Caton even flash back phenomenon.

The author believes that before the screenshot, the user should probably know the size of their own view of the screenshot, the need for a certain pre processing. The author plans to introduce a regional block in the future to allow users to customize the way to control the size of the interception view, so as to avoid the possibility of the existence of large memory problems caused by screenshots.

summary

I started a small function just wanted to achieve an interception of web content, in the implementation process could be found on this screenshot of the pit, the pit will process summed up in this article, for your reference. At the same time, the product of the pit will be opened on the Github in the form of Swift library SwViewCapture.

I will take the title as I just want to cut a screen, is a self deprecating way to describe the programmer to achieve a function of the hard ah ~ ~

PS: I level is limited, there is a mistake, please point out in a timely manner ~ ~ if you feel the article useful, you can visit a lot of wow ~!

Reference links

  1. StackOverflow – WKWebView Screenshots
  2. StackOvetflow – Getting Screenshot of UIScrollView including offscreen parts a a
  3. SwViewCapture
  4. CocoaChina+
  5. Chromium – SnapshotManager
  6. Object-C Runtime Reference