How do I make a reusable SwiftUI View with a body I can fill in like List? - swift

I want to make a generic SwiftUI View that I can plug in and out of my controls. More than just passing it data though, I want to be able to pass in subviews like you would to a List like so:
List{
Text("This works")
Text("Hello World")
Text("This works")
}
MyClass {
Text("This works")
Text("Hello World")
Text("This works")
}
Is this possible? And if so, how would I go about declaring this class?

You can use the #ViewBuilder property wrapper:
struct CustomView<Content: View>: View {
var content: () -> Content
init(#ViewBuilder _ content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
content()
}
}
}
struct ContentView: View {
var body: some View {
CustomView {
Text("hello")
Text("world")
}
}
}

Related

How can I feed ForEach with items of TupleView in SwiftUI?

I am trying to build a custom view which adds space between each content of TupleView, here is my pseudocode until now! What I should do more in codes to make it happen?
struct CustomSpacerView<Content1: View, Content2: View, Content3: View>: View {
#ViewBuilder let content: () -> TupleView<(Content1, Content2, Content3)>
var body: some View {
ForEach(Array(arrayLiteral: content().value).indices, id:\.self) { index in
Spacer()
content().value.index
}
Spacer()
}
}
use case:
struct ContentView: View {
var body: some View {
CustomSpacerView {
Text("Hello, World!")
Text("Hello, World!")
Text("Hello, World!")
}
}
}
You can use my ViewExtractor package. Here is an example with your code:
struct CustomSpacerView: View {
private let views: [AnyView]
// For 2 or more views
init<Views>(#ViewBuilder content: TupleContent<Views>) {
views = ViewExtractor.getViews(from: content)
}
// For 0 or 1 view
init<Content: View>(#ViewBuilder content: NormalContent<Content>) {
views = ViewExtractor.getViews(from: content)
}
var body: some View {
VStack {
ForEach(views.indices) { index in
Spacer()
views[index]
}
Spacer()
}
}
}
This has the same usage in ContentView.
As an overview, this works by getting a Mirror of the content, converting the raw bytes to a 'fake' type representing a View, then convert this Any type to AnyView.

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

SwiftUI Custom View Wrapper

I'm trying to figure out how to make a custom view which wraps normal SwiftUI content like this...I'm not sure if I use UIViewRepresentable or what. Please help.
CustomView { x in
VStack { ... }
}
you can use this down code and give your SwiftUI contents to content of CustomView:
struct CustomView <Content: View>: View {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) { self.content = content }
var body: some View {
content() // <<: Do anything you want with your imported View here.
}
}
The use case:
struct ContentView: View {
var body: some View {
CustomView(content: {
VStack { Text("Hello, world!").padding() }
})
}
}

How can I count #ViewBuilder Views in SwiftUI?

I want to to know how can I count my inPutView in this example of code, the code working like this, it takes some views and gave a background color and counts the count of view, thanks for help.
struct ContentView: View {
var body: some View {
ModelView(inputView: {
Text("Hello, world!").padding()
Text("Hello, world!").padding()
})
}
}
struct ModelView<Content: View>: View {
var inPutView: () -> Content
init(#ViewBuilder inputView: #escaping () -> Content) { self.inPutView = inputView }
var body: some View {
VStack {
inPutView()
}
.background(Color.green)
Text("count of inPutViews: 2").padding() // Here: How can I found out the count of inPutView?
}
}
update:
struct ContentView: View {
var inputViews: [AnyView] = [AnyView(Text("Hello, world!").padding()), AnyView(Text("Hello, world!").padding())]
var body: some View {
ModelView2(inputViews: inputViews)
}
}
struct ModelView2: View {
var inputViews: [AnyView]
var body: some View {
VStack {
ForEach(inputViews.indices, id:\.self) { index in
inputViews[index]
}
}
.background(Color.green)
Text("count of inPutViews: \(inputViews.count)")
.padding()
}
}
It's not possible to detect the count if individual views inside a #ViewBuilder closure. The #ViewBuilder creates one resulting view and your inPutView is treated as a single view.
A possible solution is to pass the [AnyView] array as the input of ModelView. But then AnyView doesn't conform to Hashable nor Identifiable, so you can't use it in a ForEach.
In your case you can create a separate struct conforming to Identifiable:
struct AnyViewItem: Identifiable {
let id = UUID()
let view: AnyView
}
and populate ModelView with an array of AnyViewItem:
struct ModelView: View {
var inputViews: [AnyViewItem]
var body: some View {
VStack {
ForEach(inputViews) {
$0.view
}
}
.background(Color.green)
Text("count of inPutViews: \(inputViews.count)")
.padding()
}
}
Then, you can use it in your main view like this:
struct ContentView: View {
var body: some View {
ModelView(
inputViews: [
Text("Hello, world!").padding(),
Text("Hello, world!").padding(),
]
.map {
AnyViewItem(view: AnyView($0))
}
)
}
}
Alternatively, as suggested in the comments, in this case you can make inputViews an [AnyView] array and iterate through its indices:
ForEach(inputViews.indices, id: \.self) {
inputViews[$0]
}

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