Access to ViewController outlets from AppDelegate - swift

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)

Related

Xcode: didTransition to Won't Run in iMessage Extension

I am making an iMessage extension that uses the didTransition(to:). However, the function won't run when I resize the iMessage extension in the simulator. Am I doing something wrong?
This is the code I have:
import UIKit
import Messages
class EditorViewController: MSMessagesAppViewController {
#IBOutlet weak var input: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
input.text = "not changed"
}
// This part isn't working:
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
input.text = "changed"
}
}
When I resize it in the simulator, nothing happens. The input.text changes the UITextView's text in the viewDidLoad() function, but not in the didTransition(to) function because it never runs.
Am I doing something wrong?
The EditorViewController is a view controller presented by the show (e.g. Push) segue, and has a NavigationController attached to it.
Here is a gif of it not changing:
The input's text never changes
How can I fix this?
EDIT: The willTransition and didTransition functions don't run when the View Controller is embedded in a Navigation Controller. Is there a way to fix this? (It works without the Navigation Controller, but I need the Navigation Controller for this project).
As pointed out in this answer, the entry point of a iMessage App need to be a subclass of MSMessagesAppViewController, so you can not use a NavigationViewController directly as root controller, until Apple adds support for this behavior.
But as suggested, you could solve this with a workaround like this:
import UIKit
import Messages
class MyRootVC: MSMessagesAppViewController {
var navVC: UINavigationViewController!
var editorVC: EditorViewController!
func viewDidLoad() {
super.viewDidLoad()
editorVC = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! EditorViewController
navVC = UINavigationController(rootViewController: editorVC)
self.addChild(navVC)
self.view.addSubview(navVC.view)
navVC.didMove(toParent: self)
}
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
editorVC.input.text = "changed"
}
}
class EditorViewController: UIViewController {
#IBOutlet weak var input: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
input.text = "not changed"
}
}

ViewController + Storyboard setting up validation with controlTextDidChange

Trying to setup validation for a few text fields in a new (and very small) Swift Mac app. Following various other topics here on SO and a few other examples, I can still not get controlTextDidChange to propagate (to my ViewController).
E.g: How to live check a NSTextField - Swift OS X
I have read at least a dozen variations of basically that same concept. Since none of the accepted answers seem to work I am just getting more and more confused by something which is generally a fairly simple task on most platforms.
I have controlTextDidChange implemented to just call NSLog to let me know if I get anything.
AppDelegate should be part of the responder chain and should eventually handle controlTextDidChange but I see nothing there either.
Using the current Xcode I start a new project. Cocoa app, Swift, Storyboard and nothing else.
From what I can gather the below isolated example should work. In my actual app I have tried some ways of inserting the ViewController into the responder chain. Some answers I found suggested it was not always there. I also tried manually adding the ViewController as the delegate in code theTextField.delegate = self
Nothing I have done seems to get text changed to trigger any events.
Any ideas why I have so much trouble setting up this delegation?
My single textfield example app
Storyboard is about as simple as it gets:
AppDelegate
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDelegate, NSTextDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func controlTextDidChange(notification: NSNotification) {
let object = notification.object as! NSTextField
NSLog("AppDelegate::controlTextDidChange")
NSLog("field contains: \(object.stringValue)")
}
}
ViewController
import Cocoa
class ViewController: NSViewController, NSTextFieldDelegate, NSTextDelegate {
#IBOutlet var theTextField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func controlTextDidChange(notification: NSNotification) {
let object = notification.object as! NSTextField
NSLog("ViewController::controlTextDidChange")
NSLog("field contains: \(object.stringValue)")
}
}
I think the samples you're following are a bit out-of-date.
Try...
override func controlTextDidChange(_ notification: Notification) {
...as the function definition for your method in your NSTextFieldDelegate.

How to use a selector from another class?

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.
}
}

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")
}

Unit testing UIButtons in a View Controller in swift with dependency injection?

In my swift app, I have a view controller with several buttons. When the user interacts with these buttons, they trigger code that may have some dependencies. It can look something like this:
class ViewController: UIViewController {
#IBOutlet weak var button1: UIButton!
#IBAction func button1Down(sender: UIButton) {
// Do some stuff
SomeUtil().doSomething()
}
}
This is a major problem for testing. I'd like to test the interface for the view controller, IE the button1Down. This button1Down is coupled to a dependency that I would like to mock in a unit test. This implementation leaves no possibility of mocking.
Now, I've found two ways to do dependency injection, both with default parameters. I can (1) set the view controller up like this:
class ViewController: UIViewController {
#IBOutlet weak var button1: UIButton!
#IBAction func button1Down(sender: UIButton) {
// Do some stuff
somemethod()
}
func somemethod(util: SomeUtil = SomeUtil()) {
util.doSomething()
}
}
This works, but I'm not really testing the view controller's interface. I'm testing some implementation specific method to get around the fact that I can't add a default parameter to an overridden function.
I can (2) set the dependency injection up like this:
class ViewController: UIViewController {
#IBOutlet weak var button1: UIButton!
var util: SomeUtil!
#IBAction func button1Down(sender: UIButton) {
// Do some stuff
util.doSomething()
}
func injectDeps(util: SomeUtil = SomeUtil()) {
self.util = util
}
override func viewDidLoad() {
super.viewDidLoad()
injectDeps()
}
}
This method is great for utilities that I use in multiple methods, as it is reasonable to save the utility as an instance variable. This is not ideal for utilities that only get used in one method. These utility instances have no reason to be associated as a variable on the instance, other than to make testing easier.
As far as mocking goes, I usually just do something like this (using quick framework):
var viewController: MyApp.ViewController!
class SomeUtilMock: SomeUtil {
override func doSomething() {
// do something test specific
}
}
var someUtil: SomeUtilMock!
beforeEach {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
viewController =
storyboard.instantiateViewControllerWithIdentifier(
"MainView") as! MyApp.ViewController
someUtil = SomeUtilMock()
viewController.beginAppearanceTransition(true, animated: false)
viewController.endAppearanceTransition()
viewController.injectDeps(someUtil)
}
Has anyone found a better way to do dependency injection and mocking in swift? Neither of these dependency injection methods are ideal.
Thanks in advance!