okta / okta-mobile-swift

okta-mobile-swift
https://github.com/okta/okta-mobile-swift
Apache License 2.0
44 stars 18 forks source link

Use of SwiftUI's `@Environment(\.dismiss)` causes infinite loop if okta-mobile-swift is listed as a dependency #163

Open NiallBegley opened 9 months ago

NiallBegley commented 9 months ago

Describe the bug?

I've run into a very particular bug where a SwiftUI iOS app will freeze and the CPU and memory usage will rapidly climb in a way consistent with something infinitely looping. It happens under these circumstances:

When the user presses the control associated with the NavigationLink the app will immediately freeze and resource consumption will rapidly climb. If you remove any of the above variables (including the use of okta-mobile-swift as a dependency) the freeze will disappear.

What is expected to happen?

When the NavigationLink is pressed the view should be pushed to the destination view as usual

What is the actual behavior?

The app immediately freezes and resource consumption climbs. You can put a breakpoint on the NavigationLink and see it keeps getting called over and over again.

Reproduction Steps?

  1. Create a new iOS SwiftUI project from scratch.
  2. Add the okta-mobile-swift dependency to the project (I used the latest version)
  3. If not already configured to do so, configure the app to use RootView as the root view and add the following code:
import SwiftUI

struct SystemCell: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Unnamed System")
                .font(.headline)
                .foregroundColor(.primary)
                .lineLimit(1)
        }
    }
}

struct RootView: View {
    var body: some View {
        ZStack {
            NavigationStack {
                List {
                    NavigationLink(destination: SystemView()
                    ) {
                        SystemCell()
                    }
                }
            }
        }
        .padding()
    }
}

struct SystemView: View {
    @Environment(\.dismiss) private var dismiss
    var body: some View {
        VStack {
            NavigationView {
                NavigationLink {
                    HuhView(object: SimpleViewModel())
                } label: {
                    Image(systemName: "magnifyingglass")
                }
            }
        }
    }
}

struct HuhView: View {
    let object: SimpleViewModel
    var body: some View {
        Text("Huh")
    }
}

class SimpleViewModel { }
  1. Run the app, select the cell that appears, then click the magnifying glass on the next screen.

Additional Information?

No response

SDK Version(s)

Verified on 1.3.0 and 1.5.0

Build Information

No response

iphone201988 commented 3 weeks ago

Same issue I'm facing with Scanner Module

import SwiftUI import VisionKit

struct ScannerView: View {

// MARK: ObservedObject Variables -
@ObservedObject var scannerViewModel = ScannerViewModel()
@ObservedObject var startcontractViewModel: StartContractViewModel

// MARK: State Variables -
@State var isNavigateToStartContractView: Bool = false
@State var scanResults: String = ""
@State private var isDeviceCapacity = false
@State var appearVia: Views = .purchaserAgreementView
@State var isHideScanningText = false
@State var cornerStrokeColor: Color = Color.blue
@State var headingContent: String = "To continue, please scan your legitimate document, such as your licence."
@State var navigateVia: NavigateToPurchaserInfoViewVia = .scanner

// MARK: Binding Variables -
@Binding var isDismiss: Bool
@Binding var navigateToScannerView: Bool

// MARK: View Body -
var body: some View {

    /// `ZStack` - A view that overlays its subviews, aligning them in both axes.
    ZStack {
        VStack {
            if isDeviceCapacity {
                DataScannerView(
                    startScanning: $isDeviceCapacity,
                    scanResult: $scanResults,
                    isNavigateToStartContractView: $isNavigateToStartContractView,
                    appearVia: $appearVia,
                    isDismiss: $isDismiss,
                    isHideScanningText: $isHideScanningText,
                    cornerStrokeColor: $cornerStrokeColor,
                    headingContent: $headingContent
                )
            } else {
                Text("Please provide access to the camera in settings")
            }
        }
        .task {
            if scannerViewModel.dataScannerAccessStatus != .scannerAvailable {
                await scannerViewModel.requestDataScannerAccessStatus()
            }
        }
        .onAppear {
            isDeviceCapacity = (DataScannerViewController.isSupported && DataScannerViewController.isAvailable)
            scannedDetails = DriverLicense()
        }

        .navigationDestination(isPresented: $isNavigateToStartContractView) {
            StartContractView(
                startcontractViewModel: startcontractViewModel,
                selectedOfferCreationOptions: .scanID,
                backToPurchaserAgreementView: $navigateToScannerView,
                navigateToStartContractViewVia: $navigateVia
            )
        }
    }
    .ignoresSafeArea()
}

}

