Swift programmatically create function for button with a closure - swift

In Swift you can create a function for a button like this:
button.addTarget(self, action: #selector(buttonAction), forControlEvents: .TouchUpInside)
However is there a way I can do something like this:
button.whenButtonIsClicked({Insert code here})
That way I do not even have too declare an explicit function for the button. I know I can use button tags but I would prefer to do this instead.

Create your own UIButton subclass to do this:
class MyButton: UIButton {
var action: (() -> Void)?
func whenButtonIsClicked(action: #escaping () -> Void) {
self.action = action
self.addTarget(self, action: #selector(MyButton.clicked), for: .touchUpInside)
}
// Button Event Handler:
// I have not marked this as #IBAction because it is not intended to
// be hooked up to Interface Builder
#objc func clicked() {
action?()
}
}
Substitute MyButton for UIButton when you create buttons programmatically and then call whenButtonIsClicked to set up its functionality.
You can also use this with UIButtons in a Storyboard (just change their class to MyButton) and then call whenButtonIsClicked in viewDidLoad.
#IBOutlet weak var theButton: MyButton!
var count = 0
override func viewDidLoad() {
super.viewDidLoad()
// be sure to declare [unowned self] if you access
// properties or methods of the class so that you
// don't create a strong reference cycle
theButton.whenButtonIsClicked { [unowned self] in
self.count += 1
print("count = \(self.count)")
}
A much more capable implementation
Recognizing the fact that programmers might want to handle more events than just .touchUpInside, I wrote this more capable version which supports multiple closures per UIButton and multiple closures per event type.
class ClosureButton: UIButton {
private var actions = [UInt : [((UIControl.Event) -> Void)]]()
private let funcDict: [UInt : Selector] = [
UIControl.Event.touchCancel.rawValue: #selector(eventTouchCancel),
UIControl.Event.touchDown.rawValue: #selector(eventTouchDown),
UIControl.Event.touchDownRepeat.rawValue: #selector(eventTouchDownRepeat),
UIControl.Event.touchUpInside.rawValue: #selector(eventTouchUpInside),
UIControl.Event.touchUpOutside.rawValue: #selector(eventTouchUpOutside),
UIControl.Event.touchDragEnter.rawValue: #selector(eventTouchDragEnter),
UIControl.Event.touchDragExit.rawValue: #selector(eventTouchDragExit),
UIControl.Event.touchDragInside.rawValue: #selector(eventTouchDragInside),
UIControl.Event.touchDragOutside.rawValue: #selector(eventTouchDragOutside)
]
func handle(events: [UIControl.Event], action: #escaping (UIControl.Event) -> Void) {
for event in events {
if var closures = actions[event.rawValue] {
closures.append(action)
actions[event.rawValue] = closures
} else {
guard let sel = funcDict[event.rawValue] else { continue }
self.addTarget(self, action: sel, for: event)
actions[event.rawValue] = [action]
}
}
}
private func callActions(for event: UIControl.Event) {
guard let actions = actions[event.rawValue] else { return }
for action in actions {
action(event)
}
}
#objc private func eventTouchCancel() { callActions(for: .touchCancel) }
#objc private func eventTouchDown() { callActions(for: .touchDown) }
#objc private func eventTouchDownRepeat() { callActions(for: .touchDownRepeat) }
#objc private func eventTouchUpInside() { callActions(for: .touchUpInside) }
#objc private func eventTouchUpOutside() { callActions(for: .touchUpOutside) }
#objc private func eventTouchDragEnter() { callActions(for: .touchDragEnter) }
#objc private func eventTouchDragExit() { callActions(for: .touchDragExit) }
#objc private func eventTouchDragInside() { callActions(for: .touchDragInside) }
#objc private func eventTouchDragOutside() { callActions(for: .touchDragOutside) }
}
Demo
class ViewController: UIViewController {
var count = 0
override func viewDidLoad() {
super.viewDidLoad()
let button = ClosureButton(frame: CGRect(x: 50, y: 100, width: 60, height: 40))
button.setTitle("press me", for: .normal)
button.setTitleColor(.blue, for: .normal)
// Demonstration of handling a single UIControl.Event type.
// If your closure accesses self, be sure to declare [unowned self]
// to prevent a strong reference cycle
button.handle(events: [.touchUpInside]) { [unowned self] _ in
self.count += 1
print("count = \(self.count)")
}
// Define a second handler for touchUpInside:
button.handle(events: [.touchUpInside]) { _ in
print("I'll be called on touchUpInside too")
}
let manyEvents: [UIControl.Event] = [.touchCancel, .touchUpInside, .touchDown, .touchDownRepeat, .touchUpOutside, .touchDragEnter,
.touchDragExit, .touchDragInside, .touchDragOutside]
// Demonstration of handling multiple events
button.handle(events: manyEvents) { event in
switch event {
case .touchCancel:
print("touchCancel")
case .touchDown:
print("touchDown")
case .touchDownRepeat:
print("touchDownRepeat")
case .touchUpInside:
print("touchUpInside")
case .touchUpOutside:
print("touchUpOutside")
case .touchDragEnter:
print("touchDragEnter")
case .touchDragExit:
print("touchDragExit")
case .touchDragInside:
print("touchDragInside")
case .touchDragOutside:
print("touchDragOutside")
default:
break
}
}
self.view.addSubview(button)
}
}

If you don't want to do anything "questionable" (i.e., using Objective-C's dynamic capabilities, or adding your own touch handlers, etc.) and do this purely in Swift, unfortunately this is not possible.
Any time you see #selector in Swift, the compiler is calling objc_MsgSend under the hood. Swift doesn't support Objective-C's dynamicism. For better or for worse, this means that in order to swap out the usage of this selector with a block, you'd probably need to perform some black magic to make it work, and you'd have to use Objective-C constructs to do that.
If you don't have any qualms about doing "yucky dynamic Objective-C stuff", you could probably implement this by defining an extension on UIButton, and then associate a function to the object dynamically using associated objects. I'm going to stop here, but if you want to read more, NSHipster has a great overview on associated objects and how to use them.

This one will work !
Make sure you don't alter the tag for buttons
extension UIButton {
private func actionHandleBlock(action:(()->())? = nil) {
struct __ {
var closure : (() -> Void)?
typealias EmptyCallback = ()->()
static var action : [EmptyCallback] = []
}
if action != nil {
// __.action![(__.action?.count)!] = action!
self.tag = (__.action.count)
__.action.append(action!)
} else {
let exe = __.action[self.tag]
exe()
}
}
#objc private func triggerActionHandleBlock() {
self.actionHandleBlock()
}
func addAction(forControlEvents control :UIControlEvents, ForAction action:#escaping () -> Void) {
self.actionHandleBlock(action: action)
self.addTarget(self, action: #selector(triggerActionHandleBlock), for: control)
}
}

You can also just subclass UIView and have a property that is a closure like vacawama has.
var action: () -> ()?
Then override the touchesBegan method to call the function whenever the button is touched. With this approach though you don't get all the benefits of starting with a UIBitton.

let bt1 = UIButton(type: UIButtonType.InfoDark)
bt1.frame = CGRectMake(130, 80, 40, 40)
let bt2 = UIButton(type: UIButtonType.RoundedRect)
bt2.frame = CGRectMake(80, 180, 150, 44)
bt2.backgroundColor = UIColor.purpleColor()
bt2.tintColor = UIColor.yellowColor()
bt2.setTitle("Tap Me", forState: UIControlState.Normal)
bt2.addTarget(self, action: "buttonTap", forControlEvents: UIControlEvents.TouchUpInside)
let bt3 = UIButton(type: UIButtonType.RoundedRect)
bt3.backgroundColor = UIColor.brownColor()
bt3.tintColor = UIColor.redColor()
bt3.setTitle("Tap Me", forState: UIControlState.Normal)
bt3.frame = CGRectMake(80, 280, 150, 44)
bt3.layer.masksToBounds = true
bt3.layer.cornerRadius = 10
bt3.layer.borderColor = UIColor.lightGrayColor().CGColor
self.view.addSubview(bt1)
self.view.addSubview(bt2)
self.view.addSubview(bt3)
}
func buttonTap(button:UIButton)
{
let alert = UIAlertController(title: "Information", message: "UIButton Event", preferredStyle: UIAlertControllerStyle.Alert)
let OKAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
alert.addAction(OKAction)
}

