Search code examples
swiftuinavigationuikit

Memory Leak Issue when Using SwiftUI in UIKit with API Calls and Navigation


I've encountered a memory leak issue in my SwiftUI implementation within a UIKit-based app. I'm using SwiftUI views embedded in UIKit view controllers, and the problem arises when performing API calls and navigating between view controllers.

Here's a simplified version of the code structure:

protocol SampleDelegate: AnyObject {
    func push()
    func pop()
}
class SampleVC: UIViewController, SampleDelegate {
    var viewModel: SampleVM!
    override func viewDidLoad() {
        super.viewDidLoad()
        let  swiftUIView = SampleView(viewModel: viewModel)
        let hostingController = UIHostingController(rootView: swiftUIView)
        self.addChild(hostingController)
        self.view.addSubview(hostingController.view)
        hostingController.didMove(toParent: self)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        // Set up Auto Layout constraints for the SwiftUI view
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // Use of viewmodel for making api controll on viewcontroller (UiKit)
        self.bindData()
    }
    func bindData() {
        viewModel.error.bind { error in
            Toast.showError(error: error ?? NetworkError.somethingWentWrong)
        }
    }
    func push() {
        // push new viewcontroller
    }
    
    func pop() {
        // pop viewcontroller
    }
}
class SampleVM: ObservableObject {
    @Published var fetchCompletion = false
    var error: Observable<Error?> = Observable(nil)
    
    func fetchData() {
        // call api to fetch and change fetched
    }
}

struct SampleView: View {
    @ObservedObject var viewModel: SampleVM
    weak var delegate: SampleDelegate?
    init(viewModel: SampleVM, delegate: SampleDelegate? = nil) {
        self.viewModel = viewModel
        self.delegate = delegate
    }
    var body: some View {
        VStack {
            Text("\((viewModel.fetchCompletion == true) ? "fetching" : "fetched")")
            Spacer()
            HStack {
                Button {
                    delegate?.pop()
                } label: {
                    Text("pop")
                }
                Button {
                    delegate?.push()
                } label: {
                    Text("push")
                }
            }
        }
    }
}

The issue seems to be related to memory usage, especially when pushing and popping view controllers. Each time I perform these operations repeatedly, the app's memory footprint increases, indicating a potential memory leak.

I've ensured that I'm using [weak self] where necessary and have checked the SwiftUI view hierarchy for strong reference cycles, but the problem persists.

Any insights or suggestions on how to troubleshoot and resolve this memory leak issue would be greatly appreciated.

Checked for strong reference cycles in the SwiftUI view hierarchy. Implemented [weak self] to prevent retain cycles in closures. Reviewed SwiftUI and UIKit interoperability guidelines. Also I removed the hostingview from the subview while popping the controller that also didn't worked.


Solution

  • Delegates don’t work well in SwiftUI views put the delegate reference inside the view model.

    SwiftUI Views are value types and there is an underlying storage that isn’t obvious.

    SwiftUI recreates the views at will and unless the value of a property lives within SwiftUI’s it can’t maintain its integrity/reference.

    The view model works because @ObservedObject tells SwiftUI not to allocate storage because it is maintained elsewhere.