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")
}
}
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)
}
I am trying to show a list of Orders in a list using LazyColumn. Here is the code:
#Composable
private fun MyOrders(
orders: List<Order>?,
onClick: (String, OrderStatus) -> Unit
) {
orders?.let {
LazyColumn {
items(
items = it,
key = { it.id }
) {
OrderDetails(it, onClick)
}
}
}
}
#Composable
private fun OrderDetails(
order: Order,
onClick: (String, OrderStatus) -> Unit
) {
println("Composing Order Item")
// Item Code Here
}
Here is the way, I call the composable:
orderVm.fetchOrders()
val state by orderVm.state.collectAsState(OrderState.Empty)
if (state.orders.isNotEmpty()) {
MyOrders(state.orders) {
// Handle status change click listener
}
}
I fetch all my orders and show in the LazyColumn. However, when a single order is updated, the entire LazyColumn gets rrecomposed. Here is my ViewModel looks like:
class OrderViewModel(
fetchrderUseCase: FetechOrdersUseCase,
updateStatusUseCase: UpdateorderUseCase
) {
val state = MutableStateFlow(OrderState.Empty)
fun fetchOrders() {
fetchrderUseCase().collect {
state.value = state.value.copy(orders = it.data)
}
}
fun updateStatus(newStatus: OrderStatus) {
updateStatusUseCase(newStatus).collect {
val oldOrders = status.value.orders
status.value = status.value.copy(orders = finalizeOrders(oldOrders))
}
}
}
NOTE: The finalizeOrders() does some list manipulation based on orderId to update one order with the updated one.
This is how my state looks like:
data class OrderState(
val orders: List<Order> = listOf(),
val isLoading: Boolean = false,
val error: String = ""
) {
companion object {
val Empty = FetchOrdersState()
}
}
If I have 10 orders in my DB and I update one's status (let's say 5th item), then OrderDetails gets called for 20 times. Not sure why. Caan I optimize it to make sure only the 5th indexed item will be recomposed and the OrderDetals gets called only with the new order.
Is the Orderclasss stable? If not it could be the reason why all the items get recomposed:
Compose skips the recomposition of a composable if all the inputs are stable and haven't changed. The comparison uses the equals method
This section in the compose's doc explains what are stable types and how to skip recomposition.
Note: If you scroll a lazy list, all invisible items will be destroyed. That means if you scroll back they will be recreated not recomposed (you can't skip recreation even if the input is stable).
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
}
I have an array of closures, within a class (I'm declaring this inside a UIViewController that is a table view), and want to set actions for one of my cells based on a closure. This is my code:
var actionItem : (Int)->Void = {
(index: Int)->Void in
if(self.pickedRoles[index] == "___") {
self.pickedRoles[index] = self.roles[index];
} else {
self.pickedRoles[index] = "___";
}
}
var roleActions : Array<(Int)->Void> = [{
actionItem(0);
}, {
actionItem(1);
}, {
actionItem(2);
}, {
actionItem(3);
}, {
actionItem(4);
}];
actionItem is my closure, pickedRoles is a class variable which is an array of Strings, similarly with roles. I want roleActions to represent what happens when a user selects a role, but at the line declaring roleActions, I get an error stating:
'Int' is not a subtype of '()'
What do I do to solve this?
Well, think about it:
actionItem is a function that takes an Int and returns Void.
actionItem(0) is a call to that function, so it is a Void.
So {actionItem(0)} is an anonymous function (a closure, as you call it) that takes Void and returns Void.
Well, you are trying to put that into an array of (Int)->Void. Clearly that's a typological mismatch: an Int input is not a Void input. And that's exactly what the compiler is telling you.
Frankly I don't see what any of this has to do with closures anyway! Your goal, as you have agree in a comment, is: "I've got 5 buttons. I want the first button, when tapped, to toggle the first item in an array, the second button to toggle the second item in an array, and so on."
So what I would do is: I would simply attach tags to each button - say, 100, 101, 102, 103, 104. I would give them each the same action method. When that method is called, we subtract 100 from the tag of the sender. Now we have the index! Now we toggle the value of that index of the array.
func doButton(sender:UIView) {
let index = view.tag - 100
if(self.pickedRoles[index] == "___") {
self.pickedRoles[index] = self.roles[index];
} else {
self.pickedRoles[index] = "___";
}
}
There are 2 possible ways to fix the compilation error, but I don't know which one is the correct one, depending in what you are trying to do.
The array of closures is filled in with parameterless closures:
{ Void -> Void in actionItem(0) }
So the array declaration is incorrect, the contained type should be Void -> Void - the fixed code is:
var roleActions : Array<Void -> Void> = [{
actionItem(0);
}, {
actionItem(1);
}, {
actionItem(2);
}, {
actionItem(3);
}, {
actionItem(4);
}]
Alternatively, if the type of the element contained in the array is correct, basing on your app logic, then you just have to skip the integer parameter that's passed to each closure, using _ in:
var roleActions : Array<Int -> Void> = [{
_ in actionItem(0);
}, {
_ in actionItem(1);
}, {
_ in actionItem(2);
}, {
_ in actionItem(3);
}, {
_ in actionItem(4);
}]
As #matt and #Antonio suggested, my initial approach was faulty, but to get the desired behavior in the end, I had to go a step further than either of their solutions.
To accomplish what #matt commented, which was my goal (to have 5 buttons where each button toggles an element in the array), I had to go with the following code:
var actionItem : ((Int)->(Void->Void)) = {
(index: Int) in
return {
if(self.pickedRoles[index] == "___") {
self.pickedRoles[index] = self.roles[index];
} else {
self.pickedRoles[index] = "___";
}
};
}
var roleActions : [Void->Void] = [actionItem(0), actionItem(1), actionItem(2), actionItem(3), actionItem(4)];
What this gives me is an array of closures, where each closure can be called with no parameters to simply toggle it's respective element in the pickedRoles array.