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

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.

Related

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

SwiftUI TabView with List not refreshing after objected deleted from / added to Core Data

Description:
When an object in a list (created from a fetchrequest) is deleted from a context, and the context is saved, the list does not properly update.
Error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value (Thrown on line 5 below)
struct DetailView: View {
#ObservedObject var event: Event
var body: some View {
Text("\(event.timestamp!, formatter: dateFormatter)")
.navigationBarTitle(Text("Detail"))
}
}
Steps to reproduce:
Create a new Master Detail App project with SwiftUI and Core Data.
In the ContentView, set the body to a TabView with the first tab being the prebuilt NavigationView, and add a second arbitrary tab.
struct ContentView: View {
#Environment(\.managedObjectContext)
var viewContext
var body: some View {
TabView {
NavigationView {
MasterView()
.navigationBarTitle(Text("Master"))
.navigationBarItems(
leading: EditButton(),
trailing: Button(
action: {
withAnimation { Event.create(in: self.viewContext) }
}
) {
Image(systemName: "plus")
}
)
Text("Detail view content goes here")
.navigationBarTitle(Text("Detail"))
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.tabItem { Text("Main") }
Text("Other Tab")
.tabItem { Text("Other Tab") }
}
}
}
Add a few items. Interact with those items in any way.
Change tabs.
Change back to Main Tab.
Attempt to delete an item.
I found a pure SwiftUI working solution:
/// This View that init the content view when selection match tag.
struct SyncView<Content: View>: View {
#Binding var selection: Int
var tag: Int
var content: () -> Content
#ViewBuilder
var body: some View {
if selection == tag {
content()
} else {
Spacer()
}
}
}
You can use it then in this way:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
SyncView(selection: $selection, tag: 0) {
ViewThatNeedsRefresh()
}
.tabItem { Text("First") }
.tag(0)
Text("Second View")
.font(.title)
.tabItem { Text("Second") }
.tag(1)
}
}
}
You can use the SyncView for each view that needs a refresh.

Creating BaseView class in SwiftUI

Lately started learning/developing apps with SwiftUI and seems pretty easy to build the UI components. However, struggling creating a BaseView in SwiftUI. My idea is to have the common UI controls like background , navigation , etc in BaseView and just subclass other SwiftUI views to have the base components automatically.
Usually you want to either have a common behaviour or a common style.
1) To have a common behaviour: composition with generics
Let's say we need to create a BgView which is a View with a full screen image as background. We want to reuse BgView whenever we want. You can design this situation this way:
struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
let content: Content
var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}
You can use BgView wherever you need it and you can pass it all the content you want.
//1
struct ContentView: View {
var body: some View {
BgView(content: Text("Hello!"))
}
}
//2
struct ContentView: View {
var body: some View {
BgView(content:
VStack {
Text("Hello!")
Button(action: {
print("Clicked")
}) {
Text("Click me")
}
}
)
}
}
2) To have a common behaviour: composition with #ViewBuilder closures
This is probably the Apple preferred way to do things considering all the SwiftUI APIs. Let's try to design the example above in this different way
struct BgView<Content>: View where Content: View {
private let bgImage = Image.init(systemName: "m.circle.fill")
private let content: Content
public init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body : some View {
ZStack {
bgImage
.resizable()
.opacity(0.2)
content
}
}
}
struct ContentView: View {
var body: some View {
BgView {
Text("Hello!")
}
}
}
This way you can use BgView the same way you use a VStack or List or whatever.
3) To have a common style: create a view modifier
struct MyButtonStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
.shadow(radius: 3)
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Button(action: {
print("Button1 clicked")
}) {
Text("Button 1")
}
.modifier(MyButtonStyle())
Button(action: {
print("Button2 clicked")
}) {
Text("Button 2")
}
.modifier(MyButtonStyle())
Button(action: {
print("Button3 clicked")
}) {
Text("Button 3")
}
.modifier(MyButtonStyle())
}
}
}
These are just examples but usually you'll find yourself using one of the above design styles to do things.
EDIT: a very useful link about #functionBuilder (and therefore about #ViewBuilder) https://blog.vihan.org/swift-function-builders/
I got a idea about how to create a BaseView in SwiftUI for common usage in other screen
By the way
Step .1 create ViewModifier
struct BaseScene: ViewModifier {
/// Scene Title
var screenTitle: String
func body(content: Content) -> some View {
VStack {
HStack {
Spacer()
Text(screenTitle)
.font(.title)
.foregroundColor(.white)
Spacer()
}.padding()
.background(Color.blue.opacity(0.8))
content
}
}
}
Step .2 Use that ViewModifer in View
struct BaseSceneView: View {
var body: some View {
VStack {
Spacer()
Text("Home screen")
.font(.title)
Spacer()
}
.modifier(BaseScene(screenTitle: "Screen Title"))
}
}
struct BaseSceneView_Previews: PreviewProvider {
static var previews: some View {
Group {
BaseSceneView()
}
}
}
Your Output be like:

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

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

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