How to toggle a PKCanvasView's editability - swift

I am using PencilKit to allow the user to draw to a custom canvas and among the various functionalities, I want to allow the user to decide if the canvas is editable or not. If it's editable then the drawing tool is visible and the canvas accepts both finger and pencil gestures, otherwise it accepts only gestures from the pencil and finger gestures are used for other things.
This is the canvas view that I have created:
struct CanvasView {
#Binding var canvasView: PKCanvasView
#State var toolPicker: PKToolPicker = PKToolPicker()
#Binding var editable: Bool
}
// MARK: - UIViewRepresentable
extension CanvasView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
canvasView.tool = PKInkingTool(.pen, color: .gray, width: 10)
canvasView.backgroundColor = .clear
canvasView.isOpaque = false
canvasView.drawingPolicy = editable ? .anyInput : .pencilOnly
canvasView.isUserInteractionEnabled = editable
toolPicker.setVisible(editable, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
return canvasView
}
func updateUIView(_ uiView: UIViewType, context: Context) {}
}
And this is my content view:
struct ContentView: View {
#State private var canvasView = PKCanvasView()
#State private var isDrawing = true
...
var body: some View {
NavigationView {
ZStack {
...
CanvasView(canvasView: $canvasView, editable: self.$isDrawing)
.allowsHitTesting(false)
.navigationBarTitle(Text("Drawing Demo"), displayMode: .inline)
.navigationBarItems(leading: HStack{}, trailing: HStack {
...
Button(action: toggleIsDrawing) {
Image(systemName: "pencil.tip.crop.circle")
}
})
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
...
// MARK: - Actions
private extension ContentView {
...
func toggleIsDrawing() {
self.isDrawing = !self.isDrawing
}
So in short: in order to toggle my canvas view's editability I have a #State var (isDrawing), which is toggled every time that the user clicks on a pencil icon in the navigation bar. This is bound with a #Binding property with the canvas view's editable property. But the problem is that nothing triggers my canvas view's makeUIView(context:) method again, so in practive nothing changes. I was expecting a change on my #State var (isDrawing) to cause the whole view hierarchy to be redrawn and thus every property recalculated, and also the makeUIView() method recalled but apparently that's not the case.How would you solve this problem?

That scenario is what the updateUIView function is for -- it allows you to modify your view when your #State or properties change.
Also, you probably want to remove .allowsHitTesting(false), as it seems to render the PencilKit view unusable.
extension CanvasView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
canvasView.tool = PKInkingTool(.pen, color: .gray, width: 10)
canvasView.backgroundColor = .clear
canvasView.isOpaque = false
canvasView.becomeFirstResponder()
return canvasView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
canvasView.drawingPolicy = editable ? .anyInput : .pencilOnly
canvasView.isUserInteractionEnabled = editable
toolPicker.setVisible(editable, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
}
}
CanvasView(canvasView: $canvasView, editable: self.$isDrawing)
//.allowsHitTesting(false)

Related

How to clean PKCanvas from strokes when I leave the screen?

I have a basic app, a Main View with links to Drawing View. When I go to the Drawing View №1 and paint something with Apple Pencil, then go back to Main View, then go back to Drawing View №1 - I still see my painting. It stayed in the memory.
Question: What is a proper way to free the memory from PKCanvas and it's strokes when leaving the view?
I know I can "remove" the drawing by assigning canvasView.drawing = PKDrawing() a new blank drawing. But does it really solve the problem of keeping junk in the memory?
Here is my bare-minimum code:
import SwiftUI
import PencilKit
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
NavigationLink(destination: DrawingView()) {
Text("Go to Drawing #1")
}
.padding()
NavigationLink(destination: DrawingView()) {
Text("Go to Drawing #2")
}
.padding()
}
}
}
}
struct DrawingView: View {
#State private var canvasView = PKCanvasView()
var body: some View {
PKCanvasViewRepresentable(canvasView: $canvasView)
.frame(width: 500, height: 500)
.border(Color.blue)
}
}
struct PKCanvasViewRepresentable: UIViewRepresentable {
#Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.tool = PKInkingTool(.pen, color: .black, width: 26)
canvasView.becomeFirstResponder()
canvasView.delegate = context.coordinator
return canvasView
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PKCanvasViewDelegate, UIPencilInteractionDelegate {
var canvas: PKCanvasViewRepresentable
init(_ canvas: PKCanvasViewRepresentable) {
self.canvas = canvas
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
print("canvasViewDrawingDidChange()")
}
}
}

