Created
November 23, 2019 21:33
-
-
Save stzn/4e49e0afe3d4208a824e8eed53f24c61 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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