Why views not destroying completely in SwiftUI - swiftui

When go to some View, go back, and then go again to this view, variables have data, so my view is not completely destroys. How can I destroy my view completely?
For example:
struct Home: View {
var body : some View {
VStack {
NavigationLink(destination : SliderView()){
Text("\(self.myData.lastText)").bold()
}
}
}
}
struct SliderView: View {
#ObservedObject myData : MyData = MyData()
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body : some View {
VStack {
Button(action : {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Dismiss").bold()
}
Text("\(self.myData.lastText)").bold()
}
}
}
When I go to SliderView then go back and go to SliderView again, self.myData.lastText is not empty, and it has previous value.

As of Xcode 12 & iOS 14, you can now use #StateObject. See the official documentation for more information.

I experienced same symptoms.
the reason is that
Home is not recreated when go back to Home as Home is root view
and Home's NavigationLink contains SliderView() and use existing SliderView
I think you can test the below and find SliderView is recreated and load data again.
This is just showing what make the problem. I don't think the code below is good.
as I have short experience on SwiftUI. kindly let me know if something wrong. and I don't know what is the best approach to solve this. though the below code can solve the problem.
struct Home: View {
#State var appearCount = 0
var body : some View {
VStack {
if (appearCount > 0) {
//this code is added just for re draw view. when appearCount is changed
}
NavigationLink(destination : SliderView()){
Text("\(self.myData.lastText)").bold()
}
}.onAppear { appearCount += 1 }
}
}

The view is destroying, but the new view uses the previous viewModel.
You are referencing the same MyData() object all the time. So if you change it anywhere, all references will be updated. And since seems like you are observing for changes (#ObservedObject) in realtime, it will update as soon as it changes anywhere.
You should provide more detail for more accurate information.

Related

Task in SwiftUI runs but view that is inside a sheet is not updated

I have a simple view that is using a class to generate a link for the user to share.
This link is generated asynchronously so is run by using the .task modifier.
class SomeClass : ObservableObject {
func getLinkURL() async -> URL {
try? await Task.sleep(for: .seconds(1))
return URL(string:"https://www.apple.com")!
}
}
struct ContentView: View {
#State var showSheet = false
#State var link : URL?
#StateObject var someClass = SomeClass()
var body: some View {
VStack {
Button ("Show Sheet") {
showSheet.toggle()
}
}
.padding()
.sheet(isPresented: $showSheet) {
if let link = link {
ShareLink(item: link)
} else {
HStack {
ProgressView()
Text("Generating Link")
}
}
}.task {
let link = await someClass.getLinkURL()
print ("I got the link",link)
await MainActor.run {
self.link = link
}
}
}
}
I've simplified my actual code to this example which still displays the same behavior.
The task is properly executed when the view appears, and I see the debug print for the link. But when pressing the button to present the sheet the link is nil.
The workaround I found for this is to move the .task modifier to be inside the sheet, but that doesn't make sense to me nor do I understand why that works.
Is this a bug, or am I missing something?
It's because the sheet's closure is created before it is shown and it has captured the old value of the link which was nil. To make it have the latest value, i.e. have a new closure created that uses the new value, then just add it to the capture list, e.g.
.sheet(isPresented: $showSheet) { [link] in
You can learn more about this problem in this answer to a different question. Also, someone who submitted a bug report on this was told by Apple to use the capture list.
By the way, .task is designed to remove the need for state objects for doing async work tied to view lifecycle. Also, you don't need MainActor.run.

Bug in SwiftUI? iOS 15. List refresh action is executed on an old instance of View -- how to work around?

I'm using the refreshable modifier on List
https://developer.apple.com/documentation/SwiftUI/View/refreshable(action:)
The List is contained in a view (TestChildView) that has a parameter. When the parameter changes, TestChildView is reinstantiated with the new value. The list has a refreshable action. However, when pulling down to refresh the list, the refresh action is run against the original view instance, so it doesn't see the current value of the parameter.
To reproduce with the following code: If you click the increment button a few times, you can see the updated value propagating to the list item labels. However, if you pull down the list to refresh, it prints the original value of the parameter.
I assume this is happening because of how refreshable works .. it sets the refresh environment value, and I guess it doesn't get updated as new instances of the view are created.
It seems like a bug, but I'm looking for a way to work around -- how can the refreshable action see the current variable/state values?
import SwiftUI
struct TestParentView: View {
#State var myVar = 0
var body: some View {
VStack {
Text("increment")
.onTapGesture {
myVar += 1
}
TestChildView(myVar: myVar)
}
}
}
struct TestChildView: View {
let myVar: Int
struct Item: Identifiable {
var id: String {
return val
}
let val: String
}
var list: [Item] {
return [Item(val: "a \(myVar)"), Item(val: "b \(myVar)"), Item(val: "c \(myVar)")]
}
var body: some View {
VStack {
List(list) { elem in
Text(elem.val)
}.refreshable {
print("myVar: \(myVar)")
}
}
}
}
The value of myVar in TestChildView is not updated because it has to be a #Binding. Otherwise, a new view is recreated.
If you pass the value #State var myVar from TestParentView to a #Binding var myVar to TestChildView, you will have the value being updated and the view kept alive the time of the parent view.
You will then notice that the printed value in your console is the refreshed one of the TestChildView.
Here is the updated code (See comments on the updated part).
import SwiftUI
struct TestParentView: View {
#State var myVar = 0
var body: some View {
VStack {
Text("increment")
.onTapGesture { myVar += 1 }
TestChildView(myVar: $myVar) // Add `$` to pass the updated value.
}
}
}
struct TestChildView: View {
#Binding var myVar: Int // Create the value to be `#Binding`.
struct Item: Identifiable {
var id: String { return val }
let val: String
}
var list: [Item] {
return [Item(val: "a \(myVar)"), Item(val: "b \(myVar)"), Item(val: "c \(myVar)")]
}
var body: some View {
VStack {
List(list) { elem in
Text(elem.val)
}
.refreshable { print("myVar: \(myVar)") }
}
}
}
Roland's answer is correct. Use a binding so that the correct myVar value is used.
As to why: .refreshable, along with other stateful modifiers like .task, .onAppear, .onReceive, etc, operate on a different phase in the SwiftUI View lifecycle. You are correct in assuming that the closure passed to refreshable is stored in the environment and doesn't get updated as the views are recreated. This is intentional. It would make little sense to recreate this closure whenever the view is updated, because updating the view is kind of its intended goal.
You can think of .refreshable (and the other modifiers mentioned above) as similar to the #State and #StateObject property wrappers, in that they are persisted across view layout updates. A #Binding property can also be considered stateful because it is a two-way 'binding' to a state variable from a parent view.
In fact generally speaking, the closures you pass to .refreshable or .task should only read and write to stateful properties (such as viewModels) for this exact reason.

Is it possible to extract logic that depends on the Environment outside of the struct that uses it?

Is it possible to extract logic that depends on the SwiftUI environment outside of the views?
For example, consider the scenario where you have a Theme struct that computes a color depending on a property in the environment, something akin to the sample code below.
What I'd like to do is extract out the logic that computes a color so that it can be used in multiple places. Ideally I'd like to use #Environment in the Theme struct so that I only have to retrieve the value it in once place - the alternative is that I retrieve from the environment at the call site of my Theme computation and inject the value in. That alternative works fine, but I'd like to avoid the need to retrieve the environment value all over the place.
/// A structure to encapsulate common logic, but the logic depends on values in the environment.
struct Theme {
#Environment(\.isEnabled) var isEnabled: Bool
var color: Color {
isEnabled ? .blue : .gray
}
}
/// One of many views that require the logic above
struct MyView: View {
let theme: Theme
var body: some View {
theme.color
}
}
/// A little app to simulate the environment values changing
struct MyApp: App {
#State var disabled: Bool
var body: some Scene {
WindowGroup {
VStack {
Toggle("Disabled", isOn: $disabled)
MyView(theme: Theme())
.disabled(disabled)
}
}
}
}
The Sample code above doesn't work, ie if you toggle the switch in the app the View's color does not change. This sample code only serves to show how I'd ideally like it to work, particularly because it doesn't require me to litter #Environment throughout MyView and similar views just to retrieve the value and pass it into a shared function.
One thing that I thought could be causing the problem is that the Theme is created outside of the scope where the Environment is changing, but if I construct a Theme inside MyView the behaviour doesn't change.
My confusion here indicates that I'm missing something fundamental in my understanding of the SwiftUI Environment. I'd love to understand why that sample code doesn't work. If Theme were a View with the color logic in its body, it would be updating, so why doesn't it cause an update in it's current setup?
The approach should be different, view parts in views, model parts in models, "separate and rule"
struct Theme { // << view layer independent !!
func color(for enabled: Bool) -> Color { // << dependency injection !!
enabled ? .blue : .gray
}
}
struct MyView: View {
#Environment(\.isEnabled) var isEnabled: Bool
let theme: Theme
var body: some View {
theme.color(for: isEnabled) // << here !!
}
}
I feel like you're mixing a few things, so let me tell you how I'd structure this.
First of all, I don't think a theme should have state, it should be a repository of colors.
struct Theme {
var enabledColor: Color = .blue
var disabledColor: Color = .gray
}
Your View is where you should have your state
struct MyView: View {
#Environment(\.isEnabled) var isEnabled: Bool
var body: some View {
// ??
}
}
What I would suggest is that you create your own EnvironmentKey and inject your theme into the environment:
struct ThemeKey: EnvironmentKey {
static let defaultValue: Theme = .init()
}
extension EnvironmentValues {
var theme: Theme {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
Now your View can use the environment to read the theme. I like to keep my views light of logic, but presentation logic in them makes sense to me.
struct MyView: View {
#Environment(\.theme) var theme
#Environment(\.isEnabled) var isEnabled: Bool
var body: some View {
isEnabled ? theme.enabledColor : theme.disabledColor
}
}
You'll need to inject your theme at some point in your app, you should add it near the top of the view hierarchy to make sure all views get access to it.
struct MyApp: App {
#State var disabled: Bool
#State var theme: Theme = .init()
var body: some Scene {
WindowGroup {
VStack {
Toggle("Disabled", isOn: $disabled)
MyView()
.disabled(disabled)
}
.environment(\.theme, theme)
}
}
}
I wrote this article about using the environment to provide values to the view hierarchy, you may find it useful.

Why the List does not appear?

Why the presetsList does not appear? No errors were thrown though.
import SwiftUI
struct AddMessagePreset: View {
let presetsList = [
Preset(name: "preset text 1"),
Preset(name: "preset text 2"),
Preset(name: "preset text 3")
]
var body: some View {
List(presetsList) { singlePresetModel in
SinglePresetChild (presetModel: singlePresetModel)
}
}
}
import SwiftUI
struct Preset: Identifiable {
let id = UUID()
let name: String
}
struct SinglePresetChild: View {
var presetModel: Preset
var body: some View {
Text("Preset Name \(presetModel.name)")
}
}
UPDATE: To show a List inside another ScrollView (or List), you have to set a height on the inner list view:
struct Preview: View {
var body: some View {
ScrollView {
AddMessagePreset().frame(height: 200)
// more views ...
}
}
}
But let me advise against doing so. Having nested scroll areas can be very confusing for the user.
As discussed in the comments, your component code is fine. However, the way you integrate it into your app causes a problem. Apparently, nesting a List inside a ScrollView does not work properly (also see this thread).
List is already scrollable vertically, so you won't need the additional ScrollView:
struct Preview: View {
var body: some View {
VStack {
AddMessagePreset()
}
}
}
P.S.: If you only want to show AddMessagePreset and won't add another sibling view, you can remove the wrapping VStack; or even show AddMessagePreset as the main view, without any wrapper.

LocationManager coordinates not available onAppear on Watch app

I am programming a watch app based on some functionality of my iPhone app. On the phone, everything is working as intended, but on the watch app, I am getting location data too late. However, I can't figure out what is the correct way of doing it.
My working ContentView looks like this:
struct ContentView: View {
#ObservedObject var locationManager = LocationManager()
#ObservedObject var sunTimeData: SunTimeData
var body: some View {
VStack {
OverView().environmentObject(sunTimeData)
}.onAppear {
self.setLocation()
}
}
private func setLocation() {
sunTimeData.latitude = locationManager.location?.latitude ?? 0
sunTimeData.longitude = locationManager.location?.longitude ?? 0
LocationManager.retreiveCityName(latitude: sunTimeData.latitude, longitude: sunTimeData.longitude) { (city) in
self.sunTimeData.cityName = city ?? "Not found"
}
}
}
This is working fine and passing my location data to the environment object where I can use it in several views.
The Watch app is structured the same way, however, it doesn't obtain location info and just passes 0. If I call the locationManager INSIDE a view (inside a Text() for example), it will work, but as I use this on many different places, I would rather like to pass it to my environment object and go from there.
I tried adding variables above the body, but they are also still 0 at the time of calling. The locationManager starts to update just after rendering the view, it seems.
var latitude: Double {
return locationManager.location?.latitude ?? 0
}
var longitude: Double {
return locationManager.location?.longitude ?? 0
}
I am using the HostingController.swift to have a paginated navigation (swipe between views) and the second page is having the correct location data.
class HostingController: WKHostingController<AnyView> {
override var body: AnyView {
return AnyView(TestView().environmentObject(SunTimeData()))
}
}
class NewHostingController: WKHostingController<AnyView> {
override var body: AnyView {
return AnyView(ListView().environmentObject(SunTimeData()))
}
}
Do I probably have to pass the locationManager to the views, too? Where is my mistake? Could anybody please guide me the right way to do this? Thank you.