Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency Injection with swift with dependency graph of two UIViewControllers without common parent

How we can apply Dependency injection without using a Framework when we have two UIViewControllers that are very deep in the hierarchy and they both need the same dependency that holds state and those two UIViewControllers they don't have a common parent.

Example:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

let's sat that VC4 and VC8 they both need UserService that holds the current user.

Note that we want to avoid Singleton.

Is there an elegant way to handle this kind of DI situations ?

After some research I found that some mention Abstract Factory, Context interfaces, Builder, strategy pattern

But I could not find an example on how to apply that on iOS

like image 376
iOSGeek Avatar asked Nov 30 '18 15:11

iOSGeek


4 Answers

Okay, I'll give this a try.

You said "no singleton", so I exclude that in the following, but please also see the bottom of this answer.

Josh Homann's comment is a good pointer for one solution already, but personally I have my problems with the coordinator pattern.

As Josh correctly said view controllers shouldn't know (much) about each other [1], but then how is e.g. a coordinator or any dependency passed around/accessed? There's several patterns out there that suggest how, but most have an issue that's basically going against your requirement: They more or less make the coordinator a singleton (either itself or as a property of another singleton like the AppDelegate). A coordinator is often times de factor a singleton, too (but not always, and it doesn't have to be).

What I tend to do is relying on simple initialized properties or (most often) lazy properties and protocol oriented programming. Let's construct an example: UserService shall be the protocol defining all the functionality your service needs, MyUserService its implementing struct. Let's assume UserService is a design construct that basically functions as a getter/setter system for some user related data: Access tokens (e.g. saved in the keychain), some preferences (an avatar image's URL) and the like. On initialization MyUserService also prepares the data (loads from the remote, for example). This is to be used in several independent screens/view controllers and is not a singleton.

Now each view controller that is interested in accessing this data has a simple property for it:

lazy var userService: UserService = MyUserService()

I keep it public because that allows me to easily mock/stub it in unit tests (if I need to do that, I can create a dummy TestUserService that mocks/stubs the behavior). The instantiation could also be a closure that I can easily switch out during a test if the init needs parameters. Obviously the properties don't even necessarily need to be lazy depending on what the objects actually do. If instantiating the object ahead of time does no harm (keep unit tests in mind, also outgoing connections), just skip the lazy.

The trick is obviously to design UserService and/or MyUserService in a way that doesn't lead to problems when creating multiple instances of it. However, I found that this is not really a problem 90% of the time as long as the actual data the instance is supposed to rely on is saved somewhere else, in a single point of truth, like the keychain, a core data stack, user defaults, or a remote backend.

I am aware this is kind of a cop-out answer, as in a way I am just saying describing an approach that's (at least part of) many generic patterns out there. However I found this to be the most generic and simple form to approach dependency injection in Swift. The coordinator pattern can be used orthogonal to it, but I found it to be less "Apple-like" in day to day use. It does solve a problem, but mostly the one you get it you don't properly use storyboards as they are intended (especially: just using them as "VC repos", instantiating them from there and transitioning yourself in code).

[1] Except some basic and/or minor things you can pass in a completion handler or prepareForSegue. That's debatable and depends on how strict you follow the coordinator or another pattern. Personally I sometimes take a shortcut here as long as it doesn't bloat up things and becomes messy. Some pop-up designs are simpler done that way.


As a closing remark, the phrase "Note that we want to avoid Singleton" as well as your comment regarding that under the question give me the impression you just follow that advice without properly having thought about the rationale. I know that "Singleton" is often times considered an anti-pattern, but just as often that judgement is mis-informed. A singleton can be a valid architectural concept (which you can see by the fact that it's used extensively in frameworks and libraries). The bad thing about it is just that it too often tempts developers to take shortcuts in design and abuse it as kind of an "object repository" so that they don't need to think about when and where to instantiate objects. This leads to messy-ness and the bad reputation of the pattern.

A UserService, depending on what that actually does in your app might be a good candidate for a singleton. My personal rule of thumb is: "If it manages state of something that's singular and unique, like a specific user that can only ever be in one state at a given time", I might go for a singleton.

Especially if you cannot design it in the way I outlined above, i.e. if you need to have in-memory, singular state data, a singleton is basically an easy and proper way to implement this. (Even then using (lazy) properties is beneficial, your view controllers then don't even need to know whether it's a singleton or not and you can still stub/mock it individually (i.e. not just the global instance).)

like image 90
Gero Avatar answered Nov 16 '22 01:11

Gero


These are your requirements as I understand them:

  1. VC4 and VC8 must be able to share state via a UserService class.
  2. UserService must not be a singleton.
  3. UserService must be supplied to VC4 and VC8 using dependency injection.
  4. A dependency injection framework must not be used.

Within these constraints, I would suggest the following approach.

Define a UserServiceProtocol that has methods and/or properties for accessing and updating the state. For example:

protocol UserServiceProtocol {
    func login(user: String, password: String) -> Bool
    func logout()
    var loggedInUser: User? //where User is some model you define
}

Define a UserService class that implements the protocol and stores its state somewhere.

If the state only needs to last as long as the app is running, you could store the state in a particular instance, but this instance would have to be shared between VC4 and VC8.

In this case, I would recommend creating and holding the instance in AppDelegate and passing it through the chain of VCs.

If the state needs to persist between launches of the app, or if you don't want to pass an instance through the chain of VCs, you could store the state in user defaults, Core Data, Realm, or any number of places external to the class itself.

In that case, you could create the UserService in VC3 and VC7 and pass it in to VC4 and VC8. VC4 and VC8 would have var userService: UserServiceProtocol?. The UserService would need to restore its state from the external source. This way even though VC4 and VC8 have different instances of the object, the state would be the same.

like image 34
Mike Taverne Avatar answered Nov 16 '22 02:11

Mike Taverne


First of all, I believe there's a wrong assumption in your question.

You define your VC'c hierarchy as such:

Example:

VC1 -> VC2 -> VC3 -> VC4

VC5 -> VC6 -> VC7 -> VC8

However, on iOS (unless you're using some very strange hacks) there's always going to be a common parent at some point, like a navigation controller, tab bar controller, master-detail controller or page view controller.

So I assume that a correct scheme could look, for example, like this:

Tab Bar Controller 1 -> Navigation Controller 1 -> VC1 -> VC2 -> VC3 -> VC4

Tab Bar Controller 1 -> Navigation Controller 2 -> VC5 -> VC6 -> VC7 -> VC8

I believe looking at it like this makes it easy to answer your question.

Now if you're asking for an opinion what's the best way to handle DI on iOS, I'd say there's no such thing as the best way. However I personally like to stick to the rule that objects should not be responsible for their own creation/initialization. So things like

private lazy var service: SomeService = SomeService()

are out of question. I'd prefer an init that requires SomeService instance or at least (easy for ViewControllers):

var service: SomeService!

That way you pass the responsibility of fetching the right models/services etc up to the creator of the instance, meanwhile you can implement your logic with a simple but important assumption that you have everything you need to have (or you make your class fail early (for example by using force unwrapping), which is actually good during development).

Now, how you fetch these models - is it by initialising them, passing them around, having a singleton, using providers, containers, coordinators etc - it's entirely up to you and also should depend on factors like the complexity of the project, client demands, whatever tools you're using - so generally, whatever works is fine, as long as you stick to good OOP practices.

like image 29
user3581248 Avatar answered Nov 16 '22 00:11

user3581248


Here is an approach I've used on a few projects that may help you.

  1. Create all your view controllers via factory methods in a ViewControllerFactory.
  2. The ViewControllerFactory has its own UserService object.
  3. Pass the ViewControllerFactory's UserService object to those view controllers that need it.

A modest example here:

struct ViewControllerFactory {

private let userService: UserServiceProtocol

init(userService: UserServiceProtocol) {
    self.userService = userService
}

// This VC needs the user service
func makeVC4() -> VC4 {
    let vc4 = VC4(userService: userService)
    return vc4
}

// This VC does not
func makeVC5() -> VC5 {
    let vc5 = VC5()
}

// This VC also needs the user service
func makeVC8() -> VC8 {
    let vc8 = VC8(userService: userService)
    return vc8
}
}  

The ViewControllerFactory object can be instantiated and stored in AppDelegate.

That's the basics. In addition, I'd also look at the following (see also the other answers that have made some good suggestions here):

  1. Create a UserServiceProtocol that UserService conforms to. This makes it easy to create mock objects for testing.
  2. Look into the Coordinator pattern to handle navigation logic.
like image 43
Markk Avatar answered Nov 16 '22 00:11

Markk