How to use a selector from another class? - swift

I have a Cocoa Touch Framework named FooFramework.
Within it, I want to manage the move up on the Y axis for selected views when the keyboard shows. I created a KeyboardManager class. Here's how it looks:
import UIKit
public class KeyboardManager {
var notifyFromObject: Any?
var observer: Any
public var viewsToPushUp: [UIView] = []
public init(observer: Any, viewsToPushUp: [UIView], notifyFromObject: Any? = nil) {
self.observer = observer
self.notifyFromObject = notifyFromObject
self.viewsToPushUp = viewsToPushUp
}
public func pushViewsUpWhenKeyboardWillShow(){
let notificationCenter = NotificationCenter.default
print(self)
notificationCenter.addObserver(self.observer, selector: #selector(FooFramework.KeyboardManager.pushViewsUp), name: NSNotification.Name.UIKeyboardWillShow, object: notifyFromObject)
}
#objc public func pushViewsUp(notification: NSNotification) {
if let keyboardRectValue = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight = keyboardRectValue.height
for view in viewsToPushUp {
view.frame.origin.y -= keyboardHeight
}
}
}
}
Then, I import this FooFramework in an iOS app named Bar. To test the FooFramework, I want to push up a UITextField. Here's the code:
import UIKit
import FooFramework
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let kb = KeyboardManager(observer: self, viewsToPushUp: [textField], notifyFromObject: nil)
kb.pushViewsUpWhenKeyboardWillShow()
}
func pushViewsUp(notification: NSNotification) {
print("This should not be printed")
}
}
My problem is that This should not be printed appears in the console and the pushViewsUp method from the KeyboardManager never gets called. Even though I used a fully qualified name for the selector, it insists on using the pushViewsUp from the ViewController. This is driving me nuts.
If I remove pushViewsUp from the ViewController, I get the following error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Bar.ViewController pushViewsUpWithNotification:]: unrecognized selector sent to instance 0x7fc540702d80'
What do I need to do so the selector properly points to FooFramework.KeyboardManager.pushViewsUp?

I believe you need to use self instead of self.observer for the observer in the addObserver function.
Also you need to declare the kb variable outside the scope of the function in order for the manager to detect the notification.
Example:
import UIKit
import FooFramework
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
var kb: KeyboardManager?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
kb = KeyboardManager(observer: self, viewsToPushUp: [textField], notifyFromObject: nil)
kb?.pushViewsUpWhenKeyboardWillShow()
}
}
KeyboardManager changes:
public func pushViewsUpWhenKeyboardWillShow() {
let notificationCenter = NotificationCenter.default
print(self)
notificationCenter.addObserver(self,
selector: #selector(FooFramework.KeyboardManager.pushViewsUp),
name: NSNotification.Name.UIKeyboardWillShow,
object: notifyFromObject)
}

Other than what Justin has suggested which is all correct, there are a few more things to consider before you fully solve the problem.
KeyBoardManager class instance itself is going to observe the keyboardWillMoveUp notification so your
var observer: Any
within it is unnecessary. You should remove that.
I would also put the addObserver part right in the init of KeyBoardManager class itself so that this extra call pushViewsUpWhenKeyboardWillShow() can be avoided which seems to be doing nothing but that. Since the KeyBoardManager class is supposed to be doing only this, I don't see why adding observer should be another function call.
So this is how your KeyboardManager class should look:
import UIKit
public class KeyboardManager {
var notifyFromObject: Any?
public var viewsToPushUp: [UIView] = []
public init(viewsToPushUp: [UIView], notifyFromObject: Any? = nil){
self.notifyFromObject = notifyFromObject
self.viewsToPushUp = viewsToPushUp
//remember KeyboardManager itself is observing the notifications and moving the views it received from the ViewController. Hence we use self.
NotificationCenter.default.addObserver(self, selector: #selector(FooFramework.KeyboardManager.pushViewsUp), name: NSNotification.Name.UIKeyboardWillShow, object: notifyFromObject)
}
#objc public func pushViewsUp(notification: NSNotification) {
if let keyboardRectValue = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
let keyboardHeight = keyboardRectValue.height
for view in viewsToPushUp {
view.frame.origin.y -= keyboardHeight
}
}
}
}
You will also need to work with the frames properly before you get the right behavior out of this.
You should extend the lifespan of your KeyboardManagerInstance to live as long as the ViewController which has the textField is alive. You do it by declaring it as an instance variable inside the ViewController as Justin has suggested. The way you were doing it, your KeyboardManager instance is a local variable which is created and immediately released as soon as the function goes out of scope. To verify this, you can add this to your KeyboardManager class and check:
deinit {
print("KeyboardManager is perhaps dying an untimely death.")
}
Finally your ViewController class should do just this
import UIKit
import FooFramework
class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
var kb: KeyboardManager?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
kb = KeyboardManager(viewsToPushUp: [textField], notifyFromObject: nil)
//observe that there is no observer param in the initializer and also no "pushViewsUpWhenKeyboardWillShow" call as that behavior has already been moved to init itself.
}
}

