So I want to have a view which presents conditionally, depending on the state of my model. I understand how to do this if each case has a view to present, but how do I handle the case where I want to show no view in some cases?
For instance:
struct MyView: View {
enum ViewState {
case A, B, C
}
let state: ViewState
var view: some View {
switch state {
case .A:
Text("A State")
case .B:
Text("B State")
case .C:
// empty
}
}
}
You can to use EmptyView and (!) mark view property as a ViewBuilder, like in below finalised example:
struct MyView: View {
enum ViewState {
case A, B, C
}
let state: ViewState
#ViewBuilder var view: some View { // << here !!
switch state {
case .A:
Text("A State")
case .B:
Text("B State")
case .C:
EmptyView() // << here !!
}
}
var body: some View { // replicated for demo
self.view
}
}
Tested with Xcode 13.2
You can use EmptyView() and mark you view with #ViewBuilder var view: some View
struct MyView: View {
enum ViewState {
case A, B, C
}
let state: ViewState
var text: String {
switch state {
case .A:
"A State"
case .B:
"B State"
case .C:
""
}
}
var view: some View {
Text(text)
}
}
Related
In SwiftUI 4, there is now a NavigationSplitView. I played around with it and detected some strange behaviour.
Consider the following code: When the content function returns the plain Text, then there is the expected behaviour - tapping a menu item changes the detail view to the related text.
However, when commenting out the first four cases, and commenting in the next four, then a tap on "Edit Profile" does not change the detail view display. (Using #ViewBuilder does not change this behaviour.)
Any ideas out there about the reasons for that? From my point of view, this may just be a simple bug, but perhaps there are things to be considered that are not documented yet?!
struct MainScreen: View {
#State private var menuItems = MenuItem.menuItems
#State private var menuItemSelection: MenuItem?
var body: some View {
NavigationSplitView {
List(menuItems, selection: $menuItemSelection) { course in
Text(course.name).tag(course)
}
.navigationTitle("HappyFreelancer")
} detail: {
content(menuItemSelection)
}
.navigationSplitViewStyle(.balanced)
}
func content(_ selection: MenuItem?) -> some View {
switch selection {
case .editProfile:
return Text("Edit Profile")
case .evaluateProfile:
return Text("Evaluate Profile")
case .setupApplication:
return Text("Setup Application")
case .none:
return Text("none")
// case .editProfile:
// return AnyView(EditProfileScreen())
//
// case .evaluateProfile:
// return AnyView(Text("Evaluate Profile"))
//
// case .setupApplication:
// return AnyView(Text("Setup Application"))
//
// case .none:
// return AnyView(Text("none"))
}
}
}
struct MainScreen_Previews: PreviewProvider {
static var previews: some View {
MainScreen()
}
}
enum MenuItem: Int, Identifiable, Hashable, CaseIterable {
var id: Int { rawValue }
case editProfile
case evaluateProfile
case setupApplication
var name: String {
switch self {
case .editProfile: return "Edit Profile"
case .evaluateProfile: return "Evaluate Profile"
case .setupApplication: return "Setup Application"
}
}
}
extension MenuItem {
static var menuItems: [MenuItem] {
MenuItem.allCases
}
}
struct EditProfileScreen: View {
var body: some View {
Text("Edit Profile")
}
}
After playing around a bit in order to force SwiftUI to redraw the details view, I succeeded in this workaround:
Wrap the NavigationSplitView into a GeometryReader.
Apply an .id(id) modifier to the GeometryReader (e.g., as #State private var id: Int = 0)
In this case, any menu item selection leads to a redraw as expected.
However, Apple should fix the bug, which it is obviously.
I've found that wrapping the Sidebar list within its own view will fix this issue:
struct MainView: View {
#State var selection: SidebarItem? = .none
var body: some View {
NavigationSplitView {
Sidebar(selection: $selection)
} content: {
content(for: selection)
} detail: {
Text("Detail")
}
}
#ViewBuilder
func content(for item: SidebarItem?) -> some View {
switch item {
case .none:
Text("Select an Item in the Sidebar")
case .a:
Text("A")
case .b:
Text("B")
}
}
}
As the time flies, by App get more and more complicated shape.
In some cases the App flow might be:
View A -> View B -> C -> D
and then back
D -> C -> B -> A..
but sometimes i need to skip the view and go
D -> B -> A..
In some cases its A -> C -> D and then D -> A
I started to use NavigationView/NavigationLink and in some cases i use the following approach:
let weekView = WeekView(journey: journey, isRoot: isRoot).environmentObject(self.thisSession)
window?.rootViewController = UIHostingController(rootView: weekView)
No i realize that it has become a complete mess.. It's time for me to rethink this..
How do you handle navigation in apps where it can't be always done by pushing/popping the views from Navigation stack?
Using ViewBuilders is a good option here.
#ViewBuilder func myViewRouter(selection: Selection) -> some View {
switch selection {
case selection1:
View1()
case selection2:
View2()
case selection3:
View3()
}
}
enum Selection { ... }
ViewBuilders are powerful, its pretty much a function that can return opaque types but notice the lack of the return keyword. This seems like a perfect use case for it. In the example I used a enum but its also common to see this used with a var selection = 0 on the parent view and have the ViewBuilder as the child. Either way, same functionality.
Below is is a good url to understand ViewBuilders.
https://swiftwithmajid.com/2019/12/18/the-power-of-viewbuilder-in-swiftui/
Last edit: Here is an example use case:
import SwiftUI
struct ContentView: View {
#State private var selection: SelectionEnum = .zero
var body: some View {
VStack {
showMyViews(selection: selection)
HStack {
ForEach(SelectionEnum.allCases, id: \.self) { selection in
Button(action: {self.selection = selection}){
Text(selection.rawValue)
.fontWeight(.bold)
.frame(width: 60, height: 60)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
}
}
}
}
}
#ViewBuilder func showMyViews(selection: SelectionEnum) -> some View {
switch selection {
case .zero:
ViewA()
case .one:
ViewB()
case .two:
ViewC()
case .three:
ViewD()
}
}
enum SelectionEnum: String, CaseIterable {
case zero
case one
case two
case three
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ViewA: View {
var body: some View {
Text("View A")
}
}
struct ViewB: View {
var body: some View {
Text("View B")
}
}
struct ViewC: View {
var body: some View {
Text("View C")
}
}
struct ViewD: View {
var body: some View {
Text("View D")
}
}
I'd like to be able to create a List from an enum that conforms to CaseIterable and CustomStringConvertible e.g.
public enum HairColor: Int, Codable, CaseIterable, CustomStringConvertible {
public var description: String {
switch self {
case .black:
return "Black"
case .blond:
return "Blond"
case .brown:
return "Brown"
case .red:
return "Red"
case .grey:
return "Gray"
case .bald:
return "Bald"
}
}
case blond, brown, black, red, grey, bald
}
struct ContentView: View {
var body: some View {
SwiftUIHelpers.enumToList(HairColor)
}
}
This is the approach I've tried but I get the error: "Cannot convert value of type 'Text' to closure result type '_"
struct SwiftUIHelpers {
static func enumToList<T: CaseIterable, RandomAccessCollection>(_ a: T) -> some View {
List {
ForEach(a, id: \.rawValue) { (o: CustomStringConvertible) in
Text(o.description)
}
}
}
}
What is the error on my ways?!?
Here is working solution. Tested with Xcode 11.4 / iOS 13.4.
struct SwiftUIHelpers {
static func enumToList<T: CaseIterable>(_ t: T.Type) -> some View
where T.AllCases: RandomAccessCollection, T: Hashable & CustomStringConvertible {
List {
ForEach(t.self.allCases, id: \.self) { o in
Text(o.description)
}
}
}
}
struct ContentView: View {
var body: some View {
SwiftUIHelpers.enumToList(HairColor.self)
}
}
public enum HairColor: Int, Codable, Hashable, CaseIterable, CustomStringConvertible {
public var description: String {
switch self {
case .black:
return "Black"
case .blond:
return "Blond"
case .brown:
return "Brown"
case .red:
return "Red"
case .grey:
return "Gray"
case .bald:
return "Bald"
}
}
case blond, brown, black, red, grey, bald
}
Update: Having played with this more I've found that the following extension can be much more helpful
extension CaseIterable where Self.AllCases: RandomAccessCollection, Self: Hashable & CustomStringConvertible {
static func toForEach() -> some View {
ForEach(Self.allCases, id: \.self) { o in
Text(o.description).tag(o)
}
}
}
because gives wider reuse possibility, like
List { HairColor.toForEach() }
and this
Form { HairColor.toForEach() }
and
struct DemoHairColorPicker: View {
#State private var hairColor: HairColor = .red
var body: some View {
VStack {
Text("Selected: \(hairColor.description)")
Picker(selection: $hairColor, label: Text("Hair")) { HairColor.toForEach() }
}
}
}
and of course in any stack VStack { HairColor.toForEach() }
Not really an answer to all parts of your question - only the first part - but offered here as an alternative...
Might be worth considering the use of #EnvironmentObject for a #Published property? I used this to populate a sidebar style menu for a macOS target.
Step 1:
Use your enum. My enum is written a little differently to yours but I thought to leave it that way because it provides an alternate construction... but with the same outcome.
(Conforming to CaseIterable here allows us to use the .allCases method in Step 2.)
enum HairColor: Int, CaseIterable {
case blond = 0, brown, black, red, grey, none
var description: String {
switch self {
case .blond: return "Blond"
case .brown: return "Brown"
case .black: return "Black"
case .red: return "Red"
case .grey: return "Grey"
case .none: return "Bald"
}
}
}
Step 2:
Create a struct for your model and include a static property that maps all cases of your HairColor enum.
(Conforming to Identifiable here allows us to use the cleaner ForEach syntax in Step 4 - that is - use ForEach(appData.hairColors) in lieu of ForEach(appData.hairColors, id: \.id)).
import SwiftUI
struct Hair: Codable, Hashable, Identifiable {
var id: Int
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
static var colors: [Hair] {
return HairColor.allCases.map({ Hair(id: $0.rawValue, name: $0.description ) })
}
}
Step 3:
Create a class that conforms to ObservableObject and that contains a #Published wrapped property to allow you to broadcast your HairColor via #EnvironmentObject.
import Combine // <- don't forget this framework import!
import SwiftUI
final class AppData: ObservableObject {
#Published var hairColors = Hair.colors
}
Step 4:
Use in a View.
struct HairList: View {
#EnvironmentObject var appData: AppData
#State var selectedHair: Hair?
var body: some View {
VStack(alignment: .leading) {
Text("Select...")
.font(.headline)
List(selection: $selectedHair) {
ForEach(appData.hairColors) { hairColor in
Text(hairColor.name).tag(hairColor)
}
}
.listStyle(SidebarListStyle())
}
.frame(minWidth: 100, maxWidth: 150)
.padding()
}
}
Step 5:
Remember to inject the environment object into the preview to make the preview usable.
struct HairList_Previews: PreviewProvider {
static var previews: some View {
HairList(selectedHair: .constant(AppData().hairColors[1]))
.environmentObject(AppData())
}
}
As of Xcode 11.4, SwiftUI doesn't allow switch statements in Function builder blocks like VStack {}, failing with a generic error like Generic parameter 'Content' could not be inferred. How can the switch statement be used in SwiftUI to create different Views depending on an enum value?
switch in SwiftUI view builders is supported since Xcode 12:
enum Status {
case loggedIn, loggedOut, expired
}
struct SwiftUISwitchView: View {
#State var userStatus: Status = .loggedIn
var body: some View {
VStack {
switch self.userStatus {
case .loggedIn:
Text("Welcome!")
case .loggedOut:
Image(systemName: "person.fill")
case .expired:
Text("Session expired")
}
}
}
}
You can use enum with #ViewBuilder as follow ...
Declear enum
enum Destination: CaseIterable, Identifiable {
case restaurants
case profile
var id: String { return title }
var title: String {
switch self {
case .restaurants: return "Restaurants"
case .profile: return "Profile"
}
}
}
Now in the View file
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
view(for: selectedDestination)
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
If you want to use the same case with the NavigationLink ... You can use it as follow
struct ContentView: View {
#State private var selectedDestination: Destination? = .restaurants
var body: some View {
NavigationView {
List(Destination.allCases,
selection: $selectedDestination) { item in
NavigationLink(destination: view(for: selectedDestination),
tag: item,
selection: $selectedDestination) {
Text(item.title).tag(item)
}
}
}
}
#ViewBuilder
func view(for destination: Destination?) -> some View {
switch destination {
case .some(.restaurants):
CategoriesView()
case .some(.profile):
ProfileView()
default:
EmptyView()
}
}
}
Normally I can display a list of items like this in SwiftUI:
enum Fruit {
case apple
case orange
case banana
}
struct FruitView: View {
#State private var fruit = Fruit.apple
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit)
}
}
}
}
This works perfectly, allowing me to select whichever fruit I want. If I want to switch fruit to be nullable (aka an optional), though, it causes problems:
struct FruitView: View {
#State private var fruit: Fruit?
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit)
}
}
}
}
The selected fruit name is no longer displayed on the first screen, and no matter what selection item I choose, it doesn't update the fruit value.
How do I use Picker with an optional type?
The tag must match the exact data type as the binding is wrapping. In this case the data type provided to tag is Fruit but the data type of $fruit.wrappedValue is Fruit?. You can fix this by casting the datatype in the tag method:
struct FruitView: View {
#State private var fruit: Fruit?
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit as Fruit?)
}
}
}
}
Bonus: If you want custom text for nil (instead of just blank), and want the user to be allowed to select nil (Note: it's either all or nothing here), you can include an item for nil:
struct FruitView: View {
#State private var fruit: Fruit?
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
Text("No fruit").tag(nil as Fruit?)
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit as Fruit?)
}
}
}
}
Don't forget to cast the nil value as well.
I made a public repo here with Senseful's solution:
https://github.com/andrewthedina/SwiftUIPickerWithOptionalSelection
EDIT: Thank you for the comments regarding posting links. Here is the code which answers the question. Copy/paste will do the trick, or clone the repo from the link.
import SwiftUI
struct ContentView: View {
#State private var selectionOne: String? = nil
#State private var selectionTwo: String? = nil
let items = ["Item A", "Item B", "Item C"]
var body: some View {
NavigationView {
Form {
// MARK: - Option 1: NIL by SELECTION
Picker(selection: $selectionOne, label: Text("Picker with option to select nil item [none]")) {
Text("[none]").tag(nil as String?)
.foregroundColor(.red)
ForEach(items, id: \.self) { item in
Text(item).tag(item as String?)
// Tags must be cast to same type as Picker selection
}
}
// MARK: - Option 2: NIL by BUTTON ACTION
Picker(selection: $selectionTwo, label: Text("Picker with Button that removes selection")) {
ForEach(items, id: \.self) { item in
Text(item).tag(item as String?)
// Tags must be cast to same type as Picker selection
}
}
if selectionTwo != nil { // "Remove item" button only appears if selection is not nil
Button("Remove item") {
self.selectionTwo = nil
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I actually prefer #Senseful's solution for a point solution, but for posterity: you could also create a wrapper enum, which if you have a ton of entity types in your app scales quite nicely via protocol extensions.
// utility constraint to ensure a default id can be produced
protocol EmptyInitializable {
init()
}
// primary constraint on PickerValue wrapper
protocol Pickable {
associatedtype Element: Identifiable where Element.ID: EmptyInitializable
}
// wrapper to hide optionality
enum PickerValue<Element>: Pickable where Element: Identifiable, Element.ID: EmptyInitializable {
case none
case some(Element)
}
// hashable & equtable on the wrapper
extension PickerValue: Hashable & Equatable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}
// common identifiable types
extension String: EmptyInitializable {}
extension Int: EmptyInitializable {}
extension UInt: EmptyInitializable {}
extension UInt8: EmptyInitializable {}
extension UInt16: EmptyInitializable {}
extension UInt32: EmptyInitializable {}
extension UInt64: EmptyInitializable {}
extension UUID: EmptyInitializable {}
// id producer on wrapper
extension PickerValue: Identifiable {
var id: Element.ID {
switch self {
case .some(let e):
return e.id
case .none:
return Element.ID()
}
}
}
// utility extensions on Array to wrap into PickerValues
extension Array where Element: Identifiable, Element.ID: EmptyInitializable {
var pickable: Array<PickerValue<Element>> {
map { .some($0) }
}
var optionalPickable: Array<PickerValue<Element>> {
[.none] + pickable
}
}
// benefit of wrapping with protocols is that item views can be common
// across data sets. (Here TitleComponent { var title: String { get }})
extension PickerValue where Element: TitleComponent {
#ViewBuilder
var itemView: some View {
Group {
switch self {
case .some(let e):
Text(e.title)
case .none:
Text("None")
.italic()
.foregroundColor(.accentColor)
}
}
.tag(self)
}
}
Usage is then quite tight:
Picker(selection: $task.job, label: Text("Job")) {
ForEach(Model.shared.jobs.optionalPickable) { p in
p.itemView
}
}
I learned almost all I know about SwiftUI Bindings (with Core Data) by reading this blog by Jim Dovey. The remainder is a combination of some research and quite a few hours of making mistakes.
So when I use Jim's technique to create Extensions on SwiftUI Binding then we end up with something like this...
public extension Binding where Value: Equatable {
init(_ source: Binding<Value>, deselectTo value: Value) {
self.init(get: { source.wrappedValue },
set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
)
}
}
Which can then be used throughout your code like this...
Picker("country", selection: Binding($selection, deselectTo: nil)) { ... }
OR
Picker("country", selection: Binding($selection, deselectTo: someOtherValue)) { ... }
OR when using .pickerStyle(.segmented)
Picker("country", selection: Binding($selection, deselectTo: -1)) { ... }
which sets the index of the segmented style picker to -1 as per the documentation for UISegmentedControl and selectedSegmentIndex.
The default value is noSegment (no segment selected) until the user
touches a segment. Set this property to -1 to turn off the current
selection.
Why not extending the enum with a default value? If this is not what you are trying to achieve, maybe you can also provide some information, why you want to have it optional.
enum Fruit: String, CaseIterable, Hashable {
case apple = "apple"
case orange = "orange"
case banana = "banana"
case noValue = ""
}
struct ContentView: View {
#State private var fruit = Fruit.noValue
var body: some View {
VStack{
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases, id:\.self) { fruit in
Text(fruit.rawValue)
}
}
Text("Selected Fruit: \(fruit.rawValue)")
}
}
}