How to pass a selector function as a paramater in Swift? - objective-c

I have a standard View Controller that has the following selector:
#objc func finished() {
//Do Something here
}
I have an outside class called ButtonFactory that looks something like this:
class ButtonFactory {
static let shared = ButttonFactory()
func createButton(functionParameter: () -> Void ) -> UIButton {
let button = UIButton()
button.addTarget(self, action: #selector(functionParameter), for: .touchUpInside)
return button
}
}
In my view controller I call createButton like this:
ButtonFactory.shared.createButton(functionParamter: self.finished)
However the above code does not compile saying: Argument of '#selector' cannot refer to parameter 'functionParameter'.
Is it possible to pass a selector function as a parameter in Swift 5?

You can pass your function using a Selector:
class ViewController: UIViewController {
#objc func finished() {
//Do Something here
}
}
class ButtonFactory {
static let shared = ButtonFactory()
func createButton(_ action: Selector) -> UIButton {
let button = UIButton()
button.addTarget(self, action: action, for: .touchUpInside)
return button
}
}
let button = ButtonFactory.shared.createButton(#selector(ViewController.finished))
Note that you should pass the view controller instance as well to your method instead of setting the target to your ButtonFactory shared instance.

Related

Swift and #objc methods: How do I transform a method so that it can be represented by #objc?

As Swift is my first programming language and also seeing that I have no Objective C experience...
I'm having difficulty understanding #objc in relation to methods.
How do I use the #objc syntax to conform to my methods?
Is there another way to select a method without using the #selector syntax?
Here is the code that I'm having difficulty with(mainly the #objc attempt at the startGame method):
import UIKit
#objc class ViewController: UITableViewController {
var allWords = [String]()
var usedWords = [String]()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem =
UIBarButtonItem(barButtonSystemItem: .add, target: self, action:
#selector(promptForAnswer))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "New
Word", style: .plain, target: self, action: #selector(startGame))
if let startWordsURL = Bundle.main.url(forResource: "start",
withExtension: "txt") {
if let startWords = try? String(contentsOf: startWordsURL) {
allWords = startWords.components(separatedBy: "\n")
}
}
if allWords.isEmpty {
allWords = ["silkworm"]
}
#objc func startGame() {
title = allWords.randomElement()
usedWords.removeAll(keepingCapacity: true)
tableView.reloadData()
{
startGame()
}
A few observations:
You do not need #objc in your view controller declaration.
The two action/selector methods should bear #objc qualifier.
I would suggest that you give these two methods descriptive names that clearly indicate that they are called when the user taps on a particular button, e.g.:
#objc func didTapNewWord(_ sender: UIBarButtonItem) {
...
}
#objc func didTapAdd(_ sender: UIBarButtonItem) {
...
}
Note, I also added a parameter to these methods. That makes it entirely unambiguous that they are button handlers. You do not need to do that, but now you can glance at the code and immediately grok what the method is for.
Obviously, you will change the code that adds these target actions accordingly:
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
target: self,
action: #selector(didTapAdd(_:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "New Word",
style: .plain,
target: self,
action: #selector(didTapNewWord(_:)))
Be careful with the placement of braces. Swift allows you to declare functions inside functions. So make sure that these selector methods are instance methods of the view controller, and not, for example, private functions declared inside another function (i.e. viewDidLoad).
If you start to lose track of the braces, you can select all the code in this file and press control+i (or in Xcode menus, “Editor” » “Structure” » “Re-Indent”). If you have missing braces somewhere, the re-indentation of the code will make this jump out at you.
So pulling that together, you get something like:
// ViewController.swift
import UIKit
class ViewController: UITableViewController {
var allWords = [String]()
var usedWords = [String]()
override func viewDidLoad() {
super.viewDidLoad()
configureButtons()
fetchData()
}
}
// MARK: - Actions
extension ViewController {
#objc func didTapNewWord(_ sender: UIBarButtonItem) {
startGame()
}
#objc func didTapAdd(_ sender: UIBarButtonItem) {
...
}
}
// MARK: - UITableViewDataSource
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
}
}
// MARK: - Private utility methods
private extension ViewController {
func configureButtons() {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
target: self,
action: #selector(didTapAdd(_:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "New Word",
style: .plain,
target: self,
action: #selector(didTapNewWord(_:)))
}
func fetchData() {
guard
let startWordsURL = Bundle.main.url(forResource: "start", withExtension: "txt"),
let startWords = try? String(contentsOf: startWordsURL).components(separatedBy: "\n"),
!startWords.isEmpty
else {
allWords = ["silkworm"]
return
}
allWords = startWords.filter { !$0.isEmpty }
}
func startGame() {
title = allWords.randomElement()
usedWords.removeAll(keepingCapacity: true)
tableView.reloadData()
}
}
A few final observations on my code sample (not directly related to your question, but just to explain why structured it like I did):
I like to put methods into extensions, so that they are in logical groups. This makes it easier to follow what is going on at a glance. You can also collapse/expand these extensions so that while you are editing, you can focus on the relevant code.
The MARK comments just puts nice section headers in the Xcode jump bar, again, making it easier to jump about in one’s code.
I personally don't put anything in the action methods except a call to some method with the “business logic”. This separates the “view” code (the handling of the button) from the business logic. Some day, you may start using view models or presenter objects, so embracing this separation of responsibilities now will make that eventual transition easier. It will also make it easier to write unit tests when you get around to that (e.g. you write unit tests for the "start game" logic, not not the tapping of a button).
I think you have syntax error in the #objc method. It should be:
#objc
func functionName() {
}
for you it will be:
#objc
func startGame() {
title = allWords.randomElement()
usedWords.removeAll(keepingCapacity: true)
tableView.reloadData()
}