Undo/redo text input w/ SwiftUI TextEditor

Admittedly this is a broad question, but is it possible to undo or redo text input (via iOS's UndoManager?) when using a SwiftUI TextEditor control? I've looked everywhere and was unable to find any resource focusing on this workflow combination (SwiftUI + TextEditor + UndoManager). I'm wondering given the relative immaturity of TextEditor that either this isn't possible at all, or requires some plumbing work to facilitate. Any guidance will be greatly appreciated!
Admittedly, this is a bit of a hack and non very SwiftUI-y, but it does work. Basically declare a binding in your UITextView:UIViewRepresentable to an UndoManager. Your UIViewRepresentable will set that binding to the UndoManager provided by the UITextView. Then your parent View has access to the internal UndoManager. Here's some sample code. Redo works as well although not shown here.
struct MyTextView: UIViewRepresentable {
/// The underlying UITextView. This is a binding so that a parent view can access it. You do not assign this value. It is created automatically.
#Binding var undoManager: UndoManager?
func makeUIView(context: Context) -> UITextView {
let uiTextView = UITextView()
// Expose the UndoManager to the caller. This is performed asynchronously to avoid modifying the view at an inappropriate time.
DispatchQueue.main.async {
undoManager = uiTextView.undoManager
}
return uiTextView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
struct ContentView: View {
/// The underlying UndoManager. Even though it looks like we are creating one here, ultimately, MyTextView will set it to its internal UndoManager.
#State private var undoManager: UndoManager? = UndoManager()
var body: some View {
NavigationView {
MyTextView(undoManager: $undoManager)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
undoManager?.undo()
} label: {
Image(systemName: "arrow.uturn.left.circle")
}
Button {
undoManager?.redo()
} label: {
Image(systemName: "arrow.uturn.right.circle")
}
}
}
}
}
}
In respect to using UIViewRepresentable as a TextView or TextField…. this approach works for undo, but not for redo it seems.
The redo button condition undoManager.canRedo seems to change appropriately. However, it doesn’t return any undone text into either the textfield or TextView
I’m now wondering is this a bug or something I’m missing in the logic?
import SwiftUI
import PlaygroundSupport
class Model: ObservableObject {
#Published var active = ""
func registerUndo(_ newValue: String, in undoManager: UndoManager?) {
let oldValue = active
undoManager?.registerUndo(withTarget: self) { target in
target.active = oldValue
}
active = newValue
}
}
struct TextView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
struct ContentView: View {
#ObservedObject private var model = Model()
#Environment(\.undoManager) var undoManager
#State var text: String = ""
var body: some View {
ZStack (alignment: .bottomTrailing) {
// Testing TextView for undo & redo functionality
TextView(text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) }))
HStack{
// Testing TextField for undo & redo functionality
TextField("Enter Text...", text: Binding<String>(
get: { self.model.active },
set: { self.model.registerUndo($0, in: self.undoManager) })).padding()
Button("Undo") {
withAnimation {
self.undoManager?.undo()
}
}.disabled(!(undoManager?.canUndo ?? false)).padding()
Button("Redo") {
withAnimation {
self.undoManager?.redo()
}
}.disabled(!(undoManager?.canRedo ?? false)).padding()
}.background(Color(UIColor.init(displayP3Red: 0.1, green: 0.3, blue: 0.3, alpha: 0.3)))
}.frame(width: 400, height: 400, alignment: .center).border(Color.black)
}
}
PlaygroundPage.current.setLiveView(ContentView())

Change background color of TextEditor in SwiftUI

