How can I access to TupleView through ViewBuilder in SwiftUI? - swiftui

I am trying to get access to my TupleView which my ViewBuilder use it to build the content, then I am trying to count it, here I made my codes until this point, I need help to find the count of my TupleView, right now for unknown reason it returns 0, which I was expecting 5! thanks for help.
import SwiftUI
struct ContentView: View {
var body: some View {
ModelView(content: {
Text("Hello, world! 1")
.padding()
Text("Hello, world! 2")
.padding()
Text("Hello, world! 3")
.padding()
Text("Hello, world! 4")
.padding()
Text("Hello, world! 5")
.padding()
})
}
}
struct ModelView<Content: View>: View {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
let size = Mirror(reflecting: content).children.count // <<: Here! it returns 0! which must be 5!
print(size)
}
var body: some View {
content()
}
}

How can I access to TupleView through ViewBuilder in SwiftUI?
Explicitly to the same way as SwiftUI does. Let's see what is buildBlock of the ViewBuilder:
That means that ViewBuilder has explicit function for every count of content views from 1 to 10. Like
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0 : View, C1 : View, C2 : View, C3 : View
}
So if you want to access view builder tuple views, you need to do the same, like (tested with Xcode 12.4)
struct ModelContentView: View {
var body: some View {
ModelView(content: {
Text("Hello, world! 1")
.padding()
Text("Hello, world! 2")
.padding()
// Text("Hello, world! 3")
// .padding()
// Text("Hello, world! 4")
// .padding()
// Text("Hello, world! 5")
// .padding()
})
}
}
struct ModelView<Content: View>: View {
var count: Int
var content: () -> Content
private init(count: Int = 1, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.count = count
}
var body: some View {
content()
}
}
extension ModelView {
init<V1: View, V2: View>(#ViewBuilder content: #escaping () -> Content) where Content == TupleView<(V1, V2)> {
self.init(count: 2, content: content)
}
init<V1: View, V2: View, V3: View>(#ViewBuilder content: #escaping () -> Content) where Content == TupleView<(V1, V2, V3)> {
self.init(count: 3, content: content)
}
// ... all other variants
}

First, by accessing Mirror(reflecting: content) you're reflecting the closure that creates Content (you can see that content is actually a closure of type () -> Content).
You probably meant to reflect the view itself. For this you need to reflect the specific instance:
let mirror = Mirror(reflecting: content()) // add the parentheses
Then you tried to access the children. If you access it using the mirror above, you'll get (type formatted for clarity):
AnyCollection<(label: Optional, value: Any)>(
_box: Swift._RandomAccessCollectionBox<
Swift.LazyMapSequence<
Swift.Range<Swift.Int>,
(label: Swift.Optional<Swift.String>, value: Any)
>
>
)
Theoretically, you could then access the value of the first key (with the label value):
if let value = Mirror(reflecting: content()).children.first?.value {
print(value)
}
You'd get the result:
(
SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout>(
content: SwiftUI.Text(
storage: SwiftUI.Text.Storage.anyTextStorage(
SwiftUI.unknown context at $7fff57642368.LocalizedTextStorage
), modifiers: []
),
modifier: SwiftUI._PaddingLayout(
edges: SwiftUI.Edge.Set(rawValue: 15),
insets: nil
)
),
SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout>(...)
)
which is a tuple of type:
(
SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout>,
SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout>,
SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout>,
SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout>,
SwiftUI.ModifiedContent<SwiftUI.Text, SwiftUI._PaddingLayout>
)
This is probably all you can get through reflection here.
Important note
From documentation:
Mirrors are used by playgrounds and the debugger.
You shouldn't probably use it in the real app.

Related

Some usages of custom generic view modifiers in SwiftUI do not compile

