SwiftUI ForEach printing duplicate items - ios

In my application I am creating various arrays of Identifiable structs using my API response. I am iterating over said array to build lists within the Content and Sidebar columns of my Navigation Split View. If I print the array before my ForEach call, the array is normal. When printing each item from within the ForEach (let _ = print(item)) the item prints twice. However the item is only added to the List once. Is this normal behavior? It appears to be happening with all of my ForEach calls. Visually the view looks correct, just want to be sure there isn’t any additional looping or view updated occurring.
Printing each item of array. Resulting in duplicate prints.
//
// TeamView.swift
// myDashboard
//
// Created by nl492k on 10/18/22.
//
import SwiftUI
struct TeamView: View {
var user: loggedInUser
var viewData = apiData()
// viewData is an instance of the apiData struct that includes 2 Arrays of identifieable structs ("gauges" & "trends") and a "team" struct that containts an array of idenfifiable structs "teamMembers" viewData is a singular object that is updated by the completion handler of my API call.
// struct apiData {
// var gauges : Array<gaugeObj>
// var trends : Array<trendObj>
// var team : teamObj
//
// init(gauges : Array<gaugeObj> = Array<gaugeObj>(), trends: Array<trendObj> = Array<trendObj>(), team: teamObj = teamObj()) {
// self.gauges = gauges
// self.trends = trends
// self.team = team
// }
// }
#Binding var uid_selection: String?
var emulation_uid: String
var body: some View {
if viewData.team.attuid == "" {
Label("Not Signed In", systemImage: "person.crop.circle.fill.badge.questionmark")
}
else {
List(selection: $uid_selection){
HStack {
NavigationLink(value: viewData.team.superv) {
AsyncImage(url: URL(string: "\(userImageUrl)\(viewData.team.attuid)")) { image in
image.resizable()
.clipShape(Circle())
.shadow(radius: 10)
.overlay(Circle().stroke(Color.gray, lineWidth: 2))
} placeholder: {
ProgressView()
}
.frame(width:30, height: 35)
VStack (alignment: .leading){
Text("\(viewData.team.fName) \(viewData.team.lName)")
Text("\(viewData.team.jobTitle)")
.font(.system(size: 10, weight: .thin))
}
}
Label("", systemImage:"arrow.up.and.person.rectangle.portrait")
}
Divider()
//------ This prints the Array of identifiable structs, as expected, with no issues --------
let _ = print(viewData.team.teamMembers)
ForEach(viewData.team.teamMembers) { employee in
//----- This prints multiple times per employee in array ------.
let _ = print(employee)
NavigationLink(value: employee.attuid) {
AsyncImage(url: URL(string: "\(userImageUrl)\(employee.attuid)")) { image in
image.resizable()
.clipShape(Circle())
.shadow(radius: 10)
.overlay(Circle().stroke(Color.gray, lineWidth: 2))
} placeholder: {
ProgressView()
}
.frame(width:30, height: 35)
VStack (alignment: .leading){
Text("\(employee.fName) \(employee.lName)")
Text("\(employee.jobTitle)")
.font(.system(size: 10, weight: .thin))
}
}
}
}
.background(Color("ContentColumn"))
.scrollContentBackground(.hidden)
}
}
}
struct TeamView_Previews: PreviewProvider {
static var previews: some View {
TeamView(user: loggedInUser.shared,
viewData: apiData(gauges:gaugesTest,
trends: trendsTest,
team: teamTest),
uid_selection: .constant(loggedInUser.shared.attuid),
emulation_uid: "")
}
}

