UIHostingController size too big - swift

I'm trying to embed a SwiftUI View within a UIKit UIView, within a View again. It will look something like this:
View
↓
UIView
↓
View
Current code:
struct ContentView: View {
var body: some View {
Representable {
Text("Hello world!")
}
}
}
struct Representable<Content: View>: UIViewRepresentable {
private let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: Context) -> UIView {
let host = UIHostingController(rootView: content())
let hostView = host.view!
return hostView
}
func updateUIView(_ uiView: UIView, context: Context) {
uiView.backgroundColor = .systemRed
}
}
I want the Representable to only set the backgroundColor of the Text. It shouldn't be any bigger. Also, this is just an example, so this isn't just a Text and setting the background color.
Now
Aim
There is also a problem if the text is really long - it doesn't get constrained by the size of the screen / parent (using hugging priority in this case):
How can I make sure that Representable is only as big as the content itself, Text in this case? It should also work if the text wraps over a line for example when constrained to a certain width.

The simplest way is to use SwiftUI-Introspect and just grab the UIView from it. This is all the code needed:
Text("This is some really long text that will have to wrap to multiple lines")
.introspect(selector: TargetViewSelector.siblingOfType) { target in
target.backgroundColor = .systemRed
}
If the view is a bit more complex and there isn't a UIView specifically for it, you can embed it in a ScrollView so the content will now be a UIView:
ScrollView {
Text("Complex content here")
}
.introspectScrollView { scrollView in
scrollView.isScrollEnabled = false
scrollView.clipsToBounds = false
scrollView.subviews.first!.backgroundColor = .systemRed
}
If you don't want to use Introspect (which I would highly recommend), there is a second solution below. The second solution works in most situations, but not all.
See solution above first.
I've created a working answer. It looks quite complicated, but it works.
It basically works by using the inside GeometryReader to measure the size of the content to be wrapped and the outside GeometryReader to measure the size of the whole container. This means that Text will now wrap lines because it's constrained by the outside container's size.
Code:
struct ContentView: View {
var body: some View {
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines")
}
}
}
struct Wrapper<Content: View>: View {
#State private var size: CGSize?
#State private var outsideSize: CGSize?
private let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
GeometryReader { outside in
Color.clear.preference(
key: SizePreferenceKey.self,
value: outside.size
)
}
.onPreferenceChange(SizePreferenceKey.self) { newSize in
outsideSize = newSize
}
.frame(width: size?.width, height: size?.height)
.overlay(
outsideSize != nil ?
Representable {
content()
.background(
GeometryReader { inside in
Color.clear.preference(
key: SizePreferenceKey.self,
value: inside.size
)
}
.onPreferenceChange(SizePreferenceKey.self) { newSize in
size = newSize
}
)
.frame(width: outsideSize!.width, height: outsideSize!.height)
.fixedSize()
.frame(width: size?.width ?? 0, height: size?.height ?? 0)
}
.frame(width: size?.width ?? 0, height: size?.height ?? 0)
: nil
)
}
}
struct SizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct Representable<Content: View>: UIViewRepresentable {
private let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIView(context: Context) -> UIView {
let host = UIHostingController(rootView: content())
let hostView = host.view!
return hostView
}
func updateUIView(_ uiView: UIView, context: Context) {
uiView.backgroundColor = .systemRed
}
}
Result:
Another example to show that it does make the wrapper the exact size as the SwiftUI view:
struct ContentView: View {
var body: some View {
VStack {
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines")
}
.border(Color.green, width: 3)
Wrapper {
Text("This is some really long text that will have to wrap to multiple lines. However, this bottom text is a bit longer and may wrap more lines - but this isn't a problem here")
}
.border(Color.blue, width: 3)
}
}
}

Related

How can I access the parent/Environment of a custom view?

