Passing data between views in UIKit and SwiftUI

From Apple Developer Academy students

Denis Matveev
Apple Developer Academy | Federico II

--

When we first started learning UIKit in October 2019, passing data seemed quite terrifying to us. We had a chance to try some basic methods, but then happily switched to the amazing SwiftUI (and have never come back until this moment). SwiftUI offers an easy and natural data flow, but we were lacking the same knowledge from the UIKit. Now it’s time to recap how things go in both frameworks, compare them and decide if SwiftUI really made a huge step in this direction.

Passing data in UIKit

So how are things going in the good ol’ UIKit? Well, this framework is based on Model-View-Controller (MVC) design pattern, which means that views can’t pass the data on their own. They have to use middlemen — view controllers — for that. View controllers have many responsibilities: responding to user’s interactions, resizing their views, managing views layout, and a particular one — updating the views according to the application data model. So in case of UIKit it’s more accurate to speak of the ways of passing data between view controllers.

Sample app

In order to explain all the nuances we’ve created a small project, called “The books I’ve read”. Easy to guess that this app helps you create a list of read books. You can also leave your review and rate each of them. You can download the full project here.

First of all let’s create our data model. It’s pretty simple, we have a struct that describes a single book:

struct Book {
var title: String
var author: String?
var rating: Int?
var review: String?
}

Of course, we need to collect our books somewhere. For this we have a simple subscript of books, pre-populated from a JSON file:

struct Books {
var items: [Book] = load("books.json")
subscript(index: Int) -> Book {
return items[index]
}
}

There are many patterns and techniques you can use to make two view controllers communicate. Generally, this communication can have one of two directions:

  • forward — when a view controller A presents a view controller B and sends it some data to show,
  • backwards — when the view controller B is being dismissed and wants to give back some other data to the A.

Let’s start with forward communication.

Segue

Segues are the most common and recommended ways of presenting and dismissing view controllers. A segue can be created visually in a storyboard without a line of code. And, of course, we can use them to pass some data.

For our project let’s create a table view controller with a list of books and another one that shows details of the selected book. We want to present the detailed screen each time user taps a book in the list, so we connect the table view cell with a detailed view controller using a segue. Just like that:

Now, if we run the prototype and tap on a book, that book’s view shows up, but it’s going to be the same all the time since we don’t pass any data. To do that we want to use prepare(for:sender:) method, which passes the data before a new view controller occurs. But first we’re going to add a variable, that stores a Book, to our second view controller, and add a function that updates our view with this data:

class BookViewController: UIViewController {    var book: Book?    func updateControls() {
self.title = book?.title
labelTitle.text = book?.title
labelAuthor.text = book?.author

if let _ = book, let review = book?.review {
reviewTextView.text = review
} else {
reviewTextView.text = "Add your review on this book here…"
reviewTextView.textColor = UIColor.secondaryLabel
}

updateRatingView()
}

Now we want to pass the book selected on the main screen over the segue:

class BooksViewController: UITableViewController {
var booksData = Books()

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let bookVC = segue.destination as? BookViewController,
let selectedIndex = self.tableView.indexPathForSelectedRow?.row
else {
return
}
bookVC.book = booksData[selectedIndex]

}

As you can see, we’ve checked that the segue actually presents BookViewController. We do this because prepare method catches all the segues that the current view controller outputs.

Unwind segue

Now, what if you want to pass some data back? That’s when unwind segues come into play. They are less obvious than forward segues in terms of visual representation though.

In our project we want to let users add new books to the list. We show our view controller for that modally through a segue, but then we want pass data about a new book back to the main view.

First we add these methods to the destination view controller:

class BookViewController: UIViewController, UITextViewDelegate, BookViewControllerDelegate {    @IBAction func saveBook(_ unwindSegue: UIStoryboardSegue) { }
@IBAction func cancelAdd(_ unwindSegue: UIStoryboardSegue) { }
}

Now that we have these empty functions, we go to the source view controller and just control-drag from the corresponding action to the orange exit icon:

And then select the proper function we created before:

Great! In the storyboard you will see that your unwind segue has been created.

The same way we create an unwind segue for the Cancel action. So now our modal view can be dismissed properly, but we are still not sending back any data. To change that let’s add the familiar prepare method to the modal view controller:

class AddBookViewController: UIViewController {



override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let title = titleTextField.text,
let author = authorNameTextField.text {
newBook = Book.init(title: title, author: author)
}
}

We are passing the new Book object back to the main view controller. Now it’s time to process this data there:

