How to Manage Swiftui State with Nested Structs

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.

Initialization of a nested struct in SwiftUI

A reasonable way to avoid the optional is an enum with associated values

For example

enum LoadingState {
case idle, loading(Double), loaded(IotShadow), failed(Error)
}

@Published var state LoadingState = .idle

In the view switch on the state.

Nested Struct models not causing view to re-render SwiftUI

I tried using mutating function and also updating value directly, both cases it worked.

UPDATED CODE (Added UIImage in new struct)

import SwiftUI
import Foundation

//Employee
struct Employee : Identifiable{

var id: String = ""
var name: String = ""
var address: Address
var userImage: UserIcon

init(name: String, id: String, address: Address, userImage: UserIcon) {
self.id = id
self.name = name
self.address = address
self.userImage = userImage
}

mutating func updateAddress(with value: Address){
address = value
}
}

//User profile image
struct UserIcon {
var profile: UIImage?

init(profile: UIImage) {
self.profile = profile
}

mutating func updateProfile(image: UIImage) {
self.profile = image
}
}

//Address
struct Address {

var houseName: String = ""
var houseNumber: String = ""
var place: String = ""

init(houseName: String, houseNumber: String, place: String) {
self.houseName = houseName
self.houseNumber = houseNumber
self.place = place
}

func getCompleteAddress() -> String{
let addressArray = [self.houseName, self.houseNumber, self.place]
return addressArray.joined(separator: ",")
}
}

//EmployeeViewModel
class EmployeeViewModel: ObservableObject {
@Published var users : [Employee] = []

func initialize() {
self.users = [Employee(name: "ABC", id: "100", address: Address(houseName: "Beautiful Villa1", houseNumber: "17ABC", place: "USA"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "XYZ", id: "101", address: Address(houseName: "Beautiful Villa2", houseNumber: "18ABC", place: "UAE"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "QWE", id: "102", address: Address(houseName: "Beautiful Villa3", houseNumber: "19ABC", place: "UK"), userImage: UserIcon(profile: UIImage(named: "discover")!))]
}


func update() { //both below cases worked
self.users[0].address.houseName = "My Villa"
//self.users[0].updateAddress(with: Address(houseName: "My Villa", houseNumber: "123", place: "London"))
self.updateImage()
}

func updateImage() {
self.users[0].userImage.updateProfile(image: UIImage(named: "home")!)
}
}

//EmployeeView
struct EmployeeView: View {
@ObservedObject var vm = EmployeeViewModel()

var body: some View {
NavigationView {
List {
ForEach(self.vm.users) { user in
VStack {
Image(uiImage: user.userImage.profile!)
Text("\(user.name) - \(user.address.getCompleteAddress())")
}
}.listRowBackground(Color.white)
}.onAppear(perform: fetch)
.navigationBarItems(trailing:
Button("Update") {
self.vm.update()
}.foregroundColor(Color.blue)
)
.navigationBarTitle("Users", displayMode: .inline)

}.accentColor(Color.init("blackTextColor"))
}

func fetch() {
self.vm.initialize()
}
}

SwiftUI state management of lists and navigation

You change thing which is ID of ForEach (by .self), so once you changed it that thing actually disappeared (for List) and new one appeared, so List's content updated.

A possible solution is to have persistent (separated) id of TestSettings so editing it would not affect its identity.

Like next

struct TestSettings : Identifiable, Hashable {
var name : String

var id = UUID() // << here !!
}

// ...

// identifiable, os explicit key-path is not needed, `id` used by default
ForEach($settings) { thing in // << here !!
NavigationLink(destination: SecondLevel(things: thing)) {
Text("X")
}
}

How to correctly show the last nested Child data from multiple json(books) in new SwiftUI while using Disclosure group/ Outline group?

This took some time to parse through. With the next question, please pull everything out of your code that is not necessary to the question, so it is easier to understand.

With you model, the first mistake you made was making everything optional. That gives you a level of complexity that is unnecessary. Your big concern was dealing with the arrays of Child, but you only have to deal with them if the arrays are not empty. If you make them optional, you are stuck having to unwrap them to then see if they are empty or not. That is unnecessary.

Also, as far as the data model goes, a BookContent == Child. There is absolutely no reason to have both, so I dropped Child.

Remodel the JSON so that every node has a value, even if it is simply an empty array or "" string. Since you control the JSON, keep it simple.

As you can see, I have rendered the views of each BookContent recursively, since every BookContent has an [BookContent]. If the [BookContent] is empty, the recursion ends.

Your Views:

struct ContentView: View {

@State var booksList: [BookModel] = [
BookModel(id: 1, bukTitle: "Book Title", isLive: false, userCanCopy: false, bookContent: [
BookContent(title: "Content Title", type: "", children: [
BookContent(title: "2nd Level Book Content", type: "", children: [
BookContent(title: "3rd Level Book Content", type: "", children: [
BookContent(title: "4th Level Book Content", type: "", children: [])
])
])
])
])]

var body: some View {
NavigationView {
VStack{
List(booksList) { book in
Text(book.bukTitle)
ForEach(book.bookContent) { bookContent in
BookContentView(bookContent: bookContent)
}
}
}
}
}
}

struct BookContentView: View {

let bookContent: BookContent

var body: some View {
Text(bookContent.title)
ForEach(bookContent.children) { bookContent in
BookContentView(bookContent: bookContent)
}
}
}

Your Models:

struct BookModel: Identifiable, Codable {
var id:Int
var bukTitle: String
var isLive: Bool
var userCanCopy: Bool
var bookContent: [BookContent]

enum CodingKeys: String, CodingKey {
case id = "id"
case bukTitle = "title"
case isLive = "is_live"
case userCanCopy = "user_can_copy"
case bookContent = "content"
}
}

struct BookContent: Identifiable, Codable {
let id = UUID()
var title, type: String
var children: [BookContent]

// Since your id is a let constant, adding CodingKeys without id
// silences the Codable warning that id won't be coded.
enum CodingKeys: String, CodingKey {
case title = "title"
case type = "type"
case children = "child"
}
}

Play with this code in a separate app, then work your way back through yours, integrating it as needed.



Related Topics



Leave a reply



Submit