Can't have conditional in SwiftUI ForEach - ios

I'm learning SwiftUI at the moment. I've been playing around with loading a list from CoreData and making changes on / filtering etc. I've run into the issues below. Essentially as soon as I try to apply any conditionals within the ForEach I I'm presented with that error.
This works if I run iterate through the organisations in List itself rather than a ForEach. This isn't the ideal solution as I loose the inbuilt deletion function.
Am I missing something stupid?
let defaults = UserDefaults.standard
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Organisation.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Organisation.name, ascending: true)])
var orgs: FetchedResults<Organisation>
var body: some View
{
NavigationView {
List {
ForEach(orgs, id: \.self) {org in
if !self.userData.showFavsOnly || org.isFavorite {
NavigationLink(destination: OrganisationView(org: org, moc: self.managedObjectContext)) {
OrganisationRow(org: org)
}
}
}
}
}
}
There error code I get is I get is on the for each line and is
Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
Thanks for your help

Type '()' cannot conform to 'View'; only struct/enum/class types can
conform to protocols
This error means that your ForEach loop expects some View. But you give it an if-statement instead. What if the condition returns false?
The solution may be to wrap the if-statement in some View - it could be a Group, VStack, ZStack...
ForEach(orgs, id: \.self) { org in
Group {
if !self.userData.showFavsOnly || org.isFavorite {
NavigationLink(destination: OrganisationView(org: org, moc: self.managedObjectContext)) {
OrganisationRow(org: org)
}
}
}
}

Related

Binding To Array Values SwiftUI iOS

I'm struggling to find how to bind an array value to a Toggle view in SwiftUI.
Lets says I have an observabled class with a Boolean array:
class TestClass: ObservabledObject {
#Published var onStates: [Bool] = [true, false, true]
static let shared = TestClass()
}
and in a View I have
...
Toggle(isOn: TestClass.shared.$onStates[0]) { // Throws error 'Referenceing subscript 'subscript(_:)' requires wrapped value of type '[Bool]'
Text("Example Toggle")
}
Why is it seemingly impossible to bind a particular array value to the toggle button?
Thanks.
We need observer in view for observable object, so fixed variant is
#StateObject var vm = TestClass.shared // << observer
var body: some View {
Toggle(isOn: $vm.onStates[0]) { // << binding via observer
Text("Example Toggle")
}
}

SwiftUI Setting Bind<String> in Text field

I've got the following object that contain list of strings:
class handler: ObservableObject {
#Published var items = [String]()
}
In some closure, I set this list with valid values :
for item in item_list! {
self.handler.items.append(item.stringData)
}
and in the ContentView part I've got Picker that support to present that list of strings in realtime:
VStack {
Picker("items", selection: $arg1) {
ForEach($handler.items, id: \.self) {
Text($0)
}
}
}
However, it fails to compile due to the following reason :
Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'
Any Idea how to resolve this ?
You don't need binding here to loop items, instead use handler directly (ie. without $), like
ForEach(handler.items, id: \.self) { // << here !!
Text($0)
}

Is there a way to create a binding off a computed array property in an enum at a particular index in SwiftUI?