From your code snippet, it looks like you're doing a print in the body of the ForEach, and seeing multiple prints per item.
Actually, this is completely normal behaviour because SwiftUI may render a view multiple times (which will cause your print statement to be called each time). There is no need to worry about such rerenders (unless you're debugging performance issues). SwiftUI's rendering heuristics isn't known to the public, and may sometimes choose to make multiple rendering passes even though no state variables have changed.

Related

SwiftUI code too long for the compiler to type-check it, but I can't seem to break it down into parts

I am trying to make a view iteratively from a JSON file fetched from a PHP URL. The view is made with a for each loop to create an information box for every different station in the JSON and also makes them a navigation link. Ideally, I would tap the box to get more information on a specific tide station (all station data hasn't been presented on the view yet).
Since I am using a for each loop, I represent the current station number in the data array with i. In order to get the same data as the box I click I use the same i number for other relevant data.
My issue is that as I am building everything in the same for each loop to keep that number i in the scope, my code gets too long for the compiler to check. I heard that this could happen even if the code wasn't too long but simply for a typing mistake somewhere, but I have yet to find a mistake that breaks the code and truly believe it is due to the length as it was working if I commented-out some parts.
To resolve that I understand I need to break my code into different sections for the compiler to check them individually so as not to start the process every time.
However, as I am building everything into the for each loop to have the i number in the scope, I cannot make sub-views that use that number.
I am not entirely sure this is the question that would best solve my issue, but how can I pass the for each loop parameter into another view/function/or something else?
I apologize for the code being very rough and not following good coding practices, if not just for SwiftUI, I am quite inexperienced with programming in general. I left the entirety of the code to be sure not to leave any possibility out.
import Foundation
import SwiftUI
import Alamofire
import SwiftyJSON
import MapKit
public var array_tides_site_name = [String]()
public var array_tides_next_lw_time = [String]()
public var array_tides_next_lw_height = [Double]()
public var array_tides_next_hw_time = [String]()
public var array_tides_next_hw_height = [Double]()
public var array_tides_tidal_state = [String]()
public var array_tides_latitude = [Double]()
public var array_tides_longitude = [Double]()
public var array_tides_observed_height = [Double]()
public var array_tides_predicted_height = [Double]()
public var array_tides_surge = [Double]()
struct Previews_GeneralTides_Previews: PreviewProvider {
static var previews: some View {
GeneralTidesView()
}
}
struct GeneralTidesView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack (alignment: .center) {
Spacer()
Image(systemName: "chart.xyaxis.line")
.font(.largeTitle)
.frame(width: 300, height: 300, alignment: .center)
Spacer()
Text("More Stations")
.font(.title3)
.foregroundStyle(LinearGradient(colors: [.primary, .secondary], startPoint: .topLeading, endPoint: .bottomTrailing))
.frame(width: screenWidth, height: 40)
.background(.thickMaterial)
Spacer()
Divider()
.onAppear() {loadStationData()}
Spacer()
StationList()
}
.navigationBarTitle("Tides")
.navigationBarTitleDisplayMode(.inline)
}
}
}
func loadStationData(){
let generalTideUrl = "http://www.pla.co.uk/hydrographics/ajax/ltoverview.php"
AF.request(generalTideUrl, method: .get).responseJSON(){ (generalTideResponse) in
switch generalTideResponse.result {
case .success:
//print(generaltideresponse.result)
let generalTideResult = try? JSON(data: generalTideResponse.data!)
//print(generaltideresult)
//print(generalTideResult!["tides"])
let generalTideArray = generalTideResult!["tides"]
array_tides_site_name.removeAll()
array_tides_next_lw_time.removeAll()
array_tides_next_lw_height.removeAll()
array_tides_next_hw_time.removeAll()
array_tides_next_hw_height.removeAll()
array_tides_tidal_state.removeAll()
array_tides_latitude.removeAll()
array_tides_longitude.removeAll()
array_tides_observed_height.removeAll()
array_tides_predicted_height.removeAll()
array_tides_surge.removeAll()
for i in generalTideArray.arrayValue {
//print(i)
let site_name = i["site_name"].stringValue
array_tides_site_name.append(site_name)
var next_lw_time = i["next_lw_time"].stringValue
let lwRange = next_lw_time.startIndex..<next_lw_time.index(next_lw_time.startIndex, offsetBy: 11)
next_lw_time.removeSubrange(lwRange)
array_tides_next_lw_time.append(next_lw_time)
let next_lw_height = i["next_lw_height"].doubleValue
array_tides_next_lw_height.append(next_lw_height)
var next_hw_time = i["next_hw_time"].stringValue
let hwRange = next_hw_time.startIndex..<next_hw_time.index(next_hw_time.startIndex, offsetBy: 11)
next_hw_time.removeSubrange(hwRange)
array_tides_next_hw_time.append(next_hw_time)
let next_hw_height = i["next_hw_height"].doubleValue
array_tides_next_hw_height.append(next_hw_height)
let tidal_state = i["tidal_state"].stringValue
array_tides_tidal_state.append(tidal_state)
let latitude = i["latitude"].doubleValue
array_tides_latitude.append(latitude)
let longitude = i["longitude"].doubleValue
array_tides_longitude.append(longitude)
let predictedHeight = i["predicted_height"].doubleValue
array_tides_predicted_height.append(predictedHeight)
let observedHeight = i["observed_height"].doubleValue
array_tides_observed_height.append(observedHeight)
let surge = i["surge_height"].doubleValue
array_tides_surge.append(surge)
}
break
case .failure:
print(generalTideResponse.error!)
break
}
}.resume()
}
struct StationList: View {
#State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.5, longitude: -0.12), span: MKCoordinateSpan(latitudeDelta: 0.6, longitudeDelta: 0.6))
var body: some View {
ForEach(0..<array_tides_site_name.count) { i in
NavigationLink(destination: Group{
VStack(alignment: .center) {
stationNavLink()
Text(array_tides_site_name[i])
.font(.largeTitle)
Map(coordinateRegion: $mapRegion, annotationItems: [Location(name: array_tides_site_name[i], coordinate: CLLocationCoordinate2D(latitude: array_tides_latitude[i], longitude: array_tides_longitude[i]))]) { location in
MapMarker(coordinate: location.coordinate)}
.frame(width: screenWidth, height: 250)
HStack {
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.thinMaterial)
.frame(width: 150, height: 120)
.overlay {
VStack(alignment: .leading , spacing: 10) {
Text("Next Low Tide:")
HStack {Text("Time: "); Text(array_tides_next_lw_time[i])}.foregroundColor(.secondary)
HStack {Text("Height: "); Text(array_tides_next_lw_height[i].description); Text("m")}.foregroundColor(.secondary)
}
}
RoundedRectangle(cornerRadius: 10)
.foregroundStyle(.thinMaterial)
.frame(width: 150, height: 120)
.overlay {
VStack(alignment: .leading, spacing: 10) {
Text("Next High Tide:")
HStack {Text("Time: "); Text(array_tides_next_hw_time[i])}.foregroundColor(.secondary)
HStack {Text("Height: "); Text(array_tides_next_hw_height[i].description); Text("m")}.foregroundColor(.secondary)
}
}
}
Text(array_tides_surge[i].description)
}
}){
ZStack {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(.white.opacity(0.3), lineWidth: 1)
.background(RoundedRectangle(cornerRadius: 8).fill(.thinMaterial))
.frame(width: screenWidth - 40, height: 80)
.overlay() {
VStack(alignment: .leading) {
Spacer()
Text(array_tides_site_name[i])
.padding(.leading, 10)
.foregroundStyle(LinearGradient(colors: [.secondary, .secondary.opacity(0.8)], startPoint: .leading, endPoint: .trailing))
Spacer()
Divider()
Spacer()
Group {
HStack(){
Spacer()
Text("High Water: ")
Text(array_tides_next_hw_time[i])
Spacer()
Text("Low Water: ")
Text(array_tides_next_lw_time[i])
Spacer()
if array_tides_tidal_state[i] == "Flood" { Image(systemName: "arrow.up").foregroundColor(.green) }
else { Image(systemName: "arrow.down").foregroundColor(.red) }
Spacer()
}
}
Spacer()
}
}
}
}
}
}
}
var stationNavLink: some View {
Text(array_tides_surge[currentStation])
}
struct Location: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
Having multiple, associated, arrays to hold your data is a definite code smell. You risk having data get out of sync between the arrays and as you can see, loading and accessing data involves a lot of code.
You can start by creating some [Codable] structs to hold you data. quicktype.io can do this for. Simply paste in your JSON and you get the structs you need.
I have modified Tide to conform to Hashable and Identifiable to make it easier to use in SwiftUI lists.
Also note the use of CodingKeys to convert the properties to Swift naming conventions.
// MARK: - TideData
struct TideData: Codable {
let timezone: String
let tides: [Tide]
}
// MARK: - Tide
struct Tide: Codable, Hashable, Identifiable {
let siteID, siteStation: Int
let siteName: String
let latitude, longitude: Double
let received: String
let observedHeight, predictedHeight, surgeHeight: Double
let nextLwTime: String
let nextLwHeight: Double
let nextHwTime: String
let nextHwHeight: Double
let tidalState: String
var id: Int {
return siteID
}
enum CodingKeys: String, CodingKey {
case siteID = "site_id"
case siteStation = "site_station"
case siteName = "site_name"
case latitude, longitude
case received = "Received"
case observedHeight = "observed_height"
case predictedHeight = "predicted_height"
case surgeHeight = "surge_height"
case nextLwTime = "next_lw_time"
case nextLwHeight = "next_lw_height"
case nextHwTime = "next_hw_time"
case nextHwHeight = "next_hw_height"
case tidalState = "tidal_state"
}
}
Now you can move your loading code into its own class. Make this an ObservableObject and have it publish the tide data.
Since our structs conform to Codable we can use AlamoFire's inbuilt JSON decoding and transformation
lass Loader:ObservableObject {
#Published var tides: [Tide] = []
func load() {
let generalTideUrl = "https://www.pla.co.uk/hydrographics/ajax/ltoverview.php"
AF.request(generalTideUrl, method: .get).responseDecodable(of: TideData.self){ (generalTideResponse) in
switch generalTideResponse.result {
case .success(let tideData):
self.tides = tideData.tides
print(self.tides)
case .failure(let error):
print(error)
}
}
}
}
Much more succinct!
Finally, we can use this data in a SwiftUI view
I have simplified your views for the sake of this answer, but I am sure you will get the idea.
struct GeneralTidesView: View {
#ObservedObject var loader: Loader
var body: some View {
NavigationView {
List {
ForEach(loader.tides) { station in
NavigationLink(destination: StationView(station:station)) {
Text(station.siteName)
}
}
}
}.onAppear {
self.loader.load()
}
}
}
struct Previews_GeneralTides_Previews: PreviewProvider {
static var previews: some View {
GeneralTidesView(loader: Loader())
}
}
struct StationView: View {
var station: Tide
var body: some View {
Form {
HStack {
Text("Predicted Height")
Text("\(station.observedHeight)")
}
HStack {
Text("Predicted Height")
Text("\(station.predictedHeight)")
}
}.navigationTitle(station.siteName)
}
}
You can see how having a struct with properties makes it much easier to pass data around. Also, since all of the properties are in a single struct, we no longer need to work about array indices.

