feat: implement emoji picker

This commit is contained in:
CMK 2021-03-25 15:56:17 +08:00
parent 91cd7322e7
commit df66cc6b4a
13 changed files with 547 additions and 32 deletions

View File

@ -120,6 +120,7 @@
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */; };
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */; };
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */; };
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */; };
DB2B3ABC25E37E15007045F9 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB2B3ABE25E37E15007045F9 /* InfoPlist.strings */; };
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
@ -134,6 +135,11 @@
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.swift */; };
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; };
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */; };
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */; };
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */; };
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */; };
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481AC25EE155900BEFB67 /* Poll.swift */; };
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B225EE16D000BEFB67 /* PollOption.swift */; };
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; };
@ -166,8 +172,6 @@
DB66728C25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+Diffable.swift */; };
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; };
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; };
DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; };
DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6672A225F9FDE500D60309 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; };
DB68A04A25E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* DarkContentStatusBarStyleNavigationController.swift */; };
DB68A05D25E9055900CFDF14 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = DB68A05C25E9055900CFDF14 /* Settings.bundle */; };
@ -245,6 +249,8 @@
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; };
DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -292,7 +298,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
DB6672A425F9FDE500D60309 /* TwitterTextEditor in Embed Frameworks */,
DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */,
DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
@ -416,6 +422,7 @@
DB1FD44F25F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerItem.swift; sourceTree = "<group>"; };
DB1FD45F25F278AF004CFCFC /* CategoryPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = "<group>"; };
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputViewModel.swift; sourceTree = "<group>"; };
DB2B3ABD25E37E15007045F9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
@ -436,6 +443,11 @@
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = "<group>"; };
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = "<group>"; };
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = "<group>"; };
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItem.swift; sourceTree = "<group>"; };
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = "<group>"; };
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
DB4481AC25EE155900BEFB67 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
DB4481B225EE16D000BEFB67 /* PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOption.swift; sourceTree = "<group>"; };
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
@ -555,7 +567,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
DB6672A325F9FDE500D60309 /* TwitterTextEditor in Frameworks */,
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
@ -565,6 +576,7 @@
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */,
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -763,6 +775,7 @@
isa = PBXGroup;
children = (
DB45FB0425CA87B4005A8AC7 /* APIService */,
DB49A61925FF327D00B98345 /* EmojiService */,
DB9A489B26036E19008B817C /* MastodonAttachmentService */,
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
@ -770,7 +783,6 @@
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */,
DB49A61925FF327D00B98345 /* EmojiService */,
);
path = Service;
sourceTree = "<group>";
@ -830,6 +842,7 @@
DB1FD44325F26CCC004CFCFC /* PickServerSection.swift */,
DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */,
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
);
path = Section;
sourceTree = "<group>";
@ -880,6 +893,7 @@
DB1E347725F519300079D7DF /* PickServerItem.swift */,
DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */,
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */,
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
);
path = Item;
sourceTree = "<group>";
@ -1106,6 +1120,8 @@
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */,
DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */,
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */,
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */,
DB221B15260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift */,
);
path = View;
sourceTree = "<group>";
@ -1167,6 +1183,8 @@
DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */,
DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */,
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */,
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */,
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */,
);
path = CollectionViewCell;
sourceTree = "<group>";
@ -1466,8 +1484,8 @@
DB5086B725CC0D6400C2C187 /* Kingfisher */,
2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */,
2D939AC725EE14620076FA61 /* CropViewController */,
DB6672A225F9FDE500D60309 /* TwitterTextEditor */,
DB9A487D2603456B008B817C /* UITextView+Placeholder */,
DBE64A8A260C49D200E6359A /* TwitterTextEditor */,
);
productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -1597,8 +1615,8 @@
DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */,
2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */,
2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */,
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */,
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
);
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = "";
@ -1797,6 +1815,7 @@
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */,
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
@ -1878,14 +1897,17 @@
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */,
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */,
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */,
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
@ -1919,6 +1941,7 @@
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
DB084B5725CBC56C00F898ED /* Toot.swift in Sources */,
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
@ -1936,6 +1959,7 @@
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */,
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
@ -2557,14 +2581,6 @@
minimumVersion = 6.1.0;
};
};
DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/twitter/TwitterTextEditor.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder";
@ -2573,6 +2589,14 @@
minimumVersion = 1.4.1;
};
};
DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/MainasuK/TwitterTextEditor";
requirement = {
branch = "feature/input-view";
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -2615,16 +2639,16 @@
package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
DB6672A225F9FDE500D60309 /* TwitterTextEditor */ = {
isa = XCSwiftPackageProductDependency;
package = DB6672A125F9FDE500D60309 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
productName = TwitterTextEditor;
};
DB9A487D2603456B008B817C /* UITextView+Placeholder */ = {
isa = XCSwiftPackageProductDependency;
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
productName = "UITextView+Placeholder";
};
DBE64A8A260C49D200E6359A /* TwitterTextEditor */ = {
isa = XCSwiftPackageProductDependency;
package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */;
productName = TwitterTextEditor;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -102,11 +102,11 @@
},
{
"package": "TwitterTextEditor",
"repositoryURL": "https://github.com/twitter/TwitterTextEditor.git",
"repositoryURL": "https://github.com/MainasuK/TwitterTextEditor",
"state": {
"branch": null,
"revision": "8aa914134c5b6aa46e862de63f239ec0e3b52a91",
"version": "1.0.0"
"branch": "feature/input-view",
"revision": "03e7b7497d424d96268f5bcca1f8e9955bb80fea",
"version": null
}
},
{

View File

@ -0,0 +1,36 @@
//
// CustomEmojiPickerItem.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-24.
//
import Foundation
import MastodonSDK
enum CustomEmojiPickerItem {
case emoji(attribute: CustomEmojiAttribute)
}
extension CustomEmojiPickerItem: Equatable, Hashable { }
extension CustomEmojiPickerItem {
final class CustomEmojiAttribute: Equatable, Hashable {
let id = UUID()
let emoji: Mastodon.Entity.Emoji
init(emoji: Mastodon.Entity.Emoji) {
self.emoji = emoji
}
static func == (lhs: CustomEmojiPickerItem.CustomEmojiAttribute, rhs: CustomEmojiPickerItem.CustomEmojiAttribute) -> Bool {
return lhs.id == rhs.id &&
lhs.emoji.shortcode == rhs.emoji.shortcode
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
}

View File

@ -27,19 +27,19 @@ extension ComposeStatusSection {
}
extension ComposeStatusSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView,
dependency: NeedsDependency,
managedObjectContext: NSManagedObjectContext,
composeKind: ComposeKind,
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
) -> UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem> {
UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak customEmojiPickerInputViewModel] collectionView, indexPath, item -> UICollectionViewCell? in
switch item {
case .replyTo(let repliedToStatusObjectID):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToTootContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToTootContentCollectionViewCell
@ -68,6 +68,7 @@ extension ComposeStatusSection {
attribute.composeContent.value = text
}
.store(in: &cell.disposeBag)
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag)
return cell
case .attachment(let attachmentService):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell
@ -136,6 +137,7 @@ extension ComposeStatusSection {
.assign(to: \.value, on: attribute.option)
.store(in: &cell.disposeBag)
cell.delegate = composeStatusPollOptionCollectionViewCellDelegate
ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag)
return cell
case .pollOptionAppendEntry:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell
@ -158,6 +160,7 @@ extension ComposeStatusSection {
}
extension ComposeStatusSection {
static func configure(
cell: ComposeStatusContentCollectionViewCell,
attribute: ComposeStatusItem.ComposeStatusAttribute
@ -187,4 +190,57 @@ extension ComposeStatusSection {
.assign(to: \.value, on: attribute.composeContent)
.store(in: &cell.disposeBag)
}
}
protocol CustomEmojiReplacableTextInput: AnyObject {
var inputView: UIView? { get set }
func reloadInputViews()
// UIKeyInput
func insertText(_ text: String)
// UIResponder
var isFirstResponder: Bool { get }
}
class CustomEmojiReplacableTextInputReference {
weak var value: CustomEmojiReplacableTextInput?
init(value: CustomEmojiReplacableTextInput? = nil) {
self.value = value
}
}
extension TextEditorView: CustomEmojiReplacableTextInput {
func insertText(_ text: String) {
try? updateByReplacing(range: selectedRange, with: text, selectedRange: nil)
}
public override var isFirstResponder: Bool {
return isEditing
}
}
extension UITextField: CustomEmojiReplacableTextInput { }
extension UITextView: CustomEmojiReplacableTextInput { }
extension ComposeStatusSection {
static func configureCustomEmojiPicker(
viewModel: CustomEmojiPickerInputViewModel?,
customEmojiReplacableTextInput: CustomEmojiReplacableTextInput,
disposeBag: inout Set<AnyCancellable>
) {
guard let viewModel = viewModel else { return }
viewModel.isCustomEmojiComposing
.receive(on: DispatchQueue.main)
.sink { [weak viewModel] isCustomEmojiComposing in
guard let viewModel = viewModel else { return }
customEmojiReplacableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil
customEmojiReplacableTextInput.reloadInputViews()
viewModel.append(customEmojiReplacableTextInput: customEmojiReplacableTextInput)
}
.store(in: &disposeBag)
}
}

View File

@ -0,0 +1,60 @@
//
// CustomEmojiPickerSection.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-24.
//
import UIKit
import Kingfisher
enum CustomEmojiPickerSection: Equatable, Hashable {
case emoji(name: String)
}
extension CustomEmojiPickerSection {
static func collectionViewDiffableDataSource(
for collectionView: UICollectionView,
dependency: NeedsDependency
) -> UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem> {
let dataSource = UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
switch item {
case .emoji(let attribute):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell
let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill)
.af.imageRounded(withCornerRadius: 4)
cell.emojiImageView.kf.setImage(
with: URL(string: attribute.emoji.url),
placeholder: placeholder,
options: [
.transition(.fade(0.2))
],
completionHandler: nil
)
return cell
}
}
dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in
guard let dataSource = dataSource else { return nil }
let sections = dataSource.snapshot().sectionIdentifiers
guard indexPath.section < sections.count else { return nil }
let section = sections[indexPath.section]
switch kind {
case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self):
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView
switch section {
case .emoji(let name):
header.titlelabel.text = name
}
return header
default:
assertionFailure()
return nil
}
}
return dataSource
}
}

