How to use GeometryProxy outside of a ViewBuilder? - swift

I implemented a BasicContainer upon GeometryReader with the functionality of a #ViewBuilder. How can I use this inside GeometryProxy, outside when declaring the content?
Here is my BasicContainer:
struct BasicContainer<Content : View> : View {
let content : Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
GeometryReader { proxy in
content
.frame(width: proxy.size.width * 0.3, height: 200)
// do some other fancy stuff ...
}
}
}
Everything works fine, when I use the BasicContainer as it's meant to be:
struct ContentView: View {
var body: some View {
BasicContainer {
Text("Roberts BasicContainer")
}
}
}
But what, if I would like to have the GeometryProxy also outside, (like Apple implemented their GeometryReader), like so:
BasicContainer { outsideGeo in
Text("Width: \(outsideGeo.size.width)")
}
I guess some kind of a #Binding could make the trick, but so far I couldn't make it work.
Is this even possible to realize?
Any help is very appreciated !!!

This should work:
let content : (GeometryProxy) -> Content
init(#ViewBuilder content: #escaping (GeometryProxy) -> Content) {
self.content = content
}
then:
GeometryReader { proxy in
content(proxy)
.frame(width: proxy.size.width * 0.3, height: 200)
// do some other fancy stuff ...
}
edit: I assumed your geometryproxy is named proxy. -> Explicit implementation to avoid confusion.
when you call your container you can do:
BasicContainer { outsideGeo in
Text("Width: \(outsideGeo.size.width)")
}

Related

UIHostingController size too big

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

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 infer generic parameter in extension?

I have a very simple CustomView which it takes a View and modify it! Like this code:
struct CustomView<ViewType: View>: View {
let content: () -> ViewType
var body: some View {
return content()
.foregroundColor(.red)
}
}
So far so good!
Now I want make a CustomModifier out of it, like this:
extension View {
func customModifier<ViewModifierType: View>(viewModifier: () -> ViewModifierType) -> some View {
// how can I import self from here?
return viewModifier()
}
}
use case:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.customModifier(viewModifier: { CustomView(content: { Text("Hello, World!") } ) }) // <<: Here is the issue!
// How can I satisfy CustomView with self in extension? that could help to cut Text("Hello, World!") to feeding as content for CustomView?
}
}
So if see the codes, all I am trying to do is cut off Text("Hello, World!") with using self from extension and trying to have this form of coding:
.customModifier(viewModifier: { CustomView() })
PS: I know that for having form of CustomView() I have to respect the generic parameter CustomView with word of where but I do not know how can I put these all puzzle together!
If I understand you correctly, then try this
extension View {
func customModifier<ViewModifierType: View>(viewModifier: (Self) -> ViewModifierType) -> some View {
return viewModifier(self)
}
}
Not sure what is your goal but if you just want to add a red foreground to a view you need to implement a view modifier:
struct RedForeground: ViewModifier {
func body(content: Content) -> some View {
content.foregroundColor(.red)
}
}
And then just call it using the modifier method:
Text("Text")
.modifier(RedForeground())
If you want to simplify it further you can extend View and add make it a computed property:
extension View {
var redForeground: some View { modifier(RedForeground()) }
}
usage:
Text("Text")
.redForeground
You can't just init CustomView without a view, but you can pass CustomView initializer as function to customModifier, like this:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.customModifier(viewModifier: CustomView.init)
}
}
struct CustomView<ViewType: View>: View {
let content: ViewType
var body: some View {
return content
.foregroundColor(.red)
}
}
extension View {
func customModifier<ViewModifierType: View>(viewModifier: (Self) -> ViewModifierType) -> some View {
return viewModifier(self)
}
}

How can I detect Empty Content as input content in SwiftUI?

