Leaks in NavigationView/List/ForEach with Dynamically Generated Views - swift

If you create a very simple example that shows a lot of leaking object within the SwiftUI code if you nest NavigationView/List/ForEach and return different types of views in the ForEach closure.
import SwiftUI
class MyStateObject : ObservableObject {
#Published var items:[Int]
init() {
self.items = Array(0..<1000)
}
}
struct ContentView: View {
#StateObject var stateObject = MyStateObject()
var body: some View {
NavigationView {
List {
ForEach(stateObject.items, id: \.self) { item in
if(item % 2 == 0) {
Text("Even \(item)")
}
else {
Image(systemName: "xmark.octagon")
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I strongly suspect this is a bug in SwiftUI but I wanted to ask if I am doing anything wrong here.
You can see the leaks by attaching Instruments. It will show immediately and increase if you scroll through the list.
Interestingly, it seems the leaks don't happen if
you remove the NavigationView from the hierarchy.
you only supply one type of View in ForEach (and don't branch via if/else).
the list of items you want to show is small (100 does not seem to result in leaks).
(Tested on XCode 12.5 and iOS 14.5 Simulator and Device,)
Since in my app I am pretty much reliant on this kind of hierarchy, I am very open for some suggestions on how to avoid the leaking.

Related

Left animation shift when using nested NavigationLink

I am developing an application using SwiftUI (XCode 12.5.1) and every time one of my View appears after exactly two links of "NavigationLink" everything that is inside a Form is shifted slightly to the left, once the appearing animation is over. The following video shows whats going on : the first two times I open the view, everything is fine. The next two times, when the view is accessed from nested NavigationLink, a slight shift to the left is done once the appearing animation is over.
https://www.dropbox.com/s/k3gjc42xlqp2auf/leftShift.mov?dl=0
I have the same problem on both the simulator and a real device (an iPhone). Here is the project: https://www.dropbox.com/s/l8r5hktg6lz69ob/Bug.zip?dl=0 . The main code is available below.
Here is the main view ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: PersonView()) {
Text("Person")
}
NavigationLink(destination: IndirectView()) {
Text("Indirect")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is the indirect view, IndirectView.swift
import SwiftUI
struct IndirectView: View {
var body: some View {
List {
NavigationLink(destination: PersonView()) {
Text("Person")
}
}
}
}
and the person view, PersonView.swift
import SwiftUI
struct PersonView: View {
var body: some View {
Form {
VStack(alignment: .leading, spacing: 5) {
Text("Last Name")
.font(.system(.subheadline))
.foregroundColor(.secondary)
Text("Fayard")
}
}
}
}
Do you have any idea on what's causing this shift?
Thanks for your help
Francois
Frankly saying I have not idea what causes the problem, but here is the fix: add this line of code no your NavigaitonView
NavigationView {
// everything else
}.navigationViewStyle(StackNavigationViewStyle())

SwiftUI #ObservedObject viewmodel in detail-view of List never released [duplicate]

This question already has answers here:
ObservedObject view-model is still in memory after the view is dismissed
(2 answers)
Closed 2 years ago.
I have a List with several items, that open a DetailView which in turn holds a viewmodel. The viewmodel is supposed to have a service class that gets initialized when the detail view appears and should be deinitialized when navigating back.
However, the first problem is that in my example below, all 3 ViewModel instances are created at the same time (when ContentView is displayed) and never get released from memory (deinit is never called).
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: DetailView()) {
Text("Link")
}
NavigationLink(destination: DetailView()) {
Text("Link")
}
NavigationLink(destination: DetailView()) {
Text("Link")
}
}
}
}
}
struct DetailView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text("Hello \(viewModel.name)")
}
}
class ViewModel: ObservableObject {
#Published var name = "John"
private let heavyClient = someHeavyService()
init() { print("INIT VM") }
deinit { print("DEINIT VM") }
}
This is probably just how SwiftUI works, but I have a hard time thinking of a way to handle class objects that are part of a detail view's state, but are not supposed to instantiate until the detail view actually appears. An example would be video conferencing app, with rooms, where the room client that establishes connections etc. should only get initialized when actually entering the room and deinitialize when leaving the room.
I'd appreciate any suggestions on how to mange this. should I initialize the heavyClient at onAppear or something similar?
The problem is that DetailView() is getting initialized as part of the navigation link. One possible solution could be the LazyView from this post.
Implemented like so:
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
And then wrap the DetailView() in the LazyView():
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
NavigationLink(destination: LazyView(DetailView())) {
Text("Link")
}
}
}
}
}
The only issue with this workaround is that there seems to always be one instance of ViewModel sitting around, though it's a large improvement.

SwiftUI - Presenting a View on top of the current View from AppDelegate

