.OnDelete not working with Realm data, How can I fix this? - swift

When I run my app and try swiping, the onDelete does not appear and doesn't work. I haven't had the chance to really test if it deletes or not because when I swipe it doesn't allow me to try deleting it. I am using RealmSwift and posted the code for the view as well as the ViewModel I use. Sorry if this isn't enough code, let me know and I'll link my GitHub repo, or share more code.
import SwiftUI
import RealmSwift
import Combine
enum ActiveAlert{
case error, noSauce
}
struct DoujinView: View {
#ObservedObject var doujin: DoujinAPI
// #ObservedResults(DoujinInfo.self) var doujinshis
#State private var detailViewShowing: Bool = false
#State private var selectedDoujin: DoujinInfo?
#StateObject var doujinModel = DoujinInfoViewModel()
var body: some View {
//Code if there are any Doujins
ScrollView(.vertical) {
LazyVStack(spacing: 0) {
ForEach(doujinModel.doujins, id: \.UniqueID) { doujinshi in
Button(action: {
self.detailViewShowing = true
self.doujinModel.selectedDoujin = doujinshi
}) {
DoujinCell(image: convertBase64ToImage(doujinshi.PictureString))
}
}
.onDelete(perform: { indexSet in
self.doujinModel.easyDelete(at: indexSet)
})
//This will preseent the sheet that displays information for the doujin
.sheet(isPresented: $detailViewShowing, onDismiss: {if doujinModel.deleting == true {doujinModel.deleteDoujin()}}, content: {
DoujinInformation(theAPI: doujin, doujinModel: doujinModel)
})
// Loading circle
if doujin.loadingCircle == true{
LoadingCircle(theApi: doujin)
}
}
}
}
}
enum colorSquare:Identifiable{
var id: Int{
hashValue
}
case green
case yellow
case red
}
class DoujinInfoViewModel: ObservableObject{
var theDoujin:DoujinInfo? = nil
var realm:Realm?
var token: NotificationToken? = nil
#ObservedResults(DoujinInfo.self) var doujins
#Published var deleting:Bool = false
#Published var selectedDoujin:DoujinInfo? = nil
#Published var loading:Bool = false
init(){
let realm = try? Realm()
self.realm = realm
token = doujins.observe({ (changes) in
switch changes{
case .error(_):break
case .initial(_): break
case .update(_, deletions: _, insertions: _, modifications: _):
self.objectWillChange.send() }
})
}
deinit {
token?.invalidate()
}
var name: String{
get{
selectedDoujin!.Name
}
}
var id: String {
get {
selectedDoujin!.Id
}
}
var mediaID:String {
get {
selectedDoujin!.MediaID
}
}
var numPages:Int{
get {
selectedDoujin!.NumPages
}
}
var pictureString:String {
get {
selectedDoujin!.PictureString
}
}
var uniqueId: String{
get{
selectedDoujin!.PictureString
}
}
var similarity:Double{
get {
selectedDoujin!.similarity
}
}
var color:colorSquare{
get{
switch selectedDoujin!.similarity{
case 0...50:
return .red
case 50...75:
return .yellow
case 75...100:
return .green
default:
return .green
}
}
}
var doujinTags: List<DoujinTags>{
get {
selectedDoujin!.Tags
}
}
func deleteDoujin(){
try? realm?.write{
realm?.delete(selectedDoujin!)
}
deleting = false
}
func easyDelete(at indexSet: IndexSet){
if let index = indexSet.first{
let realm = doujins[indexSet.first!].realm
try? realm?.write({
realm?.delete(doujins[indexSet.first!])
})
}
}
func addDoujin(theDoujin: DoujinInfo){
try? realm?.write({
realm?.add(theDoujin)
})
}
}

