上一篇文章我给大家展示了ViewChaos强大的UI调试能力,相信有部分读者会对它的实现机制有兴趣,这一篇我给大家讲一下开发这个工具碰到的坑和一些功能实现的原理。如果你还没有看上一篇ViewChaos我的UI调试之道效果篇,请先看这篇文章。另外Github地址为
ViewChaos,请大家赏脸给个Star,我将继续写更好的文章和开源项目。
怎么才能在不写一行代码的情况下启动ViewChaos
这个问题其实并不难,相信各位读者知道在Objective-C
里,有一个方法叫load
,利用它,在里面加上自己想要的代码,很容易便能在APP启动的时侯加入自己想要的东西
1 2 3 4 5 6 |
+(void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken,^{ //在这里面加入自己想要的功能 }); } |
但问题是Swift已经没有这个方法了,所以只好用另一个办法,就是initialize
方法,这个方法可以放在extension里面,当APP里的UIWindow
类每实例化一次,就会调用这个方法。所以我们还要加入单次分派,来保证只调用一次。
1 2 3 4 5 6 7 8 9 10 |
extension UIWindow { #if DEBUG //这里用了宏 public override class func initialize(){ //initialize方法 struct UIWindow_SwizzleToken { static var onceToken:dispatch_once_t = 0 } //在这里面加入自己想要的功能 } #endif } |
这样ViewChaos就能随系统启动而不用写一行代码,但这里存在的问题是这样如何后来APP开发者也想写这种功能,如果他想用扩展UIWindow
来实现自己的功能,会导致冲突。
怎么才能在Debug模式下启用功能,而Release模式下自动关闭
这个很简单,上一段代码里我用了宏,这个宏说明只有在DEBUG模式下才会编译里面的代码。所以Release自然就没有该功能了,但目前是Swift其实并不支持宏,而是通过Swift Compiler-Custom Flags
的方式来实现的,在里面的Other Swift Flags
里面加入-DDEBUG
标记就行了,
怎么添加那个小圆球
我们在UIWindow
的initialize
方法中使用了Method Swizzle,这里就不解释什么是Method Swizzle了,我在这里替换了四个方法,其中makeKeyAndVisible
方法是APP启动时必定会调用的一个方法。我替换了这个方法,在里面加入了这个小球
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//这个方法就不用我解释了吧。 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { window = UIWindow(frame: UIScreen.mainScreen().bounds) let mainViewController = ViewController() print(mainViewController.chaosName) let rootNavigationController = UINavigationController(rootViewController: mainViewController) window?.rootViewController = rootNavigationController window?.makeKeyAndVisible()//这个方法被我替换,加入了小球 return true } Chaos.hookMethod(UIWindow.self, originalSelector: #selector(UIWindow.makeKeyAndVisible), swizzleSelector: #selector(UIWindow.vcMakeKeyAndVisible)) public func vcMakeKeyAndVisible(){ self.vcMakeKeyAndVisible()//看起来是死循环,其实不是,因为已经交换过了 if self.frame.size.height > 20 let viewChaos = ViewChaos() self.addSubview(viewChaos) /加入小球 UIApplication.sharedApplication().applicationSupportsShakeToEdit = true //启用摇一摇功能 } } |
如果启动摇一摇功能
见上面代码添加UIApplication.sharedApplication().applicationSupportsShakeToEdit = true
就能启动摇一摇了,当然,关闭也可以用这个属性。然后再在
public override func motionBegan(motion: UIEventSubtype, withEvent event: UIEvent?)
方法里处理事件就OK了,当然苹果还提供了
1 2 |
public override func motionEnded(motion: UIEventSubtype, withEvent event: UIEvent?) //摇一摇结束 public override func motionCancelled(motion: UIEventSubtype, withEvent event: UIEvent?) //摇一摇取消,我不知道这个事件是会怎么触发的 |
这两个方法。
如何放大View并获取该点的颜色
这个功能比较有意思,首先在放大镜模式下App里面的点击和触摸事件都要让它失效,不然会起冲突。我定义了一个叫ZoomViewBrace
的View。它的作用是起承担override func touchesMoved(touches: Set, withEvent event: UIEvent?)
事件的,这样就可以屏蔽掉原页面里的点击和触摸事件,就可以对该View做放大操作了。
放大的View名叫ZoomView
,它是一个UIWindow
对象,它有个viewToZoom
的属性,当我们用手触摸时,截图的View传给该属性,然后再将坐标点也传进去,再调用setNeedsDisplay
方法,
ZoomView
就会自动调用下面的方法,将放大自己1.5倍后再绘制出来。
1 2 3 4 5 6 |
override func drawLayer(layer: CALayer, inContext ctx: CGContext) { CGContextTranslateCTM(ctx, self.frame.size.width / 2, self.frame.size.height / 2) CGContextScaleCTM(ctx, 1.5, 1.5) CGContextTranslateCTM(ctx, -1 * self.pointToZoom!.x, -1 * self.pointToZoom!.y) self.viewToZoom?.layer.renderInContext(ctx) } |
这样就有放大效果了
然后就是该点颜色显示功能,实现它的步骤是这样的,首先获取viewToZoom
的那个View,生成一张截图,再转化成UnsafeMutablePointer
对象,这里面包含了该截图的颜色信息。接下来就是根据坐标点提取RBG值了。这样就能获取该点颜色了。
这里的代码稍微有点长,就不写出来了,建议有兴趣的读者看源码。
如何显示所有View的边框和透明值
这个其实非常简单,就用一个递归加上循环不停在获取UIWindow下里面所有的View的位置,再生成一个和其位置一样的View,显示这个View的边框,再插入这些VIew
到UIWindow就OK啦,透明度也一样。
1 2 3 4 5 6 7 8 9 10 11 |
private func showBorderView(view:UIView){ for v in view.subviews{ let fm = v.convertRect(v.bounds, toView: self) //坐标位置转换。 let vBorder = UIView(frame: fm) vBorder.layer.borderWidth = 0.5 vBorder.tag = -5000 vBorder.layer.borderColor = UIColor.redColor().CGColor self.insertSubview(vBorder, atIndex: 500) showBorderView(v) } } |
如果获取绿色小球下的View
这个ViewChaos最为核心的功能;首先,我定义了一个 arrViewHit
的数组,它是一个[UIView]
对象,它的作用是用来保存位于该小球下的所有的View,当小球上touchesBegain
事件触发或者touchesMove
事件触发时,不停地调用topView方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
override func touchesMoved(touches: Set, withEvent event: UIEvent?) { if !isTouch { return } let touch = touches.first let point = touch?.locationInView(self.window) self.frame = CGRect(x: point!.x - CGFloat(left), y: point!.y - CGFloat(top), width: self.frame.size.width, height: self.frame.size.height)//这是为了精准定位.,要处理当前点到top和left的位移 if let view = topView(self.window!, point: point!) //如果下面有View { let fm = self.window?.convertRect(view.bounds, fromView: view) viewTouch = view viewBound.frame = fm! lblInfo.text = "(view.dynamicType) l:(view.frame.origin.x.format(".1f"))t:(view.frame.origin.y.format(".1f"))w:(view.frame.size.width.format(".1f"))h:(view.frame.size.height.format(".1f"))" windowInfo.alpha = 1 windowInfo.hidden = false } } func topView(view:UIView,point:CGPoint)->UIView?{ //从arrViewHit里面取出最外面有View arrViewHit .removeAll() hitTest(view, point: point) let viewTop = arrViewHit.last arrViewHit.removeAll() return viewTop } |
topView就是取arrViewHit
里面的最后一个View,最后一个View就是位于小球下面的最上面的View。hitTest
这个方法会将所有位置小球下的View放进arrViewHit
里面。
下面看看hitTest
这个方法