Related

How to re-publish a PassthroughSubject using another publisher in Combine

Current (working) situation:
In our app we have several publishers of type PassthroughSubject<Void, Never>.
The subscriber of this publisher send out the same type of publisher within the .sink() closure. In a simple playground it would look like that:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
import Combine
class MyViewController : UIViewController {
// MARK: - Observables
let initialPublisher: PassthroughSubject = PassthroughSubject<Void, Never>()
let rePublisher = PassthroughSubject<Void, Never>()
// MARK: - Observer
private var cancellableSubscriber = Set<AnyCancellable>()
override func loadView() {
// MARK: - View Setup
let view = UIView()
let button = UIButton(type: .system)
button.frame = CGRect(x: 100, y: 100, width: 200, height: 20)
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
view.addSubview(button)
self.view = view
// MARK: - Subscriptions
// Event of initial publisher is received and re-published using another subject.
initialPublisher
.sink { [weak self] in
self?.rePublisher.send()
}
.store(in: &cancellableSubscriber)
// The re-published event is received.
rePublisher
.sink {
print("Received!")
}
.store(in: &cancellableSubscriber)
}
#objc private func buttonAction() {
self.initialPublisher.send()
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Prefered (non-working) solution:
Instead of subscribing and re publishing using the .sink() closure and another PassthroughSubject I wanted to re-publish the initial publisher using .receive(subscriber: AnySubscriber) However somehow it doesn't seem to work or maybe I'm understanding the .receive method wrong. I tried the following without luck.
Question:
How can I make the below code work, or is it even the correct way? If not, are there more elegant ways to re-publish than in our code above?
Clarification:
If something is unclear or of you need further examples please leave a comment below and I will try to update my question.
class MyViewController : UIViewController {
// MARK: - Observables
let initialPublisher: PassthroughSubject = PassthroughSubject<Void, Never>()
let rePublisher = PassthroughSubject<Void, Never>()
var subscriber = AnySubscriber<Void, Never>()
// MARK: - Observer
private var cancellableSubscriber = Set<AnyCancellable>()
override func loadView() {
// MARK: - View Setup
let view = UIView()
let button = UIButton(type: .system)
button.frame = CGRect(x: 100, y: 100, width: 200, height: 20)
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
view.addSubview(button)
self.view = view
// MARK: - Subscriptions
//Republishing
subscriber = AnySubscriber(initialPublisher)
// Event of initial publisher is received and re-published.
rePublisher.receive(subscriber: subscriber)
// // The re-published event is received.
rePublisher
.sink {
print("Received!") // <-- does not work!
}
.store(in: &cancellableSubscriber)
}
#objc private func buttonAction() {
self.initialPublisher.send()
}
}
I think you are working too hard. Just pass the AnyPublisher around instead of trying to tie two Subjects together. It doesn't even make sense to try to tie them together because anybody can call send on either of them.
class MyViewController : UIViewController {
let initialPublisher: PassthroughSubject = PassthroughSubject<Void, Never>()
var rePublisher: AnyPublisher<Void, Never> {
initialPublisher.eraseToAnyPublisher()
}
private var cancellableSubscriber = Set<AnyCancellable>()
override func loadView() {
super.loadView()
let button: UIButton = {
let result = UIButton(type: .system)
result.frame = CGRect(x: 100, y: 100, width: 200, height: 20)
result.setTitle("Button", for: .normal)
result.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
return result
}()
view.backgroundColor = .white
view.addSubview(button)
rePublisher
.sink {
print("Received!") // works!
}
.store(in: &cancellableSubscriber)
}
#objc private func buttonAction() {
initialPublisher.send()
}
}

