Skip to content

Instantly share code, notes, and snippets.

@stzn
Created November 23, 2019 21:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stzn/4e49e0afe3d4208a824e8eed53f24c61 to your computer and use it in GitHub Desktop.
Save stzn/4e49e0afe3d4208a824e8eed53f24c61 to your computer and use it in GitHub Desktop.
import UIKit
import MobileCoreServices
class PastePlan: NSObject, NSItemProviderReading, NSItemProviderWriting, Codable {
let plan: Plan
init(_ plan: Plan) {
self.plan = plan
}
static var readableTypeIdentifiersForItemProvider: [String] = [(kUTTypeJSON) as String]
static var writableTypeIdentifiersForItemProvider: [String] = [(kUTTypeJSON) as String]
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
try JSONDecoder().decode(self, from: data)
}
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
do {
let data = try JSONEncoder().encode(self)
progress.completedUnitCount = 100
completionHandler(data, nil)
} catch {
completionHandler(nil, error)
}
return progress
}
}
enum Plan: String, CaseIterable, Equatable, Codable {
case business
case travel
case shopping
init?(_ text: String) {
if text.starts(with: "bu") {
self = .business
} else if text.starts(with: "tr") {
self = .travel
} else if text.starts(with: "sh") {
self = .shopping
} else {
return nil
}
}
var iconName: String {
switch self {
case .business:
return "bag"
case .travel:
return "airplane"
case .shopping:
return "cart"
}
}
var items: [String] {
switch self {
case .business:
return ["会議", "商談", "プレゼン", "勉強会"]
case .travel:
return ["宿泊", "日帰り"]
case .shopping:
return ["スーパー", "デパート"]
}
}
}
class ViewController: UIViewController {
@IBOutlet weak private var tableView: UITableView!
private var searchController = UISearchController()
private var filteredItems: [[String]] = []
private var searchBar: UISearchBar {
searchController.searchBar
}
private var isSearchBarEmpty: Bool {
if !searchBar.searchTextField.tokens.isEmpty {
return false
}
return searchBar.text?.isEmpty ?? true
}
override func viewDidLoad() {
super.viewDidLoad()
setupSearchBar()
setupTableView()
}
private func setupSearchBar() {
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchBar.placeholder = "検索キーワード"
definesPresentationContext = true
// UISearchTextField
searchBar.searchTextField.backgroundColor = .systemOrange
searchBar.searchTextField.textColor = .systemPurple
searchBar.searchTextField.font = UIFont(name: "American Typewriter", size: 18)
searchBar.searchTextField.tokenBackgroundColor = .systemBlue
// Delete
searchBar.searchTextField.allowsDeletingTokens = true
// Copy & Paste
searchBar.searchTextField.allowsCopyingTokens = true
searchBar.searchTextField.delegate = self
searchBar.searchTextField.pasteDelegate = self
}
private func setupTableView() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.tableHeaderView = searchController.searchBar
}
}
extension ViewController: UISearchTextFieldDelegate {
func searchTextField(_ searchTextField: UISearchTextField, itemProviderForCopying token: UISearchToken) -> NSItemProvider {
guard let plan = token.representedObject as? Plan else {
return NSItemProvider()
}
return NSItemProvider(object: PastePlan(plan))
}
}
extension ViewController: UITextPasteDelegate {
func textPasteConfigurationSupporting(_ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting, transform item: UITextPasteItem) {
guard let item = item as? UISearchTextFieldPasteItem else {
return
}
item.itemProvider.loadObject(ofClass: PastePlan.self) {
(pastePlan, error) in
guard let plan = (pastePlan as? PastePlan)?.plan else {
return
}
let token = UISearchToken(icon: UIImage(systemName: plan.iconName), text: plan.rawValue)
item.setSearchTokenResult(token)
}
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isSearchBarEmpty {
return Plan.allCases[section].items.count
}
if !filteredItems.isEmpty {
return filteredItems[section].count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var item: String? = nil
if isSearchBarEmpty {
item = Plan.allCases[indexPath.section].items[indexPath.row]
} else if !filteredItems.isEmpty {
item = filteredItems[indexPath.section][indexPath.row]
}
cell.textLabel?.text = item
return cell
}
}
extension ViewController: UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
if isSearchBarEmpty {
return Plan.allCases.count
}
if !filteredItems.isEmpty {
return filteredItems.count
}
return 0
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
Plan.allCases[section].rawValue
}
}
extension ViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
var text = searchBar.text!.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
guard !isSearchBarEmpty else {
return
}
var searchPlans: [Plan] = extractSearchPlans()
if let plan = Plan(text) {
setToken(from: plan)
searchPlans.append(plan)
text = ""
} else if let plan = extractPlanFromPasteboard() {
searchPlans.append(plan)
text = ""
}
updateUI(plans: searchPlans, text: text)
}
private func extractPlanFromPasteboard() -> Plan? {
guard let data = UIPasteboard.general.data(forPasteboardType: (kUTTypeJSON as String)) else {
return nil
}
UIPasteboard.general.items = []
return try? JSONDecoder().decode(PastePlan.self, from: data).plan
}
private func extractSearchPlans() -> [Plan] {
searchBar.searchTextField.tokens.compactMap { $0.representedObject as? Plan }
}
private func setToken(from plan: Plan) {
let planToken = UISearchToken(icon: UIImage(systemName: plan.iconName), text: plan.rawValue)
planToken.representedObject = plan
let field = searchBar.searchTextField
field.replaceTextualPortion(of: field.textualRange, with: planToken, at: field.tokens.count)
}
private func updateUI(plans: [Plan], text: String) {
filteredItems = []
if !plans.isEmpty {
filteredItems = Plan.allCases.filter { plans.contains($0) }
.map {
$0.items.filter { $0.starts(with: text) }
}
} else {
filteredItems = Plan.allCases.map {
$0.items.filter { $0.starts(with: text) } }
}
tableView.reloadData()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment