SwiftUI allow all string enums in Struct - ios

It's probably a stupid question but I'm trying to allow all string enums as type for a variable in a Struct. The code below is completely wrong but I hope that it will make my question clearer.
My idea was to conform all enums to the same protocol but I can't access .allCases with this approach.
My goal is that I can pass any string enum to the ListView which will then display all components of the enum (here: one; two; three).
Does anybody have an idea how to do this? It must be a very basic Swift thing but I wasn't able to figure it out searching through the web. Thank you so much!
import SwiftUI
struct ContentView: View {
var body: some View {
ListView(myEnum: Elements.self) //error here as well
}
}
protocol StringRepresentable {
var rawValue: String { get }
}
enum Elements: String, Equatable, CaseIterable, StringRepresentable {
case one
case two
case three
}
struct ListView: View {
let myEnum: StringRepresentable //doesn't work
var body: some View {
ForEach(myEnum.allCases, id: \.self) { elem in //can't access .allCases
Text(elem.rawValue)
}
}
}

There's several errors in your original code. First you weren't using a VStack (or List or LazyVStack) so your foreach would only render one element.
Secondly, you were passing the type, not the elements itself to the list view. And finally, your own StringRepresentable protocol is unnecessary, you can use RawRepresentable with it's associated type RawValue constrained to String
i.e. something like this:
struct ContentView: View {
var body: some View {
VStack {
ListView(values: Fruits.allCases)
ListView(values: Animals.allCases)
}
}
}
enum Fruits: String, CaseIterable {
case apple
case orange
case banana
}
enum Animals: String, CaseIterable {
case cat
case dog
case elephant
}
struct ListView<T: RawRepresentable & Hashable>: View where T.RawValue == String {
let values: [T]
var body: some View {
LazyVStack {
ForEach(values, id: \.self) { elem in
Text(elem.rawValue)
}
}
}
}
Which renders like this

Here is a variant that will work by sending in all items of an enum to the view rather that the enum itself and making the view generic.
struct ContentView: View {
var body: some View {
ListView(myEnum: Elements.allCases)
}
}
protocol StringRepresentable {
var rawValue: String { get }
}
enum Elements: String, Equatable, CaseIterable, StringRepresentable {
case one
case two
case three
}
struct ListView<T: CaseIterable & StringRepresentable>: View {
let myEnum: [T]
#State private var selectedValue: String = ""
var body: some View {
ForEach(0..<myEnum.count) { index in
Text(myEnum[index].rawValue)
.onTapGesture {
selectedValue = myEnum[index].rawValue
}
}
}
}

Related

VStack shows error "Instance method 'sheet(item:onDismiss:content:)' requires that 'Int' conform to 'Identifiable'"

The line VStack shows error "Instance method 'sheet(item:onDismiss:content:)' requires that 'Int' conform to 'Identifiable'" but i think i dont make any mistake there? did i?
It feel like error come from my sheet complaining my Int should conform to 'Identifiable' however is not Int is an 'Identifiable' since it's primitive? how would i solve this problem?
struct SheetView: View {
#State var bindingItemDiscounts: [Double] =
[ 0.99, 0.25, 0.31, 11.99, 19.99, 0.1, 0.5]
#State var discountMoreThanTen: Int?
var body: some View {
VStack(alignment: .leading) { <- this line
Button {
discountMoreThanTen = bindingItemDiscounts.count
} label: { Text("Check Discounts") }
}.sheet(item: $discountMoreThanTen) { newValue in
if discountMoreThanTen ?? 0 <= 10 {
List(bindingItemDiscounts, id: \.self) { discount in
Text("Discount : \(discount)")
}
}
}
}
}.
There is no such concept as Primitive in Swift. The "Primitives" you refer to are normal structs.
Int does not conform to Identifiable.
To fix your problem just extend Int to conform to Identifiable like so:
extension Int: Identifiable {
public var id: Int { return self }
}
You are using the Binding<Item?> sheet. The Item is a type, so it must conform to Identifiable in order to be identified uniquely.
Also, Int is not a primitive in Swift. Int is basically a Type just like your other custom data types, so you need to make it Identifiable manually.
Add this extension in your file will fix this problem:
extension Int: Identifiable {
public var id: Self { self }
}
If UUID() is required, then use this instead:
extension Int: Identifiable {
public var id: String {
return UUID().uuidString
}
}

