<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jayden Kan</title>
    <description>The latest articles on DEV Community by Jayden Kan (@jaydenkan090808).</description>
    <link>https://dev.to/jaydenkan090808</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4197312%2Fc5fd4050-f01a-4bee-a88b-28fc67129945.jpg</url>
      <title>DEV Community: Jayden Kan</title>
      <link>https://dev.to/jaydenkan090808</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jaydenkan090808"/>
    <language>en</language>
    <item>
      <title>Mac NSCollectionView 刪除 Cell 後，新的 Cell 按鈕短暫無法點擊</title>
      <dc:creator>Jayden Kan</dc:creator>
      <pubDate>Sun, 15 Nov 2026 08:05:05 +0000</pubDate>
      <link>https://dev.to/jaydenkan090808/mac-nscollectionview-shan-chu-cell-hou-xin-de-cell-an-niu-duan-zan-wu-fa-dian-ji-3nla</link>
      <guid>https://dev.to/jaydenkan090808/mac-nscollectionview-shan-chu-cell-hou-xin-de-cell-an-niu-duan-zan-wu-fa-dian-ji-3nla</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dr107y4l7f813mnnuul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0dr107y4l7f813mnnuul.png" alt="Image" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NSCollectionView 有 4 種排版：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;排版 1：1 個 Cell（1）&lt;/li&gt;
&lt;li&gt;排版 2：2 個 Cell（2）&lt;/li&gt;
&lt;li&gt;排版 3：3 個 Cell（1 + 2）&lt;/li&gt;
&lt;li&gt;排版 4：5 個 Cell（2 + 3）&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  問題描述
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;畫面目前有 5 個 Cell，使用排版 4（2 + 3）&lt;/li&gt;
&lt;li&gt;使用者點擊第一排右側的 Close Button&lt;/li&gt;
&lt;li&gt;Cell 被刪除，排版依剩餘 Cell 數量重新切換&lt;/li&gt;
&lt;li&gt;使用者立即點擊原本的位置&lt;/li&gt;
&lt;li&gt;該位置明明有 Close Button，卻沒有觸發刪除&lt;/li&gt;
&lt;li&gt;停頓一小段時間後，又可以正常點擊&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  初步排查
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;deleteItems(at:)&lt;/code&gt; 刪除 Cell，沒有呼叫 &lt;code&gt;reloadData&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;只有排版改變時，才會發生點擊後沒有刪除的情況&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;deleteItems(at:)&lt;/code&gt; 改成每次都 &lt;code&gt;reloadData&lt;/code&gt;，反而每次刪除後都會發生&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prepareForReuse&lt;/code&gt; 沒有遺漏重置資料&lt;/li&gt;
&lt;li&gt;刪除 Cell 時，main thread 上沒有耗時的工作&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  增加 Log 深入了解實際情況
&lt;/h2&gt;

&lt;p&gt;多次嘗試沒有結果後，改從滑鼠事件本身調查。我請 AI 加入更完整的 Debug Log，觀察：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mouseDown&lt;/code&gt; 事件實際由哪個 Button 接收&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mouseUp&lt;/code&gt; 事件的流向&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NSEvent.clickCount&lt;/code&gt; 的變化&lt;/li&gt;
&lt;li&gt;滑鼠目前位置透過 Hit Test 找到的 View&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;把過濾後的 Log 貼給 AI，能快速分析出情況，節省時間。&lt;/p&gt;

&lt;h2&gt;
  
  
  關鍵發現
&lt;/h2&gt;

&lt;p&gt;快速連續點擊時，Log 出現了一個很特別的現象：&lt;code&gt;clickCount&lt;/code&gt; 持續增加，但接收 &lt;code&gt;mouseDown&lt;/code&gt; 的 Button 始終是同一個實例。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Click 1
clickCount = 1
mouseDown self = 0xbb545ed00   (Button A)
hitTest = Button A

