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

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()
}
}

Related

Connect UIViewRepresentable to SwiftUI

I have a SwiftUI based app with a simple button that when pressed is supposed to open a Camera Class from AVFoundation that utilizes UIKit as well. Under the sheet I am not sure what exactly to place there. I tried CameraSession() and a few other ideas but I am sort of lost on bridging this SwiftUI button to open camera app. Thank you!
//Content View
import SwiftUI
struct ContentView: View {
//#State private var image: Image?
#State private var showingCameraSession = false
//#Binding var isShown: Bool
var body: some View {
VStack{
ControlButton(systemIconName: "slider.horizontal.3"){
//Button("Seelect Image") {
showingCameraSession = true
} .sheet(isPresented: $showingCameraSession){
//What to place here?
}
}
}
}
//CameraSession
import AVFoundation
//import RealityKit
import UIKit
import SwiftUI
struct CameraSession : UIViewControllerRepresentable {
//#Binding var isShown: Bool
typealias UIViewControllerType = CaptureSession
func makeUIViewController(context: Context) -> CaptureSession{
return CaptureSession()
}
func updateUIViewController(_ uiViewController: CaptureSession, context: Context) {
// if(self.isShown){
//CameraSession.didTapTakePhoto()
// shutterButton.addTarget(self, action: #selector(didTapTakePhoto), for: .touchUpInside) //tie button to actual function
}
}
class CaptureSession: UIViewController {
//#Binding var isShown: Bool
//Reference: https://www.youtube.com/watch?v=ZYPNXLABf3c
//CaptureSession
var session: AVCaptureSession?
//PhotoOutput --> to the Cloud
let output = AVCapturePhotoOutput()
// Video Preview
let previewLayer = AVCaptureVideoPreviewLayer()
//Shutter Button
private let shutterButton: UIButton = {
let button = UIButton(frame: CGRect(x:0, y:0, width: 100, height: 100))
button.layer.cornerRadius = 50
button.layer.borderWidth = 10
button.layer.borderColor = UIColor.white.cgColor
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
//previewLayer.backgroundColor = UIColor.systemRed.cgColor
view.layer.addSublayer(previewLayer)
view.addSubview(shutterButton)
checkCameraPermissions()
shutterButton.addTarget(self, action: #selector(didTapTakePhoto), for: .touchUpInside) //tie button to actual function
}
override func viewDidLayoutSubviews(){
super.viewDidLayoutSubviews()
previewLayer.frame = view.bounds
shutterButton.center = CGPoint(x: view.frame.size.width/2, y: view.frame.size.height - 100)
}
private func checkCameraPermissions() {
switch AVCaptureDevice.authorizationStatus(for: .video){
case .notDetermined:
//Request Permission
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard granted else {
return
}
DispatchQueue.main.async{
self?.setUpCamera()
}
}
case .restricted:
break
case .denied:
break
case .authorized:
setUpCamera()
#unknown default:
break
}
}
//with Photogrammetry, you also have to create a session similar https://developer.apple.com/documentation/realitykit/creating_3d_objects_from_photographs/
// example app: https://developer.apple.com/documentation/realitykit/taking_pictures_for_3d_object_capture
private func setUpCamera(){
let session = AVCaptureSession()
if let device = AVCaptureDevice.default(for: .video){
do{
let input = try AVCaptureDeviceInput(device: device)
if session.canAddInput(input){
session.addInput(input) //some Devices contract each other.
}
if session.canAddOutput(output) {
session.addOutput(output)
}
previewLayer.videoGravity = .resizeAspectFill //content does not get distored or filled
previewLayer.session = session
session.startRunning()
self.session = session
}
catch{
print(error)
}
}
}
//originally private
#objc private func didTapTakePhoto() {
output.capturePhoto(with: AVCapturePhotoSettings(),
delegate: self)
// let vc = UIHostingController(rootView: ContentView())
// present(vc, animated: true)
}
}
//AVCaptureOutput is AVFoundations version of photo output
extension CaptureSession: AVCapturePhotoCaptureDelegate {
func photoOutput( output: AVCaptureOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error:
Error?){
guard let data = photo.fileDataRepresentation() else { //where to store file information
return
}
let image = UIImage(data: data)
session?.stopRunning()
let imageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFill
imageView.frame = view.bounds
view.addSubview(imageView)
}
}
So to get around this first make your app has permission to access the users camera(go to Info.plist or info tab beside the build settings at the top and add Privacy camera usage and add "We need your camera to perform this action")
After that a simple call in the sheet's modifier should do the trick
struct ContentView: View {
//#State private var image: Image?
#State private var showingCameraSession = false
//#Binding var isShown: Bool
var body: some View {
VStack{
// ControlButton(systemIconName: "slider.horizontal.3"){
Button("Seelect Image") {
showingCameraSession = true
} .sheet(isPresented: $showingCameraSession){
//What to place here?
CameraSession()
}
}
}
}