.onDelete works only for List. For LazyVStack we need to create our own swipe to delete action.
Here is the sample demo. You can modify it as needed.
SwipeDeleteRow View
struct SwipeDeleteRow<Content: View>: View {
private let content: () -> Content
private let deleteAction: () -> ()
private var isSelected: Bool
#Binding private var selectedIndex: Int
private var index: Int
init(isSelected: Bool, selectedIndex: Binding<Int>, index: Int, #ViewBuilder content: #escaping () -> Content, onDelete: #escaping () -> Void) {
self.isSelected = isSelected
self._selectedIndex = selectedIndex
self.content = content
self.deleteAction = onDelete
self.index = index
}
#State private var offset = CGSize.zero
#State private var offsetY : CGFloat = 0
#State private var scale : CGFloat = 0.5
var body : some View {
HStack(spacing: 0){
content()
.frame(width : UIScreen.main.bounds.width, alignment: .leading)
Button(action: deleteAction) {
Image("delete")
.renderingMode(.original)
.scaleEffect(scale)
}
}
.background(Color.white)
.offset(x: 20, y: 0)
.offset(isSelected ? self.offset : .zero)
.animation(.spring())
.gesture(DragGesture(minimumDistance: 30, coordinateSpace: .local)
.onChanged { gestrue in
self.offset.width = gestrue.translation.width
print(offset)
}
.onEnded { _ in
self.selectedIndex = index
if self.offset.width < -50 {
self.scale = 1
self.offset.width = -60
self.offsetY = -20
} else {
self.scale = 0.5
self.offset = .zero
self.offsetY = 0
}
}
)
}
}
Demo View
struct Model: Identifiable {
var id = UUID()
}
struct CustomSwipeDemo: View {
#State var arr: [Model] = [.init(), .init(), .init(), .init(), .init(), .init(), .init(), .init()]
#State private var listCellIndex: Int = 0
var body: some View {
ScrollView(.vertical) {
LazyVStack(spacing: 0) {
ForEach(arr.indices, id: \.self) { index in
SwipeDeleteRow(isSelected: index == listCellIndex, selectedIndex: $listCellIndex, index: index) {
if let item = self.arr[safe: index] {
Text(item.id.description)
}
} onDelete: {
arr.remove(at: index)
self.listCellIndex = -1
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
}
}
}
}
Helper function
//Help to preventing delete row from index out of bounds.
extension Collection where Indices.Iterator.Element == Index {
subscript (safe index: Index) -> Iterator.Element? {
return indices.contains(index) ? self[index] : nil
}
}

Related

Cannot convert value of type 'Binding<[ContactEntity]>.Element' (aka 'Binding<ContactEntity>') to expected argument type 'ContactEntity'

Using Xcode 13.4.1 on macOS 12.5. I revised the working code to conform to MVVM. This was successful for the first Entity (all properties Optional) for all CRUD operations.
Using this code as a base, I tackled the second Entity (one Bool property NOT Optional), but it throws the compiler error inside the ForEach loop, against 'contact'. This code was error-free before the MVVM conversion. I've been at this for 4 days and am reaching out, but clearly my limited knowledge is inadequate.
ContactListView code below, supported by the ContactViewModel, which in turn relies on the CoreDataManager code.
import SwiftUI
import CoreData
//class FirstNameSort: ObservableObject {
// #Published var firstNameSort: Bool = false
//}
struct ContactsListView: View {
// MARK: - PROPERTIES
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var contactVM = ContactViewModel()
#State private var totalContacts: Int = 0
#State private var search: String = ""
#State private var searchByChampions = false
#State var searchByFirstNames = false
#State private var totalChampions = 0
// MARK: - BODY
var body: some View {
NavigationView {
VStack {
// HStack {
// Toggle("Display Champions only", isOn: $searchByChampions)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
// Toggle("Sort by First Names", isOn: $contactVM.sortFirstName)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
//}
List {
HStack {
Text(searchByChampions ? "Total Champions" : "Total Contacts")
.foregroundColor(.gray)
Spacer()
Text("\(searchByChampions ? totalChampions : totalContacts)")
.bold()
}.foregroundColor(.green)
.padding()
ForEach($contactVM.listofContacts) { contact in
NavigationLink(destination:
ModifyContactView(contact: ***contact***)
.id(UUID()), label: {
ContactRowView(contact: ***contact***)
.id(UUID())
})
}
.onDelete(perform: contactVM.deleteContact)
}.navigationTitle("Contacts")
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: AddContactView(), label: {
Image(systemName: "plus.circle")
})
}
}
.onAppear {
countContacts()
countChampions()
}
.searchable(text: $search, prompt: Text("Contact Last Name?"))
// .onChange(of: search) { value in
// if !value.isEmpty {
// listofContacts.nsPredicate = NSPredicate(format: "contactLastName CONTAINS[dc] %#", value)
// } else {
// listofContacts.nsPredicate = nil
// }
// }
}
}.navigationViewStyle(.stack)
}
func countContacts() {
totalContacts = $contactVM.listofContacts.count
}
// func countChampions() {
// totalChampions = $contactVM.listOfChampions.count
// }
}
import CoreData
import SwiftUI
class ContactViewModel: ObservableObject {
#Environment(\.dismiss) var dismiss
#ObservedObject var dataVM = CoreDataManager()
#ObservedObject var qualifierVM = QualifierViewModel()
#Published var inputFirstName: String = ""
#Published var inputLastName: String = ""
#Published var inputCellNumber: String = ""
#Published var inputEmail: String = ""
#Published var inputChampion: Bool = false
#Published var inputComments: String = ""
#Published var inputCreated: Date = Date()
#Published var inputUpdated: Date = Date()
#Published var listOfFirstNames = []
#Published var listofContacts: [ContactEntity] = []
func fetchContacts() {
let request = NSFetchRequest<ContactEntity>(entityName: "ContactEntity")
do {
dataVM.listofContacts = try dataVM.container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addContact(
contactFirstName: String,
contactLastName: String,
contactCellNumber: String,
contactEmail: String,
contactChampion: Bool,
contactComments: String,
contactCreated: Date,
contactUpdated: Date) {
let newContact = ContactEntity(context: dataVM.container.viewContext)
newContact.contactFirstName = contactFirstName
newContact.contactLastName = contactLastName
newContact.contactCellNumber = contactCellNumber
newContact.contactEmail = contactEmail
newContact.contactChampion = contactChampion
newContact.contactComments = contactComments
newContact.contactUpdated = Date()
newContact.contactCreated = Date()
let uniqueClient = Set(dataVM.selectedClient)
for client in uniqueClient {
newContact.addToClients(client)
print("Client: \(client.clientName ?? "No client")")
}
saveContact()
dismiss()
}
func deleteContact(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = dataVM.listofContacts[index]
dataVM.container.viewContext.delete(entity)
saveContact()
}
func saveContact() {
do {
try dataVM.container.viewContext.save()
fetchContacts()
} catch let error {
print("Error saving. \(error)")
}
}
func sortLastName() -> [ Array<Any>] {
let listOfLastNames = dataVM.listofContacts.sorted {
$0.contactLastName ?? "" < $1.contactLastName ?? ""
}
return [listOfLastNames]
}
func sortFirstName() -> [ Array<Any>] {
let listOfFirstNames = dataVM.listofContacts.sorted {
$0.contactFirstName ?? "" < $1.contactFirstName ?? ""
}
return [listOfFirstNames]
}
}
import Foundation
import CoreData
class CoreDataManager: ObservableObject {
let container: NSPersistentContainer
#Published var listOfQualifiers: [QQEntity] = []
#Published var listofContacts: [ContactEntity] = []
#Published var listOfClients: [ClientEntity] = []
#Published var listOfOpportunities: [OpportunityEntity] = []
//#Published var selectedClient: [ClientEntity] = []
init() {
container = NSPersistentContainer(name: "B2BContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error loading Core Data. \(error)")
} else {
print("Successfully loaded Core Data...")
}
}
}
}