TextEditor seems to have a default white background. So the following is not working and it displayed as white instead of defined red:
var body: some View {
TextEditor(text: .constant("Placeholder"))
.background(Color.red)
}
Is it possible to change the color to a custom one?
iOS 16
You should hide the default background to see your desired one:
TextEditor(text: .constant("Placeholder"))
.scrollContentBackground(.hidden) // <- Hide it
.background(.red) // To see this
iOS 15 and below
TextEditor is backed by UITextView. So you need to get rid of the UITextView's backgroundColor first and then you can set any View to the background.
struct ContentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
List {
TextEditor(text: .constant("Placeholder"))
.background(.red)
}
}
}
Demo
You can find my simple trick for growing TextEditor here in this answer
Pure SwiftUI solution on iOS and macOS
colorMultiply is your friend.
struct ContentView: View {
#State private var editingText: String = ""
var body: some View {
TextEditor(text: $editingText)
.frame(width: 400, height: 100, alignment: .center)
.cornerRadius(3.0)
.colorMultiply(.gray)
}
}
Update iOS 16 / SwiftUI 4.0
You need to use .scrollContentBackground(.hidden) instead of UITextView.appearance().backgroundColor = .clear
https://twitter.com/StuFFmc/status/1556561422431174656
Warning: This is an iOS 16 only so you'll probably need some if #available and potentially two different TextEditor component.
extension View {
/// Layers the given views behind this ``TextEditor``.
func textEditorBackground<V>(#ViewBuilder _ content: () -> V) -> some View where V : View {
self
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
.background(content())
}
}
Custom Background color with SwiftUI on macOS
On macOS, unfortunately, you have to fallback to AppKit and wrap NSTextView.
You need to declare a view that conforms to NSViewRepresentable
This should give you pretty much the same behaviour as SwiftUI's TextEditor-View and since the wrapped NSTextView does not draw its background, you can use the .background-ViewModifier to change the background
struct CustomizableTextEditor: View {
#Binding var text: String
var body: some View {
GeometryReader { geometry in
NSScrollableTextViewRepresentable(text: $text, size: geometry.size)
}
}
}
struct NSScrollableTextViewRepresentable: NSViewRepresentable {
typealias Representable = Self
// Hook this binding up with the parent View
#Binding var text: String
var size: CGSize
// Get the UndoManager
#Environment(\.undoManager) var undoManger
// create an NSTextView
func makeNSView(context: Context) -> NSScrollView {
// create NSTextView inside NSScrollView
let scrollView = NSTextView.scrollableTextView()
let nsTextView = scrollView.documentView as! NSTextView
// use SwiftUI Coordinator as the delegate
nsTextView.delegate = context.coordinator
// set drawsBackground to false (=> clear Background)
// use .background-modifier later with SwiftUI-View
nsTextView.drawsBackground = false
// allow undo/redo
nsTextView.allowsUndo = true
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
// get wrapped nsTextView
guard let nsTextView = scrollView.documentView as? NSTextView else {
return
}
// fill entire given size
nsTextView.minSize = size
// set NSTextView string from SwiftUI-Binding
nsTextView.string = text
}
// Create Coordinator for this View
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// Declare nested Coordinator class which conforms to NSTextViewDelegate
class Coordinator: NSObject, NSTextViewDelegate {
var parent: Representable // store reference to parent
init(_ textEditor: Representable) {
self.parent = textEditor
}
// delegate method to retrieve changed text
func textDidChange(_ notification: Notification) {
// check that Notification.name is of expected notification
// cast Notification.object as NSTextView
guard notification.name == NSText.didChangeNotification,
let nsTextView = notification.object as? NSTextView else {
return
}
// set SwiftUI-Binding
parent.text = nsTextView.string
}
// Pass SwiftUI UndoManager to NSTextView
func undoManager(for view: NSTextView) -> UndoManager? {
parent.undoManger
}
// feel free to implement more delegate methods...
}
}
Usage
ContenView: View {
#State private var text: String
var body: some View {
VStack {
Text("Enter your text here:")
CustomizableTextEditor(text: $text)
.background(Color.red)
}
.frame(minWidth: 600, minHeight: 400)
}
}
Edit:
Pass reference to SwiftUI UndoManager so that default undo/redo actions are available.
Wrap NSTextView in NSScrollView so that it is scrollable. Set minSize property of NSTextView to enclosing SwiftUIView-Size so that it fills the entire allowed space.
Caveat: Only first line of this custom TextEditor is clickable to enable text editing.
This works for me on macOS
extension NSTextView {
open override var frame: CGRect {
didSet {
backgroundColor = .clear
drawsBackground = true
}
}
}
struct ContentView: View {
#State var text = ""
var body: some View {
TextEditor(text: $text)
.background(Color.red)
}
Reference this answer
To achieve this visual design here is the code I used.
iOS 16
TextField(
"free_form",
text: $comment,
prompt: Text("Type your feedback..."),
axis: .vertical
)
.lineSpacing(10.0)
.lineLimit(10...)
.padding(16)
.background(Color.themeSeashell)
.cornerRadius(16)
iOS 15
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 16)
.foregroundColor(.gray)
TextEditor(text: $comment)
.padding()
.focused($isFocused)
if !isFocused {
Text("Type your feedback...")
.padding()
}
}
.frame(height: 132)
.onAppear() {
UITextView.appearance().backgroundColor = .clear
}
You can use Mojtaba's answer (the approved answer). It works in most cases. However, if you run into this error:
"Return from initializer without initializing all stored properties"
when trying to use the init{ ... } method, try adding UITextView.appearance().backgroundColor = .clear to .onAppear{ ... } instead.
Example:
var body: some View {
VStack(alignment: .leading) {
...
}
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
Using the Introspect library, you can use .introspectTextView for changing the background color.
TextEditor(text: .constant("Placeholder"))
.cornerRadius(8)
.frame(height: 100)
.introspectTextView { textView in
textView.backgroundColor = UIColor(Color.red)
}
Result
import SwiftUI
struct AddCommentView: View {
init() {
UITextView.appearance().backgroundColor = .clear
}
var body: some View {
VStack {
if #available(iOS 16.0, *) {
TextEditor(text: $viewModel.commentText)
.scrollContentBackground(.hidden)
} else {
TextEditor(text: $viewModel.commentText)
}
}
.background(Color.blue)
.frame(height: UIScreen.main.bounds.width / 2)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.red, lineWidth: 1)
)
}
}
It appears the UITextView.appearance().backgroundColor = .clear trick in IOS 16,
only works for the first time you open the view and the effect disappear when the second time it loads.
So we need to provide both ways in the app. Answer from StuFF mc works.
var body: some View {
if #available(iOS 16.0, *) {
mainView.scrollContentBackground(.hidden)
} else {
mainView.onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
}
// rename body to mainView
var mainView: some View {
TextEditor(text: $notes).background(Color.red)
}