I want to access the parent of my custom view to know whether my view parent is a HStack or VStack, like Divider() could do it. Currently I am hard coding value, but my goal is that I could be get access the parent information inside my custom view to select the right return view.
struct ContentView: View {
var body: some View {
HStack { Divider() }
VStack { Divider() }
HStack { CustomView(parentIsHStack: true) }
VStack { CustomView(parentIsHStack: false) }
}
}
struct CustomView: View {
let parentIsHStack: Bool
var body: some View {
if parentIsHStack {
Text("Parent is HStack")
}
else {
Text("Parent is VStack")
}
}
}
I would be tempted to say this is not a hidden environment variable. I don't see a relevant one when I dump all the environment variables (there are a lot though).
Instead, I believe it's how _VariadicView.Tree works. This contains a root and its content. I'll take how HStack works for example. Inspecting the SwiftUI interface, you can see the following snippet of code:
#frozen public struct HStack<Content> : SwiftUI.View where Content : SwiftUI.View {
#usableFromInline
internal var _tree: SwiftUI._VariadicView.Tree<SwiftUI._HStackLayout, Content>
#inlinable public init(alignment: SwiftUI.VerticalAlignment = .center, spacing: CoreGraphics.CGFloat? = nil, #SwiftUI.ViewBuilder content: () -> Content) {
_tree = .init(
root: _HStackLayout(alignment: alignment, spacing: spacing), content: content())
}
public static func _makeView(view: SwiftUI._GraphValue<SwiftUI.HStack<Content>>, inputs: SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs
public typealias Body = Swift.Never
}
Notice that the Body is of type Never (therefore a primitive view type). The _tree stores information about the layout, and the type HStackLayout obviously shows this is a HStack.
SwiftUI will be using _makeView(view:inputs:) internally to create the view, which I'm assuming gives special treatment to certain views.
You'll need to make custom versions of HStack/VStack and pass an environment variable down to know which kind your subview is in.
I could solve the issue with replacing Apple Stack's, without broking apple api.
struct ContentView: View {
var body: some View {
HStack { Divider() }
VStack { Divider() }
HStack { CustomView() }
VStack { CustomView() }
ZStack { CustomView() }
}
}
struct CustomView: View {
#Environment(\.stack) var stack
var body: some View {
Text("Parent is " + stack.rawValue)
}
}
struct HStack<Content>: View where Content: View {
let alignment: VerticalAlignment
let spacing: CGFloat?
let content: () -> Content
init(alignment: VerticalAlignment = VerticalAlignment.center, spacing: CGFloat? = nil, #ViewBuilder content: #escaping () -> Content) {
self.alignment = alignment
self.spacing = spacing
self.content = content
}
var body: some View {
return SwiftUI.HStack(alignment: alignment, spacing: spacing, content: { content() })
.environment(\.stack, Stack.hStack)
}
}
struct VStack<Content>: View where Content: View {
let alignment: HorizontalAlignment
let spacing: CGFloat?
let content: () -> Content
init(alignment: HorizontalAlignment = HorizontalAlignment.center, spacing: CGFloat? = nil, #ViewBuilder content: #escaping () -> Content) {
self.alignment = alignment
self.spacing = spacing
self.content = content
}
var body: some View {
return SwiftUI.VStack(alignment: alignment, spacing: spacing, content: { content() })
.environment(\.stack, Stack.vStack)
}
}
struct ZStack<Content>: View where Content: View {
let alignment: Alignment
let content: () -> Content
init(alignment: Alignment = Alignment.center, #ViewBuilder content: #escaping () -> Content) {
self.alignment = alignment
self.content = content
}
var body: some View {
return SwiftUI.ZStack(alignment: alignment, content: { content() })
.environment(\.stack, Stack.zStack)
}
}
private struct StackKey: EnvironmentKey { static let defaultValue: Stack = Stack.unknown }
extension EnvironmentValues {
var stack: Stack {
get { return self[StackKey.self] }
set(newValue) { self[StackKey.self] = newValue }
}
}
enum Stack: String { case vStack, hStack, zStack, unknown }

How can I make a Custom ScrollView without using SwiftUI ScrollView?

