How do I make my macOS SwiftUI form translucent?

I have a simple form in SwiftUI that I would like to make translucent (e.g. sidebar). How do I do this?


This would be done with a NSVisualEffectView, so what is the equivelent in SwiftUI? Changing the opacity and blur does not work, plus that would not be the right way.

Accepted Reply

Yep, that works pretty well actually. Here's a modified version:


struct VisualEffectBackground: NSViewRepresentable {
    private let material: NSVisualEffectView.Material
    private let blendingMode: NSVisualEffectView.BlendingMode
    private let isEmphasized: Bool
    
    fileprivate init(
        material: NSVisualEffectView.Material,
        blendingMode: NSVisualEffectView.BlendingMode,
        emphasized: Bool) {
        self.material = material
        self.blendingMode = blendingMode
        self.isEmphasized = emphasized
    }
    
    func makeNSView(context: Context) -> NSVisualEffectView {
        let view = NSVisualEffectView()
        
        // Not certain how necessary this is
        view.autoresizingMask = [.width, .height]
        
        return view
    }
    
    func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
        nsView.material = context.environment.visualEffectMaterial ?? material
        nsView.blendingMode = context.environment.visualEffectBlending ?? blendingMode
        nsView.isEmphasized = context.environment.visualEffectEmphasized ?? isEmphasized
    }
}

extension View {
    func visualEffect(
        material: NSVisualEffectView.Material,
        blendingMode: NSVisualEffectView.BlendingMode = .behindWindow,
        emphasized: Bool = false
    ) -> some View {
        background(
            VisualEffectBackground(
                material: material,
                blendingMode: blendingMode,
                emphasized: emphasized
            )
        )
    }
}

Replies

You can do this only for List views. On macOS you have a new list style available: SidebarListStyle. Using that will produce the desired effect, as I'll show in a second reply (all links get moderated, which takes a while…).


You can generate it fairly easily by taking the starter code from an iOS master-detail project and adjusting some things: remove the navigation bar-related items, and add in some explicit min/max sizing, and you'll end up with the following working sample:


import SwiftUI

private let dateFormatter: DateFormatter = {
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .medium
    dateFormatter.timeStyle = .medium
    return dateFormatter
}()

struct ContentView: View {
    @State private var dates: [Date] = [Date.distantPast, Date(), Date.distantFuture]

    var body: some View {
        NavigationView {
            MasterView(dates: self.$dates)
                .frame(minWidth: 210, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
            DetailView(selectedDate: Date())
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        }
        .navigationViewStyle(DoubleColumnNavigationViewStyle())
        .frame(minWidth: 600, maxWidth: .infinity, minHeight: 400, maxHeight: .infinity)
    }
}

struct MasterView: View {
    @Binding var dates: [Date]

    var body: some View {
        List {
            ForEach(dates, id: \.self) { date in
                NavigationLink(
                    destination: DetailView(selectedDate: date)
                ) {
                    Text("\(date, formatter: dateFormatter)")
                }
            }.onDelete { indices in
                indices.forEach { self.dates.remove(at: $0) }
            }
        }
        .listStyle(SidebarListStyle())
    }
}

struct DetailView: View {
    var selectedDate: Date?

    var body: some View {
        Group {
            if selectedDate != nil {
                Text("\(selectedDate!, formatter: dateFormatter)")
            } else {
                Text("Detail view content goes here")
            }
        }//.navigationBarTitle(Text("Detail"))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

>all links get moderated, which takes a while


Most, yes, but not all. Apple mothership, iCloud public/shared, and SO links, as an example, are usually whitelisted.


See FAQs 1 & 2 hereFor Best Results - Read the Label for how to deal with/perhaps shorten/avoid waiting for the mods.

Thanks, but that is not what I am looking for. I should have been clearer.


I knew that it is possible for lists, but what if I want my entire form (or only part of it) to have that material? Examples would be when you open spotlight on the mac, or the Safari opening page where the entire form is translucent.

I've managed to get what I wanted, but I am sure there should be a better way.


The code below has the desired effect, but it does have a slight side-effect: The size of the NSVisualEffectsView seems to be arbitary, since it is the top-most view. Ideally, I would have liked it to conform to the size of its children, but it does not have children, it just shares the Z-stack with them.

import SwiftUI

struct EffectsView: NSViewRepresentable {
  func makeNSView(context: Context) -> NSVisualEffectView {
    return NSVisualEffectView()
  }
  
  func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
    // Nothing to do.
  }
}

struct ContentView: View {
  var body: some View {
    ZStack {
      EffectsView()
      Text("Hallo World")
    }
  }
}


struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

Aha, in that case you will ultimately need to use an

NSVisualEffectView
, but that's actually quite easy to wrap, since there aren't many properties to think about, there's no real ‘content’ as such, and the values for just about everything can be set in the
updateNSView(_:context:)
method, which lends itself well to hooking its properties into the environment.


Here's what I was able to knock together just now for an environment-supporting

VisualEffectView
:


struct VisualEffectMaterialKey: EnvironmentKey {
    typealias Value = NSVisualEffectView.Material?
    static var defaultValue: Value = nil
}

struct VisualEffectBlendingKey: EnvironmentKey {
    typealias Value = NSVisualEffectView.BlendingMode?
    static var defaultValue: Value = nil
}

struct VisualEffectEmphasizedKey: EnvironmentKey {
    typealias Value = Bool?
    static var defaultValue: Bool? = nil
}

extension EnvironmentValues {
    var visualEffectMaterial: NSVisualEffectView.Material? {
        get { self[VisualEffectMaterialKey.self] }
        set { self[VisualEffectMaterialKey.self] = newValue }
    }
    