I have a CustomView which has a simple logic! If this CustomView get a non EmptyView should return an HStack, and If this CustomView get an EmptyView should return a Circle, how could I find out the right option depending on input content?
for example it should work like this: But it does not! I must hard code true or false for the result you see in down
CustomView() { Text("Hello") }.background(Color.red).padding()
CustomView().background(Color.red)
Goal: This question try find an answer to set right value for useHStack value depending on input content!
PS: Logically there is a way to use useHStack as parameter of CustomView, in this case there was no need to ask this question! So we are trying solve the issue without using useHStack as input parameter of CustomView or any other also! I like detect at initialization on CustomView not in body or through GeometryReader and so on . . .
My issue is there, how can I know when useHStack must be true or false depending on incoming content! incoming content means from example: Text("Hello")
import SwiftUI
struct ContentView: View {
var body: some View {
CustomView() { Text("Hello") }
CustomView()
}
}
struct CustomView<Content: View>: View {
let content: () -> Content
let useHStack: Bool
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
self.useHStack = true // or: false depending on content(), if we got EmptyView() then false otherwise true! the idea is make this part Automatic!
}
init() where Content == EmptyView {
self.init(content: { EmptyView() })
}
var body: some View {
if useHStack {
HStack {
content()
Spacer()
Circle().frame(width: 50, height: 50, alignment: .center)
}
}
else {
Circle().frame(width: 50, height: 50, alignment: .center)
}
}
}
You can set useHStack to false inside the init() where Content == EmptyView. You either would need to make it var useHStack: Bool, or create a private init:
private init(content: #escaping () -> Content, useHStack: Bool) {
self.content = content
self.useHStack = useHStack
}
init(#ViewBuilder content: #escaping () -> Content) {
self.init(content: content, useHStack: true)
}
init() where Content == EmptyView {
self.init(content: { EmptyView() }, useHStack: false)
}

How to pass one SwiftUI View as a variable to another View struct

