My curiosity takes me to pass a View type as parameter to #ViewBuilder. Passing a Model/Primitive type as param in #ViewBuilder is perfectly valid.
As shown below code.
struct TestView<Content: View>: View {
let content: (String) -> Content
init(#ViewBuilder content: #escaping (String) -> Content) {
self.content = content
}
var body: some View {
content("Some text")
}
}
struct ContentTestView: View {
var body: some View {
TestView {
Text("\($0)")
}
}
}
In place of String in
let content: (String) -> Content
If I try to pass a SwiftUI View type, then Compiler is not happy with it.
let content: (View) -> Content
Even though params for #ViewBuilder accepts custom Protocol type like Searchable but not View protocol.
compiler tell me this Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
My whole idea is that content can be allowed to hold Section/List/Text in it.
Edit: I expected code like below.
struct TestView<Content: View>: View {
let content: (View) -> Content
init(#ViewBuilder content: #escaping (View) -> Content) {
self.content = content
}
var body: some View {
content(
List {
ForEach(0..<10) { i in
Text(\(i))
}
}
)
}
}
struct ContentTestView: View {
var body: some View {
TestView { viewbody -> _ in
Section(header: Text("Header goes here")) {
viewbody
}
}
}
}
Any way can I achieve this ?
The possible solution is to use AnyView, like
struct TestView<Content: View>: View {
let content: (AnyView) -> Content
init(#ViewBuilder content: #escaping (AnyView) -> Content) {
self.content = content
}
var body: some View {
content(AnyView(
Text("Demo") // << put here any view hierarchy
))
}
}
Related
Context
I am currently working on a system for handling Sheets in SwiftUI. However, I encountered a problem when utilizing the MVVM Design Pattern and generics.
I have a Sheet struct containing the generic View the Sheet should display. I also have an #Published variable in the view model holding the currently active Sheet.
However, obviously, this does not allow sheets with different view types, since I get the following compiler errors:
Error 1: Reference to generic type 'Sheet' requires arguments in <...>
Error 2: 'nil' requires a contextual type
Code
public struct Sheet<Content: View>: Identifiable {
public let id = UUID()
let content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
}
public class SheetViewModel: ObservableObject {
public static let shared = SheetViewModel()
private init() {}
#Published var sheet: Sheet? // Error 1
public func present<Content: View>(_ sheet: Sheet<Content>) {
self.sheet = sheet
}
public func present<Content: View>(#ViewBuilder content: () -> Content) {
self.sheet = CRSheet(content: content)
}
public func dismiss() {
self.sheet = nil // Error 2
}
}
public struct SheetViewModifier: ViewModifier {
#ObservedObject private var sheetVM = SheetViewModel.shared
public func body(content: Content) -> some View {
content
.sheet(item: $sheetVM.sheet) { sheet in
sheet.content
}
}
}
Question
How can I resolve the errors, especially, be able to store sheets with different generic views inside the same #Published Variable and later use them inside the view modifier?
My idea was to maybe store the View as any View inside the sheet Struct`. However, I am not sure, how to use it inside the view modifier then?
Please Note: This code is part of a package and therefore needs to be accessed from outside. The generic content therefore must be passed to the present(_) Method.
Ok, so if I'm understanding this correctly, you want to handle the logic for showing different sheets from the Generic View.
First, let's build the GenericView:
struct GenericView<Content: View, FirstSheet: View, SecondSheet: View>: View {
private enum ModalSheet: Identifiable {
var id: Self { return self }
case firstSheet
case secondSheet
}
#State private var showModalSheet: ModalSheet?
private let content: () -> Content
private let firstSheet: () -> FirstSheet
private let secondSheet: () -> SecondSheet
public init(
#ViewBuilder content: #escaping () -> Content,
#ViewBuilder firstSheet: #escaping () -> FirstSheet,
#ViewBuilder secondSheet: #escaping () -> SecondSheet
) {
self.content = content
self.firstSheet = firstSheet
self.secondSheet = secondSheet
}
var body: some View {
ZStack {
content()
VStack {
Button("Show first sheet") {
showModalSheet = .firstSheet
}
Button("Show second sheet") {
showModalSheet = .secondSheet
}
}
}
.sheet(item: $showModalSheet) { modalSheet in
switch modalSheet {
case .firstSheet:
firstSheet()
case .secondSheet:
secondSheet()
}
}
}
}
Note that I created an enum inside GenericView to handle the logic for the different sheets
So now, if I want to use the generic view inside a regular view, I would do the following:
struct ContentView: View {
var body: some View {
GenericView {
Color.yellow.ignoresSafeArea()
} firstSheet: {
Text("Content for First Sheet")
} secondSheet: {
Text("Content for Second Sheet")
}
}
}
This way you can control which sheet you present as well as dismissing them without using a ViewModel.
Is this more or less what you had in mind?
I have a Card view that takes in a content parameter to display in the bordered view.
public struct Card<Content: View>: View {
private let content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: some View {
content
.padding(16)
.frame(maxWidth: .infinity)
.background(TileShape(cornerRadius: 8, backgroundColor: backgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
What I would like to do is introduce a stacked Card. Maybe something like this:
public struct Card<Content: View>: View {
private let content: Content
private let stackedContent: Content
public init(#ViewBuilder content: () -> Content, #ViewBuilder stackedContent: () -> Content) {
self.content = content()
self.stackedContent = stackedContent()
}
public var body: some View {
ZStack {
content
.padding(16)
.frame(maxWidth: .infinity)
.background(TileShape(cornerRadius: 8, backgroundColor: backgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 8))
stackedContent
/// put stuff here to align it correctly
}
}
}
While I can create this initializer, the problem comes in trying to give it content.
in my calling code I have
Tile {
Text("Card contents")
}
When I try to introduce the 2nd card, I get segment faults during compilation.
Tile(stackedContent: stackedCard) {
Text("Base Card Contents")
}
#ViewBuilder
var stackedCard: Card<some View> {
Card {
Text("Stacked Card Here")
}
}
Is my goal possible with SwiftUI? I'm limited to using iOS 14 as the target os version.
You might be tempted to ask, "Why not just use 2 Cards at the point of use and align them there?". The answer is, I am trying to replicate something in UIKit in a transition to SwiftUI.
You need to give different content types for generics (because in general they can differ)
So a fix would be
public struct Card<Content1: View, Content2: View>: View {
private let content: Content1
private let stackedContent: Content2
public init(#ViewBuilder content: () -> Content1, #ViewBuilder stackedContent: () -> Content2) {
self.content = content()
self.stackedContent = stackedContent()
}
// ...
}
There are several posts on how to pass a view to a struct using:
struct ContainerView<Content: View>: View {
let content: Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
var body: some View {
content
}
}
But how do you pass a view as a parameter in a function?
You can actually pass a view as a generic:
func functionName<T:View>(viewYouArePassing: T){}
You also can use AnyView,
struct Whatever {
var view: AnyView
}
let myWhatever = Whatever(view: AnyView(CustomView))
Is it possible in SwiftUI to have an optional #ViewBuilder closure? For example, let's say I want to develop a custom view that takes two view builder closures like this:
import SwiftUI
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: () -> Content
init(#ViewBuilder topContent: #escaping () -> Content, #ViewBuilder bottomContent: #escaping () -> Content) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
bottomContent()
}
}
}
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
}, bottomContent: {
Text("BOTTOM")
})
}
}
But I'd like the bottom view to be optional. I tried with:
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: (() -> Content)?
init(#ViewBuilder topContent: #escaping () -> Content, #ViewBuilder bottomContent: (() -> Content)? = nil) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
if bottomContent != nil {
bottomContent!()
}
}
}
}
but I get this error:
Function builder attribute 'ViewBuilder' can only be applied to a
parameter of function type.
Thanks.
Taking into account buildIf feature of ViewBuilder the following approach is possible that allows to keep ViewBuilder in init (that is preferable)
Tested & works with Xcode 11.2 / iOS 13.2
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: () -> Content?
init(#ViewBuilder topContent: #escaping () -> Content,
#ViewBuilder bottomContent: #escaping () -> Content? = { nil }) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
bottomContent()
}
}
}
So works as this one
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
}, bottomContent: {
Text("BOTTOM")
})
}
}
and this one
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
})
}
}
#JoeBayLD asked:
How would you do this if the topContent and bottomContent are different view types? I made a new generic property but when using the default 'nil' argument, any callers can't infer the content type
You can make both ViewBuilder parameters non-optional, and then handle the "no bottom content" case by making an extension where BottomContent == EmptyView:
struct TopAndBottomView<TopContent: View, BottomContent: View>: View {
let topContent: TopContent
let bottomContent: BottomContent
init(#ViewBuilder topContent: () -> TopContent,
#ViewBuilder bottomContent: () -> BottomContent) {
self.topContent = topContent()
self.bottomContent = bottomContent()
}
var body: some View {
VStack {
topContent
Spacer()
bottomContent
}
}
}
extension TopAndBottomView where BottomContent == EmptyView {
init(#ViewBuilder topContent: () -> TopContent) {
self.init(topContent: topContent, bottomContent: { EmptyView() })
}
}
// usage
TopAndBottomView(topContent: { Text("hello") })
TopAndBottomView(topContent: { Text("hello") }, bottomContent: { Text("world") })
In this fantastic post from Sundell, he suggests that we build a custom struct Unwrap to unwrap an optional value and turn it into a View, the following code is what he did in that post:
import SwiftUI
/// # Unwrap
/// unwraps a value (of type `Value`) and turns it
/// into `some View` (== `Optional<Content>`).
struct Unwrap<Value, Content: View>: View {
private let value : Value? // value to be unwrapped
private let content: (Value) -> Content // closure: turn `Value` into `Content`
init(
_ value: Value?,
#ViewBuilder content: #escaping (Value) -> Content // ⭐️ #ViewBuilder
) {
self.value = value
self.content = content
}
var body: some View {
// map: (by the closure `content`)
// nil (Optional<Value>.none) -> nil (Optional<Content>.none)
// Optional<Value>.some(Value) -> Optional<Content>.some(Content)
value.map(content) // Optional<Content>
}
}
And then I wrote some code to demonstrate how we could use Unwrap to construct our views:
import SwiftUI
// MyView
struct MyView: View {
#State private var isValue1Nil = false
#State private var isValue2Nil = false
var value1: Int? { isValue1Nil ? nil : 1}
var value2: Int? { isValue2Nil ? nil : 2}
var body: some View {
VStack {
// stack of `Unwrap`s
VStack {
// ⭐️ `Unwrap` used here.
Unwrap(value1) {
Color.red.overlay(Text("\($0)"))
}
Unwrap(value2) {
Color.orange.overlay(Text("\($0)"))
}
}.border(Color.blue, width: 3)
// toggles
HStack {
Toggle(isOn: $isValue1Nil) {
Text("value1 is nil")
}
Toggle(isOn: $isValue2Nil) {
Text("value2 is nil")
}
Spacer()
}
.padding()
.overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
} // VStack (container)
.padding()
.border(Color.gray, width: 3)
}
}
And the result is as follows:
----[ edited ]----
Or alternatively, we can make a View extension to do the job:
// view.ifLet(_:then:)
extension View {
#ViewBuilder func ifLet<Value, Content: View>(
_ value: Value?,
#ViewBuilder then modifySelfWithValue: (Self, Value) -> Content
) -> some View {
if value != nil {
modifySelfWithValue(self, value!)
} else { self }
}
}
The following is another demo on how to use this extension:
struct ContentView: View {
#State private var isNil = false
var value: Int? { isNil ? nil : 2 }
var body: some View {
VStack {
Color.red.overlay(Text("1"))
// ⭐️ view.ifLet(_:then:)
.ifLet(value) { (thisView, value) in
// construct new view with `thisView` and `value`
VStack {
thisView
Color.orange.overlay(Text("\(value)"))
}
} // view modified by `ifLet`
.border(Color.blue, width: 3)
// toggles
Toggle(isOn: $isNil) { Text("value is nil") }
.padding()
.overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
} // VStack (container)
.padding()
.border(Color.gray, width: 3).frame(height: 300)
}
}
and the result is:
Set the default value for #ViewBuilder View, in order to achieve what you're looking for:
struct AlertView<InputFields: View, Actions: View>: View {
private let inputFields: InputFields
private let actions: Actions
init(
#ViewBuilder inputFields: () -> InputFields = { EmptyView() }, <=== HERE
#ViewBuilder actions: () -> Actions = { EmptyView() } <=== HERE
) {
self.inputFields = inputFields()
self.actions = actions()
}
var body: some View {
VStack{
inputFields
actions
}
}
}
Instead of an optional #ViewBuilder parameter, a workaround is to set the default value of the parameter to EmptyView(). While this is not possible directly in the SwiftUI view struct, we can add an extension with an init() as follows:
/// View with mandatory icon view builder.
struct Hint<IconView: View>: View {
var message: String
#ViewBuilder var icon: IconView
var body: some View {
HStack {
icon.frame(width: 40, height: 40)
Text(message)
}
}
}
/// View Extensions that sets the icon view builder default to EmptyView().
extension Hint<EmptyView> {
init(message: String) {
self.message = message
self.icon = EmptyView()
}
}
Like this you can use the Hint-View either by including the icon view builder or by leaving it out (in which case the default EmptyView is used):
Hint(message: "This is a warning with icon!", icon: { Image(systemName: .exclamationmarkTriangle) })
Hint(message: "This is a warning with icon!")
It seems that you dont need the #ViewBuilder in your initializer so this would work:
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: (() -> Content)?
init(#ViewBuilder topContent: #escaping () -> Content, bottomContent: (() -> Content)? = nil) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
if bottomContent != nil {
bottomContent!()
}
}
}
}
And how to use:
TopAndBottomView(topContent: {
Text("top")
})
TopAndBottomView(topContent: {
Text("top")
}, bottomContent: {
Text("optional bottom")
})
With following code:
struct HomeView: View {
var body: some View {
NavigationView {
List(dataTypes) { dataType in
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
}
}
}
}
What's weird, when HomeView appears, NavigationLink immediately loads the AnotherView. As a result, all AnotherView dependencies are loaded as well, even though it's not visible on the screen yet. The user has to click on the row to make it appear.
My AnotherView contains a DataSource, where various things happen. The issue is that whole DataSource is loaded at this point, including some timers etc.
Am I doing something wrong..? How to handle it in such way, that AnotherView gets loaded once the user presses on that HomeViewRow?
The best way I have found to combat this issue is by using a Lazy View.
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Then the NavigationLink would look like this. You would place the View you want to be displayed inside ()
NavigationLink(destination: NavigationLazyView(DetailView(data: DataModel))) { Text("Item") }
EDIT: See #MwcsMac's answer for a cleaner solution which wraps View creation inside a closure and only initializes it once the view is rendered.
It takes a custom ForEach to do what you are asking for since the function builder does have to evaluate the expression
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
for each visible row to be able to show HomeViewRow(dataType:), in which case AnotherView() must be initialized too.
So to avoid this a custom ForEach is necessary.
import SwiftUI
struct LoadLaterView: View {
var body: some View {
HomeView()
}
}
struct DataType: Identifiable {
let id = UUID()
var i: Int
}
struct ForEachLazyNavigationLink<Data: RandomAccessCollection, Content: View, Destination: View>: View where Data.Element: Identifiable {
var data: Data
var destination: (Data.Element) -> (Destination)
var content: (Data.Element) -> (Content)
#State var selected: Data.Element? = nil
#State var active: Bool = false
var body: some View {
VStack{
NavigationLink(destination: {
VStack{
if self.selected != nil {
self.destination(self.selected!)
} else {
EmptyView()
}
}
}(), isActive: $active){
Text("Hidden navigation link")
.background(Color.orange)
.hidden()
}
List{
ForEach(data) { (element: Data.Element) in
Button(action: {
self.selected = element
self.active = true
}) { self.content(element) }
}
}
}
}
}
struct HomeView: View {
#State var dataTypes: [DataType] = {
return (0...99).map{
return DataType(i: $0)
}
}()
var body: some View {
NavigationView{
ForEachLazyNavigationLink(data: dataTypes, destination: {
return AnotherView(i: $0.i)
}, content: {
return HomeViewRow(dataType: $0)
})
}
}
}
struct HomeViewRow: View {
var dataType: DataType
var body: some View {
Text("Home View \(dataType.i)")
}
}
struct AnotherView: View {
init(i: Int) {
print("Init AnotherView \(i.description)")
self.i = i
}
var i: Int
var body: some View {
print("Loading AnotherView \(i.description)")
return Text("hello \(i.description)").onAppear {
print("onAppear AnotherView \(self.i.description)")
}
}
}
I had the same issue where I might have had a list of 50 items, that then loaded 50 views for the detail view that called an API (which resulted in 50 additional images being downloaded).
The answer for me was to use .onAppear to trigger all logic that needs to be executed when the view appears on screen (like setting off your timers).
struct AnotherView: View {
var body: some View {
VStack{
Text("Hello World!")
}.onAppear {
print("I only printed when the view appeared")
// trigger whatever you need to here instead of on init
}
}
}
For iOS 14 SwiftUI.
Non-elegant solution for lazy navigation destination loading, using view modifier, based on this post.
extension View {
func navigate<Value, Destination: View>(
item: Binding<Value?>,
#ViewBuilder content: #escaping (Value) -> Destination
) -> some View {
return self.modifier(Navigator(item: item, content: content))
}
}
private struct Navigator<Value, Destination: View>: ViewModifier {
let item: Binding<Value?>
let content: (Value) -> Destination
public func body(content: Content) -> some View {
content
.background(
NavigationLink(
destination: { () -> AnyView in
if let value = self.item.wrappedValue {
return AnyView(self.content(value))
} else {
return AnyView(EmptyView())
}
}(),
isActive: Binding<Bool>(
get: { self.item.wrappedValue != nil },
set: { newValue in
if newValue == false {
self.item.wrappedValue = nil
}
}
),
label: EmptyView.init
)
)
}
}
Call it like this:
struct ExampleView: View {
#State
private var date: Date? = nil
var body: some View {
VStack {
Text("Source view")
Button("Send", action: {
self.date = Date()
})
}
.navigate(
item: self.$date,
content: {
VStack {
Text("Destination view")
Text($0.debugDescription)
}
}
)
}
}
I was recently struggling with this issue (for a navigation row component for forms), and this did the trick for me:
#State private var shouldShowDestination = false
NavigationLink(destination: DestinationView(), isActive: $shouldShowDestination) {
Button("More info") {
self.shouldShowDestination = true
}
}
Simply wrap a Button with the NavigationLink, which activation is to be controlled with the button.
Now, if you're to have multiple button+links within the same view, and not an activation State property for each, you should rely on this initializer
/// Creates an instance that presents `destination` when `selection` is set
/// to `tag`.
public init<V>(destination: Destination, tag: V, selection: Binding<V?>, #ViewBuilder label: () -> Label) where V : Hashable
https://developer.apple.com/documentation/swiftui/navigationlink/3364637-init
Along the lines of this example:
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) {
Button("Tap to show second") {
self.selection = "Second"
}
}
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) {
Button("Tap to show third") {
self.selection = "Third"
}
}
}
.navigationBarTitle("Navigation")
}
}
}
More info (and the slightly modified example above) taken from https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui (under "Programmatic navigation").
Alternatively, create a custom view component (with embedded NavigationLink), such as this one
struct FormNavigationRow<Destination: View>: View {
let title: String
let destination: Destination
var body: some View {
NavigationLink(destination: destination, isActive: $shouldShowDestination) {
Button(title) {
self.shouldShowDestination = true
}
}
}
// MARK: Private
#State private var shouldShowDestination = false
}
and use it repeatedly as part of a Form (or List):
Form {
FormNavigationRow(title: "One", destination: Text("1"))
FormNavigationRow(title: "Two", destination: Text("2"))
FormNavigationRow(title: "Three", destination: Text("3"))
}
In the destination view you should listen to the event onAppear and put there all code that needs to be executed only when the new screen appears. Like this:
struct DestinationView: View {
var body: some View {
Text("Hello world!")
.onAppear {
// Do something important here, like fetching data from REST API
// This code will only be executed when the view appears
}
}
}