Related

Access to ViewController outlets from AppDelegate

I created an outlet in ViewController class and I'd like to modify it.
In the ViewController.swift file I have
import Cocoa
class ViewController: NSViewController {
#IBOutlet var LabelText: NSTextFieldCell?
override func viewDidLoad() {
super.viewDidLoad()
}
//other things
}
I'd like to change the background color of the label. How can I do that from AppDelegate?
At first I thought I could solve this problem using a function in ViewController and calling it in AppDelegate
func changeBackground() {
LabelText.textColor = NSColor.red
}
But soon I realised that it wasn't possible unless I used a static function. Then I tried to modify the code in ViewController like that
static func changeBackground() {
LabelText.textColor = NSColor.red
}
and call this function in AppDelegate like that
ViewController.changeBackground()
In this way I can access to changeBackground() function from AppDelegate, but in ViewController it gives me an error: Instance member 'LabelText' cannot be used on type 'ViewController'
I understood that this cannot be possible because somehow I'm calling "LabelText" before it's initialised (or something like that).
I don't know much about Swift and I'm trying to understand how it works. I've been searching for the answer to my question for hours, but still I don't know how to solve this.
Solution
As Rob suggested, the solution is to use NotificationCenter.
A useful link to understand how it works: https://www.appypie.com/notification-center-how-to-swift
Anyway, here how I modified the code.
In ViewController:
class ViewController: NSViewController {
#IBOutlet var label: NSTextFieldCell!
let didReceiveData = Notification.Name("didReceiveData")
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(onDidReceiveData(_:)), name: didReceiveData, object: nil)
super.viewDidLoad()
}
#objc func onDidReceiveData(_ notification: Notification) {
label.textColor = NSColor.red
}
}
And then, in AppDelegate:
let didReceiveData = Notification.Name("didReceiveData")
NotificationCenter.default.post(name: didReceiveData, object: nil)

Swift: Calling the same method from two different buttons

When I tap the orangeButtonOne in the left corner of the screen, my collectionView appears, and when I tap the orangeButtonOne again the collectionView disappears. This works fine but now... the collectionView as you can see also holds a lot of buttons and when I press one of them it calls the exact same method as orangeButtonOne that is closeDropDownView. However, when I tap a button in the collectionView the app crashes on line 99.. and I get the following error:
class TestViewController: UIViewController {
#IBOutlet weak var dropDownView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
dropDownView.isHidden = true
func closeDropDownView() {
UIView.animate(withDuration: 0.3, delay: 0, options: .curveLinear, animations: {
99 var dropTopFrame = self.dropDownView.frame <THREAD1: FATAL ERROR: UNEXPECTEDLY FOUND NIL WHILE IMPLICITLY UNWRAPPING AN OPTIONAL VALUE
var dropBottomFrame = self.dropDownView.frame
dropTopFrame.origin.y += dropBottomFrame.size.height
dropBottomFrame.origin.y -= dropTopFrame.size.height
self.dropDownView.frame = dropTopFrame
self.dropDownView.frame = dropBottomFrame
UIView.animate(withDuration: 0.5) {
self.dimView.alpha = 0
}
}, completion: { finished in
self.dropDownView.isHidden = true
print("dropView closed!")
})
}
}
I don't understand how this method works fine when the orangeButtonOne calls it but then when a collectionView button calls this method the frame value is suddenly nil?
Here is how each button calls the closeDropDownView method:
class TestViewController: UIViewController {
//viewDidLoad etc
#IBAction func orangeButtonOneTapped(_ sender: Any) {
if (dropDownView.isHidden == true ) {
openDropDownView()
}
else { closeDropDownView() }
}
extension DropDownViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
let testVC = TestViewController()
#objc func CVButtonTapped(sender: UIButton!) {
print("button tapped")
testVC.closeDropDownView()
}
}
You're getting a crash because you are creating a new instance of TestViewController in DropDownViewController and calling closeDropDownView method on that particular instance(testVC). In a new instance of TestViewController the #IBOutlet var dropDownView is empty when you just initialize it and hence you get a crash. To avoid the crash you need to pass the same instance of TestViewController to DropDownViewController to call the exact same instance of closeDropDownView to get the same functionality instead of a crash.

views are merged instead of showing them separately

