I'm writing a UserNotification library to reuse in many apps. The below .swift file is just dragged into a new project when needed. I'm then adding an NSObject to the View Controller Scene, setting it's custom class to KTUserNotification. I then drag a referencing outlet from this NSObject into the view controller. From there I use this outlet to call Show() to display notifications.
I use the NSObject with a custom class approach because the instance needs to stay allocated until the app ends. I first tried making it just a custom class, using let usernotification = KTUserNotification(). This however deallocated before the call came through to the "shouldPresent notification", causing a EXC_BAD_ACCESS. The #IBInspectables were just a nice addition I tossed in when moving to the NSObject approach.
Is there another way of keeping the instance allocated until the program ends without using an NSObject? Os is the NSObject the best way to go here?
import Cocoa
class KTUserNotification: NSObject, NSUserNotificationCenterDelegate {
#IBInspectable var showEvenIfAppIsFocus: Bool = true
#IBInspectable var title: String = ""
#IBInspectable var subTitle: String = ""
#IBInspectable var informativeText: String = ""
#IBAction func showNotification(_ sender: Any) {
Show()
}
override func awakeFromNib() {
NSUserNotificationCenter.default.delegate = self
}
func userNotificationCenter(_ center: NSUserNotificationCenter, shouldPresent notification: NSUserNotification) -> Bool {
return showEvenIfAppIsFocus
}
public func Show(title : String, subTitle: String, informativeText : String) {
let notification = NSUserNotification()
let randIdentifier = Int(arc4random_uniform(999999999) + 1)
notification.identifier = "KTUserNotification\(randIdentifier)"
notification.title = title
notification.subtitle = subTitle
notification.informativeText = informativeText
notification.soundName = NSUserNotificationDefaultSoundName
let notificationCenter = NSUserNotificationCenter.default
notificationCenter.deliver(notification)
}
public func Show() {
Show(title: title, subTitle: subTitle, informativeText: informativeText)
}
}
Related
When I switch from one screen to another, I get a black screen, I can't figure out why
Controller:
class AddContactsController: UIViewController {
// MARK: - Constants
private enum Constants {
static let textField = "nameTextCell"
static let datePicker = "datePicker"
static let pickerView = "pickerView"
static let textViewNotes = "textViewNotes"
static let alertOk = "OK"
static let alertQuestion = "It seems you made a mistake"
static let navigationTitle = "Create"
}
// MARK: - AddPresenter
var presenter: AddListPresenter?
func instantiate() -> UIViewController {
let vc = AddContactsController()
let presenter = AddListPresenter()
vc.presenter = presenter
return vc
}
Presenter:
class AddListPresenter {
weak var view: AddListController?
private var contact: Contact
init(contact: Contact? = nil) {
self.contact = contact ?? .init(
name: "",
surname: "",
middleName: "",
phone: "",
email: "",
date: "",
sex: "",
notes: ""
)
}
private var saveHieght: CGFloat = 0
How can this be avoided. I don't want to transfer presenter declarations to viewDidLoad
When you create a view controller by invoking it's initializer directly( e.g AddContactsController(), it's content view does not get created automatically like it would from a storyboad or a nibfile.
If you are creating you UI in code you need to show us your loadView() method. That's where you code to create your view hierarchy belongs.
Unless you have implemented loadView() you won't get any content view and will see a black screen exactly as you describe.
You also need to show the code that calls your instantiate() method and what you do with the view controller instance that's returned.
How can I access a title inside the navigation controller from a separate Class? I embedded my mainVC into the navigation controller via Storyboard. I can access the title from inside the viewDidLoad from inside the same Class like this self.navigationItem.title = "MyTitle". However, I need to access the title from a separate class 'CustomNavigation like this:
class: CustomNavigation: UIViewController() {
override func viewDidLoad(){
super.viewDidLoad()
func createCustomNav(){
self.navigationItem.title = "MyTitle"
}
}
}
Unfortunately it doesn't work. I also tried this:
func createCustomNav(){
let nav = UINavigationBar()
let title = UINavigationItem(title: "MyTitle")
nav.setItems([title], animated: false)
self.view.addSubview(nav)
}
This does't work neither. I don't get any errors.
I instantiate CustomNavigation inside the mainVC: var customNavigation = CustomNavigation()
Any help would be greatly appreciate it!
Set up a protocol that describes the work you want done.
protocol NavigationTitler {
func updateTitle(with title: String)
}
Define how your class will use the protocol.
class CustomNavigation: UINavigationController {
var titleDelegate: NavigationTitler?
func work() {
if let del = titleDelegate {
del.updateTitle(with: "Title from other object")
}
}
}
Implement the protocol (this is for your mainVC).
class ViewController: UIViewController, NavigationTitler {
var customNavigation: CustomNavigation?
func updateTitle(with title: String) {
self.navigationItem.title = title
}
func buildCustomController() {
customNavigation = CustomNavigation()
customNavigation!.titleDelegate = self
}
}
Get the title from a UITabBarItem, use as title in current VC:
//Titel einstellen
let x = navigationController?.tabBarItem.title ?? ""
self.title = x //titel in VC
I have a ViewController class that presents a series of two choice popup views. Each two choice popup view is different.
Popup1 - Choice1 -> Choice1Popup
Popup1 - Choice2 -> Choice2Popup
I intend the method to present Popup1 to be public, but I want the other methods that present Choice1Popup and Choice2Popup to be private.
If I decide I need to test Choice1Popup and Choice2Popup then I may have to make them internal instead of private, but they are unlikely to ever be used from any other place.
I want to write a unit test that tests when the button for Choice1 is touched that the method that presents Choice1Popup is called. I've used a protocol with method type variables to allow a Mock to inject the Mock versions of the popup presenters. I'm not feeling 100% comfortable about my approach so I wanted to get input as to whether or not there is a better way.
An aside I'm feeling conflicted about internal versus private. It would be nice to be able to test my private methods but I don't want them to be able to be called from anywhere but a unit test and making them internal exposes them.
Here is the code and a single Unit test is at the bottom:
// protocol to be used by both UserChoices class and UserChoicesMock for method injection
protocol UserChoicesPrivateUnitTesting {
static var choice1Method:(UIViewController) -> Void { get set }
static var choice2Method:(UIViewController) -> Void { get set }
}
// this popup that will be presented with a public method
public class ChoiceViewController:UIViewController {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var subjectLabel: UILabel!
#IBOutlet weak var choice1Button: UIButton!
#IBOutlet weak var choice2Button: UIButton!
var choice1Action:(() -> Void)?
var choice2Action:(() -> Void)?
// ...
}
public class UserChoices: UIViewController, UserChoicesPrivateUnitTesting {
static var choice1Method: (UIViewController) -> Void = choice1
static var choice2Method: (UIViewController) -> Void = choice2
private static func choice1(onTopViewController: UIViewController) {
//present choice1Popup
}
private static func choice2(onTopViewController: UIViewController) {
//present choice2Popup
}
public static func presentChoiceViewController(onTopViewController: UIViewController, ChoiceViewController: ChoiceViewController = ChoiceViewController.instantiateFromAppStoryBoard(appStoryBoard: .MenuStoryboard)) {
let isCustomAnimated = true
// ChoiceViewController.transitioningDelegate = transitionDelegate
ChoiceViewController.choice1Action = { [weak onTopViewController]() in
guard let weakSelf = onTopViewController else {
return
}
weakSelf.dismiss(animated: false, completion: nil)
UserChoices.choice1Method(onTopViewController!)
}
ChoiceViewController.choice2Action = { [weak onTopViewController]() in
guard let weakSelf = onTopViewController else {
return
}
weakSelf.dismiss(animated: false, completion: nil)
UserChoices.choice2Method(onTopViewController!)
}
onTopViewController.present(ChoiceViewController, animated: isCustomAnimated, completion: nil)
}
}
import XCTest
#testable import ChoiceModule
public class UserChoicesMock:UserChoicesPrivateUnitTesting {
static public var choice1Method: (UIViewController) -> Void = choice1
static public var choice2Method: (UIViewController) -> Void = choice2
static var choice1MethodCalled = false
static var choice2MethodCalled = false
static func choice1(onTopViewController: UIViewController) {
choice1MethodCalled = true
}
static func choice2(onTopViewController: UIViewController) {
choice2MethodCalled = true
}
}
class UserChoicesTests: XCTestCase {
func testChoice1CallsPrivateChoice1Method() {
// This is an example of a functional test case.
let vc = UIViewController()
let choiceViewController = ChoiceViewController.instantiateFromAppStoryBoard(appStoryBoard: .MenuStoryboard)
UserChoices.choice1Method = UserChoicesMock.choice1Method
UserChoices.presentChoiceViewController(onTopViewController: vc, ChoiceViewController: choiceViewController)
choiceViewController.choice1Button.sendActions(for: .touchUpInside)
if UserChoicesMock.choice1MethodCalled == false {
XCTFail("choice1Method not called")
}
}
}
Tests can't access anything declared private. They can access anything declared internal as long as the test code does #testable import.
When you get that queasy feeling, "But I shouldn't have to expose this," consider that your class actually has multiple interfaces. There's the "everything it does interface" and there's the "parts needed by production code interface." There are various things to consider about this:
Is there another type that is trying to get out?
Is there another protocol to express a subset of the interface? This could be used by the rest of production code.
Or maybe it's like a home theater amplifier where "controls you don't need that often" are hidden behind a panel. Don't sweat it.
I'm trying (unsuccessfully) to build a TreeController-controlled NSOutlineView. I've gone through a bunch of tutorials, but they all pre-load the data before starting anything, and this won't work for me.
I have a simple class for a device:
import Cocoa
class Device: NSObject {
let name : String
var children = [Service]()
var serviceNo = 1
var count = 0
init(name: String){
self.name = name
}
func addService(serviceName: String){
let serv = "\(serviceName) # \(serviceNo)"
children.append(Service(name: serv))
serviceNo += 1
count = children.count
}
func isLeaf() -> Bool {
return children.count < 1
}
}
I also have an even more simple class for the 'Service':
import Cocoa
class Service: NSObject {
let name: String
init(name: String){
self.name = name
}
}
Finally, I have the ViewController:
class ViewController: NSViewController {
var stepper = 0
dynamic var devices = [Device]()
override func viewDidLoad() {
super.viewDidLoad()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
}
#IBAction func addService(_ sender: Any) {
for i in 0..<devices.count {
devices[i].addService(serviceName: "New Service")
}
}
}
Obviously I have 2 buttons, one that adds a 'device' and one that adds a 'service' to each device.
What I can't make happen is any of this data show up in the NSOutlineView. I've set the TreeController's Object Controller Property to Mode: Class and Class: Device, and without setting the Children, Count, or Leaf properties I get (predictably):
2017-01-04 17:20:19.337129 OutlineTest[12550:1536405] Warning: [object class: Device] childrenKeyPath cannot be nil. To eliminate this log message, set the childrenKeyPath attribute in Interface Builder
If I then set the Children property to 'children' things go very bad:
2017-01-04 17:23:11.150627 OutlineTest[12695:1548039] [General] [ addObserver:forKeyPath:options:context:] is not supported. Key path: children
All I'm trying to do is set up the NSOutlineView to take input from the NSTreeController so that when a new 'Device' is added to the devices[] array, it shows up in the Outline View.
If anyone could point me in the right direction here I'd be most grateful.
Much gratitude to Warren for the hugely helpful work. I've got it (mostly) working. A couple of things that I also needed to do, in addition to Warren's suggestions:
Set the datastore for the Tree Controller
Bind the OutlineView to the TreeController
Bind the Column to the TreeController
Bind the TableView Cell to the Table Cell View (yes, really)
Once all that was done, I had to play around with the actual datastore a bit:
var name = "Bluetooth Devices Root"
var deviceStore = [Device]()
#IBOutlet var treeController: NSTreeController!
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
deviceStore.append(Device(name: "Bluetooth Devices"))
self.treeController.content = self
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func addDeviceAction(_ sender: Any) {
if(deviceStore[0].name == "Bluetooth Devices"){
deviceStore.remove(at: 0)
}
Turns out the Root cannot be child-less at the beginning, at least as far as I can tell. Once I add a child, I can delete the place-holder value and the tree seems to work (mostly) as I want. One other thing is that I have to reload the data and redisplay the outline whenever the data changes:
outlineView.reloadData()
outlineView.setNeedsDisplay()
Without that, nothing. I still don't have the data updating correctly (see comments below Warren's answer) but I'm almost there.
To state the obvious, a NSTreeController manages a tree of objects all of which need to answer the following three questions/requests.
Are you a leaf i.e do you have no children? = leafKeyPath
If you are not a leaf, how many children do you have ? = countKeyPath
Give me your children! = childrenKeyPath
Its simple to set these up in IB or programatically. A fairly standard set of properties is respectively.
isLeaf
childCount
children
But its totally arbitrary and can be any set of properties that answer those questions.
I normally set up a protocol named something like TreeNode and make all my objects conform to it.
#objc protocol TreeNode:class {
var isLeaf:Bool { get }
var childCount:Int { get }
var children:[TreeNode] { get }
}
For your Device object you answer 2 out 3 question with isLeaf and children but don't answer the childCount question.
Your Device's children are Service objects and they answer none of that which is some of the reason why you are getting the exceptions.
So to fix up your code a possible solution is ...
The Service object
class Service: NSObject, TreeNode {
let name: String
init(name: String){
self.name = name
}
var isLeaf:Bool {
return true
}
var childCount:Int {
return 0
}
var children:[TreeNode] {
return []
}
}
The Device object
class Device: NSObject, TreeNode {
let name : String
var serviceStore = [Service]()
init(name: String){
self.name = name
}
var isLeaf:Bool {
return serviceStore.isEmpty
}
var childCount:Int {
return serviceStore.count
}
var children:[TreeNode] {
return serviceStore
}
}
And a horrible thing to do from a MVC perspective but convenient for this answer. The root object.
class ViewController: NSViewController, TreeNode {
var deviceStore = [Device]()
var name = "Henry" //whatever you want to name your root
var isLeaf:Bool {
return deviceStore.isEmpty
}
var childCount:Int {
return deviceStore.count
}
var children:[TreeNode] {
return deviceStore
}
}
So all you need to do is set the content of your treeController. Lets assume you have an IBOutlet to it in your ViewController.
class ViewController: NSViewController, TreeNode {
#IBOutlet var treeController:NSTreeController!
#IBOutlet var outlineView:NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
treeController.content = self
}
Now each time you append a Device or add a Service just call reloadItem on the outlineView (that you also need an outlet to)
#IBAction func addDeviceAction(_ sender: Any) {
let str = "New Device #\(stepper)"
devices.append(Device(name: str))
stepper += 1
print("Added Device: \(devices[devices.count-1].name)")
outlineView.reloadItem(self, reloadChildren: true)
}
Thats the basics and should get you started but the docs for NSOutlineView & NSTreeController have a lot more info.
EDIT
In addition to the stuff above you need to bind your outline view to your tree controller.
First ensure your Outline View is in view mode.
Next bind the table column to arrangedObjects on the tree controller.
Last bind the text cell to the relevant key path. In your case it's name. objectValue is the reference to your object in the cell.
By using the delegation protocol I have tried to pass a string (inputFromUser.string) from NSViewController - mainController to custom subclass of NSView of NSPopover - PlasmidMapView, to drawRect function, see code below. But, it didn’t work. I don’t know where a mistake is. Maybe there is another way to pass this string.
Update
File 1.
protocol PlasmidMapDelegate {
func giveDataForPLasmidMap(dna: String)
}
class MainController: NSViewController {
#IBOutlet var inputFromUser: NSTextView!
var delegate: plasmidMapDelegate?
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
delegate?.giveDataForPLasmidMap(dna!)
}
}
File 2
class PlasmidMapView: NSView, PlasmidMapDelegate {
var dnaForMap = String()
func giveDataForPLasmidMap(dna: String) {
dnaForMap = dna
}
override func drawRect(dirtyRect: NSRect) {
let objectOfMainController = MainController()
objectOfMainController.delegate = self
//here I have checked if the string dnaForMap is passed
let lengthOfString = CGFloat(dnaForMap.characters.count / 10)
let pathRect = NSInsetRect(self.bounds, 10, 45)
let path = NSBezierPath(roundedRect: pathRect,
xRadius: 5, yRadius: 5)
path.lineWidth = lengthOfString //the thickness of the line should vary in dependence on the number of typed letter in the NSTextView window - inputDnaFromUser
NSColor.lightGrayColor().setStroke()
path.stroke()
}
}
Ok, there's some architecture mistakes. You don't need delegate method and protocol at all. All you just need is well defined setter method:
I. Place your PlasmidMapView into NSViewController-subclass. This view controller must be set as contentViewController-property of your NSPopover-control. Don't forget to set it the way you need in viewDidLoad-method or another.
class PlasmidMapController : NSViewController {
weak var mapView: PlacmidMapView!
}
II. In your PlacmidMapView don't forget to call needsDisplay-method on dna did set:
class PlasmidMapView: NSView {
//...
var dnaForMap = String() {
didSet {
needsDisplay()
}
//...
}
III. Set dna-string whenever you need from your MainController-class.
#IBAction func actionPopoverPlasmidMap(sender: AnyObject) {
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
let dna = inputDnaFromUser.string
if let controller = popoverPlasmidMap.contentViewController as? PlasmidMapController {
controller.mapView.dna = dna
} else {
fatalError("Invalid popover content view controller")
}
}
In order to use delegation your class PlasmidMapView needs to have an instance of the MainController (btw name convention is Class, not class) and conform to the PlasmidMapDelegate (once again name convention dictates that it should be PlasmidMapDelegate). With that instance you then can:
mainController.delegate = self
So, after several days I have found a solution without any protocols and delegation as Astoria has mentioned. All what I needed to do was to make #IBOutlet var plasmidMapIBOutlet: PlasmidMapView!for my custom NSView in MainController class and then to use it to set the value for the dnaForMap in #IBAction func actionPopoverPlasmidMap(sender: AnyObject).
class PlasmidMapView: NSView
{
var dnaForMap = String()
}
class MainController: NSViewController
{
#IBOutlet var inputFromUser: NSTextView!
#IBOutlet var plasmidMapIBOutlet: PlasmidMapView!
#IBAction func actionPopoverPlasmidMap(sender: AnyObject)
{
plasmidMapIBOutlet.dnaForMap = inputDnaFromUser.string!
popoverPlasmidMap.showRelativeToRect(sender.bounds,
ofView: sender as! NSView, preferredEdge: NSRectEdge.MinY)
}
}