Home > Enterprise >  SwiftUI Class won't DeInit
SwiftUI Class won't DeInit

Time:06-02

I've got a class class MapSearch that I instantiate when I need to auto-complete address results. It works perfectly but it never deinitializes and I can't figure out why.

Easily test by creating the files below. Use the back button after navigating to the test page and watch the console messages. You will see that the view model initializes and deinitializes as it should, but you'll only see MapSearch initialize.

HomeView.swift

import SwiftUI

struct HomeView: View {

    var body: some View {
        NavigationView {
            NavigationLink(destination: TestView(viewModel: TestViewModel()) {
                Text("TestView")
            }
        }
    }
}

TestView.swift

import SwiftUI

struct TestView: View {
    @StateObject var viewModel: ViewModel

    var body: some View {
        Text("Hello World")
    }
}

TestViewModel.swift

import Foundation

extension TestView {
    @MainActor
    class ViewModel: ObservableObject {
        @Published var mapSearch: MapSearch()
        init() {
            print("Test View Model Initialized")
        }
        deinit {
            print("Test View Model Deinitialized")
        }
    }
}

MapSearch.swift

import Combine
import CoreLocation
import Foundation
import MapKit

/// Uses MapKit and CoreLocation to auto-complete an address
class MapSearch: NSObject, ObservableObject {
    @Published var countryName: String = "United States"
    @Published var locationResults: [MKLocalSearchCompletion] = []
    @Published var searchTerm = ""

    private var cancellables: Set<AnyCancellable> = []

    private var searchCompleter = MKLocalSearchCompleter()
    private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)?

    override init() {
        super.init()
        searchCompleter.delegate = self
        searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])

        $searchTerm
            .debounce(for: .seconds(0.2), scheduler: RunLoop.main)
            .removeDuplicates()
            .flatMap({ (currentSearchTerm) in
                self.searchTermToResults(searchTerm: currentSearchTerm, countryName: self.countryName)
            })
            .sink(receiveCompletion: { (_) in
            }, receiveValue: { (results) in

                // Show country specific results
                self.locationResults = results.filter { $0.subtitle.contains(self.countryName) }
            })
            .store(in: &cancellables)

        print("MapSearch Initialized")
    }

    deinit {
        print("MapSearch Deinitialized")
    }

    func searchTermToResults(searchTerm: String, countryName: String) -> Future<[MKLocalSearchCompletion], Error> {
        Future { promise in
            self.searchCompleter.queryFragment = searchTerm
            self.currentPromise = promise
        }
    }
}

extension MapSearch: MKLocalSearchCompleterDelegate {
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
            currentPromise?(.success(completer.results))
        }

    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        // deal with the error here, it will finish the Combine publisher stream
         currentPromise?(.failure(error))
    }
}

CodePudding user response:

The MapSearch class needed to be adjusted to add [weak self] in the combine calls. Now it deinits properly.

Here's the code for reference:

import Combine
import CoreLocation
import Foundation
import MapKit

/// Uses MapKit and CoreLocation to auto-complete an address
class MapSearch: NSObject, ObservableObject {
    @Published var countryName: String = "United States"
    @Published var locationResults: [MKLocalSearchCompletion] = []
    @Published var searchTerm = ""

    private var cancellables: Set<AnyCancellable> = []

    private var searchCompleter = MKLocalSearchCompleter()
    private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)?

    override init() {
        super.init()
        searchCompleter.delegate = self
        searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])

        $searchTerm
            .debounce(for: .seconds(0.2), scheduler: RunLoop.main)
            .removeDuplicates()
            .flatMap({ [weak self] (currentSearchTerm) in
                (self?.searchTermToResults(searchTerm: currentSearchTerm, countryName: self?.countryName ?? "")) ??
                Future { [weak self] promise in
                    self?.searchCompleter.queryFragment = self?.searchTerm ?? ""
                    self?.currentPromise = promise
                }
            })
            .sink(receiveCompletion: { (_) in
            }, receiveValue: { [weak self] (results) in

                // Show country specific results
                self?.locationResults = results.filter { $0.subtitle.contains(self?.countryName ?? "") }
            })
            .store(in: &cancellables)

        print("MapSearch Initialized")
    }

    deinit {
        print("MapSearch Deinitialized")
    }

    func searchTermToResults(searchTerm: String, countryName: String) -> Future<[MKLocalSearchCompletion], Error> {
        Future { [weak self] promise in
            self?.searchCompleter.queryFragment = searchTerm
            self?.currentPromise = promise
        }
    }
}

extension MapSearch: MKLocalSearchCompleterDelegate {
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
            currentPromise?(.success(completer.results))
        }

    func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
        // deal with the error here, it will finish the Combine publisher stream
         currentPromise?(.failure(error))
    }
}
  • Related