How to resolve "AttributeGraph: cycle" warnings in the following code?

401 Views Asked by At

For context, the goal of the code below is to intercept a particular type of link inside a webview and handle navigation across a tabview natively (to a separate webview displaying the desired page) rather let the webview navigate itself. However, when I attempt to change the currentSelection to the desired index, I get a long list of "===AttributeGraph: cycle...===" messages. Below is the entirety of the code needed to repro this behavior:

import SwiftUI
import WebKit

@main
struct AttributeGraphCycleProofApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(theController: Controller())
        }
    }
}

struct ContentView: View {
    @StateObject var theController: Controller
    
    @State var currentSelection = 0
    
    private let selectedBackgroundColor = Color.green
    
    var body: some View {
        VStack(spacing: 12.0) {
            HStack(spacing: .zero) {
                ForEach(Array(0..<theController.viewModel.menuEntries.count), id: \.self) { i in
                    let currentMenuEntry = theController.viewModel.menuEntries[i]
                    Text(currentMenuEntry.title)
                        .padding()
                        .background(i == currentSelection ? selectedBackgroundColor : .black)
                        .foregroundColor(i == currentSelection ? .black : .gray)
                }
            }
            TabView(selection: $currentSelection) {
                let menuEntries = theController.viewModel.menuEntries
                ForEach(Array(0..<menuEntries.count), id: \.self) { i in
                    let currentMenuEntry = theController.viewModel.menuEntries[i]
                    WrappedWebView(slug: currentMenuEntry.slug, url: currentMenuEntry.url) { destinationIndex in
                        // cycle warnings are logged when this line is executed
                        currentSelection = destinationIndex
                    }
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
        }
        .padding()
        .background(.black)
        .onAppear { theController.start() }
    }
}

class Controller: ObservableObject {
    @Published var viewModel: ViewModel = ViewModel(menuEntries: [])
    
    func start() {
        // Represents network request to create dynamic menu entries
        DispatchQueue.main.asyncAfter(deadline: .now().advanced(by: DispatchTimeInterval.seconds(1)), execute: { [weak self] in
            self?.viewModel = ViewModel.create()
        })
    }
}

struct ViewModel {
    let menuEntries: [MenuEntry]
    
    static func create() -> Self {
        return Self(menuEntries: [
            MenuEntry(title: "Domain", slug: "domain", url: "https://www.example.com/"),
            MenuEntry(title: "Iana", slug: "iana", url: "https://www.iana.org/domains/reserved"),
        ])
    }
}

struct MenuEntry {
    let title: String
    let slug: String
    let url: String
}

struct WrappedWebView: UIViewRepresentable {
    var slug: String
    var url: String
    var navigateToSlug: ((Int) -> Void)? = nil
    
    func makeCoordinator() -> WrappedWebView.Coordinator { Coordinator(self) }
    
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        webView.navigationDelegate = context.coordinator
        
        return webView
    }
 
    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.isOpaque = false
        if let wrappedUrl = URL(string: url), webView.url != wrappedUrl {
            let request = URLRequest(url: wrappedUrl)
            webView.load(request)
        }
    }

    class Coordinator: NSObject, WKNavigationDelegate {
        let parent: WrappedWebView

        init(_ parent: WrappedWebView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy {
            let url = navigationAction.request.url
            if url?.absoluteString == parent.url {
                return .allow
            }
            
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                self.parent.navigateToSlug?(1)
            }

            return .cancel
        }
    }
}

All instrumentation and memory graph debugging are failing me as they don't describe the moment the leak occurs, all I know is that the critical line causing the leaks is the assignment of navigationIndex to currentSelection.

0

There are 0 best solutions below