Exposing custom custom UIKit events from custom UIKit controls to SwiftUI - event-handling

I have a custom UIKit control that has a custom event. Does anybody know how to expose this event to SwiftUI using the Combine framework?
I can render the control using Combine's UIViewRepresentable but cannot find a way to expose the custom events or the values changed by internal UIKit events to SwiftUI.
Here is a concrete example of this problem:
I am using this custom UIKit slider control (because it support multiple knobs/ values):
https://github.com/yonat/MultiSlider
It has a sliderChanged event which updates the current value.
How do I expose this value to SwiftUI using Combine?
I cannot simply pass in a #ObservedObject because UIViewRepresentable does not allow this. I also cannot update a #Binding variable from the event handler because the UIKit event handlers are marked with #objc and it does not like it.
It appears to be a very common use case. I expected Apple to have a standard solution for this. However, I simply cannot find one that actually works for these types of scenarios. What am I missing here?

I think, that this can be solution for your problem:
final class MultiSliderSwiftUI: UIViewRepresentable {
private let valueChanged: ([CGFloat]) -> Void
init(valueChanged: #escaping ([CGFloat]) -> Void) {
self.valueChanged = valueChanged
}
func makeUIView(context: UIViewRepresentableContext<MultiSliderSwiftUI>) -> MultiSlider {
let slider = MultiSlider()
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
return slider
}
func updateUIView(_ uiView: MultiSlider, context: UIViewRepresentableContext<MultiSliderSwiftUI>) {
}
#objc func sliderChanged(_ slider: MultiSlider) {
valueChanged(slider.value)
}
}
If you don't like create final class for SwiftUI views, you can do it in this way:
struct MultiSliderSwiftUI: UIViewRepresentable {
private let events: MultiSliderEvents
init(valueChanged: #escaping ([CGFloat]) -> Void) {
events = MultiSliderEvents(valueChanged: valueChanged)
}
func makeUIView(context: UIViewRepresentableContext<MultiSliderSwiftUI>) -> MultiSlider {
let slider = MultiSlider()
events.addEvents(for: slider)
return slider
}
func updateUIView(_ uiView: MultiSlider, context: UIViewRepresentableContext<MultiSliderSwiftUI>) {
}
}
class MultiSliderEvents {
private let valueChanged: ([CGFloat]) -> Void
init(valueChanged: #escaping ([CGFloat]) -> Void) {
self.valueChanged = valueChanged
}
func addEvents(for slider: MultiSlider) {
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
}
#objc func sliderChanged(_ slider: MultiSlider) {
valueChanged(slider.value)
}
}
In both ways, you can use it MultiSliderSwiftUI like, for example, Button:
MultiSliderSwiftUI { value in
print("\(value)")
}