    @IBAction func saveBook(_ unwindSegue: UIStoryboardSegue) {
if let addBookVC = unwindSegue.source as? AddBookViewController, let newBook = addBookVC.newBook {
booksData.items.append(newBook)
self.tableView.reloadData()
}
}

Run the project to make sure that you’ve added new books to your list.

Delegate

Unwind segue works perfectly with modals, but if user is quitting non-modal view, no segues will be generated and we won’t be able to pass our data in the right time. In this case you want your controllers to communicate through a delegate.

Delegation is a great technique, because view controller A doesn’t care about the type of view controller B, so we don’t create a tight coupling between them.

In our project users can also edit the title and author’s name in the detailed view. Then we want to pass the new title and name back from the editing screen:

First of all we need to define a delegate protocol with a method that updates the given book. We also add a protocol property to our EditBookViewController:

protocol BookViewControllerDelegate: AnyObject {
func updateBook(_ book: Book)
}

class EditBookViewController: UIViewController {

var book: Book!
weak var delegate: BookViewControllerDelegate?

Then, in the same controller we want to use delegate’s update method to pass the updated book:

    @IBAction func save(_ sender: UIBarButtonItem) {
if let title = titleTextField.text {
let author = authorNameTextField.text
let updatedBook = Book.init(title: title, author: author)
delegate?.updateBook(updatedBook)
self.dismiss(animated: true, completion: nil)
}
}

Now we should do the following:

  • make sure BookViewController conforms to our delegation protocol,
  • implementupdate method,
  • assign BookViewController as delegate, while passing the data forward to BookEditViewController.

Let’s make these changes:

class BookViewController: UIViewController, BookViewControllerDelegate {

func updateBook(_ book: Book) {
self.book?.title = book.title
self.book?.author = book.author
updateControls()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let navigationController =
segue.destination as? UINavigationController,
let editBookVC =
navigationController.viewControllers.first as? EditBookViewController {
editBookVC.book = book
editBookVC.delegate = self
}
}

It’s done! Now you can edit your books.

Closures

You can also replace delegates with closures — you will gain more flexibility, but less control.

In our app users can leave feedback and rate their books in the detailed view, and we need to pass this data back to the main controller. We cannot use segues since these views are connected non-modally:

Let’s use a closure to solve this problem! It works pretty much the same we did with the delegate. In the detailed view controller we define a closure and pass the updated book through this closure. We don’t have an unwind segue, so we send the data every time user changes the current book:

class BookViewController: UIViewController {    var updateBookClosure: ((Book) -> Void)?

var book: Book? {
didSet {
if let updatedBook = book {
updateBookClosure?(updatedBook)
}
}
}

In the main view controller while passing selected book to the BookViewController we inject a code that updates selected book with the passed data:

class BooksViewController: UITableViewController {

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let bookVC = segue.destination as? BookViewController,
let selectedIndex =
self.tableView.indexPathForSelectedRow?.row
else {
return
}
bookVC.book = booksData[selectedIndex]

bookVC.updateBookClosure = { [weak self] updatedBook in
self?.booksData.items[selectedIndex] = updatedBook
self?.tableView.reloadData()
}
}

Now our list is constantly updating with new reviews and ratings with even less code! You should take in account though, that with all benefits over delegates closures have an indirect nature, so it can be difficult to read the code with many closures.

Passing data in SwiftUI

@State and @ObservedObject

If you want to show dynamically changes of parameters in your view — you should wrap them in special wrappers:

@State — for primitive types,
@ObservedObject — for classes.

The class that will be wrapped in @ObservedObject must conform to the ObservableObject protocol. Properties required for transfer must be wrapped in @Published.

If you want to have additional functionality — you should replace this wrapping with ObservableObjectPublisher. In this case you can send changes using method willSet() inside these parameters.

import Combine
import SwiftUI
class UserAuthentication: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var username = “” {
willSet {
objectWillChange.send()
}
}
}

@Binding

In case if you want to pass data from child view to parent view — you can use @Binding wrapper.

For example we have a child view with a button. And we have a parent view, which, among other things, contains a click counter. We have @State wrapper for this counter in the parent view because we want to refresh it on the screen. In the child view we have to declare the counter with @Binding. When that value changes in the child view it will be passed back to the parent view. We also need to mark passing data with $, to show that we’re waiting for changes of this value inside our child view.

struct ContentView: View {
@State var tapCount = 0
var body: some View {
VStack {
SomeView(count: $tapCount)
Text(“you tap \(tapCount) times”)
}
}
}

The type of returned value will be Binding<Int> (not just Int).

It’s important to know if you want to create your own init() for view.

struct SomeView: View {
@Binding var tapCount: Int
init(count: Binding<Int>) {
self._tapCount = count
// if you need to do something else here
}
var body: some View {
Button(action: {self.tapCount += 1},
label: {
Text(“Tap me”)
})
}
}

Pay attention that you should use self._ when you call some wrapped properties inside init(). It works when self is still in the creating process. When we use self._ — we get Binding<Int>. If you want to call a value inside the wrapper you should use just self. or call method .wrappedValue.

Any change of @Binding variable for parent view will reopen the whole child view. It means destroying a child view instance and replacing it with a new child view and also new value of @Binding parameter. If you created a view with both @State and @Binding parameters you should think about what will happen with @State parameter in case of changing @Binding value. Most likely it’ll be reset to default value, or to the value registered in init().

@EnvironmentObject

@EnvironmentObject parameters are the same as @Binding, but they work for the whole hierarchy of views. It’s not necessary to pass them explicitly.

ContentView().environmentObject(session)

Usually it is used when you want to transmit the current state of application or some part of it, which is required by many views. It makes sense that you have to put important data inside EnvironmentObject only once in the main view. For example — data about user, session, or something similar. And you can have access to these data from any view by declaring variable with @EnvironmentObject wrapper:

@EnvironmentObject var session: Session

@Environment is quite the same, but it’s more convenient to take the current state of screen orientation (vertical or horizontal) or dark/bright theme through this wrapper.

Conclusions

As you can see passing data between two views works similarly in both frameworks. Of course, SwiftUI offers more concise and convenient @Binding method for that — it doesn’t require any extra steps like segues, delegates or closures. It’s just a variable declared in a parent view that you pass to its child, and it works both ways! But the overall approach is quite the same.

Where SwiftUI really shines is @EnvironmentObject — it’s a single source of truth for the entire app. So any view can read or even modify it, when needed. It’s fantastic and it’s something that we miss in UIKit. Maybe we can achieve the same effect there using the new Combine framework, but it’s still far from the classical practice. And anyway, while we have middlemen in the form of controllers between views and models, we’re doomed to struggle passing data in UIKit.

So we think SwiftUI has made a tremendous step in the right direction, while UIKit looks like a reliable, but a bit old-fashioned and inconvenient sofa. Unfortunately, we have to wait before this new technology gets adopted by the developers community and by users’ devices.

About the authors

We are a small team of three: Denis Matveev, Evgeniia Kiriushina and Gleb Losev. At the time of writing this article we are studying at the Apple Developer Academy in Naples and hoping to become great developers.

--

--