I am trying to replace a standard sheet modifier with a custom one that applies the same changes to the content of all sheets as to the main view (it can be useful for changing accent color, although there is a UIKit approach for it, but specifically I want to apply privacySensitive modifier to all sheets).
The code that creates the modifiers compiles ok:
import SwiftUI
struct SheetForItem<T, C>: ViewModifier where T: Identifiable, C: View {
var item: Binding<T?>
var onDismiss: (() -> Void)? = nil
var sheetContent: (T) -> C
func body(content: Content) -> some View {
content.sheet(item: item, onDismiss: onDismiss) {
sheetContent($0).privacySensitive()
}
}
}
extension View {
func appSheet<T, Content>(
item: Binding<T?>,
onDismiss: (() -> Void)? = nil,
content: #escaping (T) -> Content
) -> some View where T: Identifiable, Content: View {
modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content))
}
}
Mostly it works, but some of the usages of appSheet in the chain of other modifiers instead of sheet do not compile with an error:
Type () cannot conform to View.
The example below doesn't compile (but it will compile if I replace appSheet with sheet):
import SwiftUI
enum Actions:Identifiable {
case action1
case action2
var id: Self { self }
}
struct AppSheetExample: View {
#State var showActions = false
#State private var action: Actions?
var body: some View {
Button { showActions = true } label: {
Image(systemName: "square.and.pencil")
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
.confirmationDialog("Actions", isPresented: $showActions, titleVisibility: .visible) {
Button("Action 1") { action = .action2 }
Button("Action 2") { action = .action2 }
}
.appSheet(item: $action) { sheet in
switch sheet {
case .action1: Text("Action 1")
case .action2: Text("Action 2")
}
}
}
}
Thank you!
You need to mark your content closure with #ViewBuilder since you're not explicitly returning a View(i.e: return Text("Action 1")):
extension View {
func appSheet<Content>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
#ViewBuilder content: #escaping () -> Content
) -> some View where Content: View {
modifier(SheetIsPresented(isPresented: isPresented, onDismiss: onDismiss, sheetContent: content))
}
func appSheet<T, Content>(
item: Binding<T?>,
onDismiss: (() -> Void)? = nil,
#ViewBuilder content: #escaping (T) -> Content
) -> some View where T: Identifiable, Content: View {
modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content))
}
}

SwiftUI: Generic parameter 'Value' could not be inferred