Two extra views put inside of a ForEach with a filtering search bar

So I have a ScrollView that contains a list of all the contacts imported from a user's phone. Above the ScrollView, I have a 'filter search bar' that has a binding that causes the list to show only contacts where the name contains the same string as the search bar filter. For some reason, the last two contacts in the list always pop up at the bottom of the list, no matter what the string is (even if it's a string not contained in any of the contact names on the phone). I tried deleting a contact and the problem persists, because the original contact was just replaced with the new second to last contact. Any help fixing this would be much appreciated!
struct SomeView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: CustomContact = []
var body: some View {
var filteredContactsCount = 0
if friendsFilterText.count != 0 {
for contact in appState.savedContacts {
if contact.name.lowercased().contains(friendsFilterText.lowercased()) {
filteredContactsCount += 1
}
}
} else {
filteredContactsCount = savedContacts.count
}
return HStack {
Image(systemName: "magnifyingglass")
ZStack {
HStack {
Text("Type a name...")
.opacity(friendsFilterText.count > 0 ? 0 : 1)
Spacer()
}
CocoaTextField("", text: $friendsFilterText)
.background(Color.clear)
}
Button(action: {
friendsFilterText = ""
}, label: {
Image(systemName: "multiply.circle.fill")
})
}.frame(height: 38)
HStack(spacing: 10) {
Text("Your contacts (\(filteredContactsCount))")
Spacer()
Button(action: {
fetchContacts()
}, label: {
Image(systemName: "square.and.arrow.down")
})
Button(action: {
// edit button action
}, label: {
Text("Edit")
})
}
ScrollView {
VStack {
ForEach(savedContacts, id: \.self.name) { contact in
if contact.name.lowercased().contains(friendsFilterText.lowercased()) || friendsFilterText.count == 0 {
Button(action: {
// contact button action
}, label: {
HStack(spacing: 20) {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 41))
.frame(width: 41, height: 41)
VStack(alignment: .leading, spacing: 4) {
Text(contact.name)
Text(contact.phoneNumber)
}
Spacer()
}.frame(height: 67)
})
}
}
}
}
}
}
CustomContact is a custom struct with properties phoneNumber and name. I've attached images below of the issue I'm experiencing. I'm thinking MAYBE it's because there's something off timing-wise with the friendsFilterText and the ForEach rendering but I'm really not sure.
In the image set below, the 'Extra Contact 1' and 'Extra Contact 2' are ALWAYS rendered, unless I add a filter, then switch to a different view, then back to this view (which leads me to believe it's a timing thing again).
https://imgur.com/a/CJW2CUS
You should move the count calculation out of the view into a computed var.
And if CustomContact is your single contact struct, it should actually read #State var savedContacts: [CustomContact] = [] i.e. an array of CustomContact.
The rest worked fine with me, no extra contacts showing.
struct ContentView: View {
#State var friendsFilterText: String = ""
#State var savedContacts: [CustomContact] = []
// computed var
var filteredContactsCount: Int {
if friendsFilterText.isEmpty { return savedContacts.count }
return savedContacts.filter({ $0.name.lowercased().contains(friendsFilterText.lowercased()) }).count
}
var body: some View {
...

Swift change state and reload after API call with #binding

I currently have a list of recipes that I fetch from a local API upon screen load. This page also has a search field on it that I want to hook up to pull certain data from this API. This API call stores the results in the state of the view. My goal is, when somebody searches, to pull those records and update the state, and reloading the view with new data.
I have seen different ways of doing this with #binding and #state, however, it seems from the examples I've been looking at the binding is in a different struct within another file. In my iteration, however, I just have some functions that pull data from this API. I'd like to avoid using reloadData() as I'd like to use #binding and #state wherever possible in my application.
struct RecipesView: View {
#State var recipes = [Recipe]()
#State var searchText = ""
var body: some View {
VStack {
HStack {
TextField("Search ...", text: $searchText)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.horizontal, 10)
}
VStack{
HStack {
Text("Categories")
.bold()
.multilineTextAlignment(.trailing)
.padding(10)
Spacer()
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(SPIRITS, id:\.self){ spirit in
Button("\(spirit)", action: {
queryBySpirit(spirit: spirit)
})
.frame(width: 80, height: 40, alignment: .center)
.background(Color("Tope"))
.foregroundColor(Color("Onyx"))
.cornerRadius(15)
.font(.subheadline.bold())
}
}.padding([.leading, .trailing])
}
}
ScrollView {
VStack {
ForEach(recipes) { recipe in
NavigationLink(destination: RecipeView(recipe: recipe)) {
RecipeCard(recipe: recipe)
}
}.padding([.trailing])
}
}
}
.onAppear(perform: loadData)
.navigationBarTitle("Recipes")
.listStyle(InsetGroupedListStyle())
Color.gray.ignoresSafeArea()
}
func loadData() {
AF.request("http://localhost:3000/recipes").responseJSON { response in
guard let data = response.data else { return }
if let response = try? JSONDecoder().decode([Recipe].self, from: data) {
DispatchQueue.main.async {
self.recipes = response
}
return
}
}
}
func queryBySpirit(spirit: String) {
AF.request("http://localhost:3000/recipes?spirit=\(spirit)").responseJSON { response in
guard let data = response.data else { return }
if let response = try? JSONDecoder().decode([Recipe].self, from: data) {
DispatchQueue.main.async {
self.recipes = response
}
return
}
}
}
}
How can I use #binding within this file to take advantage of the hot reloading? Or, based on my iteration will I need to leverage reloadData() instead?
Spefically, I'd like to avoid using reloadData() as seen here: Swift CollectionView reload data after API call
I'd love to be able to leverage Changing #State variable does not update the View in SwiftUI
However I'm unsure of how to do that with what I have currently.

SwiftUI & CoreData - ordered list

I am trying to make an ordered list in SwiftUI using CoreData records.
How to print running numbers in such list?
In the following example I have one entity named SomeEntity with a String attribute named title.
import SwiftUI
struct ContentView: View {
var fetchRequest: FetchRequest<SomeEntity>
var items: FetchedResults<SomeEntity> { fetchRequest.wrappedValue }
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) {item in
NavigationLink(destination: ItemDetailsView(item: item)) {
HStack {
Text("99")
// How to print running number instead of "99" in this ordered list of CoreData records?
// I was thinking about something like this:
// Text(items.id) - but this doesn't work. Is there something similar?
.multilineTextAlignment(.center)
.frame(width: 60)
Text(item.title!)
}
}
}
}
}
}
}
Probably you need something like the following
struct ContentView: View {
var fetchRequest: FetchRequest<SomeEntity>
var items: FetchedResults<SomeEntity> { fetchRequest.wrappedValue }
var body: some View {
NavigationView {
List {
ForEach(Array(items.enumerated()), id: \.element) {(i, item) in
NavigationLink(destination: ItemDetailsView(item: item)) {
HStack {
Text("\(i + 1)")
.multilineTextAlignment(.center)
.frame(width: 60)
Text(item.title!)
}
}
}
}
}
}
}
Based on your comments, this should work: You need to use a different init of ForEach, which takes a Range<Int> as first argument:
ForEach(-items.count..<0, id: \.self) { i in
NavigationLink(destination: ItemDetailsView(item: items[-i])) {
HStack {
Text("\(items[-i].itemName)")
.multiLineTextAlignment(.center)
.frame(width: 60)
Text("\(items[-i].title!)")
}
}
}
Going from -items.count to 0 also ensures the reversed order.
I've tested it and with #FetchRequest this solution seems to be the best.
List {
ForEach(self.contacts.indices, id: \.self) { i in
Button(action: {
self.selectedId = self.contacts[i].id!
}) {
ContactRow(contact: self.contacts[i])
.equatable()
.background(Color.white.opacity(0.01))
}
.buttonStyle(ListButtonStyle())
.frame(height: 64)
.listRowBackground( (i%2 == 0) ? Color("DarkRowBackground") : .white)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
I've tested also solution with Array(self.contacts.enumerated()) but it doesn't work as well. If you there is small number of records it can be ok but for large number of records it is suboptimal.
If you use
request.fetchBatchSize = 10
to load records (entities) in batches while scrolling the list enumerated() doesn't work executes all needed SELECT ... LIMIT 10 requests at once
.indices makes it possible to fetch additional items while scrolling.

SwiftUI ForEach not correctly updating in scrollview

I have a SwiftUI ScrollView with an HStack and a ForEach inside of it. The ForEach is built off of a Published variable from an ObservableObject so that as items are added/removed/set it will automatically update in the view. However, I'm running into multiple problems:
If the array starts out empty and items are then added it will not show them.
If the array has some items in it I can add one item and it will show that, but adding more will not.
If I just have an HStack with a ForEach neither of the above problems occur. As soon as it's in a ScrollView I run into the problems.
Below is code that can be pasted into the Xcode SwiftUI Playground to demonstrate the problem. At the bottom you can uncomment/comment different lines to see the two different problems.
If you uncomment problem 1 and then click either of the buttons you'll see just the HStack updating, but not the HStack in the ScrollView even though you see init print statements for those items.
If you uncomment problem 2 and then click either of the buttons you should see that after a second click the the ScrollView updates, but if you keep on clicking it will not update - even though just the HStack will keep updating and init print statements are output for the ScrollView items.
import SwiftUI
import PlaygroundSupport
import Combine
final class Testing: ObservableObject {
#Published var items: [String] = []
init() {}
init(items: [String]) {
self.items = items
}
}
struct SVItem: View {
var value: String
init(value: String) {
print("init SVItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
struct HSItem: View {
var value: String
init(value: String) {
print("init HSItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
public struct PlaygroundRootView: View {
#EnvironmentObject var testing: Testing
public init() {}
public var body: some View {
VStack{
Text("ScrollView")
ScrollView(.horizontal) {
HStack() {
ForEach(self.testing.items, id: \.self) { value in
SVItem(value: value)
}
}
.background(Color.red)
}
.frame(height: 50)
.background(Color.blue)
Spacer()
Text("HStack")
HStack {
ForEach(self.testing.items, id: \.self) { value in
HSItem(value: value)
}
}
.frame(height: 30)
.background(Color.red)
Spacer()
Button(action: {
print("APPEND button")
self.testing.items.append("A")
}, label: { Text("APPEND ITEM") })
Spacer()
Button(action: {
print("SET button")
self.testing.items = ["A", "B", "C"]
}, label: { Text("SET ITEMS") })
Spacer()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = UIHostingController(
// problem 1
rootView: PlaygroundRootView().environmentObject(Testing())
// problem 2
// rootView: PlaygroundRootView().environmentObject(Testing(items: ["1", "2", "3"]))
)
Is this a bug? Am I missing something? I'm new to iOS development..I did try wrapping the actual items setting/appending in the DispatchQueue.main.async, but that didn't do anything.
Also, maybe unrelated, but if you click the buttons enough the app seems to crash.
Just ran into the same issue. Solved with empty array check & invisible HStack
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
if (self.items.count == 0) {
HStack{
Spacer()
}
}
}
It is known behaviour of ScrollView with observed empty containers - it needs something (initial content) to calculate initial size, so the following solves your code behaviour
#Published var items: [String] = [""]
In general, in such scenarios I prefer to store in array some "easy-detectable initial value", which is removed when first "real model value" appeared and added again, when last disappears. Hope this would be helpful.
For better readability and also because the answer didn't work for me. I'd suggest #TheLegend27 answer to be slightly modified like this:
if self.items.count != 0 {
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
}
}

Resources