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

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:

Related

Swiftui #binding ForEach loop with timer function not working

Hy folks, I work on a litte project for a time tracker and use Core Data for storing the values. Every timer Note has a seconds value stored that runs from inside of each timer view. I want to populate now these values to the parent view, but it's not working even when binding the note to the view. I know i have to populate the changes somehow... Can somebody help?
ContentView:
import SwiftUI
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
let coreDM: CoreDataManager
#State private var noteTitle: String = ""
#State private var notes: [Note] = [Note]() // That's the Core Data Model
private func populateNotes() {
notes = coreDM.getAllNotes()
}
var body: some View {
VStack {
if notes.count > 0 {
ForEach(0..<$notes.count,id: \.self) { i in
Text("\(notes[i].seconds)")
}
}
TextField("Enter title", text: $noteTitle)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Save") {
coreDM.saveNote(title: noteTitle, seconds: 0)
populateNotes()
}
List {
if notes.count > 0 {
ForEach(0..<$notes.count,id: \.self) { i in
NoteListView(note: $notes[i], coreDM: coreDM)
Button("Delete"){
coreDM.deleteNote(note: notes[i])
populateNotes()
}
}
}
}.listStyle(PlainListStyle())
Spacer()
}.padding()
.onAppear(perform: {
populateNotes()
})
}
}
NoteListView:
import SwiftUI
import Combine
struct NoteListView: View {
#Binding var note: Note
let coreDM: CoreDataManager
#State private var noteSeconds: Double = 0.0
#State private var noteIsRunning: Bool = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack{
Text(note.title ?? "")
Text("\(noteSeconds)")
.onReceive(timer) { time in
if noteIsRunning {
noteSeconds += 1
note.seconds = noteSeconds
}
}
if noteIsRunning {
Image(systemName: "pause.circle")
.resizable()
.frame(width:20, height: 20)
.onTapGesture {
withAnimation{
noteIsRunning.toggle()
note.seconds = noteSeconds
coreDM.updateNote()
}
}
}else{
Image(systemName: "record.circle")
.resizable()
.foregroundColor(.orange)
.frame(width:20, height: 20)
.onTapGesture {
withAnimation{
noteIsRunning.toggle()
}
}
}
}
.onAppear(){
noteSeconds = note.seconds
}
}
}
CoreDataManager:
import Foundation
import CoreData
class CoreDataManager {
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name: "TimeTrackerDataModel2")
persistentContainer.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
}
}
func updateNote() {
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
}
}
func deleteNote(note: Note) {
persistentContainer.viewContext.delete(note)
do {
try persistentContainer.viewContext.save()
} catch {
persistentContainer.viewContext.rollback()
print("Failed to save context \(error)")
}
}
func getAllNotes() -> [Note] {
let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
do {
return try persistentContainer.viewContext.fetch(fetchRequest)
} catch {
return []
}
}
func saveNote(title: String, seconds: Double) {
let note = Note(context: persistentContainer.viewContext)
note.title = title
note.seconds = seconds
do {
try persistentContainer.viewContext.save()
} catch {
print("Failed to save note: \(error)")
}
}
}

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()
}
}
}

changing a TextField while editing another TextField

I recently started learning SwiftUI and I want to make a unit converter. In the process I ran into a problem: it is necessary to make sure that when a numerical value is entered into one of the TextField, the rest of the TextField formulas are triggered and the total values are displayed.
import SwiftUI
import Combine
struct ContentView: View {
#State var celsius: String = ""
#State var kelvin: String = ""
#State var farenheit: String = ""
#State var reyumur: String = ""
#State var rankin: String = ""
var body: some View {
NavigationView {
Temperature(celsius: $celsius, kelvin: $kelvin, farenheit: $farenheit, reyumur: $reyumur, rankin: $rankin)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Temperature: View {
#Binding var celsius: String
#Binding var kelvin: String
#Binding var farenheit: String
#Binding var reyumur: String
#Binding var rankin: String
var body: some View {
List {
Section(
header: Text("Международная система (СИ)")) {
HStack {
TextField("Enter value", text: $celsius)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(celsius)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.celsius = filtered
}
}
Text("°C")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
HStack {
TextField("Enter value", text: $kelvin)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(kelvin)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.kelvin = filtered
}
}
Text("K")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
}
Section(
header: Text("США и Британия")) {
HStack {
TextField("Enter value" , text: $farenheit)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(farenheit)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.farenheit = filtered
}
}
Text("F")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
}
Section(
header: Text("Редкоиспользуемые")) {
HStack {
TextField("Enter value" , text: $reyumur)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(reyumur)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.reyumur = filtered
}
}
Text("Re")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
HStack {
TextField("Enter value" , text: $rankin)
.keyboardType(.numbersAndPunctuation)
.onReceive(Just(rankin)) { newValue in
let filtered = newValue.filter { "0123456789.".contains($0) }
if filtered != newValue {
self.rankin = filtered
}
}
Text("R")
.padding(.horizontal)
.font(.headline)
.foregroundColor(.blue)
}
}
}
.navigationBarTitle("Temperature")
.navigationBarTitleDisplayMode(.inline)
}
}
With SwiftUI 3, #FocusState can be used to detect if TextField is focused, which enables doing different works according to its value. You could set one variable to be the main, and other values should be calculated from it. For each value, calculate in the forward and reverse direction with two functions:
struct CelsiusAndKelvin: View {
#State private var celsius: String = "" // Assume all other values are based on celsius
#FocusState private var isKelvinFocused: Bool // When focused,
#State private var kelvinWhenFocused: String? // you may not want it to be calculated from other values
private var kelvinMask: Binding<String> {
Binding { // If `isKelvinFocused` turns true, this getter will be called first
if isKelvinFocused && kelvinWhenFocused != nil {
return kelvinWhenFocused!
}
if let x = Double(celsius) {
return String(format: "%.2f", c2k(x))
}
return ""
} set: {
if isKelvinFocused { kelvinWhenFocused = $0 }
if let x = Double($0) {
celsius = String(format: "%.2f", k2c(x))
} else {
celsius = ""
}
}
}
var body: some View {
Form {
TextField("°C", text: $celsius)
TextField("K", text: kelvinMask)
.focused($isKelvinFocused)
}
.onChange(of: isKelvinFocused) {
// Make `isKelvinFocused` always equal to `kelvinWhenFocused != nil`
if !$0 { kelvinWhenFocused = nil }
}
}
}
extension CelsiusAndKelvin {
private func c2k(_ x: Double) -> Double {
return x + 273.15
}
private func k2c(_ x: Double) -> Double {
return x - 273.15
}
}

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

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
}
}

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")
}
}
}