FinderSync context menu items beep, never call action

I want to generate a menu which will call functions on a class other than that one which generated the menu, and then place that in Finder. However, when I click the menu item, the system error beep plays and my function is never called.
Here's a SSCCE:
import Cocoa
import FinderSync
#objc(FinderSync)
class FinderSync: FIFinderSync {
var observedFolder = URL(fileURLWithPath: "/")
override init() {
super.init()
NSLog("\(FinderSync.self.className()) launched from \(Bundle.main.bundlePath)")
// Set up the directory we are syncing.
FIFinderSyncController.default().directoryURLs = [self.observedFolder]
}
// MARK: - Menu and toolbar item support
override var toolbarItemName: String {
return "Wonderful Test App"
}
override var toolbarItemToolTip: String {
return "This is wonderful"
}
override var toolbarItemImage: NSImage {
return NSImage(named: NSImage.cautionName)!
}
override func menu(for menuKind: FIMenuKind) -> NSMenu {
let menu = NSMenu(title: "")
let menuItem = NSMenuItem(title: "Click me!", action: #selector(SomeOtherClass.remoteAction), keyEquivalent: "")
menuItem.target = SomeOtherClass.shared
menuItem.action = #selector(SomeOtherClass.remoteAction)
menu.addItem(menuItem)
return menu
}
}
#objc(SomeOtherClass)
public class SomeOtherClass: NSObject {
public static let shared = SomeOtherClass()
deinit {
NSLog("Deallocated!")
preconditionFailure("Shared instance should never be deallocated!")
}
#IBAction
#objc(remoteAction:)
public func remoteAction(_ sender: Any?) {
NSLog("Remote!")
}
}
I've verified via the memory debugger that SomeOtherClass.shared is still in memory before, during, and after the menu item is clicked, so it's not being deallocated or anything.
It appears you can't add actions which are within any class other than your FinderSync extension's principal class. Which is about as stupid as everything else about NSMenuItem, so I'm not surprised.
So, you'll have to move the action(s) into your FinderSync class, despite how ugly that might be for organization.

How do I check the currently loaded view controller ? (ObjC + Swift)

In my app I have a function where I want it to print something depending on the current view controller that is loaded. I do this by setting a global variable (Bool) and then toggling the flags in the view controller classes. From my main class I have something like this:
var FirstViewControllerisVisible: Bool = false
var SecondViewControllerisVisible: Bool = false
var ThirdViewControllerisVisible: Bool = false
#objc func PlayAgainfunc(_ sender: Any) {
if counter % 15 == 0 {
if FirstViewControllerisVisible == true {
print("First View Controller is visible")
} else if SecondViewControllerisVisible == true {
print("Second View Controller is visible")
} else if ThirdViewControllerisVisible == true {
print("Third View Controller is visible")
}
}
counter += 1
}
Then, in those classes I can set the flags like this:
override func viewDidAppear(_ animated: Bool) {
FirstViewControllerisVisible = true
}
override func viewDidDisappear(_ animated: Bool) {
FirstViewControllerisVisible = false
}
This worked great when it was exclusively Swift, but the problem with global variables is that they can't be accessed by Objective-C. I can't find a way to set flags in my ObjC classes and then check if true or false. For instance, If I tried:
- (void)viewDidAppear:(BOOL)animated; {
[(FirstViewControllerisVisible) == true];
}
- (void)viewDidDisappear:(BOOL)animated; {
[(FirstViewControllerisVisible) == false];
}
I would get the error 'use of undeclared identifier' because my global vars declared in Swift are Swift only.
After the view controller has loaded, you can
if let viewController = UIApplication.shared.keyWindow?.rootViewController {
print(type(of: viewController))
}
add below mentioned extension. you will get the desired result.
extension UIApplication {
/// will return currently showing view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
and access it like:
let viewController = UIApplication.topMostViewController
Happy Coding

setStatusBarHidden is deprecated in iOS 9.0

I am upgrading my code from iOS 8 to iOS 9. I have a code snippet in my program
[[UIApplication applicationName] setStatusBarHidden:YES];.
I am getting the warning "setStatusBarHidden is deprecated in iOS 9.0, Use -[UIViewController prefersStatusBarHidden". If I just replace 'setStatusBarHidden' with 'prefersStatusBarHidden', I get 'instance method not found'.
Can someone please suggest me how to solve this problem?
Add below code to your view controller..
- (BOOL)prefersStatusBarHidden {
return NO;
}
Note :
If you change the return value for this method, call the
setNeedsStatusBarAppearanceUpdate method.
For childViewController, To specify that a child view controller
should control preferred status bar hidden/unhidden state, implement
the childViewControllerForStatusBarHidden method.
prefersStatusBarHidden is available from iOS 7+.
Use this in Your UIViewController class
var isHidden = true{
didSet{
self.setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
return isHidden
}
If you change the return value for this method, call the
setNeedsStatusBarAppearanceUpdate() method. To specify that a child
view controller should control preferred status bar hidden/unhidden
state, implement the childViewControllerForStatusBarHidden method.
you have to add method in yourViewController.m
- (BOOL)prefersStatusBarHidden {
return NO;
}
Swift 3.1 Xcode 8.2.1
Change in info.plist the row View controller-based status bar appearance and set it to NO
In your target settings tick "Hide Status bar"
Both steps are required
Here is my swift code for setting status bar hidden and style.
extension UIViewController {
public var privateStatusBarHidden: Bool {
return statusBarHidden
}
public var privateStatusBarStyle: UIStatusBarStyle {
return statusBarStyle
}
public func setStatusBarHidden(hidden: Bool, animated: Bool = false) {
statusBarHidden = hidden
if animated {
UIView.animate(withDuration: 0.25, animations: {
self.setNeedsStatusBarAppearanceUpdate()
})
} else {
self.setNeedsStatusBarAppearanceUpdate()
}
}
public func setStatusBar(style: UIStatusBarStyle) {
statusBarStyle = style
self.setNeedsStatusBarAppearanceUpdate()
}
public static func swizzleStatusBarHiddenPropertyForViewController() {
var original = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.prefersStatusBarHidden))
var changeling = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.privateStatusBarHidden))
method_exchangeImplementations(original, changeling)
original = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.preferredStatusBarStyle))
changeling = class_getInstanceMethod(UIViewController.self, #selector(getter: UIViewController.privateStatusBarStyle))
method_exchangeImplementations(original, changeling)
original = class_getClassMethod(UIViewController.self, #selector(UIViewController.swizzleStatusBarHiddenPropertyForViewController))
changeling = class_getClassMethod(UIViewController.self, #selector(UIViewController.emptyFunction))
method_exchangeImplementations(original, changeling)
}
#objc private static func emptyFunction() {}
}
Usage
in lauching function
UIViewController.swizzleStatusBarHiddenPropertyForViewController()
for hide/show statusBar, in UIViewController
. self.setStatusBar(hidden: true/false)
Swift 3 with Xcode 8.3.3
1) Add a row in you Info.plist.
2) In your ViewController ViewDidLoad() override add:
UIApplication.shared.isStatusBarHidden = true

