SwiftUI - Can I define an empty initializer for a generic view? - swift

I have a generic view with an optional #ViewBuilder.
I want to have two initializers, one is responsible for setting the #ViewBuilder and another which should serve as an empty default.
struct OptionalViewBuilder<Content: View>: View {
let content: (() -> Content)?
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
init() {
self.content = nil
}
var body: some View {
VStack {
Text("Headline")
Divider()
content?()
}
}
}
If I initialize the view with the ViewBuilder parameter, it works of course. However, when I use the empty initializer, I get the error message:
Generic parameter 'Content' could not be inferred
struct ContentView: View {
var body: some View {
OptionalViewBuilder {
Text("Text1")
Text("Text2")
}
OptionalViewBuilder() // <-- Generic parameter 'Content' could not be inferred
}
}
I know that Swift needs explicit types at runtime, but is there a way to make the default initializer work anyway?

Because OptionalViewBuilder is generic with respect to its Content type, you'd need to define that type as something in your default case. This could be EmptyView, if you don't want to render anything.
To do that, you'd need an init which is constrained to Content == EmptyView, and the content property need not be optional:
struct OptionalViewBuilder<Content: View>: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
init() where Content == EmptyView {
self.init(content: { EmptyView() })
}
var body: some View {
VStack {
Text("Headline")
Divider()
content()
}
}
}

I can make the compile error go away by adding an empty closure, like this:
struct ContentView: View {
var body: some View {
OptionalViewBuilder {
Text("Text1")
Text("Text2")
}
OptionalViewBuilder() {} // I added an empty closure
}
}
Does this get you what you need?

Related

How can I change return view type from <TupleView<(View, View)>> to <_> for initialization?

I have a view that return a view in form of <TupleView<(View, View)>>, like this:
Also I have a view that return a view in form of <_>, like this:
With knowing that both way works and my wished way is using TupleView under cover but having the look of <_>, I tried to work on MyView3, as you can see in my tried codes I want this look of (content: () -> _) -> MyView3<_> for being more simple and understandable, but i need TupleView way initialization undercover. I got 3 error's for MyView3 to make it work. Do you think is there a way for my goal?
For more information see this codes:
struct ContentView: View {
var body: some View {
MyView1(content: {
Text("Text 1").fixedSize()
Text("Text 2").fixedSize()
})
.padding()
MyView2(tupleViewContent: {
Text("Text 3").fixedSize()
Text("Text 4").fixedSize()
})
.padding()
// This part has issue!
MyView3(content: {
Text("Text 5").fixedSize()
Text("Text 6").fixedSize()
})
.padding()
}
}
struct MyView1<Content: View>: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
content()
}
}
struct MyView2<Content: View>: View {
let content: () -> Content
init<Content1: View, Content2: View>(#ViewBuilder tupleViewContent: #escaping () -> Content) where Content == TupleView<(Content1, Content2)> {
self.content = tupleViewContent
}
var body: some View {
content()
}
}
struct MyView3<Content: View>: View {
let content: () -> Content
private init<Content1: View, Content2: View>(#ViewBuilder tupleViewContent: #escaping () -> Content) where Content == TupleView<(Content1, Content2)> {
self.content = tupleViewContent
}
init(#ViewBuilder content: #escaping () -> Content) {
//Error:
// Generic parameter 'Content1' could not be inferred
// Generic parameter 'Content2' could not be inferred
// Initializer 'init(tupleViewContent:)' requires the types 'Content' and 'TupleView<(Content1, Content2)>' be equivalent
self.init(tupleViewContent: content)
}
var body: some View {
content()
}
}
The generics is resolved at compilation time, so init/s should go from more specific to more common (finally to the designated one).
In your case it should be like (tested with Xcode 13.2)
struct MyView3<Content: View>: View {
let content: () -> Content
init<Content1: View, Content2: View>(#ViewBuilder tupleViewContent: #escaping () -> Content) where Content == TupleView<(Content1, Content2)> {
self.init(content: tupleViewContent)
}
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
content()
}
}

How to pass view of different type to another view in SwiftUI?

So I have this TestView which accepts headerContent and bodyContent,
struct TestView<Content: View>: View {
var headerContent: (() -> Content)? = nil
let bodyContent: () -> Content
var body: some View {
VStack {
headerContent?()
bodyContent()
}
}
}
And I use it as,
struct ContentView: View {
var body: some View {
TestView(headerContent: {
Text("HeaderContent")
}) {
ScrollView {
}
}
}
}
But I get the following error,
Cannot convert value of type 'ScrollView<EmptyView>' to closure result type 'Text'
What am I missing?
You need to have two View generics, since headerContent and bodyContent are not the same.
Without this, you are saying there is some concrete type Content that conforms to View. However, both Text and ScrollView are different types, not the same.
Code:
struct TestView<HeaderContent: View, BodyContent: View>: View {
var headerContent: (() -> HeaderContent)? = nil
let bodyContent: () -> BodyContent
var body: some View {
VStack {
headerContent?()
bodyContent()
}
}
}

How to create a struct parameter that is a closure with input, returning `some View` instead of `AnyView` in SwiftUI?

Current situation:
I'm trying to create a reusable view in SwiftUI that returns another view passed into it. At the moment I'm using a closure that returns AnyView, as the parameter.
When I initialize the reusable view I pass on a custom view wrapped inside AnyView().
Goal:
Ideally I'd like a way to omit the AnyView() wrapper and just pass on my CustomViewA or CustomViewB as is.
My attempts and issues:
I tried changing the closure parameter to return some View but I'm getting the following error:
'some' types are only implemented for the declared type of properties and subscripts and the return type of functions
I've seen a few attempts on S.O. to work around that by somehow using a generic parameter but I'm not sure how to implement this in my case or if it's even possible. Also important to note, the closure needs to take a String as its own input parameter.
Example code:
import SwiftUI
struct ReusableView: View {
let outputView: (String) -> AnyView
var body: some View {
outputView("Hello World")
}
}
struct CustomViewA: View {
let label: String
init(_ label: String) {
self.label = label
}
var body: some View {
return Text(label)
}
}
struct CustomViewB: View {
let label: String
init(_ label: String) {
self.label = label
}
var body: some View {
return HStack{
Text(label)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ReusableView() {
AnyView(CustomViewA($0))
}
}
}
You just need to make ReusableView generic and declare the return type of your closure as the generic type.
struct ReusableView<Output: View>: View {
let outputView: (String) -> Output
var body: some View {
outputView("Hello World")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ReusableView() {
CustomViewA($0)
}
}
}

SwiftUI list empty state view/modifier

I was wondering how to provide an empty state view in a list when the data source of the list is empty. Below is an example, where I have to wrap it in an if/else statement. Is there a better alternative for this, or is there a way to create a modifier on a List that'll make this possible i.e. List.emptyView(Text("No data available...")).
import SwiftUI
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
} else {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
}
}
struct EmptyListExample_Previews: PreviewProvider {
static var previews: some View {
EmptyListExample(objects: [])
}
}
I quite like to use an overlay attached to the List for this because it's quite a simple, flexible modifier:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.overlay(Group {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
}
})
}
}
}
It has the advantage of being nicely centred & if you use larger placeholders with an image, etc. they will fill the same area as the list.
One of the solutions is to use a #ViewBuilder:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
listView
}
#ViewBuilder
var listView: some View {
if objects.isEmpty {
emptyListView
} else {
objectsListView
}
}
var emptyListView: some View {
Text("Oops, loos like there's no data...")
}
var objectsListView: some View {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
You can create a custom modifier that substitutes a placeholder view when your list is empty. Use it like this:
List(items) { item in
Text(item.name)
}
.emptyPlaceholder(items) {
Image(systemName: "nosign")
}
This is the modifier:
struct EmptyPlaceholderModifier<Items: Collection>: ViewModifier {
let items: Items
let placeholder: AnyView
#ViewBuilder func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
extension View {
func emptyPlaceholder<Items: Collection, PlaceholderView: View>(_ items: Items, _ placeholder: #escaping () -> PlaceholderView) -> some View {
modifier(EmptyPlaceholderModifier(items: items, placeholder: AnyView(placeholder())))
}
}
I tried #pawello2222's approach, but the view didn't get rerendered if the passed objects' content change from empty(0) to not empty(>0), or vice versa, but it worked if the objects' content was always not empty.
Below is my approach to work all the time:
struct SampleList: View {
var objects: [IdentifiableObject]
var body: some View {
ZStack {
Empty() // Show when empty
List {
ForEach(objects) { object in
// Do something about object
}
}
.opacity(objects.isEmpty ? 0.0 : 1.0)
}
}
}
You can make ViewModifier like this for showing the empty view. Also, use View extension for easy use.
Here is the demo code,
//MARK: View Modifier
struct EmptyDataView: ViewModifier {
let condition: Bool
let message: String
func body(content: Content) -> some View {
valideView(content: content)
}
#ViewBuilder
private func valideView(content: Content) -> some View {
if condition {
VStack{
Spacer()
Text(message)
.font(.title)
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
Spacer()
}
} else {
content
}
}
}
//MARK: View Extension
extension View {
func onEmpty(for condition: Bool, with message: String) -> some View {
self.modifier(EmptyDataView(condition: condition, message: message))
}
}
Example (How to use)
struct EmptyListExample: View {
#State var objects: [Int] = []
var body: some View {
NavigationView {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.onEmpty(for: objects.isEmpty, with: "Oops, loos like there's no data...") //<--- Here
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("Add") {
objects = [1,2,3,4,5,6,7,8,9,10]
}
Button("Empty") {
objects = []
}
}
}
}
}
}
In 2021 Apple did not provide a List placeholder out of the box.
In my opinion, one of the best way to make a placeholder, it's creating a custom ViewModifier.
struct EmptyDataModifier<Placeholder: View>: ViewModifier {
let items: [Any]
let placeholder: Placeholder
#ViewBuilder
func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.modifier(EmptyDataModifier(
items: countries,
placeholder: Text("No Countries").font(.title)) // Placeholder. Can set Any SwiftUI View
)
}
}
Also via extension can little bit improve the solution:
extension List {
func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
modifier(EmptyDataModifier(items: items, placeholder: placeholder))
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.emptyListPlaceholder(
countries,
AnyView(ListPlaceholderView()) // Placeholder
)
}
}
If you are interested in other ways you can read the article

SwiftUI can not return View on function

I'm trying return View based selected menu item on function. But it throws error:
Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
There my code:
enum MenuItem {
case Main
case Report
}
struct Menu: View {
#State var activeItem: MenuItem = .Main
private func getActiveView() -> View {
switch activeItem {
case .Main:
return DashboardView()
case .Report:
return ReportView()
}
}
var body: some View {
...
getActiveView()
...
}
}
struct DashboardView: View {
var body: some View {
Text("Contact")
}
}
struct ReportView: View {
var body: some View {
Text("Contact")
}
}
Im new on SwiftUI. Any ideas how to return View?
SwiftUI 2
Here is a solution tested with Xcode 12b / iOS 14
struct Menu: View {
#State var activeItem: MenuItem = .Main
// make function ViewBuilder
#ViewBuilder
private func getActiveView() -> some View {
switch activeItem {
case .Main:
DashboardView() // don't use 'return' as it disables ViewBuilder
case .Report:
ReportView()
}
}
var body: some View {
getActiveView()
}
}
SwiftUI gives us a type-erased wrapper called AnyView that we can return.
Tested Solution:
struct Menu: View {
#State var activeItem: MenuItem = .Main
func getActiveView() -> some View {
switch activeItem {
case .Main:
return AnyView(DashboardView())
case .Report:
return AnyView(ReportView())
}
}
var body: some View {
getActiveView()
}
}
Note: type-erased wrapper effectively forces Swift to forget about what specific
type is inside the AnyView, allowing them to look like they are the
same thing. This has a performance cost, though, so don’t use it
often.
For more information you can refer to the this cool article: https://www.hackingwithswift.com/quick-start/swiftui/how-to-return-different-view-types