I have two xib file, one which shows login view and another which shows the steps what to do after the login is successful. I am having hard time to make it work. I have created macos project not ios and using safariservices so that it will work for the safari extension either.
Here is what i have done
import SafariServices
class SafariExtensionViewController: SFSafariExtensionViewController {
#IBOutlet weak var passwordMessage: NSTextField!
#IBOutlet weak var emailMessage: NSTextField!
#IBOutlet weak var message: NSTextField!
#IBOutlet weak var email: NSTextField!
#IBOutlet weak var password: NSSecureTextField!
static let shared = SafariExtensionViewController()
override func viewDidLoad() {
self.preferredContentSize = NSSize(width: 300, height: 250)
message.stringValue = ""
emailMessage.stringValue = ""
passwordMessage.stringValue = ""
}
override func viewDidAppear() {
if let storedEmail = UserDefaults.standard.object(forKey: "email") as? String {
if let stepView = Bundle.mainBundle.loadNibNamed(NSNib.Name(rawValue: "ExtensionStepsViewController"), owner: nil, topLevelObjects: nil)[0] {
self.view.addSubview(stepView)
}
}
}
#IBAction func userLogin(_ sender: Any) {
let providedEmailAddress = email.stringValue
let providedPassword = password.stringValue
let isEmailAddressValid = isValidEmailAddress(emailAddressString: providedEmailAddress)
self.message.stringValue = ""
emailMessage.stringValue = ""
passwordMessage.stringValue = ""
if isEmailAddressValid && providedPassword.count > 0 {
/* login process is handled here and store the email in local storage /*
/* TODO for now email is not stored in browser localstorage which has to be fixed */
let controller = "ExtensionStepsViewController"
let subview = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: controller), bundle: nil)
self.view.addSubview(subview.view)
}
}
}
This way i get error like Type Bool has no subscript members my file structure looks something like this.
SafariExtensionViewController.xib (main one which is shown initially
with login screen)
SafariExtensionViewController.swift
ExtensionStepsViewController.xib(this view should be shown when user
is logged in instead of login screen)
ExtensionStepsViewController.swift
I am using xcode 10, swift 4, everything new.
UPDATE
I used the following block both in viewDidAppear(if there is email in localstorage then show extension steps view instead of login screen) and inside login function when the login is success but it does not navigate to that ExtensionStepsView
let controller = "ExtensionStepsViewController"
let subview = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: controller), bundle: nil)
self.view.addSubview(subview.view)
Use case is show login at initial but if user is logged in then show another view but issue is now the view are merged
You got the error "Type Bool has no subscript members" because loadNibNamed(_:owner:topLevelObjects:) method of Bundle returns Bool struct that has no subscript members so you can't write like
true[0]
How to use this method correctly see the link and example from there:
var topLevelObjects : NSArray?
if Bundle.main.loadNibNamed(NSNib.Name(rawValue: "ExtensionStepsViewController"), owner: self, topLevelObjects: &topLevelObjects) {
let topLevelObjects!.first(where: { $0 is NSView }) as? NSView
}
Views were merged because you didn't remove previous views from the superview and added view from ExtensionStepsViewController to the same superview.
You can do the following steps to complete your issue:
Make SafariExtensionViewController inherited from SFSafariExtensionViewController that will be container (and parent) for two child view controllers such as LoginViewController and ExtensionStepsViewController and will be used to navigate between ones.
Make separately LoginViewController and ExtensionStepsViewController (both inherited from simple NSViewController) and its xibs.
Right after user logins transit from LoginViewController to ExtensionStepsViewController
As an example but instead of ParentViewController you have to use your implementation SafariExtensionViewController as I explain above in the first step.
public protocol LoginViewControllerDelegate: class {
func loginViewControllerDidLoginSuccessful(_ loginVC: LoginViewController)
}
public class LoginViewController: NSViewController {
weak var delegate: LoginViewControllerDelegate?
#IBAction func login(_ sender: Any) {
// login logic
let isLoginSuccessful = true
if isLoginSuccessful {
self.delegate?.loginViewControllerDidLoginSuccessful(self)
}
}
}
public class ExtensionStepsViewController: NSViewController {
}
public class ParentViewController: NSViewController, LoginViewControllerDelegate {
weak var login: LoginViewController! // using of force unwrap is anti-pattern. consider other solutions
weak var steps: ExtensionStepsViewController!
public override func viewDidLoad() {
let login = LoginViewController(nibName: NSNib.Name(rawValue: "LoginViewController"), bundle: nil)
login.delegate = self
// change login view frame if needed
login.view.frame = self.view.frame
self.view.addSubview(login.view)
// instead of setting login view frame you can add appropriate layout constraints
self.addChildViewController(login)
self.login = login
let steps = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: "ExtensionStepsViewController"), bundle: nil)
steps.view.frame = self.view.frame
self.addChildViewController(steps)
self.steps = steps
}
// MARK: - LoginViewControllerDelegate
public func loginViewControllerDidLoginSuccessful(_ loginVC: LoginViewController) {
self.transition(from: self.login, to: self.steps, options: .slideLeft) {
// completion handler logic
print("transition is done successfully")
}
}
}
Here is a swift playground with this example.
UPD:
You can instantiate NSViewController in several ways:
Use NSStoryboard that allows to load view of NSViewController from .storyboard file:
let storyboard = NSStoryboard(name: NSStoryboard.Name("NameOfStoryboard"), bundle: nil)
let viewController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("NSViewControllerIdentifierInStoryboard"))
Use appropriate initialiser of NSViewController to load view of it from .xib file:
let steps = ExtensionStepsViewController(nibName: NSNib.Name(rawValue: "ExtensionStepsViewController"), bundle: nil)
Use default initialiser but you have to load view directly by overriding loadView() method if name of xib file is different from name of view controller class:
let steps = ExtensionStepsViewController()
// Also you have to override loadView() method of ExtensionStepsViewController.