I'm modelling view state in my viewModel using an enum...
enum ViewState<T> {
case idle
case error(Error)
case loading
case data([T])
I have a computed property to get the data
var data: [T] {
guard case let .data(data) = self else {
return []
}
return data
}
In one of my views I iterate through the data
var dropdownListView: some View {
ForEach(viewModel.state.data.indices, id: \.self) { index in
DropdownView(
viewModel: $viewModel.state.data[index],
isActionSheetPresented: $viewModel.isActionSheetPresented
)
}.eraseToAnyView()
}
I get an error as you can't make a binding from a computed property so make my own custom binding...
ForEach(viewModel.state.data.indicies, id: \.self) { index in
DropdownView(viewModel: Binding<ItemViewModel>(
get: {return viewModel.state.data[index] },
set: { value in
var data = viewModel.state.data
data[index] = value
viewModel.state = .data(data)
},
isActionSheetPresented: $viewModel.isActionSheetPresented
)
}
This works but are there any issues with setting the whole state again in the binding setter (I believe SwiftUI is intelligent enough that this would be efficient) or is there another way to do this here?
On my vision you mixed a state and a data, which are different things. So instead of .data([T]), I would recommend something like .loaded (ie, state) and keep data by standalone #Published var data: [T] property. If that adapted your code will look much more naturally.
Like
ForEach(viewModel.data.indices, id: \.self) { index in
DropdownView(
viewModel: $viewModel.data[index],
isActionSheetPresented: $viewModel.isActionSheetPresented
)
}//.eraseToAnyView() // << you don't need this
}

SwiftUI: How to use `!` operator before `$` bindable object operator?

I'm unable to use logical not ! operator with Bindable $ object.
Here is the scenario I want-
struct ContentView: View {
#State private var isLoggedIn:Bool = true
var body: some View {
Text("Root View")
.sheet(isPresented: !self.$isLoggedIn) {
SignInView()
}
.onAppear { self.performAuthentication() }
}
}
Sign In View should present as soon as I set isLoggedIn = false by some button action. For which I have to use logical not operator before $.
Compiler error: Cannot convert value of type 'Binding' to
expected argument type 'Bool'
How can I achieve this?
As I said in comment to question there were approach posted for SwiftUI: transform Binding into another Binding. If you, however, want to have it explicitly as operator, you can use the following (tested & works with Xcode 11.2)
extension Binding where Value == Bool {
static prefix func !(_ lhs: Binding<Bool>) -> Binding<Bool> {
return Binding<Bool>(get:{ !lhs.wrappedValue },
set: { lhs.wrappedValue = !$0})
}
}

How to conform an enumeration to Identifiable protocol in Swift?

I'm trying to make a list with the raw values of the cases from an enumeration with the new SwiftUI framework. However, I'm having a trouble with conforming the 'Data' to Identifiable protocol and I really cannot find information how to do it. It tells me "Initializer 'init(_:rowContent:)' requires that 'Data' conform to 'Identifiable'" The stub provides me with an ObjectIdentifier variable in the last extension, but don't know what should I return. Could you tell me how do it? How do I conform Data to Identifiable, so I can make a list with the raw values?
enum Data: String {
case firstCase = "First string"
case secondCase = "Second string"
case thirdCase = "Third string"
}
extension Data: CaseIterable {
static let randomSet = [Data.firstCase, Data.secondCase]
}
extension Data: Identifiable {
var id: ObjectIdentifier {
return //what?
}
}
//-------------------------ContentView------------------------
import SwiftUI
struct Lala: View {
var name: String
var body: some View {
Text(name)
}
}
struct ContentView: View {
var body: some View {
return List(Data.allCases) { i in
Lala(name: i.rawValue)
}
}
}
⚠️ Try not to use already used names like Data for your internal module. I will use MyEnum instead in this answer
When something conforms to Identifiable, it must return something that can be identified by that. So you should return something unique to that case. For String base enum, rawValue is the best option you have:
extension MyEnum: Identifiable {
var id: RawValue { rawValue }
}
Also, enums can usually be identified by their selves:
extension MyEnum: Identifiable {
var id: Self { self }
}
⚠️ Note 1: If you return something that is unstable, like UUID() or an index, this means you get a new object each time you get the object and this will kill reusability and can cause epic memory and layout process usage beside view management issues like transition management and etc.
Take a look at this weird animation for adding a new pet:
Note 2: From Swift 5.1, single-line closures don't need the return keyword.
Note 3: Try not to use globally known names like Data for your own types. At least use namespace for that like MyCustomNameSpace.Data
Inline mode
You can make any collection iterable inline by one of it's element's keypath:
For example to self:
List(MyEnum.allCases, id:\.self)
or to any other compatible keypath:
List(MyEnum.allCases, id:\.rawValue)
✅ The checklist of the identifier: (from WWDC21)
Exercise caution with random identifiers.
Use stable identifiers.
Ensure the uniqueness, one identifier per item.
Another approach with associated values would be to do something like this, where all the associated values are identifiable.
enum DataEntryType: Identifiable {
var id: String {
switch self {
case .thing1ThatIsIdentifiable(let thing1):
return thing1.id
case .thing2ThatIsIdentifiable(let thing2):
return thing2.id
}
}
case thing1ThatIsIdentifiable(AnIdentifiableObject)
case thing2ThatIsIdentifiable(AnotherIdentifiableObject)
You can try this way:
enum MyEnum: Identifiable {
case valu1, valu2
var id: Int {
get {
hashValue
}
}
}
Copyright © 2021 Mark Moeykens. All rights reserved. | #BigMtnStudio
Combine Mastery in SwiftUI book
enum InvalidAgeError: String, Error , Identifiable {
var id: String { rawValue }
case lessThanZero = "Cannot be less than zero"
case moreThanOneHundred = "Cannot be more than 100"
}

Resources