View File

@ -77,6 +77,7 @@ extension ComposeStatusPollOptionCollectionViewCell {
reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
reorderBarImageView.setContentCompressionResistancePriority(.defaultHigh + 10, for: .horizontal)
pollOptionView.checkmarkImageView.isHidden = true
pollOptionView.optionPercentageLabel.isHidden = true

View File

@ -0,0 +1,42 @@
//
// CustomEmojiPickerHeaderCollectionReusableView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-24.
//
import UIKit
final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableView {
let titlelabel: UILabel = {
let label = UILabel()
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12, weight: .bold))
label.textColor = Asset.Colors.Label.secondary.color
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension CustomEmojiPickerHeaderCollectionReusableView {
private func _init() {
titlelabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(titlelabel)
NSLayoutConstraint.activate([
titlelabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
titlelabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
titlelabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
titlelabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}

View File

@ -0,0 +1,52 @@
//
// CustomEmojiPickerItemCollectionViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-24.
//
import UIKit
final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell {
static let itemSize = CGSize(width: 44, height: 44)
let emojiImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.layer.masksToBounds = true
return imageView
}()
override var isHighlighted: Bool {
didSet {
emojiImageView.alpha = isHighlighted ? 0.5 : 1.0
}
}
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension CustomEmojiPickerItemCollectionViewCell {
private func _init() {
emojiImageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(emojiImageView)
NSLayoutConstraint.activate([
emojiImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
emojiImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
emojiImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
emojiImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}
}

View File

@ -52,9 +52,25 @@ final class ComposeViewController: UIViewController, NeedsDependency {
return collectionView
}()
var systemKeyboardHeight: CGFloat = .zero {
didSet {
// note: some system AutoLayout warning here
customEmojiPickerInputView.frame.size.height = systemKeyboardHeight != .zero ? systemKeyboardHeight : 300
}
}
// CustomEmojiPickerView
let customEmojiPickerInputView: CustomEmojiPickerInputView = {
let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard)
return view
}()
let composeToolbarView: ComposeToolbarView = {
let composeToolbarView = ComposeToolbarView()
composeToolbarView.backgroundColor = .secondarySystemBackground
let text = UITextView()
let inputView = UIInputView(frame: .init(x: 0, y: 0, width: 40, height: 40), inputViewStyle: .keyboard)
text.inputAccessoryView = inputView
composeToolbarView.backgroundColor = inputView.backgroundColor
return composeToolbarView
}()
var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
@ -153,6 +169,7 @@ extension ComposeViewController {
viewModel.setupDiffableDataSource(
for: collectionView,
dependency: self,
customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
textEditorViewTextAttributesDelegate: self,
composeStatusAttachmentTableViewCellDelegate: self,
composeStatusPollOptionCollectionViewCellDelegate: self,
@ -162,15 +179,23 @@ extension ComposeViewController {
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
collectionView.addGestureRecognizer(longPressReorderGesture)
customEmojiPickerInputView.collectionView.delegate = self
viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
viewModel.setupCustomEmojiPickerDiffableDataSource(
for: customEmojiPickerInputView.collectionView,
dependency: self
)
// respond scrollView overlap change
view.layoutIfNeeded()
// update layout when keyboard show/dismiss
Publishers.CombineLatest3(
Publishers.CombineLatest4(
KeyboardResponderService.shared.isShow.eraseToAnyPublisher(),
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher(),
viewModel.isCustomEmojiComposing.eraseToAnyPublisher()
)
.sink(receiveValue: { [weak self] isShow, state, endFrame in
.sink(receiveValue: { [weak self] isShow, state, endFrame, isCustomEmojiComposing in
guard let self = self else { return }
guard isShow, state == .dock else {
@ -182,8 +207,9 @@ extension ComposeViewController {
}
return
}
// isShow AND dock state
self.systemKeyboardHeight = endFrame.height
let contentFrame = self.view.convert(self.collectionView.frame, to: nil)
let padding = contentFrame.maxY - endFrame.minY
guard padding > 0 else {
@ -593,6 +619,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) {
viewModel.isCustomEmojiComposing.value.toggle()
}
func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) {
@ -606,6 +633,35 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
// MARK: - UITableViewDelegate
extension ComposeViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)
if collectionView === customEmojiPickerInputView.collectionView {
guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return }
let item = diffableDataSource.itemIdentifier(for: indexPath)
guard case let .emoji(attribute) = item else { return }
let emoji = attribute.emoji
let textEditorView = self.textEditorView()
// retrive active text input and insert emoji
// the leading and trailing space is REQUIRED to fix `UITextStorage` layout issue
let reference = viewModel.customEmojiPickerInputViewModel.insertText(" :\(emoji.shortcode): ")
// workaround: non-user interactive change do not trigger value update event
if reference?.value === textEditorView {
viewModel.composeStatusAttribute.composeContent.value = textEditorView?.text
// update text storage
textEditorView?.setNeedsUpdateTextAttributes()
// collection self-size
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
} else {
// do nothing
}
}
}
// MARK: - UIAdaptivePresentationControllerDelegate

View File

@ -6,13 +6,16 @@
//
import UIKit
import Combine
import TwitterTextEditor
import MastodonSDK
extension ComposeViewModel {
func setupDiffableDataSource(
for collectionView: UICollectionView,
dependency: NeedsDependency,
customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
textEditorViewTextAttributesDelegate: TextEditorViewTextAttributesDelegate,
composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
@ -24,6 +27,7 @@ extension ComposeViewModel {
dependency: dependency,
managedObjectContext: context.managedObjectContext,
composeKind: composeKind,
customEmojiPickerInputViewModel: customEmojiPickerInputViewModel,
textEditorViewTextAttributesDelegate: textEditorViewTextAttributesDelegate,
composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate,
composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate,
@ -65,4 +69,49 @@ extension ComposeViewModel {
diffableDataSource.apply(snapshot, animatingDifferences: false)
}
func setupCustomEmojiPickerDiffableDataSource(
for collectionView: UICollectionView,
dependency: NeedsDependency
) {
let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
for: collectionView,
dependency: dependency
)
self.customEmojiPickerDiffableDataSource = diffableDataSource
customEmojiViewModel
.sink { [weak self, weak diffableDataSource] customEmojiViewModel in
guard let self = self else { return }
guard let diffableDataSource = diffableDataSource else { return }
guard let customEmojiViewModel = customEmojiViewModel else {
self.customEmojiViewModelSubscription = nil
let snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
diffableDataSource.apply(snapshot)
return
}
self.customEmojiViewModelSubscription = customEmojiViewModel.emojis
.receive(on: DispatchQueue.main)
.sink { [weak self, weak diffableDataSource] emojis in
guard let _ = self else { return }
guard let diffableDataSource = diffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
let customEmojiSection = CustomEmojiPickerSection.emoji(name: customEmojiViewModel.domain.uppercased())
snapshot.appendSections([customEmojiSection])
let items: [CustomEmojiPickerItem] = {
var items = [CustomEmojiPickerItem]()
for emoji in emojis where emoji.visibleInPicker {
let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
let item = CustomEmojiPickerItem.emoji(attribute: attribute)
items.append(item)
}
return items
}()
snapshot.appendItems(items, toSection: customEmojiSection)
diffableDataSource.apply(snapshot)
}
}
.store(in: &disposeBag)
}
}

View File

@ -20,12 +20,13 @@ final class ComposeViewModel {
let composeKind: ComposeStatusSection.ComposeKind
let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
let isPollComposing = CurrentValueSubject<Bool, Never>(false)
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
let activeAuthentication: CurrentValueSubject<MastodonAuthentication?, Never>
let activeAuthenticationBox: CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>
// output
//var diffableDataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
var diffableDataSource: UICollectionViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>!
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>!
private(set) lazy var publishStateMachine: GKStateMachine = {
// exclude timeline middle fetcher state
let stateMachine = GKStateMachine(states: [
@ -46,7 +47,9 @@ final class ComposeViewModel {
let isPollToolbarButtonEnabled = CurrentValueSubject<Bool, Never>(true)
// custom emojis
var customEmojiViewModelSubscription: AnyCancellable?
let customEmojiViewModel = CurrentValueSubject<EmojiService.CustomEmojiViewModel?, Never>(nil)
let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
// attachment
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
@ -69,6 +72,10 @@ final class ComposeViewModel {
self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value)
// end init
isCustomEmojiComposing
.assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
.store(in: &disposeBag)
// bind active authentication
context.authenticationService.activeMastodonAuthentication
.assign(to: \.value, on: activeAuthentication)

View File

@ -0,0 +1,74 @@
//
// CustomEmojiPickerInputView.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-24.
//
import UIKit
final class CustomEmojiPickerInputView: UIInputView {
private(set) lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
collectionView.register(CustomEmojiPickerItemCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self))
collectionView.register(CustomEmojiPickerHeaderCollectionReusableView.self, forSupplementaryViewOfKind: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self))
collectionView.backgroundColor = .clear
return collectionView
}()
override init(frame: CGRect, inputViewStyle: UIInputView.Style) {
super.init(frame: frame, inputViewStyle: inputViewStyle)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension CustomEmojiPickerInputView {
private func _init() {
allowsSelfSizing = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: topAnchor),
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}
extension CustomEmojiPickerInputView {
func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(CustomEmojiPickerItemCollectionViewCell.itemSize.width),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .flexible(4), top: .flexible(4), trailing: .flexible(0), bottom: .flexible(0))
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(CustomEmojiPickerItemCollectionViewCell.itemSize.height))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 5
section.contentInsetsReference = .readableContent
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0)
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)),
elementKind: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self),
alignment: .top)
// sectionHeader.pinToVisibleBounds = true
sectionHeader.zIndex = 2
section.boundarySupplementaryItems = [sectionHeader]
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
}

View File

@ -0,0 +1,58 @@
//
// CustomEmojiPickerInputViewModel.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-25.
//
import UIKit
import Combine
final class CustomEmojiPickerInputViewModel {
var disposeBag = Set<AnyCancellable>()
private var customEmojiReplacableTextInputReferences: [CustomEmojiReplacableTextInputReference] = []
// input
weak var customEmojiPickerInputView: CustomEmojiPickerInputView?
// output
let isCustomEmojiComposing = CurrentValueSubject<Bool, Never>(false)
}
extension CustomEmojiPickerInputViewModel {
private func removeEmptyReferences() {
customEmojiReplacableTextInputReferences.removeAll(where: { element in
element.value == nil
})
}
func append(customEmojiReplacableTextInput textInput: CustomEmojiReplacableTextInput) {
removeEmptyReferences()
let isContains = customEmojiReplacableTextInputReferences.contains(where: { element in
element.value === textInput
})
guard !isContains else {
return
}
customEmojiReplacableTextInputReferences.append(CustomEmojiReplacableTextInputReference(value: textInput))
}
func insertText(_ text: String) -> CustomEmojiReplacableTextInputReference? {
removeEmptyReferences()
for reference in customEmojiReplacableTextInputReferences {
guard reference.value?.isFirstResponder == true else { continue }
reference.value?.insertText(text)
return reference
}
return nil
}
}