From f2ab1778c5f9975748d8d972bb8a54f3723c686d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 15 Sep 2022 21:49:50 -0400 Subject: [PATCH] Replace expanded emoji picker with SwiftUI --- Tusker.xcodeproj/project.pbxproj | 12 -- .../Compose/ComposeAutocompleteView.swift | 58 +++++--- .../Compose/EmojiCollectionViewCell.swift | 64 --------- .../EmojiPickerCollectionViewController.swift | 131 ------------------ .../Screens/Compose/EmojiPickerWrapper.swift | 46 ------ 5 files changed, 39 insertions(+), 272 deletions(-) delete mode 100644 Tusker/Screens/Compose/EmojiCollectionViewCell.swift delete mode 100644 Tusker/Screens/Compose/EmojiPickerCollectionViewController.swift delete mode 100644 Tusker/Screens/Compose/EmojiPickerWrapper.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index ebb7b1c808..d0436bd244 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -142,7 +142,6 @@ D667E5F52135BCD50057A976 /* ConversationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */; }; D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; }; D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; }; - D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D670F8B52537DC890046588A /* EmojiPickerWrapper.swift */; }; D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; }; D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; }; D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; }; @@ -246,8 +245,6 @@ D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */; }; D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */; }; D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */; }; - D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */; }; - D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */; }; D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */; }; D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */; }; D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */; }; @@ -492,7 +489,6 @@ D667E5F42135BCD50057A976 /* ConversationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTableViewController.swift; sourceTree = ""; }; D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = ""; }; D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = ""; }; - D670F8B52537DC890046588A /* EmojiPickerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerWrapper.swift; sourceTree = ""; }; D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = ""; }; D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = ""; }; D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = ""; }; @@ -593,8 +589,6 @@ D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; D6BED173212667E900F02DA0 /* TimelineStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusTableViewCell.swift; sourceTree = ""; }; D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeEmojiTextField.swift; sourceTree = ""; }; - D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionViewController.swift; sourceTree = ""; }; - D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCollectionViewCell.swift; sourceTree = ""; }; D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = ""; }; D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerNavigationDelegate.swift; sourceTree = ""; }; D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; @@ -977,9 +971,6 @@ D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */, D6E4267625327FB400C02E1C /* ComposeAutocompleteView.swift */, D6C143D9253510F4007DC240 /* ComposeEmojiTextField.swift */, - D6C143DF25354E34007DC240 /* EmojiPickerCollectionViewController.swift */, - D6C143FB25354FD0007DC240 /* EmojiCollectionViewCell.swift */, - D670F8B52537DC890046588A /* EmojiPickerWrapper.swift */, ); path = Compose; sourceTree = ""; @@ -1804,7 +1795,6 @@ D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */, - D6C143FD25354FD0007DC240 /* EmojiCollectionViewCell.swift in Sources */, D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */, D63A8D0B2561C27F00D9DFFF /* ProfileStatusesViewController.swift in Sources */, D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */, @@ -1826,7 +1816,6 @@ D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D681E4D5246E2BC30053414F /* UnmuteConversationActivity.swift in Sources */, D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameLabel.swift in Sources */, - D6C143E025354E34007DC240 /* EmojiPickerCollectionViewController.swift in Sources */, D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */, D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */, D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */, @@ -1933,7 +1922,6 @@ D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */, D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */, D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */, - D670F8B62537DC890046588A /* EmojiPickerWrapper.swift in Sources */, D6B81F442560390300F6E31D /* MenuController.swift in Sources */, D65234E12561AA68001AF9CF /* NotificationsTableViewController.swift in Sources */, D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */, diff --git a/Tusker/Screens/Compose/ComposeAutocompleteView.swift b/Tusker/Screens/Compose/ComposeAutocompleteView.swift index a82fe27d14..b32b2a21c3 100644 --- a/Tusker/Screens/Compose/ComposeAutocompleteView.swift +++ b/Tusker/Screens/Compose/ComposeAutocompleteView.swift @@ -180,8 +180,14 @@ struct ComposeAutocompleteEmojisView: View { HStack(alignment: expanded ? .top : .center, spacing: 0) { if case let .emoji(query) = uiState.autocompleteState { emojiList(query: query) - .animation(.default, value: expanded) .transition(.move(edge: .bottom)) + .onReceive(uiState.$autocompleteState, perform: queryChanged) + .onAppear { + if uiState.shouldEmojiAutocompletionBeginExpanded { + expanded = true + uiState.shouldEmojiAutocompletionBeginExpanded = false + } + } } else { // when the autocomplete view is animating out, the autocomplete state is nil // add a spacer so the expand button remains on the right @@ -197,18 +203,28 @@ struct ComposeAutocompleteEmojisView: View { @ViewBuilder private func emojiList(query: String) -> some View { if expanded { - EmojiPickerWrapper(searchQuery: query) + verticalGrid .frame(height: 150) } else { horizontalScrollView - .onReceive(uiState.$autocompleteState, perform: queryChanged) - .onAppear { - if uiState.shouldEmojiAutocompletionBeginExpanded { - expanded = true - uiState.shouldEmojiAutocompletionBeginExpanded = false + } + } + + private var verticalGrid: some View { + ScrollView { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 30), spacing: 4)]) { + ForEach(emojis, id: \.shortcode) { (emoji) in + Button { + uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):") + } label: { + CustomEmojiImageView(emoji: emoji) + .frame(height: 30) } } + } + .padding(.all, 8) } + .frame(maxWidth: .infinity) } private var horizontalScrollView: some View { @@ -226,7 +242,6 @@ struct ComposeAutocompleteEmojisView: View { } } .frame(height: 30) - .padding(.vertical, 8) } .animation(.linear(duration: 0.2), value: emojis) @@ -239,30 +254,35 @@ struct ComposeAutocompleteEmojisView: View { private var toggleExpandedButton: some View { Button { - expanded.toggle() + withAnimation { + expanded.toggle() + } } label: { - Image(systemName: expanded ? "chevron.down" : "chevron.up") + Image(systemName: "chevron.down") .resizable() .aspectRatio(contentMode: .fit) + .rotationEffect(expanded ? .zero : .degrees(180)) } .frame(width: 20, height: 20) } private func queryChanged(_ autocompleteState: ComposeUIState.AutocompleteState?) { - guard case let .emoji(query) = autocompleteState, - !query.isEmpty else { + guard case let .emoji(query) = autocompleteState else { emojis = [] return } mastodonController.getCustomEmojis { (emojis) in - let emojis: [Emoji] = - emojis.map { (emoji) -> (Emoji, (matched: Bool, score: Int)) in - (emoji, FuzzyMatcher.match(pattern: query, str: emoji.shortcode)) - } - .filter(\.1.matched) - .sorted { $0.1.score > $1.1.score } - .map(\.0) + var emojis = emojis + if !query.isEmpty { + emojis = + emojis.map { (emoji) -> (Emoji, (matched: Bool, score: Int)) in + (emoji, FuzzyMatcher.match(pattern: query, str: emoji.shortcode)) + } + .filter(\.1.matched) + .sorted { $0.1.score > $1.1.score } + .map(\.0) + } var shortcodes = Set() self.emojis = [] for emoji in emojis where !shortcodes.contains(emoji.shortcode) { diff --git a/Tusker/Screens/Compose/EmojiCollectionViewCell.swift b/Tusker/Screens/Compose/EmojiCollectionViewCell.swift deleted file mode 100644 index fbce6b9359..0000000000 --- a/Tusker/Screens/Compose/EmojiCollectionViewCell.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// EmojiCollectionViewCell.swift -// Tusker -// -// Created by Shadowfacts on 10/12/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import WebURLFoundationExtras - -class EmojiCollectionViewCell: UICollectionViewCell { - - private var emojiImageView: UIImageView! - private var emojiNameLabel: UILabel! - - private var currentEmojiShortcode: String? - private var imageRequest: ImageCache.Request? - - override init(frame: CGRect) { - super.init(frame: frame) - - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - - commonInit() - } - - private func commonInit() { - emojiImageView = UIImageView() - emojiImageView.translatesAutoresizingMaskIntoConstraints = false - emojiImageView.contentMode = .scaleAspectFit - addSubview(emojiImageView) - NSLayoutConstraint.activate([ - emojiImageView.leadingAnchor.constraint(equalTo: leadingAnchor), - emojiImageView.trailingAnchor.constraint(equalTo: trailingAnchor), - emojiImageView.topAnchor.constraint(equalTo: topAnchor), - emojiImageView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - func updateUI(emoji: Emoji) { - currentEmojiShortcode = emoji.shortcode - - imageRequest = ImageCache.emojis.get(URL(emoji.url)!) { [weak self] (_, image) in - guard let image = image else { return } - DispatchQueue.main.async { [weak self] in - guard let self = self, self.currentEmojiShortcode == emoji.shortcode else { return } - self.emojiImageView.image = image - } - } - } - - override func prepareForReuse() { - super.prepareForReuse() - - imageRequest?.cancel() - } - -} diff --git a/Tusker/Screens/Compose/EmojiPickerCollectionViewController.swift b/Tusker/Screens/Compose/EmojiPickerCollectionViewController.swift deleted file mode 100644 index 216b9dfd10..0000000000 --- a/Tusker/Screens/Compose/EmojiPickerCollectionViewController.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// EmojiPickerCollectionViewController.swift -// Tusker -// -// Created by Shadowfacts on 10/12/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -private let reuseIdentifier = "EmojiCell" - -protocol EmojiPickerCollectionViewControllerDelegate: AnyObject { - func selectedEmoji(_ emoji: Emoji) -} - -// It would be nice to replace this with a LazyVGrid when the deployment target is bumped to 14.0 -class EmojiPickerCollectionViewController: UICollectionViewController { - - weak var delegate: EmojiPickerCollectionViewControllerDelegate? - - private weak var mastodonController: MastodonController! - - private var dataSource: UICollectionViewDiffableDataSource! - - var searchQuery: String = "" { - didSet { - guard let emojis = mastodonController.customEmojis else { return } - let snapshot = createFilteredSnapshot(emojis: emojis) - DispatchQueue.main.async { - self.dataSource.apply(snapshot) - } - } - } - - init(mastodonController: MastodonController) { - self.mastodonController = mastodonController - - let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in - let hSizeClass = environment.traitCollection.horizontalSizeClass - - let itemWidth = NSCollectionLayoutDimension.fractionalWidth(1.0 / (hSizeClass == .compact ? 10 : 20)) - let itemSize = NSCollectionLayoutSize(widthDimension: itemWidth, heightDimension: itemWidth) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - - let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemWidth) - let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) - group.interItemSpacing = .fixed(4) - - let section = NSCollectionLayoutSection(group: group) - section.interGroupSpacing = 4 - return section - } - - super.init(collectionViewLayout: layout) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) - // use negative indicator insets to bring the indicators back to the edge of the containing view - // using collectionView.contentInset doesn't work the compositional layout ignores the inset when calculating fractional widths - collectionView.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: -8, bottom: 0, right: -8) - collectionView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) - - collectionView.backgroundColor = .clear - collectionView.register(EmojiCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier) - - dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! EmojiCollectionViewCell - cell.updateUI(emoji: item.emoji) - return cell - } - - mastodonController.getCustomEmojis { (emojis) in - DispatchQueue.main.async { - self.dataSource.apply(self.createFilteredSnapshot(emojis: emojis)) - } - } - } - - private func createFilteredSnapshot(emojis: [Emoji]) -> NSDiffableDataSourceSnapshot { - let items: [Item] - if searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - items = emojis.map { Item(emoji: $0) } - } else { - items = emojis - .map { ($0, FuzzyMatcher.match(pattern: searchQuery, str: $0.shortcode)) } - .filter(\.1.matched) - .sorted { $0.1.score > $1.1.score } - .map { Item(emoji: $0.0) } - } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.emojis]) - snapshot.appendItems(items) - return snapshot - } - - // MARK: UICollectionViewDelegate - - override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let item = dataSource.itemIdentifier(for: indexPath) else { return } - delegate?.selectedEmoji(item.emoji) - } - -} - -extension EmojiPickerCollectionViewController { - enum Section { - case emojis - } - - struct Item: Hashable, Equatable { - let emoji: Emoji - - func hash(into hasher: inout Hasher) { - hasher.combine(emoji.shortcode) - } - - static func ==(lhs: Item, rhs: Item) -> Bool { - lhs.emoji.shortcode == rhs.emoji.shortcode - } - } -} diff --git a/Tusker/Screens/Compose/EmojiPickerWrapper.swift b/Tusker/Screens/Compose/EmojiPickerWrapper.swift deleted file mode 100644 index 5d1a9c081c..0000000000 --- a/Tusker/Screens/Compose/EmojiPickerWrapper.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// EmojiPickerWrapper.swift -// Tusker -// -// Created by Shadowfacts on 10/14/20. -// Copyright © 2020 Shadowfacts. All rights reserved. -// - -import SwiftUI -import Pachyderm - -struct EmojiPickerWrapper: UIViewControllerRepresentable { - typealias UIViewControllerType = EmojiPickerCollectionViewController - - let searchQuery: String - - @EnvironmentObject private var mastodonController: MastodonController - @EnvironmentObject private var uiState: ComposeUIState - - func makeUIViewController(context: Context) -> EmojiPickerCollectionViewController { - let vc = EmojiPickerCollectionViewController(mastodonController: mastodonController) - vc.delegate = context.coordinator - return vc - } - - func updateUIViewController(_ uiViewController: EmojiPickerCollectionViewController, context: Context) { - uiViewController.searchQuery = searchQuery - } - - func makeCoordinator() -> Coordinator { - return Coordinator(uiState: uiState) - } - - class Coordinator: EmojiPickerCollectionViewControllerDelegate { - let uiState: ComposeUIState - - init(uiState: ComposeUIState) { - self.uiState = uiState - } - - func selectedEmoji(_ emoji: Emoji) { - uiState.currentInput?.autocomplete(with: ":\(emoji.shortcode):") - uiState.autocompleteState = nil - } - } -}