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.
Related
how can I create reusable view controller (let's call it "reusableVC") acting like UIAlertController. ReusableVC have "ok" button, that will act depending from where resuableVC called. I know about delegates and NotificationCenter. Just wondering can we pass what "ok" button should do when creating reusableVC, like this:
reusableVC.addAction(UIAlertAction(title: "", style: .default, handler: { (action) in
// some code
}))
If you only need one OK button you may use this solution, otherwise, you can still find interest in this pattern.
class ReusableVC{
var onOKPressed: ( () -> () )?
// Create all your other things and don't forget that you should call onOKPressed() whenever user pushed that OK button
}
class ViewController{
func setupReusableVC(){
let reusableVC = ReusableVC()
reusableVC.onOKPressed = {
print("ok pressed")
}
}
}
The action handler is just a closure. You can declare it everywhere.
In the reusable view controller add a property
var customAction : ((UIAlertAction) -> Void)?
and pass the property as handler
reusableVC.addAction(UIAlertAction(title: "", style: .default, handler: customAction))
In the source view controller create the action
let action : ((UIAlertAction) -> Void)? = { action in
// do something
}
and pass it in perform(segue
Create a UIViewController Extension to include Alert Functionality
extension UIViewController{
open func hideKeyBoardOnTap(){
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
self.view.addGestureRecognizer(tap)
}
#objc private func dismissKeyboard(){
self.view.endEditing(true)
}
open func showAlertWithOK(_ title: String = "Alert!",message: String = "Please take appropriate action"){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okButton = UIAlertAction(title: "Ok", style: .default, handler:{ (alertAction) in
self.okAction()
})
alert.addAction(okButton)
DispatchQueue.main.async {
self.present(alert, animated: true, completion: nil)
}
}
open func showAlertWithOkAndCancel(_ title: String = "Alert!",_ message: String = "Please take appropriate action", _ firstButtonTitle: String = "Ok", _ firstButtonStyle: UIAlertActionStyle = .default, _ secondButtonTitle: String = "Cancel",_ secondButtonStyle: UIAlertActionStyle = .cancel){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okButton = UIAlertAction(title: firstButtonTitle, style: firstButtonStyle, handler:{ (alertAction) in
self.okAction()
})
let cancelButton = UIAlertAction(title: secondButtonTitle, style: secondButtonStyle, handler: { (alertAction) in
self.cancelAction()
})
alert.addAction(okButton)
alert.addAction(cancelButton)
self.present(alert, animated: true, completion: nil)
}
#objc private func okAction(){
self.dismiss(animated: true, completion: nil)
}
#objc private func cancelAction(){
self.dismiss(animated: true, completion: nil)
}
}
How to Use
func didReceiveError(_ error: CFNetworkErrors) {
var message = error.message
self.showAlertWithOK("Error", message: message)
}
func didEndWebserviceCall() {
self.showAlertWithOK(message: "didEndWebserviceCall")
}
Advantages:
You can access alert using self(which is your viewcontroller in this case)
Code reusability
Clean code.
protocol TapEventDelegate: protocol {
func buttonTap()
}
class ClassWhereDoYouWantToCatchTheEvent: TapEventDelegate {
func buttonTap() {
print("caught!")
}
}
class YourViewControllerClass {
weak var tapEventDelegate: TapEventDelegate?
reusableVC.addAction(UIAlertAction(title: "", style: .default, handler: { (action) in
tapEventDelegate?.buttonTap()
}))
}
to bind your class with YourViewControllerClass and ClassWhereDoYouWantToCatchTheEvent use somewhere at view controller initialization:
classWhereDoYouWantToCatchTheEvent.tapEventHandler = yourViewControllerClass
You can create custom UIViewController class and pass the addAction closure and then you can call that closure on the OK button tap from your CustomAlertController.
final class CustomAlertController: UIViewController {
var actionHandler: (() -> Void)?
lazy var okButton: UIButton = {
let button = UIButton()
button.backgroundColor = .black
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("OK", for: .normal)
button.addTarget(self, action: #selector(CustomAlertController.didTapOkButton(_:)), for: .touchUpInside)
button.layer.cornerRadius = 10
return button
}()
override func loadView() {
view = UIView()
view.backgroundColor = .white
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
addActionButton()
}
private func addActionButton() {
view.addSubview(okButton)
NSLayoutConstraint.activate([
okButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
okButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
okButton.heightAnchor.constraint(equalToConstant: 50),
okButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
])
}
public func addAction(title: String, handler: #escaping (() -> Void) {
okButton.setTitle(title, for: .normal)
actionHandler = handler
}
#objc func didTapOkButton(_ button: UIButton) {
actionHandler?()
dismiss(animated: true)
}
}
Then you can present CustomAlertController from your ViewController class and add action like below
class ViewController: UIViewController {
override func loadView() {
view = UIView()
view.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let alertController = CustomAlertController()
alertController.addAction(title: "OK", handler: { [unowned self] in
self.view.backgroundColor = .blue
print("OK button tapped")
})
present(alertController, animated: true)
}
}
I have a View controller that has a tableView embedded in a ContainerView and just below the ContainerView I have a UISlider. I have code on a view controller for the UISlider and code on another view controller that controls the table view.
Properties of the UISlider are set based on the selected text field - this section of code works. I am struggling to create a function/feature that will change the textField value when the UISlider is move. I think the UISlider Action needs to on the code that controls the UISlider, but I cannot determine how to cast the value of the UISlider.setvalue between the two viewController as the slider is moved to update the textField located in a tableCell. Hopefully makes some sense.
// UISlider ViewController
override func viewDidLoad() {
super.viewDidLoad()
sliderOutlet.isContinuous = true
sliderOutlet.tintColor = UIColor.green
self.refreshSlider()
}
#objc func refreshSlider() {
sliderOutlet.minimumValue = Float(GlobalSliderValues.minimumValue)
sliderOutlet.maximumValue = Float(GlobalSliderValues.maximumValue)
sliderOutlet.value = Float(GlobalSliderValues.sliderValue)
// if let chgIntValue = Int(GlobalSliderValues.changeValue)
// { sliderOutlet.setValue(Float(Double(chgIntValue)), animated: true)
// }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(self.refreshSlider), name: Notification.Name("refreshSlider"), object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: Notification.Name("refreshSlider"), object: nil)
}
TableView Controller
override func viewDidLoad() {
super.viewDidLoad()
mortgageAmount.addTarget(self, action: #selector(chgTextFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
}
#objc func chgTextFieldDidChange(textField: UITextField)
{
if let chgStringValue = mortgageAmount.text
{
if Double(chgStringValue) ?? 1 > 10000 {
let alert = UIAlertController(title: "Input Error", message: "Value cannot be greater than 10000", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
return
}
GlobalSliderValues.minimumValue = 10
GlobalSliderValues.maximumValue = 10000
GlobalSliderValues.sliderValue = Int(mortgageAmount.text!)!
GlobalSliderValues.mortageAmountValue = Float(Int(mortgageAmount.text!)!)
NotificationCenter.default.post(name:Notification.Name("refreshSlider"), object: nil)
if let chgIntValue = Int(chgStringValue)
{ GlobalSliderValues.changeValue.setValue(Float(Double(chgIntValue)), animated: true)
}
}
}
#IBAction func valueChanged(_ sender: UISlider) {
mortgageAmount.text = String(format: "%.2f",Double(sender.value))
}
struct GlobalSliderValues {
static var minimumValue = Int()
static var maximumValue = Int()
static var lowerValue = Int()
static var UpperValue = Int()
static var locationValue = Int()
static var sliderValue = Int()
static var sliderChgValue = ""
}
We don't need notification center to deal with refreshing the slider. Since we are using constants to store value your code can be changed as follows
class ViewController: UIViewController {
#IBOutlet weak var sliderOutlet: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
sliderOutlet.isContinuous = true
sliderOutlet.tintColor = UIColor.green
}
#objc func refreshSlider() {
DispatchQueue.main.async {
self.sliderOutlet.minimumValue = Float(GlobalSliderValues.minimumValue)
self.sliderOutlet.maximumValue = Float(GlobalSliderValues.maximumValue)
self.sliderOutlet.value = Float(GlobalSliderValues.sliderValue)
print(self.sliderOutlet.minimumValue)
print(self.sliderOutlet.maximumValue)
print(self.sliderOutlet.value)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.refreshSlider()
}
}
I entered mortgage amount as so 1098 so slider look like below
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.
I have an odd (at least to me) issue in that I used the pod Localize_Swift to localize my app.
I have two issues that I cannot figure out as nearly all documentation about localizing only covers text on the same vc as the localize menu or button. In my case, I have localize strings set up and across the entire app, they are in use with a .localize suffix.
First issue:
The button either does not work. The print() function never gets triggered or if it does work, appears to be randomly, I get the unrecognized selector sent to instance that lists the func changeLanguage as the cause. I commented out all of the code within the function, but still received the error.
I have also deleted the func and recreated it, but still no go.
Second issue:
How to initialize the localization across the entire app with the strings?
SettingsViewController.swift
import UIKit
import Localize_Swift
class SettingsViewController: UITableViewController
{
var availableLanguages = Localize.availableLanguages()
var actionSheet: UIAlertController!
#IBOutlet weak var switchlangButton: UIButton!
#IBOutlet weak var flagImageView: UIImageView!
override func viewDidLoad()
{
super.viewDidLoad()
let backButton:UIBarButtonItem = UIBarButtonItem(title: "Back".localized, style: UIBarButtonItemStyle.Done, target: self, action: #selector(SettingsViewController.backBtnAction(_:)))
self.navigationItem.leftBarButtonItem = backButton
configureView()
availableLanguages.removeAtIndex(0)
print(availableLanguages)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SettingsViewController.setTranslatedText), name: LCLLanguageChangeNotification, object: nil)
}
func setTranslatedText(){
switchlangButton.setTitle("Switch Language".localized(), forState: UIControlState.Normal)
flagImageView.image = UIImage(named: "flag-en".localized() + ".png")
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
#IBAction func changeLanguage(sender: AnyObject) {
print ("changeLanguageButton pressed")
actionSheet = UIAlertController(title: nil, message: "Switch Language".localized(), preferredStyle: UIAlertControllerStyle.ActionSheet)
for language in availableLanguages {
let displayName = Localize.displayNameForLanguage(language)
let languageAction = UIAlertAction(title: displayName, style: .Default, handler: {
(alert: UIAlertAction!) -> Void in
Localize.setCurrentLanguage(language)
})
actionSheet.addAction(languageAction)
}
let cancelAction = UIAlertAction(title: "Cancel".localized(), style: UIAlertActionStyle.Cancel, handler: {
(alert: UIAlertAction) -> Void in
})
actionSheet.addAction(cancelAction)
self.presentViewController(actionSheet, animated: true, completion: nil)
}
func configureView(){
switchlangButton.setTitle("Switch Language".localized(), forState: UIControlState.Normal)
flagImageView.image = UIImage(named: "flag-en".localized() + ".png")
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
#IBAction func backBtnAction(sender:UIBarButtonItem)
{
navigationController?.popViewControllerAnimated(true)
}
}
Screenshot of the Settings scene
Just an FYI for troubleshooting, the image does not work either.
I currently have a subview that is created and added to the UIView in ViewDidLoad(). I am attempting to user UIGestureRecognizers to detect a tap and unhide a particular button. My current code:
override func viewDidLoad() {
super.viewDidLoad()
architectView = CustomClass(frame: self.view.bounds)
self.view.addSubview(architectView)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: "handleTap:")
gestureRecognizer.delegate = self
architectView.addGestureRecognizer(gestureRecognizer)
}
func handleTap(gestureRecognizer: UIGestureRecognizer) {
let alert = UIAlertController(title: "Alert", message: "Message", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
The handleTap() function is a simple test to see if the taps are being recognized. This code does not trigger the UIAlert when it is pressed? What am I missing?
I tested your code here and it does work. However, I think you might be missing to add the UIGestureRecognizerDelegate protocol to your View Controller. See below:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var architectView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
architectView = UIView(frame: self.view.bounds)
self.view.addSubview(architectView)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: "handleTap:")
gestureRecognizer.delegate = self
architectView.addGestureRecognizer(gestureRecognizer)
}
func handleTap(gestureRecognizer: UIGestureRecognizer) {
let alert = UIAlertController(title: "Alert", message: "Message", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Swift 3 version code based on Raphael Silva answer:
import UIKit
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var architectView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
architectView = UIView(frame: self.view.bounds)
self.view.addSubview(architectView)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTap(gestureRecognizer:)))
gestureRecognizer.delegate = self
architectView.addGestureRecognizer(gestureRecognizer)
}
func handleTap(gestureRecognizer: UIGestureRecognizer) {
let alert = UIAlertController(title: "Alert", message: "Message", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}