I'm trying to save the users favorite cities in UserDefaults. Found this solution saving the struct ID - builds and runs but does not appear to be saving: On app relaunch, the previously tapped Button is reset.
I'm pretty sure I'm missing something…
Here's my data struct and class:
struct City: Codable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
private var cities: Set<String>
let defaults = UserDefaults.standard
var items: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Favorites") {
let cityData = try? decoder.decode(Set<String>.self, from: data)
self.cities = cityData ?? []
return
} else {
self.cities = []
}
}
func getTaskIds() -> Set<String> {
return self.cities
}
func contains(_ city: City) -> Bool {
cities.contains(city.id)
}
func add(_ city: City) {
objectWillChange.send()
cities.contains(city.id)
save()
}
func remove(_ city: City) {
objectWillChange.send()
cities.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(tasks) {
defaults.setValue(encoded, forKey: "Favorites")
}
}
}
and here's the TestDataView
struct TestData: View {
#StateObject var favorites = Favorites()
var body: some View {
ForEach(self.favorites.items, id: \.id) { item in
VStack {
Text(item.title)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .white)
}
}
}
}
}
}
There were a few issues, which I'll address below. Here's the working code:
struct ContentView: View {
#StateObject var favorites = Favorites()
var body: some View {
VStack(spacing: 10) {
ForEach(Array(self.favorites.cities), id: \.id) { item in
VStack {
Text(item.name)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .black)
}
}
}
}
}
}
}
struct City: Codable, Hashable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
#Published var cities: Set<City> = []
#Published var favorites: Set<String> = []
let defaults = UserDefaults.standard
var initialItems: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Cities") {
cities = (try? decoder.decode(Set<City>.self, from: data)) ?? Set(initialItems)
} else {
cities = Set(initialItems)
}
self.favorites = Set(defaults.array(forKey: "Favorites") as? [String] ?? [])
}
func getTaskIds() -> Set<String> {
return self.favorites
}
func contains(_ city: City) -> Bool {
favorites.contains(city.id)
}
func add(_ city: City) {
favorites.insert(city.id)
save()
}
func remove(_ city: City) {
favorites.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(self.cities) {
self.defaults.set(encoded, forKey: "Cities")
}
self.defaults.set(Array(self.favorites), forKey: "Favorites")
defaults.synchronize()
}
}
Issues with the original:
The biggest issue was that items was getting recreated on each new launch and City has an id that is assigned a UUID on creation. This guaranteed that every new launch, each batch of cities would have different UUIDs, so a saving situation would never work.
There were some general typos and references to properties that didn't actually exist.
What I did:
Made cities and favorites both #Published properties so that you don't have to call objectWillChange.send by hand
On init, load both the cities and the favorites. That way, the cities, once initially created, will always have the same UUIDs, since they're getting loaded from a saved state
On save, I save both Sets -- the favorites and the cities
In the original ForEach, I iterate through all of the cities and then only mark the ones that are part of favorites
Important note: While testing this, I discovered that at least on Xcode 12.3 / iOS 14.3, syncing to UserDefaults is slow, even when using the now-unrecommended synchronize method. I kept wondering why my changes weren't reflected when I killed and then re-opened the app. Eventually figured out that everything works if I give it about 10-15 seconds to sync to UserDefaults before killing the app and then opening it again.
Related
I'm fairly new to Swift and Core Data. I’m having a problem resolving a state issue in a new project of mine.
I have a parent view (CategoryView)that includes a context menu item to allow editing of certain category properties (EditCategoryView). When the EditCategoryView sheet is presented and an edit to a category property is made, the CategoriesView updates correctly when the sheet is dismissed. Works fine.
There is a navigation link off of CategoriesView (ItemsView) that also includes a context menu to allow editing of certain item properties (EditItemView). Unlike the prior example, when the EditItemView sheet is presented and an edit is made to an item property, the ItemsView does not update when the sheet is dismissed. The old item property still displays. If I navigate back to CategoriesView and then return to ItemsView, the updated item property displays correctly.
I’m stumped and clearly don’t understand how state is managed in a CoreData environment. My code for the 2 views seems to be similar, yet they are behaving distinctly different. I wonder if the problem relates to the difference in the structures used in the 2 ForEach lines. That is, in CategoriesView I'm looping on the results of a Fetch and in EventsView I'm looping on the results of a computed value.
Any suggestions? thanks in advance for any guidance.
I created a simple example project that demonstrates the problem. To reproduce:
tap on Load Sample Data
choose a Category
tap and hold an Item to bring up context menu
choose Edit and change the name of the item
you’ll note when sheet dismisses the updated name is not reflected
return to Category list and then select the item again to see the updated name
https://github.com/jayelevy/CoreDataState
edit to include the code for the minimal example referenced in the repo
xcdatamodeld
2 Entities
Category
Attribute: name: String
Relationships: items, destination: Item (many-to-one)
Item
Attribute: name: String
Relationships: category, destination: Category (to one)
#main
struct CoreDataStateApp: App {
#StateObject var dataController: DataController
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
var body: some Scene {
WindowGroup {
CategoriesView()
.environment(\.managedObjectContext, dataController.container.viewContext)
.environmentObject(dataController)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), perform: save)
}
}
func save(_ note: Notification) {
dataController.save()
}
}
struct CategoriesView: View {
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
var categories: FetchedResults<Category>
var body: some View {
NavigationView {
VStack {
List {
ForEach(categories) { category in
NavigationLink {
ItemsView(category: category)
} label : {
Text(category.categoryName)
}
}
}
}
.navigationTitle("My Categories")
.toolbar {
ToolbarItem(placement: .automatic) {
Button {
dataController.deleteAll()
try? dataController.createSampleData()
} label: {
Text("Load Sample Data")
}
}
}
}
}
}
problem occurs with the following view. When an item is edited in EditItemView, the updated property (name) does not display when returning to ItemsView from the sheet.
If you return to CategoryView and then return to ItemsView, the correct property name is displayed.
struct ItemsView: View {
#ObservedObject var category: Category
#State private var isEditingItem = false
var body: some View {
VStack {
List {
ForEach(category.categoryItems) { item in
NavigationLink {
//
} label: {
Text(item.itemName)
}
.contextMenu {
Button {
isEditingItem.toggle()
} label: {
Label("Edit Item", systemImage: "pencil")
}
}
.sheet(isPresented: $isEditingItem) {
EditItemView(item: item)
}
}
}
}
.navigationTitle(category.categoryName)
}
}
struct EditItemView: View {
var item: Item
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.dismiss) private var dismiss
#State private var itemName: String
init(item: Item) {
// _item = ObservedObject(initialValue: item)
self.item = item
_itemName = State(initialValue: item.itemName)
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Item Name", text: $itemName)
}
}
}
.navigationTitle("Edit Item")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
// add any needed cancel logic
Button("Cancel") {
dismiss()
}
}
ToolbarItem {
Button {
saveItem()
dismiss()
} label: {
Text("Update")
}
.disabled(itemName.isEmpty)
}
}
}
}
func saveItem() {
item.name = itemName
dataController.save()
}
}
extension Category {
var categoryName: String {
name ?? "New Category"
}
var categoryItems: [Item] {
items?.allObjects as? [Item] ?? []
}
extension Item {
var itemName: String {
name ?? "New Item"
}
}
extension Binding {
func onChange(_ handler: #escaping () -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler()
}
)
}
}
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Fatal error loading store: \(error.localizedDescription)")
}
}
}
static var preview: DataController = {
let dataController = DataController(inMemory: true)
let viewContext = dataController.container.viewContext
do {
try dataController.createSampleData()
} catch {
fatalError("Fatal error creating preview: \(error.localizedDescription)")
}
return dataController
}()
func createSampleData() throws {
let viewContext = container.viewContext
for i in 1...4 {
let category = Category(context: viewContext)
category.name = "Category \(i)"
category.items = []
for j in 1...5 {
let item = Item(context: viewContext)
item.name = "Item \(j)"
item.category = category
}
}
try viewContext.save()
}
func save() {
if container.viewContext.hasChanges {
try? container.viewContext.save()
}
}
func delete(_ object: NSManagedObject) {
container.viewContext.delete(object)
}
func deleteAll() {
let fetchRequest1: NSFetchRequest<NSFetchRequestResult> = Item.fetchRequest()
let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1)
_ = try? container.viewContext.execute(batchDeleteRequest1)
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = Category.fetchRequest()
let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
_ = try? container.viewContext.execute(batchDeleteRequest2)
}
func count<T>(for fetchRequest: NSFetchRequest<T>) -> Int {
(try? container.viewContext.count(for: fetchRequest)) ?? 0
}
}
ItemsView needs its own #FetchRequest for CategoryItem with a predicate where category = %#.
Also, instead of passing your DataController object around just put your helper methods in an extension of NSManagedObjectContext. Then you can change DataController back to the struct it should be.
I imagine there are other opportunities to improve my code (obviously, still learning), per other posts. However, the resolution was quite simple.
Modified saveItem in EditItemView to include objectWillChange.send()
func saveItem() {
item.name = itemName
item.category = itemCategory
item.category?.objectWillChange.send()
dataController.save()
}
I have been having trouble displaying my JSON into my content view. I can decode the data and save it into a dictionary as I have printed and seen. However when its time to display it in ContentView with a ForEach. I'm getting this error Cannot convert value of type '[String : String]' to expected argument type 'Binding' Below is my code for my ContentView, Struct and ApiCall. I have read other solutions on stackoverflow and tried them all but they do not work.
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
let country = api.storedData.countries
VStack(alignment: .leading) {
ForEach(country.id, id: \.self) { country in
HStack(alignment: .top) {
Text("\(country.countries)")
}
}
.onAppear {
api.loadData()
}
}
}
}
My ApiCall class which loads the data, as well as the struct.
// MARK: - Country
struct Country: Codable, Identifiable {
let id = UUID()
var countries: [String: String]
enum CodingKeys: String, CodingKey {
case countries = "countries"
}
}
class APICALL: ObservableObject {
#Published var storedData = Country(countries: [:])
func loadData() {
let apikey = ""
guard let url = URL(string:"https://countries-cities.p.rapidapi.com/location/country/list?rapidapi-key=\(apikey)") else {
print("Your Api end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(Country.self, from: data) {
DispatchQueue.main.async {
self.storedData.countries = response.countries
print(self.storedData.countries)
}
return
}
}
}
.resume()
}
}
Any Point in the right direction would be absolutely helpful.
you could try this approach to display your countries data:
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
VStack(alignment: .leading) {
// -- here --
ForEach(Array(api.storedData.countries.enumerated()), id: \.0) { index, country in
HStack(alignment: .top) {
Text("\(country.key) \(country.value)")
}
}
.onAppear {
api.loadData()
}
}
}
}
you can also use this, if you prefer:
ForEach(api.storedData.countries.sorted(by: >), id: \.key) { key, value in
HStack(alignment: .top) {
Text("\(key) \(value)")
}
}
I am creating custom data and I want to save them into CoreData when I make favorites. In order to do that I use the Combine framework by subscribing CoreData values back into my custom data. The problem is when I try to map both CoreData and custom data, there is something wrong and I couldn't display even my custom data on the canvas. To be honest, I don't even know what I am doing because most of the ViewModel codes are based on Nick's tutorial video (from the Swiftful Thinking Youtube channel). Please help me with what is wrong with my codes. Thanks in advance.
I create my CoreData with a name "DataContainer" with entity name "DataEntity". In DataEntity, there are three attributes:
'id' with a type "Integer32"
'isFavorite' with a type "Boolean"
'timestamp' with a type "Date"
import Foundation
import CoreData
// This is CoreData class without using Singleton
class CoreDataManager {
private let container: NSPersistentContainer
#Published var savedEntities: [DataEntity] = []
init() {
container = NSPersistentContainer(name: "DataContainer")
container.loadPersistentStores { _, error in
if let error = error {
print("Error loading CoreData! \(error)")
}
}
fetchData()
}
// MARK: Privates
private func fetchData() {
let request = NSFetchRequest<DataEntity>(entityName: "DataEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching DataEntity! \(error)")
}
}
// Add to CoreData
private func addFavorite(dataID: DataArray, onTappedFavorite: Bool) {
let newFavorite = DataEntity(context: container.viewContext)
newFavorite.id = Int32(dataID.id)
newFavorite.isFavorite = onTappedFavorite
applyChanges()
}
// Update time
private func updateTime() {
let newTime = DataEntity(context: container.viewContext)
newTime.timestamp = Date()
}
// Save to CoreData
private func save() {
do {
try container.viewContext.save()
} catch let error {
print("Error saving to CoreData! \(error)")
}
}
private applyChanges() {
save()
updateTime()
fetchData()
}
private func update(entity: DataEntity, updateFavorite: Bool) {
entity.isFavorite = updateFavorite
applyChanges()
}
private func delete(entity: DataEntity) {
container.viewContext.delete(entity)
applyChanges()
}
// MARK: Public
func updateFavorite(dataID: DataArray, onTappedFavorite: Bool) {
// Checking the data is already taken
if let entity = savedEntities.first(where: { $0.id == dataID.id }) {
if onTappedFavorite {
update(entity: entity, updateFavorite: onTappedFavorite)
} else {
delete(entity: entity)
}
} else {
addFavorite(dataID: dataID, onTappedFavorite: onTappedFavorite)
}
}
}
This will be my Model:
import Foundation
import SwiftUI
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
let isFavorite: Bool
func updateFavorite(favorited: Bool) -> DataArray {
return DataArray(id: id, cities: cities, name1: name1, name2: name2, isFavorite: favorited)
}
}
public struct ListDataArray {
static let dot = [
DataArray(id: 1,
cities: "Baltimore"
name1: "John",
name2: "Mike",
isFavorite: False),
DataArray(id: 2,
cities: "Frederick"),
name1: "Joe",
name2: "Swift",
isFavorite: False),
DataArray(id: 3,
cities: "Catonsville"
name1: "Susan",
name2: "Oliver",
isFavorite: False),
// There will be a lot of data
]
}
This will be my Home ViewModel:
import Foundation
import SwiftUI
import Combine
class Prospect: ObservableObject {
#Published var datas: [DataArray] = []
private let coreDataServices = CoreDataManager()
private var cancellables = Set<AnyCancellable>()
init() {
fetchDataArrays()
fetchCoreData()
}
private func fetchDataArrays() {
let items = ListDataArray.dot
datas = items
}
private func fetchCoreData() {
coreDataServices.$savedEntities
.map({ (coreData) -> [DataArray] in
// Here is something wrong when I check and try to convert CoreData to DataArray
let arrays: [DataArray] = []
return arrays
.compactMap { (data) -> DataArray? in
guard let entity = coreData.first(where: { $0.id == data.id }) else {
return nil
}
return data.updateFavorite(favorited: entity.isFavorite)
}
})
.sink {[weak self] (receivedEntities) in
self?.datas = receivedEntities
}
.store(in: &cancellables)
}
func updateFavoriteData(dataID: DataArray, isFavorite: Bool) {
coreDataServices.updateFavorite(dataID: dataID, onTappedFavorite: isFavorite)
}
// To View Favorite
#Published var showFavorite: Bool = false
}
This is my View:
import SwiftUI
struct Home: View {
#EnvironmentObject var items: Prospect
var body: some View {
ScrollView {
LazyVStack {
ForEach(items.datas) { data in
VStack {
HStack {
Button {
//Action for making favorite or unfavorite
items.updateFavoriteData(dataID: data, isFavorite: data.isFavorite)
} label: {
Image(systemName: data.isFavorite ? "suit.heart.fill" : "suit.heart")
}
Spacer()
Button {
items.showFavorite.toggle()
} label: {
Image(systemName: "music.note.house.fill")
}
.sheet(isPresented: $items.showFavorite) {
FavoriteView()
.environmentObject(items)
}
}
Text("\(data.id)")
.font(.title3)
Text(data.cities)
.font(.subheadline)
Spacer()
}
padding()
}
.padding()
}
}
}
}
struct FavoriteView: View {
#EnvironmentObject var items: Prospect
var body: some View {
VStack {
List {
ForEach(items.datas) { data in
if data.isFavorite {
VStack(spacing: 10) {
Text(data.cities)
Text(data.name1)
Text(data.name2)
}
.font(.body)
}
}
.padding()
}
Spacer()
}
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(Prospect())
}
}
Here is a different approach
//extend DataEntity
extension DataEntity{
//Have a dataArray variable that links with your array
var dataArray: DataArray?{
ListDataArray.dot.first(where: {
$0.id == self.id
})
}
}
//extend DataArray
extension DataArray{
//have a data entity variable that retrieves the CoreData object
var dataEntity: DataEntity{
//Use DataEntity Manager to map
//Find or Create
return DataEntityManager().retrieve(dataArray: self)
}
}
//Have an entity manager
class DataEntityManager{
let container = PersistenceController.previewAware
//convenience to create
func create(id: Int32) -> DataEntity{
let entity = DataEntity(context: container.container.viewContext)
entity.id = id
save()
return entity
}
//for updating to centralize work
func update(entity: DataEntity){
//I think this is what you intend to update the timestamp when the value changes
entity.timestamp = Date()
//get the array variable
var dataArry = entity.dataArray
//See if they match to prevent loops
if dataArry?.isFavorite != entity.isFavorite{
//if they dont update the array
dataArry = dataArry?.updateFavorite(favorited: entity.isFavorite)
}else{
//Leave alone
}
save()
}
//for updating to centralize work
func update(dataArray: DataArray){
//get the entity
let entity = dataArray.dataEntity
//See if they match to prevent loops
if entity.isFavorite != dataArray.isFavorite{
//if they dont update the entity
DataEntityManager().update(entity: entity)
}else{
//leave alone
}
}
func retrieve(dataArray: DataArray) -> DataEntity{
let request: NSFetchRequest = DataEntity.fetchRequest()
request.predicate = NSPredicate(format: "id == %#", dataArray.id)
do{
let result = try controller.container.viewContext.fetch(request).first
//This is risky because it will create a new one
//You can handle it differently if you prefer
let new = create(id: Int32(dataArray.id))
update(entity: new)
return result ?? new
}catch{
print(error)
//This is risky because it will create a new one
//You can handle it differently if you prefer
let new = create(id: Int32(dataArray.id))
update(entity: new)
return new
}
}
func save() {
do{
try container.container.viewContext.save()
}catch{
print(error)
}
}
}
struct DataArray: Identifiable {
let id: Int
let cities: String
let name1: String
let name2: String
var isFavorite: Bool
//Mostly your method
mutating func updateFavorite(favorited: Bool) -> DataArray {
//get the new value
isFavorite = favorited
//update the entity
DataEntityManager().update(dataArray: self)
//return the new
return self
}
}
With this you can now access the matching variable using either object
dataEntity.dataArray
or
dataArray.dataEntity
Remember to update using the methods in the manager or the array so everything stays in sync.
Something to be aware of. CoreData objects are ObservableObjects where ever you want to see changes for the DataEntity you should wrap them in an #ObservedObject
I want to use a realm database in my SwiftUI app and I would like to apply the MVVM pattern. Unfortunately when I create a list with the elements in my database I get a Fatal error: Unexpectedly found nil while unwrapping an Optional value: error message
DatabaseManager:
class DatabaseManager{
private let realm: Realm
public static let sharedInstance = DatabaseManager()
private init(){
realm = try! Realm()
}
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = Realm.objects(type)
return results
}
}
Model:
class FlashcardDeck: Object, Codable, Identifiable{
#objc private (set) dynamic var id = NSUUID().uuidString
#objc dynamic var title: String?
var cards = RealmSwift.List<Flashcard>()
convenience init(title: String?, cards: [Flashcard]){
self.init()
self.title = title
self.cards.append(objectsIn: cards)
}
override class func primaryKey() -> String? {
return "id"
}
}
ViewModel
class FlashcardDeckViewModel: ObservableObject{
let realm = DatabaseManager.sharedInstance
#Published var decks: Results<FlashcardDeck>?
public func fetchDecks(){
decks = realm.fetchData(type: FlashcardDeck.self)
}
}
View
struct FlashcardDeckView: View {
private let gridItems = [GridItem(.flexible())]
#StateObject var viewModel = FlashcardDeckViewModel()
var body: some View {
NavigationView{
ScrollView{
LazyVGrid(columns: gridItems, spacing: 30){
ForEach(viewModel.decks!) { item in // <----- ERROR APPEARS HERE
FlashcardDeckItem(deck: item)
}
}
}
.navigationTitle("Flashcard decks")
}
.onAppear{
self.viewModel.fetchDecks()
print(self.viewModel.cards?[0].title) // <------ prints the title of the deck! So this element exists
}
}
}
I'm pretty sure that my database has an element and if I try to print the name of the deck in the fetchData()function it will be displayed. I know the line ForEach(viewModel.decks!)isn't beautiful code, but this is just for testing/debugging now.
Include it conditionally, like
NavigationView{
if viewModel.decks == nil {
Text("Loading...")
} else {
ScrollView{
LazyVGrid(columns: gridItems, spacing: 30){
ForEach(viewModel.decks!) { item in // <----- ERROR APPEARS HERE
FlashcardDeckItem(deck: item)
}
}
}
.navigationTitle("Flashcard decks")
}
}
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) }
}
}
}
}
}