I'm building a custom number pad in Swift UI. I want the buttons to be arranged in a grid that fills the screen. The only way I've found to do this for far is to use GeometryReader but this seems like overkill for this simple task. Is there a better way to write this?
GeometryReader { geometry in
HStack(spacing: 0) {
ForEach([a, b, c]) { n in
Button(action: { self.add(n) }) { Text("\(n)") }
.frame(width: geometry.size.width/3)
}
}
}.frame(height: 80)
This results in this, which is how I want it to look. Just curious if there is a good way to do this without GeometryReader.
Yes, you can do this without GeometryReader. You just have to make each key view expandable. Then your HStack and VStack will take care of the rest.
A Button tightly wraps its label subview, so you need to make the label subview expandable. A Color is a View that expands to fill whatever space it's given, so let's use Color.clear as the button's label, and overlay the real Text label on the Color. I think we should define a KeyPadButton View for this:
struct KeyPadButton: View {
var key: String
var body: some View {
Button(action: { self.action(self.key) }) {
Color.clear
.overlay(RoundedRectangle(cornerRadius: 12)
.stroke(Color.accentColor))
.overlay(Text(key))
}
}
enum ActionKey: EnvironmentKey {
static var defaultValue: (String) -> Void { { _ in } }
}
#Environment(\.keyPadButtonAction) var action: (String) -> Void
}
extension EnvironmentValues {
var keyPadButtonAction: (String) -> Void {
get { self[KeyPadButton.ActionKey.self] }
set { self[KeyPadButton.ActionKey.self] = newValue }
}
}
#if DEBUG
struct KeyPadButton_Previews: PreviewProvider {
static var previews: some View {
KeyPadButton(key: "8")
.padding()
.frame(width: 80, height: 80)
.previewLayout(.sizeThatFits)
}
}
#endif
I've defined an EnvironmentKey so that we can use the environment to pass the action callback from the higher-level keypad view to all the buttons.
KeyPadButton looks like this:
If you don't like the border, just remove that modifier.
Note that I've manually set the size of the button in the PreviewProvider. Since this view is expandable, it'll preview at the device size by default, and we don't need to see one giant button.
Now let's define a KeyPadRow view to lay out one row of buttons:
struct KeyPadRow: View {
var keys: [String]
var body: some View {
HStack {
ForEach(keys, id: \.self) { key in
KeyPadButton(key: key)
}
}
}
}
Now we can define the KeyPad view to lay out the entire keypad and provide the action for the buttons:
struct KeyPad: View {
#Binding var string: String
var body: some View {
VStack {
KeyPadRow(keys: ["1", "2", "3"])
KeyPadRow(keys: ["4", "5", "6"])
KeyPadRow(keys: ["7", "8", "9"])
KeyPadRow(keys: [".", "0", "⌫"])
}.environment(\.keyPadButtonAction, self.keyWasPressed(_:))
}
private func keyWasPressed(_ key: String) {
switch key {
case "." where string.contains("."): break
case "." where string == "0": string += key
case "⌫":
string.removeLast()
if string.isEmpty { string = "0" }
case _ where string == "0": string = key
default: string += key
}
}
}
Finally, let's define a ContentView that shows the string above the keypad:
struct ContentView : View {
var body: some View {
VStack {
HStack {
Spacer()
Text(string)
}.padding([.leading, .trailing])
Divider()
KeyPad(string: $string)
}
.font(.largeTitle)
.padding()
}
#State private var string = "0"
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
#endif
Here's the final result:
Related
I've run in to an odd problem with NavigationView on macCatalyst. Here below is a simple app with a sidebar and a detail view. Selecting an item on the sidebar shows a detail view with a scrollable list.
Everything works fine for the first NavigationLink, the detail view displays and is freely scrollable. However, if I select a list item which triggers a link to a second detail view, scrolling starts, then freezes. The app still works, only the detail view scrolling is locked up.
The same code works fine on an iPad without any freeze. If I build for macOS, the NavigationLink in the detail view is non-functional.
Are there any known workarounds ?
This is what it looks like, after clicking on LinkedView, a short scroll then the view freezes. It is still possible to click on the back button or another item on the sidebar, but the list view is blocked.
Here is the code:
ContentView.swift
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
var body: some View {
NavigationView {
List() {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailListView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...4).map({NamedItem(name: "\($0)")})
var body: some View {
VStack {
List {
Text(item.name)
NavigationLink(destination: DetailListView(item: NamedItem(name: "LinkedView"))) {
listItem(" LinkedView", "Item")
.foregroundColor(Color.blue)
}
ForEach(sections) { section in
sectionDetails(section)
}
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
TestListApp.swift
import SwiftUI
#main
struct TestListApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I had this very same problem with Mac Catalyst app. On real device (iPhone 7 with iOS 14.4.2) there was no problem but with Mac Catalyst (MacBook Pro with Big Sur 11.2.3) the scrolling in the navigation view stuck very randomly as you explained. I figured out that the issue was with Macbook's trackpad and was related to scroll indicators because with external mouse the issue was absent. So the easiest solution to this problem is to hide vertical scroll indicators in navigation view. At least it worked for me. Below is some code from root view 'ContentView' how I did it. It's unfortunate to lose scroll indicators with big data but at least the scrolling works.
import SwiftUI
struct TestView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: NewView()) {
Text("Navigation Link to new view")
}
}
.onAppear {
UITableView.appearance().showsVerticalScrollIndicator = false
}
}
}
}
OK, so I managed to find a workaround, so thought I'd post this for help, until what seems to be a macCatalyst SwiftUI bug is fixed. I have posted a radar for the list freeze problem: FB8994665
The workaround is to use NavigationLink only to the first level of the series of pages which can be navigated (which gives me the sidebar and a toolbar), and from that point onwards use the NavigationStack package to mange links to other pages.
I ran in to a couple of other gotcha's with this arrangement.
Firstly the NavigationView toolbar loses its background when scrolling linked list views (unless the window is defocussed and refocussed), which seems to be another catalyst SwiftUI bug. I solved that by setting the toolbar background colour.
Second gotcha was that under macCatalyst the onTouch view modifier used in NavigationStack's PushView label did not work for most single clicks. It would only trigger consistently for double clicks. I fixed that by using a button to replace the label.
Here is the code, no more list freezes !
import SwiftUI
import NavigationStack
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
init() {
// Ensure toolbar is allways opaque
UINavigationBar.appearance().backgroundColor = UIColor.secondarySystemBackground
}
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
var body: some View {
NavigationStackView {
DetailListView(item: item)
}
}
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
var linked = NamedItem(name: "LinkedView")
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
List {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name)
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: {
self.isSelected = linked.id
})
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
I have continued to experiment with NavigationStack and have made some modifications which will allow it to swap in and out List rows directly. This avoids the problems I was seeing with the NavigationBar background. The navigation bar is setup at the level above the NavigationStackView and changes to the title are passed via a PreferenceKey. The back button on the navigation bar hides if the stack is empty.
The following code makes use of PR#44 of swiftui-navigation-stack
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let depth: Int
let id = UUID()
init(name:String, depth: Int = 0) {
self.name = name
self.depth = depth
}
var linked: NamedItem {
return NamedItem(name: "Linked \(depth+1)", depth:depth+1)
}
}
// Preference Key to send title back down to DetailStackView
struct ListTitleKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
extension View {
func listTitle(_ title: String) -> some View {
self.preference(key: ListTitleKey.self, value: title)
}
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
#ObservedObject var navigationStack = NavigationStack()
#State var toolbarTitle: String = ""
var body: some View {
List {
NavigationStackView(noGroup: true, navigationStack: navigationStack) {
DetailListView(item: item, linked: item.linked)
.listTitle(item.name)
}
}
.listStyle(PlainListStyle())
.animation(nil)
// Updated title
.onPreferenceChange(ListTitleKey.self) { value in
toolbarTitle = value
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(toolbarTitle) \(self.navigationStack.depth)")
.toolbar(content: {
ToolbarItem(id: "BackB", placement: .navigationBarLeading, showsByDefault: self.navigationStack.depth > 0) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
.opacity(self.navigationStack.depth > 0 ? 1.0 : 0.0)
}
})
}
}
struct DetailListView: View {
var item: NamedItem
var linked: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked, linked: linked.linked)
.listTitle(linked.name)
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func buttonAction() {
self.isSelected = linked.id
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: buttonAction)
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
When, I apply custom modififer .settingBlockModifier(), it disables controllers in the View. I've also tried this way .modifier(SettingBlockModifier()).
Checked, the same behavior also, if I apply the same modifiers without custom modifier. Here's the effect I want to achieve.
The problem that the Picker and Stepper don't work, they are not tappable. How to solve it?
struct TestFile: View {
#State private var gamesAmount = ["5", "10", "20", "30"]
#State private var game: Int = 1
#State private var number = 1
var body: some View {
VStack {
VStack {
Text("How many questions?")
.bold()
Picker("How many questions?",selection: $game) {
ForEach(0..<gamesAmount.count) {
Text(gamesAmount[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}
.settingBlockModifier()
HStack {
Text("Number:")
.padding(.trailing, 40)
Stepper("\(number)", value: $number, in: 1...10)
}
.settingBlockModifier()
//.modifier(SettingBlockModifier())
}
}
}
struct SettingBlockModfier: ViewModifier {
func body(content: Content) -> some View {
content
.background(Color.white.cornerRadius(8))
.shadow(radius: 8)
.padding()
}
}
extension View {
func settingBlockModifier() -> some View {
self.modifier(SettingBlockModfier())
}
}
You can use compositingGroup:
struct SettingBlockModfier: ViewModifier {
func body(content: Content) -> some View {
content
.compositingGroup() // <- here
.background(Color.white.cornerRadius(8))
.shadow(radius: 8)
.padding()
}
}
[EDIT] - This question has been edited and simplified.
I need to create a CustomLooking TabView instead of the default one.
Here is my full code with the problem. Just run the code below.
import SwiftUI
enum TabName {
case explore, network
}
struct ContentView: View {
#State var displayedTab: TabName = .explore
var body: some View {
VStack(spacing: 0) {
Spacer()
switch displayedTab {
case .explore: AViewWhichNavigates(title: "Explore").background(Color.yellow)
case .network: AViewWhichNavigates(title: "Network").background(Color.green)
}
Spacer()
CustomTabView(displayedTab: $displayedTab)
}
}
}
struct CustomTabView: View {
#Binding var displayedTab: TabName
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
let title: String
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation")) {
Text("You are at root. Tap to navigate").navigationTitle(title)
}
})
}
}
On tab#1 click the navigation. Switch to tab#2, then Switch back to tab#1. You will see that tab#1 has popped to root.
How do I prevent the customTabView from popping to root every time i switch tabs?
All you need is a ZStack with opacity.
import SwiftUI
enum TabName {
case explore, network
}
struct ContentView: View {
#State var displayedTab: TabName = .explore
var body: some View {
VStack {
ZStack {
AViewWhichNavigates(title: "Explore")
.background(Color.green)
.opacity(displayedTab == .explore ? 1 : 0)
AViewWhichNavigates(title: "Network")
.background(Color.green)
.opacity(displayedTab == .network ? 1 : 0)
}
CustomTabView(displayedTab: $displayedTab)
}
}
}
struct CustomTabView: View {
#Binding var displayedTab: TabName
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
let title: String
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation")) {
Text("You are at root. Tap to navigate").navigationTitle(title)
}
})
}
}
The problem is that the Navigation isActive state is not recorded as well as the displayed tab state.
By recording the state of the navigation of each tab as well as which tab is active the correct navigation state can be show for each tab.
The model can be improved to remove the tuple and make it more flexible but the key thing is the use of getter and setter to use an encapsulated model of what the navigation state is for each tab in order to allow the NavigationLink to update it via a binding.
I have simplified the top level VStack and removed the top level switch as its not needed here, but it can be added back for using different types of views at the top level in a real implementation
enum TabName : String {
case Explore, Network
}
struct ContentView: View {
#State var model = TabModel()
init(){
UINavigationBar.setAnimationsEnabled(false)
}
var body: some View {
VStack(spacing: 0) {
Spacer()
AViewWhichNavigates(model: $model).background(Color.green)
Spacer()
CustomTabView(model:$model)
}
}
}
struct CustomTabView: View {
#Binding var model: TabModel
var body: some View {
HStack {
Spacer()
Text("Explore").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Explore }
Spacer()
Text("Network").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Network }
Spacer()
}
}
}
struct AViewWhichNavigates: View {
#Binding var model:TabModel
var body: some View {
NavigationView(content: {
NavigationLink(destination: Text("We are one level deep in navigation in \(model.selectedTab.rawValue)"), isActive: $model.isActive) {
Text("You are at root of \(model.selectedTab.rawValue). Tap to navigate").navigationTitle(model.selectedTab.rawValue)
}.onDisappear {
UINavigationBar.setAnimationsEnabled(model.isActive)
}
})
}
}
struct TabModel {
var selectedTab:TabName = .Explore
var isActive : Bool {
get {
switch selectedTab {
case .Explore : return tabMap.0
case .Network : return tabMap.1
}
}
set {
switch selectedTab {
case .Explore : nOn(isActive, newValue); tabMap.0 = newValue;
case .Network : nOn(isActive, newValue); tabMap.1 = newValue;
}
}
}
//tuple used to represent a fixed set of tab isActive navigation states
var tabMap = (false, false)
func nOn(_ old:Bool,_ new:Bool ){
UINavigationBar.setAnimationsEnabled(new && !old)
}
}
I think it is possible even with your custom tab view, because the issue is in rebuilding ExploreTab() when you switch tabs, so all content of that tab is rebuilt as well, so internal NavigationView on rebuilt is on first page.
Assuming you have only one ExploreTab in your app (as should be obvious), the possible solution is to make it Equatable explicitly and do not allow SwiftUI to replace it on refresh.
So
struct ExploreTab: View, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
return true // prevent replacing ever !!
}
var body: some View {
// ... your code here
}
}
and
VStack(spacing: 0) {
switch displayedTab {
case .explore: ExploreTab().equatable() // << here !!
case .network: NetworkTab()
}
CustomTabView(displayedTab: $displayedTab) //This is the Custom TabBar
}
Update: tested with Xcode 12 / iOS 14 - works as described above (actually the same idea works for standard containers)
Here is a quick demo replication of CustomTabView with test environment as described above.
Full module code:
struct ExploreTab: View, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
return true // prevent replacing ever !!
}
var body: some View {
NavigationView {
NavigationLink("Go", destination: Text("Explore"))
}
}
}
enum TestTabs {
case explore
case network
}
struct CustomTabView: View {
#Binding var displayedTab: TestTabs
var body: some View {
HStack {
Button("Explore") { displayedTab = .explore }
Divider()
Button("Network") { displayedTab = .network }
}
.frame(maxWidth: .infinity)
.frame(height: 80).background(Color.yellow)
}
}
struct TestCustomTabView: View {
#State private var displayedTab = TestTabs.explore
var body: some View {
VStack(spacing: 0) {
switch displayedTab {
case .explore: ExploreTab().equatable() // << here !!
case .network: Text("NetworkTab").frame(maxWidth: .infinity, maxHeight: .infinity)
}
CustomTabView(displayedTab: $displayedTab) //This is the Custom TabBar
}
.edgesIgnoringSafeArea(.bottom)
}
}
I want to create a grid of square buttons that I can click/tap to toggle between black and white.
As a half-way point I am creating a row of buttons that do this - see code below.
But when I click on one of them all the buttons toggle together.
I can't see why this is because I have a state variable for each Cell?
struct ContentView: View {
var body: some View {
ZStack {
Color.green
.edgesIgnoringSafeArea(.all)
RowOfCellsView(string: "X.X.X.X")
}
}
}
struct Cell: Identifiable {
var id: Int
var state: Bool
}
struct RowOfCellsView: View {
var string: String
var cells: [Cell] {
string.map { Cell(id: 1, state: $0 == ".") }
}
var body: some View {
HStack {
ForEach(cells) { cell in
CellView(isBlack: cell.state, symbol: "Q")
}
}
}
}
struct CellView: View {
#State var isBlack: Bool
#State var symbol: String
var body: some View {
Button(action: { self.isBlack.toggle() }) {
Text("")
.font(.largeTitle)
.frame(width: 40, height: 40)
.aspectRatio(1, contentMode: .fill)
}
.background(isBlack ? Color.black : Color.white)
}
}
Aha - just noticed that I have the id set to 1 for all Cells - that's the problem!
So needs to have a way to have a different id for each Cell.
For example could do:
string.enumerated().map { Cell(id: $0.0, state: $0.1 == ".") }
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)
}
}
}