NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

SOLVED: Resources to learn MVC

Forums > Swift

I have been going through the projects from Hacking with Swift and I feel like my ViewController is getting more and more cluttered with code, I have no structure whatsoever. So I wanted to ask for an advice, where could I learn how to structure my code with MVC as the architecture for example? Is that something I could get from Paul's Design Patterns book? I looked at the first 24 pages, but it seemed to have only code snippets and not a real project.

   

Paul's Swift on Sundays videos have some nice refactoring going on. That's a good start!

https://www.youtube.com/results?search_query=swift+on+sundays

1      

This also depends on what type of your code is in the view controller.

You have have for example a lot of lines that setup buttons to look in a particular way, then you can create custom UIButton subclass and move the configuration there.

If you have some functionality that deals with models, this should be on the model class. Like definitions of NSFetchRequest or validations.

For other functionality that is not truly specific for the current view controller you can create protocols and extensions that contain it.

It is not uncommon to have utils or helper classes that use the singleton pattern to move generic functionality there.

For yet other things (like autosizing table view header/footer, generic setup) it may make sense to create base view controller and move it there.

   

If you want, feel free to share your source code, I will check it out and suggest how to clean up the controller.

1      

I don't know how I could reply to each of you, so I am just gonna do it like this.

@Gakkienl Thanks for the link, I'll check it out!

@nemecek-filip That is actually really nice and more than I was expecting. I never had anyone review my code, so huge thanks in advance! Here is the ViewController of a small app I built alongside Hacking with Swift, it's a simple Todo App that saves and reads user input to and from a file.


import UIKit

extension String {
    func appendToEOF(_ fileURL: URL,_ fileInput: String) {
        if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
            defer {
                fileHandle.closeFile()
            }
            fileHandle.seekToEndOfFile()
            if let input = ("\n" + fileInput).data(using: .utf8) {
                fileHandle.write(input)
            }
        } else {
            print("Could not append text to end of file")
        }
    }
}

class ViewController: UITableViewController {

    let fileName = "items"
    let extensionName = "txt"
    let cellID = "itemCell"
    lazy var fileURL = getDocumentsDirectory().appendingPathComponent(fileName).appendingPathExtension(extensionName)

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "To do"
        navigationController?.navigationBar.prefersLargeTitles = true

        navigationItem.rightBarButtonItem = UIBarButtonItem(
        barButtonSystemItem: .add, target: self, action: #selector(showAlertToAskForInput))

        navigationItem.leftBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .trash, target: self, action: #selector(showAlertToDeleteTable))
    }

    @objc 
    private func showAlertToDeleteTable() {
        let alert = UIAlertController(title: "Warning", message: "Do you really want to delete this list?", 
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "No", style: .default, handler: nil))
        alert.addAction(UIAlertAction(title: "Yes", style: .destructive, handler: deleteWholeListFromFile))
        present(alert, animated: true)
    }

    @objc
    private func showAlertToAskForInput() {
        let alert = UIAlertController(title: "New item", message: "", preferredStyle: .alert)
        alert.addTextField(configurationHandler: {
            textField in
            textField.placeholder = "Apples"
        })
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        // How could I access alert from a separate method instead of a closure?
        //alert.addAction(UIAlertAction(title: "Enter", style: .destructive, handler: addItem))

        alert.addAction(UIAlertAction(title: "Enter", style: .destructive, handler: {
            [weak alert, weak self] (_) in
            if let text = alert?.textFields![0].text! {
                self?.writeUserInputToFile(itemToAdd: text)
            }
        }))
        present(alert, animated: true)
    }

    private func deleteWholeListFromFile(action: UIAlertAction) {
        let fm = FileManager.default
        if fm.isDeletableFile(atPath: fileURL.path) {
            do {
                try fm.removeItem(at: fileURL)
                tableView.reloadData()
            } catch {
                print(error)
                print("Could not delete file")
            }
        }
    }

    private func writeUserInputToFile(itemToAdd: String) {
        let fm = FileManager.default
        if !fm.fileExists(atPath: fileURL.path) {
            do {
                try itemToAdd.write(to: fileURL, atomically: true, encoding: .utf8)
                print("File successfully created")
            } catch {
                print(error)
                print("File could not be created")
            }
        } else {
            itemToAdd.appendToEOF(fileURL, itemToAdd)
        }
        tableView.reloadData()
    }

    private func readingFromFile() -> [String] {
        let fm = FileManager.default
        if let data = fm.contents(atPath: fileURL.path) {
            let contentsArray = String(decoding: data, as: UTF8.self).split(separator: "\n")
            return contentsArray.map({String($0)})
        }
        return [String]()
    }

    private func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }

    // TableView

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return readingFromFile().count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
        cell.textLabel?.text = readingFromFile()[indexPath.row]
        return cell
    }
}

   

Cool, thanks for sharing!

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return readingFromFile().count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
        cell.textLabel?.text = readingFromFile()[indexPath.row]
        return cell
    }

These methods are really problematic because cellForRowAt will be called as many times as the number of items you have. Ideally you should load these items to memory in viewDidLoad and then use that.

The file manipulation could easily be extracted into something like FileHelper and parts of it as a extension on FileManager. Particulary getting the documents URL.

The cellID should either be "hardcoded" in the dequing method since you are using it once or in a case of custom table view cell as a static constant on the class :)

   

Thanks a lot for your feedback, I will be looking into the things you mentioned. About the cellForRowAt method though, I wonder how I would do this since ViewDidLoad does not run everytime the tableView gets reloaded, I am assuming here. So if the user wants to insert a new item to the To do list and the cells are being "filled" with the data set in viewDidLoad, the user would not be able to see his current addition now, but only after the app would restart because ViewDidLoad would read again the data from the file and the newly created item would then be there.

I guess the best way to go here would be to update/create a single row instead of creating all the rows again after each insertion of a new item. This is the problem you meant, right?

   

No, the way you have it is correct. cellForRowAt will only be called enough times to account for the currently visible cells (and maybe a little extra, I'm not sure). As those scroll out of view, the cell objects will be reused as needed. That's why the method you call is called dequeueReusableCell; UIKit reuses the objects in order to avoid having a ton of them in memory all the time.

1      

Thank you all for your help. I figured out for myself that I should call tableView.insertRows(..) instead of tableView.reloadData(), because the later seems to reload all the cells while the first only calls cellsForRowAt for new the cell which I need to show. Since my initial question has been answered already I will be marking this as solved.

   

Hacking with Swift is sponsored by NSSpain

SPONSORED Announcing NSSpain 2020: Remote Edition! An online, continuous conference for iOS developers. We’ll start on Thursday and finish on Friday, with talks, activities, and lots of fun for 36 hours, non-stop. Sound good? Join us!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

Not logged in

Log in
 

Link copied to your pasteboard.