How to make the background of a fullscreencover transparent? - swift

This is what I want to achieve:
This is what I got:
I tried embeding the VStack in another VStack with a .background(.gray.opacity(90)) but it didn't do anything. I am using a fullScreenCover:
let width = UIScreen.main.bounds.width
.fullScreenCover(isPresented: $showCover) {
BuyWithPointsView(type: type).frame(width: (width * (91.733 / 100)), height: (width * (66.667 / 100)))
}
EDIT:
I tried implementing this answer: SwiftUI: Translucent background for fullScreenCover
.fullScreenCover(isPresented: $showCover) {
ZStack {
ZStack {
Color.gray.opacity(0.1).edgesIgnoringSafeArea(.all)
}.background(BackgroundBlurView())
BuyWithPointsView(type: type).frame(width: (width * (91.733 / 100)), height: (width * (66.667 / 100)))
}
}
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
This lead to this result: However this result does not really make the background see through like I want it to be. Changing the opacity and color didn't do much.

I found this cleaner solution for the transparent background without any flicker issues.
struct ClearBackgroundView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
return InnerView()
}
func updateUIView(_ uiView: UIView, context: Context) {
}
private class InnerView: UIView {
override func didMoveToWindow() {
super.didMoveToWindow()
superview?.superview?.backgroundColor = .clear
}
}
}
Usage
PresenterView()
.fullScreenCover(isPresented: $isPresented) {
PresentedView()
.background(ClearBackgroundView())
}

Related

Display UIViewRepresentable as a navigationBarItem in SwiftUI

I can get a button to display in the navigationbaritems in SwiftUI no problem.
I want to display a wrapped UIKit view in the same.
This is a minimal example of a real problem. I want a 100 * 4 UIView displayed as a trailing navigationbaritems.
Here is my code:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("stack overflow test")
}
.navigationBarItems(trailing: TestView())
.navigationTitle("Navigation")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct TestView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 4))
view.backgroundColor = .red
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
Right now nothing displays in the top-right hand corner of the navigation bar. Why might this be?
Instead of setting frame inside the UIViewRepresentable, set frame in navigationBarItems ContentView
Tested in Xcode 12.3 with iOS 14.3
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("stack overflow test")
}
.navigationBarItems(trailing: TestView().frame(width: 100, height: 4)) //< === Here
.navigationTitle("Navigation")
}
}
}
struct TestView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView() //< === Remove From Here
view.backgroundColor = .red
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
Note :
From iOS 14.5 navigationBarItems is deprecated. Use ToolbarItem
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("stack overflow test")
}
.toolbar { //< === Here
ToolbarItem(placement: .navigationBarTrailing) {
TestView().frame(width: 100, height: 4)
}
}
.navigationTitle("Navigation")
}
}
}

SwiftUI: Showing HTML text inside a ScrollView

I'm trying to show some HTML text in an app I'm making, but I can't get it to work properly.
The problem is that the view that contains the text is always shown with the height of only one line, even tho it is wrapped in a ScrollView. In the screenshot, you guys can see that the ScrollView works, but the original size is very small.
The code is:
ScrollView {
GeometryReader { proxy in
AttributedText(htmlContent: job.description)
.frame(height: proxy.size.height, alignment: .center)
}
}
struct AttributedText: UIViewRepresentable {
let htmlContent: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
}
struct AttributedText_Previews: PreviewProvider {
static var previews: some View {
AttributedText(htmlContent: "<h1>This is HTML String</h1>")
}
}
I have also tried to use the GeometryReader to establish the size of the ScrollView, but no luck either.
Is there any way to make this look normal and nice, scrollable, and with a proper text size?
Thanks a lot in advance!
Edit: After #RajaKishan's answer, this is how it looks like (where you can see that the content is cut-off):
As WKWebView has already scroll and you are also wrapped inside the scroll view, so parent scroll view not get the proper size.
You have to disable WKWebView scrolling. Also, bind the size with webview and update the frame of webview.
Here is the possible demo.
struct AttributedText: UIViewRepresentable {
let htmlContent: String
#Binding var size: CGSize
private let webView = WKWebView()
var sizeObserver: NSKeyValueObservation?
func makeUIView(context: Context) -> WKWebView {
webView.scrollView.isScrollEnabled = false //<-- Here
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: AttributedText
var sizeObserver: NSKeyValueObservation?
init(parent: AttributedText) {
self.parent = parent
sizeObserver = parent.webView.scrollView.observe(\.contentSize, options: [.new], changeHandler: { (object, change) in
parent.size = change.newValue ?? .zero
})
}
}
}
For view
#State private var size: CGSize = .zero
var body: some View{
ScrollView {
AttributedText(htmlContent: "<h1>This is HTML String</h1>", size: $size)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: size.height, maxHeight: .infinity)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}

