Skip to content

Instantly share code, notes, and snippets.

@steipete
Last active May 19, 2021 00:11
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steipete/01e9544b1d86d7fc5cba9f85bd72bce0 to your computer and use it in GitHub Desktop.
Save steipete/01e9544b1d86d7fc5cba9f85bd72bce0 to your computer and use it in GitHub Desktop.
How to build Tap-And-Fade using SwiftUI instead of a hard deselect. The default in SwiftUI is too hard and doesn't feel right.
// Helper to hold the parent VC weakly
class WeakViewControllerHolder: ObservableObject {
weak var vc: UIViewController?
init(_ vc: UIViewController) {
self.vc = vc
}
}
@available(iOS 13.0, *)
struct FadableButton: View {
@EnvironmentObject var sender: WeakViewControllerHolder
let title: String
let action: () -> Void
init(_ title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
}
var body: some View {
Button(title) {
sender.vc?.deselectSelectedRow()
action()
}
}
}
// The settings view where we want the nice fade
struct SettingsView: View {
@EnvironmentObject var sender: WeakViewControllerHolder
// later in your List/Form. Could also be wrapped!
FadableButton("Some Action") {
// do your action
}
}
// Settings is embedded in a regular view controller and created like this:
func makeSettingsView() -> UIHostingController<AnyView> {
UIHostingController(rootView: SettingsView().environmentObject(WeakViewControllerHolder(self)).eraseToAnyView())
}
extension UIViewController {
/// Helper to deselect the selected cell row.
func deselectSelectedRow(animated: Bool = true) {
// This helper is used for UITableViewController subclasses but also generic UIViewController or SwiftUI holding ones, so we search.
guard let tableView = view.closestSubview(of: UITableView.self),
let selectedIndex = tableView.indexPathForSelectedRow else { return }
tableView.deselectRow(at: selectedIndex, animated: animated)
}
}
@steipete
Copy link
Author

steipete commented Jan 24, 2021

Questions:

  • Is my approach to wrapping Button legit? Is there an easier/better way?
  • Any other chance to get the responder chain in a SwiftUI action to crawl up without having to pass WeakViewControllerHolder as environment object? The current approach also has the downside that it only works when wrapped in a view controller.
  • Anything else I can improve?

Tweet/Video: https://twitter.com/steipete/status/1353296114808737792?s=21

@LeoNatan
Copy link

Apple seems to like parent-child controller hierarchies. In my SwiftUI wrapper, I used a UIHostingController to spy on child controllers that are added to the hosting controller. You can use that to discover the parent table view controller.

https://github.com/LeoNatan/LNPopupUI/blob/1232830c00bd63c98763c3070c7367bd44bcb9b3/Sources/LNPopupUI/Private/LNPopupProxyViewController.swift#L206

@LeoNatan
Copy link

Since you have other issues with the table view, it’s probably a good idea to wrap that one, take over it’s delegate with a proxy object (or ISA swizzle the original 🤪) and enhance the returned values and actions.

@steipete
Copy link
Author

Folks commenting that making a Button subclass is not SwiftUI-y: I tried Configuration and ViewModifiers with gesture recognizers, but it's all cursed, fires too late and/or delays the action call on button. Hours wasted: +=2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment