Associated enum state in SwiftUI - swift

How can I use an associated enum as a #State variable in an if statement in SwiftUI?
struct ProfileView: View {
#State private var choice = Choice.simple
private enum Choice {
case simple
case associated(Int)
}
var body: some View {
if choice == .simple {
Text("Simple")
}
}
}
The compiler reports this error:
Protocol 'Equatable' requires that 'ProfileView.Choice' conform to 'Equatable'

Here is fixed variant. Tested with Xcode 11.4.
struct ProfileView: View {
#State private var choice = Choice.simple
private enum Choice: Equatable {
case simple
case associated(Int)
}
var body: some View {
Group {
if choice == .simple {
Text("Simple")
} else {
Text("Other")
}
}
}
}

You need to use if case to check if an enum variable matches a certain case.
var body: some View {
if case .simple = choice {
return Text("Simple")
} else {
return Text("Not so simple")
}
}
If you actually want to use the associated value to be displayed, I'd suggest using a switch to cover all enum cases.
var body: some View {
let text: String
switch choice {
case .simple:
text = "Simple"
case .associated(let value):
text = "\(value)"
}
return Text(text)
}

Related

Getting items from [Any?] in a ForEach loop to display text

Below is some code I have been trying
import SwiftUI
struct AnyOptional: View {
private var optionalArray: [Any?] = [1, 2, 3]
var body: some View {
VStack {
ForEach(optionalArray) { i in
Text("\(i)")
}
}
}
}
extension Optional: Identifiable {
public var id: String { self as! String }
}
struct AnyOptional_Previews: PreviewProvider {
static var previews: some View {
AnyOptional()
}
}
I had a similar problem with [String] which I solved by using this extension
extension String: Identifiable {
public var id: String { self }
}
but now I get an error saying Any? must inherit from NSObject.
Is there an easier way to do this?
A possible solution is to use your already created id extension:
var body: some View {
VStack {
ForEach(optionalArray) { i in
Text(i.id)
}
}
}
Note that not all objects can be casted down to String (self as! String will fail if the object can't be cast to String).
A better way is to use String(describing:).
For this you can create another extension (updated to remove the word Optional if there's some value):
extension Optional {
public var asString: String {
if let value = self {
return .init(describing: value)
}
return .init(describing: self)
}
}
and use it in the ForEach loop:
var body: some View {
VStack {
ForEach(optionalArray, id: \.asString) { i in
Text(i.asString)
}
}
}

SwiftUI can not return View on function

I'm trying return View based selected menu item on function. But it throws error:
Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
There my code:
enum MenuItem {
case Main
case Report
}
struct Menu: View {
#State var activeItem: MenuItem = .Main
private func getActiveView() -> View {
switch activeItem {
case .Main:
return DashboardView()
case .Report:
return ReportView()
}
}
var body: some View {
...
getActiveView()
...
}
}
struct DashboardView: View {
var body: some View {
Text("Contact")
}
}
struct ReportView: View {
var body: some View {
Text("Contact")
}
}
Im new on SwiftUI. Any ideas how to return View?
SwiftUI 2
Here is a solution tested with Xcode 12b / iOS 14
struct Menu: View {
#State var activeItem: MenuItem = .Main
// make function ViewBuilder
#ViewBuilder
private func getActiveView() -> some View {
switch activeItem {
case .Main:
DashboardView() // don't use 'return' as it disables ViewBuilder
case .Report:
ReportView()
}
}
var body: some View {
getActiveView()
}
}
SwiftUI gives us a type-erased wrapper called AnyView that we can return.
Tested Solution:
struct Menu: View {
#State var activeItem: MenuItem = .Main
func getActiveView() -> some View {
switch activeItem {
case .Main:
return AnyView(DashboardView())
case .Report:
return AnyView(ReportView())
}
}
var body: some View {
getActiveView()
}
}
Note: type-erased wrapper effectively forces Swift to forget about what specific
type is inside the AnyView, allowing them to look like they are the
same thing. This has a performance cost, though, so don’t use it
often.
For more information you can refer to the this cool article: https://www.hackingwithswift.com/quick-start/swiftui/how-to-return-different-view-types

How does one share the value between a local a #State variable and a #Binding one in SwiftUI?

import SwiftUI
enum ValueType {
case string(String)
case int(Int)
}
struct ParentView: View {
#State var value: ValueType?
var body: some View {
ChildView(boundValue: $value)
}
}
struct ChildView: View {
#Binding var boundValue: ValueType?
#State private var userInput: String = ""
var body: some View {
TextField("Enter some text", text: $userInput)
}
}
Here ChildView has a bound version of its parent's #State ... answer var. However the specific use case here involves a binding of an enum that can either have a String or Int value, whereas the ChildView has a TextField which involved a pure String value. How can the ChildView's userInput value be transferred into its boundValue.string(...)?
Thank you for reading. Apologies if this question is a duplicate, I did search but found nothing.
The TextField initializer also has two optional arguments for callbacks, onEditingChanged and onCommit, so another approach could be to put your update logic in there.
TextField("Enter some text", text: $userInput, onCommit: {
self.boundValue = .string(self.userInput)
})
You can use a second Binding to bridge the string to your custom enum, as long as you handle all of the cases. Here is an example:
import SwiftUI
enum ValueType {
case string(String)
case int(Int)
}
struct ParentView: View {
#State var value: ValueType?
var body: some View {
ChildView(boundValue: $value)
}
}
struct ChildView: View {
let boundValue: Binding<ValueType?>
let myCustomBinding: Binding<String>
init(boundValue: Binding<ValueType?>) {
myCustomBinding = Binding<String>.init(
get: {
switch boundValue.wrappedValue {
case .string(let string):
return string
case .int(let int):
return String(describing: int)
case .none:
return ""
}
},
set: { (string: String) -> Void in
boundValue.wrappedValue = Int(string).map{ ValueType.int($0)} ?? ValueType.string(string)
})
self.boundValue = boundValue.projectedValue
}
var body: some View {
TextField("Enter some text", text: myCustomBinding)
}
}

What is the correct way to create SwiftUI Binding with array of associative enum?

I got some (unexplained) crashes earlier today, and simplified my code to what is seen below. The crashing went away, but I am not 100% sure. Is the code below the correct way to create Binding on an array of enums? And if yes, can this code be made simpler?
import SwiftUI
enum TheEnum: Hashable {
case one(Int), two(Float)
}
class TestModel : ObservableObject {
#Published var enumArray = [TheEnum.one(5), TheEnum.two(6.0)]
}
struct ContentView: View {
#ObservedObject var testModel = TestModel()
var body: some View {
HStack {
ForEach(testModel.enumArray, id: \.self) { value -> AnyView in
switch value {
case .one(var intVal):
let b = Binding(get: {
intVal
}) {
intVal = $0
}
return AnyView(IntView(intVal: b))
case .two(var floatVal):
let b = Binding(get: {
floatVal
}) {
floatVal = $0
}
return AnyView(FloatView(floatVal: b))
}
}
}
}
}
struct IntView: View {
#Binding var intVal: Int
var body: some View {
Text("\(intVal)")
}
}
struct FloatView: View {
#Binding var floatVal: Float
var body: some View {
Text("\(floatVal)")
}
}

Alternative to switch statement in SwiftUI ViewBuilder block?

⚠️ 23 June 2020 Edit: From Xcode 12, both switch and if let statements will be supported in the ViewBuilder!
I’ve been trying to replicate an app of mine using SwiftUI. It has a RootViewController which, depending on an enum value, shows a different child view controller. As in SwiftUI we use views instead of view controllers, my code looks like this:
struct RootView : View {
#State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
Unfortunately, I get a warning:
Closure containing control flow statement cannot be used with function builder ViewBuilder.
So, are there any alternatives to switch so I can replicate this behaviour?
⚠️ 23 June 2020 Edit: From Xcode 12, both switch and if let statements will be supported in the ViewBuilder!
Thanks for the answers, guys. I’ve found a solution on Apple’s Dev Forums.
It’s answered by Kiel Gillard. The solution is to extract the switch in a function as Lu_, Linus and Mo suggested, but we have to wrap the views in AnyView for it to work – like this:
struct RootView: View {
#State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> AnyView {
switch containedViewType {
case .home: return AnyView(HomeView())
case .categories: return AnyView(CategoriesView())
...
}
}
Update: SwiftUI 2 now includes support for switch statements in function builders, https://github.com/apple/swift/pull/30174
Adding to Nikolai's answer, which got the switch compiling but not working with transitions, here's a version of his example that does support transitions.
struct RootView: View {
#State var containedViewType: ContainedViewType = .home
var body: some View {
VStack {
// custom header goes here
containedView()
}
}
func containedView() -> some View {
switch containedViewType {
case .home: return AnyView(HomeView()).id("HomeView")
case .categories: return AnyView(CategoriesView()).id("CategoriesView")
...
}
}
Note the id(...) that has been added to each AnyView. This allows SwiftUI to identify the view within it's view hierarchy allowing it to apply the transition animations correctly.
It looks like you don't need to extract the switch statement into a separate function if you specify the return type of a ViewBuilder. For example:
Group { () -> Text in
switch status {
case .on:
return Text("On")
case .off:
return Text("Off")
}
}
Note: You can also return arbitrary view types if you wrap them in AnyView and specify that as the return type.
You must wrap your code in a View, such as VStack, or Group:
var body: some View {
Group {
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
}
}
}
or, adding return values should work:
var body: some View {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
The best-practice way to solve this issue, however, would be to create a method that returns a view:
func nextView(for containedView: YourViewEnum) -> some AnyView {
switch containedView {
case .home: return HomeView()
case .categories: return CategoriesView()
...
}
}
var body: some View {
nextView(for: containedView)
}
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()
}
}
}
Providing default statement in the switch solved it for me:
struct RootView : View {
#State var containedView: ContainedView = .home
var body: some View {
// custom header goes here
switch containedView {
case .home: HomeView()
case .categories: CategoriesView()
...
default: EmptyView()
}
}
}
You can do with a wrapper View
struct MakeView: View {
let make: () -> AnyView
var body: some View {
make()
}
}
struct UseMakeView: View {
let animal: Animal = .cat
var body: some View {
MakeView {
switch self.animal {
case .cat:
return Text("cat").erase()
case .dog:
return Text("dog").erase()
case .mouse:
return Text("mouse").erase()
}
}
}
}
For not using AnyView(). I will use a bunch of if statements and implement the protocols Equatable and CustomStringConvertible in my Enum for retrieving my associated values:
var body: some View {
ZStack {
Color("background1")
.edgesIgnoringSafeArea(.all)
.onAppear { self.viewModel.send(event: .onAppear) }
// You can use viewModel.state == .loading as well if your don't have
// associated values
if viewModel.state.description == "loading" {
LoadingContentView()
} else if viewModel.state.description == "idle" {
IdleContentView()
} else if viewModel.state.description == "loaded" {
LoadedContentView(list: viewModel.state.value as! [AnimeItem])
} else if viewModel.state.description == "error" {
ErrorContentView(error: viewModel.state.value as! Error)
}
}
}
And I will separate my views using a struct:
struct ErrorContentView: View {
var error: Error
var body: some View {
VStack {
Image("error")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100)
Text(error.localizedDescription)
}
}
}