skip to Main Content

Watch the following video which illustrates a problem that I’m having where the built-in blur effect on the iOS navigation bar seems to be working inconsistently.

Video illustrating the problem

Here is my code. Copy it into XCode and try it out.

import SwiftUI

struct ContentView: View {
    @State var showingList = true
        
    var body: some View {
        NavigationView {
            VStack {
                
                if showingList {
                    List {
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                    }
                    .listStyle(.plain)
                    .border(.red)
                    .transition(.openInLeft)
                } else {
                    List {
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                        Text("Hello, world!")
                            .foregroundColor(.red)
                            .font(.system(size: 70))
                    }
                    .listStyle(.plain)
                    .border(.red)
                    .transition(.openInRight)
                }
            }
            .animation(.linear, value: showingList)
            .toolbar(content: {
                ToolbarItem(placement: .navigationBarLeading, content: {
                    Button("toggle") {
                        showingList.toggle()
                    }

                })
            })
            .background(.black)
            .navigationTitle("test")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}


struct FoldModifier: ViewModifier {
    let amount: Double
    let anchor: UnitPoint

    func body(content: Content) -> some View {
        if anchor == .leading || anchor == .trailing {
            content.rotation3DEffect(.degrees(amount), axis: (x: 0, y: 1, z: 0), anchor: anchor)
        } else if anchor == .top || anchor == .bottom {
            content.rotation3DEffect(.degrees(amount), axis: (x: 1, y: 0, z: 0), anchor: anchor)
        }
    }
}

extension AnyTransition {
    static var openInLeft: AnyTransition {
        .modifier(
            active: FoldModifier(amount: 90, anchor: .leading),
            identity: FoldModifier(amount: 0, anchor: .leading)
        )
    }

    static var openInRight: AnyTransition {
        .modifier(
            active: FoldModifier(amount: -90, anchor: .trailing),
            identity: FoldModifier(amount: 0, anchor: .trailing)
        )
    }
}

It seems almost like it is a bug in SwiftUI, but hopefully some guru out there can explain to me how to make it work correctly.

3

Answers


  1. Chosen as BEST ANSWER

    This is a hack at best, but so far it is the best idea I've been able to come up with. I insert a blank row at the top of the list and then in .onAppear for the list, I programmatically scroll to the second item in the list (which is the first item with real content). The programmatic scroll causes something to get triggered that lets iOS know to do the blur effect on the NavigationBar for all of the lists.

    I'm still hoping someone out there can come up with a better solution.

    import SwiftUI
    
    struct ContentView: View {
        @State var showingList = true
        
        struct item : Identifiable {
            var id: Int
            var text: AnyView
        }
        
        var items = [
            item(id: 1, text: AnyView(Text(""))),
            item(id: 2, text: AnyView(Text("First row")
                .foregroundColor(.red)
                .font(.system(size: 70))))
        ]
            
        var body: some View {
            NavigationView {
                VStack {
                    
                    if showingList {
                        ScrollViewReader {proxy in
                            List {
                                ForEach(items) { item in
                                    item.text
                                        .listRowSeparator(.hidden)
                                }
                                
                                Text("Hello, world!")
                                    .foregroundColor(.red)
                                    .font(.system(size: 70))
                                Text("Hello, world!")
                                    .foregroundColor(.red)
                                    .font(.system(size: 70))
                                Text("Hello, world!")
                                    .foregroundColor(.red)
                                    .font(.system(size: 70))
                                Text("Hello, world!")
                                    .foregroundColor(.red)
                                    .font(.system(size: 70))
                                Text("Hello, world!")
                                    .foregroundColor(.red)
                                    .font(.system(size: 70))
                            }
                            .listStyle(.plain)
                            .border(.red)
                            .onAppear() {
                                proxy.scrollTo(2, anchor: .top)
                            }
                        }
                        .transition(.openInLeft)
                    } else {
                        List {
                            Text("Hello, world!")
                                .foregroundColor(.red)
                                .font(.system(size: 70))
                            Text("Hello, world!")
                                .foregroundColor(.red)
                                .font(.system(size: 70))
                            Text("Hello, world!")
                                .foregroundColor(.red)
                                .font(.system(size: 70))
                            Text("Hello, world!")
                                .foregroundColor(.red)
                                .font(.system(size: 70))
                            Text("Hello, world!")
                                .foregroundColor(.red)
                                .font(.system(size: 70))
                        }
                        .listStyle(.plain)
                        .border(.red)
                        .transition(.openInRight)
                    }
                }
                .animation(.linear, value: showingList)
                .toolbar(content: {
                    ToolbarItem(placement: .navigationBarLeading, content: {
                        Button("toggle") {
                            showingList.toggle()
                        }
    
                    })
                })
                .background(.black)
                .navigationTitle("test")
                .navigationBarTitleDisplayMode(.inline)
            }
        }
    }
    

  2. Running the code above I got the following message:

    ignoring singular matrix: ProjectionTransform(m11: 0.0, m12: 0.5, m13: 0.0013882461823229988, m21: 0.0, m22: 1.0, m23: 0.0, m31: 0.0, m32: 0.0, m33: 1.0)

    Upon further inspection apple classifies "ignoring singular matrix" as a warning.

    The transformation matrix is singular when the scale factor is zero, as it will be at the end of your animation.

    From what I see the fix is pretty simple instead of your identity being zero use the following code:

    extension AnyTransition {
        static var openInLeft: AnyTransition {
            .modifier(
                active: FoldModifier(amount: 90, anchor: .leading),
                identity: FoldModifier(amount: 0.01, anchor: .leading)
            )
        }
    
        static var openInRight: AnyTransition {
            .modifier(
                active: FoldModifier(amount: -90, anchor: .trailing),
                identity: FoldModifier(amount: 0.01, anchor: .trailing)
            )
        }
    }
    

    Note while this solution makes the flying text go away, it also makes the blur go away. I think a possible reason as to why the blur is not activating, upon looking at the view hierarchy I was able to uncover:

    <_UIVisualEffectBackdropView: 0x13e90ef20; frame = (0 0; 393 103); autoresize = W+H; userInteractionEnabled = NO; layer = <UICABackdropLayer: 0x6000034beb50>>
    Printing description of $17:
    <UIVisualEffectView: 0x13e90e6e0; frame = (0 0; 393 103); alpha = 0; layer = <CALayer: 0x600003af3660>> effect=none
    

    The UIVisualEffectView effect is set to none. My best guess is UIKitNavigationBar and the UINavigationTransitionView have no way to communicate with the current configuration of anchors. I tested the transition with different configuration of anchors in the example below I flipped them. To see how a glimpse as to how SwiftUI and UIKit communicate click here!

    extension AnyTransition {
        static var openInLeft: AnyTransition {
            .modifier(
                active: FoldModifier(amount: 90, anchor: .trailing),
                identity: FoldModifier(amount: 0.01, anchor: .trailing)
            )
        }
    
        static var openInRight: AnyTransition {
            .modifier(
                active: FoldModifier(amount: -90, anchor: .leading),
                identity: FoldModifier(amount: 0.01, anchor: .leading)
            )
        }
    }
    

    And was able to get one of the transitions the blur effect. But of course this also changes the animation. The best way to fix this would be change how the anchors are aligned and or not use transitions to achieve this animation.
    Anyway sorry I was not able to fix this question "completely" I have ran out of time, I hope you will be able to find a proper solution best of luck.

    Login or Signup to reply.
  3. To force the Blur background to always be there (even when the list is scrolled to the top), I can revert to UIKit and add the following init() to my view.

    init() {
        let appearance = UINavigationBarAppearance()
        appearance.configureWithDefaultBackground()
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search