Please try the MultiValueSlider from the swiftui branch and tell me if it works for you.
Example usage: MultiSliderDemo.
(I'm the package author)

Related

Swift - How can cardParams be obtained from STPPaymentOptionsViewController

I want to complete the addition of new payment method that I implemented in this SO question. This save and reuse stripe tutorial shows that cardParams and billingDetails need to be passed to STPPaymentMethodParams. How to I get the cardParams and billingDetails from STPPaymentOptionsViewController?
This is the code that produce the STPPaymentOptionsViewController and then display the STPAddCardViewController when add card button is pressed.
import Foundation
import SwiftUI
import Stripe
struct PaymentOptionsView: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, STPPaymentOptionsViewControllerDelegate {
var control: PaymentOptionsView
init(_ control: PaymentOptionsView) {
self.control = control
}
// Implement required delegate methods here:
func paymentOptionsViewControllerDidCancel(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewControllerDidFinish(_ paymentOptionsViewController: STPPaymentOptionsViewController) {
}
func paymentOptionsViewController(_ paymentOptionsViewController: STPPaymentOptionsViewController, didFailToLoadWithError error: Error) {
}
}
func makeUIViewController(context: UIViewControllerRepresentableContext<PaymentOptionsView>) -> STPPaymentOptionsViewController {
let config = STPPaymentConfiguration()
// config.requiredBillingAddressFields = .none
config.appleMerchantIdentifier = "dummy-merchant-id"
return STPPaymentOptionsViewController(configuration: config, theme: STPTheme(), apiAdapter: STPCustomerContext(keyProvider: MyAPIClient()), delegate: context.coordinator)
}
func updateUIViewController(_ uiViewController: STPPaymentOptionsViewController, context: UIViewControllerRepresentableContext<PaymentOptionsView>) { }
}
STPPaymentOptionsViewController by design only returns you a PaymentMethod and not the raw card details.
Since the STPPaymentOptionsViewControllerDelegate returns you a STPPaymentOption, you can cast it to a STPPaymentMethod and then get the billingDetails for that PaymentMethod.
In your didSelect() delegate method, you can cast it like:
if let paymentMethod = paymentOption as? STPPaymentMethod {
print(paymentMethod.stripeId)
}
https://stripe.dev/stripe-ios/docs/Classes/STPPaymentMethod.html#/c:#M#Stripe#objc(cs)STPPaymentMethod(py)billingDetails

How do you call a method on a UIView from outside the UIViewRepresentable in SwiftUI?

I want to be able to pass a reference to a method on the UIViewRespresentable (or perhaps it’s Coordinator) to a parent View. The only way I can think to do this is by creating a field on the parent View struct with a class that I then pass to the child, which acts as a delegate for this behaviour. But it seems pretty verbose.
The use case here is to be a able to call a method from a standard SwiftUI Button that will zoom the the current location in a MKMapView that’s buried in a UIViewRepresentable elsewhere in the tree. I don’t want the current location to be a Binding as I want this action to be a one off and not reflected constantly in the UI.
TL;DR is there a standard way of having a parent get a reference to a child in SwiftUI, at least for UIViewRepresentables? (I understand this is probably not desirable in most cases and largely runs against the SwiftUI pattern).
I struggled with that myself, here's what worked using Combine and PassthroughSubject:
struct OuterView: View {
private var didChange = PassthroughSubject<String, Never>()
var body: some View {
VStack {
// send the PassthroughSubject over
Wrapper(didChange: didChange)
Button(action: {
self.didChange.send("customString")
})
}
}
}
// This is representable struct that acts as the bridge between UIKit <> SwiftUI
struct Wrapper: UIViewRepresentable {
var didChange: PassthroughSubject<String, Never>
#State var cancellable: AnyCancellable? = nil
func makeUIView(context: Context) → SomeView {
let someView = SomeView()
// ... perform some initializations here
// doing it in `main` thread is required to avoid the state being modified during
// a view update
DispatchQueue.main.async {
// very important to capture it as a variable, otherwise it'll be short lived.
self.cancellable = didChange.sink { (value) in
print("Received: \(value)")
// here you can do a switch case to know which method to call
// on your UIKit class, example:
if (value == "customString") {
// call your function!
someView.customFunction()
}
}
}
return someView
}
}
// This is your usual UIKit View
class SomeView: UIView {
func customFunction() {
// ...
}
}
I'm sure there are better ways, including using Combine and a PassthroughSubject. (But I never got that to work.) That said, if you're willing to "run against the SwiftUI pattern", why not just send a Notification? (That's what I do.)
In my model:
extension Notification.Name {
static let executeUIKitFunction = Notification.Name("ExecuteUIKitFunction")
}
final class Model : ObservableObject {
#Published var executeFuntionInUIKit = false {
willSet {
NotificationCenter.default.post(name: .executeUIKitFunction, object: nil, userInfo: nil)
}
}
}
And in my UIKit representable:
NotificationCenter.default.addObserver(self, selector: #selector(myUIKitFunction), name: .executeUIKitFunction, object: nil)
Place that in your init or viewDidLoad, depending on what kind of representable.
Again, this is not "pure" SwiftUI or Combine, but someone better than me can probably give you that - and you sound willing to get something that works. And trust me, this works.
EDIT: Of note, you need to do nothing extra in your representable - this simply works between your model and your UIKit view or view controller.
I was coming here to find a better answer, then the one I came up myself with, but maybe this does actually help someone?
It's pretty verbose though nevertheless and doesn't quite feel like the most idiomatic solution, so probably not exactly what the question author was looking for. But it does avoid polluting the global namespace and allows synchronous (and repeated) execution and returning values, unlike the NotificationCenter-based solution posted before.
An alternative considered was using a #StateObject instead, but I need to support iOS 13 currently where this is not available yet.
Excursion: Why would I want that? I need to handle a touch event, but I'm competing with another gesture defined in the SwiftUI world, which would take precedence over my UITapGestureRecognizer. (I hope this helps by giving some context for the brief sample code below.)
So what I came up with, was the following:
Add an optional closure as state (on FooView),
Pass it as a binding into the view representable (BarViewRepresentable),
Fill this from makeUIView,
So that this can call a method on BazUIView.
Note: It causes an undesired / unnecessary subsequent update of BarViewRepresentable, because setting the binding changes the state of the view representable though, but this is not really a problem in my case.
struct FooView: View {
#State private var closure: ((CGPoint) -> ())?
var body: some View {
BarViewRepresentable(closure: $closure)
.dragGesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded { value in
self.closure?(value.location)
})
)
}
}
class BarViewRepresentable: UIViewRepresentable {
#Binding var closure: ((CGPoint) -> ())?
func makeUIView(context: UIViewRepresentableContext<BarViewRepresentable>) -> BazUIView {
let view = BazUIView(frame: .zero)
updateUIView(view: view, context: context)
return view
}
func updateUIView(view: BazUIView, context: UIViewRepresentableContext<BarViewRepresentable>) {
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.closure = { [weak view] point in
guard let strongView = view? else {
return
}
strongView.handleTap(at: point)
}
}
}
}
class BazUIView: UIView { /*...*/ }
This is how I accomplished it succesfully. I create the UIView as a constant property in the SwiftUI View. Then I pass that reference into the UIViewRepresentable initializer which I use inside the makeUI method. Then I can call any method (maybe in an extension to the UIView) from the SwiftUI View (for instance, when tapping a button). In code is something like:
SwiftUI View
struct MySwiftUIView: View {
let myUIView = MKMapView(...) // Whatever initializer you use
var body: some View {
VStack {
MyUIView(myUIView: myUIView)
Button(action: { myUIView.buttonTapped() }) {
Text("Call buttonTapped")
}
}
}
}
UIView
struct MyUIView: UIViewRepresentable {
let myUIView: MKMapView
func makeUIView(context: UIViewRepresentableContext<MyUIView>) -> MKMapView {
// Configure myUIView
return myUIView
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MyUIView>) {
}
}
extension MKMapView {
func buttonTapped() {
print("The button was tapped!")
}
}

Creating a selector with variable of function type

I am working on two views that are subclassing subclass of UITableViewCell. In the base one (subclass of UITableViewCell) I am trying to setup gesture recognizer in a way that each of super class could change the behavior (eventually call didTapped method on it's delegate) of the tap.
I have written following code. I can use #selector(tap), however I think that using a variable instead of overriding a tap method in each super class is a much cleaner way. Is it even possible to use something like #selector(tapFunc)? If no what would be the cleanest and best from engineering point of view solution?
class BaseCell: UITableViewCell {
#objc var tapFunc: () -> () = { () in
print("Tapped")
}
#objc func tap() {
print("TEST")
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tapFunc))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
And then two views that are building on top of this one:
class ViewA: BaseCell {
//don't want to do this
override func tap() {
//do stuff
}
func setup {
//setup everything else
}
class ViewB: BaseCell {
var delegate: ViewBProtocool?
func setup {
tapFunc = { () in
delegate?.didTapped(self)
}
//setup everything else
}
You're not too far off. Make the following changes:
class BaseCell: UITableViewCell {
var tapFunc: (() -> Void)? = nil
// Called by tap gesture
#objc func tap() {
tapFunc?()
}
func setupBasicViews(withContent: () -> ()) {
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(tap))
contentView.isUserInteractionEnabled = true
contentView.addGestureRecognizer(tapGestureRecoginzer)
}
}
class ViewA: BaseCell {
func setup() {
//setup everything else
}
}
class ViewB: BaseCell {
var delegate: ViewBProtocol?
func setup() {
tapFunc = {
delegate?.didTapped(self)
}
//setup everything else
}
}
Now each subclass can optionally provide a closure for the tapFunc property.
I show above that tapFunc is optional with no default functionality in the base class. Feel free to change that to provide some default functionality if desired.

Swift: How to use instanceType in callback closure