Following codes gives an error of "Generic parameter 'Value' could not be inferred".
Appreciate anyone could point out the root cause and let me know how to fix it.
Thanks
import SwiftUI
private let readMe = """
This case study demonstrates how to enhance an existing SwiftUI component so that it can be driven \
off of optional and enum state.
The BottomMenuModifier component in this is file is primarily powered by a simple boolean binding, \
which means its content cannot be dynamic based off of the source of truth that drives its \
presentation, and it cannot make mutations to the source of truth.
However, by leveraging the binding transformations that come with this library we can extend the \
bottom menu component with additional APIs that allow presentation and dismissal to be powered by \
optionals and enums.
"""
struct ContentView: View {
#State var count: Int?
#State private var showPartialSheet = false
var body: some View {
Form {
Button("Show bottom menu") {
withAnimation {
self.count = 0
self.showPartialSheet = true
}
}
}
.bottomMenu($showPartialSheet, content: {
VStack {
Text("dfd")
}
})
.navigationTitle("Custom components")
}
}
private struct BottomMenuModifier<BottomMenuContent>: ViewModifier
where BottomMenuContent: View {
#Binding var isActive: Bool
let content: BottomMenuContent
init(isActive: Binding<Bool>, #ViewBuilder content: () -> BottomMenuContent) {
self._isActive = isActive
self.content = content()
}
func body(content: Content) -> some View {
content.overlay(
ZStack(alignment: .bottom) {
if self.isActive {
Rectangle()
.fill(Color.black.opacity(0.4))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
withAnimation {
self.isActive = false
}
}
.zIndex(1)
.transition(.opacity)
self.content
.padding()
.background(Color.white)
.cornerRadius(10)
.frame(maxWidth: .infinity)
.padding(24)
.padding(.bottom)
.zIndex(2)
.transition(.move(edge: .bottom))
}
}
.ignoresSafeArea()
)
}
}
extension View {
fileprivate func bottomMenu<Value, Content>(
_ showPartialSheet: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content
) -> some View
where Content: View {
self.modifier(
BottomMenuModifier(isActive: showPartialSheet, content: content)
)
}
}
struct CustomComponents_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Your bottomMenu function has an extra generic in its signature. Change it to:
fileprivate func bottomMenu<Content>(
(Note that Value from your original, which is unused, is removed)

How can I modify each Content inside of Curly brackets of CustomView separately in SwiftUI?

How can I modify each Content inside of Curly brackets of CustomView separately, in down code I am modifying the offset of content which is working fine, but it apply's to all Content's of CustomView and then takes the next Index of EachFor and do the same. Which is correct and doing its Job!
My Goal in this Question is finding a way to tell the CustomView to modify One by One, Not all together!
PS: my goal of using CustomView was/is making me free to modify code in ContentView. I want just load my contents inside of Curly brackets of CustomView, and all the modifying happens inside of CustomView. and this CustomView could take any View/Content.
my Goal output Image:
struct ContentView: View {
var body: some View {
CustomView {
Text("Hello, world! 1")
Text("Hello, world! 2")
Text("Hello, world! 3")
Text("Hello, world! 4")
Text("Hello, world! 5")
}
}
}
struct CustomView<Content: View>: View {
var inPutView: () -> Content
init(#ViewBuilder inputView: #escaping () -> Content) {
self.inPutView = inputView
}
var body: some View
{
VStack {
ForEach(0..<5) { index in
inPutView().offset(x: CGFloat(index*20))
}
}
}
}
output of this code:
update:
struct ContentView: View {
var body: some View {
CustomView {
Text("Hello")
Image(systemName: "folder")
Text("Omid")
Image(systemName: "star")
Circle().fill(Color.red).frame(width: 10, height: 10, alignment: .center)
HStack { Image(systemName: "person"); Image(systemName: "1.circle").foregroundColor(Color.green) }
}
}
}
struct CustomView<Content: View>: View {
var inPutView: () -> Content
init(#ViewBuilder inputView: #escaping () -> Content) {
self.inPutView = inputView
}
var body: some View
{
VStack {
ForEach(0..<5) { index in
inPutView().offset(x: CGFloat(index*20))
}
}
}
}
Here seems to me is a possible approach to solve such kind of tasks. At least a way to move...
Tested with Xcode 12.1 / iOS 14.1.
struct ContentView: View {
var body: some View {
CustomView(5) {
Text("Hello, world! \($0 + 1)")
}
}
}
struct CustomView<Content: View>: View {
private var count: Int
var inPutView: (Int) -> Content
init(_ count: Int = 1, #ViewBuilder inputView: #escaping (Int) -> Content) {
self.count = count
self.inPutView = inputView
}
var body: some View {
VStack {
ForEach((0..<count).indices, id: \.self) { index in
inPutView(index).offset(x: CGFloat(index*20))
}
}
}
}

How to loop over viewbuilder content subviews in SwiftUI

So I’m trying to create a view that takes viewBuilder content, loops over the views of the content and add dividers between each view and the other
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
// here
}
.background(Color.black)
.cornerRadius(14)
}
}
so where I wrote “here” I want to loop over the views of the content, if that makes sense. I’ll write a code that doesn’t work but that explains what I’m trying to achieve:
ForEach(content.subviews) { view in
view
Divider()
}
How to do that?
I just answered on another similar question, link here. Any improvements to this will be made for the linked answer, so check there first.
GitHub link of this (but more advanced) in a Swift Package here
However, here is the answer with the same TupleView extension, but different view code.
Usage:
struct ContentView: View {
var body: some View {
BoxWithDividerView {
Text("Something 1")
Text("Something 2")
Text("Something 3")
Image(systemName: "circle") // Different view types work!
}
}
}
Your BoxWithDividerView:
struct BoxWithDividerView: View {
let content: [AnyView]
init<Views>(#ViewBuilder content: #escaping () -> TupleView<Views>) {
self.content = content().getViews
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(content.indices, id: \.self) { index in
if index != 0 {
Divider()
}
content[index]
}
}
// .background(Color.black)
.cornerRadius(14)
}
}
And finally the main thing, the TupleView extension:
extension TupleView {
var getViews: [AnyView] {
makeArray(from: value)
}
private struct GenericView {
let body: Any
var anyView: AnyView? {
AnyView(_fromValue: body)
}
}
private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
func convert(child: Mirror.Child) -> AnyView? {
withUnsafeBytes(of: child.value) { ptr -> AnyView? in
let binded = ptr.bindMemory(to: GenericView.self)
return binded.first?.anyView
}
}
let tupleMirror = Mirror(reflecting: tuple)
return tupleMirror.children.compactMap(convert)
}
}
Result:
So I ended up doing this
#_functionBuilder
struct UIViewFunctionBuilder {
static func buildBlock<V: View>(_ view: V) -> some View {
return view
}
static func buildBlock<A: View, B: View>(
_ viewA: A,
_ viewB: B
) -> some View {
return TupleView((viewA, Divider(), viewB))
}
}
Then I used my function builder like this
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#UIViewFunctionBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 0.0) {
content()
}
.background(Color(UIColor.AdUp.carbonGrey))
.cornerRadius(14)
}
}
But the problem is this only works for up to 2 expression views. I’m gonna post a separate question for how to be able to pass it an array

