DEV Community

Theo Millard
Theo Millard

Posted on

Data Passing in iOS Apps - UI Kit

Passing data to between view controllers is crucial in your application flows.
There are 2 types of passing data:

  1. Passing Data to Another ViewController
    • when creating new view controller
  2. Returning Data to Previous Activity
    • when closing current view controller

There will be more detailed explanation with example below.


Passing Data to Another ViewController


Property Access

Detail View Controller

class DetailViewController: UIViewController {
    var gameTitle: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        print("Selected Game: \(gameTitle)")
    }
}
Enter fullscreen mode Exit fullscreen mode

Main View Controller

class MainViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print("Game List: ")

        let Button = UIButton()
        // Add Event Listener - FOR EXAMPLE
        Button.addTarget(self, action: #selector(navigateToDetail),for: .touchUpInside)
    }
    @objc func navigateToDetail() {
        // Create view controler page object 
        let otherViewController = DetailViewController()
        otherViewController.gameTitle = "Black Myth Wukong"
        // push new detail page
        navigationController?.pushViewController(otherViewController, animated: true)
    }
}
Enter fullscreen mode Exit fullscreen mode

Even though it seems much simpler, it also can introduce new bugs. Here, using variable provide no documentation.
Let's say we want to call the detail in another view controller, we simply can forgot to modify the variables.

Or we have many variables, we can easily skip one of them and only can caught it when testing or applications are running.

Benefits

  • Easy to Implement

Drawbacks

  • No Documentation on what's variable need to be filled
  • No compile-time safety: Any forgotten passes only be caught after testing or running the app

init Function

Detail View Controller

class DetailViewController: UIViewController {
    private let gameTitle: String

    init(gameTitle: String) {
        self.gameTitle = gameTitle
        // Need to implement if creating init
        super.init(nibName: nil, bundle: nil)
    }

    // Need to implement if creating init on storyboard 
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        print("Selected Game: \(gameTitle)")
    }
}
Enter fullscreen mode Exit fullscreen mode

Main View Controller

class MainViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print("Game List: ")

        let Button = UIButton()
        // Add Event Listener - FOR EXAMPLE
        Button.addTarget(self, action: #selector(navigateToDetail),for: .touchUpInside)
    }
    @objc func navigateToDetail() {
        // Create view controler page object 
        let otherViewController = DetailViewController(gameTitle: "Black Myth Wukong")
        // push new detail page
        navigationController?.pushViewController(otherViewController, animated: true)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, using init can provide more clear and documentations. If we want to call the detail view controller in other places, we can just init it with arguments.

Also, if we add more variables, other class that init this will have compile error and must adapt to new init function.

Benefits

  • Extra safety on compile-time: If you add new params, all the functions call need to adapt
  • Extra documentation on what arguments needed
  • More clean
  • More scalable

Drawbacks

  • Custom initializer

Returning Data to Previous Activity

Closure

Detail View Controller

class DetailViewController: UIViewController {
    private let gameTitle: String
    private let onSubmitHandler: (String) -> Void

    init(
        gameTitle: String,
        onSubmitHandler: @escaping (String) -> Void
    ) {
        self.gameTitle = gameTitle
        self.onSubmitHandler = onSubmitHandler
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func submitTapped() {
        onSubmitHandler("Game: \(gameTitle)")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        print("Selected Game: \(gameTitle)")
        // EXAMPLE - Directly calling the onSubmitHandler
        submitTapped()
    }
}
Enter fullscreen mode Exit fullscreen mode

Main View Controller

func navigateToDetail() {
    /// Create demo `ClosureViewController` page object
    let viewController = DetailViewController(
        gameTitle: "Black Myth Wukong",
        // weak self explanation below
        onSubmitHandler: { [weak self] result in
            // got result and can do anything
            print(result)
        }
    }

    /// Push detail page into navigation controller
    navigationController?.pushViewController(viewController, animated: true)
}
Enter fullscreen mode Exit fullscreen mode

Here, passing the closure function can use both of the method that is given on the first part. In the example we use the init one.

Passing weak self in closure, making sure there is no memory leaks. If Parent got cleaned or get deinitialized, if the Child doesn't have other reference count,
memory will be freed.

Benefits

  • No boilerplate (Protocol)
  • Easy to implement, just pass the closure

Drawbacks

  • Harder to digest (Closure)
  • Not common practices
  • Hard to test

Delegate

Detail View Controller

protocol PrinterDelegate: AnyObject {
    func printInfo(gameTitle: String) -> Void
}

class DetailViewController: UIViewController {
    private let gameTitle: String
    // weak var same explanation with above
    weak var printerDelegate: PrinterDelegate 

    init(
        gameTitle: String,
        printerDelegate: PrinterDelegate
    ) {
        self.gameTitle = gameTitle
        self.printerDelegate = printerDelegate
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func submitTapped() {
        // calling delegate's function
        printerDelegate.printInfo(self.gameTitle)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        print("Selected Game: \(gameTitle)")
        // EXAMPLE - Directly calling the onSubmitHandler
        submitTapped()
    }
}
Enter fullscreen mode Exit fullscreen mode

Main View Controller

class MainViewController: UIViewController, SelectionDelegate {
    func navigateToDetail() {
        // Conforms to delegate
        func printInfo(gameTitle: String) -> Void {
            // Can do anything we want
        }
        /// Create demo `ClosureViewController` page object
        let viewController = DetailViewController(
            gameTitle: "Black Myth Wukong",
            // Pass self
            printerDelegate: self
        }

        /// Push detail page into navigation controller
        navigationController?.pushViewController(viewController, animated: true)
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, by using protocol, each class that want's to create detail view controller needs to conform to the protocol. creating more separation.

Benefits

  • Scalable and Reusable
  • Easier only need to create normal function not closure
  • More common and easy to test

Drawbacks

  • Boilerplate

References

Top comments (0)