import SwiftUI import UIKit import VisionKit import AVFoundation

struct DataScannerView: UIViewControllerRepresentable {

// MARK: ObservedObject Variables -
@ObservedObject var scannerViewModel = ScannerViewModel()

// MARK: Binding Variables -
@Binding var startScanning: Bool
@Binding var scanResult: String
@Binding var isNavigateToStartContractView: Bool
@Binding var appearVia: Views
@Binding var isDismiss: Bool
@Binding var isHideScanningText: Bool
@Binding var cornerStrokeColor: Color
@Binding var headingContent: String

// MARK: Shared Methods -
func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

func makeUIViewController(context: Context) -> DataScannerViewController {
    let viewController = DataScannerViewController(
        recognizedDataTypes: [scannerViewModel.recognizedDataType],
        qualityLevel: .accurate,
        recognizesMultipleItems: false,
        isHighFrameRateTrackingEnabled: false,
        isGuidanceEnabled: true,
        isHighlightingEnabled: true
    )
    viewController.delegate = context.coordinator
    return viewController
}

func updateUIViewController(_ viewController: DataScannerViewController, context: Context) {
    if startScanning {
        try? viewController.startScanning()
    } else {
        viewController.stopScanning()
    }
}

// Coordinator to manage interactions with the DataScannerViewController
class Coordinator: NSObject, DataScannerViewControllerDelegate {

    // MARK: Shared Variables -
    var parent: DataScannerView
    var isPDF: Bool = false

    // MARK: Init() -
    init(_ parent: DataScannerView) {
        self.parent = parent
    }

    // MARK: Delegate and DataSource Methods
    func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
        performActionAccordingToRecognizedItem(item)
    }

    func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
        UINotificationFeedbackGenerator().notificationOccurred(.success)
        _ = performActionOnRecognizedItems(addedItems: addedItems)
        // addedItems.forEach { item in performActionAccordingToRecognizedItem(item) }
    }

    func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
    }

    func dataScanner(_ dataScanner: DataScannerViewController, becameUnavailableWithError error: DataScannerViewController.ScanningUnavailable) {
    }

    // MARK: Perform Action According To Recognized Item
    fileprivate func performActionAccordingToRecognizedItem(_ item: RecognizedItem) {
        switch item {
            // Define a closure to handle captured text
        case .text(let text):
            parent.scanResult = text.transcript
            // Define a closure to handle captured barcodes
        case .barcode(let barcode):
            // Validate symbology
            let symbology = barcode.observation.symbology
            switch symbology {
            case .pdf417, .microPDF417:
                parent.scanResult =  barcode.payloadStringValue ?? "Unknown barcode"
                parent.isHideScanningText = true
                if parent.appearVia == .purchaserAgreementView {
                    parent.isNavigateToStartContractView = true
                } else {
                    parent.isDismiss = false
                }
                break

            default:
                parent.isHideScanningText = true
            }
        default: break
        }
    }

    fileprivate func performActionOnRecognizedItems(addedItems: [RecognizedItem]) -> Bool {
        var isValidated = false
        addedItems.forEach { item in
            switch item {
            case .barcode(let barcode):
                if barcode.observation.symbology == .pdf417 || barcode.observation.symbology == .microPDF417 {
                    isValidated = true
                    parent.cornerStrokeColor = Color.green
                    parent.headingContent = "Your document is scanning, please wait..."
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                        self.performActionAccordingToRecognizedItem(item)
                    }
                    break
                } else {
                    isValidated = false
                    parent.cornerStrokeColor = Color.red
                    parent.headingContent = "Scanning..."
                }
            default: isValidated = false
                parent.cornerStrokeColor = Color.red
                parent.headingContent = "Scanning..."
            }
        }
        return isValidated
    }
}

}