The Mother of Joseph Beuys The Mother of Joseph Beuys - 2 months ago 22
Swift Question

Why is Swinject model class registered without ".inObjectScope( .Container )" producing a singleton?

This question is for people with a lot of experience of Swinject for Swift.

I will show the problematic code, and my question is at the bottom.

There's quite a lot of code, sorry about that.

This is

MySwinjectStoryboard.swift
registration:

import Swinject

extension SwinjectStoryboard
{
class func setup ()
{
defaultContainer.register( Stopwatch.self )
{
responder in Stopwatch(
signals: responder.resolve( SignalsService.self )!
)
}

defaultContainer.register( SignalsService.self )
{
_ in SignalsService()
}.inObjectScope( .Container )

defaultContainer.register( ImageService.self )
{
responder in ImageService(
signals: responder.resolve( SignalsService.self )!
, stopwatch: responder.resolve( Stopwatch.self )!
)
}.inObjectScope( .Container )

defaultContainer.registerForStoryboard( StartUpViewController.self )
{
resolvable, viewController in
viewController.stopwatch = resolvable.resolve( Stopwatch.self )!
viewController.image = resolvable.resolve( ImageService.self )!
}
}
}


This is
Stopwatch.swift
, which simply pauses a while before firing an onComplete handler:

import Foundation

class Stopwatch: StopwatchProtocol
{
var key: String { return "Stopwatch_\( _key ).Complete" }

private var
_signals: SignalsProtocol
, _key: UInt16
, _timer: NSTimer?
, _data: AnyObject?

func startWith (
Delay delay: Double
, ForListener closure: ( String, Any? ) -> Void
){
_data = nil
_startWith( Delay: delay, ForListener: closure )
}

func stop ()
{
guard let timer = _timer else { return }
timer.invalidate()
_timer = nil
_data = nil
}

private func _startWith (
Delay delay: Double
, ForListener closure: ( String, Any? ) -> Void
){
stop()

_timer = NSTimer.scheduledTimerWithTimeInterval(
NSTimeInterval( delay )
, target: self
, selector: #selector( _onTimerComplete )
, userInfo: nil
, repeats: false
)
}

@objc private func _onTimerComplete ()
{
stop()
print( "stopwatch with key `\( key )` complete." )
}

required init ( signals: SignalsProtocol )
{
_signals = signals
_key = getPrimaryKey()
print( "primary key: \( _key )" )
}
}


ImageService.swift
presently simply accepts a signals and a stopwatch property via an
init
function:

protocol ImageProtocol {}

class ImageService: ImageProtocol
{
private let
_signals: SignalsProtocol
, _stopwatch: StopwatchProtocol

required init (
signals: SignalsProtocol
, stopwatch: StopwatchProtocol
){
_signals = signals
_stopwatch = stopwatch

lo( "ImageService key: \( _stopwatch.key )" )
}
}


SignalsService.swift
is currently an empty Model class:

protocol SignalsProtocol {}

class SignalsService: SignalsProtocol {}


Whilst
StartUpViewController.swift
is a basic
UIViewController
that currently just accepts its injected properties:

import UIKit

class StartUpViewController: UIViewController
{
var image: ImageService? {
willSet {
guard _image == nil else { return }
_image = newValue
}
}

var signals: SignalsService? {
willSet {
guard _signals == nil else { return }
_signals = newValue
}
}

var stopwatch: StopwatchProtocol? {
willSet {
guard _stopwatch == nil else { return }
_stopwatch = newValue
print( "StartUpViewController key: \( _stopwatch.key )" )
}
}

internal var
_image: ImageService!
, _signals: SignalsService!
, _stopwatch: Stopwatch!
}


And finally
getPrivateKey()
is simply a global static, returning unique Ints:

private var _primaryKey = UInt16( 0 )

func getPrimaryKey () -> UInt16
{
_primaryKey += 1
return _primaryKey
}


Now as I understand it, the way I have registered
Stopwatch.swift
in
MySwinjectStoryboard.swift
means that each time an instance is injected, it will be a new, discrete instance. However, both
ImageService.swift
and
StartUpViewController.swift
are being injected with the same instance:

StartUpViewController key: Stopwatch_2.Complete
ImageService key: Stopwatch_2.Complete


ImageService
's key ought to be:

ImageService key: Stopwatch_3.Complete


Does anyone know why this is happening, please? Thank you.

Answer

Default scope for services is .Graph. From documentation:

With ObjectScope.Graph, an instance is always created, as in ObjectScope.None, if you directly call resolve method of a container, but instances resolved in factory closures are shared during the resolution of the root instance to construct the object graph.

If you want a unique instance to be created for each reference even during object graph resolution, you should use object scope .None, i.e.

defaultContainer.register(Stopwatch.self) { resolver in 
    Stopwatch(signals: resolver.resolve(SignalsService.self)!)
}.inObjectScope(.None)