// // OppositeCollapseKeywordsView.swift // Tusker // // Created by Shadowfacts on 11/1/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import SwiftUI struct OppositeCollapseKeywordsView: View { @ObservedObject private var preferences = Preferences.shared // Can't use the raw [String] for keywords, because we need a fixed ID (within the lifetime of this view) for ForEach @State private var keywords: [Keyword] = Preferences.shared.oppositeCollapseKeywords.map(Keyword.init) { didSet { preferences.oppositeCollapseKeywords = keywords.map(\.value) } } @State private var valueToAdd = "" @State private var makeAddFieldFirstResponder = false var body: some View { ZStack { // the background from the grouped ListStyle clips to the safe area, so when the keyboard is hiding/showing // the color behind it can be seen, which looks odd Color(UIColor.secondarySystemBackground) .edgesIgnoringSafeArea(.bottom) List { Section(footer: Text("A post matches if its content warning contains the text of a keyword, ignoring case.")) { ForEach(keywords) { (keyword) in Row(keyword: keyword) { keywords.removeAll(where: { $0.id == keyword.id }) } } .onDelete(perform: self.removeKeywords) FocusableTextField(placeholder: "Add Keyword", text: $valueToAdd, becomeFirstResponder: $makeAddFieldFirstResponder, onCommit: self.addKeyword) } .listRowBackground(Color.appGroupedCellBackground) } .animation(.default, value: keywords.map(\.id)) .listStyle(.grouped) .appGroupedScrollBackgroundIfAvailable() } .onAppear(perform: updateAppearance) .navigationBarTitle(preferences.expandAllContentWarnings ? "Collapse Post CW Keywords" : "Expand Post CW Keywords") } private func updateAppearance() { if #available(iOS 16.0, *) { // no longer necessary } else { UIScrollView.appearance(whenContainedInInstancesOf: [PreferencesNavigationController.self]).keyboardDismissMode = .interactive } } private func commitExisting(at index: Int) -> () -> Void { return { if keywords[index].value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { keywords.remove(at: index) } makeAddFieldFirstResponder = true } } private func removeKeywords(_ indices: IndexSet) { keywords.remove(atOffsets: indices) } private func addKeyword() { guard !valueToAdd.isEmpty else { return } keywords.append(Keyword(valueToAdd)) valueToAdd = "" } } fileprivate extension OppositeCollapseKeywordsView { // Class for wrapping keywords that provides a fixed id SwiftUI's ForEach can use class Keyword: ObservableObject, Identifiable { let id = UUID() @Published var value: String init(_ value: String) { self.value = value } } } fileprivate extension OppositeCollapseKeywordsView { // Use a separate View for the row so it can use @ObservableObject to get a binding for the keyword's value struct Row: View { @ObservedObject var keyword: Keyword let removeKeyword: () -> Void var body: some View { FocusableTextField(placeholder: "Keyword", text: $keyword.value) { if keyword.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { removeKeyword() } } } } } struct OppositeCollapseKeywordsView_Previews: PreviewProvider { static var previews: some View { OppositeCollapseKeywordsView() } }