[{"data":1,"prerenderedAt":1364},["ShallowReactive",2],{"navigation_docs":3,"-ios-event-delivery":173,"-ios-event-delivery-surround":1359},[4,22,29,36,107,138,145,161],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"博客","i-lucide-pen-line","\u002Fblog","0.blog",[10,12,17],{"title":5,"path":7,"stem":11,"icon":6},"0.blog\u002Findex",{"title":13,"path":14,"stem":15,"icon":16},"创建基于 UIKit 的原生 iOS 项目","\u002Fblog\u002Fcreate-uikit-ios-project","0.blog\u002F01.create-uikit-ios-project","i-lucide-app-window",{"title":18,"path":19,"stem":20,"icon":21},"Lockify：把账号和密码留在自己的手机里","\u002Fblog\u002Flockify","0.blog\u002F02.lockify","i-lucide-lock",{"title":23,"path":24,"stem":25,"children":26,"icon":28},"年度总结","\u002Fannual-review","1.annual-review\u002Findex",[27],{"title":23,"path":24,"stem":25,"icon":28},"i-lucide-git-commit",{"title":30,"path":31,"stem":32,"children":33,"icon":35},"故事","\u002Fstories","2.stories\u002Findex",[34],{"title":30,"path":31,"stem":32,"icon":35},"i-lucide-feather",{"title":37,"icon":38,"path":39,"stem":40,"children":41},"Android","i-simple-icons-android","\u002Fandroid","3.android",[42,44,49],{"title":37,"path":39,"stem":43,"icon":38},"3.android\u002Findex",{"title":45,"path":46,"stem":47,"icon":48},"初探Navigation","\u002Fandroid\u002Fcompose-navigation","3.android\u002F01.compose-navigation","i-lucide-route",{"title":50,"icon":51,"path":52,"stem":53,"children":54},"安卓代码加固","i-lucide-shield","\u002Fandroid\u002Fhidden-hardening","3.android\u002F02.hidden-hardening\u002Findex",[55,58,62,67,72,77,82,87,92,97,102],{"title":56,"path":52,"stem":53,"icon":57},"安卓代码加固：总览","i-lucide-map",{"title":59,"path":60,"stem":61,"icon":21},"安卓代码加固（1）：加密DEX","\u002Fandroid\u002Fhidden-hardening\u002Fencrypt-dex","3.android\u002F02.hidden-hardening\u002F01.encrypt-dex",{"title":63,"path":64,"stem":65,"icon":66},"安卓代码加固（2）：Native解密","\u002Fandroid\u002Fhidden-hardening\u002Fnative-decrypt","3.android\u002F02.hidden-hardening\u002F02.native-decrypt","i-lucide-cpu",{"title":68,"path":69,"stem":70,"icon":71},"安卓代码加固（3）：内存加载","\u002Fandroid\u002Fhidden-hardening\u002Fmemory-load","3.android\u002F02.hidden-hardening\u002F03.memory-load","i-lucide-memory-stick",{"title":73,"path":74,"stem":75,"icon":76},"安卓代码加固（4）：指令分发","\u002Fandroid\u002Fhidden-hardening\u002Fopcode-dispatch","3.android\u002F02.hidden-hardening\u002F04.opcode-dispatch","i-lucide-hash",{"title":78,"path":79,"stem":80,"icon":81},"安卓代码加固（5）：XOR混淆","\u002Fandroid\u002Fhidden-hardening\u002Fxor-obfuscate","3.android\u002F02.hidden-hardening\u002F05.xor-obfuscate","i-lucide-shuffle",{"title":83,"path":84,"stem":85,"icon":86},"安卓代码加固（6）：密钥外置","\u002Fandroid\u002Fhidden-hardening\u002Fkey-external","3.android\u002F02.hidden-hardening\u002F06.key-external","i-lucide-key",{"title":88,"path":89,"stem":90,"icon":91},"安卓代码加固（7）：依赖隔离","\u002Fandroid\u002Fhidden-hardening\u002Fdep-isolate","3.android\u002F02.hidden-hardening\u002F07.dep-isolate","i-lucide-layers",{"title":93,"path":94,"stem":95,"icon":96},"安卓代码加固（8）：构建校验","\u002Fandroid\u002Fhidden-hardening\u002Fbuild-verify","3.android\u002F02.hidden-hardening\u002F08.build-verify","i-lucide-shield-check",{"title":98,"path":99,"stem":100,"icon":101},"安卓代码加固（9）：防录屏","\u002Fandroid\u002Fhidden-hardening\u002Fflag-secure","3.android\u002F02.hidden-hardening\u002F09.flag-secure","i-lucide-eye-off",{"title":103,"path":104,"stem":105,"icon":106},"安卓代码加固（10）：ABI门禁","\u002Fandroid\u002Fhidden-hardening\u002Fabi-gate","3.android\u002F02.hidden-hardening\u002F10.abi-gate","i-lucide-git-compare",{"title":108,"icon":109,"path":110,"stem":111,"children":112},"iOS","i-simple-icons-apple","\u002Fios","4.ios",[113,115,120,125,129,133],{"title":108,"path":110,"stem":114,"icon":109},"4.ios\u002Findex",{"title":116,"path":117,"stem":118,"icon":119},"iOS面试题1","\u002Fios\u002Fios-interview1","4.ios\u002F01.ios-interview1","i-lucide-sparkles",{"title":121,"path":122,"stem":123,"icon":124},"设置启动图","\u002Fios\u002Fset-launch-screen","4.ios\u002F02.set-launch-screen","i-lucide-image",{"title":126,"path":127,"stem":128,"icon":48},"路由管理","\u002Fios\u002Frouting","4.ios\u002F03.routing",{"title":130,"path":131,"stem":132,"icon":91},"状态管理","\u002Fios\u002Fstate-management","4.ios\u002F04.state-management",{"title":134,"path":135,"stem":136,"icon":137},"事件传递","\u002Fios\u002Fevent-delivery","4.ios\u002F05.event-delivery","i-lucide-pointer",{"title":139,"path":140,"stem":141,"children":142,"icon":144},"Flutter","\u002Fflutter","5.flutter\u002Findex",[143],{"title":139,"path":140,"stem":141,"icon":144},"i-simple-icons-flutter",{"title":146,"icon":147,"path":148,"stem":149,"children":150,"page":160},"面试","i-lucide-graduation-cap","\u002Finterview","6.interview",[151,156],{"title":152,"path":153,"stem":154,"icon":155},"Flutter面试题1","\u002Finterview\u002Fflutter-interview1","6.interview\u002F01.flutter-interview1","i-lucide-boxes",{"title":157,"path":158,"stem":159,"icon":66},"Flutter面试题2","\u002Finterview\u002Fflutter-interview2","6.interview\u002F02.flutter-interview2",false,{"title":162,"icon":163,"path":164,"stem":165,"children":166},"算法学习","i-lucide-binary","\u002Falgorithm","7.algorithm",[167,169],{"title":162,"path":164,"stem":168,"icon":163},"7.algorithm\u002Findex",{"title":170,"path":171,"stem":172},"算法基础1：复杂度分析","\u002Falgorithm\u002Ftime-space-complexity","7.algorithm\u002F01.time-space-complexity",{"id":174,"title":175,"body":176,"description":1340,"extension":1341,"links":1342,"meta":1348,"navigation":1355,"path":135,"seo":1356,"stem":136,"__hash__":1358},"docs\u002F4.ios\u002F05.event-delivery.md","浅谈 iOS 事件的传递和响应过程",{"type":177,"value":178,"toc":1332},"minimark",[179,183,187,218,221,224,227,271,278,284,287,290,293,304,314,317,324,330,333,348,674,677,684,689,692,699,702,708,711,723,729,735,752,761,767,770,775,778,1286,1289,1328],[180,181,182],"p",{},"我把一次点击拆开记。传递和响应各讲一遍，图下面写我看到的东西。最后回到这几个问题。",[184,185,186],"h2",{"id":186},"问题",[188,189,190,194,197,200,203,206],"ol",{},[191,192,193],"li",{},"视图的事件怎么传递、怎么响应？",[191,195,196],{},"父视图关掉交互，子视图为什么也点不到？",[191,198,199],{},"怎么扩大按钮的点击范围？",[191,201,202],{},"怎么让父视图和子视图同时响应同一次触摸？",[191,204,205],{},"子视图关掉交互、父视图开着，点在子视图上，为什么父视图还能响应？",[191,207,208,209,213,214,217],{},"普通 ",[210,211,212],"code",{},"UIView"," 没有手势，点击会交到父视图。",[210,215,216],{},"UIControl"," 同样没有手势，父视图却收不到。为什么？",[180,219,220],{},"试验页面只有三层：窗口、父视图、子视图。每次只改一个开关。",[184,222,223],{"id":223},"分析",[180,225,226],{},"事件我分成三类。今天只拆第一类。",[228,229,230,243],"table",{},[231,232,233],"thead",{},[234,235,236,240],"tr",{},[237,238,239],"th",{},"类型",[237,241,242],{},"我遇到它的时候",[244,245,246,255,263],"tbody",{},[234,247,248,252],{},[249,250,251],"td",{},"触摸",[249,253,254],{},"手指点在屏幕上",[234,256,257,260],{},[249,258,259],{},"运动",[249,261,262],{},"摇一摇、重力感应",[234,264,265,268],{},[249,266,267],{},"远程",[249,269,270],{},"耳机上的按键",[180,272,273,274,277],{},"运动和远程不经过 ",[210,275,276],{},"hitTest","。它们从当前第一响应者进入响应链。",[180,279,280],{},[281,282,283],"strong",{},"触摸分两段。传递找出一个视图。响应再看它处理不处理。",[180,285,286],{},"传递结束时，手里只有一个命中视图。它接住了这个点，不等于它会处理这次触摸。",[184,288,289],{"id":289},"传递",[180,291,292],{},"传递在问：这个点落在哪个视图上。从窗口往子视图走，问到一个就停。",[180,294,295,296,299,300,303],{},"手指碰到屏幕，系统把事件交给当前 App。",[210,297,298],{},"UIApplication"," 收到之后交给窗口，窗口调用 ",[210,301,302],{},"hitTest(_:with:)","。",[305,306,312],"pre",{"className":307,"code":309,"language":310,"meta":311},[308],"language-text","手指\n  │\n  ▼\nUIApplication.sendEvent\n  │\n  ▼\nUIWindow.hitTest\n  │\n  ▼\n父视图.hitTest\n  │\n  ▼\n子视图.hitTest\n  │\n  ▼\n返回子视图 ──► 父视图原样向上交 ──► 窗口原样向上交\n","text","",[210,313,309],{"__ignoreMap":311},[180,315,316],{},"这张图是调用顺序，不是视图嵌套。箭头向下是在找人。最底下向右的箭头不再往下问，只把已经找到的子视图交回窗口。",[180,318,319,320,323],{},"同一层里，后加入的视图排在 ",[210,321,322],{},"subviews"," 后面，画在更上面，也先被问到。点在按钮上时，树是这样：",[305,325,328],{"className":326,"code":327,"language":310,"meta":311},[308],"UIWindow\n├─ 背景     先加入，画在下面，最后才问\n└─ 卡片     后加入，先被问\n   └─ 按钮  ● 点在这里\n",[210,329,327],{"__ignoreMap":311},[180,331,332],{},"背景画在卡片下面，所以窗口先问卡片。卡片再问按钮。按钮没有更深的子视图，点又在按钮里，按钮把自己返回。卡片和窗口都把这个结果原样交上去，背景不会被问到。",[180,334,335,336,339,340,343,344,347],{},"问子视图之前先过四关：没有隐藏，允许交互，",[210,337,338],{},"alpha"," 不小于 ",[210,341,342],{},"0.01","，点在自己的 bounds 里。有一关不过，当前视图返回 ",[210,345,346],{},"nil","，它下面的子视图都不会被问到。",[305,349,354],{"className":350,"code":351,"filename":352,"language":353,"meta":311,"style":311},"language-swift shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import UIKit\n\n\u002F\u002F 传递时我对照的过程。真正调用的是系统的 hitTest\nfunc illustratedHitTest(_ point: CGPoint, on view: UIView, event: UIEvent?) -> UIView? {\n    \u002F\u002F 不过这一关，子视图不会被问到\n    if view.isHidden || !view.isUserInteractionEnabled || view.alpha \u003C 0.01 {\n        return nil\n    }\n    if !view.point(inside: point, with: event) {\n        return nil\n    }\n    \u002F\u002F 数组末尾先问，也就是画在更上面的那个\n    for subview in view.subviews.reversed() {\n        let local = subview.convert(point, from: view)\n        if let hit = illustratedHitTest(local, on: subview, event: event) {\n            return hit\n        }\n    }\n    return view\n}\n","HitTestWalkthrough.swift","swift",[210,355,356,369,376,383,440,446,478,487,493,527,534,539,545,568,601,639,648,654,659,668],{"__ignoreMap":311},[357,358,361,365],"span",{"class":359,"line":360},"line",1,[357,362,364],{"class":363},"s7zQu","import",[357,366,368],{"class":367},"sBMFI"," UIKit\n",[357,370,372],{"class":359,"line":371},2,[357,373,375],{"emptyLinePlaceholder":374},true,"\n",[357,377,379],{"class":359,"line":378},3,[357,380,382],{"class":381},"sHwdD","\u002F\u002F 传递时我对照的过程。真正调用的是系统的 hitTest\n",[357,384,386,390,394,398,401,405,409,412,415,418,422,425,428,431,434,437],{"class":359,"line":385},4,[357,387,389],{"class":388},"spNyl","func",[357,391,393],{"class":392},"s2Zo4"," illustratedHitTest",[357,395,397],{"class":396},"sMK4o","(",[357,399,400],{"class":392},"_",[357,402,404],{"class":403},"sHdIc"," point",[357,406,408],{"class":407},"sTEyZ",": CGPoint, ",[357,410,411],{"class":392},"on",[357,413,414],{"class":403}," view",[357,416,417],{"class":407},": UIView, ",[357,419,421],{"class":420},"sjYur","event",[357,423,424],{"class":407},": UIEvent",[357,426,427],{"class":396},"?)",[357,429,430],{"class":396}," ->",[357,432,433],{"class":407}," UIView",[357,435,436],{"class":396},"?",[357,438,439],{"class":396}," {\n",[357,441,443],{"class":359,"line":442},5,[357,444,445],{"class":381},"    \u002F\u002F 不过这一关，子视图不会被问到\n",[357,447,449,452,455,458,461,464,466,469,472,476],{"class":359,"line":448},6,[357,450,451],{"class":363},"    if",[357,453,454],{"class":407}," view.isHidden ",[357,456,457],{"class":396},"||",[357,459,460],{"class":396}," !",[357,462,463],{"class":407},"view.isUserInteractionEnabled ",[357,465,457],{"class":396},[357,467,468],{"class":407}," view.alpha ",[357,470,471],{"class":396},"\u003C",[357,473,475],{"class":474},"sbssI"," 0.01",[357,477,439],{"class":396},[357,479,481,484],{"class":359,"line":480},7,[357,482,483],{"class":363},"        return",[357,485,486],{"class":396}," nil\n",[357,488,490],{"class":359,"line":489},8,[357,491,492],{"class":396},"    }\n",[357,494,496,498,500,503,505,508,511,514,517,519,522,525],{"class":359,"line":495},9,[357,497,451],{"class":363},[357,499,460],{"class":396},[357,501,502],{"class":407},"view.point",[357,504,397],{"class":396},[357,506,507],{"class":392},"inside",[357,509,510],{"class":396},":",[357,512,513],{"class":407}," point, ",[357,515,516],{"class":392},"with",[357,518,510],{"class":396},[357,520,521],{"class":407}," event",[357,523,524],{"class":396},")",[357,526,439],{"class":396},[357,528,530,532],{"class":359,"line":529},10,[357,531,483],{"class":363},[357,533,486],{"class":396},[357,535,537],{"class":359,"line":536},11,[357,538,492],{"class":396},[357,540,542],{"class":359,"line":541},12,[357,543,544],{"class":381},"    \u002F\u002F 数组末尾先问，也就是画在更上面的那个\n",[357,546,548,551,554,557,560,563,566],{"class":359,"line":547},13,[357,549,550],{"class":363},"    for",[357,552,553],{"class":407}," subview ",[357,555,556],{"class":363},"in",[357,558,559],{"class":407}," view.subviews.",[357,561,562],{"class":392},"reversed",[357,564,565],{"class":396},"()",[357,567,439],{"class":396},[357,569,571,574,577,580,583,586,588,591,594,596,598],{"class":359,"line":570},14,[357,572,573],{"class":474},"        let",[357,575,576],{"class":407}," local ",[357,578,579],{"class":396},"=",[357,581,582],{"class":407}," subview.",[357,584,585],{"class":392},"convert",[357,587,397],{"class":396},[357,589,590],{"class":407},"point, ",[357,592,593],{"class":392},"from",[357,595,510],{"class":396},[357,597,414],{"class":407},[357,599,600],{"class":396},")\n",[357,602,604,607,610,613,615,617,619,622,624,626,629,631,633,635,637],{"class":359,"line":603},15,[357,605,606],{"class":363},"        if",[357,608,609],{"class":474}," let",[357,611,612],{"class":407}," hit ",[357,614,579],{"class":396},[357,616,393],{"class":392},[357,618,397],{"class":396},[357,620,621],{"class":407},"local, ",[357,623,411],{"class":392},[357,625,510],{"class":396},[357,627,628],{"class":407}," subview, ",[357,630,421],{"class":392},[357,632,510],{"class":396},[357,634,521],{"class":407},[357,636,524],{"class":396},[357,638,439],{"class":396},[357,640,642,645],{"class":359,"line":641},16,[357,643,644],{"class":363},"            return",[357,646,647],{"class":407}," hit\n",[357,649,651],{"class":359,"line":650},17,[357,652,653],{"class":396},"        }\n",[357,655,657],{"class":359,"line":656},18,[357,658,492],{"class":396},[357,660,662,665],{"class":359,"line":661},19,[357,663,664],{"class":363},"    return",[357,666,667],{"class":407}," view\n",[357,669,671],{"class":359,"line":670},20,[357,672,673],{"class":396},"}\n",[180,675,676],{},"我点在子视图上，命中的是子视图。点在父视图空白处，子视图说点不在自己里面，父视图返回自己。",[180,678,679,680,683],{},"选出的视图记在这次 ",[210,681,682],{},"UITouch"," 上。手指接着移动、抬起，都还发给它，不会每动一次就重新找。",[180,685,686],{},[281,687,688],{},"一次传递只选出一个视图。",[184,690,691],{"id":691},"响应",[180,693,694,695,698],{},"响应在问：这个视图处理不处理。从命中视图沿 ",[210,696,697],{},"next"," 向上，直到有一层把触摸留下，或者链走完。",[180,700,701],{},"我这次的父视图就是控制器的根视图：",[305,703,706],{"className":704,"code":705,"language":310,"meta":311},[308],"子视图          touchesBegan 从这里开始\n  ↑\n父视图\n  ↑\n控制器          插在自己的根视图后面\n  ↑\n窗口\n  ↑\nUIWindowScene\n  ↑\nUIApplication\n",[210,707,705],{"__ignoreMap":311},[180,709,710],{},"箭头向上。子视图的下一位是父视图。控制器出现在自己的根视图后面。",[180,712,208,713,715,716,718,719,722],{},[210,714,212],{}," 没写触摸方法时，默认实现把消息交给 ",[210,717,697],{},"，所以上面父视图能收到。某一层写了方法又不调用 ",[210,720,721],{},"super","，链停在这一层。",[305,724,727],{"className":725,"code":726,"language":310,"meta":311},[308],"不调用 super                 调用 super\n\n子视图  打印 child           子视图  打印 child\n   ✕ 停在这里                    │\n                                 ▼\n                             父视图  打印 parent\n",[210,728,726],{"__ignoreMap":311},[180,730,731,732,734],{},"左边只有子视图的打印。右边子视图先处理自己的事，",[210,733,721],{}," 再把同一次触摸交给父视图，两行都会出现。",[180,736,737,738,741,742,741,745,741,748,751],{},"四个方法要一起这样转发：",[210,739,740],{},"touchesBegan","、",[210,743,744],{},"touchesMoved",[210,746,747],{},"touchesEnded",[210,749,750],{},"touchesCancelled","。我只转发了开始，抬起那一下父视图收不到。",[180,753,754,757,758,760],{},[210,755,756],{},"UIButton"," 是另一条结果。它是 ",[210,759,216],{},"，自己实现了这四个方法，用来跟踪手指、发送 action。",[305,762,765],{"className":763,"code":764,"language":310,"meta":311},[308],"普通 UIView                      UIButton\n没有写触摸方法                   自己实现了四个触摸方法\n默认交给 next                    不把 touchesBegan 交给父视图\n父视图能收到                     父视图没有打印\n手势列表可以是空的               手势列表也是空的\n",[210,766,764],{"__ignoreMap":311},[180,768,769],{},"两列的手势都可以是空的。父视图收不收得到，差在有没有把触摸方法留给自己。",[180,771,772],{},[281,773,774],{},"传递向下找人。响应向上找愿意处理的人。",[184,776,777],{"id":777},"解答",[779,780,781,794,826,993,1239,1263],"accordion",{},[782,783,784,785,787,788,790,791,793],"accordion-item",{"icon":48,"label":193},"传递走 ",[210,786,276],{},"，从窗口问到一个视图。响应从那个视图的 ",[210,789,740],{}," 开始，不处理就沿 ",[210,792,697],{}," 向上。",[782,795,796,806,812,821],{"icon":101,"label":196},[180,797,798,799,802,803,805],{},"父视图 ",[210,800,801],{},"isUserInteractionEnabled = false"," 时，",[210,804,276],{}," 在问子视图之前就返回。",[305,807,810],{"className":808,"code":809,"language":310,"meta":311},[308],"UIWindow.hitTest\n  │\n  ▼\n父视图.hitTest\n  │\n  ✕  交互关着\n  │\n  └── return nil\n      子视图不会被调用\n",[210,811,809],{"__ignoreMap":311},[180,813,814,815,817,818,820],{},"叉号在父视图自己身上。下面的子视图还没轮到，所以子视图上的点击也没了。隐藏，或者 ",[210,816,338],{}," 小于 ",[210,819,342],{},"，也是这一关。",[180,822,823],{},[281,824,825],{},"子视图点不到，是因为查找没有走进去。",[782,827,829,835,842,848,859,983,990],{"icon":828,"label":199},"i-lucide-expand",[180,830,831,832,834],{},"我先在 ",[210,833,276],{}," 里写：点落在按钮外侧 16pt，就返回按钮。外侧能点到，但这块地方里原来的兄弟视图也被抢走了。",[180,836,837,838,841],{},"后来只改 ",[210,839,840],{},"point(inside:with:)","。谁先被问，仍按原来的层级。变的只是「这个点算不算在按钮上」。",[305,843,846],{"className":844,"code":845,"language":310,"meta":311},[308],"┌─ 热区 ─────────┐\n│   ┌─ 图标 ─┐   │\n│   └────────┘   │\n└────────────────┘\n",[210,847,845],{"__ignoreMap":311},[180,849,850,851,854,855,858],{},"外框是手指能点中的范围，内框才是按钮的 ",[210,852,853],{},"frame","。图标大小没变，外圈靠 ",[210,856,857],{},"point(inside:)"," 算进按钮。",[305,860,863],{"className":350,"code":861,"filename":862,"language":353,"meta":311,"style":311},"import UIKit\n\nfinal class EasyTapButton: UIButton {\n    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {\n        \u002F\u002F 四周各扩 16pt，frame 不动\n        bounds.insetBy(dx: -16, dy: -16).contains(point)\n    }\n}\n","EasyTapButton.swift",[210,864,865,871,875,894,927,932,975,979],{"__ignoreMap":311},[357,866,867,869],{"class":359,"line":360},[357,868,364],{"class":363},[357,870,368],{"class":367},[357,872,873],{"class":359,"line":371},[357,874,375],{"emptyLinePlaceholder":374},[357,876,877,880,883,886,888,891],{"class":359,"line":378},[357,878,879],{"class":388},"final",[357,881,882],{"class":388}," class",[357,884,885],{"class":367}," EasyTapButton",[357,887,510],{"class":396},[357,889,890],{"class":367}," UIButton ",[357,892,893],{"class":396},"{\n",[357,895,896,899,902,904,906,908,910,912,914,916,918,920,922,925],{"class":359,"line":385},[357,897,898],{"class":388},"    override",[357,900,901],{"class":388}," func",[357,903,404],{"class":392},[357,905,397],{"class":396},[357,907,507],{"class":392},[357,909,404],{"class":403},[357,911,408],{"class":407},[357,913,516],{"class":392},[357,915,521],{"class":403},[357,917,424],{"class":407},[357,919,427],{"class":396},[357,921,430],{"class":396},[357,923,924],{"class":367}," Bool",[357,926,439],{"class":396},[357,928,929],{"class":359,"line":442},[357,930,931],{"class":381},"        \u002F\u002F 四周各扩 16pt，frame 不动\n",[357,933,934,937,940,942,945,947,950,953,956,958,960,962,965,968,970,973],{"class":359,"line":448},[357,935,936],{"class":407},"        bounds.",[357,938,939],{"class":392},"insetBy",[357,941,397],{"class":396},[357,943,944],{"class":392},"dx",[357,946,510],{"class":396},[357,948,949],{"class":474}," -16",[357,951,952],{"class":407},", ",[357,954,955],{"class":392},"dy",[357,957,510],{"class":396},[357,959,949],{"class":474},[357,961,524],{"class":396},[357,963,964],{"class":407},".",[357,966,967],{"class":392},"contains",[357,969,397],{"class":396},[357,971,972],{"class":407},"point",[357,974,600],{"class":396},[357,976,977],{"class":359,"line":480},[357,978,492],{"class":396},[357,980,981],{"class":359,"line":489},[357,982,673],{"class":396},[180,984,985],{},[281,986,987,988,303],{},"扩大点击范围，改 ",[210,989,840],{},[180,991,992],{},"按钮贴着父视图边缘时，扩出去的 16pt 落在父视图 bounds 外。父视图那一关先失败，按钮不会被问到。那一圈还是点不到。",[782,994,996,1004,1015,1217,1232],{"icon":91,"label":995},"怎么让父视图和子视图同时响应？",[180,997,998,1000,1001,1003],{},[210,999,276],{}," 只能返回一个视图。我把处理写进 ",[210,1002,276],{},"，手指一移，这个方法被调用好几次，处理也跑了好几次。",[180,1005,1006,1007,1009,1010,1012,1013,303],{},"子视图的逻辑写在 ",[210,1008,740],{}," 里，再调用 ",[210,1011,721],{},"。默认实现会交给 ",[210,1014,697],{},[305,1016,1019],{"className":350,"code":1017,"filename":1018,"language":353,"meta":311,"style":311},"import UIKit\n\nfinal class ParentView: UIView {\n    override func touchesBegan(_ touches: Set\u003CUITouch>, with event: UIEvent?) {\n        print(\"parent\")\n    }\n}\n\nfinal class ChildView: UIView {\n    override func touchesBegan(_ touches: Set\u003CUITouch>, with event: UIEvent?) {\n        print(\"child\")\n        \u002F\u002F 去掉这行，控制台只剩 child\n        super.touchesBegan(touches, with: event)\n    }\n}\n","ParentAndChildTouch.swift",[210,1020,1021,1027,1031,1047,1088,1106,1110,1114,1118,1133,1169,1184,1189,1209,1213],{"__ignoreMap":311},[357,1022,1023,1025],{"class":359,"line":360},[357,1024,364],{"class":363},[357,1026,368],{"class":367},[357,1028,1029],{"class":359,"line":371},[357,1030,375],{"emptyLinePlaceholder":374},[357,1032,1033,1035,1037,1040,1042,1045],{"class":359,"line":378},[357,1034,879],{"class":388},[357,1036,882],{"class":388},[357,1038,1039],{"class":367}," ParentView",[357,1041,510],{"class":396},[357,1043,1044],{"class":367}," UIView ",[357,1046,893],{"class":396},[357,1048,1049,1051,1053,1056,1058,1060,1063,1066,1069,1071,1073,1076,1078,1080,1082,1084,1086],{"class":359,"line":385},[357,1050,898],{"class":388},[357,1052,901],{"class":388},[357,1054,1055],{"class":392}," touchesBegan",[357,1057,397],{"class":396},[357,1059,400],{"class":392},[357,1061,1062],{"class":403}," touches",[357,1064,1065],{"class":407},": ",[357,1067,1068],{"class":367},"Set",[357,1070,471],{"class":396},[357,1072,682],{"class":407},[357,1074,1075],{"class":396},">",[357,1077,952],{"class":407},[357,1079,516],{"class":392},[357,1081,521],{"class":403},[357,1083,424],{"class":407},[357,1085,427],{"class":396},[357,1087,439],{"class":396},[357,1089,1090,1093,1095,1098,1102,1104],{"class":359,"line":442},[357,1091,1092],{"class":392},"        print",[357,1094,397],{"class":396},[357,1096,1097],{"class":396},"\"",[357,1099,1101],{"class":1100},"sfazB","parent",[357,1103,1097],{"class":396},[357,1105,600],{"class":396},[357,1107,1108],{"class":359,"line":448},[357,1109,492],{"class":396},[357,1111,1112],{"class":359,"line":480},[357,1113,673],{"class":396},[357,1115,1116],{"class":359,"line":489},[357,1117,375],{"emptyLinePlaceholder":374},[357,1119,1120,1122,1124,1127,1129,1131],{"class":359,"line":495},[357,1121,879],{"class":388},[357,1123,882],{"class":388},[357,1125,1126],{"class":367}," ChildView",[357,1128,510],{"class":396},[357,1130,1044],{"class":367},[357,1132,893],{"class":396},[357,1134,1135,1137,1139,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159,1161,1163,1165,1167],{"class":359,"line":529},[357,1136,898],{"class":388},[357,1138,901],{"class":388},[357,1140,1055],{"class":392},[357,1142,397],{"class":396},[357,1144,400],{"class":392},[357,1146,1062],{"class":403},[357,1148,1065],{"class":407},[357,1150,1068],{"class":367},[357,1152,471],{"class":396},[357,1154,682],{"class":407},[357,1156,1075],{"class":396},[357,1158,952],{"class":407},[357,1160,516],{"class":392},[357,1162,521],{"class":403},[357,1164,424],{"class":407},[357,1166,427],{"class":396},[357,1168,439],{"class":396},[357,1170,1171,1173,1175,1177,1180,1182],{"class":359,"line":536},[357,1172,1092],{"class":392},[357,1174,397],{"class":396},[357,1176,1097],{"class":396},[357,1178,1179],{"class":1100},"child",[357,1181,1097],{"class":396},[357,1183,600],{"class":396},[357,1185,1186],{"class":359,"line":541},[357,1187,1188],{"class":381},"        \u002F\u002F 去掉这行，控制台只剩 child\n",[357,1190,1191,1194,1196,1198,1201,1203,1205,1207],{"class":359,"line":547},[357,1192,1193],{"class":407},"        super.",[357,1195,740],{"class":392},[357,1197,397],{"class":396},[357,1199,1200],{"class":407},"touches, ",[357,1202,516],{"class":392},[357,1204,510],{"class":396},[357,1206,521],{"class":407},[357,1208,600],{"class":396},[357,1210,1211],{"class":359,"line":570},[357,1212,492],{"class":396},[357,1214,1215],{"class":359,"line":603},[357,1216,673],{"class":396},[180,1218,1219,1220,1222,1223,1225,1226,1228,1229,1231],{},"调用 ",[210,1221,721],{},"，控制台是 ",[210,1224,1179],{}," 然后 ",[210,1227,1101],{},"。不调用，只有 ",[210,1230,1179],{},"。另外三个触摸方法我也同样转发了。",[180,1233,1234],{},[281,1235,1236,1237,303],{},"父子要一起收到，在子视图里调用 ",[210,1238,721],{},[782,1240,1243,1249,1255,1258],{"icon":1241,"label":1242},"i-lucide-undo-2","子视图关掉交互，为什么父视图还能响应？",[180,1244,1245,1246,1248],{},"子视图返回 ",[210,1247,346],{},"。父视图继续问别的子视图，没有人接住，就返回自己。这个点还在父视图 bounds 里。",[305,1250,1253],{"className":1251,"code":1252,"language":310,"meta":311},[308],"父视图.hitTest\n  │\n  ├─ 子视图.hitTest\n  │     ✕  交互关着\n  │     └── return nil\n  │\n  └── 没有别的子视图接住\n      返回父视图自己\n",[210,1254,1252],{"__ignoreMap":311},[180,1256,1257],{},"叉号只在子视图上。父视图这一关是通过的，所以它把自己交出去。父视图 bounds 外面的视图不会因此被点到。",[180,1259,1260],{},[281,1261,1262],{},"点击退回父视图，不会穿到父视图外面。",[782,1264,1266,1278],{"icon":137,"label":1265},"没有手势时，为什么按钮不交给父视图？",[180,1267,208,1268,1270,1271,1273,1274,1277],{},[210,1269,212],{}," 不写触摸方法，点上去，父视图能收到。换成 ",[210,1272,756],{},"，父视图没有打印。按钮上没有手势，",[210,1275,1276],{},"gestureRecognizers"," 打出来也是空的。",[180,1279,1280,1285],{},[281,1281,1282,1284],{},[210,1283,756],{}," 留下这次触摸，是因为它实现了触摸方法。"," 手势列表空不空，对不上这个差别。",[184,1287,1288],{"id":1288},"参考资料",[1290,1291,1292,1301,1312,1320],"ul",{},[191,1293,1294,1300],{},[1295,1296,302],"a",{"href":1297,"rel":1298},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fuikit\u002Fuiview\u002Fhittest(_:with:)",[1299],"nofollow","：哪个视图接住这个点",[191,1302,1303,1308,1309,1311],{},[1295,1304,1307],{"href":1305,"rel":1306},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fuikit\u002Fusing-responders-and-the-responder-chain-to-handle-events",[1299],"Using responders and the responder chain to handle events","：命中之后沿 ",[210,1310,697],{}," 怎么走",[191,1313,1314,1319],{},[1295,1315,1318],{"href":1316,"rel":1317},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fuikit\u002Fuiresponder\u002Ftouchesbegan(_:with:)",[1299],"touchesBegan(_:with:)","：四个触摸方法里的开始",[191,1321,1322,1327],{},[1295,1323,1326],{"href":1324,"rel":1325},"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fuikit\u002Fuigesturerecognizer",[1299],"UIGestureRecognizer","：手势和视图收到触摸的先后",[1329,1330,1331],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sjYur, html code.shiki .sjYur{--shiki-light:#6182B8;--shiki-light-font-style:italic;--shiki-default:#82AAFF;--shiki-default-font-style:italic;--shiki-dark:#82AAFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":311,"searchDepth":371,"depth":371,"links":1333},[1334,1335,1336,1337,1338,1339],{"id":186,"depth":371,"text":186},{"id":223,"depth":371,"text":223},{"id":289,"depth":371,"text":289},{"id":691,"depth":371,"text":691},{"id":777,"depth":371,"text":777},{"id":1288,"depth":371,"text":1288},"传递和响应分开记。传递用图看 hitTest 怎么从窗口找到一个视图，响应用图看消息怎么沿 next 向上。","md",[1343,1346],{"label":276,"icon":1344,"to":1297,"target":1345},"i-lucide-book-open","_blank",{"label":1347,"icon":1344,"to":1305,"target":1345},"响应链",{"date":1349,"categories":1350,"tags":1352},"2026-09-30",[1351],"ios",[108,1353,1354,1347],"UIKit","事件",{"title":134,"icon":137},{"title":175,"description":1357},"触摸怎么从窗口找到一个视图，又怎么沿响应链向上。父视图关掉交互、扩大按钮、父子一起收到、UIView 和 UIButton 的差别，按试验来答。","_yK5vlXmOWW_xZ7qmTt85NB7MTkCby_afwJsvRSWcRw",[1360,1362],{"title":130,"path":131,"stem":132,"description":1361,"icon":91},"SwiftUI 按所有权从 @State 排到 environment。UIKit 改完模型，再在一处写进控件。页面变大之后用 MVVM 或单向数据流。Combine、RxSwift、ReactorKit、TCA 负责把变化送出去。",{"title":139,"path":140,"stem":141,"description":1363,"icon":144},"Flutter 开发实践与笔记。",1790767047591]