How can I know if a SwiftUI Button is enabled/disabled?

There is no isEnabled property for a SwiftUI button. How can i tell if it is enabled?
In regular UIKit, i would simply do
if button.isEnabeld == true {
} else {
}
but there is no SwiftUI equivalent.
Inside a view, if you wish to react to the state set by .disabled(true), you can use:
#Environment(\.isEnabled) var isEnabled
Since the environment can be used from within a View or a ViewModifier, this can be used to change layout properties of a view based on the state set from outside.
Unfortunately, ButtonStyle cannot directly use #Environment, but you can use a ViewModifier to inject environment values into a ButtonStyle in order to use the value from within a ButtonStyle:
// First create a button style that gets the isEnabled value injected
struct MyButtonStyle: ButtonStyle {
private let isEnabled: Bool
init(isEnabled: Bool = true) {
self.isEnabled = isEnabled
}
func makeBody(configuration: Configuration) -> some View {
return configuration
.label
.background(isEnabled ? .green : .gray)
.foregroundColor(isEnabled ? .black : .white)
}
}
// Then make a ViewModifier to inject the state
struct MyButtonModifier: ViewModifier {
#Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
return content.buttonStyle(MyButtonStyle(isEnabled: isEnabled))
}
}
// Then create a convenience function to apply the modifier
extension Button {
func styled() -> some View {
ModifiedContent(content: self, modifier: MyButtonModifier())
}
}
// Finally, try out the button and watch it respond to it's state
struct ContentView: View {
var body: some View {
Button("Test", {}).styled().disabled(true)
}
}
You can use this method to inject other things into a ButtonStyle, like size category and theme.
I use it with a custom style enum that contains all the flavours of button styles found in our design system.
From outside a view you should know if you used .disabled(true) modifier.
From inside a view you can use #Environment(\.isEnabled) to get that information:
struct MyButton: View {
let action: () -> Void
#Environment(\.isEnabled) private var isEnabled
var body: some View {
Button(action: action) {
Text("Click")
}
.foregroundColor(isEnabled ? .green : .gray)
}
}
struct MyButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
MyButton(action: {})
MyButton(action: {}).disabled(true)
}
}
}
The whole idea of SwiftUI, is to avoid duplication of the source of truth. You need to think differently, and consider where the source of truth is. This is where you need to go to find out the button's state. Not from the button itself.
In "Data Flow Through SwiftUI", at minute 30:50, they explain that every piece of data has a single source of truth. If your button gets its state from some #Binding, #State, #EnvironmentObject, etc, your if statement should get that information from the same place too, not from the button.
Short answer: Just use inside struct:
#Environment(\.isEnabled) private var isEnabled
Button style with:
animation on hover change
animation on disable/enable change
can be applied on any button in native way of swiftUI
you need manually set size of buttons outside of the button
usage:
#State var isDisabled = false
///.......
Button("Styled button") { isDisabled.toggle() }
.buttonStyle(ButtStyle.BigButton()) // magic inside
.frame(width: 200, height: 50)
.disabled(isDisabled)
Button("switch isDisabled") { isDisabled.toggle() }
source code:
public struct ButtStyle { }
// Added style to easy stylyng in native way for SwiftUI
#available(macOS 11.0, *)
public extension ButtStyle {
struct BigButton: ButtonStyle {
init() {
}
public func makeBody(configuration: Configuration) -> some View {
BigButtonStyleView(configuration: configuration)
}
}
}
#available(macOS 11.0, *)
struct BigButtonStyleView : View {
let configuration: ButtonStyle.Configuration
#Environment(\.isEnabled) var isEnabled // here we getting "disabled"
#State var hover : Bool = false
var body: some View {
// added animations
MainFrameMod()
.animation(.easeInOut(duration: 0.2), value: hover)
.animation(.easeInOut(duration: 0.2), value: isEnabled)
}
// added opacity on move hover change
// and disabled status
#ViewBuilder
func MainFrameMod() -> some View {
if isEnabled {
MainFrame()
.opacity(hover ? 1 : 0.8)
.onHover{ hover = $0 }
} else {
MainFrame()
.opacity(0.5)
}
}
// Main interface of button
func MainFrame() -> some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color(hex: 0xD8D8D8))
configuration.label
.foregroundColor(.black)
.font(.custom("SF Pro", size: 18))
}
}
}
As mentioned by other developers, the main idea of SwiftUI is that the UI remains synced with the data. You can perform this in many different ways. This includes #State, #EnvironmentObject, #Binding etc.
struct ContentView: View {
#State private var isEnabled: Bool = false
var body: some View {
VStack {
Button("Press me!") {
}.disabled(isEnabled)
}
.padding()
}
}

