Swinject: Resolution failure logging and SwinjectStoryboard

Created on 30 Dec 2016  路  11Comments  路  Source: Swinject/Swinject

As pointed out in #210, when using SwinjectStoryboard, logging feature will spit out a lot of redundant information, because every UIViewController which is instantiated from storyboard and does not have storyboardInitCompleted will be treated as resolution failure, even though it is not fatal.

I see 2 approaches to this issue

  • ignore it, which would probably cause every user of SwinjectStoryboard to disable logging feature
  • disable logging for non fatal resolution failures: this would probably require extensive redesign of logging feature, and would not be ideal, as there might be unexpected non-fatal failures (e.g. resolving optional dependency)
  • add option to disable logging for individual resolve calls, and disable it for SwinjectStoryboard by default.

Most helpful comment

Sorry, decided to delete that blog as it wasn't really doing much. Hang a sec, I'll see if I can find it. Here you go:

Container.loggingFunction = {

            // Find the text that contains the missing registration.
            if let startOfMissingRegistration = $0.range(of: "Swinject: Resolution failed. Expected registration:\n\t")?.upperBound,
                let startOfAvailableOptions = $0.range(of: "\nAvailable registrations:")?.lowerBound {
                let missingRegistration = $0[startOfMissingRegistration ..< startOfAvailableOptions]

                // Ignore all reports for UIKit classes.
                if missingRegistration.contains("Storyboard: UI")
                    || missingRegistration.contains("Storyboard: MyApp.IgnoreThisViewController")
                    /* || other classes you want to ignore here */  {
                    return
                }

                // Print the missing registration.
                print("Swinject failed to find registration for \(missingRegistration)")
                return
            }

            // Some other message so just print it.
            print($0)
        }

Hope that helped.

All 11 comments

IMO, setting Container.logingFunction to nil is ok for now if SwinjectStoryboard is used. This is the first option disabling logging feature. Then we can take time to investigate and redesign the logging feature as you wrote in the second option for Swinject v3.

I thought like above because adding an option to disable logging for individual resolve calls, as you wrote at last, looked too much implementation for logging.

We have this problem in an unrelated context. We have extensions methods on Resolver that wrap resolve<T>(:name:) -> T?, they are required<T>(:name:) -> T and resolve<T>(:name:defaultValue:) -> T. They act as you would imagine with required calling fatalError if it resolve returns a nil and the default value version just returns the default value if resolve returns nil.

Currently our logs are spammed with information about failed resolutions when we actually are providing default values. It's good to see we can disable these; since we use required we don't lose functionality.

Maybe Swinject could adopt these methods as well. Logging only in the appropriate context would be easy at that point. Another interesting idea is that our required methods make use of Swifts (#file, #line, #function) debug symbols to provide the exact context where the unfulfilled request occurs.

I'd be happy to PR them if it's a welcome idea.

@kdubb Thanks for the idea!
In what scenarios are you using resolve(:defaultValue:) version? While using Swinject in my projects I never needed such construct - knowing usecases for Swinject I've not yet encountered would be helpful for future improvements.

I like adding #file #line to log output - it would ease debugging. Even though it is probably relevant only for non-fatal failures (if your program crashes, you can usually track exact line in debugger) it might be worth it.

As I mentioned in OP this feature probably needs rework as - among other things - it collides with extensions such as SwinjectStoryboard or SwinjectAutoregistration. It looks good to extend log output with that change (probably something like optional parameter in resolve function which can enable / disable logging for that invocation).

We use the default value cases to provide simple values (e.g. strings, URLs, etc) but override them from the outside; mostly for testing purposes. Specifically it allows our tests to redirect network requests and set network timeouts from the outside while letting the code itself inscribe the correct values.

Also, the reason #file and #line are output via fatalError when a required resolution cannot be provided is to ease finding and fixing the requirement during development; not to provide information in crash scenarios.

@kdubb Thanks for the example, I see usecases of resolve(:defaultValue) for simple values. However, I wouldn't include such method in Swinject, as it is currently not possible to make it robust enough to use for non trivial values (correct me if I am wrong):

container.register(MyType.self) { r in MyTypeImpl(foo: r.resolve(Foo.self)!) }
container.resolve(MyType.self, defaultValue: MyTypeDefault()) // will crash if there is no `Foo` registration

Also, it seems to me that simple container.resolve(MyType.self) ?? MyTypeDefault() would be sufficient for most cases.

Having the same issue here. Some people are adding registration for UIKit classes so they don't appear in the logging. My suggestion would be to add a logging filter of some sort. Perhaps so that any class with a 'UI' prefix is ignored.

Blog is now deleted. See below for a copy of the code.

I have an override of the logging function which I use. I've blogged it here. Basically it examines each incoming log message and strips out the listing of the container registrations, leaving just a single like for the (possibly) missing registration. It's a bit crude, but does the job for us.

@drekka , do you still have your blog available? I'm getting file not found.

Sorry, decided to delete that blog as it wasn't really doing much. Hang a sec, I'll see if I can find it. Here you go:

Container.loggingFunction = {

            // Find the text that contains the missing registration.
            if let startOfMissingRegistration = $0.range(of: "Swinject: Resolution failed. Expected registration:\n\t")?.upperBound,
                let startOfAvailableOptions = $0.range(of: "\nAvailable registrations:")?.lowerBound {
                let missingRegistration = $0[startOfMissingRegistration ..< startOfAvailableOptions]

                // Ignore all reports for UIKit classes.
                if missingRegistration.contains("Storyboard: UI")
                    || missingRegistration.contains("Storyboard: MyApp.IgnoreThisViewController")
                    /* || other classes you want to ignore here */  {
                    return
                }

                // Print the missing registration.
                print("Swinject failed to find registration for \(missingRegistration)")
                return
            }

            // Some other message so just print it.
            print($0)
        }

Hope that helped.

Closing due to inactivity

Was this page helpful?
0 / 5 - 0 ratings