Swift unique property in multiple instances

I know that theoretically it's possible to create multiple instances of the same class with a property that would have a different value for each instance.
The thing is, I can't make it happen.
Each time I'm creating a new instance, it gets the property's value of the other instances, and when I'm changing one value for an instance, it changes the other's too.
So my guess is that I'm doing something wrong (obviously), like accessing the class property value instead of the instance property value... Here's the code.
class CustomUIImageView: UIImageView {
var someParameter: Bool = false // This is the property I want to be different in each version of the instance.
}
class ClassSiege: UIViewController, UIGestureRecognizerDelegate {
var myView: CustomUIImageView! //the instance declaration.
// I use this gesture recognizer to find out the value of the instance I'm tapping on.
func handleTap (sender: UITapGestureRecognizer) {
print("value of someParameter \(self.myView.someParameter)")
}
func handlePan(recognizer: UIPanGestureRecognizer) {
let iv: UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x // 'Blackboard' is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
if centerRondY - centerBoardY < 100 {
self.myView.someParameter = true // If the distance between myView and the blackboard is under 100 I want the instance's property to become true.
} else {
self.myView.someParameter = false // On the other hand, if the distance is greater than 100, I want it to be false.
}
}
// When the user pushes a button, it triggers this method that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
// some code...
// Here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}
}
So clearly that's not the good way to do it, but what's wrong, and how can it be solved?
Basing my answer on your related question.
If what you want to achieve is initializing a property with a value that you provide, just add a new parameter to the initializer. If for instance you are using the initializer with a CGRect passed in, then you can implement an initializer like this:
class CustomUIImageView : UIImageView {
let someParameter : Bool
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(frame: CGRect, someParameter: Bool) {
self.someParameter = someParameter
super.init(frame: frame)
}
}
I hope that this is what you are looking for - let me know otherwise.
I've found the solution, and if you've been facing the same issu, here's how to deal with it.
The secret is to downcast the recognizer.view to take the parameter of the subclass CustomUIImageView.
here's how :
func handleTap (sender: UITapGestureRecognizer) {
println("value of someParameter \(self.myView.someParameter)") //I use this gesture recognizer to find out the value of the instance I'm tapping on.
}
func handlePan(recognizer:UIPanGestureRecognizer) {
let iv : UIView! = recognizer.view
let translation = recognizer.translationInView(self.view)
iv.center.x += translation.x
iv.center.y += translation.y
recognizer.setTranslation(CGPointZero, inView: self.view)
var centerBoardX = BlackBoard.center.x //blackboard is a fixed image on the screen.
var centerBoardY = BlackBoard.center.y
var centerRondX = iv.center.x
var centerRondY = iv.center.y
var myParameter = recognizer.view as CustomUIImageView //<- this is the key point. Downcasting let you access the custom subclass parameters of the object that is currently moved
if centerRondY - centerBoardY < 100 {
myParameter.someParameter = true //so now I'm really changing the parameter's value inside the object rather than changing a global var like I did before.
} else {
myParameter.someParameter = false
}
}
//when user pushes a button, it triggers this func that creates a new instance of myView and add it to the screen.
#IBAction func showContent(sender: AnyObject) {
some code...
//here I'm creating the instance of the view and I give it the gesture recognizer parameters. I don't think that relevant to the issue, so I'm not adding the code.
}