Sobes.tech
Middle

Jak system określa, w którym widoku nastąpiło dotknięcie?

sobes.tech AI

Odpowiedź od AI

System używa dwóch kluczowych mechanizmów: testu trafienia (hit-testing) i hierarchii widoków.

  1. Hit-testing: Framework UIKit używa metody hitTest(_:with:) głównego widoku okna (UIWindow), aby określić, który widok znajduje się pod punktem dotyku. Ta metoda rekurencyjnie przechodzi przez hierarchię widoków, zaczynając od głównego.

  2. Hierarchia widoków:

    • Metoda hitTest(_:with:) najpierw sprawdza, czy punkt dotyku znajduje się w granicach bieżącego widoku. Jeśli nie, zwraca nil.
    • Jeśli tak, metoda przegląda widoki potomne w odwrotnej kolejności (od ostatnio dodanego do pierwszego). Dla każdego widoku potomnego wywołuje własną metodę hitTest(_:with:) z tym samym punktem dotyku (przekształconym na współrzędne widoku potomnego).
    • Pierwszy widok potomny, którego hitTest(_:with:) zwraca wynik nie-nil (czyli, on lub któryś z jego potomków zawiera punkt), jest uważany za "trafiony" i ten wynik jest zwracany do góry w hierarchii.
    • Jeśli żaden widok potomny nie zawiera punktu, a bieżący widok go zawiera i nie ma ustawionego userInteractionEnabled = false, system uznaje, że dotknięcie miało miejsce w bieżącym widoku, i metoda zwraca self.
    • Jeśli userInteractionEnabled bieżącego widoku jest false, lub hidden jest true, lub alpha jest mniejsze niż 0.01, metoda zwraca nil, nawet jeśli punkt jest w granicach.

Proces kontynuuje się w dół hierarchii, aż znajdzie się najniższy widok zawierający punkt dotyku. Ten widok staje się hit-test view lub hit-test result, i to jemu (lub jego najbliższemu odpowiedniemu przodkowi) zostanie delegowana dalsza obsługa zdarzenia (touchesBegan, touchesMoved itd.).

Oto jak nadpisać hitTest lub point(inside:with:) dla niestandardowego zachowania:

import UIKit

class CustomView: UIView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Sprawdzamy, czy punkt znajduje się wewnątrz naszej widoku.
        if !self.point(inside: point, with: event) {
            return nil
        }

        // Przechodzimy przez widoki potomne w odwrotnej kolejności.
        for subview in subviews.reversed() {
            // Konwertujemy punkt do współrzędnych widoku potomnego.
            let convertedPoint = subview.convert(point, from: self)

            // Rekurencyjnie wywołujemy hitTest na widoku potomnym.
            if let hitTestView = subview.hitTest(convertedPoint, with: event) {
                // Jeśli widok potomny lub jego potomkowie zawierają punkt, zwracamy ten wynik.
                return hitTestView
            }
        }

        // Jeśli żaden widok potomny nie obsłużył dotknięcia,
        // a ten widok jest interaktywny i zawiera punkt, zwracamy self.
        return self
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Podstawowa implementacja sprawdza, czy punkt jest w granicach i czy widok jest interaktywny.
        // return super.point(inside: point, with: event)

        // Można nadpisać dla niestandardowych kształtów lub marginesów, np. powiększyć obszar dotyku.
        let extendedBounds = bounds.insetBy(dx: -20, dy: -20) // Powiększamy obszar o 20pts z każdej strony
        return extendedBounds.contains(point)
    }
}

Metoda point(inside:with:) jest wywoływana z hitTest(_:with:) w celu szybkiego sprawdzenia, czy punkt znajduje się w obszarze widoku, zanim zostaną sprawdzone widoki potomne.

Ostatecznie znajduje się najniższy widok w hierarchii, który zawiera punkt dotyku i ma ustawione userInteractionEnabled na true, hidden na false i alpha większe niż 0.01.