Reputation: 5193
I've created a UIViewRepresentable
for UIVisualEffectView
in order to make certain components vibrant. This works, however it seems to shrink controls vertically at times or just at random alter their bounds at runtime. I can't seem to make it work reliably. I need this to work with any SwiftUI content or even other UIViewRepresentable
used in place of content. Wrapping the UIVisualEffectView
inside of a UIView
and using auto layout seems to help, but other controls (such as a custom UILabel
wrapped inside of a UIViewRepresnetable
gets vertically clipped).
public struct VibrantView<Content: View>: UIViewRepresentable {
private let content: UIView!
private let vibrancyBlurEffectStyle: UIBlurEffect.Style
init(vibrancyBlurEffectStyle: UIBlurEffect.Style, @ViewBuilder content: () -> Content) {
self.content = UIHostingController(rootView: content()).view
self.vibrancyBlurEffectStyle = vibrancyBlurEffectStyle
}
public func makeUIView(context: Context) -> UIView {
let containerView = UIView()
let blurEffect = UIBlurEffect(style: vibrancyBlurEffectStyle)
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
let blurView = UIVisualEffectView(effect: vibrancyEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(blurView)
content.backgroundColor = .clear
content.translatesAutoresizingMaskIntoConstraints = false
blurView.contentView.addSubview(content)
NSLayoutConstraint.activate([
blurView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
blurView.heightAnchor.constraint(equalTo: containerView.heightAnchor),
content.widthAnchor.constraint(equalTo: blurView.widthAnchor),
content.heightAnchor.constraint(equalTo: blurView.heightAnchor),
])
content.setContentHuggingPriority(.defaultLow, for: .vertical)
content.setContentHuggingPriority(.defaultLow, for: .horizontal)
content.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
content.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
blurView.setContentHuggingPriority(.defaultLow, for: .vertical)
blurView.setContentHuggingPriority(.defaultLow, for: .horizontal)
blurView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
blurView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
return containerView
}
public func updateUIView(_ uiView: UIView, context: Context) {
}
}
Used as:
...
VibrantView(vibrancyBlurEffectStyle: .dark) {
Text("Hello")
.foregroundColor(Color.gray)
}
When run on device with other views inside of a VStack
, you'll see "Hello" clipped partially from the bottom. In Preview, you'll see a much larger blue rectangle (bounds) around "Hello", whereas I'd like this to be hugging the content. The VStack
does not assume the full natural height of the overall view.
Using fixedSize()
doesn't work and it produces even weirder results when used with other controls.
Upvotes: 3
Views: 3427
Reputation: 5193
After trying various techniques and hacks - I simply could not get the UIKit container (i.e. VibrantView
) to hug its SwiftUI contents reliably, without adding a fixed sized .frame(...)
modifier on top - which makes it difficult to use this with dynamically sized Text
.
What did work for me was a bit of a hack and probably won't work for every generic view out there (and probably won't scale well for dozens of views), but works well for simple use cases, especially if you're hosting this inside of a dynamically sized UITableViewCell
.
The idea is to use a dummy version of the same view, and set the VibrantView
in an .overlay( ... )
. This will force the overlay to assume the same overall size of the parent SwitfUI View
. Since the view being applied the modifier is a copy of the same view that VibrantView
wraps, you end up with the correct dynamic size at runtime and in Xcode previews.
So something like this:
SomeView()
.frame(minWidth: 0, maxWidth: .infinity)
.overlay(
VibrantView(vibrancyBlurEffectStyle: .dark) {
SomeView()
.frame(minWidth: 0, maxWidth: .infinity)
}
)
I can imagine turning this into a modifier so that it wraps the above in a single call, but in my case to ensure it remains performant for Images, I'm doing something like this:
Circle()
.foregroundColor(.clear)
.frame(width: 33, height: 33)
.overlay(
VibrantView(vibrancyBlurEffectStyle: .systemMaterialDark) {
Image("some image")
.resizable()
}
)
Creating a Circle
is arguably lighter weight compared to the actual image. I create a transparent circle, set the actual size of the image there, and then put the VibrantView
container into the overlay
.
Upvotes: 6
Reputation: 8116
why so complicated?
try this:
struct Blur: UIViewRepresentable {
#if os(iOS)
var style: UIBlurEffect.Style = .systemMaterial
#else
var style: UIBlurEffect.Style = .light
#endif
init(_ style: UIBlurEffect.Style = .dark) {
self.style = style
}
func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
and use:
Text("Great text with prominent")
.font(.largeTitle)
.padding()
.background(Blur(.prominent))
in general you should not use constraints in UIViewRepresentable -> the size will be defined by parent view like Text in this example or VStack, who gives the right size to the blur. Normally a blur is not standing alone but the typical is a blurred background because you want to put text on it so you can read the text better.
Upvotes: -1