Delegation: How to Pass a Property Between Custom Views - swift

In the code below, which (I hope) includes all that's relevant to my question, a mouseEntered/-Exited event in ChangerView is supposed to change the display in ChangingView. (ChangerView and ChangingView are displayed side-by-side and share a view controller.) As an OOP newbie, though, I'm seriously missing something about how to set up delegation between these views. Here's ChangerView (in which DoThis?.show = nil, despite that I thought I was setting it to true or false):
import Cocoa
protocol DoThis { var show: Bool { get set } }
class ChangerView: NSView {
var changeDelegate: DoThis?
// Set up for mouseEntered/-Exited
override func mouseEntered(theEvent: NSEvent) { DoThis?.show = true }
override func mouseExited(theEvent: NSEvent) { DoThis?.show = false }
}
And here's changing view:
import Cocoa
class ChangingView: NSView, DoThis {
var show: Bool = false { didSet { needsDisplay = true } }
// Draw into the view
override func drawRect(dirtyRect: NSRect) {
switch show {
case true: // Display setup contingent on show = true
case false: // Display setup contingent on show = false
}
// Draw contingent display
}
}
As I understand things, views should do their own basic display work, and view controllers should handle model-related and higher-level display changes. For that reason, and to keep things simple, I want ChangerView and ChangingView to communicate directly. Unfortunately, I couldn't find any explanations about delegation close enough to this situation—at least not that I could understand.
What am I missing (besides a properly functioning brain)?
Thanks!

