How to manage SwiftUI state with nested structs? How to manage SwiftUI state with nested structs? swift swift

How to manage SwiftUI state with nested structs?


I made the full project, to demonstrate how to pass the data.

It is available on GitHub at GeorgeElsham/BookshelvesExample if you want to download the full project to see all the code. This is what the project looks like:

GIF of project

This project is quite similar to my answer for SwiftUI - pass data to different views.

As a summary, I created an ObservableObject which is used with @EnvironmentObject. It looks like this:

class BookshelvesModel: ObservableObject {    @Published var shelves = [...]    var books: [Book] {       shelves[shelfId].books    }    var pages: [Page] {       shelves[shelfId].books[bookId].pages    }        var shelfId = 0    var bookId = 0        func addShelf(title: String) {        /* ... */    }    func addBook(title: String) {        /* ... */    }    func addPage(content: String) {        /* ... */    }        func totalBooks(for shelf: Shelf) -> String {        /* ... */    }    func totalPages(for book: Book) -> String {        /* ... */    }}

The views are then all connected using NavigationLink. Hope this works for you!


If you are remaking this manually, make sure you replace

let contentView = ContentView()

with

let contentView = ContentView().environmentObject(BookshelvesModel())

in the SceneDelegate.swift.


Basically, you need a storage for your books/pages, and preferably that storage can be uniquely referenced among your views. This means a class :)

class State: ObservableObject {    @Published var shelves = [Shelf]()    func add(shelf: Shelf) { ... }    func add(book: Book, to shelf: Shelf) { ... }    func add(page: Page, to book: Book) { ... }    func update(text: String, for page: Page) { ... }}

You can then either inject the State instance downstream in the view hierarchy, on inject parts of it, like a Shelf instance:

struct ShelvesList: View {    @ObserverdObject var state: State    var body: some View {        ForEach(state.shelves) { ShelfView(shelf: $0, shelfOperator: state) }    }}// this conceptually decouples the storage and the operations, allowing// downstream views to see only parts of the entire functionalityprotocol ShelfOperator: BookOperator {    func add(book: Book, to shelf: Shelf)}extension State: ShelfOperator { }struct ShelfView: View    var shelf: Shelf    @State var selectedBook: Book    var shelfOperator: ShelfOperator    var body: some View {        ForEach(shelf.books) { book in            Text(book.title).tapGesture {               // intercepting tap to update the book view with the new selected book               self.selectedBook = book            }        }        BookView(book: selectedBook, bookOperator: operator)    }}// This might seem redundant to ShelfOperator, however it's not// A view that renders a book doesn't need to know about shelf operations// Interface Segregation Principle FTW :)protocol BookOperator: PageOperator {    func add(page: Page, to book: Book)}struct BookView: View {    var book: Book    var bookOperator: BookOperator    var body: some View { ... }}// Segregating the functionality via protocols has multiple advantages:// 1. this "leaf" view is not polluted with all kind of operations the big// State would have// 2. PageView is highly reusable, since it only depends on entities it needs// to do its job.protocol PageOperator {    func update(text: String, for page: Page)}struct PageView: View {    var page: Page    var pageOperator: PageOperator    var body: some View { ... }

What happens with the code above is that data flows downstream, and events propagate upstream, and any changes caused by events are then propagated downstream, meaning your views are always in sync with the data.

Once you're done with the editing, just grab the list of shelves from the State instance and send them to the backend.


Well, the preferable design in this case would be to use MVVM based on ObservableObject for view model (it allows do not touch/change generated model, but wrap it into convenient way for use in View).

It would look like following

class Library: ObservableObject {  @Published var shelves: [Shelf] = []}

However, of course, if required, all can be done with structs only based on @State/@Binding only.

Assuming (from mockup) that the initial shelf is loaded in some other place the view hierarchy (in simplified presentation just to show the direction) can be:

struct ShelfView: View {    @State private var shelf: Shelf    init(_ shelf: Shelf) {        _shelf = State<Shelf>(initialValue: shelf)    }    var body: some View {        NavigationView {            List {                ForEach(Array(shelf.books.enumerated()), id: \.1.id) { (i, book) in                    NavigationLink("Book \(i)", destination: BookView(book: self.$shelf.books[i]))                }                .navigationBarTitle(shelf.title)            }        }    }}struct BookView: View {    @Binding var book: Book    var body: some View {        List {            ForEach(Array(book.pages.enumerated()), id: \.1.id) { (i, page) in                NavigationLink("Page \(i)", destination: PageView(page: page))            }            .navigationBarTitle(book.title)        }    }}struct PageView: View {    var page: Page    var body: some View {        ScrollView {            Text(page.content)        }    }}