I'm currently working on Custom ScrollView without ScrollView.
I don't want to add too many features, but I want to be able to scroll by dragging.
I've searched the Internet, but all I can find are examples of wrapped ScrollView.
The code I'm working on is as follows(WIP code):
import SwiftUI
struct ContentView: View {
#State private var yOffset: CGFloat = 0
#State private var contentSize: CGSize = .zero
var body: some View {
CustomScrollView {
ForEach(0..<100) { i in
Text(
"\(i)"
)
.frame(
maxWidth: .infinity
)
.background(
Color.green
)
}
.offset(y: 0)
.size(size: $contentSize)
}
.offset(y: 0)
}
}
struct CustomScrollView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
self.content
}
}
//==================================================
// For extension
//==================================================
struct ChildSizeReader<Content: View>: View {
#Binding var size: CGSize
let content: () -> Content
var body: some View {
// Remove ZStack from the existing answer.
content().background(
GeometryReader { proxy in
Color.clear.preference(
key: SizePreferenceKey.self,
value: proxy.size
)
}
)
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.size = preferences
}
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}
extension View {
func size(size: Binding<CGSize>) -> some View {
ChildSizeReader(size: size) {
self
}
}
}
As you can see in the attached image, the top of the content is missing.
I would like to adjust it so that the top is visible, but I don't know how to do that.
Note:
The dragging and other processes will be implemented after the top is visible first.
Reference:
https://www.youtube.com/watch?v=vuFUX1Qwrmo
SwiftUI - Get size of child?

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

Size of View in SwiftUI

Here I am reading my View Size in a background process, everything works fine except than sending the un-wished value!
In this down code, I used (CGSize) -> Content, and It send the exist value of CGSize, which means CGSize() or (0.0, 0.0). Which it make sense because it is first finable value, but as you see in the codes I am calculating the needed value and I want send that value.
my Goal: how can I send my (CGSize) -> Content which CGSize is calculated.
PS: I am thinking using completionhandler on CGSize.
Something like this but I am not sure:
var content: (#escaping () -> CGSize) -> Content
or even this:
var content: (#escaping (Content) -> CGSize) -> Content
In this way that it waits until CGSize get calculated then sent it with Content together, as you can see in (CGSize) -> Content it capture first possible CGSize!
Console:
(94.66666666666666, 20.333333333333332)
read size onAppear is: (0.0, 0.0)
read size onChange is: (94.66666666666666, 20.333333333333332)
import SwiftUI
struct ContentView: View {
var body: some View {
CustomView { size in
Text("Hello, world!")
.background(Color.yellow)
.onAppear() {
print("read size onAppear is:", size.debugDescription)
}
.onChange(of: size) { newValue in
print("read size onChange is:", newValue.debugDescription)
}
}
}
}
struct CustomView<Content: View>: View {
#State private var sizeOfText: CGSize = CGSize()
var content: (CGSize) -> Content
var body: some View {
return content(sizeOfText)
.background(
GeometryReader { geometry in
Color.clear
.onAppear() { sizeOfText = geometry.size; print(sizeOfText) }
})
}
}
I could be able to find the magical answer:
extension View {
func sizeReader(size: #escaping (CGSize) -> Void) -> some View {
return self
.background(
GeometryReader { geometry in
Color.clear
.preference(key: ContentSizeReaderPreferenceKey.self, value: geometry.size)
.onPreferenceChange(ContentSizeReaderPreferenceKey.self) { newValue in size(newValue) }
}
.hidden()
)
}
}
struct ContentSizeReaderPreferenceKey: PreferenceKey {
static var defaultValue: CGSize { get { return CGSize() } }
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
}
Use case:
You can use .sizeReader on any View you like to read or save the Size of View. It is reverse GeometryReader, and I can say it make a significant difference in your app or project when you need to read or work with the size of a View! Look how much would be cleaner and easier using .sizeReader in your project.
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.sizeReader { size in print(size) }
}
}

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