Value of Type has no member Delegate error

So, I have the following code snippet:
protocol AllFiltersButtonsProtocol: AnyObject{
func didTapReset()
func didTapApply()
}
class AllFiltersUberResetApplyTableViewCell: UITableViewCell {
var delegate: AllFiltersButtonsProtocol?
var leftButton: UIButton = {
let leftButton = UIButton(.secondary, .large)
leftButton.setTitle("Reset", for: .normal)
leftButton.add(closureFor: .touchUpInside) { [self] in
self.delegate.didTapReset()
}
return leftButton
}()
But I am consistently getting the following error:
Value of type '(AllFiltersUberResetApplyTableViewCell) -> () -> AllFiltersUberResetApplyTableViewCell' has no member 'delegate What is wrong with my code? Why is is it saying there is no member delegate when there clearly is a member?
leftButton will be initialised before UITableViewCell is completely initialised and self exists. So You cannot use self inside var leftButton: UIButton. Try making it a lazy var to make sure that it gets called only when it is accessed for the first time.
lazy var leftButton: UIButton = {
let leftButton = UIButton(.secondary, .large)
leftButton.setTitle("Reset", for: .normal)
leftButton.add(closureFor: .touchUpInside) { [self] in
self.delegate.didTapReset()
}
return leftButton
}()

Swift: Create Unit Test for Private Functions in ViewController

I'm trying to test my viewcontroller, but I'm not sure how to test the private functions in there. It says is private. Obviously I could make it public but this seems to defeat the purpose...
import Quick
import Nimble
#testable import Recur
class ProfileDetailVCSpec: QuickSpec {
class TestProfileDetailVC: ProfileDetailVC {
var isProfileUpdated = false
override func updateProfile() {
isProfileUpdated = true
}
func pressDone() {
doneButtonPressed() //COMPILER WON'T ALLOW, BECAUSE IT'S PRIVATE
}
}
override func spec() {
var testProfileDetailVC: TestProfileDetailVC!
beforeEach {
testProfileDetailVC = TestProfileDetailVC()
}
describe("edit profile") {
context("user makes changes to name") {
it("should call updateProfile") {
testProfileDetailVC.nameTextFieldValidInputEntered(ProfileEditNameView(), "TestFirst", "TestLast")
testProfileDetailVC.pressDone()
expect(testProfileDetailVC?.isProfileUpdated).to(equal(true))
}
}
context("user makes changes to photo") {
it("should call updateProfile") {
testProfileDetailVC.nameTextFieldValidInputEntered(ProfileEditNameView(), "TestFirst", "TestLast")
testProfileDetailVC.pressDone()
expect(testProfileDetailVC?.isProfileUpdated).to(equal(true))
}
}
context("user doesn't make any changes") {
it("should not call updateProfile") {
testProfileDetailVC.pressDone()
expect(testProfileDetailVC?.isProfileUpdated).to(equal(false))
}
}
}
}
}
Here is the viewcontroller. Some of the logic my coworker is still working on, but it's mostly there. I just can't seem to be able to call the private functions in swift, so I can't run these tests
class ProfileDetailVC: UIViewController {
private let doneButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Done", for: .normal)
button.titleLabel?.font = UIFont(name: "SFCompactRounded-Semibold", size: 16)
button.tintColor = .recurBlue
button.addTarget(self, action: #selector(doneButtonPressed), for: .touchUpInside)
return button
}()
let profileNameEditView = ProfileEditNameView()
let errorLabel: UILabel = {
let label = UILabel()
label.textColor = .red
label.font = .regularSubtitle
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
loadProfileImage()
setupUI()
profileNameEditView.delegate = self
}
func updateProfile() {
}
private func loadProfileImage() {
if let profile = Profile.currentProfile {
profileImage.configure(with: profile, imageSize: CGSize(width: 120, height: 120))
}
}
#objc private func doneButtonPressed() {
updateProfile()
}
extension ProfileDetailVC: ProfileEditNameViewDelegate {
func nameTextFieldNonValidInputEntered(_: ProfileEditNameView) {
errorLabel.text = "First and last name required"
}
func nameTextFieldValidInputEntered(_: ProfileEditNameView, _ firstNameText: String, _ lastNameText: String) {
errorLabel.text = ""
}
}
There is no way to access a private func for testing. The #testable attribute you are using will allow you to use an internal func in a test however. So you can drop the private keyword and the func will default to internal as the class is internal.

RxSwift - rx.tap not working

I have view controller. Inside it I have view
lazy var statusView: StatusView = {
var statusView = StatusView()
return statusView
}()
Inside statusView I have button
lazy var backButton: UIButton = {
var button = UIButton(type: .system)
button.titleLabel?.font = UIFont().regularFontOfSize(size: 20)
return button
}()
In controller I have
override func viewDidLoad() {
super.viewDidLoad()
setupRx()
}
func setupRx() {
_ = statusView.backButton.rx.tap.subscribe { [weak self] in
guard let strongSelf = self else { return }
print("hello")
}
}
But when I tap to button, nothing get printed to console.
What am I doing wrong ?
In general nothing wrong, but there's a minor hidden trick.
You use
backButton.rx.tap.subscribe { [weak self] in
But you need to use
backButton.rx.tap.subscribe { [weak self] _ in ...
Did you notice underscore in the second version? The second version calls method
public func subscribe(_ on: #escaping (Event<E>) -> Void)
of ObservableType. In this case on closure to deliver an event is provided, but we just ignore incoming parameter of this closure using underscore
It looks like the subscription is going out of scope as soon as setupRx returns. If you add a DisposeBag to the view controller and add the subscription to the dispose bag, does that solve the problem? Something like this:
func setupRx() {
statusView.backButton.rx.tap
.subscribe { [weak self] in
guard let strongSelf = self else { return }
print("hello")
}
}
.addDisposableTo(self.disposeBag)
}
Hope that helps.

Passing selector via IBInspectable in Swift 3

I created a custom control and I want to pass the action in an #IBInspectable property to achieve the same effect of setting up an #IBAction using UIButton. How should I go about doing this?
class MyControl: UIButton {
// Problem with this string approach is
//I have no way to know which instance to perform the selector on.
#IBInspectable var tapAction: String?
// set up tap gesture
...
func labelPressed(_ sender: UIGestureRecognizer) {
if let tapAction = tapAction {
// How should I convert the string in tapAction into a selector here?
//I also want to pass an argument to this selector.
}
}
}
I really don't know why do you want it, but... Here is my solution:
Create a MyActions class, with actions that MyControl can to call:
class MyActions: NSObject {
func foo() {
print("foo")
}
func bar() {
print("bar")
}
func baz() {
print("baz")
}
}
Replace your MyControl class to
class MyControl: UIButton {
#IBInspectable var actionClass: String?
#IBInspectable var tapAction: String?
private var actions: NSObject?
override func awakeFromNib() {
// initialize actions class
let bundleName = Bundle.main.bundleIdentifier!.components(separatedBy: ".").last!
let className = "\(bundleName).\(actionClass!)"
guard let targetClass = NSClassFromString(className) as? NSObject.Type else {
print("Class \(className) not found!")
return
}
self.actions = targetClass.init()
// create tap gesture
let tap = UITapGestureRecognizer(target: self, action: #selector(pressed(_:)))
self.addGestureRecognizer(tap)
}
func pressed(_ sender: UIGestureRecognizer) {
actions!.perform(Selector(tapAction!))
}
}
And, set attributes of your button:
You can change the Tap Action in run time, for example:
#IBAction func buttonChangeAction(_ sender: Any) {
buttonMyControl.tapAction = textField.text!
}
Maybe you can change my code to pass parameters, but... it's you want?