I'm implementing a very custom NavigationLink called MenuItem and would like to reuse it across the project. It's a struct that conforms to View and implements var body : some View which contains a NavigationLink.
I need to somehow store the view that shall be presented by NavigationLink in the body of MenuItem but have yet failed to do so.
I have defined destinationView in MenuItem's body as some View and tried two initializers:
This seemed too easy:
struct MenuItem: View {
private var destinationView: some View
init(destinationView: View) {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
2nd try:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Cannot assign value of type 'V' to type 'some View'.
Final try:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView as View
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Error: Cannot assign value of type 'View' to type 'some View'.
I hope someone can help me. There must be a way if NavigationLink can accept some View as an argument.
Thanks ;D
To sum up everything I read here and the solution which worked for me:
struct ContainerView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
}
}
This not only allows you to put simple Views inside, but also, thanks to #ViewBuilder, use if-else and switch-case blocks:
struct SimpleView: View {
var body: some View {
ContainerView {
Text("SimpleView Text")
}
}
}
struct IfElseView: View {
var flag = true
var body: some View {
ContainerView {
if flag {
Text("True text")
} else {
Text("False text")
}
}
}
}
struct SwitchCaseView: View {
var condition = 1
var body: some View {
ContainerView {
switch condition {
case 1:
Text("One")
case 2:
Text("Two")
default:
Text("Default")
}
}
}
}
Bonus:
If you want a greedy container, which will claim all the possible space (in contrary to the container above which claims only the space needed for its subviews) here it is:
struct GreedyContainerView<Content: View>: View {
#ViewBuilder let content: Content
var body: some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
If you need an initializer in your view then you can use #ViewBuilder for the parameter too. Even for multiple parameters if you will:
init(#ViewBuilder content: () -> Content) {…}
The way Apple does it is using function builders. There is a predefined one called ViewBuilder. Make it the last argument, or only argument, of your init method for MenuItem, like so:
..., #ViewBuilder builder: #escaping () -> Content)
Assign it to a property defined something like this:
let viewBuilder: () -> Content
Then, where you want to diplay your passed-in views, just call the function like this:
HStack {
viewBuilder()
}
You will be able to use your new view like this:
MenuItem {
Image("myImage")
Text("My Text")
}
This will let you pass up to 10 views and use if conditions etc. though if you want it to be more restrictive you will have to define your own function builder. I haven't done that so you will have to google that.
You should make the generic parameter part of MenuItem:
struct MenuItem<Content: View>: View {
private var destinationView: Content
init(destinationView: Content) {
self.destinationView = destinationView
}
var body : some View {
// ...
}
}
You can create your custom view like this:
struct ENavigationView<Content: View>: View {
let viewBuilder: () -> Content
var body: some View {
NavigationView {
VStack {
viewBuilder()
.navigationBarTitle("My App")
}
}
}
}
struct ENavigationView_Previews: PreviewProvider {
static var previews: some View {
ENavigationView {
Text("Preview")
}
}
}
Using:
struct ContentView: View {
var body: some View {
ENavigationView {
Text("My Text")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can pass a NavigationLink (or any other view widget) as a variable to a subview as follows:
import SwiftUI
struct ParentView: View {
var body: some View {
NavigationView{
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
}
struct ChildView<Content: View>: View {
var destinationView: Content
var title: String
init(destinationView: Content, title: String) {
self.destinationView = destinationView
self.title = title
}
var body: some View {
NavigationLink(destination: destinationView){
Text("This item opens the \(title) view").foregroundColor(Color.black)
}
}
}
struct ThirdView: View {
var body: some View {
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
The accepted answer is nice and simple. The syntax got even cleaner with iOS 14 + macOS 11:
struct ContainerView<Content: View>: View {
#ViewBuilder var content: Content
var body: some View {
content
}
}
Then continue to use it like this:
ContainerView{
...
}
I really struggled to make mine work for an extension of View. Full details about how to call it are seen here.
The extension for View (using generics) - remember to import SwiftUI:
extension View {
/// Navigate to a new view.
/// - Parameters:
/// - view: View to navigate to.
/// - binding: Only navigates when this condition is `true`.
func navigate<SomeView: View>(to view: SomeView, when binding: Binding<Bool>) -> some View {
modifier(NavigateModifier(destination: view, binding: binding))
}
}
// MARK: - NavigateModifier
fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {
// MARK: Private properties
fileprivate let destination: SomeView
#Binding fileprivate var binding: Bool
// MARK: - View body
fileprivate func body(content: Content) -> some View {
NavigationView {
ZStack {
content
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: destination
.navigationBarTitle("")
.navigationBarHidden(true),
isActive: $binding) {
EmptyView()
}
}
}
}
}
Alternatively you can use a static function extension. For example, I make a titleBar extension to Text. This makes it very easy to reuse code.
In this case you can pass a #Viewbuilder wrapper with the view closure returning a custom type that conforms to view. For example:
import SwiftUI
extension Text{
static func titleBar<Content:View>(
titleString:String,
#ViewBuilder customIcon: ()-> Content
)->some View {
HStack{
customIcon()
Spacer()
Text(titleString)
.font(.title)
Spacer()
}
}
}
struct Text_Title_swift_Previews: PreviewProvider {
static var previews: some View {
Text.titleBar(titleString: "title",customIcon: {
Image(systemName: "arrowshape.turn.up.backward")
})
.previewLayout(.sizeThatFits)
}
}
If anyone is trying to pass two different views to other view, and can't do it because of this error:
Failed to produce diagnostic for expression; please submit a bug report...
Because we are using <Content: View>, the first view you passed, the view is going to store its type, and expect the second view you are passing be the same type, this way, if you want to pass a Text and an Image, you will not be able to.
The solution is simple, add another content view, and name it differently.
Example:
struct Collapsible<Title: View, Content: View>: View {
#State var title: () -> Title
#State var content: () -> Content
#State private var collapsed: Bool = true
var body: some View {
VStack {
Button(
action: { self.collapsed.toggle() },
label: {
HStack {
self.title()
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
VStack {
self.content()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none)
.clipped()
.animation(.easeOut)
.transition(.slide)
}
}
}
Calling this View:
Collapsible {
Text("Collapsible")
} content: {
ForEach(1..<5) { index in
Text("\(index) test")
}
}
Syntax for 2 Views
struct PopOver<Content, PopView> : View where Content: View, PopView: View {
var isShowing: Bool
#ViewBuilder var content: () -> Content
#ViewBuilder var popover: () -> PopView
var body: some View {
ZStack(alignment: .center) {
self
.content()
.disabled(isShowing)
.blur(radius: isShowing ? 3 : 0)
ZStack {
self.popover()
}
.frame(width: 112, height: 112)
.opacity(isShowing ? 1 : 0)
.disabled(!isShowing)
}
}
}