diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0c8d53cd..7504032b 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -67,6 +67,10 @@ DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; }; DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; }; DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */; }; + DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; + DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; + DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; }; DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; }; DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -97,6 +101,7 @@ DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; }; DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; }; DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; + DBD4ED1125CC0FEB0041B741 /* HomeTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD4ED1025CC0FEB0041B741 /* HomeTimelineViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -220,6 +225,9 @@ DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = ""; }; DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; DB45FB1C25CA9D23005A8AC7 /* APIService+HomeTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+HomeTimeline.swift"; sourceTree = ""; }; + DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarBarButtonItem.swift; sourceTree = ""; }; + DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarConfigurableView.swift; sourceTree = ""; }; + DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashPreference.swift; sourceTree = ""; }; DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = ""; }; DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -252,6 +260,7 @@ DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = ""; }; + DBD4ED1025CC0FEB0041B741 /* HomeTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineViewModel.swift; sourceTree = ""; }; DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = ""; }; DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = ""; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = ""; }; @@ -266,6 +275,7 @@ DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, 45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */, @@ -339,6 +349,7 @@ 2D42FF8325C82245004A627A /* Button */ = { isa = PBXGroup; children = ( + DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */, 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */, ); path = Button; @@ -364,6 +375,7 @@ 2D69CFF225CA9E2200C3A1B2 /* Protocol */ = { isa = PBXGroup; children = ( + DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */, 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */, ); path = Protocol; @@ -529,13 +541,14 @@ children = ( DB427DE325BAA00100D1B89D /* Info.plist */, DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, - 2D69CFF225CA9E2200C3A1B2 /* Protocol */, 2D76319C25C151DE00929FB9 /* Diffiable */, DB8AF52A25C13561002E6C99 /* State */, 2D61335525C1886800CAE157 /* Service */, DB8AF55525C1379F002E6C99 /* Scene */, DB8AF54125C13647002E6C99 /* Coordinator */, DB8AF56225C138BC002E6C99 /* Extension */, + DB5086CB25CC0DB400C2C187 /* Preference */, + 2D69CFF225CA9E2200C3A1B2 /* Protocol */, DB98338425C945ED00AD9700 /* Generated */, DB3D0FF825BAA6B200EAA174 /* Resources */, DB3D0FF725BAA68500EAA174 /* Supporting Files */, @@ -587,6 +600,14 @@ path = CoreData; sourceTree = ""; }; + DB5086CB25CC0DB400C2C187 /* Preference */ = { + isa = PBXGroup; + children = ( + DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */, + ); + path = Preference; + sourceTree = ""; + }; DB89B9EF25C10FD0008580ED /* CoreDataStack */ = { isa = PBXGroup; children = ( @@ -677,10 +698,10 @@ isa = PBXGroup; children = ( 2D7631A425C1532200929FB9 /* Share */, + DB8AF54E25C13703002E6C99 /* MainTab */, DB01409B25C40BB600F9F3CF /* Authentication */, 2D76316325C14BAC00929FB9 /* PublicTimeline */, - DB8AF54E25C13703002E6C99 /* MainTab */, - DB8AF55625C137A8002E6C99 /* HomeViewController.swift */, + DBD4ED0B25CC0FD40041B741 /* HomeTimeline */, ); path = Scene; sourceTree = ""; @@ -720,6 +741,15 @@ path = Generated; sourceTree = ""; }; + DBD4ED0B25CC0FD40041B741 /* HomeTimeline */ = { + isa = PBXGroup; + children = ( + DB8AF55625C137A8002E6C99 /* HomeViewController.swift */, + DBD4ED1025CC0FEB0041B741 /* HomeTimelineViewModel.swift */, + ); + path = HomeTimeline; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -758,6 +788,7 @@ 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, 2D42FF6025C8177C004A627A /* ActiveLabel */, DB0140BC25C40D7500F9F3CF /* CommonOSLog */, + DB5086B725CC0D6400C2C187 /* Kingfisher */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -884,6 +915,7 @@ 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -1082,11 +1114,13 @@ DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, + DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */, 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */, 2D45E5BF25C9549700A6D639 /* PublicTimelineViewModel+State.swift in Sources */, 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, + DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */, @@ -1096,6 +1130,7 @@ DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, 2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */, DB084B5725CBC56C00F898ED /* Toot.swift in Sources */, + DBD4ED1125CC0FEB0041B741 /* HomeTimelineViewModel.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, @@ -1116,6 +1151,7 @@ 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */, DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, + DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */, DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */, ); @@ -1661,6 +1697,14 @@ minimumVersion = 4.1.0; }; }; + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.1.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1688,6 +1732,11 @@ package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; + DB5086B725CC0D6400C2C187 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index f783b798..bbf66235 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -46,6 +46,15 @@ "version": "0.1.1" } }, + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "state": { + "branch": null, + "revision": "daebf8ddf974164d1b9a050c8231e263f3106b09", + "version": "6.1.0" + } + }, { "package": "swift-nio", "repositoryURL": "https://github.com/apple/swift-nio.git", diff --git a/Mastodon/Protocol/AvatarConfigurableView.swift b/Mastodon/Protocol/AvatarConfigurableView.swift new file mode 100644 index 00000000..dfeb3e5b --- /dev/null +++ b/Mastodon/Protocol/AvatarConfigurableView.swift @@ -0,0 +1,146 @@ +// +// AvatarConfigurableView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-2-4. +// + +import UIKit +import AlamofireImage +import Kingfisher + +protocol AvatarConfigurableView { + static var configurableAvatarImageViewSize: CGSize { get } + static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { get } + var configurableAvatarImageView: UIImageView? { get } + var configurableAvatarButton: UIButton? { get } + var configurableVerifiedBadgeImageView: UIImageView? { get } + func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input) + func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) +} + +extension AvatarConfigurableView { + + static var configurableAvatarImageViewBadgeAppearanceStyle: AvatarConfigurableViewConfiguration.BadgeAppearanceStyle { return .mini } + + public func configure(withConfigurationInput input: AvatarConfigurableViewConfiguration.Input) { + // TODO: set badge + configurableVerifiedBadgeImageView?.isHidden = true + + let cornerRadius = Self.configurableAvatarImageViewSize.width * 0.5 + // let scale = (configurableAvatarImageView ?? configurableAvatarButton)?.window?.screen.scale ?? UIScreen.main.scale + + let placeholderImage: UIImage = { + let placeholderImage = input.placeholderImage ?? UIImage.placeholder(size: Self.configurableAvatarImageViewSize, color: .systemFill) + return placeholderImage.af.imageRoundedIntoCircle() + }() + + // cancel previous task + configurableAvatarImageView?.af.cancelImageRequest() + configurableAvatarImageView?.kf.cancelDownloadTask() + configurableAvatarButton?.af.cancelImageRequest(for: .normal) + configurableAvatarButton?.kf.cancelImageDownloadTask() + + // 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 + + defer { + let configuration = AvatarConfigurableViewConfiguration(input: input) + avatarConfigurableView(self, didFinishConfiguration: configuration) + } + + // set placeholder if no asset + guard let avatarImageURL = input.avatarImageURL else { + configurableAvatarImageView?.image = placeholderImage + configurableAvatarButton?.setImage(placeholderImage, for: .normal) + return + } + + if let avatarImageView = configurableAvatarImageView { + // set avatar (GIF using Kingfisher) + switch avatarImageURL.pathExtension { + case "gif": + avatarImageView.kf.setImage( + with: avatarImageURL, + placeholder: placeholderImage, + options: [ + .transition(.fade(0.2)) + ] + ) + avatarImageView.layer.masksToBounds = true + avatarImageView.layer.cornerRadius = cornerRadius + avatarImageView.layer.cornerCurve = .circular + default: + let filter = ScaledToSizeCircleFilter(size: Self.configurableAvatarImageViewSize) + avatarImageView.af.setImage( + withURL: avatarImageURL, + placeholderImage: placeholderImage, + filter: filter, + imageTransition: .crossDissolve(0.3), + runImageTransitionIfCached: false, + completion: nil + ) + } + } + + if let avatarButton = configurableAvatarButton { + switch avatarImageURL.pathExtension { + case "gif": + avatarButton.kf.setImage( + with: avatarImageURL, + for: .normal, + placeholder: placeholderImage, + options: [ + .transition(.fade(0.2)) + ] + ) + avatarButton.layer.masksToBounds = true + avatarButton.layer.cornerRadius = cornerRadius + avatarButton.layer.cornerCurve = .circular + default: + let filter = ScaledToSizeCircleFilter(size: Self.configurableAvatarImageViewSize) + avatarButton.af.setImage( + for: .normal, + url: avatarImageURL, + placeholderImage: placeholderImage, + filter: filter, + completion: nil + ) + } + } + } + + func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) { } + +} + +struct AvatarConfigurableViewConfiguration { + + enum BadgeAppearanceStyle { + case mini + case normal + } + + struct Input { + let avatarImageURL: URL? + let placeholderImage: UIImage? + let blocked: Bool + let verified: Bool + + init(avatarImageURL: URL?, placeholderImage: UIImage? = nil, blocked: Bool = false, verified: Bool = false) { + self.avatarImageURL = avatarImageURL + self.placeholderImage = placeholderImage + self.blocked = blocked + self.verified = verified + } + } + + let input: Input + +} diff --git a/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift b/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift new file mode 100644 index 00000000..f117c212 --- /dev/null +++ b/Mastodon/Scene/Share/View/Button/AvatarBarButtonItem.swift @@ -0,0 +1,49 @@ +// +// 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 configurableAvatarImageViewSize: CGSize { return avatarButtonSize } + var configurableAvatarImageView: UIImageView? { return nil } + var configurableAvatarButton: UIButton? { return avatarButton } + var configurableVerifiedBadgeImageView: UIImageView? { return nil } +} diff --git a/README.md b/README.md index 948ddf48..0847c82e 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,14 @@ arch -x86_64 pod install ## Acknowledgements -- [ActiveLabel](https://github.com/optonaut/ActiveLabel.swift) +- [ActiveLabel](https://github.com/TwidereProject/ActiveLabel.swift) - [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - [Alamofire](https://github.com/Alamofire/Alamofire) - [CommonOSLog](https://github.com/mainasuk/CommonOSLog) - [DateToolSwift](https://github.com/MatthewYork/DateTools) - [Kanna](https://github.com/tid-kijyun/Kanna) +- [Kingfisher](https://github.com/onevcat/Kingfisher) - [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)