0

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.

9
  • This can be helpful stackoverflow.com/a/63018486/12299030. Commented Aug 16, 2022 at 7:39
  • Unfortunately that didn't yield any good information to debug the issue, the only output was assembly. Commented Aug 16, 2022 at 14:22
  • 1
    Provided code not testable - needed minimal reproducible example to investigate. Commented Aug 16, 2022 at 14:31
  • Replacing code above with all necessary code to repro, standby... Commented Aug 16, 2022 at 16:21
  • Not reproducible here. Xcode 13.4 / iOS 15.5 Commented Aug 16, 2022 at 16:38

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.