Should I pass viewModel or only model to view to upload image/Pdf Document in swiftui in MVVM?Do I need to keep progress for each document?

I have a situation Where I can add multiple images or videos URLs in Array 1 by 1. Similarly, a separate View (AssetView) is modified based on the array elements added. Now, the status of the Image/Video/PDF upload is changed and needs to reflect the progress upload and progress done. But here, I am unable to use MVVM.
I am confused about where should I call the upload function.
Should I pass viewModel to asset View or only view??
I am adding the Source code below to show my problem.
This is the first Screen where the user will get an option to show the card. On Card, the User can select Image/Video/Pdf any document.
struct ContentView: View {
#State var cardShown = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
cardShown.toggle()
}, label: {
Text("Show Card")
.bold()
.foregroundColor(Color.white)
.background(Color.blue)
.frame(width: 200, height: 50)
})
BottomCard(cardShown: $cardShown, height: 400, content: {
CardContent()
.padding()
})
}
}
}
}
This is the CardContentView, Where the user will add documents.
enum ActionType {
case ImageButtonAction
case VideoButtonAction
case None
}
struct CardContent: View {
#State private var text = ""
#State private var image: Image? = Image("UserProfilePlaceholder")
#State private var shouldPresentImagePicker = false
#State private var shouldPresentActionScheet = false
#State private var shouldPresentCamera = false
#State private var galleryAssetTypeSelected = GalleryAssetType.None
#State private var actionType = ActionType.None
#StateObject var messageAttachmentViewModel = MessageAttachmentViewModel()
// Document
#State private var shouldPresentDocumentPicker = false
var body: some View {
VStack {
Text("Photo Collage")
.bold()
.font(.system(size: 30))
.padding()
Text("You can create awesome photo grids and share them with all of your friends")
.font(.system(size: 18))
.multilineTextAlignment(.center)
TextEditor(text: $text)
.frame(height: 40)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(0..<self.messageAttachmentViewModel.commonMessageAttachmentModel.count, id: \.self) { i in
AssetView(messageAttachmentViewModel: messageAttachmentViewModel, index: i)
}
}
}
.background(Color.white)
.frame(height: 140)
HStack {
Button(action: {
self.shouldPresentActionScheet = true
self.actionType = .ImageButtonAction
}, label: {
Text("IMAGE")
})
Button(action: {
self.shouldPresentActionScheet = true
self.actionType = .VideoButtonAction
}, label: {
Text("VIDEO")
})
Button(action: {
self.galleryAssetTypeSelected = .PDF
self.shouldPresentDocumentPicker = true
}, label: {
Text("PDF")
})
Spacer()
Text("500")
.font(.system(size: 18))
Button(action: {
}, label: {
Text("SEND")
})
}
}
.padding()
.sheet(isPresented: $shouldPresentImagePicker) {
ImagePicker(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, image: self.$image, isPresented: self.$shouldPresentImagePicker, galleryAssetType: $galleryAssetTypeSelected, messageAttachmentViewModel: messageAttachmentViewModel)
}.actionSheet(isPresented: $shouldPresentActionScheet) { () -> ActionSheet in
ActionSheet(title: Text("Choose mode"), message: Text("Please choose your preferred mode to set your profile image"), buttons: [ActionSheet.Button.default(Text("Camera"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
self.galleryAssetTypeSelected = .None
}), ActionSheet.Button.default(Text(actionType == ActionType.ImageButtonAction ? "Photo Library" : "Video"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
self.galleryAssetTypeSelected = (actionType == ActionType.ImageButtonAction) ? GalleryAssetType.Photo : GalleryAssetType.Video
self.galleryAssetTypeSelected = actionType == ActionType.ImageButtonAction ? .Photo : .Video
}), ActionSheet.Button.cancel()])
}
// .sheet(isPresented: $shouldPresentDocumentPicker) {
// DocumentPicker(isDocumentPickerPresented: $shouldPresentDocumentPicker, galleryAssetType: $galleryAssetTypeSelected, commentAttachments: $commentAttachments)
// }
}
}
Below is Image Picker Struct to select Image/Video from Gallery.
struct ImagePicker: UIViewControllerRepresentable {
var sourceType: UIImagePickerController.SourceType = .photoLibrary
#Binding var image: Image?
#Binding var isPresented: Bool
#Binding var galleryAssetType: GalleryAssetType
#ObservedObject var messageAttachmentViewModel: MessageAttachmentViewModel
func makeCoordinator() -> ImagePickerViewCoordinator {
return ImagePickerViewCoordinator(image: $image, isPresented: $isPresented, galleryAssetType: $galleryAssetType, messageAttachmentViewModel: messageAttachmentViewModel)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let pickerController = UIImagePickerController()
pickerController.sourceType = sourceType
pickerController.delegate = context.coordinator
if galleryAssetType == .Photo {
pickerController.mediaTypes = ["public.image"]
} else if galleryAssetType == .Video {
pickerController.mediaTypes = ["public.movie"]
pickerController.videoQuality = .typeHigh
}
return pickerController
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
// Nothing to update here
}
}
class ImagePickerViewCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var image: Image?
#Binding var isPresented: Bool
#Binding var galleryAssetType: GalleryAssetType
#ObservedObject var messageAttachmentViewModel: MessageAttachmentViewModel
init(image: Binding<Image?>, isPresented: Binding<Bool>, galleryAssetType: Binding<GalleryAssetType>, messageAttachmentViewModel: MessageAttachmentViewModel) {
self._image = image
self._isPresented = isPresented
self._galleryAssetType = galleryAssetType
self.messageAttachmentViewModel = messageAttachmentViewModel
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
self.image = Image(uiImage: image)
}
if galleryAssetType == .Photo {
if let imageURL = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerImageURL") ] as? URL {
let image = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerOriginalImage")] as? UIImage
let messageAttachmentModel = MessageAttachmentModel(assetType: .Photo, assetUrl: imageURL, image: image, uploadStatus: false)
self.messageAttachmentViewModel.commonMessageAttachmentModel.append(messageAttachmentModel)
}
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
self.image = Image(uiImage: image)
}
} else if galleryAssetType == .Video {
if let videoURL = info[UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL") ] as? URL {
let messageAttachmentModel = MessageAttachmentModel(assetType: .Video, assetUrl: videoURL, uploadStatus: false)
self.messageAttachmentViewModel.commonMessageAttachmentModel.append(messageAttachmentModel)
}
}
self.isPresented = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
self.isPresented = false
}
}
This is VideoThumnail View to show only thumbnail after selection. The actual video has to be uploaded to the server.
struct VideoThumbnail: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var videoThumbnail = UIImage()
var state = LoadState.loading
init(url: URL) {
if url.pathComponents.isEmpty {
self.state = .failure
return
}
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
avAssetImageGenerator.maximumSize = CGSize(width: 150, height: 150)
let thumnailTime = CMTimeMake(value: 2, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumnailTime, actualTime: nil)
self.videoThumbnail = UIImage(cgImage: cgThumbImage)
self.state = .success
} catch {
print(error.localizedDescription)
self.state = .failure
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
.aspectRatio(contentMode: .fit)
}
init(url: URL, loading: Image = Image(systemName: "photo"), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
return Image(uiImage: loader.videoThumbnail)
}
}
}
Below is PDFThumbnail View.
struct PdfThumbnailView: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var pdfThumbnail = UIImage()
var state = LoadState.loading
init(url: URL) {
if url.pathComponents.isEmpty {
self.state = .failure
return
}
let pdfDocument = PDFDocument(url: url)
if let pdfDocumentPage = pdfDocument?.page(at: 1) {
pdfThumbnail = pdfDocumentPage.thumbnail(of: CGSize(width: 150, height: 150), for: PDFDisplayBox.trimBox)
self.state = .success
} else {
self.state = .failure
}
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
.aspectRatio(contentMode: .fit)
}
init(url: URL, loading: Image = Image(systemName: "photo"), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
return Image(uiImage: loader.pdfThumbnail)
}
}
}
MessageAttachmentModel: This Model is created when Image/Video/Pdf is selected.
struct MessageAttachmentModel {
var assetType = GalleryAssetType.None
var assetUrl: URL
var image: UIImage?
var uploadStatus: Bool
init(assetType: GalleryAssetType, assetUrl: URL, image: UIImage? = nil, uploadStatus: Bool) {
self.assetType = assetType
self.assetUrl = assetUrl
self.image = image
self.uploadStatus = uploadStatus
}
}
MessageAttachmentModelView: This ModelView contains an array of MessageAttachmentModel as a published property to reflect the change.
class MessageAttachmentViewModel: ObservableObject {
#Published var commonMessageAttachmentModel: [MessageAttachmentModel] = []
#Published var isUploadedLeft: Bool = false
func getIsUploadedStatus() {
let leftToUpload = commonMessageAttachmentModel.filter({ $0.uploadStatus == false })
isUploadedLeft = (leftToUpload.count > 0) ? true : false
}
func updateData() {
for var model in commonMessageAttachmentModel {
if model.uploadStatus == false {
if let endUsedId = getEndUserDataId(), let data = getDataFromURL(url: model.assetUrl) {
let timestamp = Date().timeIntervalSince1970
let key = "u_me_\(endUsedId)_\(timestamp))"
var assetType = (model.assetType == .Photo) ? ("Image") : ((model.assetType == .Video) ? "Video" : "Files")
uploadFileData(assetType: assetType, key: key, data: data) { status, urlString in
if status {
model.uploadStatus = true
}
}
}
}
}
}
func uploadFileData(assetType: String, key: String, data: Data , completion: #escaping (Bool, String) -> Void ) {
/// Server Data Upload
}
func getEndUserDataId() -> String? {
var endUserId: String?
return "5"
}
I have to show the progress of Image/Video/Pdf upload on Asset View. I am unable to identify how can I achieve it.
I am adding a simulator screenshot also to understand the situation clearly.
I am struggling to identify that do I need to keep progress for each document? Please help.

Update View Only After Aync Is Resolved with Completion Handler

I'm trying to update my view, only after the Async call is resolved. In the below code the arrayOfTodos.items comes in asynchronously a little after TodoListApp is rendered. The problem I'm having is that when onAppear runs, self.asyncTodoList.items is always empty since it hasn't received the values of the array yet from the network call. I'm stuck trying to figure out how to hold off on running onAppear until after the Promise is resolved, like with a completion handler?? And depending on the results of the network call, then modify the view. Thanks for any help! I've been stuck on this longer than I'll ever admit!
struct ContentView: View {
#StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
var body: some View {
TodoListApp(asyncTodoList: arrayOfTodos)
}
}
struct TodoListApp: View {
#ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
#State private var showPopUp: Bool = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("Top Area")
Text("List Area")
}
if self.showPopUp == true {
VStack {
Text("THIS IS MY POPUP!")
Text("No Items Added Yet")
}.frame(width: 300, height: 400)
}
}.onAppear {
let arrayItems = self.asyncTodoList
if arrayItems.items.isEmpty {
self.showPopUp = true
}
/*HERE! arrayItems.items.isEmpty is ALWAYS empty when onAppear
runs since it's asynchronous. What I'm trying to do is only
show the popup if the array is empty after the promise is
resolved.
What is happening is even if array resolved with multiple todos,
the popup is still showing because it was initially empty on
first run. */
}
}
}
}
class AsyncGetTodosNetworkCall: ObservableObject {
#AppStorage(DBUser.userID) var currentUserId: String?
private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
#Published var items = [TodoItem]()
func fetchTodos(toDetach: Bool) {
guard let userID = currentUserId else {
return
}
let userDoc = REF_USERS.document(String(userID))
.collection(DBCOLLECTION.todos)
.addSnapshotListener({ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents Found")
return
}
self.items = documents.map { document -> TodoItem in
let todoID = document.documentID
let todoName = document.get(ToDo.todoName) as? String ?? ""
let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false
return TodoItem(
id: todoID,
todoName: todoName,
todoCompleted: todoCompleted
)
}
})
if toDetach == true {
userDoc.remove()
}
}
}
While preparing my question, i found my own answer. Here it is in case someone down the road might run into the same issue.
struct ContentView: View {
#StateObject var arrayOfTodos = AsyncGetTodosNetworkCall()
#State var hasNoTodos: Bool = false
func getData() {
self.arrayOfTodos.fetchTodos(toDetach: false) { noTodos in
if noTodos {
self.hasNoTodos = true
}
}
}
func removeListeners() {
self.arrayOfTodos.fetchTodos(toDetach: true)
}
var body: some View {
TabView {
TodoListApp(asyncTodoList: arrayOfTodos, hasNoTodos : self.$hasNoTodos)
}.onAppear(perform: {
self.getData()
}).onDisappear(perform: {
self.removeListeners()
})
}
}
struct TodoListApp: View {
#ObservedObject var asyncTodoList: AsyncGetTodosNetworkCall
#Binding var hasNoTodos: Bool
#State private var hidePopUp: Bool = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("Top Area")
ScrollView {
LazyVStack {
ForEach(asyncTodoList.items) { item in
HStack {
Text("\(item.name)")
Spacer()
Text("Value")
}
}
}
}
}
if self.hasNoTodos == true {
if self.hidePopUp == false {
VStack {
Text("THIS IS MY POPUP!")
Text("No Items Added Yet")
}.frame(width: 300, height: 400)
}
}
}
}
}
}
class AsyncGetTodosNetworkCall: ObservableObject {
#AppStorage(DBUser.userID) var currentUserId: String?
private var REF_USERS = DB_BASE.collection(DBCOLLECTION.appUsers)
#Published var items = [TodoItem]()
func fetchTodos(toDetach: Bool, handler: #escaping (_ noTodos: Bool) -> ()) {
guard let userID = currentUserId else {
handler(true)
return
}
let userDoc = REF_USERS.document(String(userID))
.collection(DBCOLLECTION.todos)
.addSnapshotListener({ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No Documents Found")
handler(true)
return
}
self.items = documents.map { document -> TodoItem in
let todoID = document.documentID
let todoName = document.get(ToDo.todoName) as? String ?? ""
let todoCompleted = document.get(Todo.todoCompleted) as? Bool ?? false
return TodoItem(
id: todoID,
todoName: todoName,
todoCompleted: todoCompleted
)
}
handler(false)
})
if toDetach == true {
userDoc.remove()
}
}
}

