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

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
}

Related

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)
}

Can't have conditional in SwiftUI ForEach

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)
}
}
}
}

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"
}

rxswift viewmodel with input output

I am trying to achieve something similar in rxswift example project from RxSwift repo. But in my case there are dependent observables. I couldn't find any solution without using binding in viewmodel
Here is the structure of my viewmodel:
First the definitions of input, output and viewmodel
typealias UserListViewModelInput = (
viewAppearAction: Observable<Void>,
deleteAction: Observable<Int>
)
typealias UserListViewModelOutput = Driver<[User]>
typealias UserListViewModel = (UserListViewModelInput, #escaping UserApi) -> UserListViewModelOutput
Then there is actual implementation which doesn't compile.
let userListViewModel: UserListViewModel = { input, loadUsers in
let loadedUserList = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.elements()
.asDriver(onErrorDriveWith: .never())
let userListAfterDelete = input.deleteAction
.withLatestFrom(userList) { index, users in
users.enumerated().compactMap { $0.offset != index ? $0.element : nil }
}
.asDriver(onErrorJustReturn: [])
let userList = Driver.merge([loadedUserList, userListAfterDelete])
return userList
}
Viewmodel has two job. First load the user list. Second is delete a user at index. The final output is the user list which is downloaded with UserApi minus deleted users.
The problem in here in order the define userList I need to define userListAfterDelete. And in order to define userListAfterDelete I need to define userList.
So is there a way to break this cycle without using binding inside view model? Like a placeholder observable or operator that keeps state?
This is a job for a state machine. What you will see in the code below is that there are two actions that can affect the User array. When the view appears, a new array is downloaded, when delete comes in, a particular user is removed.
This is likely the most common pattern seen in reactive code dealing with state. So common that there are whole libraries that implement some variation of it.
let userListViewModel: UserListViewModel = { input, loadUsers in
enum Action {
case reset([User])
case delete(at: Int)
}
let resetUsers = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.compactMap { $0.element }
.map { Action.reset($0) }
let delete = input.deleteAction.map { Action.delete(at: $0) }
return Observable.merge(resetUsers, delete)
.scan(into: [User](), accumulator: { users, action in
switch action {
case let .reset(newUsers):
users = newUsers
case let .delete(index):
users.remove(at: index)
}
})
.asDriver(onErrorJustReturn: [])
}

Swift: can properties be required based on another property being set?

I want to re-use a single viewcontroller but re-purposed slightly and I'm wondering if there is a structured way to require some properties to be set based on another property.
For example assume that the viewcontroller has the following properties
- var displayMode: DisplayMode // see below
- var id: Int
- var description: String
- var name: String
If we are in quickView mode then I expect a ID and Description values to be set.
Else if we are in defaultView mode I expect the Name property to be set.
enum DisplayMode {
case quickView
case defaultView
}
Obviously I could just set those and expect them to be set but I'm wondering if there is a structured Swift-like way of forcing this, like having the properties nested in the DisplayMode type?
Consider using associated values for your enum cases, like so:
enum DisplayMode {
case quickview(id: Int, description: String)
case defaultview(name: String)
}
This forces the user to provide valid associated values whenever a DisplayMode variable is declared:
var mode = DisplayMode.quickView(id: 11, description: "Prosecco")
To get the associated values back out, you bind them to variables in your switch:
switch mode {
case let .quickView(id, description):
// do something with id and description
case let .defaultview(name):
// do something with name
}
By using associated values, you wouldn't have to declare stand-alone properties (i.e. object variables) for id, description, or name.
I think something like this is what you want:
var displayMode: DisplayMode {
didSet {
if displayMode == quickview {
// self.id = whatever
// do whatever else you want
}
else if displayMode == default {
// self.id = whatever
// do whatever else you want
}
}
}
var id: Int
var description: String
var name: String
(Edit: Of course, it'd be a little different because you're using an enum, but you get the gist of it.)
Another option might be to use KVO. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html

Resources