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:
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
Swiftui Label Text and Image Vertically Misaligned
Alamofire Type 'Parameterencoding' Has No Member 'Url' Swift 3
Can't Form Range with End < Start Check Range Before Doing for Loop
Swift: Programmatically Enumerate Outgoing Segues from a Uiviewcontroller
How to Prevent Timer Slowing Down in Background
Swift: How to Implement a Login Storyboard
Calculate the Angle Between a Line and X-Axis
Wkwebview Auto Fill Login Form Swift 2
What Is Trailing Closure Syntax in Swift
Detect Left and Right Click Events on Nsstatusitem (Swift)
Swift 3 - Search Result Also with Diacritics
Accessor Gives the Wrong Value in Swift 1.2/2.0 Release Build Only
How to Use Trailing Closure in If Condition
How to Create Instance Variable and Class Variable of the Same Name