App passed the Instruments leak check but crashes after running for about 20 mins due to memory issues

I tested my app with Instruments and found no leaks but memory increases every time any value is updated and My app is updated every 0.5 second in multiple places. After running for about 20 mins, My app crashed and i got "Terminated due to memory issue" message.
I tried to find out which place cause this issue and ItemView seems to be causing the problem. I created a code snippet and the test result is below.
Please explain what's wrong with my code.
Thanks for any help!
import SwiftUI
import Combine
struct ContentView: View {
#State private var laserPower: Float = 0
var body: some View {
VStack {
ItemView(
title: "Laser Power",
value: $laserPower,
unit: "W",
callback: { (newValue) in
print("setPower")
//Api.setPower(watts: newValue)
}
)
}
.onAppear {
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in
//Memory increases every time the laserPower's value is updated.
laserPower = Float(Int.random(in: 1..<160) * 10)
}
}
}
}
struct ItemView: View {
let title: String
#Binding var value: Float
let unit: String
let callback: (_ newValue: Float) -> Void
#State private var stringValue = ""
#State private var isEditingValue = false
#State var editingValue: Float = 0
func getStringValue(from value: Float) -> String {
return String(format: "%.1f", value)
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text(title)
Spacer()
}
HStack {
TextField(
"",
text: $stringValue,
onEditingChanged: { editingChanged in
DispatchQueue.main.async {
if !editingChanged {
if stringValue.contain(pattern: "^-?\\d+\\.\\d$") {
editingValue = Float(stringValue)!
} else if stringValue.contain(pattern: "^-?\\.\\d$") {
stringValue = "0.\(stringValue.split(separator: ".", omittingEmptySubsequences: false)[1])"
editingValue = Float(stringValue)!
} else if stringValue.contain(pattern: "^-?\\d+\\.?$") {
stringValue = "\(stringValue.split(separator: ".", omittingEmptySubsequences: false)[0]).0"
editingValue = Float(stringValue)!
} else {
stringValue = getStringValue(from: value)
}
callback(Float(getStringValue(from: editingValue))!)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isEditingValue = false
}
} else {
isEditingValue = true
}
}
},
onCommit: {
DispatchQueue.main.async {
callback(Float(getStringValue(from: editingValue))!)
}
}
)
.font(.title)
.multilineTextAlignment(.center)
.padding(4)
.overlay(RoundedRectangle(cornerRadius: 5).stroke())
Text(unit)
.padding(5)
}
}
.padding(10)
.onReceive(Just(stringValue)) { newValue in
if newValue.contain(pattern: "^-?\\d+\\.\\d?$") {
return
}
var filtered = newValue.filter { "0123456789.-".contains($0) }
let split = filtered.split(separator: ".", omittingEmptySubsequences: false)
if (split.count > 1 && String(split[1]).count > 1) || split.count > 2 {
let dec = split[1]
filtered = "\(split[0]).\(dec.isEmpty ? "" : String(dec[dec.startIndex]))"
}
self.stringValue = filtered
}
.onChange(of: value) { _ in
if !isEditingValue {
stringValue = getStringValue(from: self.value)
editingValue = value
}
}
.onAppear {
stringValue = getStringValue(from: value)
editingValue = value
}
}
}
extension String {
func contain(pattern: String) -> Bool {
guard let regex = try? NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options()) else {
return false
}
return regex.firstMatch(in: self, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, self.count)) != nil
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Memory report:
Instruments report:

