Singleton class initialisation creates multiple versions

The attached code has a singleton class, and a superclass based on it.
When I run the code, I get:
Singleton explicit init
Singleton explicit init
SuperClass explicit init
Base device count: 2

Singleton explicit init
SuperClass explicit init
Base device count: 2

Singleton explicit init
SuperClass explicit init
Base device count: 2

Device count: 3

  1. Why is the init() in the singleton class re-run every time a new instance of the superclass is created?
  2. Why is the print() in the static initialisation of the singleton class never executed?
  3. The singleton class is initialised with 2 elements in the deviceList array, but then has another element added at line 7 in func test(), yet each time a member of the superclass is subsequently initialised, the superclass only reports 2 elements in the deviceList. Why?

If the print line 30 in the SingletonClass explicit init() is commented out so that the compiler should eliminate it, the print() in the static initialisation still never happens, with the result:

SuperClass explicit init
Base device count: 2

SuperClass explicit init
Base device count: 2

SuperClass explicit init
Base device count: 2

Device count: 3

import Foundation

func test() {
let singletonClass = SingletonClass()
var superList = SuperClass

singletonClass.addDevice()

for index in 0..<singletonClass.deviceList.count {
    superList.append(SuperClass(thisDevice: singletonClass.deviceList[index]))
}
print("Device count: \(superList.count)")

}

test()

struct Device: Identifiable {
let id: Int
}

class SingletonClass {
static let shared: SingletonClass = {
let instance = SingletonClass()
print("Singleton init")
return instance
}()
var deviceList = [Device(id: 99), Device(id: 98)]

init() {
    print("Singleton explicit init")
}

func addDevice() {
    let i = deviceList.count
    deviceList.append(Device(id: i))
}

}

class SuperClass: SingletonClass {
var thisDevice: Device

init(thisDevice: Device) {
    self.thisDevice = thisDevice
    super.init()
    print("SuperClass explicit init")
    print("Base device count: \(deviceList.count)");print()
}

}

The code to create the Singleton comes from Apple
https://developer.apple.com/documentation/swift/managing-a-shared-resource-using-a-singleton

For explanation, the attached test code is extremely simplified, but the singleton class represents a Bluetooth BLEManager, and the devices are asynchronously found devices such as a heart rate monitor, a blood oxygen monitor, or similar, each of which needs to communicate through the singleton BLEManager.

The Apple example you linked is missing a key element. After defining the Singleton with a static variable, you should not call SingletonClass() anywhere. Instead you would use SingletonClass.shared where you need to reference it. As long as you access the singleton using shared, it will only be initialized once.

In terms of your SuperClass, that actually appears to be a subclass of SingletonClass. So by calling super.init(), that is initializing SingletonClass (and not using the .shared instance)

1 Like

I'm inferring what your goals are but you could use a singleton to hold devices like this. Note clients would use it directly with DeviceList.shared. Nothing would need to subclass this.

struct Device: Identifiable {
    let id: Int
}

class DeviceList {
    
    var deviceList = [Device(id: 99), Device(id: 98)]
    
    static let shared: DeviceList = {
        let instance = DeviceList()
        print("Singleton init")
        return instance
    }()
    
    init() {
        print("Singleton explicit init")
    }
    
    func addDevice(device: Device) {
        let i = deviceList.count
        deviceList.append(device)
    }
}

DeviceList.shared.addDevice(device: Device(id: 1))
DeviceList.shared.addDevice(device: Device(id: 2))

If you still wanted that second class, you could do:

class SuperClass {
    var thisDevice: Device
    
    init(thisDevice: Device) {
        self.thisDevice = thisDevice
        DeviceList.shared.addDevice(device: thisDevice)
        print("SuperClass explicit init")
    }
}
1 Like

That makes a step forward!
I changed line 4 to add the shared
let singletonClass = SingletonClass.shared
which now causes the print() in the static initialisation to be called, so with commenting out line 30 to remove that print(), I now get

Singleton init
SuperClass explicit init
Base device count: 2

SuperClass explicit init
Base device count: 2

SuperClass explicit init
Base device count: 2

Device count: 3

But I expect the 'Base device count' to report 3 every time, not 2.

If I comment out line 44 to remove the super.init(), I get an error on line 46
'self' used in property access 'deviceList' before 'super.init' call

My naming may be suspect in terms of superclass vs subclass, but how do I create a new class which relies on my shared SingletonClass?

Thanks again. Actually my SingletonClass when its the BLEManager has lots of functions that the devices need access to - its not the shared DeviceList that's the problem. I need the whole SingletonClass to be accessible to a found device.

For the sake of discussion, lets rename SuperClass NewClass which is based on SingletonClass.

So how should NewClass be declared so that 'Base device count' shows 3 elements. Any change to SingletonClass should be part of every instance of it.

Typically you'd want to make a singleton class final and its init private.
Otherwise it would be possible making "more than one instance of a singleton" (an oxymoron!)

Probably this is the setup you are looking for:

final class BLEManager {
    static let singleton = BLEManager()
    private init() { ... }
    func someMethodCall() { ... }
}

class Subclass: BLEManager {...}   // 🛑 error
let manager = BLEManager()         // 🛑 error
let manager = BLEManager.singleton // ✅ ok

class Device { // many of these
    func foo() {
        BLEManager.singleton.someMethodCall()
    }
}
7 Likes

@tera that advice seems to have done the trick. SubClass shouldn't inherit from SingeltonClass directly, but add a 'let' instance to provide access to all of SingeltonClass's functions and values.

So my contrived example code below now runs from the command line and always shows 3 as the correct 'Base device count'.

import Foundation

func test() {
    let singletonClass = SingletonClass.shared
    var subList = [SubClass]()
    
    singletonClass.addDevice()
    
    for index in 0..<singletonClass.deviceList.count {
        subList.append(SubClass(thisDevice: singletonClass.deviceList[index]))
    }
    print("Device count: \(subList.count)")
}

test()

struct Device: Identifiable {
    let id: Int
}

final class SingletonClass {   // representing BleManager
    static let shared: SingletonClass = {
        let instance = SingletonClass()
       print("Singleton init")
       return instance
   }()
    var deviceList = [Device(id: 99), Device(id: 98)]
   
   private init() {
//        print("Singleton explicit init")
   }
    
   func addDevice() {
       let i = deviceList.count
       deviceList.append(Device(id: i))
   }
}

class SubClass {
   var thisDevice: Device
   let manager = SingletonClass.shared  // provides access to BleManager
   
   init(thisDevice: Device) {
       self.thisDevice = thisDevice
       print("DeviceClass explicit init")
        print("Base device count: \(manager.deviceList.count)");print()
   }
}

Line 10 is supposed to pick up the actual DeviceClass.id to use to create the new SubClass instance, but that is not relevant to this post.

Thanks to @ibex10 for explaining how to format code with 3 backticks.