It looks like there are two issues.
In your ChangerView class, you should be using the delegate to set the show variable, like this:
import Cocoa
protocol DoThis { var show: Bool { get set } }
class ChangerView: NSView {
var changeDelegate: DoThis?
// Set up for mouseEntered/-Exited
override func mouseEntered(theEvent: NSEvent) { changeDelegate?.show = true }
override func mouseExited(theEvent: NSEvent) { changeDelegate?.show = false }
}
You may want to make the delegate variable weak to prevent reference cycles
The other issue is you've forgot the step where you assign the delegate. I (and I think everyone else) forget this often. Once you get used to setting up delegates you'll remember to check for it if things don't work at first.
So at some point you need to set the changeDelegate var to an instance of the ChangingView class (this is often done in the viewDidLoad() function.
It will look something like this:
class SomeViewController: UIViewController {
#IBOutlet weak var SomeChangerView: ChangerView!
#IBOutlet weak var SomeChangingView: ChangingView!
override func viewDidLoad() {
super.viewDidLoad()
SomeChangerView.changerDelegate = SomeChangingView
}

Related

Reach two IBOutlets of NSTextField at once

I have a login-page with two NSTextFields, so I created two #IBOutlets in my viewcontroller. For some styling I have to call both of them. I want to give them the same properties, but I dont want to call them individual. So how can I reach them at once to give them a property like inputFields.isBordered = true
For iOS swift provides #IBOutletCollection but for MacOS not.
#IBOutlet weak var emailInput: NSTextField!
#IBOutlet weak var passwordInput: NSTextField!
override func viewWillAppear() {
emailInput.isBordered = true
passwordInput.isBordered = true
}
So I want two call them like inputFields.isBordered = true and not individual.
You can do this in many different ways. For example:
func modifyTextFields(_ closure: (NSTextField) -> Void) {
closure(emailInput)
closure(passwordInput)
}
// usage:
modifyTextFields { $0.isBordered = true }
Alternatively,
var textFields: [NSTextField] { // this kind of works like an outlet collection
return [emailInput, passwordInput]
}
// usage:
textFields.forEach { $0.isBordered = true }

Am I using the State Pattern correctly?

I'm learning the State Pattern (Finite State Machine)
In the sample project that I built the only way that I figured out to update the UI is to pass a reference of the presenting view to the state machine, and then update the UI from the state I'm working. Am I doing it wrong?đŸ˜•
Here's my State Machine
class CapturePhotoStateMachine {
var noPictureTakenState: NoPictureTakenState?
var pictureTakenState: PictureTakenState?
var initialState: InitialState?
var vc: SignupAvatarView?
var capturePhotoState: CapturePhotoState?
init(viewController: SignupAvatarView) {
noPictureTakenState = NoPictureTakenState(stateMachine: self)
pictureTakenState = PictureTakenState(stateMachine: self)
initialState = InitialState(stateMachine: self)
vc = viewController
capturePhotoState = initialState
}
func setCapturePhotoState(newState: CapturePhotoState) {
self.capturePhotoState = newState
}
func takePicture() {
self.capturePhotoState?.takePicture()
}
func savePicture(image: UIImage) {
self.capturePhotoState?.savePicture(image: image)
}
func retakePicture() {
self.capturePhotoState?.retakePicture()
}
func setup() {
self.capturePhotoState?.setup()
}
}
Here's my protocol
protocol CapturePhotoState {
func takePicture()
func savePicture(image: UIImage)
func retakePicture()
func setup()
}
Here's a subclass of state
class NoPictureTakenState: CapturePhotoState {
var stateMachine: CapturePhotoStateMachine?
init(stateMachine: CapturePhotoStateMachine) {
self.stateMachine = stateMachine
}
func takePicture() {
stateMachine!.vc?.previewView.isHidden = true
stateMachine!.vc?.capturedImage.isHidden = false
stateMachine!.vc?.saveButton.isHidden = false
stateMachine!.vc?.retakePhoto.isHidden = false
stateMachine?.setCapturePhotoState(newState: (stateMachine?.pictureTakenState)!)
}
func savePicture(image: UIImage) {
}
func retakePicture() {}
func setup() {}
}
The key to your state machine's purpose seems to be that you have interface objects you want to enable or disable depending on the state. That enablement / disablement should be the job of the view controller. The state itself is simply the basis on which questions can be answered such as "What is the current situation" and "What should happen next".
Here's a short simple state machine example that illustrates. It is deliberately trivial. We have just two buttons, and just two states; in each state, exactly one button should be enabled. The states are represented by cases of an enum, and we use a setter observer on that enum to respond whenever the state changes. The enum encapsulates the logic of how many states there are and what the next state is, while the view controller mediates between state change and interface change:
class ViewController: UIViewController {
#IBOutlet weak var takePictureButton: UIButton!
#IBOutlet weak var deletePictureButton: UIButton!
#IBOutlet weak var pictureImageView: UIImageView! // not used in the example
#IBAction func doTakePicture(_ sender: Any) {
// do stuff
doNextState()
}
#IBAction func doDeletePicture(_ sender: Any) {
// do stuff
doNextState()
}
enum State {
case pictureNotTaken
case pictureTaken
var nextState : State {
switch self {
case .pictureNotTaken:
return .pictureTaken
case .pictureTaken:
return .pictureNotTaken
}
}
}
var state : State = .pictureNotTaken {
didSet {
updateInterface()
}
}
func doNextState() {
self.state = self.state.nextState // triggers the setter observer
}
func updateInterface() {
switch state {
case .pictureNotTaken:
takePictureButton.isEnabled = true
deletePictureButton.isEnabled = false
case .pictureTaken:
takePictureButton.isEnabled = false
deletePictureButton.isEnabled = true
}
}
}
Probably what you want is some expansion of that pattern.
the only way that I figured out to update the UI is to pass a reference of the presenting view to the state machine
That is what the above pattern does not do. The setter observer solves that problem for us.
Now, you might object that the switch statement in updateInterface is doing the wrong kind of work. It locates the knowledge of how the interface reflects the state entirely in the view controller. Your impulse is to say that surely that knowledge is part of the state (and that's why you constructed your code the way you did).
My reply would be: well, yes and no. I do sometimes feel that way, and the way I solve the problem is to endow the state machine with properties expressing all the questions the view controller might have about what the current state means as regards the interface. That way, the knowledge is moved to the state, but the interface is still governed, correctly, by the view controller.
So, for example, we might add these two properties to our State enum:
enum State {
// ... everything else is as before ...
var userCanTakePicture : Bool { return self == .pictureNotTaken }
var userCanDeletePicture : Bool { return self == .pictureTaken }
}
So, now, our updateInterface doesn't need any special knowledge about what each state means; it merely asks the state what the interface should be, which is simpler and gives perhaps a more satisfying separation of powers:
func updateInterface() {
self.takePictureButton.isEnabled = state.userCanTakePicture
self.deletePictureButton.isEnabled = state.userCanDeletePicture
}

Conforming to protocol but nothing happens

I have 3 UIViewControllers. ContainerVC which contains 2 ContainerViews. First Container View is DashboardVC and second one is SidebarVC. The DashboardVC covers the entire screen, while the SidebarVC is outside.
I have a leading constraint for the SidebarVC that should be animated and the SidebarVC should slide in (from the left side). On the DashboardVC I have a UIBarButtonItem and when it's pressed it should perform the animation. The problem is that I'm doing something wrong with the delegate and when the ContainerVC conforms to the protocol, nothing happens.
PS: I have very hard time understanding protocols/delegates despite having watch a bunch of different videos on this concept. Here's the code:
DashboardVC
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
class DashboardVC: UIViewController {
var delegate: SideBarDelegate?
var isSideMenuOpen = true
#IBAction func menuButtonPressed(_ sender: UIBarButtonItem) {
if isSideMenuOpen {
delegate?.showMenu()
isSideMenuOpen = false
}
else {
delegate?.hideMenu()
isSideMenuOpen = true
}
}
}
ContainerVC
class ContainerVC: UIViewController {
#IBOutlet weak var sideBarMenuLeadingConstraint: NSLayoutConstraint!
}
extension ContainerVC : SideBarDelegate {
func showMenu() {
sideBarMenuLeadingConstraint.constant = -290
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func hideMenu() {
sideBarMenuLeadingConstraint.constant = 0
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
You use the delegate only on classes. To prevent memory leaks, do those two things:
Change:
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
to:
protocol SideBarDelegate: class {
func showMenu()
func hideMenu()
}
Now, rename delegate property to:
weak var delegate: SideBarDelegate?
Weak does not increase the reference counting. This is important to prevent memory leaks.
Your instance of ContainerVC must have some sort of reference to an instance of DashboardVC (or make the delegate static but I have never seen something like that). Then, in your viewDidLoad method of ContainterVC, set this:
myInstanceReferenceToDashboardVC.delegate = self

Using NSTreeController with NSOutlineView

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.

UIButton to trigger UIProgressView?

I am attempting to create an app in Swift using a Storyboard. I want to have a button trigger the UIProgressView's animation.
*Expected '{' in body of function declaration*
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func PressKit(sender: AnyObject) {
func startAnimating()
}
}
If you are trying to call the function "startAnimating", then don't use the "func" keyword in front of it, just use "startAnimating()" to call the function. You will then need to define the function (outside of the "PressKit" function). You might want to do this somewhere else in the class using, say:
func startAnimating() {
...some code...
}