I noticed a problem when playing with SwiftUI's List, when I build my row views I create a view model like so:
struct MyListView: View {
var body: some View {
NavigationView {
List(models) { model in
RowView(viewModel: RowViewModel(model: models))
}
.navigationTitle("Landmarks")
}
}
it doesn't matter how big is the list, my view models are never deallocated, I am not sure to which extend the views are actually being recycled, but I can see by using the memory debugger on Xcode that as I scroll and more views are built, more and more view models are allocated and kept in memory, this is how my row view looks like:
struct RowView: View {
var viewModel: RowViewModel
var body: some View {
HStack {
Text(viewModel.title)
Spacer()
}
}
}
and my view model:
class RowViewModel {
private let model: Model
init(model: model) {
self.model = model
}
var title: String {
model.title
}
}
I also tried before using #ObservedObject but that did not make a difference.
Related
I'd like to hide/show the details split view of a NavigationSplitView on macOS.
However NavigationSplitViewVisibility does not seem to have such option. Changing .navigationSplitViewColumnWidth() or .frame() has no effect on the details view although it works well with the content and list view.
NavigationSplitView {
List(selection: $selection)
} content: {
content(for: selection)
} detail: {
Text("Detail")
}
Did Apple forget to implement such a feature? :/
Trying to figure out an answer to the same question for myself, I have come to this conclusion:
A NavigationSplitView is meant to display a hierarchy where each next level (sidebar, content, detail) is a sub-level of of the previous one. In such a structure you might always want to show a detail view, even it is empty.
In any case, even if that is not the logic, the way to make the "detail" part hidable would be by implementing a two-column navigation with NavigationSplitView and adding a DetailView, enclosing all of these in an HStack and making the DetailView visibility conditional:
struct MyView: View {
#State var showingDetail: Bool = true
var body: some View {
HStack {
NavigationSplitView {
SidebarView()
} detail: {
ContentView()
}
if showingDetail {
DetailView()
}
}
.toolbar {
Toggle(isOn: $showingDetail) {
Image(systemName: "sidebar.trailing")
}
.toggleStyle(.button)
}
}
}
I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.
I looked everywhere for a solution to my problem, but I couldn't find any. There's this question that
is similar, but I think I'm having a different problem here. So, my code (Xcode 12.1, developing for iOS 14.0) looks like this:
import SwiftUI
struct ContentView: View {
#ObservedObject var cm : FolderModel //Which is conformed to Codable, Identifiable, Equatable, ObservableObject
#ObservedObject var dm : DataManager //Which is conformed to Equatable, Identifiable, ObservableObject
#State var pressedFolder = false
#State var valview : ValuesView
NavigationView {
VStack{
ScrollView(.horizontal) {
HStack { ForEach(dm.storageFolders) { foldersForEach in
Button(action: {
valview = ValuesView(cm: foldersForEach, dm: dm)
pressedFolder = true
}, label: {
Text(foldersForEach.folderName)})
}
if pressedFolder == false {
Form {
ForEach(dm.values) { passwordDelForEach in
NavigationLink(//This works correctly)
}
}
} else if pressedFolder == true {
valview //This is the thing that it's not updating when values are added to the folders
}
}
struct ValuesView : View {
#ObservedObject var cm : FolderModel //Which is conformed to Codable, Identifiable, Equatable, ObservableObject
#ObservedObject var dm : DataManager //Which is conformed to Equatable, Identifiable, ObservableObject
var body : some View {
Form {
ForEach (cm.folderValues) { folderValuesForEach in
NavigationLink(//This works correctly)
}
}
}
}
The arrays into the DataManager are all declared like this:
#Published var storage : [StorageModel] = [] {
didSet {
objectWillChange.send()
}
}
typealias Storage = [StorageModel]
If I add anything into the arrays (from another View), data is stored correctly because by opening the .plist file (that the DataManager creates) I can see it gets correctly updated. Plus, every Button that I use has either the func of the DataManager save() (which has objectWillChange.send() within it) or I manually add dm.objectWillChange.send() to the action of the Button.
Despite all this, the things into the ForEach don't update. I only see the things that were there the first time I open the app, and to see the changes I have to close the app and reopen it.
What am I doing wrong?
Thanks to everyone who will answer!
I am seeing the same thing, only as of using the 14.2 simulator. I'm still trying to figure it out, but it seems like views inside of a ForEach are not properly re-rendered on data change.
I am currently developing an app for watchOS 6 (independent app) using Swift/SwiftUI in XCode 11.5 on macOS Catalina.
Before a user can use my app, a configuration process is required. As the configuration process consists of several different views which are shown one after each other, I implemented this by using navigation links.
After the configuration process has been finished, the user should click on a button to return to the "main" app (main view). For controlling views which are on the same hierarchical level, my plan was to use an EnvironmentObject (as far as I understood, an EnvironmentObject once injected is handed over to the subviews and subviews can use the EnvironmentObject) in combination with a "controlling view" which controls the display of the views. Therefore, I followed the tutorial: https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
This is my code:
ContentView.swift
struct ContentView: View {
var body: some View {
ContentViewManager().environmentObject(AppStateControl())
}
}
struct ContentViewManager: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
if(appStateControl.callView == "AppConfig") {
AppConfig()
}
if(appStateControl.callView == "AppMain") {
AppMain()
}
}
}
}
AppStateControl.swift
class AppStateControl: ObservableObject {
#Published var callView: String = "AppConfig"
}
AppConfig.swift
struct AppConfig: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Main")
NavigationLink(destination: DetailView1().environmentObject(appStateControl)) {
Text("Show Detail View 1")
}
}
}
}
struct DetailView1: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 1")
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
}
}
}
struct DetailView2: View {
#EnvironmentObject var appStateControl: AppStateControl
var body: some View {
VStack {
Text("App Config Detail View 2")
Button(action: {
self.appStateControl.callView = "AppMain"
}) {
Text("Go to main App")
}
}
}
}
AppMain.swift
struct AppMain: View {
var body: some View {
Text("Main App")
}
}
In a previous version of my code (without the handing over of the EnvironmentObject all the time) I got a runtime error ("Thread 1: Fatal error: No ObservableObject of type AppStateControl found. A View.environmentObject(_:) for AppStateControl may be missing as an ancestor of this view.") caused by line 41 in AppConfig.swift. In the internet, I read that this is probably a bug of NavigationLink (see: https://www.hackingwithswift.com/forums/swiftui/environment-object-not-being-inherited-by-child-sometimes-and-app-crashes/269, https://twitter.com/twostraws/status/1146315336578469888). Thus, the recommendation was to explicitly pass the EnvironmentObject to the destination of the NavigationLink (above implementation). Unfortunately, this also does not work and instead a click on the button "Go to main App" in "DetailView2" leads to the view "DetailView1" instead of "AppMain".
Any ideas how to solve this problem? To me, it seems that a change of the EnvironmentObject in a view called via a navigation link does not refresh the views (correctly).
Thanks in advance.
One of the solutions is to create a variable controlling whether to display a navigation stack.
class AppStateControl: ObservableObject {
...
#Published var isDetailActive = false // <- add this
}
Then you can use this variable to control the first NavigationLink by setting isActive parameter. Also you need to add .isDetailLink(false) to all subsequent links.
First link in stack:
NavigationLink(destination: DetailView1().environmentObject(appStateControl), isActive: self.$appStateControl.isDetailActive) {
Text("Show Detail View 1")
}
.isDetailLink(false)
All other links:
NavigationLink(destination: DetailView2().environmentObject(appStateControl)) {
Text("Show Detail View 2")
}
.isDetailLink(false)
Then just set isDetailActive to false to pop all your NavigationLinks and return to the main view:
Button(action: {
self.appStateControl.callView = "AppMain"
self.appStateControl.isDetailActive = false // <- add this
}) {
Text("Go to main App")
}
I have a struct model called Questions stored in DataRep which is also stored in EnvironmentObject. There are 100+ questions in 12 categories.
struct Question: Hashable, Codable, Identifiable {
var id: Int
var questionText: String
var questionComment: String
var category: String
}
class DataRep: ObservableObject {
#Published var QuestionList : [Question] = QuestionListData
#Published var selectedCategory = "all"
}
On the user interface, I placed 12 buttons at top and list view down to list the questions in that category.
When user clicks on a new category, I update the selectedCategory parameter and filter the main questions list object to select the relevant questions.
struct QuestionList: View {
#EnvironmentObject var datarep: DataRep
var body: some View {
NavigationView{
Form {
ForEach(self.filterQuestions(datarep.QuestionList)) { question in
HStack{
QuestionView (question: question)
}
}
}//List
.navigationBarTitle(self.datarep.selectedCategory )
.labelsHidden()
.listStyle(GroupedListStyle())
}
}
func filterQuestions(_ activeList : [Question]) -> [Question]
{
if self.datarep.selectedCategory != "all" {
return activeList.filter{ $0.category.contains(self.datarep.selectedCategory) }
}
return activeList
}
}
However I am running into issues with filter method as it is generating a new array each time category is changed. There is no way I know to create a binding.
any suggestions?
Regards,
M
Assuming the QuestionView will take binding (to change the question), as
struct QuestionView: View {
#Binding var question: Question
...
it can be bound (even after filtering) via main container like following
ForEach(self.filterQuestions(datarep.QuestionList)) { question in
HStack{
QuestionView (question:
self.$datarep.QuestionList[self.datarep.QuestionList.firstIndex(of: question)!])
}
}