Search code examples
swiftdependency-injectionsolid-principlesdependency-inversion

crash while try store value using dependency inversion


I want to implemented dependency inversion In app delegate in my app as my rootController is my UITabBarController but when I want to try it there is an error

Fatal error: Unexpectedly found nil while unwrapping optional value

This is my code in my appDelagate

let exploreStore = ExploreStore()

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    let rootController = (window?.rootViewController as? UITabBarController)?.children.first as? ExploreViewController
    // Inject Data
    rootController?.exploreStore = exploreStore
    return true
}

This is my explore class

class ExploreStore {
    fileprivate var allItems = [ExploreItem]()
    func fetch() {
        for data in loadData() {
            allItems.append(ExploreItem(dict: data))
        }
    }
    func numberOfItem() -> Int {
        return allItems.count
    }
    func explore(at index: IndexPath) -> ExploreItem {
        return allItems[index.item]
    }
    fileprivate func loadData() -> [[String: AnyObject]] {
        guard
            let path = Bundle.main.path(forResource: "ExploreData", ofType: "plist"),
            let items = NSArray(contentsOfFile: path)
        else { return [[:]] }
        return items as! [[String: AnyObject]]
    }
}

This is my exlporeViewController

var exploreStore: ExploreStore!

override func viewDidLoad() {
    super.viewDidLoad()
    // This is where the error found nil
    exploreStore.fetch()
}

Actually the code work if I don't use dependency inversion, like my explore view controller not use force unwrapping like this

var exploreStore = ExploreStore()

but since I want gain knowledge and learn S.O.L.I.D principle using dependency inversion, I want to stick with this principle.


Solution

  • If I understood your question correctly you want to initialise your class at AppDelegate class and then you want to pass it to your UITabBarController's first children and for that you need to make some modifications into your didFinishLaunchingWithOptions method like shown below:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
        let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
        let vc = storyBoard.instantiateViewController(withIdentifier: "tabBar")
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window?.rootViewController = vc
        let myTabBar = self.window?.rootViewController as! UITabBarController
        let firstViewController = myTabBar.children.first as? FirstViewController
        firstViewController?.exploreStore = exploreStore
        self.window?.makeKeyAndVisible()
    
        return true
    }
    

    Here I have made some modification's because I am retrieving Info.plist from Bundle and your ExploreStore will look like:

    class ExploreStore {
    
        var allItems = [ExploreItem]()
    
        func fetch() {
            if let dataObjectFromPlist = loadData() {
                allItems.append(ExploreItem(dict: dataObjectFromPlist))
            }
        }
        func numberOfItem() -> Int {
            return allItems.count
        }
        func explore(at index: IndexPath) -> ExploreItem {
            return allItems[index.item]
        }
        fileprivate func loadData() -> [String: AnyObject]? {
    
            var resourceFileDictionary: [String: AnyObject]?
            if let path = Bundle.main.path(forResource: "Info", ofType: "plist") {
                if let dict = NSDictionary(contentsOfFile: path) as? Dictionary<String, AnyObject> {
                    resourceFileDictionary = dict
                }
            }
            return resourceFileDictionary
        }
    }
    

    Then in my FirstViewController I can fetch the data from ExploreStore class with

    exploreStore.fetch()
    

    and my code for that UIViewController is

    class FirstViewController: UIViewController {
    
        var exploreStore: ExploreStore!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            exploreStore.fetch()
            print(exploreStore.allItems[0].data)
        }
    }
    

    Here exploreStore.allItems[0].data will print my whole info.plist file.

    You can try it by your self with THIS demo project and check if that's the correct behaviour.

    EDIT

    You need to update didFinishLaunchingWithOptions method like:

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
    
        setupDefaultColors()
    
        let exploreStoryBoard = UIStoryboard(name: "Explore", bundle:nil)
        let navigationController = exploreStoryBoard.instantiateViewController(withIdentifier: "ExploreViewControllerNavigation") as! UINavigationController
        if let exploreViewController = navigationController.children.first as? ExploreViewController {
            exploreViewController.store = ExploreStore()
            self.window = UIWindow(frame: UIScreen.main.bounds)
            self.window?.rootViewController = exploreViewController
            self.window?.makeKeyAndVisible()
        }
    
        return true
    }
    

    And you also need to update ExploreStore class as shown below:

    class ExploreStore {
    
        var allItems = [ExploreItem]()
    
        func fetch() {
            if let dataObjectFromPlist = loadData() {
                allItems.append(ExploreItem(dict: dataObjectFromPlist))
            }
        }
    
        func numberOfItem() -> Int {
            return allItems.count
        }
    
        func explore(at index: IndexPath) -> ExploreItem {
            return allItems[index.item]
        }
    
        fileprivate func loadData() -> [String: AnyObject]? {
            var resourceFileDictionary: [String: AnyObject]?
            if let path = Bundle.main.path(forResource: "ExploreData", ofType: "plist") {
                if let dict = NSDictionary(contentsOfFile: path) as? Dictionary<String, AnyObject> {
                    resourceFileDictionary = dict
                }
            }
            return resourceFileDictionary
        }
    }
    

    Because from plist you will get Dictionary<String, AnyObject> type object.

    And you will still not get data from plist file because its added into subfolder. So you need find correct path first for your plist.

    You also needs to assign respective identifiers to navigation controller and tab bar controller.

    Here is your demo project.