SwiftUI ForEach with array or sub-array
You don’t have to use $ sign in front of your arrays.
ForEach(isExpanded ? items[..<items.count] : items[..<4], id: \.self) { sub in
}
@Binding and ForEach in SwiftUI
You can use something like the code below. Note that you will get a deprecated warning, but to address that, check this other answer: https://stackoverflow.com/a/57333200/7786555
import SwiftUI
struct ContentView: View {
@State private var boolArr = [false, false, true, true, false]
var body: some View {
List {
ForEach(boolArr.indices) { idx in
Toggle(isOn: self.$boolArr[idx]) {
Text("boolVar = \(self.boolArr[idx] ? "ON":"OFF")")
}
}
}
}
}
In SwiftUI, how to iterate over array that is and attribute of a custom object
you mean like this?
import SwiftUI
struct Role : Identifiable, Hashable {
var id = UUID().uuidString
var roleName: String
var roleActions: [String]
}
struct RowView : View {
var role : Role
var body: some View {
ScrollView(.horizontal, content: {
HStack {
ForEach(role.roleActions, id: \.self) { action in
Text(action)
}
}
})
}
}
struct ContentView: View {
var roles = [Role(roleName: "Witch", roleActions: ["Action 1","Action 2"]),
Role(roleName: "Hero", roleActions: ["Action 3", "Action4"])]
var body: some View {
List (roles, id: \.self) { role in
Text(role.roleName)
.font(.title).fontWeight(.heavy)
VStack() {
Text("Choose Your Actions")
.font(.headline)
RowView(role: role)
}
}.environment(\.defaultMinListRowHeight, 140)
}
}
Using ForEach with a an array of Bindings (SwiftUI)
Trying a different approach. The FormField maintains it's own internal state and publishes (via completion) when its text is committed:
struct FormField : View {
@State private var output: String = ""
let viewModel: FormFieldViewModel
var didUpdateText: (String) -> ()
var body: some View {
VStack {
TextField($output, placeholder: Text(viewModel.placeholder), onCommit: {
self.didUpdateText(self.output)
})
Line(color: Color.lightGray)
}.padding()
}
}
ForEach(viewModel.viewModels) { vm in
FormField(viewModel: vm) { (output) in
vm.output = output
}
}
Using a protocol array with ForEach and bindable syntax
This is a thorny problem with your data types.
If you can change your data types, you can make this easier to solve.
For example, maybe you can model your data like this instead, using an enum
instead of a protocol
to represent the variants:
struct Testable {
let id: UUID
var name: String
var variant: Variant
enum Variant {
case animal(Animal)
case human(Human)
}
struct Animal {
var owner: String
}
struct Human {
var age: Int
}
}
It will also help to add accessors for the two variants' associated data:
extension Testable {
var animal: Animal? {
get {
guard case .animal(let animal) = variant else { return nil }
return animal
}
set {
guard let newValue = newValue, case .animal(_) = variant else { return }
variant = .animal(newValue)
}
}
var human: Human? {
get {
guard case .human(let human) = variant else { return nil }
return human
}
set {
guard let newValue = newValue, case .human(_) = variant else { return }
variant = .human(newValue)
}
}
}
Then you can write your view like this:
class ContentViewModel: ObservableObject {
@Published var testables: [Testable] = []
}
struct ContentView: View {
@StateObject var vm: ContentViewModel = ContentViewModel()
var body: some View {
VStack {
List {
ForEach($vm.testables, id: \.id) { $testable in
VStack {
TextField("Name", text: $testable.name)
if let human = Binding($testable.human) {
Stepper("Age: \(human.wrappedValue.age)", value: human.age)
}
else if let animal = Binding($testable.animal) {
HStack {
Text("Owner: ")
TextField("Owner", text: animal.owner)
}
}
}
}
}
HStack {
Button("Add animal") {
vm.testables.append(Testable(
id: UUID(),
name: "Mick",
variant: .animal(.init(owner: "harry"))
))
}
Button("Add Human") {
vm.testables.append(Testable(
id: UUID(),
name: "Ash",
variant: .human(.init(age: 26))
))
}
}
}
}
}
SwiftUI - Using an Array of Ints that is wrapped with @Binding
How about trying something like this example code,
using .onAppear{...}
for updating each episode with the showTitle.count
.
Also removing this "appending" from analyzeShowTitle
function. This append(...)
should not be used within the ForEach
loop, because it triggers a view refresh,
which then triggers another append(...)
etc...
struct PlayButton: View {
// input API
@Binding var isPlaying: Bool
var episodes: [Episode]
// output API
@Binding var charactersInShowTitle: [Int]
var body: some View {
Button(action: {
isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
ForEach(episodes) { episode in
Text("CHILD: \(analyzeShowTitle(episode))")
}
.onAppear { // <-- here
for episode in episodes {
charactersInShowTitle.append(episode.showTitle.count)
}
}
}
func analyzeShowTitle( _ episode: Episode ) -> String {
return "\( episode.showTitle ) - \( episode.showTitle.count ) chars" // <-- here
}
}
Iterating over array of Codable in SwiftUI
Your code works fine the way it is for printing to the console, but ForEach
requires that GeoResult
conforms to either Identifiable
(preferred) or at least Hashable
. Given that you didn't include the property id
in your code, let's have that struct conforming to Hashable
.
So, assuming that each GeoResult
is different because formatted_address
is never the same (you must check if that's true), you can add two functions to ensure conformance. You will get the following:
struct GeoResult: Codable, Hashable { // <- Conform to Hashable
// Differentiating
static func == (lhs: GeoResult, rhs: GeoResult) -> Bool {
lhs.formatted_address == rhs.formatted_address
}
// Hashing
func hash(into hasher: inout Hasher) {
hasher.combine(formatted_address)
}
struct Geometry: Codable {
struct Location: Codable {
let lat: Float
let lng: Float
init() {
lat = 32
lng = 30
}
}
let location: Location
}
let formatted_address: String
let geometry: Geometry
}
In the view, add an array of GeoResult
, that will be the @State
variable to iterate over. Place the .task()
modifier on the outermost view.
// This is the list
@State private var geoArray: [GeoResult] = []
var body: some View {
NavigationView {
VStack {
// GeoResult is not Identifiable, so it is necessary to include id: \.self
ForEach(geoArray, id: \.self) { result in
NavigationLink {
Text("Lat/long: (\(result.geometry.location.lat), \(result.geometry.location.lng))")
} label: {
Text("Address: \(result.formatted_address)")
}
}
.navigationTitle("Quotes")
}
}
// Attach the task to the outermost view, in this case the NavigationView
.task {
await handleData()
}
}
Finally, change the @State
variable in your function, after decoding:
func handleData() async {
// ...
let decoder = JSONDecoder()
do {
let obj = try decoder.decode(GeoService.self, from: geoResult)
// Add this
geoArray = obj.results
} catch {
print("Did not work :(\n\(error)")
}
}
How to bind an array and List if the array is a member of ObservableObject?
The fix
Change your ForEach
block to
ForEach(model.results, id: \.self) { text in
Text(text)
}
Explanation
SwiftUI's error messages aren't doing you any favors here. The real error message (which you will see if you change Text(text)
to Text(text as String)
and remove the $
before model.results
), is "Generic parameter 'ID' could not be inferred".
In other words, to use ForEach
, the elements that you are iterating over need to be uniquely identified in one of two ways.
- If the element is a struct or class, you can make it conform to the Identifiable protocol by adding a property
var id: Hashable
. You don't need theid
parameter in this case. - The other option is to specifically tell
ForEach
what to use as a unique identifier using theid
parameter. Update: It is up to you to guarentee that your collection does not have duplicate elements. If two elements have the same ID, any change made to one view (like an offset) will happen to both views.
In this case, we chose option 2 and told ForEach
to use the String element itself as the identifier (\.self
). We can do this since String conforms to the Hashable protocol.
What about the $
?
Most views in SwiftUI only take your app's state and lay out their appearance based on it. In this example, the Text views simply take the information stored in the model and display it. But some views need to be able to reach back and modify your app's state in response to the user:
- A Toggle needs to update a Bool value in response to a switch
- A Slider needs to update a Double value in response to a slide
- A TextField needs to update a String value in response to typing
The way we identify that there should be this two-way communication between app state and a view is by using a Binding<SomeType>
. So a Toggle requires you to pass it a Binding<Bool>
, a Slider requires a Binding<Double>
, and a TextField requires a Binding<String>
.
This is where the @State
property wrapper (or @Published
inside of an @ObservedObject
) come in. That property wrapper "wraps" the value it contains in a Binding
(along with some other stuff to guarantee SwiftUI knows to update the views when the value changes). If we need to get the value, we can simply refer to myVariable
, but if we need the binding, we can use the shorthand $myVariable
.
So, in this case, your original code contained ForEach($model.results)
. In other words, you were telling the compiler, "Iterate over this Binding<[String]>
", but Binding
is not a collection you can iterate over. Removing the $
says, "Iterate over this [String]," and Array is a collection you can iterate over.
Binding in a ForEach in SwiftUI
Assuming your elements
is state of items array, it can be
List {
ForEach(elements.indices, id: \.self) { i in
CheckBoxView(checked: $elements[i].checked)
}
}
Related Topics
Why Does a Public Class/Struct in Swift Require an Explicit Public Initializer
Code Migration from Swift 2.X to Swift 4
Swift - Lazy Var VS. Let When Creating Views Programmatically (Saving Memory)
Why Are Iboutlets Optionals After Swift 5 Migration
Scene Kit Performance with Cube Test
Swiftui - How to Use Oncommand with Nsmenuitem on MACos
Proper Model for Multiple Alamofire Requests for Multiple Websites
How to Add a Bottom Line on Textfield (Swiftui)
Optional Chaining with Swift Strings
Executefetchrequest Doesn't Return the Nsmanagedobject Subclass
Storyboard Entry Point Missing
Accessing a String Enum by Index
What Language Is Swift Written In
Realitykit - Set Text Programmatically of an Entity of Reality Composer
How to Get the Centre of the View
Sprite-Kit: Moving an Element in Circular Path