iosswiftmobileswiftuiplaygrounds

Connect TextEditor text count to ProgressView in SwiftUI


I'm creating a journaling app that would encourage the user to write at least a 100 characters in the Morning Pages section. I’d like to do that by placing a ProgressView at the bottom of a TextEditor. The progress bar would then go from 0 to 100% based on the user's input and stay at 100 if they write more than say 100 characters. The progress bar indicates the minimum chars needed to create the entry.

import SwiftUI
import Combine

struct AddNewEntryView: View {

@Environment(\.dismiss) var dismiss
@EnvironmentObject var data : JournalEntriesData
@State var typeOfNewEntry : EntryType
@State var newEntry : Entry
@State var progressValue: Double = 0.0
let MIN_ENTRY_CHARS = 10

let detector = PassthroughSubject<Void, Never>()
let publisher: AnyPublisher<Void, Never>

init(entryType: EntryType) {
    _typeOfNewEntry = State(initialValue: entryType)
    _newEntry = State(initialValue: Entry(type: entryType, text: ""))
    
    publisher = detector
        .debounce(for: .seconds(1), scheduler: DispatchQueue.main)
        .eraseToAnyPublisher()
}

func limitTo(num: Double, maxNum: Double) -> Double {
    return num > maxNum ? maxNum : num
}

private func updateProgressBar() {
    print("char count", newEntry.text.count)
    progressValue = Double(newEntry.text.count / MIN_ENTRY_CHARS)
}

var body: some View {
    VStack {
        Form {
            Text(newEntry.created, style: .date)
                .font(.headline)
            Section("Entry") {
                TextEditor(text: $newEntry.text)
                    .foregroundColor(.secondary)
                    .padding(.horizontal)
                    .frame(minHeight: 80)
                    .onChange(of: newEntry.text) { _ in detector.send() }
                    .onReceive(publisher) {  updateProgressBar() 
                    }
                }
            ProgressView("Character count", value: $progressValue, total: 1.0)
        }
    }
    .toolbar {
        ToolbarItem {
            Button("Add") {
                data.entries.append(newEntry)
                dismiss()
            }
        }
      }
  }
}

struct AddNewEntryView_Previews: PreviewProvider {
    static var previews: some View {
        AddNewEntryView(entryType: .MORNING_PAGES)
    }
}

I almost got it now, but my ProgressView tells me that it cannot convert value of type Double to expected argument type Binding but I’m already using the $dollar sign there. It also says in the second error that initializer(in it value total) requires that binding Conforms to ‘BinaryFloatingPoint’. I’m new to Swift and getting a little lost in the typings here.


Solution

  • You don't need binding, so fix is probably simple

    ProgressView("Character count", value: progressValue, total: 1.0)
    

    but you have complicated solution, I recommend to consider much simpler one using only onChange ('cause this calculation is fast and progress can be applied directly, w/o publisher). As well to reset progressValue directly after new entry added.

    Here is simplified demo. Tested with Xcode 13.4 / iOS 15.5

    struct DemoView: View {
        @State private var text = ""
        @State private var progressValue: Double = 0
        var body: some View {
            VStack {
                Form {
                    Section("Entry") {
                        TextEditor(text: $text)
                            .foregroundColor(.secondary)
                            .padding(.horizontal)
                            .frame(minHeight: 80)
                            .onChange(of: text) { value in
                                if value.count <= 100 {
                                    progressValue = Double(value.count) / 100.0
                                }
                            }
                    }
                    ProgressView("Character count", value: progressValue)
                }
            }
        }
    }