Click 2
clickCount = 2
mouseDown self = 0xbb545ed00   (Button A)
hitTest = Button B

Click 3
clickCount = 3
mouseDown self = 0xbb545ed00   (Button A)
hitTest = Button B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;這代表：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;第一下點擊時，接收事件的是 Button A。&lt;/li&gt;
&lt;li&gt;Cell 刪除並重新排版後，滑鼠下方已經變成 Button B（Hit Test 的結果）。&lt;/li&gt;
&lt;li&gt;但後續點擊的 &lt;code&gt;mouseDown&lt;/code&gt;，仍然由 Button A 接收。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;畫面上滑鼠下方的 Button，和實際收到滑鼠事件的 Button，並不是同一個。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TODO（發文前請確認）：目前只觀察到現象，尚未確認 AppKit 為何在連續點擊時沿用舊的接收者，也沒驗證「停頓後恢復」是否與系統雙擊間隔有關。若有實測或文件佐證，請補在這裡；沒有的話，這段維持「觀察到的現象」即可。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  解決方式：將事件交給正確的 Button
&lt;/h2&gt;

&lt;p&gt;在 Button 的 &lt;code&gt;mouseDown&lt;/code&gt; 中判斷事件是否真的落在自己身上：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;覆寫 &lt;code&gt;mouseDown&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;將滑鼠座標轉換到目前 Button 的座標系&lt;/li&gt;
&lt;li&gt;檢查滑鼠位置是否位於自己的 &lt;code&gt;bounds&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;如果在 &lt;code&gt;bounds&lt;/code&gt; 內，照常處理&lt;/li&gt;
&lt;li&gt;如果不在，重新做 Hit Test，找到游標下方真正的 Button，並把事件交給它處理&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;範例&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CloseButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NSButton&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;override&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;mouseDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NSEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 事件座標（window 座標）轉換成自己的座標系&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;localPoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locationInWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localPoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mouseDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// 事件被送到不在游標下方的舊 Button：重新 Hit Test。&lt;/span&gt;
        &lt;span class="c1"&gt;// hitTest(_:) 的參數必須是「接收者的 superview」座標系，&lt;/span&gt;
        &lt;span class="c1"&gt;// 所以從 contentView 的 superview 開始找。&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contentView&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;superview&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mouseDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locationInWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hitTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;NSButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// NSButton 的 mouseDown 會自行追蹤到 mouseUp，&lt;/span&gt;
            &lt;span class="c1"&gt;// 所以只轉發 mouseDown 即可。&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mouseDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mouseDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;注意事項：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;目標可能是 Button 內部的子 View，必要時沿著 &lt;code&gt;superview&lt;/code&gt; 往上找到 Button。&lt;/li&gt;
&lt;li&gt;這個作法依賴 NSButton 自己會追蹤到 &lt;code&gt;mouseUp&lt;/code&gt;。如果是自訂 View 並在 &lt;code&gt;mouseUp&lt;/code&gt; 處理點擊，&lt;code&gt;mouseUp&lt;/code&gt; 仍會回到原本的 View，需要另外處理。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mac 所有 NSCollectionView 都需要覆寫 mouseDown 嗎？
&lt;/h2&gt;

&lt;p&gt;不需要。這個問題只會在「快速連續點擊」加上「點擊後版面隨即重排，原位置換成另一個 Button」時出現。一般的刪除和點擊流程不受影響。&lt;/p&gt;

&lt;p&gt;也不建議用 &lt;code&gt;reloadData&lt;/code&gt; 來解決：排查時改用 &lt;code&gt;reloadData&lt;/code&gt; 後，問題反而每次刪除都會發生。如果專案中有類似「點擊後版面立即變動，使用者又快速再點」的互動，再考慮加上這個覆寫。&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>ios</category>
      <category>softwaredevelopment</category>
      <category>swift</category>
    </item>
  </channel>
</rss>
