Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active July 22, 2024 04:23
Show Gist options
  • Save IanKeen/abb807d72c5843fb0f4c3ab10804b9f7 to your computer and use it in GitHub Desktop.
Save IanKeen/abb807d72c5843fb0f4c3ab10804b9f7 to your computer and use it in GitHub Desktop.
SwiftUI: discover underlying components to fill in gaps in SwiftUI api
struct Test: View {
var body: some View {
List {
ForEach(1...5, id: \.self) { value in
Text("\(value)")
.listRowBackground(Color.blue)
}
}
.discover { (scrollView: UIScrollView) in
scrollView.backgroundColor = .red
}
}
}
import SwiftUI
extension View {
public func discover<T: UIView>(
where predicate: @escaping (T) -> Bool = { _ in true },
_ closure: @escaping (T) -> Void
) -> some View {
overlay(
DiscoveryView(predicate: predicate, setup: closure)
.frame(width: 0, height: 0)
)
}
}
extension View {
public func discover<T: UIViewController>(
where predicate: @escaping (T) -> Bool = { _ in true },
_ closure: @escaping (T) -> Void
) -> some View {
var match: T?
return discover(
where: { (view: UIView) in
guard let controller = view.findViewController() else { return false }
let responderChain = sequence(first: controller, next: \.next)
for item in responderChain {
if let controller = item as? T, predicate(controller) {
match = controller
return true
}
}
return false
},
{ _ in
guard let controller = match else {
return print("⚠️ Unable to find a view controller of type '\(T.self)'")
}
closure(controller)
}
)
}
}
private extension UIView {
func findViewController() -> UIViewController? {
if let next = next as? UIViewController {
return next
} else if let next = next as? UIView {
return next.findViewController()
} else {
return nil
}
}
}
private struct DiscoveryView<T: UIView>: UIViewRepresentable {
class UIViewType: UIView {
private var predicate: (T) -> Bool
private var setup: (T) -> Void
required init(
predicate: @escaping (T) -> Bool,
setup: @escaping (T) -> Void
) {
self.predicate = predicate
self.setup = setup
super.init(frame: .zero)
isUserInteractionEnabled = false
backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
super.didMoveToWindow()
guard superview != nil, window != nil, tag == 0 else {
return
}
tag = 1
guard
let host = findViewHost(from: self),
let discovered = findClosestView(T.self, host: host, where: predicate)
else {
return print("⚠️ Unable to find a view of type '\(T.self)'")
}
setup(discovered)
}
}
var predicate: (T) -> Bool
var setup: (T) -> Void
func makeUIView(context: Context) -> UIViewType {
.init(predicate: predicate, setup: setup)
}
func updateUIView(_ uiView: UIViewType, context: Context) { }
}
private func findViewHost(from entry: UIView) -> UIView? {
var superview = entry.superview
while let s = superview {
if NSStringFromClass(type(of: s)).contains("ViewHost") {
return s
}
superview = s.superview
}
return nil
}
private func findClosestView<T: UIView>(_: T.Type = T.self, host: UIView, where predicate: @escaping (T) -> Bool) -> T? {
// find the view hosts index in it's superview
// search from that index back to 0
// - look down the hierarchy at each item for T
guard
let superview = host.superview,
let index = superview.subviews.firstIndex(of: host)
else { return nil }
let branches = superview.subviews[0...index].reversed()
return branches.lazy.compactMap({ $0.firstView(T.self, where: predicate) }).first
}
private extension UIView {
func firstView<T: UIView>(_: T.Type = T.self, where predicate: @escaping (T) -> Bool = { _ in true }) -> T? {
if let result = self as? T, predicate(result) {
return result
}
return subviews.lazy
.compactMap { $0.firstView(where: predicate) }
.first
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment