SwiftUI extension generic where clause not matching - swift

I have this simple ThemedNavigationButton view that handles some stuff whilst creating a NavigationLink (The inner workings aren't important):
struct ThemedNavigationButton<Destination, L>: View where Destination: View, L: View {
var destination: () -> Destination
var label: () -> L
var body: some View {
...
}
}
I use L here and not Label because I need to use the SwiftUI Label
next
which I use like this:
ThemedNavigationButton {
NextView()
} label: {
Label {
Text("Some text")
} icon: {
Image(systemName: "check")
.foregroundColor(theme.tint)
}
}
I want to create a simpler initialiser when it is used in this manner, so I came up with this:
extension ThemedNavigationButton where L == Label<Text, Image> {
init(text: String, systemImage: String, destination: #escaping () -> Destination) {
self.destination = destination
self.label = {
Label {
Text(text + text)
} icon: {
Image(systemName: systemImage)
}
}
}
}
which works great like this:
ThemedNavigationButton(text: "Some text", systemImage: "check") { NextView() }
The problem I have, is as soon as I add the image tint colour to the new initialiser I get the error:
Cannot convert value of type 'some View' to closure result type
'Image'
I'm guessing because my Image is no longer an Image. But what is it and how do I declare it. I can't use some View which is what the compiler is telling me it is.

Generics specialisation requires concrete types, so here is a possible approach to resolve this situation - introduce custom wrapper/proxy type and use it in extension.
Tested with Xcode 13.2
struct MyLabel: View { // new wrapper type
let text: String
let systemImage: String
var tintColor = Color.green
var body: some View {
Label {
Text(text + text)
} icon: {
Image(systemName: systemImage)
.foregroundColor(tintColor)
}
}
}
extension ThemedNavigationButton where L == MyLabel { // << here !!
init(text: String, systemImage: String, destination: #escaping () -> Destination) {
self.destination = destination
self.label = {
MyLabel(text: text, systemImage: systemImage)
}
}
}

To use the following notation:
ThemedNavigationButton(text: "Some text", systemImage: "check") { NextView() }
you can create a View with just one generic type for Destination, because the Label will receive basic String types.
You can set ThemedNavigationButton as follows:
// Only one generic type needed
struct ThemedNavigationButton<Destination: View>: View {
// Constants for the label (make them appear before "destination")
let text: String
let systemImage: String
// Destination view
var destination: () -> Destination
var body: some View {
// Show the views the way you want
VStack {
destination()
// Use the label this way
Label {
Text(text)
} icon: {
Image(systemName: systemImage)
}
}
}
}
Customise the body the way you need.
You can use it calling:
ThemedNavigationButton(text: "Some text", systemImage: "check") { NextView() }

Related

SwiftUI - Adding a keyboard toolbar button for only one TextField adds it for all TextFields

Background
I have two TextFields, one of which has a keyboard type of .decimalPad.
Given that there is no 'Done' button when using a decimal pad keyboard to close it, rather like the return key of the standard keyboard, I would like to add a 'Done' button within a toolbar above they keypad only for the decimal keyboard in SwiftUI.
Problem
Adding a .toolbar to any TextField for some reason adds it to all of the TextFields instead! I have tried conditional modifiers, using focussed states and checking for the Field value (but for some reason it is not set when checking, maybe an ordering thing?) and it still adds the toolbar above the keyboard for both TextFields.
How can I only have a .toolbar for my single TextField that accepts digits, and not for the other TextField that accepts a string?
Code
Please note that I've tried to make a minimal example that you can just copy and paste into Xcode and run it for yourself. With Xcode 13.2 there are some issues with displaying a keyboard for TextFields for me, especially within a sheet, so maybe simulator is required to run it properly and bring up the keyboard with cmd+K.
import SwiftUI
struct TestKeyboard: View {
#State var str: String = ""
#State var num: Float = 1.2
#FocusState private var focusedField: Field?
private enum Field: Int, CaseIterable {
case amount
case str
}
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
// I'm only adding .toolbar here, but it still shows for the one above..
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
Spacer()
}
}
}
class FloatNumberFormatter: NumberFormatter {
override init() {
super.init()
self.numberStyle = .currency
self.currencySymbol = "€"
self.minimumFractionDigits = 2
self.maximumFractionDigits = 2
self.locale = Locale.current
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
// So you can preview it quickly
struct TestKeyboard_Previews: PreviewProvider {
static var previews: some View {
TestKeyboard()
}
}
Try to make toolbar content conditional and move toolbar outside, like below. (No possibility to test now - just idea)
Note: test on real device
var body: some View {
VStack {
Spacer()
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
TextField("", value: $num, formatter: FloatNumberFormatter())
.focused($focusedField, equals: .amount)
.keyboardType(.decimalPad)
Spacer()
}
.toolbar { // << here !!
ToolbarItem(placement: .keyboard) {
if field == .amount { // << here !!
Button("Done") {
focusedField = nil
}
}
}
}
}
Using introspect you can do something like this in any part in your View:
.introspectTextField { textField in
textField.inputAccessoryView = UIView.getKeyboardToolbar {
textField.resignFirstResponder()
}
}
and for the getKeyboardToolbar:
extension UIView {
static func getKeyboardToolbar( _ callback: #escaping (()->()) ) -> UIToolbar {
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44))
let doneButton = CustomBarButtonItem(title: "Done".localized, style: .done) { _ in
callback()
}
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolBar.items = [space, doneButton]
return toolBar
}
}
and for the CustomBarButtonItem this is a bar button item that takes a closure
import UIKit
class CustomBarButtonItem: UIBarButtonItem {
typealias ActionHandler = (UIBarButtonItem) -> Void
private var actionHandler: ActionHandler?
convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionHandler?) {
self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
#objc func barButtonItemPressed(sender: UIBarButtonItem) {
actionHandler?(sender)
}
}
I tried it a lot but I ended up in the below one.
.focused($focusedField, equals: .zip)
.toolbar{
ToolbarItem(placement: .keyboard) {
switch focusedField{
case .zip:
HStack{
Spacer()
Button("Done"){
focusedField = nil
}
}
default:
Text("")
}
}
}
This is my solution:
func textFieldSection(title: String,
text: Binding<String>,
keyboardType: UIKeyboardType,
focused: FocusState<Bool>.Binding,
required: Bool) -> some View {
TextField(
vm.placeholderText(isRequired: required),
text: text
)
.focused(focused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focused.wrappedValue {
Spacer()
Button {
focused.wrappedValue = false
} label: {
Text("Done")
}
}
}
}
}
For my project I have five TextField views on one View, so I created this method in the View's extension.
I pass the unique FocusState<Bool>.Binding value and use it in the ToolbarItemGroup closure to determine if we should display the content (Spacer, Button). If the particular TextField is focused, we display the toolbar content (all other unfocused TextFields won't).
Number Pad return solution in SwiftUI,
Tool bar button over keyboard,
Focused Field
struct NumberOfBagsView:View{
#FocusState var isInputActive: Bool
#State var phoneNumber:String = ""
TextField("Place holder",
text: $phoneNumber,
onEditingChanged: { _ in
//do actions while writing something in text field like text limit
})
.keyboardType(.numberPad)
.focused($isInputActive)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
print("done clicked")
isInputActive = false
}
}
}
}
I've found wrapping each TextField in its own NavigationView gives each its own context and thus a unique toolbar. It feels not right and I've seen constraint warnings in the console. Use something like this:
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
NavigationView {
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
}
// I'm only adding .toolbar here, but it still shows for the one above..
NavigationView {
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
Spacer()
}
}
There is work. But the other TextField will still display toolbar.
--- update ---
Hi, I updated the code to use ViewModifier to make the code easier to use and this time the code does compile and run >_<
struct ToolbarItemWithShow<Toolbar>: ViewModifier where Toolbar: View {
var show: Bool
let toolbar: Toolbar
let placement: ToolbarItemPlacement
func body(content: Content) -> some View {
content.toolbar {
ToolbarItemGroup(placement: placement) {
ZStack(alignment: .leading) {
if show {
HStack { toolbar }
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
extension View {
func keyboardToolbar<ToolBar>(_ show: Bool, #ViewBuilder toolbar: () -> ToolBar) -> some View where ToolBar: View {
modifier(ToolbarItemWithShow(show: show, toolbar: toolbar(), placement: .keyboard))
}
}
struct ContentView: View {
private enum Field: Hashable {
case name
case age
case gender
}
#State var name = "Ye"
#State var age = "14"
#State var gender = "man"
#FocusState private var focused: Field?
var body: some View {
VStack {
TextField("Name", text: $name)
.focused($focused, equals: .name)
.keyboardToolbar(focused == .name) {
Text("Input Name")
}
TextField("Age", text: $age)
.focused($focused, equals: .age)
.keyboardToolbar(focused == .age) {
Text("Input Age")
}
TextField("Gender", text: $gender)
.focused($focused, equals: .gender)
.keyboardToolbar(focused == .gender) {
Text("Input Sex")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
--- old ---
struct TextFieldWithToolBar<Label, Toolbar>: View where Label: View, Toolbar: View {
#Binding public var text: String
public let toolbar: Toolbar?
#FocusState private var focus: Bool
var body: some View {
TextField(text: $text, label: { label })
.focused($focus)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
ZStack(alignment: .leading) {
if focus {
HStack {
toolbar
Spacer()
Button("Done") {
focus = false
}
}
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
TextFieldWithToolBar("Name", text: $name)
TextFieldWithToolBar("Name", text: $name){
Text("Only Brand")
}
TextField("Name", "Set The Name", text: $name)
with Done with Toolbar without

SwiftUI passing ViewModifier as parameter

I am creating a custom TextField view that consists of multiple adornment views. I want to be able to set up the inner TextField with view modifiers such as keyboard, capitalization, etc. that apply just to that sub-view.
Rather than creating properties for each of these I figured the best way would be to pass in a single optional ViewModifier parameter and use it something like this:
struct MySuperTextField: View {
var vm: ViewModifier?
var body: some View {
TextField(...)
.modifier( vm ?? EmptyModifier() )
// ... more views here
}
}
This doesn't work due to the associatedType in ViewModifier. Alas there is no such thing as AnyViewModifier either (and I could't figure out how to make one that worked).
Anyone manage to do something like this? I couldn't find anything searching the web.
An example would be
struct LastNameModifier: ViewModifier {
func body(content: Content) -> some View {
content
.autocapitalization(.words)
.textContentType(.familyName)
.backgroundColor(.green)
// ... anything else specific to names
}
}
struct EmailModifier: ViewModifier {
func body(content: Content) -> some View {
content
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.backgroundColor(.yellow)
// ... anything else specific to emails
}
}
and then use them with my MySuperTextField like this:
VStack {
MySuperTextField("Last Name", $lastName, vm: LastNameModifier())
MySuperTextField("Email", $email, vm: EmailModifier())
}
If I understood correctly, you can make your MySuperTextField accept a generic parameter:
struct MySuperTextField<V>: View where V: ViewModifier {
private let placeholder: String
#Binding private var text: String
private let vm: V
init(_ placeholder: String, text: Binding<String>, vm: V) {
self.placeholder = placeholder
self._text = text
self.vm = vm
}
var body: some View {
TextField(placeholder, text: $text)
.modifier(vm)
}
}
Then, you can pass some ViewModifier as the parameter:
struct ContentView: View {
#State private var text: String = "Test"
var body: some View {
MySuperTextField("Last Name", text: $text, vm: LastNameModifier())
}
}
If you need a way to skip the vm parameter when creating MySuperTextField:
MySuperTextField("Last Name", text: $text)
you can create an extension:
extension MySuperTextField where V == EmptyModifier {
init(_ placeholder: String, text: Binding<String>) {
self.placeholder = placeholder
self._text = text
self.vm = EmptyModifier()
}
}
Set the ViewModifier up as a struct and just call it on the view. You just have to be specific as to the type, such as this buttonStyle modifier I used:
struct PurchaseButtonStyle: ButtonStyle {
let geometry: GeometryProxy
func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(minWidth: 0, idealWidth: 300, maxWidth: .infinity)
.padding(.all, geometry.size.height / 30)
.foregroundColor(.black)
.background(.orange)
.cornerRadius(30)
}
}
The use is:
Button(...)
.buttonStyle(PurchaseButtonStyle(geometry: Geometry))
You just have to write it to be the specific modifier. I just used this because it was handy in an app I was working on.
since ViewModifier has an associated type that you cannot use as an instance variable but I propose you another way of use:
MySuperTextField does not have to maintain instances of the ViewModifier type.
struct MySuperTextField: View {
#State var textValue = ""
var body: some View {
TextField("", text: $textValue)
}
}
For each structure that conforms to the ViewModifier protocol you create an extension:
extension MySuperTextField {
func lastNameModifier() -> some View {
modifier(LastNameModifier())
}
func emailModifier() -> some View {
modifier(EmailModifier())
}
}
and you can use it this way:
struct ContentView : View {
var body: some View {
VStack {
MySuperTextField(textValue: "Last Name")
.lastNameModifier()
MySuperTextField(textValue: "Email")
.emailModifier()
}
}
}

SwiftUI Index out of range in ForEach

After hours of debugging I figured out the error is inside the foreach loop in MenuItemView in the folder ContentViews.
The app crashes and the error is:
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444.
Information:
I have got an ObservableObject with an Array of Structs inside as data storage.
The problem:
The ForEach goes between 0 and the array count + 1. This is so I can have an extra item for adding new elements. In the ForEach is a check if the index is inside the bounds (if (idx >= palettesOO.palettes.count) then show the plus).
But it crashes when I right click any cell and click "Remove". This calls the function RemovePalette in the class Manager. There the data gets removed from the array inside the ObservableObject - this also works.
AFTER the function gets called the app crashes (I know this because I printed a message after the function call). I figured out that the crash occurs when the view gets redrawn (updated).
If I have a view element which does not need a binding, for example a Text, then it works, if it needs a binding, for example a TextField it crashes. Text(palettesOO.palettes[idx].palName) inside of the else inside the ForEach works but view elements or subviews which require Bindings do not work: TextField("", text: $palettesOO.palettes[idx].palName) crashes.
I have tried modifying the ForEach with things like these but with no success.
The Code and Data:
class PalettesOO: ObservableObject {
#Published var palettes = [Palette]()
}
MenuItemView:
struct MenuItemView: View {
#ObservedObject var palettesOO = PalettesOO()
var body: some View {
VStack {
SectionView("Palettes") {
LazyVGrid(columns: Array(repeating: GridItem(.fixed(viewCellSize), spacing: viewCellSpacing), count: viewColCount), spacing: viewCellSpacing) {
ForEach(0..<palettesOO.palettes.count + 1, id: \.self) { idx in
if (idx >= palettesOO.palettes.count) {
Button(action: {
newPalettePopover = true
}, label: {
Image(systemName: "plus.square").font(.system(size: viewCellSize))
}).buttonStyle(PlainButtonStyle())
}
else {
// Works
Text(palettesOO.palettes[idx].palName)
// Does not work
TextField("ASD", text: $palettesOO.palettes[palettesOO.palettes.count - 1].palName).frame(width: 100, height: 100).background(Color.red).contextMenu(ContextMenu(menuItems: {
Button(action: {}, label: {
Text("Rename")
})
Button(action: { Manager.RemovePalette(name: palettesOO.palettes[idx].palName); print("Len \(palettesOO.palettes.count)") }, label: {
Text("Delete")
})
}))
// Original code, also crashes (PalettePreviewView is a custom subview which does not matter for this)
// PalettePreviewView(palette: $palettesOO.palettes[palettesOO.palettes.count - 1], colNum: $previewColCount, cellSize: $viewCellSize).cornerRadius(viewCellSize / 100 * viewCellRadius).contextMenu(ContextMenu(menuItems: {
// Button(action: {}, label: {
// Text("Rename")
// })
// Button(action: { Manager.RemovePalette(name: palettesOO.palettes[idx].palName); print("Len \(palettesOO.palettes.count)") }, label: {
// Text("Delete")
// })
// }))
}
}
}
}
}.padding().fixedSize()
}
}
Manager:
class Manager {
static func RemovePalette(name: String) {
var url = assetFilesDirectory(name: "Palettes", shouldCreate: true)
url?.appendPathComponent("\(name).json")
if (url == nil) {
return
}
do {
try FileManager.default.removeItem(at: url!)
} catch let error as NSError {
print("Error: \(error.domain)")
}
LoadAllPalettes()
UserDefaults.standard.removeObject(forKey: "\(k_paletteIndicies).\(name)")
}
}
I know that such complex problems are not good to post on Stack Overflow but I can't think of any other way.
The project version control is public on my GitHub, in case it's needed to find a solution.
EDIT 12/21/2020 # 8:30pm:
Thanks to #SHS it now works like a charm!
Here is the final working code:
struct MenuItemView: View {
#ObservedObject var palettesOO = PalettesOO()
var body: some View {
VStack {
...
ForEach(0..<palettesOO.palettes.count + 1, id: \.self) { idx in
...
//// #SHS Changed :-
Safe(self.$palettesOO.palettes, index: idx) { binding in
TextField("ASD", text: binding.palName).frame(width: 100, height: 100).background(Color.red).contextMenu(ContextMenu(menuItems: {
Button(action: {}, label: {
Text("Rename")
})
Button(action: { Manager.RemovePalette(name: binding.wrappedValue.palName); print("Len \(palettesOO.palettes.count)") }, label: {
Text("Delete")
})
}))
}
}
}
...
}
}
//// #SHS Added :-
//// You may keep the following structure in different file or Utility folder. You may rename it properly.
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, index: T.Index, #ViewBuilder content: #escaping (BoundElement) -> C) {
self.content = content
self.binding = .init(get: { binding.wrappedValue[index] },
set: { binding.wrappedValue[index] = $0 })
}
var body: some View {
content(binding)
}
}
As per Answer at stackoverflow link
Create a struct as under
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, index: T.Index, #ViewBuilder content: #escaping (BoundElement) -> C) {
self.content = content
self.binding = .init(get: { binding.wrappedValue[index] },
set: { binding.wrappedValue[index] = $0 })
}
var body: some View {
content(binding)
}
}
Then wrap your code for accessing it as under
Safe(self.$palettesOO.palettes, index: idx) { binding in
//Text(binding.wrappedValue.palName)
TextField("ASD", text: binding.palName)
//TextField("ASD", text: $palettesOO.palettes[palettesOO.palettes.count - 1].palName)
.frame(width: 100, height: 100).background(Color.red)
.contextMenu(ContextMenu(menuItems: {
Button(action: {}, label: {
Text("Rename")
})
Button(action: { Manager.RemovePalette(name: binding.wrappedValue.palName); print("Len \(palettesOO.palettes.count)") }, label: {
Text("Delete")
})
}))
}
I hope this can help you ( till it is corrected in Swift )

How do I add closures to Viewmodifiers in SwiftUI?

I am using the ClearButton ViewModifier here
.modifier(ClearButton(text: $someBinding))
But I want to run a function after clear textfield. Like this or similar
.modifier(ClearButton(text: $someBinding)) {
print("")
}
Is it possible?
You can pass a function as a parameter like this:
struct ClearButton: ViewModifier {
#Binding var text: String
var action: () -> Void = {} // pass the function here
public func body(content: Content) -> some View {
ZStack(alignment: .trailing) {
content
if !text.isEmpty {
Button(action: {
self.text = ""
action() // call the `action` here
}) {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
.padding(.trailing, 8)
}
}
}
}
and apply this modifier to your TextField:
struct ContentView: View {
#State private var text = ""
var body: some View {
TextField("Some Text", text: $text)
.modifier(
ClearButton(text: $text) {
print("TextField cleared")
}
)
}
}
Note that if you want to skip the label for the trailing closure, the action parameter must come last.

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