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).
Related
I am making an app with various screens, and I want to collect all the screens that all conform to View in an array of views. On the app's main screen, I want to show a list of all the screen titles with a NavigationLink to all of them.
The problem I am having is that if I try to create a custom view struct and have a property that initialises to a given screen and returns it. I keep running into issues and the compiler forces me to change the variable to accept any View rather than just View or the erased type some View.
struct Screen: View, Identifiable {
var id: String {
return title
}
let title: String
let destination: any View
var body: some View {
NavigationLink(destination: destination) { // Type 'any View' cannot conform to 'View'
Text(title)
}
}
}
This works:
struct Screen<Content: View>: View, Identifiable {
var id: String {
return title
}
let title: String
let destination: Content
var body: some View {
NavigationLink(destination: destination) {
Text(title)
}
}
}
But the issue with this approach is that I cannot put different screens into an Array which can be fixed as follows:
struct AllScreens {
var screens: [any View] = []
init(){
let testScreen = Screen(title: "Charties", destination: SwiftChartsScreen())
let testScreen2 = Screen(title: "Test", destination: NavStackScreen())
screens = [testScreen, testScreen2]
}
}
But when I try to access the screens in a List it cannot infer what view it is without typecasting. The end result I am trying to achieve is being able to pass in an array of screens and get their title displayed in a list like below.
At the moment I can only achieve this by hardcoding the lists elements, which works.
import SwiftUI
struct MainScreen: View {
let screens = AppScreens.allCases
var body: some View {
NavigationStack() {
List() {
AppScreens.chartsScreen
AppScreens.navStackScreen
}
.navigationTitle("WWDC 22")
}
}
}
SwiftUI is data-driven reactive framework and Swift is strict typed language, so instead of trying to put different View types (due to generics) into one array (requires same type), we can make data responsible for providing corresponding view (that now with help of ViewBuilder is very easy).
So here is an approach. Tested with Xcode 13+ / iOS 15+ (NavigationView or NavigationStack - it is not important)
enum AllScreens: CaseIterable, Identifiable { // << type !!
case charts, navStack // << known variants
var id: Self { self }
#ViewBuilder var view: some View { // corresponding view !!
switch self {
case .charts:
Screen(title: "Charties", destination: SwiftChartsScreen())
case .navStack:
Screen(title: "Test", destination: NavStackScreen())
}
}
}
usage is obvious:
struct MainScreen: View {
var body: some View {
NavigationStack { // or `NavigationView` for backward compatibility
List(AllScreens.allCases) {
$0.view // << data knows its presenter
}
.navigationTitle("WWDC 22")
}
}
}
Test module on GitHub
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)
}
}
}
For structs, Swift auto synthesizes the hashValue for us. So I am tempting to just use it to conform to the identifiable.
I understand that hashValue is many to one, but ID must be one to one. However, hash collision is very rare and I have faith in the math that it's most likely never going to happen to my app before I die.
I am wondering is there any other problems besides collision?
This is my code:
public protocol HashIdentifiable: Hashable & Identifiable {}
public extension HashIdentifiable {
var id: Int {
return hashValue
}
}
using hashValue for id is a bad idea !
for example you have 2 structs
struct HashID: HashIdentifiable {
var number: Int
}
struct NormalID: Identifiable {
var id = UUID()
var number: Int
}
when number is changed:
HashID's id will be changed as well makes SwiftUI thinks that this is completely new item and old one is gone
NormalID's id stays the same, so SwiftUI knows that item only modified its property
It's very important to let SwiftUI knows what's going on, because it will affect animations, performance, ... That's why using hashValue for id makes your code looks bad and you should stay away from it.
I am wondering is there any other problems besides collision?
Yes, many.
E.g., with this…
struct ContentView: View {
#State private var toggleData = (1...5).map(ToggleDatum.init)
var body: some View {
List($toggleData) { $datum in
Toggle(datum.display, isOn: $datum.value)
}
}
}
Collisions cause complete visual chaos.
struct ToggleDatum: Identifiable & Hashable {
var value: Bool = false
var id: Int { hashValue }
var display: String { "\(id)" }
init(id: Int) {
// Disregard for now.
}
}
No collisions, with unstable identifiers, breaks your contract of "identity" and kills animation. Performance will suffer too, but nobody will care about that when it's so ugly even before they notice any slowness or battery drain.
struct ToggleDatum: Identifiable & Hashable {
var value: Bool = false
var id: Int { hashValue }
let display: String
init(id: Int) {
self.display = "\(id)"
}
}
However, while it is not acceptable to use the hash value as an identifier, it is fine to do the opposite: use the identifier for hashing, as long as you know the IDs to be unique for the usage set.
/// An `Identifiable` instance that uses its `id` for hashability.
public protocol HashableViaID: Hashable, Identifiable { }
// MARK: - Hashable
public extension HashableViaID {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct ToggleDatum: HashableViaID {
var value: Bool = false
let id: Int
var display: String { "\(id)" }
init(id: Int) {
self.id = id
}
}
This protocol works perfectly for Equatable classes, as classes already have a default ID ready for use.
extension Identifiable where Self: AnyObject {
public var id: ObjectIdentifier {
return ObjectIdentifier(self)
}
}
Not that I at all recommend using a reference type for this example, but it would look like this:
public extension Equatable where Self: AnyObject {
static func == (class0: Self, class1: Self) -> Bool {
class0 === class1
}
}
class ToggleDatum: HashableViaID {
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
}
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
}
}
}
}