I want to pass self as instancetype to the callback closure of this function:
extension UIView {
public func onTap(_ handler: #escaping (_ gesture: UITapGestureRecognizer, _ view: Self) -> Void) -> UITapGestureRecognizer {
...
}
}
let view = UIView.init()
view.onTap { tap, v in
...
}
But I got an error:
Self' is only available in a protocol or as the result of a method in a class; did you mean 'UIView'?
How can I do this?
that is just the perfect scenario (by book) when you can use protocols and extensions in Swift quite efficiently:
protocol Tappable { }
extension Tappable { // or alternatively: extension Tappable where Self: UIView {
func onTap(_ handler: #escaping (UITapGestureRecognizer, Self) -> Void) -> UITapGestureRecognizer {
return UITapGestureRecognizer() // as default to make this snippet sane
}
}
extension UIView: Tappable { }
then for e.g.:
let button = UIButton.init()
button.onTap { tap, v in
// v is UIButton...
}
while for e.g.:
let label = UILabel.init()
label.onTap { tap, v in
// v is UILabel...
}
etc...
NOTE: you can read more about Extensions or the Protocols in the Swift Programming Language Book from Apple.

Custom UIControl subclass with RxSwift

I am creating a custom subclass of UIControl (I need to override its draw method) and I want to add RxSwift to bind its isSelected property to my model.
So far so good. This works fine.
My problem is how can I do to change the value isSelected property in response of user touchUpInside event?.
My first try was to use the addTarget method of UIControl, but changing the value of isSelected programmatically is not reported by the ControlProperty (as stated in the doc). But I can figure another way to resolve this.
Any help appreciated.
Source code of the subclass:
class SYYesNoButton: UIControl {
override init(frame: CGRect) {
super.init(frame: frame)
// subscribe to touchUpInside event
addTarget(
self,
action: #selector(userDidTouchUpInside),
for: UIControlEvents.touchUpInside)
}
func userDidTouchUpInside() {
// change the value of the property
// this does not work,
// the change is not reported to the ControlProperty
// HOW CAN I CHANGE THIS ??
self.isSelected = !isSelected
}
}
Extensions to add reactive support:
extension SYYesNoButton {
var rx_isSelected: ControlProperty<Bool> {
return UIControl.valuePublic(
self,
getter: { (button) -> Bool in
return button.isSelected
},
setter: { (button, value) in
button.isSelected = value
})
}
}
extension UIControl {
static func valuePublic<T, ControlType: UIControl>(_ control: ControlType, getter: #escaping (ControlType) -> T, setter: #escaping (ControlType, T) -> ()) -> ControlProperty<T> {
let values: Observable<T> = Observable.deferred { [weak control] in
guard let existingSelf = control else {
return Observable.empty()
}
return (existingSelf as UIControl).rx.controlEvent([.allEditingEvents, .valueChanged])
.flatMap { _ in
return control.map { Observable.just(getter($0)) } ?? Observable.empty()
}
.startWith(getter(existingSelf))
}
return ControlProperty(values: values, valueSink: UIBindingObserver(UIElement: control) { control, value in
setter(control, value)
})
}
}
Thanks for all.
Once you have an actual UIControl, there's an even nicer way to a "native" RxCocoa extension called a ControlProperty using a helper method in RxCocoa.
For example:
extension Reactive where Base: someControl {
var someProperty: ControlProperty<Float> {
return controlProperty(editingEvents: .valueChanged,
getter: { $0.value },
setter: { $0.value = $1 })
}
}
This will expose the current value from the getter block whenever the specified UIControlEvent is fired, and will also set the value whenever some stream is bound to it.
It sort of acts like an Observable and Observer type together - you can observe its value, but can also subscribe to it.
If you are subclassing from UIControl, then you are making your own control class and you have to override one or more of beginTracking(_:with:), continueTracking(_:with:), endTracking(_:with:), or cancelTracking(with:) to make the control work the way you want. Then call sendActions(for:) with the correct event. The guts of a UIControl would not have Rx in it.
Taking a queue from UIButton, your control should not select itself, although it can highlight and unhighlight itself (when the user's finger is on it for example.)
Once you have properly created your UIControl, code outside the control can use Rx to observe it with no extra work on your part.
The following works (Updated for Swift 5/RxSwift 5):
class ViewController: UIViewController {
#IBOutlet weak var yesNoButton: SYYesNoButton!
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
yesNoButton.rx.controlEvent(.touchUpInside)
.scan(false) { v, _ in !v }
.bind(to: yesNoButton.rx.isSelected)
.disposed(by: bag)
}
}
#IBDesignable
class SYYesNoButton: UIControl {
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = isSelected ? .green : .red
}
override var isSelected: Bool {
didSet {
super.isSelected = isSelected
backgroundColor = isSelected ? .green : .red
}
}
}