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.
Related
I have a DiscoveredSerialNumbers class that I want to access from various swift files:
class DiscoveredSerialNumbers {
var snConnect: String = ""
}
In my ViewController I change the value of snConnect based on the selection from a Picker View.
class ViewController: UIViewController, UIPickerViewDataSource,UIPickerViewDelegate {
#IBOutlet weak var SerialNumbers: UIPickerView!
var serialNums: [String] = [String]()
...
override func viewDidLoad() {
...
SerialNumbers.dataSource = self
SerialNumbers.delegate = self
}
...
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let global = DiscoveredSerialNumbers()
global.snConnect = serialNums[row]
print(serialNums[row])
print(global.snConnect)
}
}
When I print out the new value of snConnect set in the following line:
global.snConnect = serialNums[row]
Immediately afterward I get the new updated value of snConnect.
However, when I try to access the updated value of snConnect in a different swift file that controls a different ViewController in the following code:
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
let global = DiscoveredSerialNumbers()
var sn = global.snConnect
...
}
The value of snConnect reverts back to the original value which is "".
How do I stop the value from reverting back to the initial value? I think it has something to do with me initializing the class DiscoveredSerialNumbers but I do not know how to access the value of snConnect in a different swift file otherwise.
Edit: Thanks to Don's comments, I am trying to have the snConnect value persist between instances of the application launching. I want to set the value of snConnect in the main app and access it when I launch an extension to the main app, in this case a custom keyboard extension.
Update: Question was a bit misleading you actually need to save the variable. I'm not sure if UserDefaults for app and keyboard extension are the same, you can try this.
class DiscoveredSerialNumbers {
static var main = DiscoveredSerialNumbers()
var snConnect: String {
get {
// Read from UserDefaults
return UserDefaults.standard.string(forKey: "snConnect") ?? ""
}
set {
// Save to UserDefaults
UserDefaults.standard.set(newValue, forKey: "snConnect")
}
}
init() {
print("New instance of DiscoveredSerialNumbers initialized.")
}
}
You can do this with a number of different ways,
however easiest one is creating a singleton of DiscoveredSerialNumbers() object, so your object and values can be used globally through it.
(although this method should be used with caution, it can cause a number of problems)
class DiscoveredSerialNumbers {
static var main = DiscoveredSerialNumbers()
var snConnect: String = ""
init() {
print("New instance of DiscoveredSerialNumbers initialized.")
}
}
now whenever you call DiscoveredSerialNumbers.main.snConnect old value will be kept and can be used/changed from anywhere.
Edit: Here's a sample Playground code for you to test out how singletons work
class Singleton
{
var someVariable = ""
static var main = Singleton()
}
class ClassA
{
init() {
Singleton.main.someVariable = "Hey I was changed in Class A"
}
}
class ClassB
{
init() {
print(Singleton.main.someVariable)
Singleton.main.someVariable = "And now I'm changed in class B"
}
}
let _ = ClassA()
let _ = ClassB()
print(Singleton.main.someVariable)
For an app extension to access data stored through it's container app, both the application and extension need to be part of the same app group. App groups are set in Signing & Capabilities section of Xcode for your project.
Once your app and extension are part of the same app group, you can use the following code to set the value of a global variable:
let defaults = UserDefaults(suiteName:"group.dataShare")
defaults?.set(serialNums[row], forKey: "snConnect")
Where group.dataShare is the name of your App group.
To retrieve the value, you can use the following code in your extension:
let defaults = UserDefaults(suiteName:"group.dataShareImada")
var sn = defaults?.string(forKey: "snConnect")
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
}
I am working on a project containing a "Create New Account" view controller with its accompanying Swift class called "CreateNewAccount." The user can place 4 input values into this view controller, a first name, last name, user name, and password. Upon clicking the "Create Account" button in this VC, the 4 input values are passed on to a Swift class (within the model layer of MVC, I believe) called UserInfoRetrieveModel where they are supposedly stored.
I would then like to pass these values to another Swift class (that is a model as well) called UserInfoModel, which will then delegate out the first name value to the text value of label located in a VC called "ThanksForJoining" (and its accompanying class).
I have figured out how to pass values from VC to model (CreateNewAccount to UserInfoRetrieveModel) and from model to VC (UserInfoModel to ThanksForJoining), but somewhere in my transference from model to model (UserInfoRetrieveModel to UserInfoModel) the values initially inputted into "CreateNewAccount," which I would like to pass over to the second model class UserInfoModel become nil.
Below is the code for CreateNewAccount, UserInfoRetrieve, UserInfo, and ThanksForJoining:
CreateNewAccount ->
import UIKit
class CreateNewAccount: UIViewController{
#IBOutlet weak var FNInput: UITextField!
#IBOutlet weak var LNInput: UITextField!
#IBOutlet weak var usernameInput: UITextField!
#IBOutlet weak var passwordInput: UITextField!
var uInfoRetrieve = UInfoRetrieveModel()
#IBAction func thanksForJoining(_ sender: Any) {
uInfoRetrieve.firstName = FNInput.text!
uInfoRetrieve.lastName = LNInput.text!
uInfoRetrieve.userName = usernameInput.text!
uInfoRetrieve.password = passwordInput.text!
uInfoRetrieve.delegate = self
uInfoRetrieve.retrieving()
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension CreateNewAccount: UInfoRetrieveModelDelegate{
func credentialTransfer(data: String) {
print(data)
}
}
UserInfoRetrieve ->
import Foundation
protocol UInfoRetrieveModelDelegate: class {
func credentialTransfer(data:String)
}
class UInfoRetrieveModel: NSObject {
weak var delegate: UInfoRetrieveModelDelegate?
var firstName: String = ""
var lastName: String = ""
var userName: String = ""
var password: String = ""
func retrieving(){
delegate?.credentialTransfer(data: firstName)
delegate?.credentialTransfer(data: lastName)
delegate?.credentialTransfer(data: userName)
delegate?.credentialTransfer(data: password)
}
}
UserInfo ->
import Foundation
protocol UserInfoModelDelegate: class {
func didReceiveDataUpdate(data: String)
}
class UserInfoModel {
weak var delegate: UserInfoModelDelegate?
let uInfoRetrieve = UInfoRetrieveModel()
func requestData() -> Array<String> {
let firstName = uInfoRetrieve.firstName
let lastName = uInfoRetrieve.lastName
let userName = uInfoRetrieve.userName
let password = uInfoRetrieve.password
delegate?.didReceiveDataUpdate(data: firstName)
delegate?.didReceiveDataUpdate(data: lastName)
delegate?.didReceiveDataUpdate(data: userName)
delegate?.didReceiveDataUpdate(data: password)
let credentials = [firstName, lastName, userName, password] as [Any]
return credentials as! Array<String>
}
}
ThanksForJoining ->
import UIKit
class ThanksForJoining: UIViewController {
var userInfo = UserInfoModel()
#IBOutlet weak var firstName: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
userInfo.delegate = self
firstName.text = userInfo.requestData()[0]
print("yo")
print(userInfo.requestData()[0])
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ThanksForJoining: UserInfoModelDelegate {
func didReceiveDataUpdate(data: String) {
print(data)
}
}
UserInfoModel and CreateNewAccount do both create a new instance of UInfoRetrieveModel. You have to connect them properly for them to pass on information.
Connecting properly means (in the simplest form) one constructs the other and sets itself as the delegate of the other, so UInfoRetrieveModel can pass on data. The constructing of a child model is usually done via a computed property.
Example
struct Account {
let firstName: String, lastName: String
let userName: String, password: String
}
extension UInfoRetrieveModelDelegate: class {
createAccount(_ account: Account): Bool
}
extension UserInfoModel: UInfoRetrieveModelDelegate{
func createAccount(_ account: Account) -> Bool {
// Handling creation of account.
return success == true
}
var newUInfoRetrieveModel: UInfoRetrieveModel {
let helperModel = UInfoRetrieveModel(parent: self)
helperModel.delegate = self
return helperModel
}
}
Explanation
Yes. You usually have a Model, your data, then have something that controls access to it to make changes on your model, manages how the model is stored, maybe syncing with a cloud-service, thats the ModelController which you pass around between ViewControllers, more/other controllers you usually use incase that makes things simpler. In your case you would probably pass createAccount(the call) on to a controller/viewController which is in charge of telling the modelController to create the account and then telling one of its views/viewControllers to display the modal/whatever.
The usual way to pass data to a higher level is to have for the viewController/controller a delegate it uses to communicate with higher up, the one âresponsible for actions the ViewController/controller cannot do by itselfâ, eg pushing data up(creation calls, modification calls, deletion calls) if it makes no sense to give it a modelController since its not control of that part of the application, etc. In your case you can of course pass a modelController to each little viewController/view, but its usually more practical/simpler to only give it to the one controlling the part and let others communicate with that currently-that-part-controlling controller/viewController.
More partical here means that you may not want CreateAccountViewController to display the success dialog, but rather another, which CreateAccountViewController can then do not by itself since itâs not on the stack anymore.
Using the MVC approach for iOS app development, I would like to observe changes to the model by posting to the NotificationCenter. For my example, the Person.swift model is:
class Person {
static let nameDidChange = Notification.Name("nameDidChange")
var name: String {
didSet {
NotificationCenter.default.post(name: Person.nameDidChange, object: self)
}
}
var age: Int
var gender: String
init(name: String, age: Int, gender: String) {
self.name = name
self.age = age
self.gender = gender
}
}
The view controller that observes the model is shown below:
class ViewController: UIViewController {
let person = Person(name: "Homer", age: 44, gender: "male")
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var ageLabel: UILabel!
#IBOutlet weak var genderLabel: UILabel!
#IBOutlet weak var nameField: UITextField!
#IBOutlet weak var ageField: UITextField!
#IBOutlet weak var genderField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = person.name
ageLabel.text = String(person.age)
genderLabel.text = person.gender
NotificationCenter.default.addObserver(self,
selector: #selector(self.updateLabels),
name: Person.nameDidChange, object: nil)
}
#IBAction func updatePerson(_ sender: Any) {
guard let name = nameField.text, let age = ageField.text, let gender = genderField.text else { return }
guard let ageNumber = Int(age) else { return }
person.name = name
person.age = ageNumber
person.gender = gender
}
#objc func updateLabels() {
nameLabel.text = person.name
ageLabel.text = String(person.age)
genderLabel.text = person.gender
}
}
The example app works as follows:
Enter a name, age, and gender in the text fields
Press the updatePerson button to update the model from the text field values
When the model is updated, the notification observer calls the updateLabels function to update the user interface.
This approach requires the person.name to be set last otherwise the updatePerson button must be pressed twice to update the entire user interface. And since I'm only observing one property, the notification does not represent the entire class. Is there a better way to observe changes of models (a class or struct) in Swift?
Note - I am not interested in using RxSwift for this example.
This is more of a dumping comment than a fulfilling answer. But long story short KVO is the feature you should be using, not NotificationCenter. The binding process becomes significantly more simple in Swift4
As for what KVO is: See here and here. For some examples which are MVVM focused you can see here and here. And don't let the MVVM sway you away. It's just MVC with bindings which you are trying to do the exact same thing + moving the presentation logic to a different layer.
A simple KVO example in Swift 4 would look like this:
#objcMembers class Foo: NSObject {
dynamic var string: String
override init() {
string = "hotdog"
super.init()
}
}
let foo = Foo()
// Here it is, kvo in 2 lines of code!
let observation = foo.observe(\.string) { (foo, change) in
print("new foo.string: \(foo.string)")
}
foo.string = "not hotdog"
// new foo.string: not hotdog
You can also create your own Observable type like below:
class Observable<ObservedType>{
private var _value: ObservedType?
init(value: ObservedType) {
_value = value
}
var valueChanged : ((ObservedType?) -> ())?
public var value: ObservedType? {
get{
return _value // The trick is that the public value is reading from the private value...
}
set{
_value = newValue
valueChanged?(_value)
}
}
func bindingChanged(to newValue : ObservedType){
_value = newValue
print("value is now \(newValue)")
}
}
Then to create an observable property you'd do:
class User {
// var name : String <-- you normally do this, but not when you're creating as such
var name : Observable<String>
init (name: Observable<String>){
self.name = name
}
}
The class above (Observable) is copied and pasted from Swift Designs patterns book
To simply visualize the picture, you should be aware of the fact that you are observing only the name change. So it doesn't make sense to update all of the other properties of Person. You are observing name change and it's being updated accordingly, let alone others.
So it's not an ideal assumption that age and gender might have been changed in the process of changing name. Being said that, you should consider observing all of the properties one by one and bind actions differently and modify only the UI component that is mapped to that specific property.
Something like this:
override func viewDidLoad() {
...
NotificationCenter.default.addObserver(self,
selector: #selector(self.updateName),
name: Person.nameDidChange, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(self.updateAge),
name: Person.ageDidChange, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(self.updateGender),
name: Person.genderDidChange, object: nil)
...
}
#objc func updateName() {
nameLabel.text = person.name
}
#objc func updateAge() {
ageLabel.text = String(person.age)
}
#objc func updateGender() {
genderLabel.text = person.gender
}
I have written a very simple MVC program with a label and a button. The label displays the value of a counter which is incremented when the button is pressed.
The code that follows gives the model file and the view controller file. The model file is a class.
That code works.
Here is the model file:
import Foundation
protocol MVCModelDelegate {
var message: String {get set}
}
// when I change class to struct
// delegate is set to nil
// causing the program not to update the label
class Model {
var delegate: MVCModelDelegate?
var counter: Int
var message: String {
didSet {
delegate?.message = model.message
}
}
init(counter: Int, message: String) {
self.counter = counter
self.message = message
}
}
// create instance
var model = Model(counter: 0, message: "")
// counting
func incrementCounter() {
model.counter += 1
model.message = "Counter Value: \(model.counter)"
}
Here is the view controller file
import Cocoa
class ViewController: NSViewController, MVCModelDelegate {
// communication link to the model
var model1 = model
var message = model.message {
didSet {
didUpdateModel(message: message)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.model1.delegate = self
}
// update display
func didUpdateModel(message: String) {
Label1.stringValue = model1.message
}
// Label
#IBOutlet weak var Label1: NSTextField! {
didSet {
Label1.stringValue = " counter not started"
}
}
// Button
#IBAction func testButton(_ sender: NSButton) {
incrementCounter()
}
}
Problem: I now want to change the code of the Model file to use a struct in place of a class as this very simple program does not need all the functionalities of a class. But as soon as I change class for struct. The program still runs, but the label is not updated as the delegate variable is set to nil and never gets another value.
Question: What am I missing? The swift documentation encourages to use struct when possible. And I do not see in that code what could be a problem of transforming the class into a struct.
A struct is immutable and it is not passed by reference like a class, but by value. This means that when you do
var model1 = model
You are creating a copy of model.
modifying model1 will not propagate the changes to model, therefore all you do in incrementCounter is not reflected in your view controller.
If you want your incrementCounter to affect the viewController it must use the model in your viewController instance.
If your intention is to share the model properties and change the label value when this model is updated, struct is not the best choice.