27

I'm currently developing an application using SwiftUI.

I'm looking for some way to make a background color with opacity on a Sheet view.

is there any way to do that?


I've tried to do that with a code below, I can change the color with opacity property, but I can't see a text(Sheet) under the sheet View...

import SwiftUI

struct ContentView: View {
    
    @State var isSheet = false
    
    var body: some View {
       
        Button(action: {self.isSheet.toggle()}) {
            Text("Sheet")
        }.sheet(isPresented: $isSheet){
            Color.yellow.opacity(0.5)
        }
    }
}

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

Xcode: Version 11.7

Swift: Swift 5

8 Answers 8

44

You cannot do it with standard sheet official API (because every hosting controller view by default is opaque), so you can either create custom sheet view (with any features you needed) or use run-time workaround to find that view and set its background to clear. Like below (only for demo)

demo

struct DemoView: View {

    @State var isSheet = false

    var body: some View {

        Button(action: {self.isSheet.toggle()}) {
            Text("Sheet")
        }.sheet(isPresented: $isSheet){
            Color.yellow.opacity(0.5)
                .background(BackgroundClearView())
        }
    }
}

struct BackgroundClearView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        DispatchQueue.main.async {
            view.superview?.superview?.backgroundColor = .clear
        }
        return view
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

backup

Sign up to request clarification or add additional context in comments.

4 Comments

If we add ".ignoresSafeArea()" after ".background(BackgroundClearView())" the view will be exactly what we want.
I found that, at least on iOS 16, there would sometimes be a very brief delay in the background colour being set so it would flicker for me when the view appeared. Instead of using an async dispatch, swapping the UIView for this works better: class SuperviewRecolourView: UIView { override func layoutSubviews() { guard let parentView = superview?.superview else { print("ERROR: Failed to get parent view to make it clear") return } parentView.backgroundColor = .clear } } (beautiful comment layout :D)
This doesn't work when the sheet is a NavigationView or NavigationStack. Does anyone know how to fix this?
Since iOS 16.4 there is a .presentationBackground modifier. ` .sheet(isPresented: $isPresented) { Text("Sheet") .presentationBackground(.yellow.opacity(0.5)) } `
36

Using the AWESOME answer from @Asperi that I have been trying to find all day, I have built a simple view modifier that can now be applied inside a .sheet or .fullScreenCover modal view and provides a transparent background. You can then set the frame modifier for the content as needed to fit the screen without the user having to know the modal is not custom sized.

import SwiftUI

struct ClearBackgroundView: UIViewRepresentable {
    func makeUIView(context: Context) -> some UIView {
        let view = UIView()
        DispatchQueue.main.async {
            view.superview?.superview?.backgroundColor = .clear
        }
        return view
    }
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

struct ClearBackgroundViewModifier: ViewModifier {
    
    func body(content: Content) -> some View {
        content
            .background(ClearBackgroundView())
    }
}

extension View {
    func clearModalBackground()->some View {
        self.modifier(ClearBackgroundViewModifier())
    }
}

Usage:

.sheet(isPresented: $isPresented) {
            ContentToDisplay()
            .frame(width: 300, height: 400)
            .clearModalBackground()
    }

2 Comments

I found that, at least on iOS 16, there would sometimes be a very brief delay in the background colour being set so it would flicker for me when the view appeared. Instead of using an async dispatch, swapping the UIView for this works better: class SuperviewRecolourView: UIView { override func layoutSubviews() { guard let parentView = superview?.superview else { print("ERROR: Failed to get parent view to make it clear") return } parentView.backgroundColor = .clear } } (beautiful comment layout :D)
I have not tested this on an iOS 16 device, so I appreciate the head's up!
28

From iOS 16.4 it should be possible to use .presentationBackground(_ style: S) with colours and blurs (material types). If you want to make a transparent background just for one sheet:

struct ContentView: View {    
    @State private var isPresented = false
    var body: some View {
        Button(action: {
            isPresented.toggle()
        }, label: {
            Label("Sheet", systemImage: "list.bullet.rectangle.portrait")
        })
        .sheet(isPresented: $isPresented) {
            Text("Detail")
                .presentationBackground(.clear)
        }
    }
}

Or you can use .presentationBackground() to inherit background style from presenter.

struct ContentView: View {
    @State private var isPresented = false
    var body: some View {
        Button(action: {
            isPresented.toggle()
        }, label: {
            Label("Sheet", systemImage: "list.bullet.rectangle.portrait")
        })
        .sheet(isPresented: $isPresented) {
            Text("Detail")
                .presentationBackground()
        }
        .backgroundStyle(Color("myThemeBackground"))
    }
}

1 Comment

this should be the accepted answer, given that it's the only one that doesn't depend on meddling with the internal view hierarchy
4

While the provided solutions do work, they will keep the background transparent for other sheets as well when they are swapped out.

So it is needed to restore the background color in dismantleUIView like this:

struct TransparentBackground: UIViewRepresentable {
    @MainActor
    private static var backgroundColor: UIColor?

    func makeUIView(context: Context) -> UIView {
        let view = UIView()
        Task {
            Self.backgroundColor = view.superview?.superview?.backgroundColor
            view.superview?.superview?.backgroundColor = .clear
        }
        return view
    }

