Is it possible to update view from function swiftui? - swift

I have such view:
struct PersonalPage: View {
let preferences = UserDefaults.standard
var body: some View {
VStack(alignment:.leading){
HStack {
Image(systemName: "mappin.circle.fill")
VStack {
Text("88")
Text("88")
Text("88")
}
}
}.onAppear {
self.getPersonalData()
}
}
var mainSession = Session(configuration: URLSessionConfiguration.default, interceptor: EnvInterceptor())
func getPersonalData(){
var request = URLRequest(url: URL(string:Pathes.init().userInfo)!)
request.httpMethod = "GET"
mainSession.request(request).responseDecodable(of:PersonalInfo.self) { (response) in
switch response.result{
case .success:
guard let userData = response.value else { return }
self.preferences.set(userData.applicant.email,forKey: "applicant_email")
self.preferences.set(userData.applicant.id, forKey: "applicant_id")
self.preferences.set(userData.applicant.delete, forKey: "applicant_can_delete")
self.preferences.set(userData.applicant.caseManager, forKey: "casemanager_assigned")
self.preferences.set(userData.applicant.photoChecksum, forKey: "photo_checksum")
self.preferences.set(userData.consultant.firstname, forKey: "cons_firstname")
self.preferences.set(userData.consultant.lastname, forKey: "cons_lastname")
self.preferences.set(userData.consultant.id, forKey: "cons_id")
self.preferences.synchronize()
HomeScreen().self.addBadges()
case .failure:
print(response.response?.statusCode as Any)
}
}
}
}
and as you can see I have method for getting data from api. So, is it possible to update for example Text with some text from server response and if it possible how I can do it? As I understood I can't do smth like global view variable and have access to its elements from any place of struct?

If you declare a #State property for each of your desired Text views you can use them in the View body and update them from your function. Here is a brief example I hope helps;
struct PersonalPage: View {
let preferences = UserDefaults.standard
#State private var textOne = ""
#State private var textTwo = ""
#State private var textThree = ""
var body: some View {
VStack(alignment:.leading){
HStack {
Image(systemName: "mappin.circle.fill")
VStack {
Text(textOne)
Text(textTwo)
Text(textThree)
}
}
}.onAppear {
self.getPersonalData()
}
}
func getPersonalData() {
textOne = "Hello World"
textTwo = "Map"
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
textThree = "Delayed Text"
}
}
}

struct PersonalPage: View {
#State var personalDataModel = PersonalDataModel()
var body: some View {
VStack(alignment:.leading){
HStack {
Image(systemName: "mappin.circle.fill")
VStack {
Text(personalDataModel.cons_lastname)
Text(personalDataModel.cons_firstname)
Text(personalDataModel.photo_checksum)
}
}
}.onAppear {
self.getPersonalData()
}
}
var mainSession = Session(configuration: URLSessionConfiguration.default, interceptor: EnvInterceptor())
func getPersonalData(){
var request = URLRequest(url: URL(string:Pathes.init().userInfo)!)
request.httpMethod = "GET"
mainSession.request(request).responseDecodable(of:PersonalInfo.self) { (response) in
switch response.result{
case .success:
guard let userData = response.value else { return }
let personalDataModel = PersonalDataModel()
personalDataModel.applicant_email = userData.applicant.email
personalDataModel.applicant_id = userData.applicant.id
personalDataModel.cons_firstname = userData.consultant.firstname
self.personalDataModel = personalDataModel
HomeScreen().self.addBadges()
case .failure:
print(response.response?.statusCode as Any)
}
}
}
}
struct PersonalDataModel : Codable {
public var applicant_email: String = ""
public var id: String = ""
public var applicant_can_delete: String = ""
public var applicant_id: String = ""
public var designation: String = ""
public var casemanager_assigned: String = ""
public var photo_checksum: String = ""
public var cons_firstname: String = ""
public var cons_lastname: String = ""
public var cons_id: String = ""
}

Related

How to change the value of a var with a TextField SwiftUI