Measure the rendered size of a SwiftUI view?

Is there a way to measure the computed size of a view after SwiftUI runs its view rendering phase? For example, given the following view:
struct Foo : View {
var body: some View {
Text("Hello World!")
.font(.title)
.foregroundColor(.white)
.padding()
.background(Color.red)
}
}
With the view selected, the computed size is displayed In the preview canvas in the bottom left corner. Does anyone know of a way to get access to that size in code?
Printing out values is good, but being able to use them inside the parent view (or elsewhere) is better. So I took one more step to elaborate it.
struct GeometryGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { (g) -> Path in
print("width: \(g.size.width), height: \(g.size.height)")
DispatchQueue.main.async { // avoids warning: 'Modifying state during view update.' Doesn't look very reliable, but works.
self.rect = g.frame(in: .global)
}
return Path() // could be some other dummy view
}
}
}
struct ContentView: View {
#State private var rect1: CGRect = CGRect()
var body: some View {
HStack {
// make to texts equal width, for example
// this is not a good way to achieve this, just for demo
Text("Long text").background(Color.blue).background(GeometryGetter(rect: $rect1))
// You can then use rect in other places of your view:
Text("text").frame(width: rect1.width, height: rect1.height).background(Color.green)
Text("text").background(Color.yellow)
}
}
}
You could add an "overlay" using a GeometryReader to see the values. But in practice it would probably be better to use a "background" modifier and handle the sizing value discretely
struct Foo : View {
var body: some View {
Text("Hello World!")
.font(.title)
.foregroundColor(.white)
.padding()
.background(Color.red)
.overlay(
GeometryReader { proxy in
Text("\(proxy.size.width) x \(proxy.size.height)")
}
)
}
}
Here is the ugly way I came up with to achieve this:
struct GeometryPrintingView: View {
var body: some View {
GeometryReader { geometry in
return self.makeViewAndPrint(geometry: geometry)
}
}
func makeViewAndPrint(geometry: GeometryProxy) -> Text {
print(geometry.size)
return Text("")
}
}
And updated Foo version:
struct Foo : View {
var body: some View {
Text("Hello World!")
.font(.title)
.foregroundColor(.white)
.padding()
.background(Color.red)
.overlay(GeometryPrintingView())
}
}
To anyone who wants to obtain a size out of Jack's solution and store it in some property for further use:
.overlay(
GeometryReader { proxy in
Text(String())
.onAppear() {
// Property, eg
// #State private var viewSizeProperty = CGSize.zero
viewSizeProperty = proxy.size
}
.opacity(.zero)
}
)
This is a bit dirty obviously, but why not if it works.
Try to use PreferenceKey like this.
struct HeightPreferenceKey : PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}
struct WidthPreferenceKey : PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}
struct SizePreferenceKey : PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
extension View {
func readWidth() -> some View {
background(GeometryReader {
Color.clear.preference(key: WidthPreferenceKey.self, value: $0.size.width)
})
}
func readHeight() -> some View {
background(GeometryReader {
Color.clear.preference(key: HeightPreferenceKey.self, value: $0.size.height)
})
}
func onWidthChange(perform action: #escaping (CGFloat) -> Void) -> some View {
onPreferenceChange(WidthPreferenceKey.self) { width in
action(width)
}
}
func onHeightChange(perform action: #escaping (CGFloat) -> Void) -> some View {
onPreferenceChange(HeightPreferenceKey.self) { height in
action(height)
}
}
func readSize() -> some View {
background(GeometryReader {
Color.clear.preference(key: SizePreferenceKey.self, value: $0.size)
})
}
func onSizeChange(perform action: #escaping (CGSize) -> Void) -> some View {
onPreferenceChange(SizePreferenceKey.self) { size in
action(size)
}
}
}
struct MyView: View {
#State private var height: CGFloat = 0
var body: some View {
...
.readHeight()
.onHeightChange {
height = $0
}
}
}
As others pointed out, GeometryReader and a custom PreferenceKey is the best way forward for now. I've implemented a helper drop-in library which does pretty much that: https://github.com/srgtuszy/measure-size-swiftui