上一篇文章我給大家展示了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
這個方法
1 2 3 4 5 6 7 |
func hitTest(view:UIView, point:CGPoint){ var pt = point if view is UIScrollView{ pt.x += (view as! UIScrollView).contentOffset.x pt.y += (view as! UIScrollView).contentOffset.y } if view.pointInside(point, withEvent: nil) |
首先如果該View是UIScrollView
的話,需要把contentOffset
加上去。然後這裡有四個條件需要判斷:當前觸控的點一定要在要抓的View裡面,View不能是隱藏的或者透明的,View不是我們用於定位的邊界View,同時也不是我們用於定位的View.也就是說isDescendantOfView
。然後如果這些條件都滿足,那麼新增這個View到arrViewHit
裡面。然後再對這個View的所有subviews遞迴呼叫這個方法,注意座標需要轉換一下。所有方法遞迴完成之後,arrViewHit
裡面會儲存所有滿足條件的View,也就是所有位於小球下面的View,然後取最後一個出來就行了。
實現顯示View所有資訊的表格&修改View的各種屬性的的控制皮膚
能夠抓取出來那個View,做這個表格和控制皮膚就不難了,主要是堆邏輯和UI,要寫很多很多的程式碼。建議讀者找到自己有興趣的部分再學習研究
這篇文章並不長,主要給讀者詳解了ViewChaos的一些實現原理和難點,主要面向有興趣看原始碼和實現機制的讀者。其實ViewChaos的功能還可以更強大,希望讀者可以好好利用並且提出建議。