swiftuiduplicatesalert

Detect duplicate modifiers in swiftui?


In my particular case I faced a problem in swiftui which doesn't allow to add more than 1 alert to the same View. This problem is known and is already resolved for example by adding additional empty views:

https://stackoverflow.com/a/67395026/805701

But there is another problem - SwiftUI doesn't react to this situation at all (alerts just skipped). So you need manually search through all the code to find these duplicates which may occur in different files.

Is it possible somehow to indicate where these duplicates occur for example by adding a wrapper over the default .alert(...) modifier which throws fatalError when this modifier is already added to a view?


Solution

  • Suppose the modifier you want to wrap is called coolModifier. We will create a coolModifierWrapper.

    First, add a ContainerValue to record whether coolModifier has already been applied.

    @available(iOS 18, *)
    extension ContainerValues {
        @Entry var coolModifierHasBeenApplied = false
    }
    

    We use a ContainerValues instead of an environment or preference value, because we don't want this flag to escape out of a container. That is, something like this should not count as "duplicate modifiers".

    VStack {
        Color.red.coolModifierWrapper()
    }
    .coolModifierWrapper()
    

    Next, we can write a ViewModifier that reads this container value, and call fatalError if it happens to be true.

    @available(iOS 18, *)
    struct OnlyOnceEnforcer: ViewModifier {
        let keyPath: KeyPath<ContainerValues, Bool>
    
        // for error reporting
        let file: StaticString
        let line: UInt
        
        func body(content: Content) -> some View {
            ForEach(subviews: content) { subview in
                let alreadyApplied = subview.containerValues[keyPath: keyPath]
                if alreadyApplied {
                    fatalError(file: file, line: line)
                }
                subview.containerValue(\.coolModifierHasBeenApplied, true)
            }
        }
    }
    

    As an extension, try changing the type of the container value to be a struct with file and line properties, so that you can also report where the first occurrence of coolModifierWrapper is.

    Finally,

    extension View {
        func coolModifierWrapper(file: StaticString = #file, line: UInt = #line) -> some View {
            Group {
                if #available(iOS 18, *) {
                    self.modifier(OnlyOnceEnforcer(keyPath: \.coolModifierHasBeenApplied, file: file, line: line))
                } else {
                    self
                }
            }
            .coolModifier()
        }
    }
    

    This detects duplicate modifiers applied to the same view, as long as the uses are not separated by a container. For example, this will crash:

    Color.red
        .coolModifierWrapper()
        .padding()
        .coolModifierWrapper()
    

    If you do want to detect duplicate usages across containers, use an environment value.

    extension EnvironmentValues {
        @Entry var coolModifierHasBeenApplied = false
    }
    
    extension View {
        func coolModifierWrapper(file: StaticString = #file, line: UInt = #line) -> some View {
            self
                .transformEnvironment(\.coolModifierHasBeenApplied) {
                    if $0 {
                        fatalError(file: file, line: line)
                    }
                    $0 = true
                }
                .coolModifier()
        }
    }