I am working on a project that at some point it receives a notification. When that happens, I need to show a View. I am not able to catch notification from any View so I am looking for a way to change to control it from outside of View structs. After the View's purpose is done, I need to dismiss it where the app left off. Think like the native behaviour when there is an active call.
I thought I could use sheet however I could not find any way to trigger it for every View that could be active when the notifications come. Or maybe trying to extend native View class would work but again, no luck finding a tutorial.
Any help will be appreciated.
Just update your model based on notification. There is not necessary to define .sheet (modal view) everywhere in your view hierarchy. Doing it in root view should be enough.
To demonstrate that (copy - paste - run) I create small project where I mimic notification with SwiftUI Toggle.
import SwiftUI
class Model: ObservableObject {
#Published var show = false
}
struct SubView: View {
#EnvironmentObject var model: Model
var tag: Int
var body: some View {
VStack {
NavigationLink(destination: SubView(tag: tag + 1).environmentObject(model)) {
Text("subview \(tag)")
}
if tag == 2 {
Toggle(isOn: $model.show) {
Text("toggle")
}.padding()
}
}.navigationBarTitle("subview \(tag)")
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
SubView(tag: 0).environmentObject(model)
}.sheet(isPresented: $model.show) {
Text("sheet")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the result

SwiftUI CoreData crashes preview

I have the following code to draw a list of cars, data stored in coredata.
However the swiftui preview seems to break when I add the line of code that fetches data from the databases.
the error logs tells the following:
PotentialCrashError: test app.app may have crashed
mileage app.app may have crashed. Check
~/Library/Logs/DiagnosticReports for any crash logs from your
application.
==================================
| Error Domain=com.apple.dt.ultraviolet.service Code=12 "Rendering
service was interrupted" UserInfo={NSLocalizedDescription=Rendering
service was interrupted}
this is the code the part where foreach starts and ends causes the error:
import SwiftUI
struct CarListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: Car.all()) var cars: FetchedResults<Car>
var body: some View {
NavigationView {
ZStack {
List {
Section(header: Text("Cars")) {
ForEach(self.cars, id: \.numberPlate) { car in
HStack {
VStack(alignment: .leading) {
Text(car.name)
Text(car.numberPlate)
}
}
}
}
}
}
}
}
}
struct CarListView_Previews: PreviewProvider {
static var previews: some View {
CarListView()
}
}
The issue seems to be related to the fact it couldn't somehow get a context which allows to fetch the data in preview mode. By manually doing so for preview mode it fixes the issue.
struct CarListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return CarListView().environment(\.managedObjectContext, context)
}
}
In case anyone else was wondering, this crashed because when simulating a preview for a specific view, it doesn't have contextual information that is provided by the base system environment: in this case, the managedObjectContext. So, it will crash because he's referencing an object provided by the environment. By providing a static version of the object (viewContext: ManagedObjectContext), it allows the preview to load and assert any needed context.
For most newer applications the following will also work:
struct CarListView_Previews: PreviewProvider {
static var previews: some View {
let persistentController = PersistentController.preview
CarListView().environment(\.managedObjectContext, persistentController.container.viewContext)
}
}

How to use #State and #Binding parallel?

My setup:
I am having a ContentView that represents a the length of a final selection (selection.count). Therefore I need a selection variable on my ContentView using a #State propertyWrapper since I want the View to get update as soon as the value changes. The selection is supposed to be made on my SelectionView therefore I am creating a Binding between my selection variables on the ContentView and SelectionView.
My Problem: The UI on my SelectionView is supposed to be updated as well when the selection variable changes but since it is using #Binding and not #State the view does not get updated. So I would need something where I can use a #State and #Binding at the same time or a #Binding which also makes the UI reload.
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(selection.count)")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
Form {
ForEach((0...9).identified(by: \.self)) { i in
Button(action: {
if self.selection.contains(i) {
self.selection = self.selection.filter { !($0 == i) }
} else {
self.selection.append(i)
}
}) {
if self.selection.contains(i) {
Text("Unselect \(i)")
} else {
Text("Select \(i)")
}
}
}
}
}
}
}
Note: If I am using #State on the SelectionView instead of #Binding it works properly (which obviously requires me to not create the binding which I want).
There's nothing wrong with your binding. It is the right thing to do and it works the way you want it to. A binding is the way you pass mutable state around in SwiftUI, and you are doing that, and it works. A change to a binding does make the view reload.
To convince yourself of that, just get rid of all the extra stuff in your example, and concentrate on the heart of the matter, the binding:
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(selection.count)")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
VStack {
Button.init("Append") {
self.selection.append(1)
}
Text("Selection: \(selection.count)")
}
}
}
}
Run the example, tap the link, tap the button repeatedly. The display of selection.count is changed in both views. That's what you wanted and that's what happens.
Here's a variant on your original code that displays selection more explicitly (instead of selection.count), and you can see that the right thing is happening:
struct ContentView: View {
#State var selection: [Int] = []
var body: some View {
NavigationView {
Form {
NavigationLink(destination: SelectionView(selection: $selection)) {
Text("Selection: \(String(describing:selection))")
}
}
}
}
}
struct SelectionView: View {
#Binding var selection: [Int]
var body: some View {
NavigationView {
List {
ForEach(0...9, id:\.self) { i in
Button(action: {
if let ix = self.selection.firstIndex(of:i) {
self.selection.remove(at: ix)
} else {
self.selection.append(i)
}
}) {
if self.selection.contains(i) {
Text("Unselect \(i)")
} else {
Text("Select \(i)")
}
}
}
}
}
}
}
The solution to your problem is: upgrade to the latest Xcode.
I tested your code in a playground under Xcode 11 beta 4 and it worked correctly.
I tested your code in a playground under Xcode 11 beta 3 and it failed in the way you describe (I think).
The current version of Xcode 11 as of this answer is beta 5, under which your code doesn't compile because the identified(by:) modifier has been removed. When I changed your code to work under beta 5, by replacing ForEach((0...9).identified(by: \.self)) with ForEach(0...9, id: \.self), it worked correctly.
Therefore I deduce that you are still running beta 2 or beta 3. (Form wasn't available in beta 1 so I know you're not using that version.)
Please understand that, at the moment, SwiftUI is still quite buggy, and also still undergoing incompatible API changes. The bugs are unfortunate, but the API evolution is good. It's better that we suffer a few months of changes now than years of less optimal API later.
This means that you need to try to stay on the latest Xcode 11 beta unless it introduces bugs (like the Path bug in beta 5, if your app uses Path) that prevent you from making any progress.
Thanks to #robmayoff I was able to solve the problem. It was a problem with Xcode 11 Beta 3, installing the newest beta version solved the problem