Tony Pendleton
Tony Pendleton

Reputation: 368

Reusing UIViewController (with UITableView) for Master/Detail View Controller in Master-Detail Application?

I am using this Ray Wenderlich tutorial as a guide for a project I am working on. However, I am trying to expand this template to incorporate sub-directories, sub-sub directories, sub-sub-sub directories, and so forth.

For example, if you the click the "Candy Cane" cell within the main/home directory screen, it will take you to a "new" view controller with table view (aka a sub-directory screen) that will show various candy cane vendors and whether their prices are expensive or cheap (this will be the subtitle for each row). See the subCategory... txt files down below.

I use the words category and directory interchangably throughout this post.

Here's the first catch. In the past, I made a project that does just this, but I did it via many storyboard and view controller files for each sub-category I navigated too. Same principle goes for moving from sub-category to sub-sub-category screens and so forth.

I AM NOT TRYING TO ADVERTISE: But specifically, the app I am working on is called iEngineering and its available for free on the AppStore. Please see the library part of the app as I believe it will provide additional help with understanding my end goal. Keep in mind, the current version of the application, was built using an endless number of storyboard and view controller files that has to build each time I run the simulator in Xcode. Viewing the app screenshots that show the library feature should suffice without having to download the app.

Here's the second catch. I also have the sub-sub-categories and so forth in this app. For example, if you click a cell in the sub-category screen that shows the list of candy cane vendors and prices, it will navigate you to a new screen (sub-sub-category) to all of the different candy canes that specific vendor sales. So in my made-up text file below, subCandyCaneTextFile.txt, if you were to click on the cell with label "Candy Cane King" it will take you to a screen that shows "Candy Cane King" as the navigation title, and present/load a listing (imported via reading text file) that shows all of the candy canes that Candy Cane King provides. I did not provide this and other txt files that I am considering to be sub-sub categories . I would like you all to just keep this in mind for when I ask the root/heart of my question down below.

For THIS project, I am wanting to MOVE AWAY from using multiple storyboards with many view controllers. This is because my previous project would take ~5 to 10 minutes to compile/build every time I go to build it in the simulator. Therefore, I am attempting to read in a text file at project run-time for each new/category screen in my application. I am hoping this will give me much, much quicker run times (~10 seconds).

In the example below, the category/main view controller presents its data by reading in the txt file candyTextFile.txt

MY QUESTION: How can I go about (transitioning from | removing) old data and (loading | replacing) in new data for sub category screens? I thought about trying to have this be the Detail View Controller screen in the Ray Wenderlich tutorial tutorial; however, I am then not sure how I would navigate to a sub-sub-category screen. Therefore, I think my other option may to be reuse one view controller with UITableView (Master View Controller) over and over again. I am not sure the proper way to go about this as I am still relatively new to learning Swift and Xcode.

Overview of current project via Xcode for this post.