Swift UI Binding TextField in Collection

I have two columns with nested data(Parent/child). Each item in first column is parent. When selecting anyone of them then it shows its child in second column as list.
When selecting any item from second column then it must show "clipAttr" attribute in third column as text editor where we can edit it.
Now I need help how to do that when edit the 'ClipAttr' then it automatically update in SampleDataModel collection. Below is the complete code.
struct SampleClip: Identifiable, Hashable {
var uid = UUID()
var id :String
var itemType:String?
var clipTitle: String?
var creationDate: Date?
var clipAttr:NSAttributedString?
}
struct SampleClipset: Identifiable, Hashable {
var id = UUID()
var clipsetName :String
var isEditAble:Bool
init( clipsetName:String, isEditAble:Bool){
self.clipsetName = clipsetName
self.isEditAble = isEditAble
}
}
struct SampleClipItem: Identifiable, Hashable {
var id = UUID()
var clipsetObject: SampleClipset
var clipObjects: [SampleClip]
}
class SampleDataModel: ObservableObject {
#Published var dict:[SampleClipItem] = []
#Published var selectedItem: SampleClipItem? {
didSet {
if self.selectedItem != nil {
if( self.selectedItem!.clipObjects.count > 0){
self.selectedItemClip = self.selectedItem!.clipObjects[0]
}
}
}
}
#Published var selectedItemClip: SampleClip? {
didSet {
if self.selectedItemClip != nil {
}
}
}
}
struct SampleApp: View {
#ObservedObject var vm = SampleDataModel()
#State var clipText = NSAttributedString(string: "Enter your text")
var body: some View {
VStack {
//Button
HStack{
//Clipset button
VStack{
Text("Add Parent data")
.padding(10)
Button("Add") {
let clipset1 = SampleClipset(clipsetName: "Example clipset\(self.vm.dict.count)", isEditAble: false)
var clip1 = SampleClip(id: "0", itemType: "", clipTitle: "Clip 1")
clip1.clipAttr = NSAttributedString(string: clip1.clipTitle!)
clip1.creationDate = Date()
var clip2 = SampleClip(id: "1", itemType: "", clipTitle: "Clip 2")
clip2.clipAttr = NSAttributedString(string: clip2.clipTitle!)
clip2.creationDate = Date()
let item = SampleClipItem(clipsetObject: clipset1, clipObjects: [clip1, clip2] )
self.vm.dict.append(item)
}
Button("Update") {
let index = self.vm.dict.count - 1
self.vm.dict[index].clipsetObject.clipsetName = "Modifying"
}
}
Divider()
//Clip button
VStack{
Text("Add Child data")
.padding(10)
Button("Add") {
let object = self.vm.dict.firstIndex(of: self.vm.selectedItem!)
if( object != nil){
let index = self.vm.selectedItem?.clipObjects.count
var clip1 = SampleClip(id: "\(index)", itemType: "", clipTitle: "Clip \(index)")
clip1.clipAttr = NSAttributedString(string: clip1.clipTitle!)
clip1.creationDate = Date()
self.vm.dict[object!].clipObjects.append(clip1)
self.vm.selectedItem = self.vm.dict[object!]
}
}
Button("Update") {
let index = (self.vm.selectedItem?.clipObjects.count)! - 1
self.vm.selectedItem?.clipObjects[index].clipAttr = NSAttributedString(string:"Modifying")
}
}
}.frame(height: 100)
//End button frame
//Start Column frame
Divider()
NavigationView{
HStack{
//Clipset list
List(selection: self.$vm.selectedItem){
ForEach(Array(self.vm.dict), id: \.self) { key in
Text("\(key.clipsetObject.clipsetName)...")
}
}
.frame(width:200)
.listStyle(SidebarListStyle())
Divider()
VStack{
//Clip list
if(self.vm.selectedItem?.clipObjects.count ?? 0 > 0){
List(selection: self.$vm.selectedItemClip){
ForEach(self.vm.selectedItem!.clipObjects, id: \.self) { key in
Text("\(key.clipTitle!)...")
}
}
.frame(minWidth:200)
}
}
//TextEditor
Divider()
SampleTextEditor(text: self.$clipText)
.frame(minWidth: 300, minHeight: 300)
}
}
}
}
}
struct SampleApp_Previews: PreviewProvider {
static var previews: some View {
SampleApp()
}
}
//New TextView
struct SampleTextEditor: View, NSViewRepresentable {
typealias Coordinator = SampleEditorCoordinator
typealias NSViewType = NSScrollView
let text : Binding<NSAttributedString>
func makeNSView(context: NSViewRepresentableContext<SampleTextEditor>) -> SampleTextEditor.NSViewType {
return context.coordinator.scrollView
}
func updateNSView(_ nsView: NSScrollView, context: NSViewRepresentableContext<SampleTextEditor>) {
if ( context.coordinator.textView.textStorage != text.wrappedValue){
context.coordinator.textView.textStorage?.setAttributedString(text.wrappedValue)
}
}
func makeCoordinator() -> SampleEditorCoordinator {
let coordinator = SampleEditorCoordinator(binding: text)
return coordinator
}
}
class SampleEditorCoordinator : NSObject, NSTextViewDelegate {
let textView: NSTextView;
let scrollView : NSScrollView
let text : Binding<NSAttributedString>
init(binding: Binding<NSAttributedString>) {
text = binding
textView = NSTextView(frame: .zero)
textView.autoresizingMask = [.height, .width]
textView.textStorage?.setAttributedString(text.wrappedValue)
textView.textColor = NSColor.textColor
//Editor min code
textView.isContinuousSpellCheckingEnabled = true
textView.usesFontPanel = true
textView.usesRuler = true
textView.isRichText = true
textView.importsGraphics = true
textView.usesInspectorBar = true
textView.drawsBackground = true
textView.allowsUndo = true
textView.isRulerVisible = true
textView.isEditable = true
textView.isSelectable = true
textView.backgroundColor = NSColor.white
//
scrollView = NSScrollView(frame: .zero)
scrollView.hasVerticalScroller = true
scrollView.autohidesScrollers = false
scrollView.autoresizingMask = [.height, .width]
scrollView.documentView = textView
super.init()
textView.delegate = self
}
func textDidChange(_ notification: Notification) {
switch notification.name {
case NSText.didChangeNotification :
text.wrappedValue = (notification.object as? NSTextView)?.textStorage ?? NSAttributedString(string: "")
default:
print("Coordinator received unwanted notification")
//os_log(.error, log: uiLog, "Coordinator received unwanted notification")
}
}
}
First use custom Binding.
SampleTextEditor(text: Binding(get: {
return self.vm.selectedItemClip?.clipAttr
}, set: {
self.vm.selectedItemClip?.clipAttr = $0
}))
Second, update your view on child update button.
Button("Update") {
guard let mainIndex = self.vm.dict.firstIndex(where: { (data) -> Bool in
if let selectedId = self.vm.selectedItem?.id {
return data.id == selectedId
}
return false
}),
let subIndex = self.vm.dict[mainIndex].clipObjects.firstIndex(where: { (data) -> Bool in
if let selectedId = self.vm.selectedItemClip?.id {
return data.id == selectedId
}
return false
}),
let obj = self.vm.selectedItemClip
else {
return
}
self.vm.dict[mainIndex].clipObjects[subIndex] = obj
self.vm.selectedItem = self.vm.dict[mainIndex]
}
Inside the SampleEditorCoordinator class and SampleTextEditor struct use optional binding. And change your textDidChange methods.
struct SampleTextEditor: View, NSViewRepresentable {
typealias Coordinator = SampleEditorCoordinator
typealias NSViewType = NSScrollView
let text : Binding<NSAttributedString?>
func makeNSView(context: NSViewRepresentableContext<SampleTextEditor>) -> SampleTextEditor.NSViewType {
return context.coordinator.scrollView
}
func updateNSView(_ nsView: NSScrollView, context: NSViewRepresentableContext<SampleTextEditor>) {
if ( context.coordinator.textView.textStorage != text.wrappedValue){
if let value = text.wrappedValue {
context.coordinator.textView.textStorage?.setAttributedString(value)
}
}
}
// Other code
}
class SampleEditorCoordinator : NSObject, NSTextViewDelegate {
let textView: NSTextView;
let scrollView : NSScrollView
var text : Binding<NSAttributedString?>
init(binding: Binding<NSAttributedString?>) {
text = binding
// Other code
}
func textDidChange(_ notification: Notification) {
switch notification.name {
case NSText.didChangeNotification :
self.text.wrappedValue = NSAttributedString(attributedString: textView.attributedString())
default:
print("Coordinator received unwanted notification")
//os_log(.error, log: uiLog, "Coordinator received unwanted notification")
}
}
}