I am trying to add an action to my custom view in my navigation bar. I have it showing up fine, but I can't figure out how to add a tap action to it.
I have tried adding a button inside the view and handling it there. I have tried making my navigation controller a delegate of the custom view, I have tried adding a tap gesture recognizer to the view in the nav controller. Nothing has worked. Any advice or feedback is greatly appreciated.
Thanks!
My custom Navigation Controller:
class MainNavVC: UINavigationController {
var loadStatus = LoadStatus()
override func viewDidLoad() {
super.viewDidLoad()
// load status
loadStatus.bounds = CGRect(x: -24, y: -6, width: 0, height: 0)
let loadStatusButton = UIBarButtonItem(customView: loadStatus)
self.viewControllers.last?.navigationItem.leftBarButtonItem = loadStatusButton
let tap = UITapGestureRecognizer(target: self, action: #selector(loadStatusPressed))
loadStatus.addGestureRecognizer(tap)
loadStatus.isUserInteractionEnabled = true
}
#objc func loadStatusPressed(recognizer: UIGestureRecognizer) {
print("tapped")
let alert = UIAlertController(title: "Load Status", message: "When you are under a load, you are being tracked by a shipper.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
My custom view:
class LoadStatus: UIView {
#IBOutlet var contentView: UIView!
private func commonInit(){
Bundle.main.loadNibNamed("LoadStatus", owner: self, options: nil)
addSubview(contentView)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder ) {
super.init(coder: aDecoder)
commonInit()
}
}
As of iOS11 you need to give your customview a width and height constraint, like:
loadStatus.widthAnchor.constraint(equalToConstant: 44).isActive = true
loadStatus.heightAnchor.constraint(equalToConstant: 44).isActive = true
Otherwise your customview may be visible but internally zerosized. It's a bug.
(I assume UINavigationController is now (iOS11+) constraint-based but not before!?)
Adding a tap gesture recognizer for a UIBarButtonItem seems to be kind of weird, by default, when creating a new UIBarButtonItem -programatically as shown in your code snippet- by using init(barButtonSystemItem:target:action:), you would be able to add the desired action for the bar button, example:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// like this:
let loadStatusButton = UIBarButtonItem(title: "Button Title", style: .plain, target: self, action: #selector(loadStatusPressed))
// ...
}
#objc func loadStatusPressed() {
print("tapped")
let alert = UIAlertController(title: "Load Status", message: "When you are under a load, you are being tracked by a shipper.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
And for the purpose of setting the custom view, you could simply add:
loadStatusButton.customView = loadStatus
Thus there is no need to add a tap gesture for a bar button.
Related
I'm trying to show a UIAlertController over a UIButton. The button is of type custom, and I have set the tint color to UIColor.clear, because I don't want any tint color. I've read that setting your UIButton to custom turns off the tint color, but that doesn't work for me. This method works out ok, until I show an alert controller on top.
This is before adding the alert:
This is what it looks like when I show the alert:
There's a clear grey tint color around the UIButton. It does this with any .selected UIButtons. How do I get rid of this tint? I really don't want to make my own button class just to get rid of this.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var starBtn: UIButton!
var selected: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
starBtn.isSelected = selected
starBtn.setImage(UIImage(named: "StarButtonSelected"), for: .selected)
starBtn.setImage(UIImage(named: "StarButtonDeselected"), for: .normal)
starBtn.isHighlighted = false
starBtn.tintColor = UIColor.clear
starBtn.backgroundColor = UIColor.clear
}
#IBAction func starBtnUp(_ sender: Any) {
selected = !selected
starBtn.isSelected = selected
}
#IBAction func showAlertBtnUp(_ sender: Any) {
let shareFileAction = UIAlertAction(title: "Share File", style: .default) { action in
}
let shareWebLink = UIAlertAction(title: "Share Link", style: .default) { action in
}
let alert = UIAlertController(title: "Share", message: "What do you want to share?", preferredStyle: .alert)
alert.addAction(shareFileAction)
alert.addAction(shareWebLink)
self.present(alert, animated: true) {
}
}
}
Edit: Okay, so if I create the button programmatically only, without using IB, it doesn't do the grey tint thing. I really don't want to have to not use IB though. Here's the re-written code that works how I want it to.
class ViewController: UIViewController {
var starBtn: UIButton!
var selected: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
starBtn = UIButton(type: .custom)
self.view.addSubview(starBtn)
starBtn.translatesAutoresizingMaskIntoConstraints = false
starBtn.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
starBtn.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
starBtn.heightAnchor.constraint(equalToConstant: 60).isActive = true
starBtn.widthAnchor.constraint(equalToConstant: 60).isActive = true
starBtn.isSelected = selected
starBtn.tintColor = UIColor.clear
starBtn.setImage(UIImage(named: "StarButtonSelected"), for: .selected)
starBtn.setImage(UIImage(named: "StarButtonDeselected"), for: .normal)
starBtn.isHighlighted = false
starBtn.backgroundColor = UIColor.clear
starBtn.addTarget(self, action: #selector(starBtnUp(_:)), for: .touchUpInside)
}
#IBAction func starBtnUp(_ sender: Any) {
selected = !selected
starBtn.isSelected = selected
}
#IBAction func showAlertBtnUp(_ sender: Any) {
let shareFileAction = UIAlertAction(title: "Share File", style: .default) { action in
}
let shareWebLink = UIAlertAction(title: "Share Link", style: .default) { action in
}
let alert = UIAlertController(title: "Share", message: "What do you want to share?", preferredStyle: .actionSheet)
alert.addAction(shareFileAction)
alert.addAction(shareWebLink)
self.present(alert, animated: true) {
}
}
}
So I have created this UIButton Sub-class. It's a reusable component and can be added in any view controller. I need to present an alert view controller with the tap in the button. So basically I need to find out in which view controller the button lies so I can pass a view controller as a parameter.
final class ProfileButton: UIButton {
let picker = UIImagePickerController()
lazy var shadowImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "download")
return imageView
}()
override public init(frame: CGRect) {
super.init(frame: frame)
setupSelf()
self.addTarget(self, action: #selector(profileButtonTapped), for: .touchUpInside)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override public func layoutSubviews() {
super.layoutSubviews()
shadowImageView.layer.cornerRadius = self.frame.width/2.70
shadowImageView.layer.masksToBounds = true
}
#objc func profileButtonTapped(){
imageHandler(presenter: )
}
#objc func imageHandler(presenter: UIViewController!){
let alertController = UIAlertController(title: "Profile Picture", message: "Please select your profile picture", preferredStyle: .alert)
let okAction = UIAlertAction(title: "Photo Library", style: .default, handler: { (action: UIAlertAction!) in
print("I am working")
})
let cameraAction = UIAlertAction(title: "Camera", style: .default, handler: { (action: UIAlertAction!) in
print("I am working")
})
alertController.addAction(okAction)
alertController.addAction(cameraAction)
presenter.present(alertController, animated: true, completion: nil)
}
I actually need to find out the view controller so I can pass it in the imageHandler(presenter: UIViewController) function.
In this case you can get the top controller using this extension
extension UIApplication {
class func getTopMostViewController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
} else {
return nil
}
}
}
#objc func imageHandler() {
//...
UIApplication.getTopMostViewController()?.present(alertController, animated: true, completion: nil)
}
You could add a weak var reference to the controller inside your UIButton. Better way would be add the handler function in the UIViewController extension and addTarget in the viewDidLoad instead of the UIButton's init(frame:). Here's an example:
final class ProfileButton: UIButton {
//Remove addTarget from init(frame:)
}
class ViewController: UIViewController {
let profileButton = ProfileButton()
override func viewDidLoad() {
super.viewDidLoad()
//...
profileButton.addTarget(self, action: #selector(imageHandler), for: .touchUpInside)
}
}
extension UIViewController {
#objc func imageHandler() {
let alertController = UIAlertController(title: "Profile Picture", message: "Please select your profile picture", preferredStyle: .alert)
let okAction = UIAlertAction(title: "Photo Library", style: .default, handler: { (action: UIAlertAction!) in
print("I am working")
})
let cameraAction = UIAlertAction(title: "Camera", style: .default, handler: { (action: UIAlertAction!) in
print("I am working")
})
alertController.addAction(okAction)
alertController.addAction(cameraAction)
present(alertController, animated: true, completion: nil)
}
}
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 UIAlertController of action sheet style, it has 2 normal actions and one cancel action, I wanted to have different colors of text for them and cus of this, I subclassed UIAlertController:
class CustomAlertViewController: UIAlertController {
internal var cancelText: String?
private let fontRegular = UIFont(name: "IRANSans", size: 16)
private let fontBold = UIFont(name: "IRANSans-Bold", size: 16)
override func viewDidLoad() {
super.viewDidLoad()
self.view.tintColor = UIColor.black
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.findLabel(scanView: self.view)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.findLabel(scanView: self.view)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.findLabel(scanView: self.view)
}
func findLabel(scanView: UIView!) {
if (scanView.subviews.count > 0) {
for subview in scanView.subviews {
if let label: UILabel = subview as? UILabel {
if (self.cancelText != nil && label.text == self.cancelText!) {
DispatchQueue.main.async {
label.tintColor = UIColor.red
label.highlightedTextColor = UIColor.green
label.font = self.fontBold
}
} else {
label.font = self.fontRegular
label.tintColor = UIColor(rgb: 0x2699FB)
label.highlightedTextColor = UIColor.black
}
}
self.findLabel(scanView: subview)
}
}
}
} // class end
so now normal labels has blue color and cancel label has red color. but when I select them, I don't want them to highlight as same color. this is where problem accurs, it seems that the labels highlight as UIAlertController's view.tintColor. does anyone know how can I do what I want? I mean how can I define my own highlight color for different labels?
Jafar Khoshtabiat, see below code for display red text in action sheet, I hope it's helps you. see following link for more details https://nshipster.com/uialertcontroller/
//UIActionSheet
let actionSheet = UIActionSheet(title: "Takes the appearance of the bottom bar if specified; otherwise, same as UIActionSheetStyleDefault.", delegate: self, cancelButtonTitle: "Cancel", destructiveButtonTitle: "Destroy", otherButtonTitles: "OK")
actionSheet.actionSheetStyle = .Default
actionSheet.showInView(self.view)
// MARK: UIActionSheetDelegate
func actionSheet(actionSheet: UIActionSheet, clickedButtonAtIndex buttonIndex: Int) {
switch buttonIndex {
...
}
}
//UIAlertController
let alertController = UIAlertController(title: nil, message: "Takes the appearance of the bottom bar if specified; otherwise, same as UIActionSheetStyleDefault.", preferredStyle: .ActionSheet)
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel) { (action) in
// ...
}
alertController.addAction(cancelAction)
let OKAction = UIAlertAction(title: "OK", style: .Default) { (action) in
// ...
}
alertController.addAction(OKAction)
let destroyAction = UIAlertAction(title: "Destroy", style: .Destructive) { (action) in
println(action)
}
alertController.addAction(destroyAction)
self.presentViewController(alertController, animated: true) {
// ...
}
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.
}
}