When I include a Stepper inside a List with many rows such that it will scroll off the screen when scrolled the layout becomes completely mucked up. See attached image.
Has anybody had this issue and managed to solve it?
Code to reproduce in Xcode 11.3.1 / iOS 13.3
struct Item: Identifiable {
let id: String
let name: String
init(_ name: String) {
self.id = UUID().uuidString
self.name = name
}
}
struct ContentView: View {
let items: [Item] = [
Item("Car"),
Item("Shoe"),
Item("House"),
Item("Pencil"),
Item("Crayon"),
Item("PC"),
Item("Spoon"),
Item("Box"),
Item("Tennis Racket"),
Item("Mobile Phone"),
Item("Hat"),
Item("Table"),
Item("Chair"),
Item("Painting"),
Item("Couch"),
Item("Desk"),
Item("Fireplace"),
Item("Stove"),
Item("Kettle"),
Item("Fork"),
Item("Knife"),
Item("Dinner Plate")
]
var body: some View {
List() {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
}
struct ItemRow: View {
let item: Item
#State private var quantity: Int = 1
var body: some View {
HStack(spacing: 10) {
Text(item.name)
Stepper(onIncrement: {
self.quantity = self.quantity + 1
}, onDecrement: {
self.quantity = self.quantity - 1
}, label: {
Text("\(quantity)")
.padding()
})
}
}
}
Thanks in advance,
Matt
You're not the one with this problem. There is some issue with list, but in canvas only. According to this answer, you can try your list on the real device and it should looks in normal way. So I tried it on iPhone 7:
Related
struct Item: Identifiable {
let id: String
let url = URL(string: "https://styles.redditmedia.com/t5_j6lc8/styles/communityIcon_9uopq0bazux01.jpg")!
}
struct Content: View {
let model: [Item] = {
var model = [Item]()
for i in 0 ..< 100 {
model.append(.init(id: String(i)))
}
return model
}()
var body: some View {
List(model) { item in
Row(item: item)
}
}
}
struct Row: View {
let item: Item
var body: some View {
AsyncImage(url: item.url)
}
}
Running code above with Xcode 14.1 on iOS 16.1 simulator, AsyncImage sometimes doesn’t properly show downloaded image but grey rectangle instead when scrolling in list. Is this bug or am I missing something? Thanks
My solution was to use VStack in ScrollView instead of List. It looks like it's working and it doesn't have any other drawbacks.
In the last few months, many developers have reported NavigationLinks to unexpectedly pop out and some workarounds have been published, including adding another empty link and adding .navigationViewStyle(StackNavigationViewStyle()) to the navigation view.
Here, I would like to demonstrate another situation under which a NavigationLink unexpectedly pops out:
When there are two levels of child views, i.e. parentView > childLevel1 > childLevel2, and childLevel2 modifies childLevel1, then, after going back from level 2 to level 1, level 1 pops out and parentView is shown.
I have filed a bug report but not heard from apple since. None of the known workarounds seem to work. Does someone have an idea what to make of this? Just wait for iOS 15.1?
Below is my code (iPhone app). In the parent view, there is a list of persons from which orders are taken. In childLevel1, all orders from a particular person are shown. Each order can be modified by clicking on it, which leads to childLevel2. In childLevel2, several options are available (here only one is shown for the sake of brevity), which is the reason why the user is supposed to leave childLevel2 via "< Back".
import SwiftUI
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var paId: Int
var n: Int // used only in the real code
}
class Data : ObservableObject {
init() {
pastries = [
Pastry(id: 0, name: "Prezel"),
Pastry(id: 1, name: "Donut"),
Pastry(id: 2, name: "bagel"),
Pastry(id: 3, name: "cheese cake"),
]
persons = [
Person(id: 0, name: "Alice", orders: [Order(paId: 1, n: 1)]),
Person(id: 1, name: "Bob", orders: [Order(paId: 2, n: 1), Order(paId: 3, n: 1)])
]
activePersonsIds = [0, 1]
}
#Published var activePersonsIds: [Int] = []
#Published var persons: [Person] = []
#Published var pastries: [Pastry]
#Published var latestOrder = Order(paId: 0, n: 1)
lazy var pastryName: (Int) -> String = { (paId: Int) -> String in
if self.pastries.first(where: { $0.id == paId }) == nil {
return "undefined pastryId " + String(paId)
}
return self.pastries.first(where: { $0.id == paId })!.name
}
var activePersons : [Person] {
return activePersonsIds.compactMap {id in persons.first(where: {$0.id == id})}
}
}
#main
struct Bretzel_ProApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
#StateObject var data = Data()
var body: some View {
TabView1(data: data)
// in the real code, there are more tabs
}
}
struct TabView1: View {
#StateObject var data: Data
var body: some View {
NavigationView {
List {
ForEach(data.activePersons, id: \.self) { person in
NavigationLink(
destination: EditPerson(data: data, psId: person.id),
label: {
VStack (alignment: .leading) {
Text(person.name)
}
}
)
}
}
.listStyle(PlainListStyle())
.navigationTitle("Orders")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct EditPerson: View {
#ObservedObject var data: Data
var psId: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
List() {
ForEach (0...p.orders.count-1, id: \.self) { loop in
Section(header:
HStack() {
Text("BESTELLUNG " + String(loop+1))
}
) {
EPSubview1(data: data, psId: psId, loop: loop)
}
}
}.navigationTitle(p.name)
.listStyle(InsetGroupedListStyle())
}
}
struct EPSubview1: View {
#ObservedObject var data: Data
var psId: Int
var loop: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
let o1: Order = p.orders[loop]
NavigationLink(
destination: SelectPastry(data: data)
.onAppear() {
data.latestOrder.paId = o1.paId
}
.onDisappear() {
data.persons[pindex].orders[loop].paId = data.latestOrder.paId
},
label: {
VStack(alignment: .leading) {
Text(String(o1.n) + " x " + data.pastryName(o1.paId))
}
}
)
}
}
struct SelectPastry: View {
#ObservedObject var data : Data
var body: some View {
VStack {
List {
ForEach(data.pastries, id: \.self) {pastry in
Button(action: {
data.latestOrder.paId = pastry.id
}) {
Text(pastry.name)
.foregroundColor(data.latestOrder.paId == pastry.id ? .primary : .secondary)
}
}
}.listStyle(PlainListStyle())
}
}
}
The problem is your ForEach. Despite that fact that Person conforms to Identifiable, you're using \.self to identify the data. Because of that, every time an aspect of the Person changes, so does the value of self.
Instead, just use this form, which uses the id vended by Identifiable:
ForEach(data.activePersons) { person in
Which is equivalent to:
ForEach(data.activePersons, id: \.id) { person in
OK, I nailed down the problem to the dynamic list with sections, so I changed the title and I'm going to shorten my post...
In my project, there is a complex view that contains a dynamic list with sections that depend on many state variables.
Problem: after a few user interactions and subsequent changes to the list, I see strange behaviour:
sometimes a section doesn't show up although the condition of the if-clause in which it is embedded is definitely met
sometimes, the section header label is replaced with the label of a list item
sometimes, it crashes
when the view is broken and I enforce a refresh of the view without changing any of the state variables, everything looks good again
The code below is taken from my project, however, greatly simplified by removing parts that are not necessary to produce the error. The state variable altered by user interaction is pIdStatus. From pIdStatus, the function data.analyze() calculates the arrays pastriesWithStatus1, pastriesWithStatus2 and pastriesWithStatus3.
Interestingly, when I remove the tab-view in which everything is embedded, everything works fine.
Any help is greatly appreciated.
import SwiftUI
struct ContentView: View {
#StateObject var data = Data()
var body: some View {
TabView { // needed in my project and needed for crash.
List {
// for debugging purpose: create a button that triggers a refresh
Button {
data.counter += 1
} label: {
HStack() {
Text("Refresh")
.font(.system(size: 16, weight: .regular))
}
}
// Section with status3-items
if data.pastriesWithStatus3.count > 0 {
Section(header:
HStack() {
Text("STATUS 3")
}
){
ForEach(data.pastriesWithStatus3, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 3)
}
}
}
// section with other items
Section(header:
HStack() {
Text("OTHER ITEMS")
}
){
ForEach(data.pastriesWithStatus2, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 2)
}
ForEach(data.pastriesWithStatus1, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 1)
}
}
}
.listStyle(InsetGroupedListStyle())
.tabItem {
Image(systemName: "cart")
Text("shopping")
}.onAppear {
data.analyze()
}
}
}
}
struct subview21: View {
#ObservedObject var data : Data
let hspacing: CGFloat = 20
let tab1: CGFloat = 150
var label: String
var id: Int
var status: Int // 1=red, 2=unchecked, 3=green
var body: some View {
if data.testcondition { // this line is required for crash. Although it is always true.
HStack (spacing: hspacing) {
Text(data.pastries.first(where: { $0.id == id })!.name)
.frame(width: tab1, alignment: .leading)
Button {
data.pIdStatus[id] = 1
data.analyze()
} label: {
Image(systemName: (status == 1 ? "checkmark.square.fill" : "square"))
.foregroundColor(.red)
}
.buttonStyle(BorderlessButtonStyle())
Button {
data.pIdStatus[id] = 3
data.analyze()
} label: {
Image(systemName: (status == 3 ? "checkmark.square.fill" : "square"))
.foregroundColor(.green)
}
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
let maxPId = 99
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var pastryId: Int
var n: Int
var otherwise: OrderL2?
}
struct OrderL2: Hashable {
let level: Int = 2
var pastryId: Int
var n: Int
}
class Data : ObservableObject {
#Published var persons: [Person] = [
Person(id: 1, name: "John", orders: [Order(pastryId: 0, n:1, otherwise:OrderL2(pastryId: 1, n:2))]),
]
#Published var pastries: [Pastry] = [Pastry(id: 0, name: "Donut"),
Pastry(id: 1, name: "prezel")]
#Published var counter: Int = 0 // for debugging
#Published var pIdStatus: [Int] = Array(repeating: 2, count: maxPId)
#Published var testcondition: Bool = true // needed in my project, for simplicity, here, always true
#Published var pastriesWithStatus1: [Int] = []
#Published var pastriesWithStatus2: [Int] = []
#Published var pastriesWithStatus3: [Int] = []
func analyze() {
var localPastriesWithStatus1: [Int] = []
var localPastriesWithStatus2: [Int] = []
var localPastriesWithStatus3: [Int] = []
for p in pastries { // there may be more elegant ways to code, however, in real life, there's a lot more functionality here
if p.id < maxPId {
if pIdStatus[p.id] == 1 {
localPastriesWithStatus1.append(p.id)
}
if pIdStatus[p.id] == 2 {
localPastriesWithStatus2.append(p.id)
}
if pIdStatus[p.id] == 3 {
localPastriesWithStatus3.append(p.id)
}
}
}
pastriesWithStatus1 = localPastriesWithStatus1
pastriesWithStatus2 = localPastriesWithStatus2
pastriesWithStatus3 = localPastriesWithStatus3
}
}
Apparently, swiftui List is not made for Lists that are built from scratch each time the view is refreshed. Instead, Lists must be linked to arrays to/from which items are added/removed one by one.
I solved the problem by avoiding List altogether, instead using Stack, Text etc.
Consider the following project with two views. The first view presents the second one:
import SwiftUI
struct ContentView: View {
private let data = 0...1000
#State private var selection: Set<Int> = []
#State private var shouldShowSheet = false
var body: some View {
self.showSheet()
//self.showPush()
}
private func showSheet() -> some View {
Button(action: {
self.shouldShowSheet = true
}, label: {
Text("Selected: \(selection.count) items")
}).sheet(isPresented: self.$shouldShowSheet) {
EditFormView(selection: self.$selection)
}
}
private func showPush() -> some View {
NavigationView {
Button(action: {
self.shouldShowSheet = true
}, label: {
NavigationLink(destination: EditFormView(selection: self.$selection),
isActive: self.$shouldShowSheet,
label: {
Text("Selected: \(selection.count) items")
})
})
}
}
}
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
#State private var editMode: EditMode = .active
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
Text("\(value)")
}
}.environment(\.editMode, self.$editMode)
}
}
Steps to reproduce:
Create an app with the above two views
Run the app and present the sheet with the editable list
Select some items at random indexes, for example a handful at index 0-10 and another handful at index 90-100
Close the sheet by swiping down/tapping back button
Open the sheet again
Scroll to indexes 90-100 to view the selection in the reused cells
Expected:
The selected indexes as you had will be in “selected state”
Actual:
The selection you had before is not marked as selected in the UI, even though the binding passed to List contains those indexes.
This occurs both on the “sheet” presentation and the “navigation link” presentation.
If you select an item in the list, the “redraw” causes the original items that were originally not shown as selected to now be shown as selected.
Is there a way around this?
It looks like EditMode bug, worth submitting feedback to Apple. The possible solution is to use custom selection feature.
Here is a demo of approach (modified only part). Tested & worked with Xcode 11.4 / iOS 13.4
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
self.cell(for: value)
}
}
}
// also below can be separated into standalone view
private func cell(for value: Int) -> some View {
let selected = self.selection.contains(value)
return HStack {
Image(systemName: selected ? "checkmark.circle" : "circle")
.foregroundColor(selected ? Color.blue : nil)
.font(.system(size: 24))
.onTapGesture {
if selected {
self.selection.remove(value)
} else {
self.selection.insert(value)
}
}.padding(.trailing, 8)
Text("\(value)")
}
}
}
I want to change another unrelated #State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers #State. Is there another way to solve this?
Deployment target of iOS 14 or newer
Apple has provided a built in onChange extension to View, which can be used like this:
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
#State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"
I think this is simpler solution:
#State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
#Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
#State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher and a PassthroughSubject publisher as the selection. That let me satisfy SwiftUI and with an onReceive() I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange and a PassthroughSubject<Int, Never> publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
#ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: #escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})
SwiftUI 1 & 2
Use onReceive and Just:
import Combine
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact class is an #ObservedObject var contact: Contact
saveContact is a do-catch function to try viewContext.save()...
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})