I was trying to make a weather api call, the api call needs to have a location. The location that I pass is a variable, but now I want to change the location value based on a TextField's input.
I made the apiKey shorter just for safety measures. There's more code, but it's not relevant.
I just need to know how to change the city variable that is on the WeatherClass using the TextField that is in the cityTextField View.
Thanks.
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
var city: String = ""
func fetch() {
let location = city
let apiKey = "AP8LUYMSTHZ"
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(location)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}//----------------------------------- End of fetch()
}
struct WeatherData: Decodable {
let resolvedAddress: String
let days: [WeatherDays]
}
struct WeatherDays: Hashable, Decodable {
let datetime: String
let tempmax: Double
let tempmin: Double
let description: String
}
struct cityTextField: View {
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city).frame(height:30).multilineTextAlignment(.center).background().cornerRadius(25).padding(.horizontal)
}
}
I already watched a lot of tutorials for similar things buts none of them really helped me.
Try this approach using minor modifications to
func fetch(_ city: String){...} to fetch the weather for the city in your
TextField using .onSubmit{...}
struct ContentView: View {
#StateObject var weatherModel = WeatherClass()
var body: some View {
VStack {
cityTextField(weatherModel: weatherModel)
}
}
}
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
#State var city: String = ""
var body: some View {
TextField("Search city", text: $city)
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch(city) // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
func fetch(_ city: String) { // <-- here
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}
Alternatively, as suggested by synapticloop, you could use this approach:
struct cityTextField: View {
#ObservedObject var weatherModel: WeatherClass // <-- here
var body: some View {
TextField("Search city", text: $weatherModel.city) // <-- here
.frame(height:30)
.multilineTextAlignment(.center)
.background()
.cornerRadius(25)
.padding(.horizontal)
.onSubmit {
weatherModel.fetch() // <-- here
}
}
}
class WeatherClass: ObservableObject {
#Published var weatherAddress: String = ""
#Published var weatherDays: [WeatherDays] = []
#Published var city: String = "" // <-- here
func fetch() {
let apiKey = "AP8LUYMSTHZ"
// -- here
let url = URL(string: "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/\(city)?key=\(apiKey)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
if let weather = try? JSONDecoder().decode(WeatherData.self, from: data) {
DispatchQueue.main.async {
self.weatherAddress = weather.resolvedAddress
self.weatherDays = weather.days
}
} else {
print("City?")
}
}.resume()
}
}

Displaying State of an Async Api call in SwiftUI

This question builds on my previous question. Basically Im making an async call to the Google Books Api when a certain button is pressed. While I got the call working when its a method of the View however I want to overlay an activity indicator while it's loading. Hence I tried making an ObservableObject to make the call instead but Im not sure how to do it.
Here's what I have so far:
class GoogleBooksApi: ObservableObject {
enum LoadingState<Value> {
case loading(Double)
case loaded(Value)
}
#Published var state: LoadingState<GoogleBook> = .loading(0.0)
enum URLError : Error {
case badURL
}
func fetchBook(id identifier: String) async throws {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { throw URLError.badURL }
self.state = .loading(0.25)
let (data, _) = try await URLSession.shared.data(from: url)
self.state = .loading(0.75)
self.state = .loaded(try JSONDecoder().decode(GoogleBook.self, from: data))
}
}
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
#ObservedObject var api: GoogleBooksApi
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
Task {
do {
try await api.fetchBook(id: code)
let fetchedBooks = api.state
let book = fetchedBooks.items[0].volumeInfo
name = book.title
author = book.authors?[0] ?? ""
total = String(book.pageCount!)
} catch {
print(error)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
and this is what works without the loading states:
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
enum URLError : Error {
case badURL
}
private func fetchBook(id identifier: String) async throws -> GoogleBook {
guard let encodedString = "https://www.googleapis.com/books/v1/volumes?q={\(identifier)}"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: encodedString) else { throw URLError.badURL}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(GoogleBook.self, from: data)
}
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
Task {
do {
let fetchedBooks = try await fetchBook(id: code)
let book = fetchedBooks.items[0].volumeInfo
name = book.title
author = book.authors?[0] ?? ""
total = String(book.pageCount!)
} catch {
print(error)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
I would go a step further and add idle and failed states.
Then instead of throwing an error change the state to failed and pass the error description. I removed the Double value from the loading state to just show a spinning ProgressView
#MainActor
class GoogleBooksApi: ObservableObject {
enum LoadingState {
case idle
case loading
case loaded(GoogleBook)
case failed(Error)
}
#Published var state: LoadingState = .idle
func fetchBook(id identifier: String) async {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { state = .failed(URLError(.badURL)); return }
self.state = .loading
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(GoogleBook.self, from: data)
self.state = .loaded(response)
} catch {
state = .failed(error)
}
}
}
In the view you have to switch on the state and show different views.
And – very important – you have to declare the observable object as #StateObject. This is a very simple implementation
struct ContentView: View {
#State var code = "ISBN"
#StateObject var api = GoogleBooksApi()
var body: some View {
VStack {
switch api.state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded(let books):
if let info = books.items.first?.volumeInfo {
Text("Name: \(info.title)")
Text("Author: \(info.authors?.joined(separator: ", ") ?? "")")
Text("total: \(books.totalItems)")
}
case .failed(let error):
if error is DecodingError {
Text(error.description)
} else {
Text(error.localizedDescription)
}
}
Button(action: {
code = "978-0441013593"
Task {
await api.fetchBook(id: code)
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
It seems like you're not initializing the GoogleBooksApi.
#ObservedObject var api: GoogleBooksApi
neither any init where it can be modified.
Other than that - I'd suggest using #StateObject (provided you deployment target is minimum iOS 14.0). Using ObservableObject might lead to multiple initializations of the GoogleBooksApi (whereas you need only once)
You should use #StateObject for any observable properties that you
initialize in the view that uses it. If the ObservableObject instance
is created externally and passed to the view that uses it mark your
property with #ObservedObject.

Can't add data fetched from Firebase to new Array

I'm am fetching data from firebase from 3 different collections. Once I have the data fetched I would like to append the data from the 3 functions to 1 new array so I have all the data stored one place. But once I append it comes out empty like the fetch functions didn't work. I've tested and debugged and the data is there but I can't seem to add the fetched data to a new Array.
Model
import Foundation
import SwiftUI
struct GameModel: Identifiable {
var id = UUID()
var title: String
var descriptionMenu: String
var imageNameMenu: String
}
Fetch Data Class
import Foundation
import SwiftUI
import Firebase
class SearchController: ObservableObject {
#Published var allGames = [GameModel]()
#Published var cardsMenu = [GameModel]()
#Published var diceMenu = [GameModel]()
#Published var miscellaneuosMenu = [GameModel]()
private var db = Firestore.firestore()
func fetchCardGamesData() {...}
func fetchDiceGamesData() {...}
func fetchMiscGamesData() {...}
func combineGames() {
for i in cardsMenu {
allGames.append(i)
}
for n in diceMenu {
allGames.append(n)
}
for x in miscellaneuosMenu {
allGames.append(x)
}
}
}
Fetch data Functions
func fetchCardGamesData() {
db.collection("cardsMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.cardsMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let allGameInfo = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return allGameInfo
}
}
}
func fetchDiceGamesData() {
db.collection("diceMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.diceMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let allGameInfo = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return allGameInfo
}
}
}
func fetchMiscGamesData() {
db.collection("miscellaneuosMenu").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.miscellaneuosMenu = documents.map { (queryDocumentSnapshot) -> GameModel in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let descriptionMenuRecieved = data["descriptionMenu"] as? String ?? ""
let descriptionMenu = descriptionMenuRecieved.replacingOccurrences(of: "\\n", with: "\n")
let imageNameMenu = data["imageNameMenu"] as? String ?? ""
let miscellaneousGames = GameModel(title: title, descriptionMenu: descriptionMenu, imageNameMenu: imageNameMenu)
return miscellaneousGames
}
}
}
View
import SwiftUI
import Foundation
struct SearchView: View {
#ObservedObject var allGames = SearchController()
var body: some View {
NavigationView{
ZStack(alignment: .top) {
GeometryReader{_ in
//Text("Home")
}
.background(Color("Color").edgesIgnoringSafeArea(.all))
SearchBar(data: self.$allGames.allGames)
.padding(.top)
}
.navigationBarTitle("Search")
.padding(.top, -20)
.onAppear(){
self.allGames.fetchCardGamesData()
self.allGames.fetchDiceGamesData()
self.allGames.fetchMiscGamesData()
self.allGames.combineGames()
}
}
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(PlainListStyle())
}
}
SearchBar
struct SearchBar: View {
#State var txt = ""
#Binding var data: [GameModel]
var body: some View {
VStack(spacing: 0){
HStack{
TextField("Search", text: self.$txt)
if self.txt != "" {
Button(action: {
self.txt = ""
}, label: {
Image(systemName: "xmark.circle.fill")
})
.foregroundColor(.gray)
}
}.padding()
if self.txt != ""{
if self.data.filter({$0.title.lowercased().contains(self.txt.lowercased())}).count == 0 {
Text("No Results Found")
.foregroundColor(Color.black.opacity(0.5))
.padding()
}
else {
List(self.data.filter{$0.title.lowercased().contains(self.txt.lowercased())}){
i in
NavigationLink(destination: i.view.navigationBarTitleDisplayMode(.inline).onAppear(){
// Clear searchfield when return
txt = ""
}) {
Text(i.title)
}
}
.frame(height: UIScreen.main.bounds.height / 3)
.padding(.trailing)
}
}
}
.background(Color.white)
.cornerRadius(10)
.padding()
}
}
Hope you guys can help me.
All of your fetchCardGamesData, fetchDiceGamesData, and fetchMiscGamesData functions have asynchronous requests in them. That means that when you call combineGames, none of them have completed, so you're just appending empty arrays.
In your situation, the easiest would probably be to make allGames a computed property. Then, whenever one of the other #Published properties updates after their fetch methods, the computed property will be re-computed and represented in your SearchBar:
class SearchController: ObservableObject {
#Published var cardsMenu = [GameModel]()
#Published var diceMenu = [GameModel]()
#Published var miscellaneuosMenu = [GameModel]()
var allGames: [GameModel] {
cardsMenu + diceMenu + miscellaneuosMenu
}
}
Note there's no longer a combineGames function, so you won't call that anymore.

Swift load user data for dashboard after login

I am trying to retrieve user data once the user gets to the dashboard of my app
I have essentially this to get data:
class UserController: ObservableObject {
#Published var firstName: String = ""
func fetchUser(token: String) {
/* Do url settings */
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let rData = try! JSONDecoder().decode(User.self, from: data)
let userData = [
"id": rData.id,
"firstName": rData.firstName,
"lastName": rData.lastName,
"department": rData.department,
]
UserDefaults.standard.set(userData, forKey: "user")
DispatchQueue.main.async {
self.firstName = rData.firstName
}
}.resume()
}
}
And then my view looks like this
struct HomeViewCollection: View {
#Binding var isAuthenticated: Bool
#ObservedObject var userController: UserController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
}
}
I'm just not sure how can I activate fetchUser from the View.
I have tried this in the controller
init() {
guard let tokenData = KeyChain.load(key: "token") else { return }
var token = String(data: tokenData, encoding: .utf8)
if(token != nil) {
print("Token: \(token)")
fetchUser(token: token!)
}
}
That didn't work, and then I tried userController.fetchUser(token: KeyChainTokenHere) and that didn't work because it doesn't conform to the struct.
Try passing the token to HomeViewCollection and initiating the call in onAppear completion block.
struct HomeViewCollection: View {
var token: String
#Binding var isAuthenticated: Bool
#ObservedObject var userController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
.onAppear {
self.userController.fetchUser(token: self.token)
}
}
}
Also, make sure the firstName property is getting set.
#Published var firstName: String = "" {
didSet {
print("firstName is set as \(firstName)")
}
}

Crash when deleting data with relation in RealmSwift

I am creating an application using RealmSwift.
The following implementation crashed when deleting related data.
After removing only "UnderlayerItem", it succeeded.
Crash when deleting UnderlayerItem and deleting Item.
The error is:
Thread 1: Exception: "The RLMArray has been invalidated or the object
containing it has been deleted."
How do I delete without crashing?
struct ListView: View {
#ObservedObject private var fetcher = Fetcher()
#State private var title = ""
var body: some View {
NavigationView {
VStack {
TextField("add", text: $title) {
let item = Item()
item.title = self.title
let realm = try! Realm()
try! realm.write {
realm.add(item)
}
self.title = ""
}
ForEach(self.fetcher.items) { (item: Item) in
NavigationLink(destination: DetailView(item: item, id: item.id)) {
Text(item.title)
}
}
}
}
}
}
struct DetailView: View {
var item: Item
var id: String
#State private var title = ""
#ObservedObject private var fetcher = Fetcher()
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
TextField("add", text: $title) {
let realm = try! Realm()
if let item = realm.objects(Item.self).filter("id == '\(self.id)'").first {
try! realm.write() {
let underlayerItem = UnderlayerItem()
underlayerItem.title = self.title
item.underlayerItems.append(underlayerItem)
}
}
self.title = ""
}
ForEach(self.item.underlayerItems) { (underlayerItems: UnderlayerItem) in
Text(underlayerItems.title)
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
self.fetcher.delete(id: self.id)
}) {
Text("Delete")
}
}
}
}
class Fetcher: ObservableObject {
var realm = try! Realm()
var objectWillChange: ObservableObjectPublisher = .init()
private(set) var items: Results<Item>
private var notificationTokens: [NotificationToken] = []
init() {
items = realm.objects(Item.self)
notificationTokens.append(items.observe { _ in
self.objectWillChange.send()
})
}
func delete(id: String) {
guard let item = realm.objects(Item.self).filter("id == '\(id)'").first else { return }
try! realm.write() {
for underlayerItem in item.underlayerItems {
realm.delete(realm.objects(UnderlayerItem.self).filter("id == '\(underlayerItem.id)'").first!)
}
}
try! realm.write() {
realm.delete(item)
}
}
}
class Item: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
let underlayerItems: List<UnderlayerItem> = List<UnderlayerItem>()
override static func primaryKey() -> String? {
return "id"
}
}
class UnderlayerItem: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
override static func primaryKey() -> String? {
return "id"
}
}
You don't need to iterate over the objects in the list to delete them. Just do this
try! realm.write() {
realm.delete(item.underlayerItems)
}
I believe it's crashing because you're attempting to access an item that was deleted
self.item.underlayerItems