Trying to understand why using a Shape within a ForEach would cause reverse iteration of the array. Example:
var body: some View {
ForEach(0...10,id:\.self) { index in
TestShape()
}
}
struct TestShape:Shape {
func path(in rect: CGRect) -> Path {
return Path()
}
}
If you examine index with each pass, it would start with 10 and work backwards. However, if instead of calling TestShape you instead had say Text("test"), index would start at 0 and work its way up. So the key difference here is in the call to a shape. Why is this happening?
this is the test I used to show that index is not reversed:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct TestShape:Shape {
func path(in rect: CGRect) -> Path {
return Path()
}
}
struct ContentView: View {
var body: some View {
ForEach(0...10,id:\.self) { index in
let _ = print("---> index: \(index)")
TestShape()
}
}
}
output is:
---> index: 0
---> index: 1
---> index: 2
---> index: 3
---> index: 4
---> index: 5
---> index: 6
---> index: 7
---> index: 8
---> index: 9
---> index: 10
Related
I have realized that some caching mechanism is present if the List's row is a View.
Let's start with a simple list:
struct ContentView: View {
#State var model = [String]()
var body: some View {
VStack {
Button("Add") {
model.append(UUID().uuidString)
}
List {
ForEach(0 ..< model.count, id:\.self) { idx in
let item = model[idx]
Print("\(idx)")
makeRow(item: item)
}
}
}
}
func makeRow(item: String) -> some View {
HStack {
Print("Row \(item)")
Text("\(item)")
}
}
}
extension View {
func Print(_ vars: Any...) -> some View {
for v in vars { print(v) }
return EmptyView()
}
}
Adding a new single row, this appears on the console:
4
Row 44899379-B7FA-4667-ADB9-86A59EA69EF0
0
Row A219638D-9C6E-42C8-9055-C5569B20D9B8
1
Row B6187186-408F-46B2-A121-840C399CB12F
2
Row 856F873F-8639-4CA4-A832-19EF92BFA7A4
3
Row 0C080275-12D9-451C-8C7A-C8B97C4DE658
Then each row is recreated.
Let's replace each row with a View:
func makeRow(item: String) -> some View {
RowView(item: item)
}
struct RowView: View {
var item: String
var body: some View {
HStack {
Print("Row \(item)")
Text("\(item)")
}
}
}
Now the console shows:
4
Row E55A0F48-5848-4CFE-8612-AB8CC799F532
0
1
2
3
Only the RowView for the just added row is created.
Apparently all the other RowViews all cached. I think this is good in most cases.
I'm working on a complex application and, in some use cases, I need to redraw all the rows.
How can I force to recreate each RowView?
in this simple reproduced code I abstracted a problem that I was experiencing in a bigger project.
What I am trying to do is create a Rectangle (successful) and draw multiple ellipses, each one bigger than the last, inside of the Rectangle (failure). Below is code that is ready to be copied and pasted... What am I doing wrong?
Updated code to fix unrelated issue
import SwiftUI
struct ContentView: View {
var body: some View {
MyShape()
.stroke()
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct MyShape: Shape {
#State private var horizontalMultiplier: CGFloat = 0.5
#State private var verticalMultiplier: CGFloat = 0.5
func path(in rect: CGRect) -> Path {
var path = Path()
path.addRect(rect)
var newPath = Path()
for _ in 0...5 {
let transform = CGAffineTransform(scaleX: 1 * horizontalMultiplier, y: 1 * verticalMultiplier)
newPath.addEllipse(in: rect, transform: transform)
path.addPath(newPath)
horizontalMultiplier += 0.25
verticalMultiplier += 0.25
}
return path
}
}
Move the declarations of the variables you're trying increment outside your loop -- they're getting recreated (and thus stay 0) each time right now.
Update, after edit: there doesn't appear to be a reason to try to use #State here -- just define the variables within path. If you do need mutable state over time, store it in the parent view and pass it through via parameters
struct MyShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.addRect(rect)
var newPath = Path()
var horizontalMultiplier: CGFloat = 0.5
var verticalMultiplier: CGFloat = 0.5
for _ in 0...5 {
let transform = CGAffineTransform(scaleX: 1 * horizontalMultiplier, y: 1 * verticalMultiplier)
newPath.addEllipse(in: rect, transform: transform)
path.addPath(newPath)
horizontalMultiplier += 0.25
verticalMultiplier += 0.25
}
return path
}
}
So I have a view of lines. The number of lines are determined by the user's input. I know about creating lines and how to render those lines with different effects.. but how would I modify a line that has already been created.
So for example I would like to make the second drawn line have a break in the middle after maybe a button tap from the user..
Here is some minimally reproduced code..
import SwiftUI
struct ContentView: View {
#State private var path = Path()
#State private var horizontalLines = 0
var body: some View {
GeometryReader { geo in
VStack {
Draw(path: $path)
.stroke()
.clipped()
.padding()
HorizontalLine(path: $path, horizontalLines: $horizontalLines, rect: geo.frame(in: .local))
}
}
}
private func addLine() {
path.addEllipse(in: path.boundingRect)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Draw: Shape {
#Binding var path: Path
func path(in rect: CGRect) -> Path {
var path = path
path.addRect(rect)
return path
}
}
struct HorizontalLine: View {
#Binding var path: Path
#Binding var horizontalLines: Int
let rect: CGRect
var verticalOffset: CGFloat {
let height = rect.size.height
let tenths = height / 10
return tenths * CGFloat(horizontalLines)
}
var body: some View {
Button("Add Horizontal Lines") {
path = addHLine()
}
}
private func addHLine() -> Path {
var path = path
path.move(to: CGPoint(x: rect.minX + 50, y: verticalOffset))
path.addLine(to: CGPoint(x: rect.maxX - 75 , y: verticalOffset))
self.horizontalLines += 1
return path
}
}
In the release notes to iOS 13 Apple said:
The Binding structure’s conditional conformance to the Collection protocol is removed. (51624798). And then suggested iterating through arrays this way:
var body: some View {
List(landmarks.indexed(), id: \.1.id) { (index, landmark) in
Toggle(landmark.name, isOn: self.$landmarks[index].isFavorite)
}
}
But you also need to implement this:
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.startIndex } // Possibly this should be endIndex?
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
So first of all there appears to be a typo on line 6: that might need to be changed to base.endIndex but it doesn't work either way. It's a shame because it looks like a neat way to iterate with both element and index. So, is there a mistake I am making in how I'm implementing it?
Code:
import SwiftUI
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
struct SingleBall: Identifiable {
var id = UUID()
var position = CGPoint.zero
var isDeleting = false
}
class BallStorage: ObservableObject {
#Published var balls: [SingleBall] = [
SingleBall(position: CGPoint(x: 110, y: 220)),
SingleBall(position: CGPoint(x: 150, y: 120)),
SingleBall(position: CGPoint(x: 200, y:160)),
SingleBall(position: CGPoint(x: 200, y: 200))
]
}
struct StartScreen: View {
#ObservedObject var ballStorage = BallStorage()
#State var isDeleting = false
var body: some View {
ZStack { // stack of balls
ForEach(ballStorage.balls.indexed(), id: \.1.id) {
(number, item) in
littleBall(
ballStorage: ballStorage,
numberInArray: number )
}
}
.frame(width:screenWidth, height:screenHeight) //end stack balls
}
}
Littleball is a thing that just displays a black circle. So when I am doing the same thing by iterating over indices, four circles get rendered nicely (but looking for alternative way because you can't delete things from the array with indices).
With the above code nothing gets rendered. Why?
EDIT: To clarify, the .indexed() implementation is obviously also in the scope of the posted code.
This version of .indexed() does work:
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { self.base.startIndex }
var endIndex: Index { self.base.endIndex }
func index(after i: Index) -> Index {
self.base.index(after: i)
}
func index(before i: Index) -> Index {
self.base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
self.base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: self.base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
Full example and more information
I'm using Xcode 12 beta and trying to create a view where items from a left list can be dragged onto a right list and dropped there.
This crashes in the following situations:
The list is empty.
The list is not empty, but the item is dragged behind the last list element, after dragging it onto other list elements first. The crash already appears while the item is dragged, not when it is dropped (i.e., the .onInsert is not called yet).
The crash message tells:
SwiftUI`generic specialization <SwiftUI._ViewList_ID.Views> of (extension in Swift):Swift.RandomAccessCollection< where A.Index: Swift.Strideable, A.Indices == Swift.Range<A.Index>, A.Index.Stride == Swift.Int>.index(after: A.Index) -> A.Index:
Are there any ideas why this happens and how it can be avoided?
The left list code:
struct AvailableBuildingBricksView: View {
#StateObject var buildingBricksProvider: BuildingBricksProvider = BuildingBricksProvider()
var body: some View {
List {
ForEach(buildingBricksProvider.availableBuildingBricks) { buildingBrickItem in
Text(buildingBrickItem.title)
.onDrag {
self.provider(buildingBrickItem: buildingBrickItem)
}
}
}
}
private func provider(buildingBrickItem: BuildingBrickItem) -> NSItemProvider {
let image = UIImage(systemName: buildingBrickItem.systemImageName) ?? UIImage()
let provider = NSItemProvider(object: image)
provider.suggestedName = buildingBrickItem.title
return provider
}
}
final class BuildingBricksProvider: ObservableObject {
#Published var availableBuildingBricks: [BuildingBrickItem] = []
init() {
self.availableBuildingBricks = [
TopBrick.personalData,
TopBrick.education,
TopBrick.work,
TopBrick.overviews
].map({ return BuildingBrickItem(title: $0.title,
systemImageName: "stop") })
}
}
struct BuildingBrickItem: Identifiable {
var id: UUID = UUID()
var title: String
var systemImageName: String
}
The right list code:
struct DocumentStructureView: View {
#StateObject var documentStructureProvider: DocumentStructureProvider = DocumentStructureProvider()
var body: some View {
List {
ForEach(documentStructureProvider.documentSections) { section in
Text(section.title)
}
.onInsert(of: ["public.image"]) {
self.insertSection(position: $0,
itemProviders: $1,
top: true)
}
}
}
func insertSection(position: Int, itemProviders: [NSItemProvider], top: Bool) {
for item in itemProviders.reversed() {
item.loadObject(ofClass: UIImage.self) { image, _ in
if let _ = image as? UIImage {
DispatchQueue.main.async {
let section = DocumentSectionItem(title: item.suggestedName ?? "Unknown")
self.documentStructureProvider.insert(section: section, at: position)
}
}
}
}
}
}
final class DocumentStructureProvider: ObservableObject {
#Published var documentSections: [DocumentSectionItem] = []
init() {
documentSections = [
DocumentSectionItem(title: "Dummy")
]
}
func insert(section: DocumentSectionItem, at position: Int) {
if documentSections.count == 0 {
documentSections.append(section)
return
}
documentSections.insert(section, at: position)
}
}
struct DocumentSectionItem: Identifiable {
var id: UUID = UUID()
var title: String
}
Well, I succeeded to make the problem reproducable, code below.
Steps to reproduce:
Drag "A" on "1" as first item on the right.
Drag another "A" on "1", hold it dragged, draw it slowly down after "5" -> crash.
The drop function is not called before the crash.
struct ContentView: View {
var body: some View {
HStack {
LeftList()
Divider()
RightList()
}
}
}
import SwiftUI
import UniformTypeIdentifiers
struct LeftList: View {
var list: [String] = ["A", "B", "C", "D", "E"]
var body: some View {
List(list) { item in
Text(item)
.onDrag {
let stringItemProvider = NSItemProvider(object: item as NSString)
return stringItemProvider
}
}
}
}
import SwiftUI
import UniformTypeIdentifiers
struct RightList: View {
#State var list: [String] = ["1", "2", "3", "4", "5"]
var body: some View {
List {
ForEach(list) { item in
Text(item)
}
.onInsert(
of: [UTType.text],
perform: drop)
}
}
private func drop(at index: Int, _ items: [NSItemProvider]) {
debugPrint(index)
for item in items {
_ = item.loadObject(ofClass: NSString.self) { text, _ in
debugPrint(text)
DispatchQueue.main.async {
debugPrint("dispatch")
text.map { self.list.insert($0 as! String, at: index) }
}
}
}
}
}