I think I found a retain cycle when binding an observable to a .searchable modifier...
to verify: create a new swiftUI app; replace ContentView.swift's content with this:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink("To Retain Cycle") {
RetainCycleView()
}
}
.navigationTitle("Retain Cycle Demo")
}
.navigationViewStyle(.stack)
}
}
struct RetainCycleView: View {
#StateObject var model = Retainer()
// #State var enteredText: String = ""
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Navigate back to the previous view.")
Text("You will see that 'Retainer' was NOT deallocated.")
Text("(it's deinit function prints deallocing Retainer)")
.font(.callout)
}
.padding()
.searchable(text: $model.enteredText)
// ^---- retain cycle
// .searchable(text: $enteredText)
// ^---- no retain cycle when using the #State var
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Retainer: ObservableObject {
#Published var enteredText: String = ""
init() { print("instantiated Retainer") }
deinit { print("deallocing Retainer") }
}
When I pop the view off the stack, deinit won't be called for Retainer. If I remove the .searchable modifier, it does behave as expected.
Related
My environmentObject isn't working.I tap on navigationLink and see nothing in there.
I change note but it does not get updated.I made viewModel and share data from it everywhere I need it
I made the second TextEditor to do changes to my notes, but I cannot see changes.I just want to write smith and data should be updated
So how can I fix that?
import SwiftUI
#main
struct WhatToDoAppApp: App {
#StateObject private var vm = NoteViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
}
}
}
//ContentView.swift
import SwiftUI
struct ContentView: View {
#EnvironmentObject var vm: NoteViewModel
#State private var showSheet = false
#State private var searchText = ""
var body: some View {
NavigationView {
List {
ForEach(vm.notes) { item in
NavigationLink(destination: NoteDetailView()) {
Text(item.task)
.lineLimit(1)
}
}
.onDelete(perform: vm.deleteTask)
.onMove(perform: vm.moveTask)
}
.searchable(text: $searchText) {
if !searchResult.isEmpty {
ForEach(searchResult) { item in
NavigationLink(destination: NoteDetailView()) {
Text(item.task)
.lineLimit(1)
}
}
}
}
.navigationBarTitle("Notes")
.safeAreaInset(edge: .bottom) {
Color.clear
.frame(maxHeight: 40)
.background(.gray.opacity(0.7))
HStack {
Spacer(minLength: 160)
Text("\(vm.notes.count) notes")
.foregroundColor(.black.opacity(0.3))
Spacer()
Button {
showSheet = true
} label: {
Image(systemName: "square")
.font(.largeTitle)
.padding(.trailing)
}
}
}
.sheet(isPresented: $showSheet) {
NoteView()
}
}
}
var searchResult: [ToDoItem] {
guard !searchText.isEmpty else { return vm.notes }
return vm.notes.filter { $0.task.contains(searchText) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.preferredColorScheme(.dark)
ContentView()
.preferredColorScheme(.light)
}
.environmentObject(NoteViewModel())
}
}
//NoteDetailView.swift
import SwiftUI
struct NoteDetailView: View {
#EnvironmentObject var vm: NoteViewModel
var body: some View {
VStack {
TextEditor(text: $vm.text)
Spacer()
}
}
}
struct NotedetailView_Previews: PreviewProvider {
static var previews: some View {
NoteDetailView().environmentObject(NoteViewModel())
}
}
//NoteView.swift
import SwiftUI
struct NoteView: View {
// #State private var text = ""
#EnvironmentObject var vm: NoteViewModel
#Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
VStack {
TextEditor(text: $vm.text)
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
addTask()
dismiss()
vm.text = ""
}, label: {
Text("Done")
.font(.system(size: 25))
.foregroundColor(.accentColor)
})
}
}
}
}
func addTask() {
vm.add(ToDoItem(task: vm.text))
}
}
struct NoteView_Previews: PreviewProvider {
static var previews: some View {
NoteView()
.environmentObject(NoteViewModel())
}
}
import Foundation
struct ToDoItem: Identifiable, Codable {
var id = UUID()
var task : String
}
class NoteViewModel: ObservableObject {
#Published var notes = [ToDoItem]()
#Published var text = ""
let saveKey = "SavedKey"
init() {
if let data = UserDefaults.standard.data(forKey: saveKey) {
if let decoded = try? JSONDecoder().decode([ToDoItem].self, from: data) {
notes = decoded
return
}
}
notes = []
}
private func save() {
if let encoded = try? JSONEncoder().encode(notes) {
UserDefaults.standard.set(encoded, forKey: saveKey)
}
}
func add(_ note: ToDoItem) {
notes.append(note)
save()
}
func deleteTask(indexSet: IndexSet) {
indexSet.forEach { index in
self.notes.remove(at: index)
save()
}
}
}
The detail view should be a #Binding, and you can use the array that you have in the viewModel as an Bindable List here the fixes:
List {
ForEach($vm.notes) { $item in
NavigationLink(item.task, destination: NoteDetailView(note: $item))
}
The detail view should look like this:
struct NoteDetailView: View {
#Binding var note: ToDoItem
#EnvironmentObject var vm: NoteViewModel
var body: some View {
VStack {
TextEditor(text: $note.task)
Spacer()
}
.onDisappear {
vm.save()
}
}}
This way every time the user updates and closes the modal, the list will be saved.
I am using SwiftUi 3.0 and I am new to it . I am learning about ObservedObjects . What I am trying to do is update the count of a variable every time that I close a view . This is the entire small app . The screen starts at DataUpdateView view when I click Next View I go to DataUpdateView2 view . Once I close DataUpdateView2 and go back to the original view I want to have the
Text("Score Count \(progress.score)")
score number increase by 1 since in the second view I do a +1 every time that I close that view . Any suggestions would be great
import SwiftUI
class UserProgress: ObservableObject {
#Published var score = 0
}
struct DataUpdateView: View {
#State var nextView = false
#StateObject var progress = UserProgress()
var body: some View {
VStack {
Text("Score Count \(progress.score)")
Text("Next View")
.onTapGesture {
nextView = true
}.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2()
})
}
}
}
struct DataUpdateView_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView()
}
}
struct DataUpdateView2: View {
#ObservedObject var progress = UserProgress()
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Back")
.onTapGesture {
progress.score += 1
self.presentationMode.wrappedValue.dismiss()
}
}
}
struct DataUpdateView2_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView2()
}
}
You're probably not seeing the first view update since both views are instantiating their own UserProgress(). You need to pass the object you already created in the first view along to the second in the initializer
So In DataUpdateView:
.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2(progress: progress)
})
}
And then in DataUpdateView2:
struct DataUpdateView2: View {
#ObservedObject var progress: UserProgress
#Environment(\.presentationMode) var presentationMode
// ...
}
So now the second view is receiving the object from the first rather than creating its own.
Note: If you are not using an ObservableObject, then take a look at the second part.
In this specific situation, you don't even need a Binding variable, you can just use the .onDisappear method. .onDisappear Documentation.
import SwiftUI
class UserProgress: ObservableObject {
#Published var score = 0
}
struct DataUpdateView: View {
#State var nextView = false
#StateObject var progress = UserProgress()
var body: some View {
VStack {
Text("Score Count \(progress.score)")
Text("Next View")
.onTapGesture {
nextView = true
}.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2()
})
}
}
}
struct DataUpdateView_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView()
}
}
struct DataUpdateView2: View {
#ObservedObject var progress = UserProgress()
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("Back")
.onTapGesture{
presentationMode.wrappedValue.dismiss()
print("Dismissed!")
}
.onDisappear{
//This is called when the view disappears.
progress.score += 1
}
}
}
struct DataUpdateView2_Previews: PreviewProvider {
static var previews: some View {
DataUpdateView2()
}
}
Second Part
If you want the variable to update when the view closes, you could use the .onDisappear method and a Binding value. An example implementation of this is below:
struct ViewOne: View{
#State var number = 0
var body: some View{
VStack{
Text("Number: \(number)")
NavigationLink(destination: ViewTwo(variable: $number)){
Text("Go To View Two")
}
}
}
}
struct ViewTwo: View{
#Binding var variable: Int
var body: some View{
//Content of view 2 here
Text("View Two")
.onDisappear{
//This is called when the view disappears
variable += 1
}
}
}
In short you need to use same view model in both views. A possible and seems simplest approach in your code is to inject view model from first view to second via environment object, like
#StateObject var progress = UserProgress()
var body: some View {
VStack {
Text("Score Count \(progress.score)")
Text("Next View")
.onTapGesture {
nextView = true
}.fullScreenCover(isPresented: $nextView, content: {
DataUpdateView2()
.environmentObject(progress) // << here !!
})
and use it internally, like
struct DataUpdateView2: View {
#EnvironmentObject var progress: UserProgress // << injected automatically !!
If I create an ObservableObject with a #Published property and inject it into a SwifUI view with .environmentObject(), the view responds to changes in the ObservableObject as expected.
class CounterStore: ObservableObject {
#Published private(set) var counter = 0
func increment() {
counter += 1
}
}
struct ContentView: View {
#EnvironmentObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Tapping on "Increment" will increase the count.
However, if I don't use the EnvironmentObject and instead pass the store instance into the view, the compiler does not complain, the store method increment() is called when the button is tapped, but the count in the View does not update.
struct ContentViewWithStoreAsParameter: View {
var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter) (DOES NOT UPDATE)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Here's how I'm calling both Views:
#main
struct testApp: App {
var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // broken
}
}
}
}
Is there a way to pass an ObservableObject into a View as a parameter? (Or what magic is .environmentalObject() doing behind the scenes?)
It should be observed somehow, so next works
struct ContentViewWithStoreAsParameter: View {
#ObservedObject var store: CounterStore
//...
You can pass down your store easily as #StateObject:
#main
struct testApp: App {
#StateObject var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // also works
}
}
}
}
struct ContentViewWithStoreAsParameter: View {
#StateObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)") // now it does update
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
However, the store should normally only be available for the views that need it, why this solution would make the most sense in this context:
struct ContentView: View {
#StateObject var store = CounterStore()
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Ever since the advent of swiftUI 2.0, I have been unable to update a view according to a change done in another modally-presented view (the settings view).
I display a string on my main ContentView that derives its content from a segmented Picker value on the SettingsView.
The problem is that after the user changes the setting and discards the SettingsView, the string in ContentView is not updated. The body is not redrawn.
I am making use of #ObservableObject and #StateObject so every change to it should trigger a redraw, but I can't make it work...
I created a class that conforms to the ObservableObject protocol : AppState
I am using that class to try and pass data and -more importantly- data changes between the views in order to have my ContentView redrawn according to the the user's settings.
In order to instantiate this class, I registered a single UserDefaults in my AppDelegate file.
I also imported the Combine Framework into my project and added the import Combine line in each and every file !
I've simplified my code as much as possible, in order to illustrate the issue, so the following might seem a bit circumvolutated, but it is derived from a much more complex app, sorry about that.
Here is my ContentView code :
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var appState: AppState
#State var modalViewCaller = 0 // used to present correct modalView
#State var modalIsPresented = false // to present the modal views
var body: some View {
let stringArray = generateString() // func to generate string according to user's pref
let recapString = stringArray[0]
return ZStack {
NavigationView {
VStack {
// MARK: - texts :
VStack {
Text(recapString)
.bold()
.multilineTextAlignment(/*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
} // end of VStack
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color(UIColor.systemBlue), lineWidth: 4))
.padding()
} // END of VStack
.onAppear() {
self.modalViewCaller = 0
print("\n\n*********** Content View onAppear triggered ! ************\n")
}
.navigationBarTitle("DataFun", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 1 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.onAppear() {
self.appState.updateValues()
}
} // End of ZStack
.sheet(isPresented: $modalIsPresented) {
sheetContent(modalViewCaller: $modalViewCaller, appState: AppState())
}
.navigationViewStyle(StackNavigationViewStyle())
}
// MARK: - struct sheetContent() :
struct sheetContent: View {
#Binding var modalViewCaller: Int // Binding to the #State modalViewCaller variable from ContentView
#StateObject var appState: AppState
var body: some View {
if modalViewCaller == 1 { // The settings view is called
SettingsView(appState: AppState())
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
} else if modalViewCaller == 2 { // the "other view" is called
OtherView()
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
}
}
} // END of func sheetContent
// MARK: - generateString()
func generateString() -> [String] {
var recapString = "" // The recap string
var myArray = [""]
// We create the recap string :
if UserDefaults.standard.integer(forKey: "rules selection") == 0 { // ICAO
recapString = "User chose LEFT"
} else if UserDefaults.standard.integer(forKey: "rules selection") == 1 { // AF Rules
recapString = "User chose RIGHT"
}
myArray = [recapString]
return myArray
} // End of func generateString()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appState: AppState())
}
}
Here is my AppState code :
import Foundation
import SwiftUI
import Combine
class AppState: ObservableObject {
#Published var rulesSelection: Int = UserDefaults.standard.integer(forKey: "rules selection")
func updateValues() { // When the user changes a setting, the UserDefault is updated. Here, we align the AppState's value with what is now in the UserDefaults
self.rulesSelection = UserDefaults.standard.integer(forKey: "rules selection")
print("\nappState value (ruleSelection) updated from Appstate class func \"updateValues")
}
}
Here is my SettingsView code :
import SwiftUI
import Combine
struct SettingsView: View {
#Environment(\.presentationMode) var presentationMode // in order to dismiss the Sheet
#StateObject var appState: AppState
#State private var rulesSelection = UserDefaults.standard.integer(forKey: "rules selection") // 0 is LEFT, 1 is RIGHT
var body: some View {
NavigationView {
VStack {
Spacer()
Text("Choose a setting below")
.padding()
Picker("", selection: $rulesSelection) {
Text("LEFT").tag(0)
Text("RIGHT").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
Spacer()
}
.navigationBarItems(
leading:
Button("Done") {
self.saveDefaults() // We set the UserDefaults
self.presentationMode.wrappedValue.dismiss() // This dismisses the view
// self.modalViewCaller = 0
}
) // END of NavBarItems
} // END of NavigationBiew
} // END of body
func saveDefaults() {
UserDefaults.standard.set(rulesSelection, forKey: "rules selection")
self.appState.updateValues() // This is a func from the AppState class that will align the appState's value to the UserDefaults
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView(appState: AppState())
}
}
And a working project if anyone has the time to check this "live" :
https://github.com/Esowes/dataFun
Thanks for any pointers.
Regards.
Well... it was... in short many changes, so here is complete ContentView.swift with fixes.
Note: you need only one StateObject, and one instance set into it, and you need to have published property of observable object in view otherwise it is not refreshed, and changes in UserDefaults do not refresh view until you use AppStorage, etc.
Verified with Xcode 12.1 / iOS 14.1
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var appState: AppState
#State var modalViewCaller = 0 // used to present correct modalView
#State var modalIsPresented = false // to present the modal views
var body: some View {
return ZStack {
NavigationView {
VStack {
// MARK: - texts :
VStack {
RecapStringView(appState: appState)
} // end of VStack
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color(UIColor.systemBlue), lineWidth: 4))
.padding()
} // END of VStack
.onAppear() {
self.modalViewCaller = 0
print("\n\n*********** Content View onAppear triggered ! ************\n")
}
.navigationBarTitle("DataFun", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
self.modalViewCaller = 1 // SettingsView
self.modalIsPresented = true
}
) {
Image(systemName: "gear")
.imageScale(.large)
}
))
} // END of NavigationView
.onAppear() {
self.appState.updateValues()
}
} // End of ZStack
.sheet(isPresented: $modalIsPresented) {
sheetContent(modalViewCaller: $modalViewCaller, appState: appState)
}
.navigationViewStyle(StackNavigationViewStyle())
}
// MARK: - struct sheetContent() :
struct sheetContent: View {
#Binding var modalViewCaller: Int // Binding to the #State modalViewCaller variable from ContentView
#ObservedObject var appState: AppState
var body: some View {
if modalViewCaller == 1 { // The settings view is called
SettingsView(appState: appState)
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
} else if modalViewCaller == 2 { // the "other view" is called
OtherView()
.navigationViewStyle(StackNavigationViewStyle())
.onDisappear { self.modalViewCaller = 0 }
}
}
} // END of func sheetContent
}
struct RecapStringView: View {
#ObservedObject var appState: AppState
var body: some View {
Text("User chose " + "\(appState.rulesSelection == 0 ? "LEFT" : "RIGHT")")
.bold()
.multilineTextAlignment(.center)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(appState: AppState())
}
}
I'm currently developing an application using SwiftUI.
This app has 3 structs
①ContentView
②FirstView
③SecondView
These 3 structs do page transition in Tab View.
And this app has a common variable type of Bool using ObservableObject annotation.
I want to change to appear and disappear Text View in the FirstView and the SecondView depends on the condition of the variable, but the FirstView doesn't change a view as I expected...
How can I solve this situation?
Here are the codes:
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
FirstView()
.tabItem {
Text("First")
}.tag(1)
SecondView()
.tabItem {
Text("Second")
}.tag(2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
FirstView.swift
import SwiftUI
struct FirstView: View {
#ObservedObject var firstCheck: ViewModel = ViewModel()
var body: some View {
VStack{
if firstCheck.check == true{
Text("checked")
}
}
}
}
struct FirstView_Previews: PreviewProvider {
static var previews: some View {
FirstView()
}
}
SecondView.swift
import SwiftUI
struct SecondView: View {
#ObservedObject var secondCheck = ViewModel()
var body: some View {
VStack{
Toggle(
isOn: $secondCheck.check
){
Text("change")
}
if self.secondCheck.check == true{
Text("checked")
}
}
}
}
struct SecondView_Previews: PreviewProvider {
static var previews: some View {
SecondView()
}
}
ViewModel.swift
import Foundation
final class ViewModel: ObservableObject {
#Published var check: Bool = false
}
Xcode: Version 11.7
Keep object in one place, can be parent view
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
// #StateObject var viewModel = ViewModel() // SwiftUI 2.0
var body: some View {
TabView {
// .. other code here
}
.environmentObject(viewModel) // << inject here
}
}
and then use in both views like (for second the same)
struct FirstView: View {
#EnvironmentObject var firstCheck: ViewModel // declare only
// will be injected by type
var body: some View {
VStack{
if firstCheck.check == true{
Text("checked")
}
}
}
}