    static func dismantleUIView(_ uiView: UIView, coordinator: ()) {
        uiView.superview?.superview?.backgroundColor = Self.backgroundColor
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

Note that I used the new concurrency features in Swift.

5 Comments

This is great and works perfectly in iOS 16 as well!
Works well on the Simulator but in a real iPhone (14 Pro iOS 16.4.1) it is shown as white background though. I couldn't figure out why.
@juanjovn facing same issue, did you got the solution.
@GauravPandey Maybe you can use the solution provided in this answer: stackoverflow.com/a/75567958/5252984 conditionally for iOS >= 16.4. Does it work?
@GauravPandey As Jay Lee suggested, I did exactly that. For iOS 16.4 and above I used the new function from SwiftUI's API and the TransparentBackground solution from this post for previous iOS versions.
3

Preview

iOS 16.4 onward

import SwiftUI

struct ContentView: View {
    @State private var isSheet: Bool = false
    
    var body: some View {
        Button(action: {self.isSheet.toggle()}) {
            Text("Sheet")
        }.sheet(isPresented: $isSheet){
            ZStack {
                Text("Hello, World!")
            }
            .presentationBackground(.yellow.opacity(0.5))
        }
    }
}

#Preview {
    ContentView()
}

Comments

2

For those who think relying on the order of messages in DispatchQueue is not a good idea (it's not really), here's a solution that relies on overriding didMoveToSuperview().

This is still not 100% future proof as we are modifying superview's superview and there's no guarantee it will be available at the moment our own view is added to the hierarchy, but I think this is still better than the DispatchQueue solution, since views are usually added from top to bottom, i.e. superview's superview will likely be available when didMoveToSuperview() is called.

struct DemoView: View {

    @State var isSheet = false

    var body: some View {

        Button(action: {self.isSheet.toggle()}) {
            Text("Sheet")
        }.sheet(isPresented: $isSheet){
            Color.yellow.opacity(0.5)
                .background(BackgroundClearView())
        }
    }
}

struct BackgroundClearView: UIViewRepresentable {

    private class View: UIView {
        override func didMoveToSuperview() {
            super.didMoveToSuperview()
            superview?.superview?.backgroundColor = .clear
        }
    }

    func makeUIView(context: Context) -> UIView {
        return View()
    }

    func updateUIView(_ uiView: UIView, context: Context) {}
}

Comments

0

Put this in a Modal.swift:

import SwiftUI

// View modifiers for modals: .sheet, .fullScreenCover

struct ModalColorView: UIViewRepresentable {
    
    let color: UIColor
    
    func makeUIView(context: Context) -> some UIView {
        let view = UIView()
        DispatchQueue.main.async {
            view.superview?.superview?.backgroundColor = color
        }
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {}
}

struct ModalColorViewModifier: ViewModifier {
    
    let color: UIColor
    
    func body(content: Content) -> some View {
        content
            .background(ModalColorView(color: color))
    }
}

extension View {
    /// Set transparent or custom color for a modal background (.screen, .fullScreenCover)
    func modalColor(_ color: UIColor = .clear) -> some View {
        self.modifier(ModalColorViewModifier(color: color))
    }
}

Use like so:

.sheet(isPresented: $show) {
  YourModalView(isPresented: $show)
    // Zero args default is transparent (.clear)
    //.modalColor()
    // ..or specify a UIColor of choice.
    .modalColor(UIColor(white: 0.2, alpha: 0.3))
  }
}

3 Comments

I found that, at least on iOS 16, there would sometimes be a very brief delay in the background colour being set so it would flicker for me when the view appeared. Instead of using an async dispatch, swapping the UIView for this works better: class SuperviewRecolourView: UIView { override func layoutSubviews() { guard let parentView = superview?.superview else { print("ERROR: Failed to get parent view to make it clear") return } parentView.backgroundColor = .clear } } (beautiful comment layout :D)
@CMash haha yes gorgeous syntax highlighting in the comments section isn't it! Thank you for this insight, it's helpful. I posted this pre-iOS 16 and am now looking to use something similar for iOS 16, so your comment is both well timed and appreciated.
@CMash I'm seeing the flicker you mentioned. I tried implenting exactly what you said, but the background does not seem to change. Would you be able to post your solution as an answer and include the ui view representable piece? - Edit: I had the background in the wrong place. This works flawlessly. Great work!!!
0

Assperi don't want his answer to be edited, so posting it separately:

struct ContentView: View {

    @SwiftUI.State var showSheet = false
    @SwiftUI.State var appear: Bool = false
    
    var body: some View {

        Button(action: {self. showSheet.toggle()}) {
            Text("Sheet")
        }.sheet(isPresented: $showSheet){
            Color.blue.opacity(0.5)
            .background(ClearBackgroundView(appear: $appear))
            .onAppear {DispatchQueue.main.async {appear = true}}
        }
    }
}

struct ClearBackgroundView: UIViewRepresentable {

    @Binding var appear: Bool

    class ClearView: UIView {
        override func didMoveToSuperview() {
            super.didMoveToSuperview()
            clearBackgroundColor()
        }

        func clearBackgroundColor() {
            var view: UIView? = self.superview?.superview
            while let subview = view {
                subview.backgroundColor = .clear
                view = subview.superview
            }
        }
    }

    func makeUIView(context: Context) -> ClearView {
        return ClearView()
    }

    func updateUIView(_ uiView: ClearView, context: Context) {
        uiView.clearBackgroundColor()
    }
}

Comments

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.