Is there a method to blur a background in SwiftUI?

I'm looking to blur a view's background but don't want to have to break out into UIKit to accomplish it (eg. a UIVisualEffectView) I'm digging through docs and got nowhere, seemingly there is no way to live-clip a background and apply effects to it. Am I wrong or looking into it the wrong way?
1. The Native SwiftUI way:
Just add .blur() modifier on anything you need to be blurry like:
Image("BG")
.blur(radius: 20)
Note the top and bottom of the view
Note that you can Group multiple views and blur them together.
2. The Visual Effect View:
You can bring the prefect UIVisualEffectView from the UIKit:
VisualEffectView(effect: UIBlurEffect(style: .dark))
With this tiny struct:
struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView { UIVisualEffectView() }
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) { uiView.effect = effect }
}
3. iOS 15: Materials
You can use iOS predefined materials with one line code:
.background(.ultraThinMaterial)
I haven't found a way to achieve that in SwiftUI yet, but you can use UIKit stuff via UIViewRepresentable protocol.
struct BlurView: UIViewRepresentable {
let style: UIBlurEffect.Style
func makeUIView(context: UIViewRepresentableContext<BlurView>) -> UIView {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
let blurEffect = UIBlurEffect(style: style)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(blurView, at: 0)
NSLayoutConstraint.activate([
blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
])
return view
}
func updateUIView(_ uiView: UIView,
context: UIViewRepresentableContext<BlurView>) {
}
}
Demo:
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
List(1...100) { item in
Rectangle().foregroundColor(Color.pink)
}
.navigationBarTitle(Text("A List"))
ZStack {
BlurView(style: .light)
.frame(width: 300, height: 300)
Text("Hey there, I'm on top of the blur")
}
}
}
}
}
I used ZStack to put views on top of it.
ZStack {
// List
ZStack {
// Blurred View
// Text
}
}
And ends up looking like this:
The simplest way is here by Richard Mullinix:
struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
Then just use it somewhere in your code like background:
//...
MyView()
.background(Blur(style: .systemUltraThinMaterial))
As mentioned by #mojtaba, it's very peculiar to see white shade at top of image when you set resizable() along with blur().
As simple trick is to raise the Image padding to -ve.
var body: some View {
return
ZStack {
Image("background_2").resizable()
.edgesIgnoringSafeArea(.all)
.blur(radius: 5)
.scaledToFill()
.padding(-20) //Trick: To escape from white patch #top & #bottom
}
}
Result:
New in iOS 15 , SwiftUI has a brilliantly simple equivalent to UIVisualEffectView, that combines ZStack, the background() modifier, and a range of built-in materials.
ZStack {
Image("niceLook")
Text("Click me")
.padding()
.background(.thinMaterial)
}
You can adjust the “thickness” of your material – how much of the background content shines through – by using one of several material types. From thinnest to thickest, they are:
.ultraThinMaterial
.thinMaterial
.regularMaterial
.thickMaterial
.ultraThickMaterial
I have found an interesting hack to solve this problem. We can use UIVisualEffectView to make live "snapshot" of its background. But this "snapshot" will have an applied effect of UIVisualEffectView. We can avoid applying this effect using UIViewPropertyAnimator.
I didn't find any side effect of this hack. You can find my solution here: my GitHub Gist
Code
/// A View which content reflects all behind it
struct BackdropView: UIViewRepresentable {
func makeUIView(context: Context) -> UIVisualEffectView {
let view = UIVisualEffectView()
let blur = UIBlurEffect(style: .extraLight)
let animator = UIViewPropertyAnimator()
animator.addAnimations { view.effect = blur }
animator.fractionComplete = 0
animator.stopAnimation(true)
animator.finishAnimation(at: .start)
return view
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) { }
}
/// A transparent View that blurs its background
struct BackdropBlurView: View {
let radius: CGFloat
#ViewBuilder
var body: some View {
BackdropView().blur(radius: radius)
}
}
Usage
ZStack(alignment: .leading) {
Image(systemName: "globe")
.resizable()
.frame(width: 200, height: 200)
.foregroundColor(.accentColor)
.padding()
BackdropBlurView(radius: 6)
.frame(width: 120)
}
#State private var amount: CGFLOAT = 0.0
var body: some View {
VStack{
Image("Car").resizable().blur(radius: amount, opaque: true)
}
}
Using "Opaque: true" with blur function will eliminate white noise
There is a very useful but unfortunately private (thanks Apple) class CABackdropLayer
It draws a copy of the layers below, I found it useful when using blend mode or filters, It can also be used for blur effect
Code
open class UIBackdropView: UIView {
open override class var layerClass: AnyClass {
NSClassFromString("CABackdropLayer") ?? CALayer.self
}
}
public struct Backdrop: UIViewRepresentable {
public init() {}
public func makeUIView(context: Context) -> UIBackdropView {
UIBackdropView()
}
public func updateUIView(_ uiView: UIBackdropView, context: Context) {}
}
public struct Blur: View {
public var radius: CGFloat
public var opaque: Bool
public init(radius: CGFloat = 3.0, opaque: Bool = false) {
self.radius = radius
self.opaque = opaque
}
public var body: some View {
Backdrop()
.blur(radius: radius, opaque: opaque)
}
}
Usage
struct Example: View {
var body: some View {
ZStack {
YourBelowView()
YourTopView()
.background(Blur())
.background(Color.someColor.opacity(0.4))
}
}
}
Source
Button("Test") {}
.background(Rectangle().fill(Color.red).blur(radius: 20))