From 72a225bbc36948f1d7b241738678c3b97fed50b6 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 21 Jul 2021 19:45:24 +0800 Subject: [PATCH] chore: drop Nuke --- Mastodon.xcodeproj/project.pbxproj | 71 +++---- .../xcschemes/xcschememanagement.plist | 6 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Compose/CustomEmojiPickerSection.swift | 12 +- .../Section/Status/NotificationSection.swift | 8 +- .../Section/Status/StatusSection.swift | 4 +- Mastodon/Extension/FLAnimatedImageView.swift | 81 +++++++ Mastodon/Extension/ImageTask.swift | 15 -- .../Protocol/AvatarConfigurableView.swift | 62 ++---- .../StatusProvider+UITableViewDelegate.swift | 8 +- .../Cell/AutoCompleteTableViewCell.swift | 6 +- ...tomEmojiPickerItemCollectionViewCell.swift | 10 +- .../Scene/Compose/ComposeViewController.swift | 1 - .../Compose/View/ReplicaStatusView.swift | 5 +- .../MediaPreviewViewController.swift | 78 +++---- .../MediaPreviewImageViewController.swift | 6 +- .../Image/MediaPreviewImageViewModel.swift | 29 +-- .../Button/NotificationAvatarButton.swift | 85 ++++++++ .../NotificationStatusTableViewCell.swift | 65 ++---- .../Header/View/ProfileHeaderView.swift | 5 +- .../SearchResultTableViewCell.swift | 34 ++- .../View/Button/AvatarBarButtonItem.swift | 50 ----- .../Share/View/Button/AvatarButton.swift | 129 ++++++++++++ .../AvatarStackContainerButton.swift | 29 +-- .../Scene/Share/View/Content/StatusView.swift | 32 ++- .../View/ImageView/AvatarImageView.swift | 11 + Mastodon/Service/APIService/APIService.swift | 5 - Mastodon/Service/PhotoLibraryService.swift | 198 ++++++++++++------ 28 files changed, 622 insertions(+), 427 deletions(-) create mode 100644 Mastodon/Extension/FLAnimatedImageView.swift delete mode 100644 Mastodon/Extension/ImageTask.swift create mode 100644 Mastodon/Scene/Notification/Button/NotificationAvatarButton.swift delete mode 100644 Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift create mode 100644 Mastodon/Scene/Share/View/Button/AvatarButton.swift rename Mastodon/Scene/Share/View/{Control => Button}/AvatarStackContainerButton.swift (84%) create mode 100644 Mastodon/Scene/Share/View/ImageView/AvatarImageView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b8d8b9f99..bbe751571 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -193,7 +193,9 @@ DB0C946526A6FD4D0088FB11 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB0C946426A6FD4D0088FB11 /* AlamofireImage */; }; DB0C946B26A700AB0088FB11 /* MastodonUser+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */; }; DB0C946C26A700CE0088FB11 /* MastodonUser+Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */; }; - DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */ = {isa = PBXBuildFile; productRef = DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */; }; + DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */; }; + DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947126A7D2D70088FB11 /* AvatarButton.swift */; }; + DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */; }; DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; }; DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; }; @@ -275,10 +277,8 @@ DB4F097F26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */; }; DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */; }; DB4FFC2C269EC39600D62E92 /* SearchTransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */; }; - DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; - DB52D33A26839DD800D43133 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB52D33926839DD800D43133 /* ImageTask.swift */; }; DB564BD0269F2F83001E39A7 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DB564BCE269F2F83001E39A7 /* Localizable.stringsdict */; }; DB564BD3269F3B35001E39A7 /* StatusFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */; }; DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; }; @@ -443,9 +443,10 @@ DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */; }; - DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = DBAEDE5E267A0B1500D25FF5 /* Nuke */; }; DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; + DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; + DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; @@ -892,6 +893,9 @@ DB084B5625CBC56C00F898ED /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = ""; }; DB0C946A26A700AB0088FB11 /* MastodonUser+Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonUser+Property.swift"; sourceTree = ""; }; + DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarImageView.swift; sourceTree = ""; }; + DB0C947126A7D2D70088FB11 /* AvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarButton.swift; sourceTree = ""; }; + DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = ""; }; DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = ""; }; @@ -980,10 +984,8 @@ DB4F097E26A03DA600D62E92 /* SearchHistoryFetchedResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHistoryFetchedResultController.swift; sourceTree = ""; }; DB4FFC29269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchToSearchDetailViewControllerAnimatedTransitioning.swift; sourceTree = ""; }; DB4FFC2A269EC39600D62E92 /* SearchTransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTransitionController.swift; sourceTree = ""; }; - DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; DB51D170262832380062B7A1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; DB51D171262832380062B7A1 /* BlurHashEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = ""; }; - DB52D33926839DD800D43133 /* ImageTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTask.swift; sourceTree = ""; }; DB564BCF269F2F83001E39A7 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = ""; }; DB564BD1269F2F8A001E39A7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; DB564BD2269F3B35001E39A7 /* StatusFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusFilterService.swift; sourceTree = ""; }; @@ -1143,6 +1145,7 @@ DBAEDE5B267A058D00D25FF5 /* BlurhashImageCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurhashImageCacheService.swift; sourceTree = ""; }; DBAEDE60267B342D00D25FF5 /* StatusContentCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentCacheService.swift; sourceTree = ""; }; DBAFB7342645463500371D5F /* Emojis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emojis.swift; sourceTree = ""; }; + DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = ""; }; DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = ""; }; DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = ""; }; DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = ""; }; @@ -1253,7 +1256,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DBAEDE5F267A0B1500D25FF5 /* Nuke in Frameworks */, DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB03F7ED268976B5007B274C /* MetaTextView in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, @@ -1266,7 +1268,6 @@ DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, - DB0E2D2E26833FF700865C3C /* NukeFLAnimatedImagePlugin in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, @@ -1544,7 +1545,8 @@ 2D42FF8325C82245004A627A /* Button */ = { isa = PBXGroup; children = ( - DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */, + DB0C947126A7D2D70088FB11 /* AvatarButton.swift */, + DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */, 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */, 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */, ); @@ -1695,6 +1697,7 @@ DB9D6C1325E4F97A0051B173 /* Container */, DBA9B90325F1D4420012E7B6 /* Control */, 2D152A8A25C295B8009AA50C /* Content */, + DB0C947026A7D2AB0088FB11 /* ImageView */, DB87D45C2609DE6600D12C0D /* TextField */, DB1D187125EF5BBD003F1F23 /* TableView */, 2D7631A625C1533800929FB9 /* TableviewCell */, @@ -1910,6 +1913,22 @@ path = CoreDataStack; sourceTree = ""; }; + DB0C947026A7D2AB0088FB11 /* ImageView */ = { + isa = PBXGroup; + children = ( + DB0C946E26A7D2A80088FB11 /* AvatarImageView.swift */, + ); + path = ImageView; + sourceTree = ""; + }; + DB0C947826A7FE950088FB11 /* Button */ = { + isa = PBXGroup; + children = ( + DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */, + ); + path = Button; + sourceTree = ""; + }; DB1D187125EF5BBD003F1F23 /* TableView */ = { isa = PBXGroup; children = ( @@ -2509,7 +2528,7 @@ DBCC3B35261440BA0045B23D /* UINavigationController.swift */, DB97131E2666078B00BD1E90 /* Date.swift */, DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */, - DB52D33926839DD800D43133 /* ImageTask.swift */, + DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */, ); path = Extension; sourceTree = ""; @@ -2568,13 +2587,14 @@ DB9D6BFD25E4F57B0051B173 /* Notification */ = { isa = PBXGroup; children = ( + DB0C947826A7FE950088FB11 /* Button */, + 2D35237F26256F470031AF25 /* TableViewCell */, DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */, DB63BE7E268DD1070011D3F9 /* NotificationViewController+StatusProvider.swift */, 2D607AD726242FC500B70763 /* NotificationViewModel.swift */, 2D084B8C26258EA3003AA3AF /* NotificationViewModel+Diffable.swift */, 2D084B9226259545003AA3AF /* NotificationViewModel+LoadLatestState.swift */, 2D24E12C2626FD2E00A59D4F /* NotificationViewModel+LoadOldestState.swift */, - 2D35237F26256F470031AF25 /* TableViewCell */, ); path = Notification; sourceTree = ""; @@ -2639,7 +2659,6 @@ DBA9B90325F1D4420012E7B6 /* Control */ = { isa = PBXGroup; children = ( - DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */, DB59F11725EFA35B001F1DAB /* StripProgressView.swift */, DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */, ); @@ -2976,12 +2995,10 @@ 2D939AC725EE14620076FA61 /* CropViewController */, DB9A487D2603456B008B817C /* UITextView+Placeholder */, DBB525072611EAC0002F1F29 /* Tabman */, - DBAEDE5E267A0B1500D25FF5 /* Nuke */, DBAC6482267D0B21007FE9FD /* DifferenceKit */, DBAC649D267DFE43007FE9FD /* DiffableDataSources */, DBAC64A0267E6D02007FE9FD /* Fuzi */, DBF7A0FB26830C33004176A2 /* FPSIndicator */, - DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */, DB03F7EA268976B5007B274C /* MastodonMeta */, DB03F7EC268976B5007B274C /* MetaTextView */, DBC6462A26A1738900B0E31B /* MastodonUI */, @@ -3204,7 +3221,6 @@ DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, - DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */, DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */, DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */, DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */, @@ -3540,6 +3556,7 @@ DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */, 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */, DBAEDE61267B342D00D25FF5 /* StatusContentCacheService.swift in Sources */, + DB0C946F26A7D2A80088FB11 /* AvatarImageView.swift in Sources */, 2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */, DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */, DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */, @@ -3569,6 +3586,7 @@ 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */, + DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */, 2DE0FAC82615F5F000CDF649 /* SearchRecommendAccountsCollectionViewCell.swift in Sources */, DBF1D251269DB01200C1C08A /* SearchHistoryViewController.swift in Sources */, 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, @@ -3607,6 +3625,7 @@ DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */, 2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */, DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */, + DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */, 5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */, DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */, DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, @@ -3664,7 +3683,6 @@ DB6180E02639194B0018D199 /* MediaPreviewPagingViewController.swift in Sources */, DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */, 2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */, - DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */, 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */, 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */, @@ -3676,7 +3694,6 @@ DBCBCC012680AF2A000F5B51 /* AsyncHomeTimelineViewModel+Diffable.swift in Sources */, DBCC3B36261440BA0045B23D /* UINavigationController.swift in Sources */, DBB525852612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift in Sources */, - DB52D33A26839DD800D43133 /* ImageTask.swift in Sources */, 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */, DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */, 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, @@ -3821,6 +3838,7 @@ DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, + DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DBBC24E026A54BCB00398BB9 /* MastodonField.swift in Sources */, DBFEF07B26A6BCE8006D7ED1 /* APIService+Status+Publish.swift in Sources */, @@ -4011,6 +4029,7 @@ DBBC24C826A5456400398BB9 /* ThemeService.swift in Sources */, DBBC24C926A5456400398BB9 /* MastodonTheme.swift in Sources */, DBFEF07C26A6BD0A006D7ED1 /* APIService+Status+Publish.swift in Sources */, + DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */, DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */, DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */, DBBC24C626A5456000398BB9 /* Theme.swift in Sources */, @@ -5402,14 +5421,6 @@ minimumVersion = 3.1.3; }; }; - DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kean/Nuke.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 10.3.0; - }; - }; DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/uias/Tabman"; @@ -5478,11 +5489,6 @@ package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB0E2D2D26833FF700865C3C /* NukeFLAnimatedImagePlugin */ = { - isa = XCSwiftPackageProductDependency; - package = DB0E2D2C26833FF600865C3C /* XCRemoteSwiftPackageReference "Nuke-FLAnimatedImage-Plugin" */; - productName = NukeFLAnimatedImagePlugin; - }; DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; @@ -5528,11 +5534,6 @@ package = DBAC649F267E6D01007FE9FD /* XCRemoteSwiftPackageReference "Fuzi" */; productName = Fuzi; }; - DBAEDE5E267A0B1500D25FF5 /* Nuke */ = { - isa = XCSwiftPackageProductDependency; - package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */; - productName = Nuke; - }; DBB525072611EAC0002F1F29 /* Tabman */ = { isa = XCSwiftPackageProductDependency; package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index ded3ecfad..238c933d4 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 - 29 + 21 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -37,12 +37,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 27 + 22 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 28 + 23 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 45f99fad9..55f7d7113 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire.git", "state": { "branch": null, - "revision": "f96b619bcb2383b43d898402283924b80e2c4bae", - "version": "5.4.3" + "revision": "4d19ad82f80cc71ff829b941ded114c56f4f604c", + "version": "5.4.2" } }, { diff --git a/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift index 57d7b6019..70de18c6b 100644 --- a/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift +++ b/Mastodon/Diffiable/Section/Compose/CustomEmojiPickerSection.swift @@ -6,7 +6,6 @@ // import UIKit -import Nuke enum CustomEmojiPickerSection: Equatable, Hashable { case emoji(name: String) @@ -24,14 +23,9 @@ 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.imageTask = Nuke.loadImage( - with: attribute.emoji.url, - options: .init( - placeholder: placeholder, - transition: .fadeIn(duration: 0.2) - ), - into: cell.emojiImageView - ) + + let url = URL(string: attribute.emoji.url) + cell.emojiImageView.setImage(url: url, placeholder: placeholder, scaleToSize: CustomEmojiPickerItemCollectionViewCell.itemSize) cell.accessibilityLabel = attribute.emoji.shortcode return cell } diff --git a/Mastodon/Diffiable/Section/Status/NotificationSection.swift b/Mastodon/Diffiable/Section/Status/NotificationSection.swift index 5769c912d..01229235b 100644 --- a/Mastodon/Diffiable/Section/Status/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/Status/NotificationSection.swift @@ -11,7 +11,6 @@ import CoreDataStack import Foundation import MastodonSDK import UIKit -import Nuke enum NotificationSection: Equatable, Hashable { case main @@ -56,17 +55,16 @@ extension NotificationSection { .af.imageAspectScaled(toFit: CGSize(width: 14, height: 14)) } - cell.actionImageView.image = createActionImage() + cell.avatarButton.badgeImageView.backgroundColor = notification.notificationType.color + cell.avatarButton.badgeImageView.image = createActionImage() cell.traitCollectionDidChange .receive(on: DispatchQueue.main) .sink { [weak cell] in guard let cell = cell else { return } - cell.actionImageView.image = createActionImage() + cell.avatarButton.badgeImageView.image = createActionImage() } .store(in: &cell.disposeBag) - cell.actionImageView.backgroundColor = notification.notificationType.color - // configure author name, notification description, timestamp cell.nameLabel.configure(content: notification.account.displayNameWithFallback, emojiDict: notification.account.emojiDict) let createAt = notification.createAt diff --git a/Mastodon/Diffiable/Section/Status/StatusSection.swift b/Mastodon/Diffiable/Section/Status/StatusSection.swift index e1823851a..18406da66 100644 --- a/Mastodon/Diffiable/Section/Status/StatusSection.swift +++ b/Mastodon/Diffiable/Section/Status/StatusSection.swift @@ -688,12 +688,12 @@ extension StatusSection { cell.statusView.usernameLabel.text = "@" + author.acct // avatar if let reblog = status.reblog { - cell.statusView.avatarImageView.isHidden = true + cell.statusView.avatarButton.isHidden = true cell.statusView.avatarStackedContainerButton.isHidden = false cell.statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: reblog.author.avatarImageURL())) cell.statusView.avatarStackedContainerButton.bottomTrailingAvatarStackedImageView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) } else { - cell.statusView.avatarImageView.isHidden = false + cell.statusView.avatarButton.isHidden = false cell.statusView.avatarStackedContainerButton.isHidden = true cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL())) } diff --git a/Mastodon/Extension/FLAnimatedImageView.swift b/Mastodon/Extension/FLAnimatedImageView.swift new file mode 100644 index 000000000..1e6e62ad8 --- /dev/null +++ b/Mastodon/Extension/FLAnimatedImageView.swift @@ -0,0 +1,81 @@ +// +// FLAnimatedImageView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-21. +// + +import Foundation +import Combine +import Alamofire +import AlamofireImage +import FLAnimatedImage + +private enum FLAnimatedImageViewAssociatedKeys { + static var activeAvatarRequestURL = "FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL" + static var avatarRequestCancellable = "FLAnimatedImageViewAssociatedKeys.avatarRequestCancellable" +} + +extension FLAnimatedImageView { + + var activeAvatarRequestURL: URL? { + get { + objc_getAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL) as? URL + } + set { + objc_setAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.activeAvatarRequestURL, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + var avatarRequestCancellable: AnyCancellable? { + get { + objc_getAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.avatarRequestCancellable) as? AnyCancellable + } + set { + objc_setAssociatedObject(self, &FLAnimatedImageViewAssociatedKeys.avatarRequestCancellable, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func setImage(url: URL?, placeholder: UIImage?, scaleToSize: CGSize?) { + // cancel task + activeAvatarRequestURL = nil + avatarRequestCancellable?.cancel() + + // set placeholder + image = placeholder + + // set image + guard let url = url else { return } + activeAvatarRequestURL = url + let avatarRequest = AF.request(url).publishData() + avatarRequestCancellable = avatarRequest + .sink { response in + switch response.result { + case .success(let data): + DispatchQueue.global().async { + let image: UIImage? = { + if let scaleToSize = scaleToSize { + return UIImage(data: data)?.af.imageScaled(to: scaleToSize, scale: UIScreen.main.scale) + } else { + return UIImage(data: data) + } + }() + let animatedImage = FLAnimatedImage(animatedGIFData: data) + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if self.activeAvatarRequestURL == url { + if let animatedImage = animatedImage { + self.animatedImage = animatedImage + } else { + self.image = image + } + } + } + } + case .failure: + break + } + } + } +} diff --git a/Mastodon/Extension/ImageTask.swift b/Mastodon/Extension/ImageTask.swift deleted file mode 100644 index 86be32d15..000000000 --- a/Mastodon/Extension/ImageTask.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ImageTask.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-6-24. -// - -import Foundation -import Nuke - -extension ImageTask { - func store(in set: inout Set) { - set.insert(self) - } -} diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift index 5807eed18..d771fa5a9 100644 --- a/Mastodon/Protocol/AvatarConfigurableView.swift +++ b/Mastodon/Protocol/AvatarConfigurableView.swift @@ -5,16 +5,16 @@ // Created by Cirno MainasuK on 2021-2-4. // +import Foundation import UIKit +import Combine import AlamofireImage import FLAnimatedImage -import Nuke protocol AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { get } static var configurableAvatarImageCornerRadius: CGFloat { get } - var configurableAvatarImageView: UIImageView? { get } - var configurableAvatarButton: UIButton? { get } + var configurableAvatarImageView: FLAnimatedImageView? { get } func configure(with configuration: AvatarConfigurableViewConfiguration) func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) } @@ -43,69 +43,31 @@ extension AvatarConfigurableView { } return placeholderImage }() - - // reset layer attributes - configurableAvatarImageView?.layer.masksToBounds = false - configurableAvatarImageView?.layer.cornerRadius = 0 - configurableAvatarImageView?.layer.cornerCurve = .circular - - configurableAvatarButton?.layer.masksToBounds = false - configurableAvatarButton?.layer.cornerRadius = 0 - configurableAvatarButton?.layer.cornerCurve = .circular // accessibility configurableAvatarImageView?.accessibilityIgnoresInvertColors = true - configurableAvatarButton?.accessibilityIgnoresInvertColors = true - + defer { avatarConfigurableView(self, didFinishConfiguration: configuration) } - guard let imageDisplayingView: ImageDisplayingView = configurableAvatarImageView ?? configurableAvatarButton?.imageView else { + guard let configurableAvatarImageView = configurableAvatarImageView else { return } // set corner radius (due to GIF won't crop) - imageDisplayingView.layer.masksToBounds = true - imageDisplayingView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius - imageDisplayingView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular + configurableAvatarImageView.layer.masksToBounds = true + configurableAvatarImageView.layer.cornerRadius = Self.configurableAvatarImageCornerRadius + configurableAvatarImageView.layer.cornerCurve = Self.configurableAvatarImageCornerRadius < Self.configurableAvatarImageSize.width * 0.5 ? .continuous :.circular // set border - configureLayerBorder(view: imageDisplayingView, configuration: configuration) + configureLayerBorder(view: configurableAvatarImageView, configuration: configuration) - - // set image - let url = configuration.avatarImageURL - let processors: [ImageProcessing] = [ - ImageProcessors.Resize( - size: Self.configurableAvatarImageSize, - unit: .points, - contentMode: .aspectFill, - crop: false - ), - ImageProcessors.RoundedCorners( - radius: Self.configurableAvatarImageCornerRadius - ) - ] - - let request = ImageRequest(url: url, processors: processors) - let options = ImageLoadingOptions( + configurableAvatarImageView.setImage( + url: configuration.avatarImageURL, placeholder: placeholderImage, - transition: .fadeIn(duration: 0.2) + scaleToSize: Self.configurableAvatarImageSize ) - - Nuke.loadImage( - with: request, - options: options, - into: imageDisplayingView - ) { result in - switch result { - case .failure: - break - case .success: - break - } - } } func configureLayerBorder(view: UIView, configuration: AvatarConfigurableViewConfiguration) { diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift index 77580296b..1abfcf70b 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift @@ -168,9 +168,9 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { guard let self = self else { return } self.attachment(of: status, index: i) .setFailureType(to: Error.self) - .compactMap { attachment -> AnyPublisher? in + .compactMap { attachment -> AnyPublisher? in guard let attachment = attachment, let url = URL(string: attachment.url) else { return nil } - return self.context.photoLibraryService.saveImage(url: url) + return self.context.photoLibraryService.save(imageSource: .url(url)) } .switchToLatest() .sink(receiveCompletion: { [weak self] completion in @@ -197,9 +197,9 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { guard let self = self else { return } self.attachment(of: status, index: i) .setFailureType(to: Error.self) - .compactMap { attachment -> AnyPublisher? in + .compactMap { attachment -> AnyPublisher? in guard let attachment = attachment, let url = URL(string: attachment.url) else { return nil } - return self.context.photoLibraryService.copyImage(url: url) + return self.context.photoLibraryService.copy(imageSource: .url(url)) } .switchToLatest() .sink(receiveCompletion: { completion in diff --git a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift index 65044f95f..8fa5d8644 100644 --- a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift +++ b/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift @@ -6,6 +6,7 @@ // import UIKit +import FLAnimatedImage final class AutoCompleteTableViewCell: UITableViewCell { @@ -27,7 +28,7 @@ final class AutoCompleteTableViewCell: UITableViewCell { return stackView }() - let avatarImageView = UIImageView() + let avatarImageView = FLAnimatedImageView() let titleLabel: UILabel = { let label = UILabel() @@ -129,8 +130,7 @@ extension AutoCompleteTableViewCell { extension AutoCompleteTableViewCell: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { avatarImageSize } static var configurableAvatarImageCornerRadius: CGFloat { avatarImageCornerRadius } - var configurableAvatarImageView: UIImageView? { avatarImageView } - var configurableAvatarButton: UIButton? { nil } + var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView } } #if canImport(SwiftUI) && DEBUG diff --git a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift index 7e305dbb0..d88b919f4 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/CustomEmojiPickerItemCollectionViewCell.swift @@ -6,16 +6,14 @@ // import UIKit -import Nuke +import FLAnimatedImage final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell { static let itemSize = CGSize(width: 44, height: 44) - var imageTask: ImageTask? - - let emojiImageView: UIImageView = { - let imageView = UIImageView() + let emojiImageView: FLAnimatedImageView = { + let imageView = FLAnimatedImageView() imageView.contentMode = .scaleAspectFit imageView.layer.masksToBounds = true return imageView @@ -29,8 +27,6 @@ final class CustomEmojiPickerItemCollectionViewCell: UICollectionViewCell { override func prepareForReuse() { super.prepareForReuse() - imageTask?.cancel() - imageTask = nil } override init(frame: CGRect) { diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index eb3b95c98..4ce020837 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -13,7 +13,6 @@ import MastodonSDK import MetaTextView import MastodonMeta import Meta -import Nuke import MastodonUI final class ComposeViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift index 9dde2f638..609e4bcc6 100644 --- a/Mastodon/Scene/Compose/View/ReplicaStatusView.swift +++ b/Mastodon/Scene/Compose/View/ReplicaStatusView.swift @@ -66,7 +66,7 @@ final class ReplicaStatusView: UIView { view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile return view }() - let avatarImageView: UIImageView = FLAnimatedImageView() + let avatarImageView = FLAnimatedImageView() let nameLabel: ActiveLabel = { let label = ActiveLabel(style: .statusName) @@ -250,6 +250,5 @@ extension ReplicaStatusView { extension ReplicaStatusView: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize } static var configurableAvatarImageCornerRadius: CGFloat { return 4 } - var configurableAvatarImageView: UIImageView? { avatarImageView } - var configurableAvatarButton: UIButton? { nil } + var configurableAvatarImageView: FLAnimatedImageView? { avatarImageView } } diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index 0f2d06b9b..29ddc206a 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -205,45 +205,51 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, contextMenuActionPerform action: MediaPreviewImageViewController.ContextMenuAction) { switch action { case .savePhoto: - switch viewController.viewModel.item { - case .status(let meta): - context.photoLibraryService.saveImage(url: meta.url) - .sink { [weak self] completion in - guard let self = self else { return } - switch completion { - case .failure(let error): - guard let error = error as? PhotoLibraryService.PhotoLibraryError, - case .noPermission = error else { return } - let alertController = SettingService.openSettingsAlertController(title: L10n.Common.Alerts.SavePhotoFailure.title, message: L10n.Common.Alerts.SavePhotoFailure.message) - self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) - case .finished: - break - } - } receiveValue: { _ in - // do nothing + let savePublisher: AnyPublisher = { + switch viewController.viewModel.item { + case .status(let meta): + return context.photoLibraryService.save(imageSource: .url(meta.url)) + case .local(let meta): + return context.photoLibraryService.save(imageSource: .image(meta.image)) + } + }() + savePublisher + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + guard let error = error as? PhotoLibraryService.PhotoLibraryError, + case .noPermission = error else { return } + let alertController = SettingService.openSettingsAlertController(title: L10n.Common.Alerts.SavePhotoFailure.title, message: L10n.Common.Alerts.SavePhotoFailure.message) + self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) + case .finished: + break } - .store(in: &context.disposeBag) - case .local(let meta): - context.photoLibraryService.save(image: meta.image, withNotificationFeedback: true) - } + } receiveValue: { _ in + // do nothing + } + .store(in: &context.disposeBag) case .copyPhoto: - switch viewController.viewModel.item { - case .status(let meta): - context.photoLibraryService.copyImage(url: meta.url) - .sink { completion in - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - case .finished: - break - } - } receiveValue: { _ in - // do nothing + let copyPublisher: AnyPublisher = { + switch viewController.viewModel.item { + case .status(let meta): + return context.photoLibraryService.copy(imageSource: .url(meta.url)) + case .local(let meta): + return context.photoLibraryService.copy(imageSource: .image(meta.image)) + } + }() + copyPublisher + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + break } - .store(in: &context.disposeBag) - case .local(let meta): - context.photoLibraryService.copy(image: meta.image, withNotificationFeedback: true) - } + } receiveValue: { _ in + // do nothing + } + .store(in: &context.disposeBag) case .share: let applicationActivities: [UIActivity] = [ SafariActivity(sceneCoordinator: self.coordinator) diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift index 960746b0f..03004028e 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewController.swift @@ -8,7 +8,6 @@ import os.log import UIKit import Combine -import Nuke protocol MediaPreviewImageViewControllerDelegate: AnyObject { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) @@ -91,11 +90,14 @@ extension MediaPreviewImageViewController { // } viewModel.image .receive(on: RunLoop.main) // use RunLoop prevent set image during zooming (TODO: handle transitioning state) - .sink { [weak self] image in + .sink { [weak self] image, animatedImage in guard let self = self else { return } guard let image = image else { return } self.previewImageView.imageView.image = image self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) + if let animatedImage = animatedImage { + self.previewImageView.imageView.animatedImage = animatedImage + } self.previewImageView.imageView.accessibilityLabel = self.viewModel.altText } .store(in: &disposeBag) diff --git a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift index a6163a8cf..f44a6a189 100644 --- a/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Paging/Image/MediaPreviewImageViewModel.swift @@ -8,7 +8,9 @@ import os.log import UIKit import Combine -import Nuke +import Alamofire +import AlamofireImage +import FLAnimatedImage class MediaPreviewImageViewModel { @@ -18,34 +20,35 @@ class MediaPreviewImageViewModel { let item: ImagePreviewItem // output - let image: CurrentValueSubject + let image: CurrentValueSubject<(UIImage?, FLAnimatedImage?), Never> let altText: String? init(meta: RemoteImagePreviewMeta) { self.item = .status(meta) - self.image = CurrentValueSubject(meta.thumbnail) + self.image = CurrentValueSubject((meta.thumbnail, nil)) self.altText = meta.altText let url = meta.url - - ImagePipeline.shared.imagePublisher(with: url) - .sink { completion in - switch completion { + AF.request(url).publishData() + .map { response in + switch response.result { + case .success(let data): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) + let image = UIImage(data: data, scale: UIScreen.main.scale) + let animatedImage = FLAnimatedImage(animatedGIFData: data) + return (image, animatedImage) case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) - case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) + return (nil, nil) } - } receiveValue: { [weak self] response in - guard let self = self else { return } - self.image.value = response.image } + .assign(to: \.value, on: image) .store(in: &disposeBag) } init(meta: LocalImagePreviewMeta) { self.item = .local(meta) - self.image = CurrentValueSubject(meta.image) + self.image = CurrentValueSubject((meta.image, nil)) self.altText = nil } diff --git a/Mastodon/Scene/Notification/Button/NotificationAvatarButton.swift b/Mastodon/Scene/Notification/Button/NotificationAvatarButton.swift new file mode 100644 index 000000000..6eafdd1dd --- /dev/null +++ b/Mastodon/Scene/Notification/Button/NotificationAvatarButton.swift @@ -0,0 +1,85 @@ +// +// NotificationAvatarButton.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-21. +// + +import UIKit +import FLAnimatedImage + +final class NotificationAvatarButton: AvatarButton { + + // Size fixed + static let containerSize = CGSize(width: 35, height: 35) + static let badgeImageViewSize = CGSize(width: 24, height: 24) + static let badgeImageMaskSize = CGSize(width: badgeImageViewSize.width + 4, height: badgeImageViewSize.height + 4) + + let badgeImageView: UIImageView = { + let imageView = RoundedImageView() + imageView.contentMode = .center + imageView.isOpaque = true + imageView.layer.shouldRasterize = true + imageView.layer.rasterizationScale = UIScreen.main.scale + return imageView + }() + + override func _init() { + super._init() + + avatarImageSize = CGSize(width: 35, height: 35) + + let path: CGPath = { + let path = CGMutablePath() + path.addRect(CGRect(origin: .zero, size: NotificationAvatarButton.containerSize)) + let x: CGFloat = { + if UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft { + return -0.5 * NotificationAvatarButton.badgeImageMaskSize.width + } else { + return NotificationAvatarButton.containerSize.width - 0.5 * NotificationAvatarButton.badgeImageMaskSize.width + } + }() + path.addPath(UIBezierPath( + ovalIn: CGRect( + x: x, + y: NotificationAvatarButton.containerSize.height - 0.5 * NotificationAvatarButton.badgeImageMaskSize.width, + width: NotificationAvatarButton.badgeImageMaskSize.width, + height: NotificationAvatarButton.badgeImageMaskSize.height + ) + ).cgPath) + return path + }() + + let maskShapeLayer = CAShapeLayer() + maskShapeLayer.backgroundColor = UIColor.black.cgColor + maskShapeLayer.fillRule = .evenOdd + maskShapeLayer.path = path + avatarImageView.layer.mask = maskShapeLayer + + badgeImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(badgeImageView) + NSLayoutConstraint.activate([ + badgeImageView.centerXAnchor.constraint(equalTo: trailingAnchor), + badgeImageView.centerYAnchor.constraint(equalTo: bottomAnchor), + badgeImageView.widthAnchor.constraint(equalToConstant: NotificationAvatarButton.badgeImageViewSize.width).priority(.required - 1), + badgeImageView.heightAnchor.constraint(equalToConstant: NotificationAvatarButton.badgeImageViewSize.height).priority(.required - 1), + ]) + } + + override func updateAppearance() { + super.updateAppearance() + badgeImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0 + } + +} + +final class RoundedImageView: UIImageView { + + override func layoutSubviews() { + super.layoutSubviews() + + layer.masksToBounds = true + layer.cornerRadius = bounds.width / 2 + layer.cornerCurve = .circular + } +} diff --git a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift index 6042c0bbd..ab7b6a518 100644 --- a/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift +++ b/Mastodon/Scene/Notification/TableViewCell/NotificationStatusTableViewCell.swift @@ -14,7 +14,6 @@ import ActiveLabel import MetaTextView import Meta import FLAnimatedImage -import Nuke protocol NotificationTableViewCellDelegate: AnyObject { var context: AppContext! { get } @@ -46,31 +45,8 @@ final class NotificationStatusTableViewCell: UITableViewCell, StatusCell { var containerStackViewBottomLayoutConstraint: NSLayoutConstraint! let containerStackView = UIStackView() - let avatarImageView: UIImageView = { - let imageView = FLAnimatedImageView() - return imageView - }() - - + let avatarButton = NotificationAvatarButton() let traitCollectionDidChange = PassthroughSubject() - - let actionImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .center - imageView.isOpaque = true - imageView.layer.masksToBounds = true - imageView.layer.cornerRadius = NotificationStatusTableViewCell.actionImageViewSize.width * 0.5 - imageView.layer.cornerCurve = .circular - imageView.layer.borderWidth = NotificationStatusTableViewCell.actionImageBorderWidth - imageView.layer.shouldRasterize = true - imageView.layer.rasterizationScale = UIScreen.main.scale - return imageView - }() - - let avatarContainer: UIView = { - let view = UIView() - return view - }() let contentStackView = UIStackView() @@ -181,25 +157,11 @@ extension NotificationStatusTableViewCell { containerStackViewBottomLayoutConstraint.priority(.required - 1), ]) - containerStackView.addArrangedSubview(avatarContainer) - avatarImageView.translatesAutoresizingMaskIntoConstraints = false - avatarContainer.addSubview(avatarImageView) + avatarButton.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(avatarButton) NSLayoutConstraint.activate([ - avatarImageView.topAnchor.constraint(equalTo: avatarContainer.topAnchor), - avatarImageView.leadingAnchor.constraint(equalTo: avatarContainer.leadingAnchor), - avatarImageView.trailingAnchor.constraint(equalTo: avatarContainer.trailingAnchor), - avatarImageView.bottomAnchor.constraint(equalTo: avatarContainer.bottomAnchor), - avatarImageView.heightAnchor.constraint(equalToConstant: 35).priority(.required - 1), - avatarImageView.widthAnchor.constraint(equalToConstant: 35).priority(.required - 1), - ]) - - actionImageView.translatesAutoresizingMaskIntoConstraints = false - avatarContainer.addSubview(actionImageView) - NSLayoutConstraint.activate([ - actionImageView.centerYAnchor.constraint(equalTo: avatarContainer.bottomAnchor), - actionImageView.centerXAnchor.constraint(equalTo: avatarContainer.trailingAnchor), - actionImageView.widthAnchor.constraint(equalToConstant: NotificationStatusTableViewCell.actionImageViewSize.width).priority(.required - 1), - actionImageView.heightAnchor.constraint(equalTo: actionImageView.widthAnchor, multiplier: 1.0), + avatarButton.heightAnchor.constraint(equalToConstant: NotificationAvatarButton.containerSize.width).priority(.required - 1), + avatarButton.widthAnchor.constraint(equalToConstant: NotificationAvatarButton.containerSize.height).priority(.required - 1), ]) containerStackView.addArrangedSubview(contentStackView) @@ -274,10 +236,8 @@ extension NotificationStatusTableViewCell { filteredLabel.isHidden = true statusView.delegate = self - - let avatarImageViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - avatarImageViewTapGestureRecognizer.addTarget(self, action: #selector(NotificationStatusTableViewCell.avatarImageViewTapGestureRecognizerHandler(_:))) - avatarImageView.addGestureRecognizer(avatarImageViewTapGestureRecognizer) + + avatarButton.addTarget(self, action: #selector(NotificationStatusTableViewCell.avatarButtonDidPressed(_:)), for: .touchUpInside) let authorNameLabelTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer authorNameLabelTapGestureRecognizer.addTarget(self, action: #selector(NotificationStatusTableViewCell.authorNameLabelTapGestureRecognizerHandler(_:))) nameLabel.addGestureRecognizer(authorNameLabelTapGestureRecognizer) @@ -312,8 +272,8 @@ extension NotificationStatusTableViewCell { extension NotificationStatusTableViewCell { private func setupBackgroundColor(theme: Theme) { - actionImageView.layer.borderColor = theme.systemBackgroundColor.cgColor - avatarImageView.layer.borderColor = Asset.Theme.Mastodon.systemBackground.color.cgColor +// actionImageView.layer.borderColor = theme.systemBackgroundColor.cgColor +// avatarImageView.layer.borderColor = Asset.Theme.Mastodon.systemBackground.color.cgColor statusContainerView.layer.borderColor = Asset.Colors.Border.notificationStatus.color.cgColor statusContainerView.backgroundColor = UIColor(dynamicProvider: { traitCollection in return traitCollection.userInterfaceStyle == .light ? theme.systemBackgroundColor : theme.tertiarySystemGroupedBackgroundColor @@ -323,9 +283,9 @@ extension NotificationStatusTableViewCell { } extension NotificationStatusTableViewCell { - @objc private func avatarImageViewTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + @objc private func avatarButtonDidPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.notificationStatusTableViewCell(self, avatarImageViewDidPressed: avatarImageView) + delegate?.notificationStatusTableViewCell(self, avatarImageViewDidPressed: avatarButton.avatarImageView) } @objc private func authorNameLabelTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { @@ -408,6 +368,5 @@ extension NotificationStatusTableViewCell { extension NotificationStatusTableViewCell: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { CGSize(width: 35, height: 35) } static var configurableAvatarImageCornerRadius: CGFloat { 4 } - var configurableAvatarImageView: UIImageView? { avatarImageView } - var configurableAvatarButton: UIButton? { nil } + var configurableAvatarImageView: FLAnimatedImageView? { avatarButton.avatarImageView } } diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 86975a8de..95a91b994 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -73,7 +73,7 @@ final class ProfileHeaderView: UIView { return view }() - let avatarImageView: UIImageView = { + let avatarImageView: FLAnimatedImageView = { let imageView = FLAnimatedImageView() let placeholderImage = UIImage .placeholder(size: ProfileHeaderView.avatarImageViewSize, color: Asset.Theme.Mastodon.systemGroupedBackground.color) @@ -559,8 +559,7 @@ extension ProfileHeaderView: ProfileStatusDashboardViewDelegate { extension ProfileHeaderView: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { avatarImageViewSize } static var configurableAvatarImageCornerRadius: CGFloat { avatarImageViewCornerRadius } - var configurableAvatarImageView: UIImageView? { return avatarImageView } - var configurableAvatarButton: UIButton? { return nil } + var configurableAvatarImageView: FLAnimatedImageView? { return avatarImageView } } diff --git a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift index 8223c6ddf..1b48d1d2d 100644 --- a/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift +++ b/Mastodon/Scene/Search/SearchDetail/TableViewCell/SearchResultTableViewCell.swift @@ -11,12 +11,11 @@ import Foundation import MastodonSDK import UIKit import FLAnimatedImage -import Nuke final class SearchResultTableViewCell: UITableViewCell { - let _imageView: UIImageView = { - let imageView = FLAnimatedImageView() + let _imageView: AvatarImageView = { + let imageView = AvatarImageView() imageView.tintColor = Asset.Colors.Label.primary.color imageView.layer.cornerRadius = 4 imageView.clipsToBounds = true @@ -48,7 +47,7 @@ final class SearchResultTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - Nuke.cancelRequest(for: _imageView) + _imageView.af.cancelImageRequest() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -155,32 +154,19 @@ extension SearchResultTableViewCell { extension SearchResultTableViewCell { func config(with account: Mastodon.Entity.Account) { - Nuke.loadImage( - with: account.avatarImageURL(), - options: ImageLoadingOptions( - placeholder: UIImage.placeholder(color: .systemFill), - transition: .fadeIn(duration: 0.2) - ), - into: _imageView - ) + configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL())) _titleLabel.text = account.displayName.isEmpty ? account.username : account.displayName _subTitleLabel.text = account.acct } func config(with account: MastodonUser) { - Nuke.loadImage( - with: account.avatarImageURL(), - options: ImageLoadingOptions( - placeholder: UIImage.placeholder(color: .systemFill), - transition: .fadeIn(duration: 0.2) - ), - into: _imageView - ) + configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: account.avatarImageURL())) _titleLabel.text = account.displayNameWithFallback _subTitleLabel.text = account.acct } func config(with tag: Mastodon.Entity.Tag) { + configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: nil)) let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) _imageView.image = image _titleLabel.text = "#" + tag.name @@ -195,6 +181,7 @@ extension SearchResultTableViewCell { } func config(with tag: Tag) { + configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: nil)) let image = UIImage(systemName: "number.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 34, weight: .regular))!.withRenderingMode(.alwaysTemplate) _imageView.image = image _titleLabel.text = "# " + tag.name @@ -211,6 +198,13 @@ extension SearchResultTableViewCell { } } +// MARK: - AvatarStackedImageView +extension SearchResultTableViewCell: AvatarConfigurableView { + static var configurableAvatarImageSize: CGSize { CGSize(width: 42, height: 42) } + static var configurableAvatarImageCornerRadius: CGFloat { 4 } + var configurableAvatarImageView: FLAnimatedImageView? { _imageView } +} + #if canImport(SwiftUI) && DEBUG import SwiftUI diff --git a/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift b/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift deleted file mode 100644 index 254403bde..000000000 --- a/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// AvatarBarButtonItem.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021-2-4. -// - -import UIKit - -final class AvatarBarButtonItem: UIBarButtonItem { - - static let avatarButtonSize = CGSize(width: 32, height: 32) - - let avatarButton: UIButton = { - let button = UIButton(type: .custom) - button.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - button.widthAnchor.constraint(equalToConstant: avatarButtonSize.width).priority(.defaultHigh), - button.heightAnchor.constraint(equalToConstant: avatarButtonSize.height).priority(.defaultHigh), - ]) - return button - }() - - override init() { - super.init() - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension AvatarBarButtonItem { - - private func _init() { - customView = avatarButton - } - -} - -extension AvatarBarButtonItem: AvatarConfigurableView { - static var configurableAvatarImageSize: CGSize { return avatarButtonSize } - static var configurableAvatarImageCornerRadius: CGFloat { return 4 } - var configurableAvatarImageView: UIImageView? { return nil } - var configurableAvatarButton: UIButton? { return avatarButton } - var configurableVerifiedBadgeImageView: UIImageView? { return nil } -} diff --git a/Mastodon/Scene/Share/View/Button/AvatarButton.swift b/Mastodon/Scene/Share/View/Button/AvatarButton.swift new file mode 100644 index 000000000..a8f7212ae --- /dev/null +++ b/Mastodon/Scene/Share/View/Button/AvatarButton.swift @@ -0,0 +1,129 @@ +// +// AvatarButton.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-21. +// + +import os.log +import UIKit + +class AvatarButton: UIControl { + + // UIControl.Event - Application: 0x0F000000 + static let primaryAction = UIControl.Event(rawValue: 1 << 25) // 0x01000000 + var primaryActionState: UIControl.State = .normal + + var avatarImageSize = CGSize(width: 42, height: 42) + let avatarImageView = AvatarImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + func _init() { + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(avatarImageView) + NSLayoutConstraint.activate([ + avatarImageView.topAnchor.constraint(equalTo: topAnchor), + avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor), + avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor), + avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + func updateAppearance() { + avatarImageView.alpha = primaryActionState.contains(.highlighted) ? 0.6 : 1.0 + } + +} + +extension AvatarButton { + + override var intrinsicContentSize: CGSize { + return avatarImageSize + } + + override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + defer { updateAppearance() } + + updateState(touch: touch, event: event) + return super.beginTracking(touch, with: event) + } + + override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + defer { updateAppearance() } + + updateState(touch: touch, event: event) + return super.continueTracking(touch, with: event) + } + + override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + defer { updateAppearance() } + resetState() + + if let touch = touch { + if AvatarButton.isTouching(touch, view: self, event: event) { + sendActions(for: AvatarButton.primaryAction) + } else { + // do nothing + } + } + + super.endTracking(touch, with: event) + } + + override func cancelTracking(with event: UIEvent?) { + defer { updateAppearance() } + + resetState() + super.cancelTracking(with: event) + } + +} + +extension AvatarButton { + + private static func isTouching(_ touch: UITouch, view: UIView, event: UIEvent?) -> Bool { + let location = touch.location(in: view) + return view.point(inside: location, with: event) + } + + private func resetState() { + primaryActionState = .normal + } + + private func updateState(touch: UITouch, event: UIEvent?) { + primaryActionState = AvatarButton.isTouching(touch, view: self, event: event) ? .highlighted : .normal + } + +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct AvatarButton_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 42) { + let avatarButton = AvatarButton() + avatarButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + avatarButton.widthAnchor.constraint(equalToConstant: 42), + avatarButton.heightAnchor.constraint(equalToConstant: 42), + ]) + return avatarButton + } + .previewLayout(.fixed(width: 42, height: 42)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift b/Mastodon/Scene/Share/View/Button/AvatarStackContainerButton.swift similarity index 84% rename from Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift rename to Mastodon/Scene/Share/View/Button/AvatarStackContainerButton.swift index 83b66454b..6c2d00e3c 100644 --- a/Mastodon/Scene/Share/View/Control/AvatarStackContainerButton.swift +++ b/Mastodon/Scene/Share/View/Button/AvatarStackContainerButton.swift @@ -9,19 +9,20 @@ import os.log import UIKit import FLAnimatedImage -final class AvatarStackedImageView: FLAnimatedImageView { } +final class AvatarStackedImageView: AvatarImageView { } // MARK: - AvatarConfigurableView extension AvatarStackedImageView: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { CGSize(width: 28, height: 28) } static var configurableAvatarImageCornerRadius: CGFloat { 4 } - var configurableAvatarImageView: UIImageView? { self } - var configurableAvatarButton: UIButton? { nil } + var configurableAvatarImageView: FLAnimatedImageView? { self } } final class AvatarStackContainerButton: UIControl { static let containerSize = CGSize(width: 42, height: 42) + static let avatarImageViewSize = CGSize(width: 28, height: 28) + static let avatarImageViewCornerRadius: CGFloat = 4 static let maskOffset: CGFloat = 2 // UIControl.Event - Application: 0x0F000000 @@ -46,13 +47,6 @@ final class AvatarStackContainerButton: UIControl { extension AvatarStackContainerButton { private func _init() { - // GIF get worse when enable rasterize -// topLeadingAvatarStackedImageView.layer.shouldRasterize = true -// topLeadingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale -// -// bottomTrailingAvatarStackedImageView.layer.shouldRasterize = true -// bottomTrailingAvatarStackedImageView.layer.rasterizationScale = UIScreen.main.scale - topLeadingAvatarStackedImageView.translatesAutoresizingMaskIntoConstraints = false addSubview(topLeadingAvatarStackedImageView) NSLayoutConstraint.activate([ @@ -75,16 +69,16 @@ extension AvatarStackContainerButton { let offset: CGFloat = 2 let path: CGPath = { let path = CGMutablePath() - path.addRect(CGRect(origin: .zero, size: AvatarStackedImageView.configurableAvatarImageSize)) + path.addRect(CGRect(origin: .zero, size: AvatarStackContainerButton.avatarImageViewSize)) let mirrorScale: CGFloat = UIApplication.shared.userInterfaceLayoutDirection == .rightToLeft ? -1 : 1 path.addPath(UIBezierPath( roundedRect: CGRect( - x: mirrorScale * (AvatarStackContainerButton.containerSize.width - AvatarStackedImageView.configurableAvatarImageSize.width - offset), - y: AvatarStackContainerButton.containerSize.height - AvatarStackedImageView.configurableAvatarImageSize.height - offset, - width: AvatarStackedImageView.configurableAvatarImageSize.width, - height: AvatarStackedImageView.configurableAvatarImageSize.height + x: mirrorScale * (AvatarStackContainerButton.containerSize.width - AvatarStackContainerButton.avatarImageViewSize.width - offset), + y: AvatarStackContainerButton.containerSize.height - AvatarStackContainerButton.avatarImageViewSize.height - offset, + width: AvatarStackContainerButton.avatarImageViewSize.width, + height: AvatarStackContainerButton.avatarImageViewSize.height ), - cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius + cornerRadius: AvatarStackedImageView.configurableAvatarImageCornerRadius + 1 // 1pt overshoot ).cgPath) return path }() @@ -93,9 +87,6 @@ extension AvatarStackContainerButton { maskShapeLayer.fillRule = .evenOdd maskShapeLayer.path = path topLeadingAvatarStackedImageView.layer.mask = maskShapeLayer - - topLeadingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill) - bottomTrailingAvatarStackedImageView.image = UIImage.placeholder(color: .systemFill) } override var intrinsicContentSize: CGSize { diff --git a/Mastodon/Scene/Share/View/Content/StatusView.swift b/Mastodon/Scene/Share/View/Content/StatusView.swift index e596b39e4..a12a7bd28 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView.swift +++ b/Mastodon/Scene/Share/View/Content/StatusView.swift @@ -95,10 +95,7 @@ final class StatusView: UIView { view.accessibilityLabel = L10n.Common.Controls.Status.showUserProfile return view }() - let avatarImageView: FLAnimatedImageView = { - let imageView = FLAnimatedImageView() - return imageView - }() + let avatarButton = AvatarButton() let avatarStackedContainerButton: AvatarStackContainerButton = AvatarStackContainerButton() let nameLabel: ActiveLabel = { @@ -317,13 +314,13 @@ extension StatusView { avatarView.widthAnchor.constraint(equalToConstant: StatusView.avatarImageSize.width).priority(.required - 1), avatarView.heightAnchor.constraint(equalToConstant: StatusView.avatarImageSize.height).priority(.required - 1), ]) - avatarImageView.translatesAutoresizingMaskIntoConstraints = false - avatarView.addSubview(avatarImageView) + avatarButton.translatesAutoresizingMaskIntoConstraints = false + avatarView.addSubview(avatarButton) NSLayoutConstraint.activate([ - avatarImageView.topAnchor.constraint(equalTo: avatarView.topAnchor), - avatarImageView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor), - avatarImageView.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor), - avatarImageView.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor), + avatarButton.topAnchor.constraint(equalTo: avatarView.topAnchor), + avatarButton.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor), + avatarButton.trailingAnchor.constraint(equalTo: avatarView.trailingAnchor), + avatarButton.bottomAnchor.constraint(equalTo: avatarView.bottomAnchor), ]) avatarStackedContainerButton.translatesAutoresizingMaskIntoConstraints = false avatarView.addSubview(avatarStackedContainerButton) @@ -473,11 +470,7 @@ extension StatusView { headerInfoLabel.isUserInteractionEnabled = true headerInfoLabel.addGestureRecognizer(headerInfoLabelTapGestureRecognizer) - let avatarImageViewTapGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer - avatarImageViewTapGestureRecognizer.addTarget(self, action: #selector(StatusView.avatarImageViewDidPressed(_:))) - avatarImageView.addGestureRecognizer(avatarImageViewTapGestureRecognizer) - avatarImageView.isUserInteractionEnabled = true - + avatarButton.addTarget(self, action: #selector(StatusView.avatarButtonDidPressed(_:)), for: .touchUpInside) avatarStackedContainerButton.addTarget(self, action: #selector(StatusView.avatarStackedContainerButtonDidPressed(_:)), for: .touchUpInside) revealContentWarningButton.addTarget(self, action: #selector(StatusView.revealContentWarningButtonDidPressed(_:)), for: .touchUpInside) pollVoteButton.addTarget(self, action: #selector(StatusView.pollVoteButtonPressed(_:)), for: .touchUpInside) @@ -544,9 +537,9 @@ extension StatusView { delegate?.statusView(self, headerInfoLabelDidPressed: headerInfoLabel) } - @objc private func avatarImageViewDidPressed(_ sender: UITapGestureRecognizer) { + @objc private func avatarButtonDidPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - delegate?.statusView(self, avatarImageViewDidPressed: avatarImageView) + delegate?.statusView(self, avatarImageViewDidPressed: avatarButton.avatarImageView) } @objc private func avatarStackedContainerButtonDidPressed(_ sender: UIButton) { @@ -633,8 +626,7 @@ extension StatusView: PlayerContainerViewDelegate { extension StatusView: AvatarConfigurableView { static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize } static var configurableAvatarImageCornerRadius: CGFloat { return 4 } - var configurableAvatarImageView: UIImageView? { avatarImageView } - var configurableAvatarButton: UIButton? { nil } + var configurableAvatarImageView: FLAnimatedImageView? { avatarButton.avatarImageView } } #if canImport(SwiftUI) && DEBUG @@ -662,7 +654,7 @@ struct StatusView_Previews: PreviewProvider { UIViewPreview(width: 375) { let statusView = StatusView() statusView.headerContainerView.isHidden = false - statusView.avatarImageView.isHidden = true + statusView.avatarButton.isHidden = true statusView.avatarStackedContainerButton.isHidden = false statusView.avatarStackedContainerButton.topLeadingAvatarStackedImageView.configure( with: AvatarConfigurableViewConfiguration( diff --git a/Mastodon/Scene/Share/View/ImageView/AvatarImageView.swift b/Mastodon/Scene/Share/View/ImageView/AvatarImageView.swift new file mode 100644 index 000000000..0b3f2a8f4 --- /dev/null +++ b/Mastodon/Scene/Share/View/ImageView/AvatarImageView.swift @@ -0,0 +1,11 @@ +// +// AvatarImageView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-7-21. +// + +import UIKit +import FLAnimatedImage + +class AvatarImageView: FLAnimatedImageView { } diff --git a/Mastodon/Service/APIService/APIService.swift b/Mastodon/Service/APIService/APIService.swift index 9d1468ce0..d9ae017d9 100644 --- a/Mastodon/Service/APIService/APIService.swift +++ b/Mastodon/Service/APIService/APIService.swift @@ -13,7 +13,6 @@ import CoreDataStack import MastodonSDK import AlamofireImage import AlamofireNetworkActivityIndicator -import Nuke final class APIService { @@ -34,10 +33,6 @@ final class APIService { // setup cache. 10MB RAM + 50MB Disk URLCache.shared = URLCache(memoryCapacity: 10 * 1024 * 1024, diskCapacity: 50 * 1024 * 1024, diskPath: nil) - - // setup Nuke cache - // using LRU disk cache - ImagePipeline.shared = ImagePipeline(configuration: .withDataCache) // enable network activity manager for AlamofireImage NetworkActivityIndicatorManager.shared.isEnabled = true diff --git a/Mastodon/Service/PhotoLibraryService.swift b/Mastodon/Service/PhotoLibraryService.swift index 53e7529c0..99dcb61ff 100644 --- a/Mastodon/Service/PhotoLibraryService.swift +++ b/Mastodon/Service/PhotoLibraryService.swift @@ -9,7 +9,9 @@ import os.log import UIKit import Combine import Photos +import Alamofire import AlamofireImage +import FLAnimatedImage final class PhotoLibraryService: NSObject { @@ -19,88 +21,150 @@ extension PhotoLibraryService { enum PhotoLibraryError: Error { case noPermission + case badPayload + } + + enum ImageSource { + case url(URL) + case image(UIImage) } } extension PhotoLibraryService { - - func saveImage(url: URL) -> AnyPublisher { + + func save(imageSource source: ImageSource) -> AnyPublisher { + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + let notificationFeedbackGenerator = UINotificationFeedbackGenerator() + + + let imageDataPublisher: AnyPublisher = { + switch source { + case .url(let url): + return PhotoLibraryService.fetchImageData(url: url) + case .image(let image): + return PhotoLibraryService.fetchImageData(image: image) + } + }() + + return imageDataPublisher + .flatMap { data in + PhotoLibraryService.save(imageData: data) + } + .handleEvents(receiveSubscription: { _ in + impactFeedbackGenerator.impactOccurred() + }, receiveCompletion: { completion in + switch completion { + case .failure: + notificationFeedbackGenerator.notificationOccurred(.error) + case .finished: + notificationFeedbackGenerator.notificationOccurred(.success) + } + }) + .eraseToAnyPublisher() + } + +} + +extension PhotoLibraryService { + + func copy(imageSource source: ImageSource) -> AnyPublisher { + + let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + let notificationFeedbackGenerator = UINotificationFeedbackGenerator() + + let imageDataPublisher: AnyPublisher = { + switch source { + case .url(let url): + return PhotoLibraryService.fetchImageData(url: url) + case .image(let image): + return PhotoLibraryService.fetchImageData(image: image) + } + }() + + return imageDataPublisher + .flatMap { data in + PhotoLibraryService.copy(imageData: data) + } + .handleEvents(receiveSubscription: { _ in + impactFeedbackGenerator.impactOccurred() + }, receiveCompletion: { completion in + switch completion { + case .failure: + notificationFeedbackGenerator.notificationOccurred(.error) + case .finished: + notificationFeedbackGenerator.notificationOccurred(.success) + } + }) + .eraseToAnyPublisher() + } +} + +extension PhotoLibraryService { + + static func fetchImageData(url: URL) -> AnyPublisher { + AF.request(url).publishData() + .tryMap { response in + switch response.result { + case .success(let data): + return data + case .failure(let error): + throw error + } + } + .eraseToAnyPublisher() + } + + static func fetchImageData(image: UIImage) -> AnyPublisher { + return Future { promise in + DispatchQueue.global().async { + let imageData = image.pngData() + DispatchQueue.main.async { + if let imageData = imageData { + promise(.success(imageData)) + } else { + promise(.failure(PhotoLibraryError.badPayload)) + } + } + } + } + .eraseToAnyPublisher() + } + + static func save(imageData: Data) -> AnyPublisher { guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .denied else { return Fail(error: PhotoLibraryError.noPermission).eraseToAnyPublisher() } - return processImage(url: url) - .handleEvents(receiveOutput: { image in - self.save(image: image) - }) - .eraseToAnyPublisher() - } - - func copyImage(url: URL) -> AnyPublisher { - return processImage(url: url) - .handleEvents(receiveOutput: { image in - UIPasteboard.general.image = image - }) - .eraseToAnyPublisher() - } - - func processImage(url: URL) -> AnyPublisher { - let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - - return Future { promise in - ImageDownloader.default.download(URLRequest(url: url), completion: { response in - switch response.result { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) + return Future { promise in + PHPhotoLibrary.shared().performChanges { + PHAssetCreationRequest.forAsset().addResource(with: .photo, data: imageData, options: nil) + } completionHandler: { isSuccess, error in + if let error = error { promise(.failure(error)) - case .success(let image): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) - promise(.success(image)) + } else { + promise(.success(Void())) } - }) - } - .handleEvents(receiveSubscription: { _ in - impactFeedbackGenerator.impactOccurred() - }, receiveCompletion: { completion in - switch completion { - case .failure: - notificationFeedbackGenerator.notificationOccurred(.error) - case .finished: - notificationFeedbackGenerator.notificationOccurred(.success) } - }) + } .eraseToAnyPublisher() } - - func save(image: UIImage, withNotificationFeedback: Bool = false) { - UIImageWriteToSavedPhotosAlbum( - image, - self, - #selector(PhotoLibraryService.image(_:didFinishSavingWithError:contextInfo:)), - nil - ) - - // assert no error - if withNotificationFeedback { - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - notificationFeedbackGenerator.notificationOccurred(.success) + + static func copy(imageData: Data) -> AnyPublisher { + Future { promise in + DispatchQueue.global().async { + let image = UIImage(data: imageData, scale: UIScreen.main.scale) + DispatchQueue.main.async { + if let image = image { + UIPasteboard.general.image = image + promise(.success(Void())) + } else { + promise(.failure(PhotoLibraryError.badPayload)) + } + } + } } + .eraseToAnyPublisher() } - func copy(image: UIImage, withNotificationFeedback: Bool = false) { - UIPasteboard.general.image = image - - // assert no error - if withNotificationFeedback { - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - notificationFeedbackGenerator.notificationOccurred(.success) - } - } - - @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { - // TODO: notify banner - } - }