    var visualEffectBlending: NSVisualEffectView.BlendingMode? {
        get { self[VisualEffectBlendingKey.self] }
        set { self[VisualEffectBlendingKey.self] = newValue }
    }
    
    var visualEffectEmphasized: Bool? {
        get { self[VisualEffectEmphasizedKey.self] }
        set { self[VisualEffectEmphasizedKey.self] = newValue }
    }
}

struct VisualEffectView<Content: View>: NSViewRepresentable {
    private let material: NSVisualEffectView.Material
    private let blendingMode: NSVisualEffectView.BlendingMode
    private let isEmphasized: Bool
    private let content: Content
    
    fileprivate init(
        content: Content,
        material: NSVisualEffectView.Material,
        blendingMode: NSVisualEffectView.BlendingMode,
        emphasized: Bool) {
        self.content = content
        self.material = material
        self.blendingMode = blendingMode
        self.isEmphasized = emphasized
    }
    
    func makeNSView(context: Context) -> NSVisualEffectView {
        let view = NSVisualEffectView()
        let wrapper = NSHostingView(rootView: content)
        
        // Not certain how necessary this is
        view.autoresizingMask = [.width, .height]
        wrapper.autoresizingMask = [.width, .height]
        wrapper.frame = view.bounds
        
        view.addSubview(wrapper)
        return view
    }
    
    func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
        nsView.material = context.environment.visualEffectMaterial ?? material
        nsView.blendingMode = context.environment.visualEffectBlending ?? blendingMode
        nsView.isEmphasized = context.environment.visualEffectEmphasized ?? isEmphasized
    }
}

extension View {
    func visualEffect(
        material: NSVisualEffectView.Material,
        blendingMode: NSVisualEffectView.BlendingMode = .behindWindow,
        emphasized: Bool = false
    ) -> some View {
        VisualEffectView(
            content: self,
            material: material,
            blendingMode: blendingMode,
            emphasized: emphasized
        )
    }
}


You can wrap any existing view via the

visualEffect(material:blendingMode:emphasized:)
modifier, and you can set any of the three values from higher up the view chain via the properties on
EnvironmentValues
.


A plain use for an entire

ContentView
would go something like this:


NavigationView {
    MasterView(dates: self.$dates)
        .frame(minWidth: 210, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        .background(Color.clear)
    DetailView(selectedDate: Date())
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.frame(minWidth: 600, maxWidth: .infinity, minHeight: 400, maxHeight: .infinity)
.visualEffect(material: .sidebar)


Alternatively, if you'd like to see the environment at work, you can do something like this:


NavigationView {
    MasterView(dates: self.$dates)
        .frame(minWidth: 210, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        .background(Color.clear)
    DetailView(selectedDate: Date())
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        .visualEffect(material: .sidebar)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.frame(minWidth: 600, maxWidth: .infinity, minHeight: 400, maxHeight: .infinity)
.environment(\.visualEffectMaterial, .dark)


If you wanted to hook it up to

PreferenceKey
s to enable subviews to affect the background, then you'd need to add a
@State
property to hold the prefs values, then you'd need to decide which has precedence—environment from above, or preferences from below. With that decision, you can change
updateNSView(_:context:)
to use the values you've chosen.


I shall probably write this one up & post it at alanquatermain.me at some point, it was quite fun and it's a good example of how to integrate with several parts of the SwiftUI ecosystem.

You can use the .background() modifier to make it conform to the size of the main content. A background takes the size of the view to which it's attached, as does an overlay. These allow you to perform some z-ordering with an explicit definition of which layer determines the overall size/frame.

Thanks, that is exactly what I was looking for, and is much more elegant (and re-usable).


You are correct, this demonstrates nicely what can be done with SwiftUI. Thanks for the great answer.

Thinking about it, it may be better to have VisualEffectView be just a plain view with no content, and assign it as a .background() in the view modifier, rather than as a wrapper.

Why?

Yep, that works pretty well actually. Here's a modified version:


struct VisualEffectBackground: NSViewRepresentable {
    private let material: NSVisualEffectView.Material
    private let blendingMode: NSVisualEffectView.BlendingMode
    private let isEmphasized: Bool
    
    fileprivate init(
        material: NSVisualEffectView.Material,
        blendingMode: NSVisualEffectView.BlendingMode,
        emphasized: Bool) {
        self.material = material
        self.blendingMode = blendingMode
        self.isEmphasized = emphasized
    }
    
    func makeNSView(context: Context) -> NSVisualEffectView {
        let view = NSVisualEffectView()
        
        // Not certain how necessary this is
        view.autoresizingMask = [.width, .height]
        
        return view
    }
    
    func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
        nsView.material = context.environment.visualEffectMaterial ?? material
        nsView.blendingMode = context.environment.visualEffectBlending ?? blendingMode
        nsView.isEmphasized = context.environment.visualEffectEmphasized ?? isEmphasized
    }
}

extension View {
    func visualEffect(
        material: NSVisualEffectView.Material,
        blendingMode: NSVisualEffectView.BlendingMode = .behindWindow,
        emphasized: Bool = false
    ) -> some View {
        background(
            VisualEffectBackground(
                material: material,
                blendingMode: blendingMode,
                emphasized: emphasized
            )
        )
    }
}

I agree, it is better (and simpler), plus it seems to have some layout advantages.