Text with string interpolation of Enum value, No exact matches in call to instance method 'appendInterpolation'

I'm lost, why Text("\(type)") would get compile error meantime Text(str) is not. Did that string interpolation not create a string?
For the error please check screenshot in below.
enum ExpenseType: Codable, CaseIterable {
case Personal
case Business
}
struct AddView: View {
#State private var type: ExpenseType = .Personal
let types: [ExpenseType] = ExpenseType.allCases
var body: some View {
Form {
...
Picker("Type", selection: $type) {
ForEach(types, id: \.self) { type in
let str = "\(type)"
Text(str)
// Compile error
Text("\(type)")
}
}
...
}
Xcode fails to detect which Text initializer should be used, a rather annoying bug.
Possible workarounds:
Using String(describing:) initializer:
Text(String(describing: type))
Declaring a variable at first:
let text = "\(type)"
Text(text)
You need to use rawValue, and try to loop more efficiently over the allCases.
enum ExpenseType: String, CaseIterable {
case Personal
case Business
}
struct ContentView: View {
#State var expenseType = ExpenseType.Personal
var body: some View {
List {
Picker(selection: $expenseType, label: Text("Picker")) {
ForEach(ExpenseType.allCases, id: \.self) { type in
Text(type.rawValue)
}
}
.pickerStyle(.inline)
}
}
}

SwuiftUI Custom Sorting

I'm just starting out in SwuiftUI so bear with me.
I have a Game stuct that has a field lastUpdated and title. I want to have the choice to sort by my list by lastUpdated or title. But I'm not sure how this works. I've looked into sorted(by:) but I can't really get anything to work. Suggestions?
struct Game: Identifiable, Codable {
let id: UUID
var title: String
var players: [Player]
var lastUsed: Date }
GameView
struct GameListView: View {
#Binding var games: [Game]
var body: some View {
List {
ForEach($games) { $game in
NavigationLink(destination: GameView(game: $game)) {
Text(game.title)}
}
}
The scenario is slightly complicated by the Binding form of ForEach, but you should still be able to return a sorted collection. It might look like this:
struct GameListView: View {
#Binding var games: [Game]
#State var sortType : SortType = .lastUsed
enum SortType {
case lastUsed, title
}
var sortedGames : [Binding<Game>] {
$games.sorted(by: { a, b in
switch sortType {
case .lastUsed:
return a.wrappedValue.lastUsed < b.wrappedValue.lastUsed
case .title:
return a.wrappedValue.title < b.wrappedValue.title
}
})
}
var body: some View {
List {
ForEach(sortedGames) { $game in
NavigationLink(destination: GameView(game: $game)) {
Text(game.title)}
}
}
}
}

Why can't one iterate through enum data within a SwiftUI view?

Say I have some enum, Channel, filled with sample property data:
enum Channel: CaseIterable {
case abc
case efg
case hij
var name: String {
switch self {
case .abc: return "ABC"
case .efg: return "EFG"
case .hij: return "HIJ"
}
}
}
extension Channel: Identifiable {
var id: Self { self }
}
And some view implementation to fill said content with:
struct ChannelHome: View {
var body: some View {
VStack {
ForEach(Channel.allCases) { channel in
NavigationLink(destination: ChannelDetail(channel)) {
Text(channel.name)
}
}
}
}
} // big closure yikes
Why doesn't this work? Is this due to the nature of enums not having static properties and SwiftUI being declarative? Are there ways around this? Such as:
struct ChannelData {
static let channels: [Channel] = [.abc, .efg, .hij]
// or
static func getChannels() -> [Channel] {
var channelArray: [Channel]
for channel in Channel.allCases {
channelArray.append(channel)
}
return channelArray
}
}
Or, more preferably, are there better practices for presenting small data sets like these in a SwiftUI view? I like being able to implement a visual UI of my properties for when I'm debugging, as a whole, at runtime (closer to the production stage).
I apologies in advance for the question, I am just getting into it with SwiftUI.
Here are 2 simple ways for you, but not sure where can be helpful for you because with enum you cannot update ForEach elements, but it has a use case for showing data!
First way without any Identifiable id:
struct ContentView: View {
var body: some View {
ForEach(TestEnum.allCases, id: \.rawValue) { item in
Text(item.rawValue)
}
}
}
enum TestEnum: String, CaseIterable { case a, b, c }
with Identifiable id that conforms to Identifiable protocol, you can cut , id: \.id even:
struct ContentView: View {
var body: some View {
ForEach(TestEnum.allCases, id: \.id) { item in
Text(item.rawValue)
}
}
}
enum TestEnum: String, CaseIterable, Identifiable { case a, b, c
var id: String { return self.rawValue }
}
Either Xcode was getting hung up on cache errors or I am a fool (probably the latter). Regardless, the issue seems to have been resolved with the following Identifiable extension to your enum, as well as a good ol' derived data purge and clean:
extension Channel: Identifiable {
var id: Self { self }
}
I seem to have confused this issue with another long-time issue that I've constantly run into; mostly regarding enums and their distaste for storing static data (as is simply just the nature of their fast and lite structure).

SwiftUI String conform Identifiable seems not correct

I am using an array in a List, in the List there's a ForEach, like:
struct AView: View {
#State var foo: [String] = ["a", "b", "c"]
var body: some View {
ZStack {
Color.white
List {
ForEach(foo.indices) { index in
Text(foo[index])
}
}
}
}
}
This works well, then I want to add a button to insert new items:
List {
ForEach(foo.indices) { index in
Text(foo[index])
}
}
Button("Add") {
foo.append("foo")
}
}
Then I got the error, which is obviously:
ForEach<Range<Int>, Int, Text> count (4) != its initial count (3). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
Here mentioned
Identifiable or use ForEach(_:id:content:)
I can use ForEach(foo.indices, id:\.self) which solved the issue.
I also want to try Identifiable, and don't use id:\.self in the ForEach, ForEach(foo.indices).
I added extension to String like:
extension String: Identifiable {
public var id: String { self }
}
But still got the same issue. Any thing I miss-understood? thanks!
EDIT
According to the comment #New Dev, as I literal indices, so I added extension to Int:
extension Int: Identifiable {
public var id: Int { self }
}
still doesn't work.
ForEach has multiple init overloads.
init(Range<Int>, content: (Int) -> Content) only works with a constant range - hence the error.
init(Data, content: (Data.Element) -> Content) requires Data to conform to RandomAccessCollection of Identifiable elements. That's the one you want to use.
The problem is that your RandomAccessCollection (to which Range<Int> conforms) is a collection of Int elements.
Though you can conform Int to Identifiable, this would still not work. It would still use the first ForEach.init with a Range<Int> parameter, because of method overload preference - i.e. Range<Int> matches the more specific init with Range<Int> parameter, rather than the less specific init with RandomAccessCollection.
So, your choices are:
Use the third init(Data, id: KeyPath<Data.Element, ID>, content: (Data.Element) -> Content) by explicitly specifying the id.
ForEach(foo.indices, id:\.self) { index in
}
Convert to Array<Int> and conform Int: Identifiable:
extension Int: Identifiable { var id: Self { self } }
ForEach(Array(foo.indices)) { index in
}

Resources