Two-way databinding using Combine

I'm building a reusable custom stepper view.
I have it all working, and am using Combine for observing value changes. However, I'd like to improve it by using two-way databinding, but not sure if that is possible?
Here is my current code:
class Stepper: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
#Published var count = 0
private let stackView = UIStackView()
private let minusButton = BorderedRoundButton()
private let plusButton = BorderedRoundButton()
private let countLabel = UILabel()
private var cancellables = Set<AnyCancellable>()
func setup() {
translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
minusButton.setTitleColor(.primaryTint, for: .normal)
plusButton.setTitleColor(.primaryTint, for: .normal)
minusButton.setTitle("-", for: .normal)
plusButton.setTitle("+", for: .normal)
countLabel.text = "0"
countLabel.textColor = .blackText
countLabel.font = .preferredFont(forTextStyle: .body)
countLabel.textAlignment = .center
stackView.addArrangedSubview(minusButton)
stackView.addArrangedSubview(countLabel)
stackView.addArrangedSubview(plusButton)
minusButton.tapPublisher.sink { [weak self] _ in
self?.count -= 1
}.store(in: &cancellables)
plusButton.tapPublisher.sink { [weak self] _ in
self?.count += 1
}.store(in: &cancellables)
$count.map { "\($0)" }.assign(to: \.text, on: countLabel).store(in: &cancellables)
$count.map { $0 > 0 }.assign(to: \.isEnabled, on: minusButton).store(in: &cancellables)
addSubview(stackView)
NSLayoutConstraint.activate([
minusButton.widthAnchor.constraint(equalToConstant: 38),
minusButton.heightAnchor.constraint(equalToConstant: 38),
plusButton.widthAnchor.constraint(equalToConstant: 38),
plusButton.heightAnchor.constraint(equalToConstant: 38),
countLabel.widthAnchor.constraint(equalToConstant: 44),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}
And I am using it like this:
class PickerViewController: UIViewController {
#IBOutlet private var stepper: Stepper!
private var cancellables = Set<AnyCancellable>()
#Published var count = 0
override func viewDidLoad() {
super.viewDidLoad()
stepper.count = count
stepper.$count.sink { [weak self] value in
self?.count = value
}.store(in: &cancellables)
}
}
And that is the bit I want to improve: I now need to both set the initial value and then observe the changes. I know it's hardly a lot of code, but I am just wondering if I can do it in one line, with a sort of two-way databinding?
I thought about using SwiftUI's #Bindable propertywrapper (or a rebuilt version of it as to not have to import SwiftUI), so that PickerViewController.count is the one source, and the Stepper would automatically update it. However, then I have the problem that #Bindable is not observable itself, so these lines would no longer work:
$count.map { "\($0)" }.assign(to: \.text, on: countLabel).store(in: &cancellables)
$count.map { $0 > 0 }.assign(to: \.isEnabled, on: minusButton).store(in: &cancellables)
So my question is: can I improve the code to use two-way databinding, or should I just stick with setting the value and then observing it separately?

How does iOS Custom Keyboard buttons action triggered?

I am trying to make custom keyboard extension. Keyboard buttons showing nicely but no action triggered! Here is Buttons UI:
struct MyKeyButtons: View {
let data: [String] = ["A", "B", "C"]
var body: some View {
HStack {
ForEach(data, id: \.self) { aData in
Button(action: {
KeyboardViewController().keyPressed()
}) {
Text(aData)
.fontWeight(.bold)
.font(.title)
.foregroundColor(.purple)
.padding()
.border(Color.purple, width: 5)
}
}
}
}
}
The KeyboardViewController code here:
import SwiftUI
class KeyboardViewController: UIInputViewController {
#IBOutlet var nextKeyboardButton: UIButton!
override func updateViewConstraints() {
super.updateViewConstraints()
// Add custom view sizing constraints here
}
override func viewDidLoad() {
super.viewDidLoad()
let child = UIHostingController(rootView: MyKeyButtons())
//that's wrong, it must be true to make flexible constraints work
// child.translatesAutoresizingMaskIntoConstraints = false
child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(child.view)
addChild(child)//not sure what is this for, it works without it.
// Perform custom UI setup here
self.nextKeyboardButton = UIButton(type: .system)
self.nextKeyboardButton.setTitle(NSLocalizedString("Next Keyboard", comment: "Title for 'Next Keyboard' button"), for: [])
self.nextKeyboardButton.sizeToFit()
self.nextKeyboardButton.translatesAutoresizingMaskIntoConstraints = false
self.nextKeyboardButton.addTarget(self, action: #selector(handleInputModeList(from:with:)), for: .allTouchEvents)
self.view.addSubview(self.nextKeyboardButton)
self.nextKeyboardButton.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.nextKeyboardButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
}
override func viewWillLayoutSubviews() {
self.nextKeyboardButton.isHidden = !self.needsInputModeSwitchKey
super.viewWillLayoutSubviews()
}
override func textWillChange(_ textInput: UITextInput?) {
// The app is about to change the document's contents. Perform any preparation here.
}
override func textDidChange(_ textInput: UITextInput?) {
// The app has just changed the document's contents, the document context has been updated.
var textColor: UIColor
let proxy = self.textDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.dark {
textColor = UIColor.white
} else {
textColor = UIColor.black
}
self.nextKeyboardButton.setTitleColor(textColor, for: [])
}
//==================================
func keyPressed() {
print("test--- clicked! ")
//textDocumentProxy.insertText("a")
(textDocumentProxy as UIKeyInput).insertText("a")
}
}
For more info see the GitHub project: https://github.com/ask2asim/KeyboardTest1

"Extensions must not contain stored properties" preventing me from refactoring code

I have a 13 lines func that is repeated in my app in every ViewController, which sums to a total of 690 lines of code across the entire project!
/// Adds Menu Button
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
for menuTappedAction function to work, I have to declare a weak var like this:
extension UIViewController {
weak var coordinator: MainCoordinator?
But by doing this I get error Extensions must not contain stored properties
What I tried so far:
1) Removing the weak keyword will cause conflicts in all my app.
2) Declaring this way:
weak var coordinator: MainCoordinator?
extension UIViewController {
Will silence the error but the coordinator will not perform any action. Any suggestion how to solve this problem?
You can move your addMenuButton() function to a protocol with a protocol extension. For example:
#objc protocol Coordinated: class {
var coordinator: MainCoordinator? { get set }
#objc func menuTappedAction()
}
extension Coordinated where Self: UIViewController {
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
}
Unfortunately, you can't add #objc methods to class extensions (see: this stackoverflow question), so you'll still have to setup your view controllers like this:
class SomeViewController: UIViewController, Coordinated {
weak var coordinator: MainCoordinator?
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
}
It'll save you some code, and it will allow you to refactor the bigger function addMenuButton(). Hope this helps!
For it to work in an extension you have to make it computed property like so : -
extension ViewController {
// Make it computed property
weak var coordinator: MainCoordinator? {
return MainCoordinator()
}
}
You could use objc associated objects.
extension UIViewController {
private struct Keys {
static var coordinator = "coordinator_key"
}
private class Weak<V: AnyObject> {
weak var value: V?
init?(_ value: V?) {
guard value != nil else { return nil }
self.value = value
}
}
var coordinator: Coordinator? {
get { (objc_getAssociatedObject(self, &Keys.coordinator) as? Weak<Coordinator>)?.value }
set { objc_setAssociatedObject(self, &Keys.coordinator, Weak(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
}
This happens because an extension is not a class, so it can't contain stored properties. Even if they are weak properties.
With that in mind, you have two main options:
The swift way: Protocol + Protocol Extension
The nasty objc way: associated objects
Option 1: use protocol and a protocol extension:
1.1. Declare your protocol
protocol CoordinatorProtocol: class {
var coordinator: MainCoordinator? { get set }
func menuTappedAction()
}
1.2. Create a protocol extension so you can pre-implement the addMenuButton() method
extension CoordinatorProtocol where Self: UIViewController {
func menuTappedAction() {
// Do your stuff here
}
}
1.3. Declare the weak var coordinator: MainCoordinator? in the classes that will be adopting this protocol. Unfortunately, you can't skip this
class SomeViewController: UIViewController, CoordinatorProtocol {
weak var coordinator: MainCoordinator?
}
Option 2: use objc associated objects (NOT RECOMMENDED)
extension UIViewController {
private struct Keys {
static var coordinator = "coordinator_key"
}
public var coordinator: Coordinator? {
get { objc_getAssociatedObject(self, &Keys.coordinator) as? Coordinator }
set { objc_setAssociatedObject(self, &Keys.coordinator, newValue, .OBJC_ASSOCIATION_ASSIGN) }
}
}
You can do it through subclassing
class CustomVC:UIViewController {
weak var coordinator: MainCoordinator?
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
}
class MainCoordinator {
func openMenu() {
}
}
class ViewController: CustomVC {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Use a NSMapTable to create a state container for your extension, but make sure that you specify to use weak references for keys.
Create a class in which you want to store the state. Let's call it ExtensionState and then create a map as a private field in extension file.
private var extensionStateMap: NSMapTable<TypeBeingExtended, ExtensionState> = NSMapTable.weakToStrongObjects()
Then your extension can be something like this.
extension TypeBeingExtended {
private func getExtensionState() -> ExtensionState {
var state = extensionStateMap.object(forKey: self)
if state == nil {
state = ExtensionState()
extensionStateMap.setObject(state, forKey: self)
}
return state
}
func toggleFlag() {
var state = getExtensionState()
state.flag = !state.flag
}
}
This works in iOS and macOS development, but not on server side Swift as there is no NSMapTable there.

Swift programmatically create function for button with a closure

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)
}