Tio
Tio

Reputation: 1036

How can I make a background color with opacity on a Sheet view?

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

Upvotes: 26

Views: 23086

Answers (8)

Heshantha Don
Heshantha Don

Reputation: 121

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()
}

Upvotes: 3

Alexander Volkov
Alexander Volkov

Reputation: 8407

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()
    }
}

Upvotes: 0

Asperi
Asperi

Reputation: 258393

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

Upvotes: 44

Paul B
Paul B

Reputation: 5125

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"))
    }
}

Upvotes: 24

Jay Lee
Jay Lee

Reputation: 1912

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.

Upvotes: 4

mojuba
mojuba

Reputation: 12237

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) {}
}

Upvotes: 2

kwiknik
kwiknik

Reputation: 885

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))
  }
}

Upvotes: 0

Mike R
Mike R

Reputation: 658

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()
    }

Upvotes: 36

Related Questions