How to update the content inside the ScrollView : SwiftUI

I am trying to update my content inside the scroll view but since makeUIView called only once when view builds. It doesn't refresh when I click on 1 image and then 2 images.
However, If I click on No image and then click on Image 2 it works as expected.
Here is my code.
struct ContentView: View {
#State var images:[String] = []
var body: some View {
VStack{
HStack{
Button(action: { self.images = [] }, label: {
Text("No Images")
})
Button(action: { self.images = ["sample_1"] }, label: {
Text("1 Images")
})
Button(action: { self.images = ["sample_1","sample_2"] }, label: {
Text("2 Images")
})
}
if self.images.count > 0{
GeometryReader { (reader) in
STHorizontalScrollView(images: self.images, size: reader.size)
}
}else{
Text("No Images")
}
}
}
}
struct STHorizontalScrollView:UIViewRepresentable {
var images:[String]
var size:CGSize
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
// Calculating the Width
let widthForContent = CGFloat(self.images.count) * size.width
scrollView.contentSize = CGSize.init(width: widthForContent, height: size.height)
scrollView.isPagingEnabled = true
// Hosting Controller
let hostingController = UIHostingController(rootView: HorizontalList(images: images, size: size))
hostingController.view.frame = CGRect.init(x: 0, y: 0, width: widthForContent, height: size.height)
// Add View to scrollview
scrollView.addSubview(hostingController.view)
return scrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
}
}
struct HorizontalList:View {
var images:[String]
var size:CGSize
var body: some View{
HStack(spacing: 0.0) {
ForEach(self.images, id:\.self){ image in
Image(image)
.resizable()
.aspectRatio(contentMode: ContentMode.fill)
.frame(width: self.size.width, height: self.size.height)
.clipped()
}
}
}
}
Not sure what to write here in the update method as the actual content is inside HorizontalList struct.
Any help will be appreciated.
Thanks
Representable is updated whenever some binding of dependent data is updated, so you need to rebuild internals in update updateUIView when images changes.
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
Usage of representable
// images as binding
STHorizontalScrollView(images: self.$images, size: reader.size)
and representable itself
struct STHorizontalScrollView:UIViewRepresentable {
#Binding var images:[String]
var size:CGSize
func makeUIView(context: Context) -> UIScrollView {
UIScrollView()
}
func updateUIView(_ scrollView: UIScrollView, context: Context) {
_ = scrollView.subviews.map { $0.removeFromSuperview() }
guard images.count != 0 else { return }
// Calculating the Width
let widthForContent = CGFloat(self.images.count) * size.width
scrollView.contentSize = CGSize.init(width: widthForContent, height: size.height)
scrollView.isPagingEnabled = true
// Hosting Controller
let hostingController = UIHostingController(rootView: HorizontalList(images: images, size: size))
hostingController.view.frame = CGRect.init(x: 0, y: 0, width: widthForContent, height: size.height)
// Add View to scrollview
scrollView.addSubview(hostingController.view)
}
}

SwiftUI frame size

How do you get the frame size in UIViewRepresentable?
I have a simple DrawView class that draw something and I would like to integrate with SwiftUI. This code works if I hardcode the frame size to 640x480, but is it possible to know the current frame size of ContentView?
struct ContentView: View {
var body: some View {
SwiftDrawView()
}
}
struct SwiftDrawView: UIViewRepresentable {
func makeUIView(context: Context) -> DrawView {
DrawView(frame:CGRect(x: 0, y: 0, width: 640, height: 480))
}
....
}
The Apple tutorial always use .zero and it won't work in this case.
struct ContentView: View {
var body: some View {
GeometryReader { proxy in
SwiftDrawView(frame: proxy.frame(in: .local))
}
}
}
struct SwiftDrawView: UIViewRepresentable {
let frame: CGRect
func makeUIView(context: Context) -> DrawView {
DrawView(frame:frame)
}
....
}
The frame does not matter at UIView creation time (ie. in makeUIView), because constructed view will be resized according to final layout in container view and resulting frame will be passed in UIView.draw.
Here is complete example (Xcode 11.1) and Preview (Note: if you remove VStack in below example DrawView fills entire screen).
import SwiftUI
import UIKit
class DrawView : UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
UIColor.green.setFill()
path.fill()
}
}
struct SwiftDrawView: UIViewRepresentable {
typealias UIViewType = DrawView
func makeUIView(context: Context) -> DrawView {
DrawView()
}
func updateUIView(_ uiView: DrawView, context: UIViewRepresentableContext<SwiftDrawView>) {
// change here some DrawView properties if needed, eg. color
}
}
struct ContentView: View {
var body: some View {
VStack {
SwiftDrawView()
Text("SwiftUI native")
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

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