Listening for NSWorkspace Notifications in Swift 4

The simple Swift 4 example below should stop when the computer's display goes to sleep.
class Observer {
var asleep = false
func addDNC () {
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: nil, using: notificationRecieved)
}
func notificationRecieved (n: Notification) {
asleep = true
}
}
let observer = Observer ()
observer.addDNC ()
while (!observer.asleep) {}
print ("zzzz")
However, the program gets stuck in the while loop. What am I doing wrong, and what is the proper way to wait for a Notification?
I have tried using a selector (#selector (notificationRecieved), with #objc in the function declaration, of course), to no avail.
Start a template app in Xcode and modify the ViewController.swift to do this:
import Cocoa
class Observer {
var asleep = false
func addDNC () {
NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.screensDidSleepNotification, object: nil, queue: nil, using: notificationRecieved)
}
func notificationRecieved (n: Notification) {
print("got sleep notification!")
asleep = true
}
}
class ViewController: NSViewController {
let observer = Observer ()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
observer.addDNC ()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
The difference between your code and mine is that I'm not doing the wacky sleepy polling thing you're doing (that's going to lead to a spinning pizza cursor), and I'm also setting observer to be a property off the ViewController object, so the observer property sticks around as long as the view controller does.

NSComboBox getGet value on change

I am new to OS X app development. I manage to built the NSComboBox (Selectable, not editable), I can get it indexOfSelectedItem on action button click, working fine.
How to detect the the value on change? When user change their selection, what kind of function I shall use to detect the new selected index?
I tried to use the NSNotification but it didn't pass the new change value, always is the default value when load. It is because I place the postNotificationName in wrong place or there are other method should use to get the value on change?
I tried searching the net, video, tutorial but mostly written for Objective-C. I can't find any answer for this in SWIFT.
import Cocoa
class NewProjectSetup: NSViewController {
let comboxRouterValue: [String] = ["No","Yes"]
#IBOutlet weak var projNewRouter: NSComboBox!
#IBAction func btnAddNewProject(sender: AnyObject) {
let comBoxID = projNewRouter.indexOfSelectedItem
print(“Combo Box ID is: \(comBoxID)”)
}
#IBAction func btnCancel(sender: AnyObject) {
self.dismissViewController(self)
}
override func viewDidLoad() {
super.viewDidLoad()
addComboxValue(comboxRouterValue,myObj:projNewRouter)
self.projNewRouter.selectItemAtIndex(0)
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(
self,
selector: “testNotication:”,
name:"NotificationIdentifier",
object: nil)
NSNotificationCenter.defaultCenter().postNotificationName("NotificationIdentifier", object: projNewRouter.indexOfSelectedItem)
}
func testNotication(notification: NSNotification){
print("Found Combo ID \(notification.object)")
}
func addComboxValue(myVal:[String],myObj:AnyObject){
let myValno: Int = myVal.count
for var i = 0; i < myValno; ++i{
myObj.addItemWithObjectValue(myVal[i])
}
}
}
You need to define a delegate for the combobox that implements the NSComboBoxDelegate protocol, and then use the comboBoxSelectionDidChange(_:) method.
The easiest method is for your NewProjectSetup class to implement the delegate, as in:
class NewProjectSetup: NSViewController, NSComboBoxDelegate { ... etc
Then in viewDidLoad, also include:
self.projNewRouter.delegate = self
// self (ie. NewProjectSetup) implements NSComboBoxDelegate
And then you can pick up the change in:
func comboBoxSelectionDidChange(notification: NSNotification) {
print("Woohoo, it changed")
}