Commit b5a5dc8963
Verified · cmc
Layout: unified · split
LibreEdit/ContentView.swift +54 −82
| @@ -6,104 +6,81 @@ | ||
| 6 | 6 | // |
| 7 | 7 | |
| 8 | 8 | import SwiftUI |
| 9 | import UniformTypeIdentifiers | |
| 9 | 10 | |
| 10 | func getDatetime() -> String { | |
| 11 | let dateFormatter = DateFormatter() | |
| 12 | let enUSPosixLocale = Locale(identifier: "en_US_POSIX") | |
| 13 | dateFormatter.locale = enUSPosixLocale | |
| 14 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" | |
| 15 | dateFormatter.calendar = Calendar(identifier: .gregorian) | |
| 11 | struct TextFile: FileDocument { | |
| 12 | // tell the system we support only plain text | |
| 13 | static var readableContentTypes = [UTType.plainText] | |
| 16 | 14 | |
| 17 | let iso8601String = dateFormatter.string(from: Date()) | |
| 15 | // by default our document is empty | |
| 16 | var text = "" | |
| 18 | 17 | |
| 19 | return iso8601String | |
| 18 | // a simple initializer that creates new, empty documents | |
| 19 | init(initialText: String = "") { | |
| 20 | text = initialText | |
| 21 | } | |
| 22 | ||
| 23 | // this initializer loads data that has been saved previously | |
| 24 | init(configuration: ReadConfiguration) throws { | |
| 25 | if let data = configuration.file.regularFileContents { | |
| 26 | text = String(decoding: data, as: UTF8.self) | |
| 27 | } else { | |
| 28 | throw CocoaError(.fileReadCorruptFile) | |
| 29 | } | |
| 30 | } | |
| 31 | ||
| 32 | // this will be called when the system wants to write our data to disk | |
| 33 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { | |
| 34 | let data = Data(text.utf8) | |
| 35 | return FileWrapper(regularFileWithContents: data) | |
| 36 | } | |
| 20 | 37 | } |
| 21 | 38 | |
| 22 | // Ensure that `Supports Document Browser` = YES in the `ios-edit.xcodeproj` file, | |
| 23 | // so that users can browse saved files in the Files app | |
| 24 | func getDocumentsDirectory() -> URL { | |
| 25 | let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) | |
| 26 | let documentsDirectory = paths[0] | |
| 27 | return documentsDirectory | |
| 39 | func countWords(text: String) -> Int { | |
| 40 | let words = text.split { $0 == " " || $0.isNewline } | |
| 41 | return words.count | |
| 28 | 42 | } |
| 29 | 43 | |
| 30 | 44 | struct ContentView: View { |
| 31 | enum FocusedField { | |
| 32 | case content | |
| 33 | } | |
| 34 | ||
| 35 | @State private var content = "" | |
| 36 | @State private var wordCount: Int = 0 | |
| 45 | @Binding var document: TextFile | |
| 37 | 46 | @State private var showingPopover = false |
| 38 | @FocusState private var focusedField: FocusedField? | |
| 39 | ||
| 47 | @State private var wordCount: Int = 0 | |
| 48 | ||
| 40 | 49 | var body: some View { |
| 41 | NavigationView { | |
| 42 | VStack(alignment: .leading) { | |
| 43 | Text("\(wordCount) words") | |
| 44 | .font(.headline) | |
| 45 | .foregroundColor(.secondary) | |
| 46 | .padding(.trailing) | |
| 47 | TextEditor(text: $content) | |
| 48 | .onChange(of: content) { | |
| 49 | let words = content.split { $0 == " " || $0.isNewline } | |
| 50 | self.wordCount = words.count | |
| 51 | } | |
| 52 | .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | |
| 53 | .focused($focusedField, equals: .content) | |
| 54 | } | |
| 50 | Text("\(wordCount) words") | |
| 51 | .foregroundColor(Color.white) | |
| 52 | .font(.headline) | |
| 53 | .padding(.trailing) | |
| 54 | .frame(maxWidth: .infinity, maxHeight: 50) | |
| 55 | .background(Color.gray.opacity(0.3)) | |
| 56 | .padding(.top, 20) | |
| 57 | TextEditor(text: $document.text) | |
| 55 | 58 | .onAppear { |
| 56 | focusedField = .content | |
| 59 | self.wordCount = countWords(text: document.text) | |
| 60 | } | |
| 61 | .onChange(of: document.text) { | |
| 62 | self.wordCount = countWords(text: document.text) | |
| 57 | 63 | } |
| 58 | .padding() | |
| 59 | .navigationTitle("Libre Edit") | |
| 60 | 64 | .toolbar { |
| 61 | ToolbarItemGroup(placement: .primaryAction) { | |
| 62 | Button(action: { | |
| 63 | do { | |
| 64 | let data = Data(self.content.utf8) | |
| 65 | let url = URL.documentsDirectory.appending(path: (getDatetime() + ".txt")) | |
| 66 | try data.write(to: url, options: [.atomic, .completeFileProtection]) | |
| 67 | ||
| 68 | // TODO: Alerts currently aren't working/aren't visible | |
| 69 | Text("File saved successfully!") | |
| 70 | .foregroundColor(.white) | |
| 71 | .bold() | |
| 72 | .font(.footnote) | |
| 73 | .frame(width: 140, height: 30) | |
| 74 | .background(Color.indigo.cornerRadius(7)) | |
| 75 | print("Saved content to file: \(url)") | |
| 76 | } catch { | |
| 77 | // TODO: Alerts currently aren't working/aren't visible | |
| 78 | Text("Failed to save file.") | |
| 79 | .foregroundColor(.white) | |
| 80 | .bold() | |
| 81 | .font(.footnote) | |
| 82 | .frame(width: 140, height: 30) | |
| 83 | .background(Color.red.cornerRadius(7)) | |
| 84 | print("Error saving content to file: \(error)") | |
| 85 | } | |
| 86 | }) { | |
| 87 | Text("Save") | |
| 88 | } | |
| 89 | // TODO: Confirm (possibly save) & then clear TextEditor | |
| 90 | Button(action: { | |
| 91 | print("TODO: This will create a new note.") | |
| 92 | }) { | |
| 93 | Text("New") | |
| 94 | } | |
| 95 | } | |
| 96 | 65 | ToolbarItemGroup(placement: .secondaryAction) { |
| 97 | 66 | Button { |
| 98 | 67 | showingPopover = true |
| 99 | 68 | } label: { |
| 100 | Text("App Info") | |
| 69 | Text("More Info") | |
| 101 | 70 | }.popover(isPresented: $showingPopover) { |
| 102 | 71 | VStack(alignment: .leading) { |
| 103 | Text("App Info") | |
| 72 | Text("More Info") | |
| 104 | 73 | .font(.largeTitle) |
| 105 | 74 | Text("") |
| 106 | Text("Libre Edit is a free and open source text editor for iOS built by [Christian Cleberg](https://cleberg.net).") | |
| 75 | Text("Instructions") | |
| 76 | .font(.title) | |
| 77 | Text("") | |
| 78 | Text("LibreEdit provides a direct interface with the Apple Files app on your iPhone. Simply navigate to your preferred directory, edit existing files, or create new files!") | |
| 79 | Text("") | |
| 80 | Text("Developer") | |
| 81 | .font(.title) | |
| 82 | Text("") | |
| 83 | Text("LibreEdit is a free and open source text editor for iOS built by [Christian Cleberg](https://cleberg.net).") | |
| 107 | 84 | Text("") |
| 108 | 85 | Text("Visit the [GitHub Repository](https://github.com/ccleberg/ios-edit) to view the source code.") |
| 109 | 86 | Text("") |
| @@ -114,10 +91,5 @@ struct ContentView: View { | ||
| 114 | 91 | } |
| 115 | 92 | } |
| 116 | 93 | } |
| 117 | } | |
| 118 | 94 | } |
| 119 | 95 | } |
| 120 | ||
| 121 | #Preview { | |
| 122 | ContentView() | |
| 123 | } | |
LibreEdit/LibreEditApp.swift +2 −2
| @@ -10,8 +10,8 @@ import SwiftUI | ||
| 10 | 10 | @main |
| 11 | 11 | struct LibreEditApp: App { |
| 12 | 12 | var body: some Scene { |
| 13 | WindowGroup { | |
| 14 | ContentView() | |
| 13 | DocumentGroup(newDocument: TextFile()) { file in | |
| 14 | ContentView(document: file.$document) | |
| 15 | 15 | } |
| 16 | 16 | } |
| 17 | 17 | } |