diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index cb96c5bb0..6f195f704 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -226,6 +226,12 @@ DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; }; DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; }; DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; }; + DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */; }; + DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */; }; + DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */; }; + DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */; }; + DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */; }; + DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */; }; DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; @@ -344,8 +350,6 @@ DB75BF1E263C1C1B00EDBF1F /* CustomScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */; }; DB789A0B25F9F2950071ACA0 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */; }; DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; }; - DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */; }; - DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */; }; DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; }; DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; }; DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; }; @@ -842,6 +846,13 @@ DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRelationshipActionButton.swift; sourceTree = ""; }; DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldView.swift; sourceTree = ""; }; DB35FC2E26130172006193C9 /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = ""; }; + DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = ""; }; + DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = ""; }; + DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = ""; }; + DB3667A2268AC3BB0027D07F /* MetaTextView */ = {isa = PBXFileReference; lastKnownFileType = folder; name = MetaTextView; path = ../MetaTextView; sourceTree = ""; }; + DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollTableViewCell.swift; sourceTree = ""; }; + DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollSection.swift; sourceTree = ""; }; + DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -957,8 +968,6 @@ DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScheduler.swift; sourceTree = ""; }; DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; - DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentCollectionViewCell.swift; sourceTree = ""; }; - DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentCollectionViewCell.swift; sourceTree = ""; }; DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = ""; }; DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = ""; }; DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = ""; }; @@ -1529,6 +1538,8 @@ 2D35237926256D920031AF25 /* NotificationSection.swift */, 2D198648261C0B8500F0B013 /* SearchResultSection.swift */, DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, + DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */, + DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */, DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, DB6D9F7C26358ED4008423CD /* SettingsSection.swift */, 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */, @@ -1593,6 +1604,8 @@ DB1E347725F519300079D7DF /* PickServerItem.swift */, DB1FD45925F27898004CFCFC /* CategoryPickerItem.swift */, DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */, + DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */, + DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */, DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */, DB6D9F8326358EEC008423CD /* SettingsItem.swift */, DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */, @@ -1747,6 +1760,8 @@ children = ( DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */, DB03F7EF26899097007B274C /* ComposeStatusContentTableViewCell.swift */, + DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */, + DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */, ); path = TableViewCell; sourceTree = ""; @@ -1812,6 +1827,7 @@ children = ( DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */, DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */, + DB3667A2268AC3BB0027D07F /* MetaTextView */, DB3D0FED25BAA42200EAA174 /* MastodonSDK */, DB427DD425BAA00100D1B89D /* Mastodon */, DB427DEB25BAA00100D1B89D /* MastodonTests */, @@ -2114,8 +2130,6 @@ DB789A2125F9F76D0071ACA0 /* CollectionViewCell */ = { isa = PBXGroup; children = ( - DB789A2A25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift */, - DB789A1B25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift */, DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */, DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */, DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */, @@ -3217,6 +3231,7 @@ DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */, 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, 2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */, + DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */, DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */, 2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */, @@ -3240,6 +3255,7 @@ DB98338825C945ED00AD9700 /* Assets.swift in Sources */, DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */, DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, + DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */, 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */, DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */, DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */, @@ -3329,17 +3345,18 @@ 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */, DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */, DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, + DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */, DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */, 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */, DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, - DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, 2D084B8D26258EA3003AA3AF /* NotificationViewModel+Diffable.swift in Sources */, DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */, + DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */, @@ -3426,6 +3443,7 @@ DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, + DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DBCBCC032680AF6E000F5B51 /* AsyncHomeTimelineViewController+DebugAction.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, @@ -3494,6 +3512,7 @@ DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */, DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */, DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */, + DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */, DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */, 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, DB1EE7B2267F9525000CC337 /* StatusProvider+StatusNodeDelegate.swift in Sources */, @@ -3516,7 +3535,6 @@ DBAFB7352645463500371D5F /* Emojis.swift in Sources */, DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */, DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, - DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */, DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */, 5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */, DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index ebbde8329..b782cd198 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 21 + 23 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,7 +37,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 22 + 21 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Item/ComposeStatusAttachmentItem.swift b/Mastodon/Diffiable/Item/ComposeStatusAttachmentItem.swift new file mode 100644 index 000000000..834e1da49 --- /dev/null +++ b/Mastodon/Diffiable/Item/ComposeStatusAttachmentItem.swift @@ -0,0 +1,14 @@ +// +// ComposeStatusAttachmentItem.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import Foundation + +enum ComposeStatusAttachmentItem { + case attachment(attachmentService: MastodonAttachmentService) +} + +extension ComposeStatusAttachmentItem: Hashable { } diff --git a/Mastodon/Diffiable/Item/ComposeStatusItem.swift b/Mastodon/Diffiable/Item/ComposeStatusItem.swift index d60a76e82..96ea8b05f 100644 --- a/Mastodon/Diffiable/Item/ComposeStatusItem.swift +++ b/Mastodon/Diffiable/Item/ComposeStatusItem.swift @@ -13,14 +13,10 @@ import CoreData enum ComposeStatusItem { case replyTo(statusObjectID: NSManagedObjectID) case input(replyToStatusObjectID: NSManagedObjectID?, attribute: ComposeStatusAttribute) - case attachment(attachmentService: MastodonAttachmentService) - case pollOption(attribute: ComposePollOptionAttribute) - case pollOptionAppendEntry - case pollExpiresOption(attribute: ComposePollExpiresOptionAttribute) + case attachment(attachmentAttribute: ComposeStatusAttachmentAttribute) + case pollOption(pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute], pollExpiresOptionAttribute: ComposeStatusPollItem.PollExpiresOptionAttribute) } -extension ComposeStatusItem: Equatable { } - extension ComposeStatusItem: Hashable { } extension ComposeStatusItem { @@ -50,88 +46,22 @@ extension ComposeStatusItem { } } -protocol ComposePollAttributeDelegate: AnyObject { - func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) -} - extension ComposeStatusItem { - final class ComposePollOptionAttribute: Equatable, Hashable { + final class ComposeStatusAttachmentAttribute: Hashable { private let id = UUID() - - var disposeBag = Set() - weak var delegate: ComposePollAttributeDelegate? - let option = CurrentValueSubject("") - - init() { - option - .sink { [weak self] option in - guard let self = self else { return } - self.delegate?.composePollAttribute(self, pollOptionDidChange: option) - } - .store(in: &disposeBag) + var attachmentServices: [MastodonAttachmentService] + + init(attachmentServices: [MastodonAttachmentService]) { + self.attachmentServices = attachmentServices } - - deinit { - disposeBag.removeAll() + + static func == (lhs: ComposeStatusAttachmentAttribute, rhs: ComposeStatusAttachmentAttribute) -> Bool { + return lhs.attachmentServices == rhs.attachmentServices } - - static func == (lhs: ComposePollOptionAttribute, rhs: ComposePollOptionAttribute) -> Bool { - return lhs.id == rhs.id && - lhs.option.value == rhs.option.value - } - + func hash(into hasher: inout Hasher) { hasher.combine(id) } } } - -extension ComposeStatusItem { - final class ComposePollExpiresOptionAttribute: Equatable, Hashable { - private let id = UUID() - - let expiresOption = CurrentValueSubject(.thirtyMinutes) - - - static func == (lhs: ComposePollExpiresOptionAttribute, rhs: ComposePollExpiresOptionAttribute) -> Bool { - return lhs.id == rhs.id && - lhs.expiresOption.value == rhs.expiresOption.value - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - enum ExpiresOption: Equatable, Hashable, CaseIterable { - case thirtyMinutes - case oneHour - case sixHours - case oneDay - case threeDays - case sevenDays - - var title: String { - switch self { - case .thirtyMinutes: return L10n.Scene.Compose.Poll.thirtyMinutes - case .oneHour: return L10n.Scene.Compose.Poll.oneHour - case .sixHours: return L10n.Scene.Compose.Poll.sixHours - case .oneDay: return L10n.Scene.Compose.Poll.oneDay - case .threeDays: return L10n.Scene.Compose.Poll.threeDays - case .sevenDays: return L10n.Scene.Compose.Poll.sevenDays - } - } - - var seconds: Int { - switch self { - case .thirtyMinutes: return 60 * 30 - case .oneHour: return 60 * 60 * 1 - case .sixHours: return 60 * 60 * 6 - case .oneDay: return 60 * 60 * 24 - case .threeDays: return 60 * 60 * 24 * 3 - case .sevenDays: return 60 * 60 * 24 * 7 - } - } - } - } -} diff --git a/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift b/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift new file mode 100644 index 000000000..a6d9a36e8 --- /dev/null +++ b/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift @@ -0,0 +1,105 @@ +// +// ComposeStatusPollItem.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import Foundation +import Combine + +enum ComposeStatusPollItem { + case pollOption(attribute: PollOptionAttribute) + case pollOptionAppendEntry + case pollExpiresOption(attribute: PollExpiresOptionAttribute) +} + +extension ComposeStatusPollItem: Hashable { } + +extension ComposeStatusPollItem { + + final class PollOptionAttribute: Equatable, Hashable { + private let id = UUID() + + var disposeBag = Set() + weak var delegate: ComposePollAttributeDelegate? + + let option = CurrentValueSubject("") + + init() { + option + .sink { [weak self] option in + guard let self = self else { return } + self.delegate?.composePollAttribute(self, pollOptionDidChange: option) + } + .store(in: &disposeBag) + } + + deinit { + disposeBag.removeAll() + } + + static func == (lhs: PollOptionAttribute, rhs: PollOptionAttribute) -> Bool { + return lhs.id == rhs.id && + lhs.option.value == rhs.option.value + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + } + +} + +protocol ComposePollAttributeDelegate: AnyObject { + func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) +} + +extension ComposeStatusPollItem { + final class PollExpiresOptionAttribute: Equatable, Hashable { + private let id = UUID() + + let expiresOption = CurrentValueSubject(.thirtyMinutes) + + + static func == (lhs: PollExpiresOptionAttribute, rhs: PollExpiresOptionAttribute) -> Bool { + return lhs.id == rhs.id && + lhs.expiresOption.value == rhs.expiresOption.value + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + enum ExpiresOption: Equatable, Hashable, CaseIterable { + case thirtyMinutes + case oneHour + case sixHours + case oneDay + case threeDays + case sevenDays + + var title: String { + switch self { + case .thirtyMinutes: return L10n.Scene.Compose.Poll.thirtyMinutes + case .oneHour: return L10n.Scene.Compose.Poll.oneHour + case .sixHours: return L10n.Scene.Compose.Poll.sixHours + case .oneDay: return L10n.Scene.Compose.Poll.oneDay + case .threeDays: return L10n.Scene.Compose.Poll.threeDays + case .sevenDays: return L10n.Scene.Compose.Poll.sevenDays + } + } + + var seconds: Int { + switch self { + case .thirtyMinutes: return 60 * 30 + case .oneHour: return 60 * 60 * 1 + case .sixHours: return 60 * 60 * 6 + case .oneDay: return 60 * 60 * 24 + case .threeDays: return 60 * 60 * 24 * 3 + case .sevenDays: return 60 * 60 * 24 * 7 + } + } + } + } +} diff --git a/Mastodon/Diffiable/Section/ComposeStatusAttachmentSection.swift b/Mastodon/Diffiable/Section/ComposeStatusAttachmentSection.swift new file mode 100644 index 000000000..4de7653a5 --- /dev/null +++ b/Mastodon/Diffiable/Section/ComposeStatusAttachmentSection.swift @@ -0,0 +1,13 @@ +// +// ComposeStatusAttachmentSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import Foundation + +enum ComposeStatusAttachmentSection: Hashable { + case main +} + diff --git a/Mastodon/Diffiable/Section/ComposeStatusPollSection.swift b/Mastodon/Diffiable/Section/ComposeStatusPollSection.swift new file mode 100644 index 000000000..cd06572dc --- /dev/null +++ b/Mastodon/Diffiable/Section/ComposeStatusPollSection.swift @@ -0,0 +1,12 @@ +// +// ComposeStatusPollSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import Foundation + +enum ComposeStatusPollSection: Hashable { + case main +} diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 93005cca8..b82116ad4 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -30,266 +30,6 @@ extension ComposeStatusSection { } extension ComposeStatusSection { - static func collectionViewDiffableDataSource( - for collectionView: UICollectionView, - dependency: NeedsDependency, - managedObjectContext: NSManagedObjectContext, - composeKind: ComposeKind, - repliedToCellFrameSubscriber: CurrentValueSubject, - customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, - metaTextDelegate: MetaTextDelegate, - metaTextViewDelegate: UITextViewDelegate, - composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, - composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, - composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, - composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate - ) -> UICollectionViewDiffableDataSource { - UICollectionViewDiffableDataSource(collectionView: collectionView) { [ - weak customEmojiPickerInputViewModel, - weak metaTextDelegate, - weak metaTextViewDelegate, - weak composeStatusAttachmentTableViewCellDelegate, - weak composeStatusPollOptionCollectionViewCellDelegate, - weak composeStatusNewPollOptionCollectionViewCellDelegate, - weak composeStatusPollExpiresOptionCollectionViewCellDelegate - ] collectionView, indexPath, item -> UICollectionViewCell? in - switch item { - case .replyTo(let replyToStatusObjectID): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell - // set empty text before retrieve real data to fix pseudo-text display issue - cell.statusView.nameLabel.text = " " - cell.statusView.usernameLabel.text = " " - managedObjectContext.performAndWait { - guard let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else { - return - } - let status = replyTo.reblog ?? replyTo - - // set avatar - cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) - // set name username - cell.statusView.nameLabel.text = { - let author = status.author - return author.displayName.isEmpty ? author.username : author.displayName - }() - cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct - // set text - //status.emoji -// cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:]) - // set date - cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow - - cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag) - } - return cell - case .input(let replyToStatusObjectID, let attribute): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell - do { - let metaContent = try MastodonMetaContent.convert( - document: MastodonContent(content: attribute.composeContent.value ?? "", emojis: [:]) - ) - cell.metaText.configure(content: metaContent) - } catch { - assertionFailure() - } - cell.metaText.delegate = metaTextDelegate - cell.metaText.textView.delegate = metaTextViewDelegate - cell.statusContentWarningEditorView.textView.text = attribute.contentWarningContent.value - managedObjectContext.performAndWait { - guard let replyToStatusObjectID = replyToStatusObjectID, - let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else { - cell.statusView.headerContainerView.isHidden = true - return - } - cell.statusView.headerContainerView.isHidden = false - cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) - cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback) - } - ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute) -// cell.composeContent -// .removeDuplicates() -// .receive(on: DispatchQueue.main) -// .sink { [weak collectionView] text in -// guard let collectionView = collectionView else { return } -// // self size input cell -// // needs restore content offset to resolve issue #83 -// let oldContentOffset = collectionView.contentOffset -// collectionView.collectionViewLayout.invalidateLayout() -// collectionView.layoutIfNeeded() -// collectionView.contentOffset = oldContentOffset -// -// // bind input data -// attribute.composeContent.value = text -// } -// .store(in: &cell.disposeBag) - attribute.isContentWarningComposing - .receive(on: DispatchQueue.main) - .sink { [weak cell, weak collectionView] isContentWarningComposing in - guard let cell = cell else { return } - guard let collectionView = collectionView else { return } - // self size input cell - collectionView.collectionViewLayout.invalidateLayout() - cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing - cell.statusContentWarningEditorView.alpha = 0 - UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) { - cell.statusContentWarningEditorView.alpha = 1 - } completion: { _ in - // do nothing - } - } - .store(in: &cell.disposeBag) - cell.contentWarningContent - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak collectionView] text in - guard let collectionView = collectionView else { return } - // self size input cell - collectionView.collectionViewLayout.invalidateLayout() - // bind input data - attribute.contentWarningContent.value = text - } - .store(in: &cell.disposeBag) - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag) - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag) - - return cell - case .attachment(let attachmentService): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell - cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value - cell.delegate = composeStatusAttachmentTableViewCellDelegate - attachmentService.thumbnailImage - .receive(on: DispatchQueue.main) - .sink { [weak cell] thumbnailImage in - guard let cell = cell else { return } - let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) - guard let image = thumbnailImage else { - let placeholder = UIImage.placeholder( - size: size, - color: Asset.Colors.Background.systemGroupedBackground.color - ) - .af.imageRounded( - withCornerRadius: AttachmentContainerView.containerViewCornerRadius - ) - cell.attachmentContainerView.previewImageView.image = placeholder - return - } - cell.attachmentContainerView.previewImageView.image = image - .af.imageAspectScaled(toFill: size) - .af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius) - } - .store(in: &cell.disposeBag) - Publishers.CombineLatest( - attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(), - attachmentService.error.eraseToAnyPublisher() - ) - .receive(on: DispatchQueue.main) - .sink { [weak cell, weak attachmentService] uploadState, error in - guard let cell = cell else { return } - guard let attachmentService = attachmentService else { return } - cell.attachmentContainerView.emptyStateView.isHidden = error == nil - cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil - if let error = error { - cell.attachmentContainerView.activityIndicatorView.stopAnimating() - cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription - } else { - guard let uploadState = uploadState else { return } - switch uploadState { - case is MastodonAttachmentService.UploadState.Finish, - is MastodonAttachmentService.UploadState.Fail: - cell.attachmentContainerView.activityIndicatorView.stopAnimating() - cell.attachmentContainerView.emptyStateView.label.text = { - if let file = attachmentService.file.value { - switch file { - case .jpeg, .png, .gif: - return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) - case .other: - return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) - } - } else { - return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) - } - }() - default: - break - } - } - } - .store(in: &cell.disposeBag) - NotificationCenter.default.publisher( - for: UITextView.textDidChangeNotification, - object: cell.attachmentContainerView.descriptionTextView - ) - .receive(on: DispatchQueue.main) - .sink { notification in - guard let textField = notification.object as? UITextView else { return } - let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) - attachmentService.description.value = text - } - .store(in: &cell.disposeBag) - return cell - case .pollOption(let attribute): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell - cell.pollOptionView.optionTextField.text = attribute.option.value - cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1) - cell.pollOption - .receive(on: DispatchQueue.main) - .assign(to: \.value, on: attribute.option) - .store(in: &cell.disposeBag) - cell.delegate = composeStatusPollOptionCollectionViewCellDelegate - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) - return cell - case .pollOptionAppendEntry: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell - cell.delegate = composeStatusNewPollOptionCollectionViewCellDelegate - return cell - case .pollExpiresOption(let attribute): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell - cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) - attribute.expiresOption - .receive(on: DispatchQueue.main) - .sink { [weak cell] expiresOption in - guard let cell = cell else { return } - cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) - } - .store(in: &cell.disposeBag) - cell.delegate = composeStatusPollExpiresOptionCollectionViewCellDelegate - return cell - } - } - } -} - -extension ComposeStatusSection { - - static func configureStatusContent( - cell: ComposeStatusContentCollectionViewCell, - attribute: ComposeStatusItem.ComposeStatusAttribute - ) { - // set avatar - attribute.avatarURL - .receive(on: DispatchQueue.main) - .sink { avatarURL in - cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarURL)) - } - .store(in: &cell.disposeBag) - // set display name and username - Publishers.CombineLatest( - attribute.displayName.eraseToAnyPublisher(), - attribute.username.eraseToAnyPublisher() - ) - .receive(on: DispatchQueue.main) - .sink { displayName, username in - cell.statusView.nameLabel.text = displayName - cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " " - } - .store(in: &cell.disposeBag) - - // bind compose content - cell.composeContent - .map { $0 as String? } - .assign(to: \.value, on: attribute.composeContent) - .store(in: &cell.disposeBag) - } static func configureStatusContent( cell: ComposeStatusContentTableViewCell, @@ -335,16 +75,6 @@ class CustomEmojiReplaceableTextInputReference { } } -//extension TextEditorView: CustomEmojiReplaceableTextInput { -// func insertText(_ text: String) { -// try? updateByReplacing(range: selectedRange, with: text, selectedRange: nil) -// } -// -// public override var isFirstResponder: Bool { -// return isEditing -// } -// -//} extension UITextField: CustomEmojiReplaceableTextInput { } extension UITextView: CustomEmojiReplaceableTextInput { } diff --git a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift index 20dc5b809..57d7b6019 100644 --- a/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift +++ b/Mastodon/Diffiable/Section/CustomEmojiPickerSection.swift @@ -6,7 +6,7 @@ // import UIKit -import Kingfisher +import Nuke enum CustomEmojiPickerSection: Equatable, Hashable { case emoji(name: String) @@ -24,13 +24,13 @@ extension CustomEmojiPickerSection { 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 + cell.imageTask = Nuke.loadImage( + with: attribute.emoji.url, + options: .init( + placeholder: placeholder, + transition: .fadeIn(duration: 0.2) + ), + into: cell.emojiImageView ) cell.accessibilityLabel = attribute.emoji.shortcode return cell @@ -48,7 +48,7 @@ extension CustomEmojiPickerSection { 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 + header.titleLabel.text = name } return header default: diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift deleted file mode 100644 index 8da4c0729..000000000 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeRepliedToStatusContentCollectionViewCell.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// ComposeRepliedToStatusContentCollectionViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-11. -// - -import UIKit -import Combine - -final class ComposeRepliedToStatusContentCollectionViewCell: UICollectionViewCell { - - var disposeBag = Set() - - let statusView = StatusView() - - let framePublisher = PassthroughSubject() - - override func prepareForReuse() { - super.prepareForReuse() - - statusView.updateContentWarningDisplay(isHidden: true, animated: false) - disposeBag.removeAll() - } - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - - override func layoutSubviews() { - super.layoutSubviews() - framePublisher.send(bounds) - } - -} - -extension ComposeRepliedToStatusContentCollectionViewCell { - - private func _init() { - backgroundColor = .clear - - statusView.actionToolbarContainer.isHidden = true - statusView.revealContentWarningButton.isHidden = true - - statusView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(statusView) - NSLayoutConstraint.activate([ - statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).identifier("statusView.top to ComposeRepliedToStatusContentCollectionViewCell.contentView.top"), - statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10).identifier("ComposeRepliedToStatusContentCollectionViewCell.contentView.bottom to statusView.bottom"), - ]) - } - -} - diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentCollectionViewCell.swift deleted file mode 100644 index fbe7a2023..000000000 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusContentCollectionViewCell.swift +++ /dev/null @@ -1,169 +0,0 @@ -// -// ComposeStatusContentCollectionViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-11. -// - -import os.log -import UIKit -import Combine -import MetaTextView - -final class ComposeStatusContentCollectionViewCell: UICollectionViewCell { - - var disposeBag = Set() - - let statusView = StatusView() - - let statusContentWarningEditorView = StatusContentWarningEditorView() - - let textEditorViewContainerView = UIView() - - static let metaTextViewTag: Int = 333 - let metaText: MetaText = { - let metaText = MetaText() - metaText.textView.tag = ComposeStatusContentCollectionViewCell.metaTextViewTag - metaText.textView.isScrollEnabled = false - metaText.textView.keyboardType = .twitter - metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) - metaText.textView.attributedPlaceholder = { - var attributes = metaText.textAttributes - attributes[.foregroundColor] = Asset.Colors.Label.secondary.color - return NSAttributedString( - string: L10n.Scene.Compose.contentInputPlaceholder, - attributes: attributes - ) - }() - return metaText - }() - - // output - let composeContent = PassthroughSubject() - let contentWarningContent = PassthroughSubject() - - override func prepareForReuse() { - super.prepareForReuse() - - metaText.delegate = nil - metaText.textView.delegate = nil - } - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeStatusContentCollectionViewCell { - - private func _init() { - // selectionStyle = .none - layer.zPosition = 999 - preservesSuperviewLayoutMargins = true - - statusContentWarningEditorView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(statusContentWarningEditorView) - NSLayoutConstraint.activate([ - statusContentWarningEditorView.topAnchor.constraint(equalTo: contentView.topAnchor), - statusContentWarningEditorView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - statusContentWarningEditorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - ]) - statusContentWarningEditorView.preservesSuperviewLayoutMargins = true - statusContentWarningEditorView.containerBackgroundView.isHidden = false - - statusView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(statusView) - NSLayoutConstraint.activate([ - statusView.topAnchor.constraint(equalTo: statusContentWarningEditorView.bottomAnchor, constant: 20), - statusView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - statusView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), - ]) - statusView.statusContainerStackView.isHidden = true - statusView.actionToolbarContainer.isHidden = true - statusView.nameTrialingDotLabel.isHidden = true - statusView.dateLabel.isHidden = true - - statusView.setContentHuggingPriority(.defaultHigh, for: .vertical) - statusView.setContentCompressionResistancePriority(.required - 1, for: .vertical) - - textEditorViewContainerView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(textEditorViewContainerView) - NSLayoutConstraint.activate([ - textEditorViewContainerView.topAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 10), - textEditorViewContainerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - textEditorViewContainerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - contentView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor, constant: 10), - ]) - textEditorViewContainerView.preservesSuperviewLayoutMargins = true - -// textEditorView.translatesAutoresizingMaskIntoConstraints = false -// textEditorViewContainerView.addSubview(textEditorView) -// NSLayoutConstraint.activate([ -// textEditorView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor), -// textEditorView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.leadingAnchor), -// textEditorView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.trailingAnchor), -// textEditorView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor), -// textEditorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), -// ]) -// textEditorView.setContentCompressionResistancePriority(.required - 2, for: .vertical) - - metaText.textView.translatesAutoresizingMaskIntoConstraints = false - textEditorViewContainerView.addSubview(metaText.textView) - NSLayoutConstraint.activate([ - metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor), - metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.leadingAnchor), - metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.readableContentGuide.trailingAnchor), - metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor), - metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 88).priority(.defaultHigh), - ]) - metaText.textView.setContentCompressionResistancePriority(.required - 2, for: .vertical) - - statusContentWarningEditorView.textView.delegate = self - //textEditorView.changeObserver = self - - statusContentWarningEditorView.isHidden = true - statusView.revealContentWarningButton.isHidden = true - } - -} - -// MARK: - TextEditorViewChangeObserver -//extension ComposeStatusContentCollectionViewCell: TextEditorViewChangeObserver { -// func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) { -// defer { -// textEditorViewChangeObserver?.textEditorView(textEditorView, didChangeWithChangeResult: changeResult) -// } -// -// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textEditorView.text) -// guard changeResult.isTextChanged else { return } -// composeContent.send(textEditorView.text) -// } -//} - -// MARK: - UITextViewDelegate -extension ComposeStatusContentCollectionViewCell: UITextViewDelegate { - - func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - if textView === statusContentWarningEditorView.textView { - // disable input line break - guard text != "\n" else { return false } - } - return true - } - - func textViewDidChange(_ textView: UITextView) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: text: %s", ((#file as NSString).lastPathComponent), #line, #function, textView.text) - guard textView === statusContentWarningEditorView.textView else { return } - // replace line break with space - textView.text = textView.text.replacingOccurrences(of: "\n", with: " ") - contentWarningContent.send(textView.text) - } - -} diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift index 4ef0dbe5a..e4569356f 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift @@ -10,7 +10,7 @@ import UIKit import Combine protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject { - func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption) + func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) } final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell { @@ -41,7 +41,7 @@ final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCe extension ComposeStatusPollExpiresOptionCollectionViewCell { - private typealias ExpiresOption = ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption + private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption private func _init() { durationButton.translatesAutoresizingMaskIntoConstraints = false diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift index 61753a4c2..30d5986ab 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerHeaderCollectionReusableView.swift @@ -9,7 +9,7 @@ import UIKit final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableView { - let titlelabel: UILabel = { + 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 @@ -30,13 +30,13 @@ final class CustomEmojiPickerHeaderCollectionReusableView: UICollectionReusableV extension CustomEmojiPickerHeaderCollectionReusableView { private func _init() { - titlelabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(titlelabel) + 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), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor), ]) } } diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift index 49e6c1fe2..7e305dbb0 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift @@ -6,10 +6,13 @@ // import UIKit +import Nuke final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell { static let itemSize = CGSize(width: 44, height: 44) + + var imageTask: ImageTask? let emojiImageView: UIImageView = { let imageView = UIImageView() @@ -23,6 +26,12 @@ final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell { emojiImageView.alpha = isHighlighted ? 0.5 : 1.0 } } + + override func prepareForReuse() { + super.prepareForReuse() + imageTask?.cancel() + imageTask = nil + } override init(frame: CGRect) { super.init(frame: frame) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 2a8844cfa..0e3efede2 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -48,26 +48,12 @@ final class ComposeViewController: UIViewController, NeedsDependency { let barButtonItem = UIBarButtonItem(customView: publishButton) return barButtonItem }() - -// let collectionView: ComposeCollectionView = { -// let collectionViewLayout = ComposeViewController.createLayout() -// let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) -// collectionView.register(ComposeRepliedToStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self)) -// collectionView.register(ComposeStatusContentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self)) -// collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self)) -// collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self)) -// collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self)) -// collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self)) -// collectionView.backgroundColor = Asset.Scene.Compose.background.color -// collectionView.alwaysBounceVertical = true -// collectionView.keyboardDismissMode = .onDrag -// return collectionView -// }() let tableView: ComposeTableView = { let tableView = ComposeTableView() tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self)) tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self)) + tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self)) tableView.backgroundColor = Asset.Scene.Compose.background.color tableView.alwaysBounceVertical = true tableView.separatorStyle = .none @@ -174,15 +160,6 @@ extension ComposeViewController { tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - -// collectionView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(collectionView) -// NSLayoutConstraint.activate([ -// collectionView.topAnchor.constraint(equalTo: view.topAnchor), -// collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), -// ]) composeToolbarView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(composeToolbarView) @@ -210,7 +187,11 @@ extension ComposeViewController { tableView: tableView, metaTextDelegate: self, metaTextViewDelegate: self, - customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel + customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, + composeStatusAttachmentCollectionViewCellDelegate: self, + composeStatusPollOptionCollectionViewCellDelegate: self, + composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self, + composeStatusPollExpiresOptionCollectionViewCellDelegate: self ) viewModel.composeStatusAttribute.composeContent @@ -218,6 +199,7 @@ extension ComposeViewController { .receive(on: RunLoop.main) .sink { [weak self] _ in guard let self = self else { return } + guard self.view.window != nil else { return } UIView.performWithoutAnimation { self.tableView.beginUpdates() self.tableView.endUpdates() @@ -225,21 +207,6 @@ extension ComposeViewController { } .store(in: &disposeBag) -// collectionView.delegate = self -// viewModel.setupDiffableDataSource( -// for: collectionView, -// dependency: self, -// customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel, -// metaTextDelegate: self, -// metaTextViewDelegate: self, -// composeStatusAttachmentTableViewCellDelegate: self, -// composeStatusPollOptionCollectionViewCellDelegate: self, -// composeStatusNewPollOptionCollectionViewCellDelegate: self, -// composeStatusPollExpiresOptionCollectionViewCellDelegate: self -// ) -// let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:))) -// collectionView.addGestureRecognizer(longPressReorderGesture) - customEmojiPickerInputView.collectionView.delegate = self viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView viewModel.setupCustomEmojiPickerDiffableDataSource( @@ -273,8 +240,8 @@ extension ComposeViewController { // update keyboard background color guard isShow, state == .dock else { - self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin - self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin + self.tableView.contentInset.bottom = extraMargin + self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin if let superView = self.autoCompleteViewController.tableView.superview { let autoCompleteTableViewBottomInset: CGFloat = { @@ -319,8 +286,8 @@ extension ComposeViewController { return } - self.tableView.contentInset.bottom = padding - self.tableView.verticalScrollIndicatorInsets.bottom = padding + self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom + self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom UIView.animate(withDuration: 0.3) { self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height self.view.layoutIfNeeded() @@ -487,6 +454,12 @@ extension ComposeViewController { self.markTextEditorViewBecomeFirstResponser() } } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + viewModel.isViewAppeared = true + } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) @@ -526,56 +499,56 @@ extension ComposeViewController { viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView } -// private func pollOptionCollectionViewCell(of item: ComposeStatusItem) -> ComposeStatusPollOptionCollectionViewCell? { -// guard case .pollOption = item else { return nil } -// guard let diffableDataSource = viewModel.diffableDataSource else { return nil } -// guard let indexPath = diffableDataSource.indexPath(for: item), -// let cell = collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else { -// return nil -// } -// -// return cell -// } - -// private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { -// guard let diffableDataSource = viewModel.diffableDataSource else { return nil } -// let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll) -// let firstPollItem = items.first { item -> Bool in -// guard case .pollOption = item else { return false } -// return true -// } -// -// guard let item = firstPollItem else { -// return nil -// } -// -// return pollOptionCollectionViewCell(of: item) -// } - -// private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { -// guard let diffableDataSource = viewModel.diffableDataSource else { return nil } -// let items = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll) -// let lastPollItem = items.last { item -> Bool in -// guard case .pollOption = item else { return false } -// return true -// } -// -// guard let item = lastPollItem else { -// return nil -// } -// -// return pollOptionCollectionViewCell(of: item) -// } - -// private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() { -// guard let cell = firstPollOptionCollectionViewCell() else { return } -// cell.pollOptionView.optionTextField.becomeFirstResponder() -// } + private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? { + guard case .pollOption = item else { return nil } + guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } + guard let indexPath = dataSource.indexPath(for: item), + let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else { + return nil + } -// private func markLastPollOptionCollectionViewCellBecomeFirstResponser() { -// guard let cell = lastPollOptionCollectionViewCell() else { return } -// cell.pollOptionView.optionTextField.becomeFirstResponder() -// } + return cell + } + + private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { + guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } + let items = dataSource.snapshot().itemIdentifiers(inSection: .main) + let firstPollItem = items.first { item -> Bool in + guard case .pollOption = item else { return false } + return true + } + + guard let item = firstPollItem else { + return nil + } + + return pollOptionCollectionViewCell(of: item) + } + + private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { + guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } + let items = dataSource.snapshot().itemIdentifiers(inSection: .main) + let lastPollItem = items.last { item -> Bool in + guard case .pollOption = item else { return false } + return true + } + + guard let item = lastPollItem else { + return nil + } + + return pollOptionCollectionViewCell(of: item) + } + + private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() { + guard let cell = firstPollOptionCollectionViewCell() else { return } + cell.pollOptionView.optionTextField.becomeFirstResponder() + } + + private func markLastPollOptionCollectionViewCellBecomeFirstResponser() { + guard let cell = lastPollOptionCollectionViewCell() else { return } + cell.pollOptionView.optionTextField.becomeFirstResponder() + } private func showDismissConfirmAlertController() { let alertController = UIAlertController( @@ -652,43 +625,6 @@ extension ComposeViewController { dismiss(animated: true, completion: nil) } - // seealso: ComposeViewModel.setupDiffableDataSource(…) -// @objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) { -// switch(sender.state) { -// case .began: -// guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), -// let cell = collectionView.cellForItem(at: selectedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else { -// break -// } -// // check if pressing reorder bar no not -// let locationInCell = sender.location(in: cell) -// guard cell.reorderBarImageView.frame.contains(locationInCell) else { -// return -// } -// -// collectionView.beginInteractiveMovementForItem(at: selectedIndexPath) -// case .changed: -// guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), -// let diffableDataSource = viewModel.diffableDataSource else { -// break -// } -// guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath), -// case .pollOption = item else { -// collectionView.cancelInteractiveMovement() -// return -// } -// -// var position = sender.location(in: collectionView) -// position.x = collectionView.frame.width * 0.5 -// collectionView.updateInteractiveMovementTargetPosition(position) -// case .ended: -// collectionView.endInteractiveMovement() -// collectionView.reloadData() -// default: -// collectionView.cancelInteractiveMovement() -// } -// } - } // MARK: - MetaTextDelegate @@ -708,7 +644,7 @@ extension ComposeViewController: MetaTextDelegate { extension ComposeViewController: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { - if textView.tag == ComposeStatusContentCollectionViewCell.metaTextViewTag { + if textEditorView()?.textView === textView { // update model guard let metaText = textEditorView() else { return } let backedString = metaText.backedString @@ -857,7 +793,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate { } self.suffixedAttachmentViews.removeAll() - // set normal apperance + // set normal appearance let attributedString = NSMutableAttributedString(attributedString: attributedString) attributedString.removeAttribute(.suffixedAttachment, range: stringRange) attributedString.removeAttribute(.underlineStyle, range: stringRange) @@ -987,17 +923,17 @@ extension ComposeViewController: ComposeToolbarViewDelegate { // setup initial poll option if needs if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty { - viewModel.pollOptionAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()] + viewModel.pollOptionAttributes.value = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()] } -// if viewModel.isPollComposing.value { -// // Magic RunLoop -// DispatchQueue.main.async { -// self.markFirstPollOptionCollectionViewCellBecomeFirstResponser() -// } -// } else { -// markTextEditorViewBecomeFirstResponser() -// } + if viewModel.isPollComposing.value { + // Magic RunLoop + DispatchQueue.main.async { + self.markFirstPollOptionCollectionViewCellBecomeFirstResponser() + } + } else { + markTextEditorViewBecomeFirstResponser() + } } func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: UIButton) { @@ -1160,19 +1096,19 @@ extension ComposeViewController: UIDocumentPickerDelegate { extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate { func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) { -// guard let diffableDataSource = viewModel.diffableDataSource else { return } -// guard let indexPath = collectionView.indexPath(for: cell) else { return } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// guard case let .attachment(attachmentService) = item else { return } -// -// var attachmentServices = viewModel.attachmentServices.value -// guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } -// let removedItem = attachmentServices[index] -// attachmentServices.remove(at: index) -// viewModel.attachmentServices.value = attachmentServices -// -// // cancel task -// removedItem.disposeBag.removeAll() + guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return } + guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard case let .attachment(attachmentService) = item else { return } + + var attachmentServices = viewModel.attachmentServices.value + guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } + let removedItem = attachmentServices[index] + attachmentServices.remove(at: index) + viewModel.attachmentServices.value = attachmentServices + + // cancel task + removedItem.disposeBag.removeAll() } } @@ -1190,72 +1126,72 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega // handle delete backward event for poll option input func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) { -// guard (text ?? "").isEmpty else { return } -// guard let diffableDataSource = viewModel.diffableDataSource else { return } -// guard let indexPath = collectionView.indexPath(for: cell) else { return } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// guard case let .pollOption(attribute) = item else { return } -// -// var pollAttributes = viewModel.pollOptionAttributes.value -// guard let index = pollAttributes.firstIndex(of: attribute) else { return } -// -// // mark previous (fallback to next) item of removed middle poll option become first responder -// let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll) -// if let indexOfItem = pollItems.firstIndex(of: item), index > 0 { -// func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? { -// guard index > 0 else { return nil } -// let indexBeforeRemoved = pollItems.index(before: indexOfItem) -// let itemBeforeRemoved = pollItems[indexBeforeRemoved] -// return pollOptionCollectionViewCell(of: itemBeforeRemoved) -// } -// -// func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? { -// guard index < pollItems.count - 1 else { return nil } -// let indexAfterRemoved = pollItems.index(after: index) -// let itemAfterRemoved = pollItems[indexAfterRemoved] -// return pollOptionCollectionViewCell(of: itemAfterRemoved) -// } -// -// var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved() -// if cell == nil { -// cell = cellAfterRemoved() -// } -// cell?.pollOptionView.optionTextField.becomeFirstResponder() -// } -// -// guard pollAttributes.count > 2 else { -// return -// } -// pollAttributes.remove(at: index) -// -// // update data source -// viewModel.pollOptionAttributes.value = pollAttributes + guard (text ?? "").isEmpty else { return } + guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } + guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } + guard let item = dataSource.itemIdentifier(for: indexPath) else { return } + guard case let .pollOption(attribute) = item else { return } + + var pollAttributes = viewModel.pollOptionAttributes.value + guard let index = pollAttributes.firstIndex(of: attribute) else { return } + + // mark previous (fallback to next) item of removed middle poll option become first responder + let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main) + if let indexOfItem = pollItems.firstIndex(of: item), index > 0 { + func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? { + guard index > 0 else { return nil } + let indexBeforeRemoved = pollItems.index(before: indexOfItem) + let itemBeforeRemoved = pollItems[indexBeforeRemoved] + return pollOptionCollectionViewCell(of: itemBeforeRemoved) + } + + func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? { + guard index < pollItems.count - 1 else { return nil } + let indexAfterRemoved = pollItems.index(after: index) + let itemAfterRemoved = pollItems[indexAfterRemoved] + return pollOptionCollectionViewCell(of: itemAfterRemoved) + } + + var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved() + if cell == nil { + cell = cellAfterRemoved() + } + cell?.pollOptionView.optionTextField.becomeFirstResponder() + } + + guard pollAttributes.count > 2 else { + return + } + pollAttributes.remove(at: index) + + // update data source + viewModel.pollOptionAttributes.value = pollAttributes } // handle keyboard return event for poll option input func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) { -// guard let diffableDataSource = viewModel.diffableDataSource else { return } -// guard let indexPath = collectionView.indexPath(for: cell) else { return } -// let pollItems = diffableDataSource.snapshot().itemIdentifiers(inSection: .poll).filter { item in -// guard case .pollOption = item else { return false } -// return true -// } -// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } -// guard let index = pollItems.firstIndex(of: item) else { return } -// -// if index == pollItems.count - 1 { -// // is the last -// viewModel.createNewPollOptionIfPossible() -// DispatchQueue.main.async { -// self.markLastPollOptionCollectionViewCellBecomeFirstResponser() -// } -// } else { -// // not the last -// let indexAfter = pollItems.index(after: index) -// let itemAfter = pollItems[indexAfter] -// let cell = pollOptionCollectionViewCell(of: itemAfter) -// cell?.pollOptionView.optionTextField.becomeFirstResponder() -// } + guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } + guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } + let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in + guard case .pollOption = item else { return false } + return true + } + guard let item = dataSource.itemIdentifier(for: indexPath) else { return } + guard let index = pollItems.firstIndex(of: item) else { return } + + if index == pollItems.count - 1 { + // is the last + viewModel.createNewPollOptionIfPossible() + DispatchQueue.main.async { + self.markLastPollOptionCollectionViewCellBecomeFirstResponser() + } + } else { + // not the last + let indexAfter = pollItems.index(after: index) + let itemAfter = pollItems[indexAfter] + let cell = pollOptionCollectionViewCell(of: itemAfter) + cell?.pollOptionView.optionTextField.becomeFirstResponder() + } } } @@ -1264,15 +1200,15 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate { func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) { viewModel.createNewPollOptionIfPossible() -// DispatchQueue.main.async { -// self.markLastPollOptionCollectionViewCellBecomeFirstResponser() -// } + DispatchQueue.main.async { + self.markLastPollOptionCollectionViewCellBecomeFirstResponser() + } } } // MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate { - func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusItem.ComposePollExpiresOptionAttribute.ExpiresOption) { + func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) { viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption } } diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift index 936d70f0f..1999152d3 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift @@ -5,6 +5,7 @@ // Created by MainasuK Cirno on 2021-3-11. // +import os.log import UIKit import Combine import CoreDataStack @@ -19,185 +20,83 @@ extension ComposeViewModel { tableView: UITableView, metaTextDelegate: MetaTextDelegate, metaTextViewDelegate: UITextViewDelegate, - customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel - ) { - let dataSource = UITableViewDiffableDataSource(tableView: tableView) { [ - weak self, - weak metaTextDelegate, - weak metaTextViewDelegate, - weak customEmojiPickerInputViewModel - ] tableView, indexPath, item in - guard let self = self else { return UITableViewCell() } - let managedObjectContext = self.context.managedObjectContext - - switch item { - case .replyTo(let statusObjectID): - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell - managedObjectContext.performAndWait { - guard let replyTo = managedObjectContext.object(with: statusObjectID) as? Status else { - return - } - let status = replyTo.reblog ?? replyTo - - // set avatar - cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) - // set name username - cell.statusView.nameLabel.text = { - let author = status.author - return author.displayName.isEmpty ? author.username : author.displayName - }() - cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct - // set text - let content = MastodonContent(content: status.content, emojis: status.emojiMeta) - do { - let metaContent = try MastodonMetaContent.convert(document: content) - cell.statusView.contentMetaText.configure(content: metaContent) - } catch { - cell.statusView.contentMetaText.textView.text = " " - assertionFailure() - } - // set date - cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow - - cell.framePublisher - .assign(to: \.value, on: self.repliedToCellFrame) - .store(in: &cell.disposeBag) - } - return cell - case .input(let replyToStatusObjectID, let attribute): - let cell = self.composeStatusContentTableViewCell - // configure header - managedObjectContext.performAndWait { - guard let replyToStatusObjectID = replyToStatusObjectID, - let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else { - cell.statusView.headerContainerView.isHidden = true - return - } - cell.statusView.headerContainerView.isHidden = false - cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) - cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback) - } - // configure author - ComposeStatusSection.configureStatusContent(cell: cell, attribute: attribute) - // bind content warning - attribute.isContentWarningComposing - .receive(on: DispatchQueue.main) - .sink { [weak cell, weak tableView] isContentWarningComposing in - guard let cell = cell else { return } - guard let tableView = tableView else { return } - // self size input cell - //tableView. - cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing - cell.statusContentWarningEditorView.alpha = 0 - UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) { - cell.statusContentWarningEditorView.alpha = 1 - } completion: { _ in - // do nothing - } - } - .store(in: &cell.disposeBag) - cell.contentWarningContent - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak tableView] text in - guard let tableView = tableView else { return } - // self size input cell - UIView.performWithoutAnimation { - tableView.beginUpdates() - tableView.endUpdates() - } - // bind input data - attribute.contentWarningContent.value = text - } - .store(in: &cell.disposeBag) - // configure custom emoji picker - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag) - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag) - // setup delegate - cell.metaText.delegate = metaTextDelegate - cell.metaText.textView.delegate = metaTextViewDelegate - - return cell - case .attachment(let attachmentService): - return UITableViewCell() - case .pollOption, .pollOptionAppendEntry, .pollExpiresOption: - return UITableViewCell() - } - } - self.dataSource = dataSource - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.repliedTo, .status, .attachment, .poll]) - switch composeKind { - case .reply(let statusObjectID): - snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo) - snapshot.appendItems([.input(replyToStatusObjectID: statusObjectID, attribute: composeStatusAttribute)], toSection: .repliedTo) - case .hashtag, .mention, .post: - snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status) - } - dataSource.apply(snapshot, animatingDifferences: false) - } - - func setupDiffableDataSource( - for collectionView: UICollectionView, - dependency: NeedsDependency, customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, - metaTextDelegate: MetaTextDelegate, - metaTextViewDelegate: UITextViewDelegate, - composeStatusAttachmentTableViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, + composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, - composeStatusNewPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, + composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate ) { - let diffableDataSource = ComposeStatusSection.collectionViewDiffableDataSource( - for: collectionView, - dependency: dependency, - managedObjectContext: context.managedObjectContext, - composeKind: composeKind, - repliedToCellFrameSubscriber: repliedToCellFrame, - customEmojiPickerInputViewModel: customEmojiPickerInputViewModel, - metaTextDelegate: metaTextDelegate, - metaTextViewDelegate: metaTextViewDelegate, - composeStatusAttachmentTableViewCellDelegate: composeStatusAttachmentTableViewCellDelegate, - composeStatusPollOptionCollectionViewCellDelegate: composeStatusPollOptionCollectionViewCellDelegate, - composeStatusNewPollOptionCollectionViewCellDelegate: composeStatusNewPollOptionCollectionViewCellDelegate, - composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate - ) + // content + composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate + composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate + // attachment + composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate + // poll + composeStatusPollTableViewCell.delegate = self + composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel + composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate + composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate + composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate - diffableDataSource.reorderingHandlers.canReorderItem = { item in - switch item { - case .pollOption: return true - default: return false + // setup data source + tableView.dataSource = self + + attachmentServices + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak self] attachmentServices in + guard let self = self else { return } + guard self.isViewAppeared else { return } + + let cell = self.composeStatusAttachmentTableViewCell + guard let dataSource = cell.dataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) } + snapshot.appendItems(items, toSection: .main) + + tableView.performBatchUpdates { + dataSource.apply(snapshot, animatingDifferences: true) + } completion: { _ in + // do nothing + } } - } - - // update reordered data source - diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in + .store(in: &disposeBag) + + Publishers.CombineLatest( + isPollComposing, + pollOptionAttributes + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] isPollComposing, pollOptionAttributes in guard let self = self else { return } - - let items = transaction.finalSnapshot.itemIdentifiers - var pollOptionAttributes: [ComposeStatusItem.ComposePollOptionAttribute] = [] - for item in items { - guard case let .pollOption(attribute) = item else { continue } - pollOptionAttributes.append(attribute) + guard self.isViewAppeared else { return } + + let cell = self.composeStatusPollTableViewCell + guard let dataSource = cell.dataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + var items: [ComposeStatusPollItem] = [] + if isPollComposing { + for attribute in pollOptionAttributes { + items.append(.pollOption(attribute: attribute)) + } + if pollOptionAttributes.count < 4 { + items.append(.pollOptionAppendEntry) + } + items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)) + } + snapshot.appendItems(items, toSection: .main) + + tableView.performBatchUpdates { + dataSource.apply(snapshot, animatingDifferences: true) + } completion: { _ in + // do nothing } - self.pollOptionAttributes.value = pollOptionAttributes } - - self.diffableDataSource = diffableDataSource - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.repliedTo, .status, .attachment, .poll]) - switch composeKind { - case .reply(let statusObjectID): - snapshot.appendItems([.replyTo(statusObjectID: statusObjectID)], toSection: .repliedTo) - snapshot.appendItems([.input(replyToStatusObjectID: statusObjectID, attribute: composeStatusAttribute)], toSection: .repliedTo) - case .hashtag, .mention, .post: - snapshot.appendItems([.input(replyToStatusObjectID: nil, attribute: composeStatusAttribute)], toSection: .status) - } - diffableDataSource.apply(snapshot, animatingDifferences: false) - - // some magic fix modal presentation animation issue - collectionView.dataSource = diffableDataSource + .store(in: &disposeBag) } func setupCustomEmojiPickerDiffableDataSource( @@ -246,3 +145,140 @@ extension ComposeViewModel { } } + +// MARK: - UITableViewDataSource +extension ComposeViewModel: UITableViewDataSource { + + enum Section: CaseIterable { + case repliedTo + case status + case attachment + case poll + } + + func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section.allCases[section] { + case .repliedTo: + switch composeKind { + case .reply: return 1 + default: return 0 + } + case .status: return 1 + case .attachment: + return 1 + case .poll: + return 1 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section.allCases[indexPath.section] { + case .repliedTo: + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell + guard case let .reply(statusObjectID) = composeKind else { return cell } + let managedObjectContext = context.managedObjectContext + managedObjectContext.performAndWait { + guard let replyTo = managedObjectContext.object(with: statusObjectID) as? Status else { + return + } + let status = replyTo.reblog ?? replyTo + + // set avatar + cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) + // set name username + cell.statusView.nameLabel.text = { + let author = status.author + return author.displayName.isEmpty ? author.username : author.displayName + }() + cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct + // set text + let content = MastodonContent(content: status.content, emojis: status.emojiMeta) + do { + let metaContent = try MastodonMetaContent.convert(document: content) + cell.statusView.contentMetaText.configure(content: metaContent) + } catch { + cell.statusView.contentMetaText.textView.text = " " + assertionFailure() + } + // set date + cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow + + cell.framePublisher + .assign(to: \.value, on: self.repliedToCellFrame) + .store(in: &cell.disposeBag) + } + return cell + case .status: + let cell = self.composeStatusContentTableViewCell + // configure header + let managedObjectContext = context.managedObjectContext + managedObjectContext.performAndWait { + guard case let .reply(replyToStatusObjectID) = self.composeKind, + let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else { + cell.statusView.headerContainerView.isHidden = true + return + } + cell.statusView.headerContainerView.isHidden = false + cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage) + cell.statusView.headerInfoLabel.text = L10n.Scene.Compose.replyingToUser(replyTo.author.displayNameWithFallback) + } + // configure author + ComposeStatusSection.configureStatusContent(cell: cell, attribute: composeStatusAttribute) + // bind content warning + composeStatusAttribute.isContentWarningComposing + .receive(on: DispatchQueue.main) + .sink { [weak cell, weak tableView] isContentWarningComposing in + guard let cell = cell else { return } + guard let tableView = tableView else { return } + // self size input cell + //tableView. + cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing + cell.statusContentWarningEditorView.alpha = 0 + UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) { + cell.statusContentWarningEditorView.alpha = 1 + } completion: { _ in + // do nothing + } + } + .store(in: &cell.disposeBag) + cell.contentWarningContent + .removeDuplicates() + .receive(on: DispatchQueue.main) + .sink { [weak tableView, weak self] text in + guard let tableView = tableView else { return } + guard let self = self else { return } + // self size input cell + UIView.performWithoutAnimation { + tableView.beginUpdates() + tableView.endUpdates() + } + // bind input data + self.composeStatusAttribute.contentWarningContent.value = text + } + .store(in: &cell.disposeBag) + // configure custom emoji picker + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.metaText.textView, disposeBag: &cell.disposeBag) + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag) + return cell + case .attachment: + let cell = self.composeStatusAttachmentTableViewCell + return cell + case .poll: + let cell = self.composeStatusPollTableViewCell + return cell + } + } +} + +// MARK: - ComposeStatusPollTableViewCellDelegate +extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate { + func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + self.pollOptionAttributes.value = options + } +} diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 1deb698f4..af8c8e2ac 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -13,7 +13,7 @@ import CoreDataStack import GameplayKit import MastodonSDK -final class ComposeViewModel { +final class ComposeViewModel: NSObject { static let composeContentLimit: Int = 500 @@ -33,11 +33,14 @@ final class ComposeViewModel { let repliedToCellFrame = CurrentValueSubject(.zero) let autoCompleteRetryLayoutTimes = CurrentValueSubject(0) let autoCompleteInfo = CurrentValueSubject(nil) + var isViewAppeared = false // output let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell() + let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell() + let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell() + var dataSource: UITableViewDiffableDataSource! - var diffableDataSource: UICollectionViewDiffableDataSource! var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource! private(set) lazy var publishStateMachine: GKStateMachine = { // exclude timeline middle fetcher state @@ -77,8 +80,8 @@ final class ComposeViewModel { let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([]) // polls - let pollOptionAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([]) - let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute() + let pollOptionAttributes = CurrentValueSubject<[ComposeStatusPollItem.PollOptionAttribute], Never>([]) + let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() init( context: AppContext, @@ -93,7 +96,9 @@ final class ComposeViewModel { self.selectedStatusVisibility = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value?.user.locked == true ? .private : .public) self.activeAuthentication = CurrentValueSubject(context.authenticationService.activeMastodonAuthentication.value) self.activeAuthenticationBox = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value) + super.init() // end init + switch composeKind { case .reply(let repliedToStatusObjectID): context.managedObjectContext.performAndWait { @@ -145,7 +150,7 @@ final class ComposeViewModel { case .post: self.preInsertedContent = nil } - + isCustomEmojiComposing .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing) .store(in: &disposeBag) @@ -284,45 +289,13 @@ final class ComposeViewModel { self.customEmojiViewModel.value = self.context.emojiService.dequeueCustomEmojiViewModel(for: domain) } .store(in: &disposeBag) - - // bind snapshot - Publishers.CombineLatest3( - attachmentServices.eraseToAnyPublisher(), - isPollComposing.eraseToAnyPublisher(), - pollOptionAttributes.eraseToAnyPublisher() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] attachmentServices, isPollComposing, pollAttributes in - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: trigger attachments upload…", ((#file as NSString).lastPathComponent), #line, #function) - guard let self = self else { return } - guard let diffableDataSource = self.diffableDataSource else { return } - var snapshot = diffableDataSource.snapshot() - - snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .attachment)) - var attachmentItems: [ComposeStatusItem] = [] - for attachmentService in attachmentServices { - let item = ComposeStatusItem.attachment(attachmentService: attachmentService) - attachmentItems.append(item) - } - snapshot.appendItems(attachmentItems, toSection: .attachment) - - snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .poll)) - if isPollComposing { - var pollItems: [ComposeStatusItem] = [] - for pollAttribute in pollAttributes { - let item = ComposeStatusItem.pollOption(attribute: pollAttribute) - pollItems.append(item) - } - snapshot.appendItems(pollItems, toSection: .poll) - if pollAttributes.count < 4 { - snapshot.appendItems([ComposeStatusItem.pollOptionAppendEntry], toSection: .poll) - } - snapshot.appendItems([ComposeStatusItem.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)], toSection: .poll) - } - - diffableDataSource.apply(snapshot) - + // setup attribute updater + Publishers.CombineLatest( + attachmentServices, + context.timestampUpdatePublisher + ) + .sink { attachmentServices, _ in // drive service upload state // make image upload in the queue for attachmentService in attachmentServices { @@ -395,7 +368,7 @@ extension ComposeViewModel { func createNewPollOptionIfPossible() { guard pollOptionAttributes.value.count < 4 else { return } - let attribute = ComposeStatusItem.ComposePollOptionAttribute() + let attribute = ComposeStatusPollItem.PollOptionAttribute() pollOptionAttributes.value = pollOptionAttributes.value + [attribute] } @@ -467,7 +440,7 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate { // MARK: - ComposePollAttributeDelegate extension ComposeViewModel: ComposePollAttributeDelegate { - func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) { + func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) { // trigger update pollOptionAttributes.value = pollOptionAttributes.value } diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift index 586895dd8..3e8d732f6 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift @@ -43,6 +43,7 @@ final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell { extension ComposeRepliedToStatusContentTableViewCell { private func _init() { + selectionStyle = .none backgroundColor = .clear statusView.actionToolbarContainer.isHidden = true diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift new file mode 100644 index 000000000..713fcc356 --- /dev/null +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift @@ -0,0 +1,155 @@ +// +// ComposeStatusAttachmentTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import UIKit +import Combine +import AlamofireImage + +final class ComposeStatusAttachmentTableViewCell: UITableViewCell { + + private(set) var dataSource: UICollectionViewDiffableDataSource! + weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate? + var observations = Set() + + private static func createLayout() -> UICollectionViewLayout { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.contentInsetsReference = .readableContent + return UICollectionViewCompositionalLayout(section: section) + } + + private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! + let collectionView: ComposeCollectionView = { + let collectionViewLayout = ComposeStatusAttachmentTableViewCell.createLayout() + let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self)) + collectionView.backgroundColor = Asset.Scene.Compose.background.color + collectionView.alwaysBounceVertical = true + collectionView.isScrollEnabled = false + return collectionView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ComposeStatusAttachmentTableViewCell { + + private func _init() { + collectionView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(collectionView) + collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 200).priority(.defaultHigh) + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + collectionViewHeightLayoutConstraint, + ]) + + collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in + guard let self = self else { return } + self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height + } + .store(in: &observations) + + self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [ + weak self + ] collectionView, indexPath, item -> UICollectionViewCell? in + guard let self = self else { return UICollectionViewCell() } + switch item { + case .attachment(let attachmentService): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell + cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value + cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate + attachmentService.thumbnailImage + .receive(on: DispatchQueue.main) + .sink { [weak cell] thumbnailImage in + guard let cell = cell else { return } + let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) + guard let image = thumbnailImage else { + let placeholder = UIImage.placeholder( + size: size, + color: Asset.Colors.Background.systemGroupedBackground.color + ) + .af.imageRounded( + withCornerRadius: AttachmentContainerView.containerViewCornerRadius + ) + cell.attachmentContainerView.previewImageView.image = placeholder + return + } + cell.attachmentContainerView.previewImageView.image = image + .af.imageAspectScaled(toFill: size) + .af.imageRounded(withCornerRadius: AttachmentContainerView.containerViewCornerRadius) + } + .store(in: &cell.disposeBag) + Publishers.CombineLatest( + attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(), + attachmentService.error.eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .sink { [weak cell, weak attachmentService] uploadState, error in + guard let cell = cell else { return } + guard let attachmentService = attachmentService else { return } + cell.attachmentContainerView.emptyStateView.isHidden = error == nil + cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil + if let error = error { + cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription + } else { + guard let uploadState = uploadState else { return } + switch uploadState { + case is MastodonAttachmentService.UploadState.Finish, + is MastodonAttachmentService.UploadState.Fail: + cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = { + if let file = attachmentService.file.value { + switch file { + case .jpeg, .png, .gif: + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + case .other: + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) + } + } else { + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + } + }() + default: + break + } + } + } + .store(in: &cell.disposeBag) + NotificationCenter.default.publisher( + for: UITextView.textDidChangeNotification, + object: cell.attachmentContainerView.descriptionTextView + ) + .receive(on: DispatchQueue.main) + .sink { notification in + guard let textField = notification.object as? UITextView else { return } + let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) + attachmentService.description.value = text + } + .store(in: &cell.disposeBag) + return cell + } + } + } + +} + diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift index cd3ede003..c9f6b758b 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift @@ -24,10 +24,10 @@ final class ComposeStatusContentTableViewCell: UITableViewCell { static let metaTextViewTag: Int = 333 let metaText: MetaText = { let metaText = MetaText() - metaText.textView.tag = ComposeStatusContentCollectionViewCell.metaTextViewTag metaText.textView.backgroundColor = .clear metaText.textView.isScrollEnabled = false metaText.textView.keyboardType = .twitter + metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) metaText.textView.attributedPlaceholder = { var attributes = metaText.textAttributes @@ -65,7 +65,7 @@ final class ComposeStatusContentTableViewCell: UITableViewCell { extension ComposeStatusContentTableViewCell { private func _init() { - // selectionStyle = .none + selectionStyle = .none layer.zPosition = 999 preservesSuperviewLayoutMargins = true diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift new file mode 100644 index 000000000..b39a86daf --- /dev/null +++ b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift @@ -0,0 +1,184 @@ +// +// ComposeStatusPollTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import UIKit + +protocol ComposeStatusPollTableViewCellDelegate: AnyObject { + func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) +} + +final class ComposeStatusPollTableViewCell: UITableViewCell { + + private(set) var dataSource: UICollectionViewDiffableDataSource! + var observations = Set() + + weak var customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel? + weak var delegate: ComposeStatusPollTableViewCellDelegate? + weak var composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate? + weak var composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate? + weak var composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? + + + private static func createLayout() -> UICollectionViewLayout { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.contentInsetsReference = .readableContent + return UICollectionViewCompositionalLayout(section: section) + } + + private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! + let collectionView: ComposeCollectionView = { + let collectionViewLayout = ComposeStatusPollTableViewCell.createLayout() + let collectionView = ComposeCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) + collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self)) + collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self)) + collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self)) + collectionView.backgroundColor = Asset.Scene.Compose.background.color + collectionView.alwaysBounceVertical = true + collectionView.isScrollEnabled = false + return collectionView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ComposeStatusPollTableViewCell { + + private func _init() { + collectionView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(collectionView) + collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 300).priority(.defaultHigh) + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + collectionViewHeightLayoutConstraint, + ]) + + let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeStatusPollTableViewCell.longPressReorderGestureHandler(_:))) + collectionView.addGestureRecognizer(longPressReorderGesture) + + collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in + guard let self = self else { return } + print(collectionView.contentSize) + self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height + } + .store(in: &observations) + + self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [ + weak self + ] collectionView, indexPath, item -> UICollectionViewCell? in + guard let self = self else { return UICollectionViewCell() } + + switch item { + case .pollOption(let attribute): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell + cell.pollOptionView.optionTextField.text = attribute.option.value + cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1) + cell.pollOption + .receive(on: DispatchQueue.main) + .assign(to: \.value, on: attribute.option) + .store(in: &cell.disposeBag) + cell.delegate = self.composeStatusPollOptionCollectionViewCellDelegate + if let customEmojiPickerInputViewModel = self.customEmojiPickerInputViewModel { + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) + } + return cell + case .pollOptionAppendEntry: + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell + cell.delegate = self.composeStatusPollOptionAppendEntryCollectionViewCellDelegate + return cell + case .pollExpiresOption(let attribute): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell + cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) + attribute.expiresOption + .receive(on: DispatchQueue.main) + .sink { [weak cell] expiresOption in + guard let cell = cell else { return } + cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) + } + .store(in: &cell.disposeBag) + cell.delegate = self.composeStatusPollExpiresOptionCollectionViewCellDelegate + return cell + } + } + + dataSource.reorderingHandlers.canReorderItem = { item in + switch item { + case .pollOption: return true + default: return false + } + } + + // update reordered data source + dataSource.reorderingHandlers.didReorder = { [weak self] transaction in + guard let self = self else { return } + + let items = transaction.finalSnapshot.itemIdentifiers + var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] + for item in items { + guard case let .pollOption(attribute) = item else { continue } + pollOptionAttributes.append(attribute) + } + self.delegate?.composeStatusPollTableViewCell(self, pollOptionAttributesDidReorder: pollOptionAttributes) + } + } + +} + +extension ComposeStatusPollTableViewCell { + + @objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) { + switch(sender.state) { + case .began: + guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), + let cell = collectionView.cellForItem(at: selectedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else { + break + } + // check if pressing reorder bar no not + let locationInCell = sender.location(in: cell) + guard cell.reorderBarImageView.frame.contains(locationInCell) else { + return + } + + collectionView.beginInteractiveMovementForItem(at: selectedIndexPath) + case .changed: + guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), + let dataSource = self.dataSource else { + break + } + guard let item = dataSource.itemIdentifier(for: selectedIndexPath), + case .pollOption = item else { + collectionView.cancelInteractiveMovement() + return + } + + var position = sender.location(in: collectionView) + position.x = collectionView.frame.width * 0.5 + collectionView.updateInteractiveMovementTargetPosition(position) + case .ended: + collectionView.endInteractiveMovement() + collectionView.reloadData() + default: + collectionView.cancelInteractiveMovement() + } + } + +} diff --git a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift index c4cf4f490..8516db569 100644 --- a/Mastodon/Scene/Share/View/Container/AudioContainerView.swift +++ b/Mastodon/Scene/Share/View/Container/AudioContainerView.swift @@ -98,15 +98,15 @@ extension AudioContainerView { NSLayoutConstraint.activate([ playButton.centerXAnchor.constraint(equalTo: playButtonBackgroundView.centerXAnchor), playButton.centerYAnchor.constraint(equalTo: playButtonBackgroundView.centerYAnchor), - playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32), - playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32), + playButtonBackgroundView.heightAnchor.constraint(equalToConstant: 32).priority(.required - 1), + playButtonBackgroundView.widthAnchor.constraint(equalToConstant: 32).priority(.required - 1), ]) container.addArrangedSubview(slider) container.addArrangedSubview(timeLabel) NSLayoutConstraint.activate([ - timeLabel.widthAnchor.constraint(equalToConstant: 40), + timeLabel.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1), ]) } }