I previously asked a question about how to push a view with data received from an asynchronous callback. The method I ended up with has turned out to cause a Memory Leak.
I'm trying to structure my app with MVVM for SwiftUI, so a ViewModel should publish another ViewModel, that a View then knows how to present on screen. Once the presented view is dismissed from screen, I expect the corresponding ViewModel to be deinitialised. However, that's never the case with the proposed solution.
After UserView is dismissed, I end up having an instance of UserViewModel leaked in memory. UserViewModel never prints "Deinit UserViewModel", at least not until next time a view is pushed on pushUser.
struct ParentView: View {
#ObservedObject var vm: ParentViewModel
var presentationBinding: Binding<Bool> {
.init(get: { vm.pushUser != nil },
set: { isPresented in
if !isPresented {
vm.pushUser = nil
}
}
)
}
var body: some View {
VStack {
Button("Get user") {
vm.getUser()
}
Button("Read user") {
print(vm.pushUser ?? "No userVm")
}
if let userVm = vm.pushUser {
NavigationLink(
destination: UserView(vm: userVm),
isActive: presentationBinding,
label: EmptyView.init
)
}
}
}
}
class ParentViewModel: ObservableObject {
#Published var pushUser: UserViewModel? = nil
var cancellable: AnyCancellable?
private func fetchUser() -> AnyPublisher<User, Never> {
Just(User.init(id: "1", name: "wiingaard"))
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
func getUser() {
cancellable = api.getUser().sink { [weak self] user in
self?.pushUser = UserViewModel(user: user)
}
}
}
struct User: Identifiable {
let id: String
let name: String
}
class UserViewModel: ObservableObject, Identifiable {
deinit { print("Deinit UserViewModel") }
#Published var user: User
init(user: User) { self.user = user }
}
struct UserView: View {
#ObservedObject var vm: UserViewModel
var body: some View {
Text(vm.user.name)
}
}
After dismissing the UserView and I inspect the Debug Memory Graph, I see an instance of UserViewModel still allocated.
The top reference (view.content.vm) has kind: (AnyViewStorage in $7fff57ab1a78)<ModifiedContent<UserView, (RelationshipModifier in $7fff57ad2760)<String>>> and hierarchy: SwiftUI.(AnyViewStorage in $7fff57ab1a78)<SwiftUI.ModifiedContent<MyApp.UserView, SwiftUI.(RelationshipModifier in $7fff57ad2760)<Swift.String>>> AnyViewStorageBase _TtCs12_SwiftObject
What's causing this memory leak, and how can I remove it?
I can see that ViewModel is deinit() if you use #State in your View, and listen to your #Publisher in your ViewModel.
Example:
#State var showTest = false
NavigationLink(destination: SessionView(sessionViewModel: outgoingCallViewModel.sessionViewModel),
isActive: $showTest,
label: { })
.isDetailLink(false)
)
.onReceive(viewModel.$showView, perform: { show in
if show {
showTest = true
}
})
if you use viewModel.$show in your NavigationLink as isActive, viewModel never deinit().
Please refer to this post (https://stackoverflow.com/a/62511130/11529487), it solved the issue for the memory leak bug in SwiftUI by adding on the NavigationView:
.navigationViewStyle(StackNavigationViewStyle())
However it breaks the animation, there is a hacky solution to this issue. The animation problem occurs because of the optional chaining in "if let".
When setting "nil" as the destination in the NavigationLink, it essentially does not go anywhere, even if the "presentationBinding" is true.
I invite you to try this piece of code as it fixed the animtaion problem that resulted from the StackNavigationViewStyle (and no memory leaks):
Although not as pretty as the optional chaining, it does the job.
Related
The point of this app is to use core data to permanently add types of fruit to a list. I have two views: ContentView and SecondScreen. SecondScreen is a pop-up sheet. When I input a fruit and press 'save' in SecondScreen, I want to immediately update the list in ContentView to reflect the type of fruit that has just been added to core data as well as the other fruits which have previously been added to core data. My problem is that when I hit the 'save' button in SecondScreen, the new fruit is not immediately added to the list in ContentView. Instead, I have to restart the app to see the new fruit in the list.
Here is the class for my core data:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my ContentView struct:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { entity in
Text(entity.name ?? "NO NAME")
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh)
})
}
}
}
Here is my SecondScreen struct:
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#StateObject var vm = CoreDataViewModel()
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
To try and solve this issue, I've created a #State boolean variable called 'refresh' in ContentView and bound it with the 'refresh' variable in SecondScreen. This variable is toggled when the user hits the 'save' button on SecondScreen, and I was thinking that maybe this would change the #State variable in ContentView and trigger ContentView to reload, but it doesn't work.
In your second screen , change
#StateObject var vm = CoreDataViewModel()
to
#ObservedObject var vm: CoreDataViewModel
then provide for the instances that compiler will ask for
hope it helps
You need to use #FetchRequest instead of #StateObject and NSFetchRequest. #FetchRequest will call body to update the Views when the fetch result changes.
Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.
I'm experiencing this really weird issue/bug with SwiftUI. In the setupSubscription method, I'm creating a subscription to subject and inserting it into the cancellables Set. And yet, when I print the count of cancellables, I get zero. How can the set be empty if I just inserted an element into it?
This is presumably why the handleValue method is not called when I tap on the button. Here's the full output from the console:
init
begin setupSubscription
setupSubscription subject sink: receive subscription: (CurrentValueSubject)
setupSubscription subject sink: request unlimited
setupSubscription subject sink: receive value: (initial value)
handleValue: 'initial value'
setupSubscription: cancellables.count: 0
setupSubscription subject sink: receive cancel
sent value: 'value 38'
cancellables.count: 0
sent value: 'value 73'
cancellables.count: 0
sent value: 'value 30'
cancellables.count: 0
What am I doing wrong? why Is my subscription to subject getting cancelled? Why is handleValue not getting called when I tap the button?
import SwiftUI
import Combine
struct Test: View {
#State private var cancellables: Set<AnyCancellable> = []
let subject = CurrentValueSubject<String, Never>("initial value")
init() {
print("init")
self.setupSubscription()
}
var body: some View {
VStack {
Button(action: {
let newValue = "value \(Int.random(in: 0...100))"
self.subject.send(newValue)
print("sent value: '\(newValue)'")
print("cancellables.count:", cancellables.count)
}, label: {
Text("Tap Me")
})
}
}
func setupSubscription() {
print("begin setupSubscription")
let cancellable = self.subject
.print("setupSubscription subject sink")
.sink(receiveValue: handleValue(_:))
self.cancellables.insert(cancellable)
print("setupSubscription: cancellables.count:", cancellables.count)
// prints "setupSubscription: cancellables.count: 0"
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
}
}
a few things you are doing wrong here.
Never try to store things in swiftUI structs. They get invalidated and reloaded every time your view changes. This is likely why your subscription is getting canceled.
For something like this, you should use an ObservableObject or StateObject with published properties. When ObservableObjects or StateObjects change. The views that contain them reload just like with #State or #Binding:
// ObservedObjects have an implied objectWillChange publisher that causes swiftUI views to reload any time a published property changes. In essence they act like State or Binding variables.
class ViewModel: ObservableObject {
// Published properties ARE combine publishers
#Published var subject: String = "initial value"
}
then in your view:
#ObservedObject var viewModel: ViewModel = ViewModel()
If you do need to use a publisher. Or if you need to do something when an observable object property changes. You don't need to use .sink. That is mostly used for UIKit apps using combine. SwiftUI has an .onReceive viewmodifier that does the same thing.
Here are my above suggestions put into practice:
struct Test: View {
class ViewModel: ObservedObject {
#Published var subject: String = "initial value"
}
#ObservedObject var viewModel: Self.ViewModel
var body: some View {
VStack {
Text("\(viewModel.subject)")
Button {
viewModel.subject = "value \(Int.random(in: 0...100))"
} label: {
Text("Tap Me")
}
}
.onReceive(viewModel.$subject) { [self] newValue in
handleValue(newValue)
}
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
}
}
You just incorrectly use state - it is view related and it becomes available (prepared back-store) only when view is rendered (ie. in body context). In init there is not yet state back-storage, so your cancellable just gone.
Here is possible working approach (however I'd recommend to move everything subject related into separated view model)
Tested with Xcode 12 / iOS 14
struct Test: View {
private var cancellable: AnyCancellable?
private let subject = CurrentValueSubject<String, Never>("initial value")
init() {
cancellable = self.subject
.print("setupSubscription subject sink")
.sink(receiveValue: handleValue(_:))
}
var body: some View {
VStack {
Button(action: {
let newValue = "value \(Int.random(in: 0...100))"
self.subject.send(newValue)
print("sent value: '\(newValue)'")
}, label: {
Text("Tap Me")
})
}
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
}
}
I have a subview which is a List containing a ForEach on a Published array variable from an ObservedObject. The subview is displayed within a TabView and NavigationView on the main ContentView.
However, the List does not render the updated array when it is updated from a network request using the onAppear function.
Placing the network call in another callback, for example, an on-tap gesture on another view element causes the view to update correctly.
I have tried using willSet and calling objectWillChange manually, to no success.
ServerListView.swift:
struct ServerListView: View {
#ObservedObject var viewModel: ViewModel
#State private var searchText = ""
var body: some View {
List {
TextField("Search", text: self.$searchText)
ForEach(self.viewModel.servers) { server in
Text(server.name)
}
}
.navigationBarTitle(Text("Your Servers"))
.onAppear(perform: fetchServers)
}
func fetchServers() {
self.viewModel.getServers()
}
}
ViewModel.swift:
class ViewModel: ObservableObject {
private let service: ApiService
#Published var servers: [Server] = []
init(service: ApiService) {
self.service = service
}
func getServers() {
self.service.getServers { [weak self] result in
switch result {
case .failure(let error):
print(error)
case .success(let serverListModel):
DispatchQueue.main.async {
self?.servers = serverListModel.servers
}
}
}
}
}
If this view is not placed within a parent view, then the functionality works as intended.
Edit for more information:
The ApiService class just handles a URLSession dataTask and the model conforms to Decodable to decode from JSON.
The parent View creates ServerListView as follows in the example, where service.authenticated is a Bool which is set true or false depending on whether the client is authenticated with the API. I have tested without any other code, just by creating the ServerListView inside the body and nothing else. The same outcome is seen.
struct ContentView: View {
var service: ApiService
#ViewBuilder
var body: some View {
if service.authenticated {
TabView {
NavigationView {
ServerListView(ServerListViewModel(service: service))
}
.tabItem {
Image(systemName: "desktopcomputer")
Text("Servers")
}
}
}
else {
LoginView(service: service)
}
}
}
After further testing, placing the API Call in the struct initializer in ServerViewList.swift means the code works as intended. But, this does mean the API call is placed twice. The struct appears to be constructed twice, but the onAppear is only called once.
I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set) and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
Is there a way to get a one-way binding in SwiftUI - i.e. some of the UI will update based on a value it cannot change.
or
How should I have architected this code! It's very likely that I'm writing code in a style which doesn't work with SwiftUI, but all the tutorials I can see online neatly ignore things like loading / error states.
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List doesn't need to modify api.state (as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what #State is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject (through Environment).
The final missing piece is to mark any of your properties that should trigger an update with #Published, which is a convenience to fire objectWillChange signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
#Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
#Published var alertState: AlertState? = nil
#Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
#ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
#ViewBuilder content: #escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}