Swift: how to change a class into a struct - swift

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.

Related

Modify #Published variable from another class that is not declared in | SwiftUI

I want to put the logic of all my #Published in a model class, however when I try to separate it, it doesn't update. I recreated a little example:
The code below works, it increases every time the button is clicked:
struct ContentView: View {
#StateObject var myClass = MyClass()
var body: some View {
Button(action: {
myClass.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
func doStuff(numb: Int) {
people += numb
}
}
However, once I split the logic and try to have my #Published in a separate class to have it more clean, it doesn't update, see below:
struct ContentView: View {
#StateObject var myClass = MyClass()
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass()
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
}
I think it's because there are two different instances in the view right? Anyway, how can I separate the #Publish correctly have it updated?
Thanks
Your first form is absolutely fine! You may, though, consider your ContentView using a #ObservedObject instead a #StateObject.
Your second form is flawed, for several reasons:
don't move logic into a view
don't use class variables to keep "state".
The first statement is due to a sane design that keeps your models and views nicely separated.
The second statement is due to how SwiftUI works. If you need to have some "state" in your views, use #State where the value is a struct.
Using #State ensures, that it's value is bound to the actual life time of the "conceptual view", i.e. the thing we human perceive as the view. And this "conceptual view" (managed as some data object by SwiftUI) is distinct from the struct View, which is merely a means to describe how to create and modify this conceptual view - that is, struct view is rather a function that will be used to initially create the "conceptual view" and modify it. Once this is done, it gets destroyed, and gets recreated when the conceptual view needs to be modified. That also means, the life time of this struct is not bound to the life time of its "state". The state's life time is bound to the conceptual view, and thus has usually longer life time than the struct view, where the struct view can be created and destroyed several times.
Now, imagine what happens when you always execute let modify = Modify() whenever the (conceptual) view or its content view is modified and needs to be recalculated and rendered by creating a struct view and then - after it has been rendered - destroying it again.
Also, this "state" is considered private for the view, means it is considered an implementation detail for the view. If you want to exchange data from "outside" and "inside" use a #ObservedObject or a Binding.
The problem is that you have 2 separate instances of MyClass:
#StateObject var myClass = MyClass()
var myClass = MyClass()
You are updating the myClass in Modify, which you aren't receiving updates from. A way to fix this is by having one instance of MyClass, passed into Modify during initialization:
struct ContentView: View {
#StateObject var myClass: MyClass
let modify: Modify
init() {
let temp = MyClass()
_myClass = StateObject(wrappedValue: temp)
modify = Modify(myClass: temp)
}
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
let myClass: MyClass
init(myClass: MyClass) {
self.myClass = myClass
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
Another method is to have a #Published property in Modify to observe the changes of MyClass:
struct ContentView: View {
#StateObject var modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(modify.myClass.people)")
}
}
}
class Modify: ObservableObject {
#Published var myClass = MyClass()
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = myClass.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
you could try this approach using a singleton. Works well for me:
struct ContentView: View {
#StateObject var myClass = MyClass.shared // <--- here
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass.shared // <--- here
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
static let shared = MyClass() // <--- here
}

How to stop variable from going back to default value when read in different file

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

Why is my array empty when updating the view? (Swift)

Whilst debugging I can see that the fixtureList in the controller returns 6 values. However, when the code jumps into the fixtureView.update(...) within the View, the fixtureList value is 0.
I'm sure it's something to do with creating the same variable in both controller and view, but can't figure it out!
What would be the best way to resolve this issue? Esentially I want the 6 values to be received by the View so that the user interface can be populated.
Controller
var fixtureList = [Fixture]()
func updateScores() {
liveScoreApi.fetchFixtures() { (
isOK, fixture) in
if isOK == true {
for fixture in (fixture?.fixtures)! {
self.fixtureList.append(fixture)
}
self.fixtureView.update(/*dump*/(self.fixtureList))
}
else {
NSLog("error fetching!")
}
}
}
View
class FixtureView: NSView {
#IBOutlet weak var homeNameTextField: NSTextField!
#IBOutlet weak var timeTextField: NSTextField!
#IBOutlet weak var awayNameTextField: NSTextField!
let fixtureList = [Fixture]()
func update(_ fixture: [Fixture]) {
// do UI updates on the main thread
DispatchQueue.main.async {
for fixture in self.fixtureList {
self.homeNameTextField.stringValue = fixture.homeName
self.timeTextField.stringValue = fixture.time
self.awayNameTextField.stringValue = fixture.awayName
print("added \(fixture.homeName)")
}
}
}
}
Your update method should look like this:
//let fixtureList = [Fixture]() // you don't need this
func update(_ fixtureList: [Fixture]) {
// do UI updates on the main thread
DispatchQueue.main.async {
// (though you probably don't want this ultimately)
for fixture in fixtureList {
self.homeNameTextField.stringValue = fixture.homeName
self.timeTextField.stringValue = fixture.time
self.awayNameTextField.stringValue = fixture.awayName
print("added \(fixture.homeName)")
}
}
}
The problem is that you are newing up an array of Fixture objects at the class level. Then, inside your update method, you are attempting to iterate over the new array rather than your passed in array.
Just eliminate the unnecessary array at the class level.
change the name of your parameter to fixtureList
remove self. from fixtureList your for in loop

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.

variable cannot be modified in another class

am trying to modify a variable in another class, but it doesn't change.
class ViewController: UIViewController {
var t = 1
override func viewDidLoad() {
super.viewDidLoad()
t = 9
var pp = action().test()
println(pp) // got “2” here
}
the above should print "10" , but it shows "2".
another swift file:
class action {
var k = ViewController().t
func test()->Int{
k++
return k
}
}
Anything I made wrong?
Thnaks.
When you have a class with properties, and create an instance of that class, properties are bound to the class instance and not the class type. So if you create 2 instances of the same class their properties are independent, so if you change a property in one instance, that won't affect the same property in the other instance.
Note: by convention, in swift type names always start in uppercase, so I have renamed action to Action in my code.
Your code isn't working as expected because in the Action class you are creating a new instance of ViewController:
var k = ViewController().t
which has no relationship with the instance used to instantiate Action - so the new instance will have its t property set to 1.
The correct way to fix it is to pass the view controller instance to Action, and let it work on that instance.
class ViewController: UIViewController {
var t = 1
override func viewDidLoad() {
super.viewDidLoad()
t = 9
var action = Action(viewController: self)
var pp = action.test()
println(pp) // got “2” here
}
}
class Action {
var k: Int
init(viewController: ViewController) {
self.k = viewController.t
}
func test()->Int{
k++
return k
}
}
The above code should give an indication of what's wrong with your code, but it can be written in a better way. Action doesn't really need the ViewController instance, it just needs an integer passed in to its initializer, so a better way to achieve the same result is by modifying the code as follows:
class ViewController: UIViewController {
var t = 1
override func viewDidLoad() {
super.viewDidLoad()
t = 9
var action = Action(t: self.t)
var pp = action.test()
println(pp) // got “2” here
}
}
class Action {
var k: Int
init(t: Int) {
self.k = t
}
func test()->Int{
k++
return k
}
}