In my MasterViewController.swift,

    import UIKit

    class MasterViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    // MARK: - Properties
    @IBOutlet var tableView: UITableView!
    @IBOutlet var searchFooter: SearchFooter!

    var detailViewController: DetailViewController? = nil

    var candies = [Candy]()
    var filteredCandies = [Candy]()

    let searchController = UISearchController(searchResultsController: nil)

    // MARK: - View Setup
    override func viewDidLoad() {
    super.viewDidLoad()

    // Setup the Search Controller
    searchController.searchResultsUpdater = self
        searchController.obscuresBackgroundDuringPresentation = false
    searchController.searchBar.placeholder = "Search Candies"
    navigationItem.searchController = searchController
    definesPresentationContext = true

    // Setup the Scope Bar
    searchController.searchBar.scopeButtonTitles = ["All", "Chocolate","Hard", "Other"]
    searchController.searchBar.delegate = self

    // Setup the search footer
    tableView.tableFooterView = searchFooter


    setupArray()



    if let splitViewController = splitViewController {
        let controllers = splitViewController.viewControllers
        detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
    }

    }


    private func setupArray() {
        if let filepath = Bundle.main.path(forResource: "candyTextFile", ofType: "txt") {
            do {
                let contents = try String(contentsOfFile: filepath)
                let lines_separatedBy_n : [String] = contents.components(separatedBy: "\n")
                let string = lines_separatedBy_n.map { String($0) }.joined(separator: ", ")
                var lines_separatedBy_comma : [String] = string.components(separatedBy: ", ")

                // I've put this in to remove the last bit of the file that was causing the count to be one too high.
                // I'm guessing that's why you had something similar previously?
                lines_separatedBy_comma.removeLast()

                for (index, element) in lines_separatedBy_comma.enumerated() {
                    if index % 2 == 0 {
                        let newCategory = element
                        let newName = lines_separatedBy_comma[index + 1]
                        let newCandy = Candy(category: newCategory, name: newName)
                        candies.append(newCandy)
                    }
                }
                for candy in candies {
                    print("category: \(candy.category), name: \(candy.name)")
                }
                //("\ncandies: \(candies)")
            } catch let error as NSError {
                print(error.localizedDescription)
            }
        }

            print("\ncandies: \(candies)")

    }

    override func viewWillAppear(_ animated: Bool) {
    print("splitViewController!.isCollapsed: \(splitViewController!.isCollapsed)")

    if splitViewController!.isCollapsed {
      if let selectionIndexPath = tableView.indexPathForSelectedRow {
        tableView.deselectRow(at: selectionIndexPath, animated: animated)
      }
    }
    super.viewWillAppear(animated)
    }

    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    }

    // MARK: - Table View
    func numberOfSections(in tableView: UITableView) -> Int {
    return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if isFiltering() {
        print("Is filtering")
        searchFooter.setIsFilteringToShow(filteredItemCount: filteredCandies.count, of: candies.count)
        return filteredCandies.count
    }
    print("Is not filtering")
    searchFooter.setNotFiltering()
    return candies.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let candy: Candy
    if isFiltering() {
        candy = filteredCandies[indexPath.row]
    } else {
        candy = candies[indexPath.row]
    }
    cell.textLabel!.text = candy.name
    cell.detailTextLabel!.text = candy.category
    return cell
    }

    // MARK: - Segues
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showDetail" {
            if let indexPath = tableView.indexPathForSelectedRow {
                let candy: Candy
                if isFiltering() {
                    candy = filteredCandies[indexPath.row]
                } else {
                    candy = candies[indexPath.row]
                }
                let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
                controller.detailCandy = candy
                controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
                controller.navigationItem.leftItemsSupplementBackButton = true
            }
        }
    }

    func filterContentForSearchText(_ searchText: String, scope: String = "All") {


        filteredCandies = candies.filter({
            (candy : Candy) -> Bool in
        let doesCategoryMatch = (scope == "All") || (candy.category == scope)
            if searchBarIsEmpty(){
                return doesCategoryMatch
            }else {
                return doesCategoryMatch && candy.name.lowercased().contains(searchText.lowercased())

            }
        })

        tableView.reloadData()
    }


    func searchBarIsEmpty() -> Bool {
        return searchController.searchBar.text?.isEmpty ?? true
    }

    func isFiltering() -> Bool {
        let searchBarScoperIsFiltering = searchController.searchBar.selectedScopeButtonIndex != 0
        return searchController.isActive && (!searchBarIsEmpty() || searchBarScoperIsFiltering)
    }


    }

    extension MasterViewController: UISearchBarDelegate {
    // MARK: - UISearchBar Delegate
    func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
        filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
    }

    }

    extension MasterViewController: UISearchResultsUpdating {
    // MARK: - UISearchBar Delegate
    func updateSearchResults(for searchController: UISearchController) {
        let searchBar = searchController.searchBar
        let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
        filterContentForSearchText(searchController.searchBar.text!, scope: scope)
    }
    }

In candyTextFile.txt,

Chocolate, Chocolate Bar
Chocolate, Chocolate Chip
Chocolate, Dark Chocolate
Hard, Lollipop
Hard, Candy Cane
Hard, Jaw Breaker
Other, Caramel
Other, Sour Chew
Other, Gummi Bear
Other, Candy Floss
Chocolate, Chocolate Coin
Chocolate, Chocolate Egg
Other, Jelly Beans
Other, Liquorice
Hard, Toffee Apple

In subCandyCaneTextFile.txt,

Cheap, Brachs
Expensive, Spangler
Expensive, Bobs
Cheap, Candy Cane King
Expensive, Jelly Belly

Other, sub-category, files:

In subDarkChocolateTextFile.txt,

Cheap, Ghirardelli
Expensive, Dove
Expensive, Lindt
Cheap, Hersheys
Expensive, Hu Dark

In subLollipopTextFile.txt,

Cheap, Zollipops
Cheap, YumEarth
Expensive, Dum Dums

Thank you all for your time and any guidance you all may be able to offer. I greatly appreciate it.

Upvotes: 0

Views: 78

Answers (1)

schmidt9
schmidt9

Reputation: 4528

You can reuse DetailViewController from the same storyboard. Assign a Storyboard ID to it and then you can create new instance of DetailViewController class from the current one. You can keep index of the new VC for example incrementing it and using it for instance to get next category file name from a predefined array and to load it for instance in viewDidLoad

In DetailViewController class:

    var index = 0;

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let st = UIStoryboard(name: "Main", bundle: nil)
        let vc = st.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
        vc.index = self.index + 1;
        self.navigationController?.pushViewController(vc, animated: true)
    }

storyboard setup

EDIT

How to use index

Declare array with your filenames: let files = ["file1", "file2", "file3"]

In setupArray() use it

if let filepath = Bundle.main.path(forResource: files[index], ofType: "txt")

Upvotes: 1

Related Questions