How to pass value from NSViewController to custom NSView of NSPopover? - swift

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

Related

"Extensions must not contain stored properties" preventing me from refactoring code

I have a 13 lines func that is repeated in my app in every ViewController, which sums to a total of 690 lines of code across the entire project!
/// Adds Menu Button
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
for menuTappedAction function to work, I have to declare a weak var like this:
extension UIViewController {
weak var coordinator: MainCoordinator?
But by doing this I get error Extensions must not contain stored properties
What I tried so far:
1) Removing the weak keyword will cause conflicts in all my app.
2) Declaring this way:
weak var coordinator: MainCoordinator?
extension UIViewController {
Will silence the error but the coordinator will not perform any action. Any suggestion how to solve this problem?
You can move your addMenuButton() function to a protocol with a protocol extension. For example:
#objc protocol Coordinated: class {
var coordinator: MainCoordinator? { get set }
#objc func menuTappedAction()
}
extension Coordinated where Self: UIViewController {
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
}
Unfortunately, you can't add #objc methods to class extensions (see: this stackoverflow question), so you'll still have to setup your view controllers like this:
class SomeViewController: UIViewController, Coordinated {
weak var coordinator: MainCoordinator?
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
}
It'll save you some code, and it will allow you to refactor the bigger function addMenuButton(). Hope this helps!
For it to work in an extension you have to make it computed property like so : -
extension ViewController {
// Make it computed property
weak var coordinator: MainCoordinator? {
return MainCoordinator()
}
}
You could use objc associated objects.
extension UIViewController {
private struct Keys {
static var coordinator = "coordinator_key"
}
private class Weak<V: AnyObject> {
weak var value: V?
init?(_ value: V?) {
guard value != nil else { return nil }
self.value = value
}
}
var coordinator: Coordinator? {
get { (objc_getAssociatedObject(self, &Keys.coordinator) as? Weak<Coordinator>)?.value }
set { objc_setAssociatedObject(self, &Keys.coordinator, Weak(newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
}
This happens because an extension is not a class, so it can't contain stored properties. Even if they are weak properties.
With that in mind, you have two main options:
The swift way: Protocol + Protocol Extension
The nasty objc way: associated objects
Option 1: use protocol and a protocol extension:
1.1. Declare your protocol
protocol CoordinatorProtocol: class {
var coordinator: MainCoordinator? { get set }
func menuTappedAction()
}
1.2. Create a protocol extension so you can pre-implement the addMenuButton() method
extension CoordinatorProtocol where Self: UIViewController {
func menuTappedAction() {
// Do your stuff here
}
}
1.3. Declare the weak var coordinator: MainCoordinator? in the classes that will be adopting this protocol. Unfortunately, you can't skip this
class SomeViewController: UIViewController, CoordinatorProtocol {
weak var coordinator: MainCoordinator?
}
Option 2: use objc associated objects (NOT RECOMMENDED)
extension UIViewController {
private struct Keys {
static var coordinator = "coordinator_key"
}
public var coordinator: Coordinator? {
get { objc_getAssociatedObject(self, &Keys.coordinator) as? Coordinator }
set { objc_setAssociatedObject(self, &Keys.coordinator, newValue, .OBJC_ASSOCIATION_ASSIGN) }
}
}
You can do it through subclassing
class CustomVC:UIViewController {
weak var coordinator: MainCoordinator?
func addMenuButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
let menuImage = UIImage(named: "MenuWhite")
menuButton.setImage(menuImage, for: .normal)
menuButton.addTarget(self, action: #selector(menuTappedAction), for: .touchDown)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: menuButton)
}
/// Launches the MenuViewController
#objc func menuTappedAction() {
coordinator?.openMenu()
}
}
class MainCoordinator {
func openMenu() {
}
}
class ViewController: CustomVC {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Use a NSMapTable to create a state container for your extension, but make sure that you specify to use weak references for keys.
Create a class in which you want to store the state. Let's call it ExtensionState and then create a map as a private field in extension file.
private var extensionStateMap: NSMapTable<TypeBeingExtended, ExtensionState> = NSMapTable.weakToStrongObjects()
Then your extension can be something like this.
extension TypeBeingExtended {
private func getExtensionState() -> ExtensionState {
var state = extensionStateMap.object(forKey: self)
if state == nil {
state = ExtensionState()
extensionStateMap.setObject(state, forKey: self)
}
return state
}
func toggleFlag() {
var state = getExtensionState()
state.flag = !state.flag
}
}
This works in iOS and macOS development, but not on server side Swift as there is no NSMapTable there.

How to update variable in MVVM?

I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪

Access a UIViewController class property without casting?

I have this function in a helper class. It's to show/hide some stuff.
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if controller is AddNewViewController {
let addNewViewController = controller as! AddNewViewController
addNewViewController.bgButton.isHidden = isHidden
} else if controller is EditViewController {
let editViewController = controller as! EditViewController
editViewController.bgButton.isHidden = isHidden
}
...
}
Is there is a way around to have one if statement, instead of one if statement for each controller? Something like,
if controller.hasProperty(bgButton) {
controller.bgButton.isHidden = isHidden
}
Thanks
You still need to cast using as? ..., however in order not to do that for all view controllers that have the bgButton, you can define a base protocol enforcing all classes conforming to it to have the bgButton:
public protocol Buttoned {
var bgButton: UIButton { get set }
func setHideButton(_ isHidden: Bool)
}
extension Buttoned {
public func setHideButton(_ isHidden: Bool) {
bgButton.isHidden = isHidden
}
}
public class AddNewViewController: Buttoned {
#IBOutlet fileprivate weak var bgButton: UIButton!
....
}
public class EditViewController: Buttoned {
#IBOutlet fileprivate weak var bgButton: UIButton!
....
}
then you can handle the action in the actual view controller like below:
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if let controller = controller as? Buttoned {
controller.setHideButton(isHidden)
}
...
}
In the given scenario to replace your all if with single if means that you should have a common base class or your class conform to same protocol. However type cast still requires. You can use below code to achieve your desired functionality.
Create a protocol BackgroundButton
public protocol BackgroundButton {
var bgButton: UIButton { get }
}
Conforms all your custom UIViewController with this protocol in Extension like below
extension AddNewViewController : BackgroundButton {
var bgButton : UIButton {
return yourbutton // Use any instance of UIButton from your AddNewViewController
}
}
extension EditViewController : BackgroundButton {
var bgButton : UIButton {
return yourbutton // Use any instance of UIButton from your EditViewController
}
}
Finally update your method like this
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if let controller = controller as? BackgroundButton {
controller.bgButton.isHidden = isHidden
controller.bgButton. //Do any thing which you want with your button
}
...
}
Hope this will help you to reduce your numbers of if

Swift OSX - Delegate protocol function returns nil, crashes when unwrapping textfield value

I'm working on an OSX app with Swift which makes use of an NSSplitView which holds two view controllers: "TableViewController" and "EntryViewController". I'm using delegates in order to transmit a custom NSObject ("Entry") on click from TableViewController up to the SplitViewController, then back down to the EntryViewController.
My problem is this: When the Entry object is received in the EntryViewController, any attempt to assign its properties to a text field value result in an unexpectedly found nil type error, never mind that the IBOutlets are properly linked, and that it can both print the Entry.property and the textfield string value (provided it is in a different, unrelated function).
I have tried many arrangements to solve this problem, which is why the current configuration might be a bit over-complicated. A delegate relation straight from Table VC to Entry VC caused the same issues.
Is there some way that the IBOutlets are not connecting, even though the view has loaded before the delegate is called? I've read many many articles on delegation—mostly for iOS—and yet can't seem to find the root of my problems. I'll be the first to admit that my grasp of Swift is a little bit piecemeal, so I am open to the possibility that what I am trying to do is simply bad/hacky coding and that I should try something completely different.
Thanks for your help!
TableViewController:
protocol SplitViewSelectionDelegate: class {
func sendSelection(_ entrySelection: NSObject)
}
class TableViewController: NSViewController {
#IBOutlet weak var searchField: NSSearchField!
#IBOutlet var tableArrayController: NSArrayController!
#IBOutlet weak var tableView: NSTableView!
var sendDelegate: SplitViewSelectionDelegate?
dynamic var dataArray = [Entry]()
// load array from .plist array of dictionaries
func getItems(){
let home = FileManager.default.homeDirectoryForCurrentUser
let path = "Documents/resources.plist"
let urlUse = home.appendingPathComponent(path)
let referenceArray = NSArray(contentsOf: urlUse)
dataArray = [Entry]()
for item in referenceArray! {
let headwordValue = (item as AnyObject).value(forKey: "headword") as! String
let defValue = (item as AnyObject).value(forKey: "definition") as! String
let notesValue = (item as AnyObject).value(forKey: "notes") as! String
dataArray.append(Entry(headword: headwordValue, definition: defValue, notes: notesValue))
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.sendDelegate = SplitViewController()
getItems()
print("TVC loaded")
// Do any additional setup after loading the view.
}
// send selection forward to entryviewcontroller
#IBAction func tableViewSelection(_ sender: Any) {
let index = tableArrayController.selectionIndex
let array = tableArrayController.arrangedObjects as! Array<Any>
let obj: Entry
let arraySize = array.count
if index <= arraySize {
obj = array[index] as! Entry
print(index)
print(obj)
sendDelegate?.sendSelection(obj)
}
else {
print("index unassigned")
}
}
}
SplitViewController:
protocol EntryViewSelectionDelegate: class {
func sendSecondSelection(_ entrySelection: NSObject)
}
class SplitViewController: NSSplitViewController, SplitViewSelectionDelegate {
var delegate: EntryViewSelectionDelegate?
#IBOutlet weak var mySplitView: NSSplitView!
var leftPane: NSViewController?
var contentView: NSViewController?
var entrySelectionObject: NSObject!
override func viewDidLoad() {
super.viewDidLoad()
// assign tableview and entryview as child view controllers
let story = self.storyboard
leftPane = story?.instantiateController(withIdentifier: "TableViewController") as! TableViewController?
contentView = story?.instantiateController(withIdentifier: "EntryViewController") as! EntryViewController?
self.addChildViewController(leftPane!)
self.addChildViewController(contentView!)
print("SVC loaded")
}
func sendSelection(_ entrySelection: NSObject) {
self.delegate = EntryViewController() //if this goes in viewDidLoad, then delegate is never called/assigned
entrySelectionObject = entrySelection
print("SVC:", entrySelectionObject!)
let obj = entrySelectionObject!
delegate?.sendSecondSelection(obj)
}
}
And Finally, EntryViewController:
class EntryViewController: NSViewController, EntryViewSelectionDelegate {
#IBOutlet weak var definitionField: NSTextField!
#IBOutlet weak var notesField: NSTextField!
#IBOutlet weak var entryField: NSTextField!
var entryObject: Entry!
override func viewDidLoad() {
super.viewDidLoad()
print("EVC loaded")
}
func sendSecondSelection(_ entrySelection: NSObject) {
self.entryObject = entrySelection as! Entry
print("EVC:", entryObject)
print(entryObject.headword)
// The Error gets thrown here:
entryField.stringValue = entryObject.headword
}
}
You don't need a delegate / protocol since there is a reference to EntryViewController (contentView) – by the way the instance created with EntryViewController() is not the instantiated instance in viewDidLoad.
Just use the contentView reference:
func sendSelection(_ entrySelection: NSObject) {
contentView?.sendSecondSelection(entrySelection)
}

How to change NSTextField value from another NSView (different swift files)

I have two different NSViews. viewA is controlled from ViewA.swift viewB is controlled from ViewB.swift I want to change textField (NSTextField) that is in viewB from viewA.
i change it by creating an instance of viewB from viewA, but i get an error
Here is how i create the instance in viewA:
let myViewB = ViewB()
myViewB.changeValue = myString
And in viewB i declared:
var myString = "" {
didSet {
myNSTextField.stringValue(myString)
}
}
This is the error I get when i change the NSTextField value:
unexpectedly found nil while unwrapping an Optional value
UPDATE:
file ViewB.swift
class ViewB: NSView {
...
#IBOutlet weak var widthTextField: NSTextField!
...
}
file ViewA.swift
let myViewB = ViewB()
class ViewA: NSView {
...
if *statement* {
....
myViewB.widthTextField.stringValue("try") // <- here i get the error
....
}
...
}
I finally fixed it! I create one variable which contains the text it needs to change it to like this: (using your test-project)
In class ViewA
import Cocoa
var textToSet = "Hello"
class ViewA: NSView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
// Drawing code here.
}
#IBAction func editPressed(sender: AnyObject) {
textToSet = "No"
}
}
In class ViewB
import Cocoa
class ViewB: NSView{
#IBOutlet var myTextField: NSTextField!
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
myTextField.stringValue = textToSet
// Drawing code here.
}
}
In your code you generate a new object of the stringValue (I think). Does it work like this?
class ViewB: NSView {
#IBOutlet weak var widthTextField: NSTextField!
}
let myViewB = ViewB()
class ViewA: NSView {
if *statement* {
myViewB.widthTextField.stringValue = "try" //setting it "try" without recreating the stringValue (done by "()")
}
}