20

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

Tio
  • 944
  • 3
  • 15
  • 35

6 Answers6

40

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) {}
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • If we add ".ignoresSafeArea()" after ".background(BackgroundClearView())" the view will be exactly what we want. – Kerem Cesme Aug 25 '21 at 06:29
  • 5
    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 Oct 02 '22 at 14:13
  • This doesn't work when the sheet is a NavigationView or NavigationStack. Does anyone know how to fix this? – Stitch Nov 23 '22 at 16:20
35

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()
    }
Mike R
  • 568
  • 4
  • 11
  • 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 Oct 02 '22 at 14:11
  • I have not tested this on an iOS 16 device, so I appreciate the head's up! – Mike R Oct 05 '22 at 13:05
11

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"))
    }
}
Paul B
  • 3,989
  • 33
  • 46
3

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.

Jay Lee
  • 1,684
  • 1
  • 15
  • 27
  • This is great and works perfectly in iOS 16 as well! – Joshua Rapp Feb 15 '23 at 01:12
  • 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 Apr 14 '23 at 18:58
  • @juanjovn facing same issue, did you got the solution. – Gaurav Pandey Aug 01 '23 at 10:56
  • 1
    @GauravPandey Maybe you can use the solution provided in this answer: https://stackoverflow.com/a/75567958/5252984 conditionally for iOS >= 16.4. Does it work? – Jay Lee Aug 01 '23 at 19:26
  • 1
    @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. – juanjovn Aug 02 '23 at 15:22
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) {}
}

mojuba
  • 11,842
  • 9
  • 51
  • 72
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))
  }
}
kwiknik
  • 570
  • 3
  • 7
  • 1
    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 Oct 02 '22 at 14:13
  • @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. – kwiknik Feb 23 '23 at 21:00