How Can I Dynamically Display Content of an Outer View Based on the State in a Nested View in SwiftUI?

155 views Asked by At

First time using swift and this is what I've gathered so far. I'm not sure why the HomeContentView isn't displaying the content dynamically based on filemanager.directoryFiles. Is there a simple way to display the content of an Outer view dynamically based on a state present within an Inner view?

import SwiftUI

struct FileManagerView: View {
    
    @State private var directoryFiles: String
    
    fileprivate init(directoryFiles: String) {
        self.directoryFiles = directoryFiles
    }
    
    func getDirectoryFiles() -> String {
        return self.directoryFiles
    }
    
    func selectFolder() {
        self.directoryFiles = "Test"
    }
    
    var body: some View {
        Button(action: {
            self.directoryFiles = "Test"
        }) {
            Text("Select Folder")
        }
    }
}

struct HomeContentView: View {

    let filemanager = FileManagerView(directoryFiles: "")
    
    var body: some View {
        VStack {
            Text("Select a file directory to generate photo albums from.")
                .padding(5)
                .multilineTextAlignment(.center)
            
            if (filemanager.getDirectoryFiles() == "") {
                filemanager
            } else if (filemanager.getDirectoryFiles() == "Test") {
                Text("Test is selected")
            } else {
                Text(filemanager.getDirectoryFiles())
            }
        }
    }
}

#Preview {
    HomeContentView()
}

2

There are 2 answers

0
rayaantaneja On BEST ANSWER

Okay so hopefully you managed to get this to work using a binding on your own. Here is my solution to compare it to:

struct HomeContentView: View {
    @State private var fileSelectedViaFileManagerView = ""
    
    var body: some View {
        VStack {
            Text("Select a file directory to generate photo albums from.")
                .padding(5)
                .multilineTextAlignment(.center)
            
            if fileSelectedViaFileManagerView == "" {
                FileManagerView(selectedFile: $fileSelectedViaFileManagerView)
            } else {
                Text("\(fileSelectedViaFileManagerView) is selected")
            }
        }
    }
}

struct FileManagerView: View {
    @Binding var selectedFile: String
    
    var body: some View {
        Button("Select Folder") {
            selectedFile = "Test"
        }
    }
}

You should read this amazing article on passing data between views in SwiftUI. It's all you'll ever need: https://www.vadimbulavin.com/passing-data-between-swiftui-views/

In summary:

  • From Parent to Direct Child: Use Initializer
  • From Parent to Distant Child: Use @Environment
  • From Child to Direct Parent: Use @Binding and Callbacks
  • From Child to Distant Parent: Use PreferenceKey
  • Between Children: Lift the State Up (have the parent contain the @State property)

The reason why your view wasn't updating earlier was because SwiftUI only finds out when the value of properties have changed. So using the return value of a function like

if (filemanager.getDirectoryFiles() == "") {
    filemanager
} else if (filemanager.getDirectoryFiles() == "Test") {
    Text("Test is selected")
} else {
    Text(filemanager.getDirectoryFiles())
}

wouldn't trigger an update as those aren't properties. Best of luck learning Swift!

0
vadian On

You are mixing up a view and a view model.

The source of truth must be created in the parent view and is passed through with a Binding

struct HomeContentView: View {
    @State private var directoryFiles = ""
    
    var body: some View {
        VStack {
            Text("Select a file directory to generate photo albums from.")
                .padding(5)
                .multilineTextAlignment(.center)
            
            if directoryFiles.isEmpty {
                FileManagerView(directoryFiles: $directoryFiles)
            } else if directoryFiles == "Test" {
                Text("Test is selected")
            } else {
                Text(directoryFiles)
            }
        }
    }
}

struct FileManagerView: View {
    
    @Binding var directoryFiles : String
    
    func selectFolder() {
        directoryFiles = "Test"
    }
    
    var body: some View {
        Button(action: selectFolder) {
            Text("Select Folder")
        }
    }
}

An alternative with a real view model is a class conforming to @ObservableObject but it follows the same rule that the source of truth is created in the parent view

class AFileManager : ObservableObject {
    @Published var directoryFiles = ""
}

struct HomeContentView: View {

    @StateObject private var fileManager = AFileManager()
    
    var body: some View {
        VStack {
            Text("Select a file directory to generate photo albums from.")
                .padding(5)
                .multilineTextAlignment(.center)
            
            if fileManager.directoryFiles.isEmpty {
                FileManagerView(fileManager: fileManager)
            } else if fileManager.directoryFiles == "Test" {
                Text("Test is selected")
            } else {
                Text(fileManager.directoryFiles)
            }
        }
    }
}

struct FileManagerView: View {
    
    @ObservedObject var fileManager : AFileManager
    
    func selectFolder() {
        fileManager.directoryFiles = "Test"
    }
    
    var body: some View {
        Button(action: selectFolder) {
            Text("Select Folder")
        }
    }
}