I'm working with an app in prototype stage of development. Some interface elements do not have any action assigned to them either through storyboard or programmatically.
According to UX guidelines, I want to find these "inactive" buttons in app and have them display a "feature not available" alert when tapped during testing. Can this be done through an extension of UIButton?
How can I assign a default action to UIButton to show an alert unless another action is assigned via interface builder or programmatically?
Well what you are trying to achieve can be done. I have done this using a UIViewController extension and adding a closure as the target of a button which does not have a target. In case the button does not have an action an alert is presented.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.checkButtonAction()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
#IBAction func btn_Action(_ sender: UIButton) {
}
}
extension UIViewController{
func checkButtonAction(){
for view in self.view.subviews as [UIView] {
if let btn = view as? UIButton {
if (btn.allTargets.isEmpty){
btn.add(for: .touchUpInside, {
let alert = UIAlertController(title: "Test 3", message:"No selector", preferredStyle: UIAlertControllerStyle.alert)
// add an action (button)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
// show the alert
self.present(alert, animated: true, completion: nil)
})
}
}
}
}
}
class ClosureSleeve {
let closure: ()->()
init (_ closure: #escaping ()->()) {
self.closure = closure
}
#objc func invoke () {
closure()
}
}
extension UIControl {
func add (for controlEvents: UIControlEvents, _ closure: #escaping ()->()) {
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
I have tested it. Hope this helps. Happy coding.
As you cannot override methods in extensions, the only remaining options are:
1. subclass your buttons - but probably not what you're looking for, because I assume you want to use this functionality for already existing buttons
2. method swizzling - to change the implementation of existing function, i.e. init
How can I assign a default action to UIButton to show an alert unless
another action is assigned via interface builder or programmatically?
I would suggest to do method swizzling:
Through swizzling, the implementation of a method can be replaced with
a different one at runtime, by changing the mapping between a specific
selector(method) and the function that contains its implementation.
https://www.uraimo.com/2015/10/23/effective-method-swizzling-with-swift/
Remark: I would recommend to check the article above.
As an exact answer for your question, the following code snippet should be -in general- what are you trying to achieve:
class ViewController: UIViewController {
// MARK:- IBOutlets
#IBOutlet weak var lblMessage: UILabel!
// MARK:- IBActions
#IBAction func applySwizzlingTapped(_ sender: Any) {
swizzleButtonAction()
}
#IBAction func buttonTapped(_ sender: Any) {
print("Original!")
lblMessage.text = "Original!"
}
}
extension ViewController {
func swizzleButtonAction() {
let originalSelector = #selector(buttonTapped(_:))
let swizzledSelector = #selector(swizzledAction(_:))
let originalMethod = class_getInstanceMethod(ViewController.self, originalSelector)
let swizzledMethod = class_getInstanceMethod(ViewController.self, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
func swizzledAction(_ sender: Any) {
print("Swizzled!")
lblMessage.text = "Swizzled!"
}
}
After calling swizzleButtonAction() -by tapping the "Apply Swizzling" button (applySwizzlingTapped)-, the selector of "Button" should changed from buttonTapped to swizzledAction.
Output:
I would implement this kind of function by using IBOutlet collection variable, and then removing buttons from that collection as soon as the feature is implemented. something like this:
class ViewController {
#IBOutlet var inactiveBtns: [UIButton]!
func viewDidLoad() {
inactiveBtns.forEach { (button) in
button.addTarget(self, action: #selector(showDialog), for: UIControlEvents())
}
}
func showDialog() {
// show the dialog
}
}
That IBOutlet is visible in the interface builder, so you can add multiple buttons in there.
Related
How can I send action (similar kind of tap event) from sub class of UIImageView to View Controller.
Here is reference answer, that I want to apply for UIImageView. (UIImageView does not have UIControl in its super class hierarchy)
Following is my code but not working as I don't know, how to implement, what I need.
class TappableImageView: UIImageView {
// Initializer methods.....
//------------------------------------------
private func addTapGesture(){
let tapOnImage = UITapGestureRecognizer(target: self, action: #selector(TappableImageView.handleTapGesture(tapGesture:)))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(tapOnImage)
}
//----------------------------------------------
#objc func handleTapGesture(tapGesture: UITapGestureRecognizer) {
// how can I send tap/click event to all view controllers, where I've used this image from this point.
}
}
Is there any alternate/other solution that may work for me?
I will recommend you to use a closure to handle taps:
class TappableImageView: UIImageView {
var handleTap: (() -> Void)? = nil
//------------------------------------------
private func addTapGesture(){
let tapOnImage = UITapGestureRecognizer(target: self, action: #selector(TappableImageView.handleTapGesture(tapGesture:)))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(tapOnImage)
}
//----------------------------------------------
#objc func handleTapGesture(tapGesture: UITapGestureRecognizer) {
handleTap?()
}
}
And then I your view controller you can use this i.e. in viewDidLoad method:
override func viewDidLoad() {
super.viewDidLoad()
yourTappableImageView.handleTap = {
print("an image view was tapped")
}
}
It assumes that your TappableImageView is stored in variable named yourTappableImageView.
Can somebody please tell me how i want to show 3 buttons in the navigation bar and it shows in all the screens or view controllers ? I want to make this in the single class and calling that class in all the view controllers. I don't know how to use this.
You can add a container view that will be treated as a global view.
Here is the Method which you can make use
NavButton class:
import Foundation
import UIKit
protocol navProtocol : class {
func button1()
func button2()
func button3()
}
class navButtons
{
var navBtn1 = UIBarButtonItem()
var navBtn2 = UIBarButtonItem()
var navBtn3 = UIBarButtonItem()
var navProtocolObj : navProtocol?
static var shared = navButtons()
func createButtonView() -> (UIBarButtonItem,UIBarButtonItem,UIBarButtonItem)
{
navBtn1 = UIBarButtonItem(title: "btn1", style: UIBarButtonItemStyle.plain, target: self, action: #selector(navButtons.button1Action(sender:)))
navBtn2 = UIBarButtonItem(title: "btn2", style: UIBarButtonItemStyle.plain, target: self, action: #selector(navButtons.button2Action(sender:)))
navBtn3 = UIBarButtonItem(title: "btn3", style: UIBarButtonItemStyle.plain, target: self, action: #selector(navButtons.button3Action(sender:)))
return (navBtn1,navBtn2,navBtn3)
}
#objc func button1Action(sender:UIBarButtonItem){
navProtocolObj?.button1()
}
#objc func button2Action(sender:UIBarButtonItem){
navProtocolObj?.button2()
}
#objc func button3Action(sender:UIBarButtonItem){
navProtocolObj?.button3()
}
}
in Destination where you want all these to be performed
import UIKit
class SourceVC: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
let buttonAdded = navButtons.shared.createButtonView()
navButtons.shared.navProtocolObj = self
self.navigationItem.rightBarButtonItems = [buttonAdded.0,buttonAdded.1,buttonAdded.2]
}
}
extension SourceVC : navProtocol {
#objc func button1() {
print("Button 1 Tapped")
}
func button2() {
print("Button 2 Tapped")
}
func button3() {
print("Button 3 Tapped")
}
}
Simulator Output Above:
Console Output Above:
Make a Class named BaseVC. This will be your BaseViewController class. customise it and add 3 buttons to its's navigation bar programatically.
Now, extend this class in every other view controller and voila you are done.
My app is in prototype stage of development. Some sliders do not have any action assigned to them either through storyboard or programmatically.
I need to display an alert when slider drag stops during testing. Can this be done through an extension of UISlider?
How can I assign a default action to UISlider when the drag ends to show an alert unless another action is assigned via interface builder or programmatically?
Similar Question
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.checkButtonAction()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
}
extension UIViewController{
func checkButtonAction(){
for view in self.view.subviews as [UIView] {
if let btn = view as? UISlider {
if (btn.allTargets.isEmpty){
btn.add(for: .allTouchEvents, {
if (!btn.isTracking){
let alert = UIAlertController(title: "Test 3", message:"No selector", preferredStyle: UIAlertControllerStyle.alert)
// add an action (button)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
// show the alert
self.present(alert, animated: true, completion: nil)
}
})
}
}
}
}
}
class ClosureSleeve {
let closure: ()->()
init (_ closure: #escaping ()->()) {
self.closure = closure
}
#objc func invoke () {
closure()
}
}
extension UIControl {
func add (for controlEvents: UIControlEvents, _ closure: #escaping ()->()) {
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Please check this modified Answer.
Usually we have a predefined set of UIBarButtonItem in the project that can be used in the project and multiple times like a left menu button to open a side menu, it can be used in different UIViewControllers also a close button that dismiss the presented view controller.
The classic way is to add these buttons as needed, but this introduce a code duplication and we all want to avoid that.
My come up with an approach, but it's far from being perfect :
enum BarButtonItemType {
case menu, close, notification
}
enum BarButtonItemPosition{
case right, left
}
extension UIViewController {
func add(barButtons:[BarButtonItemType], position: BarButtonItemPosition) {
let barButtonItems = barButtons.map { rightBarButtonType -> UIBarButtonItem in
switch rightBarButtonType {
case .menu:
return UIBarButtonItem(image: UIImage(named:"menu"),
style: .plain,
target: self,
action: #selector(presentLeftMenu(_:)))
case .notification:
return UIBarButtonItem(image: UIImage(named:"notification"),
style: .plain,
target: self,
action: #selector(showNotification(_:)))
case .close:
return UIBarButtonItem(image: UIImage(named:"close"),
style: .plain,
target: self,
action: #selector(dismissController(_:)))
}
}
switch position {
case .right:
self.navigationItem.rightBarButtonItems = barButtonItems
case .left:
self.navigationItem.leftBarButtonItems = barButtonItems
}
}
// MARK: Actions
#objc fileprivate func presentLeftMenu(_ sender:AnyObject) {
self.parent?.presentLeftMenuViewController(sender)
}
#objc fileprivate func dismissController(_ sender:AnyObject) {
self.dismiss(animated: true, completion: nil)
}
#objc fileprivate func showNotification(_ sender:AnyObject) {
let notificationViewController = UINavigationController(rootViewController:NotificationViewController())
self.present(notificationViewController, animated: true, completion: nil)
}
}
and then the usage:
override func viewDidLoad() {
super.viewDidLoad()
self.add(barButtons: [.close], position: .right)
self.add(barButtons: [.menu], position: .left)
}
The limitations of my approach are:
The extension needs to know how to instantiate new view controller (case of notification for example) and what if viewController must be inited with parameters
It assumes that you only want to present a UIViewController
Not elegant.
I am sure that there is better way with Swift language and protocol oriented programming that can achieve the intended result with more flexibility, any thoughts ?
It seems that you're after having a default bar button configuration but specific (to subclass of UIViewController) bar button action implementations. You mentioned 1. "The extension needs to know how to instantiate new view controller" and your second point 2. "It assumes that you only want to present a UIViewController", thats a good sign that your extension should delegate that job to a subclass that knows what to do with those actions. Here I've done a sample implementation:
enum BarButtonItemPosition {
case right, left
}
enum BarButtonItemType {
case menu(BarButtonItemPosition)
case close(BarButtonItemPosition)
case notification(BarButtonItemPosition)
}
/// Has default implementation on UIViewControllers that conform to BarButtonActions.
protocol BarButtonItemConfiguration: class {
func addBarButtonItem(ofType type: BarButtonItemType)
}
/// Hate that we're forced to expose button targets to objc runtime :(
/// but I don't know any other way for the time being, maybe in Swift 6 :)
#objc protocol BarButtonActions {
#objc func presentLeftMenu(_ sender:AnyObject)
#objc func dismissController(_ sender:AnyObject)
#objc func showNotification(_ sender:AnyObject)
}
extension BarButtonItemConfiguration where Self: UIViewController, Self: BarButtonActions {
func addBarButtonItem(ofType type: BarButtonItemType) {
func newButton(imageName: String, position: BarButtonItemPosition, action: Selector?) {
let button = UIBarButtonItem(image: UIImage(named: imageName), style: .plain, target: self, action: action)
switch position {
case .left: self.navigationItem.leftBarButtonItem = button
case .right: self.navigationItem.rightBarButtonItem = button
}
}
switch type {
case .menu(let p): newButton(imageName: "", position: p, action: #selector(Self.presentLeftMenu(_:)))
case .notification(let p): newButton(imageName: "", position: p, action: #selector(Self.showNotification(_:)))
case .close(let p): newButton(imageName: "", position: p, action: #selector(Self.dismissController(_:)))
}
}
}
/// Conform to this in subclasses of UIViewController and implement BarButtonActions (its impl. differs from vc to vc).
protocol BarButtonConfigarable: BarButtonItemConfiguration, BarButtonActions {}
/// example
class SampleVC: UIViewController, BarButtonConfigarable {
override func viewDidLoad() {
super.viewDidLoad()
addBarButtonItem(ofType: .menu(.right))
addBarButtonItem(ofType: .menu(.left))
}
#objc func presentLeftMenu(_ sender:AnyObject) {
// TODO:
}
#objc func dismissController(_ sender:AnyObject) {
// TODO:
}
#objc func showNotification(_ sender:AnyObject) {
// TODO:
}
}
I have a function which should "toggle" a bar button item by changing between 2 images.
class Buttons {
func ToggleBarButton(button : UIBarButtonItem, name : String, location : BarButtonLocation, isEnabled : Bool, viewController : UIViewController) {
var iconName = name
if (!isEnabled) {
iconName += "EnabledIcon"
} else {
iconName += "DisabledIcon"
}
let newIcon = UIImage(named: iconName)
let newButton = UIBarButtonItem(image: newIcon, style: .Plain, target: self, action: button.action);
switch location {
case BarButtonLocation.Left:
viewController.navigationItem.leftBarButtonItem = newButton;
viewController.navigationItem.leftBarButtonItem?.tintColor = UIColor.blackColor();
case BarButtonLocation.SecondLeft:
viewController.navigationItem.leftBarButtonItems?[1] = newButton
viewController.navigationItem.leftBarButtonItems?[1].tintColor = UIColor.blackColor()
default:
return;
}
}
}
I also have a view controller class, in which there is the action of the bar button item.
class GradesViewController: UIViewController {
var isFilterEnabled = false
var isViewEnabled = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func filterButton_Pressed(sender: UIBarButtonItem) {
Buttons().ToggleBarButton(sender, name : "Filter", location: BarButtonLocation.Left, isEnabled: isFilterEnabled, viewController: self);
isFilterEnabled = !isFilterEnabled;
}
#IBAction func viewButton_Pressed(sender: UIBarButtonItem) {
Buttons().ToggleBarButton(sender, name : "View", location: BarButtonLocation.SecondLeft, isEnabled: isViewEnabled, viewController: self);
isViewEnabled = !isViewEnabled;
}
}
On first press it successfully changes the image to the enabled form, but on second press it doesn't do anything (press event doesn't even fire). I checked, and button.action is correctly identified as "filterButton_Pressed:". What's the problem, or is there an easier way to do this? Thanks for the answer in advance.
Put the break statement after each case and try.
And also remove the semi colons.
I just realized the problem was that I copied the code from the view controller to the button class, and didn't change target: self to target: viewController. But thanks for all the answers anyways...