diff --git a/Localization/app.json b/Localization/app.json index a43e56c10..f29c588c2 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -266,6 +266,7 @@ } }, "profile": { + "subtitle": "%s posts", "dashboard": { "posts": "posts", "following": "following", diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7e50253b4..da27f7a67 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -340,6 +340,10 @@ internal enum L10n { } } internal enum Profile { + /// %@ posts + internal static func subtitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "Scene.Profile.Subtitle", String(describing: p1)) + } internal enum Dashboard { /// followers internal static let followers = L10n.tr("Localizable", "Scene.Profile.Dashboard.Followers") diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 64b62233f..e94101cc4 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -124,6 +124,7 @@ tap the link to confirm your account."; "Scene.Profile.SegmentedControl.Media" = "Media"; "Scene.Profile.SegmentedControl.Posts" = "Posts"; "Scene.Profile.SegmentedControl.Replies" = "Replies"; +"Scene.Profile.Subtitle" = "%@ posts"; "Scene.PublicTimeline.Title" = "Public"; "Scene.Register.Error.Item.Agreement" = "Agreement"; "Scene.Register.Error.Item.Email" = "Email"; diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 8f382336e..38695f34f 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -29,6 +29,16 @@ final class ProfileHeaderViewController: UIViewController { var viewModel: ProfileHeaderViewModel! + let titleView: DoubleTitleLabelNavigationBarTitleView = { + let titleView = DoubleTitleLabelNavigationBarTitleView() + titleView.titleLabel.textColor = .white + titleView.titleLabel.alpha = 0 + titleView.subtitleLabel.textColor = .white + titleView.subtitleLabel.alpha = 0 + titleView.layer.masksToBounds = true + return titleView + }() + let profileHeaderView = ProfileHeaderView() let pageSegmentedControl: UISegmentedControl = { let segmenetedControl = UISegmentedControl(items: ["A", "B"]) @@ -97,6 +107,18 @@ extension ProfileHeaderViewController { ]) pageSegmentedControl.addTarget(self, action: #selector(ProfileHeaderViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged) + + Publishers.CombineLatest( + viewModel.viewDidAppear.eraseToAnyPublisher(), + viewModel.isTitleViewContentOffsetSet.eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] viewDidAppear, isTitleViewContentOffsetDidSetted in + guard let self = self else { return } + self.titleView.titleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSetted ? 1 : 0 + self.titleView.subtitleLabel.alpha = viewDidAppear && isTitleViewContentOffsetDidSetted ? 1 : 0 + } + .store(in: &disposeBag) viewModel.needsSetupBottomShadow .receive(on: DispatchQueue.main) @@ -176,7 +198,7 @@ extension ProfileHeaderViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.viewDidAppear.send() + viewModel.viewDidAppear.value = true // Deprecated: // not needs this tweak due to force layout update in the parent @@ -281,20 +303,56 @@ extension ProfileHeaderViewController { let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil) let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height - + + // scroll from bottom to top: 1 -> 2 -> 3 if bannerContainerInWindow.origin.y > containerSafeAreaInset.top { + // 1 + // banner top pin to window top and expand bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height } else if bannerContainerBottomOffset < containerSafeAreaInset.top { + // 3 + // banner bottom pin to navigation bar bottom and + // the `progress` growth to 1 then segemented control pin to top bannerImageView.frame.origin.y = -containerSafeAreaInset.top let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset) bannerImageView.frame.size.height = bannerImageHeight } else { + // 2 + // banner move with scrolling from bottom to top until the + // banner bottom higher than navigation bar bottom bannerImageView.frame.origin.y = -containerSafeAreaInset.top bannerImageView.frame.size.height = bannerContainerInWindow.size.height + containerSafeAreaInset.top } - // TODO: handle titleView + // set title view offset + let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) + let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y + let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset + titleView.containerView.transform = CGAffineTransform(translationX: 0, y: max(0, titleViewContentOffset)) + + if viewModel.viewDidAppear.value { + viewModel.isTitleViewContentOffsetSet.value = true + } + + // set avatar + if progress > 0 { + setProfileBannerFade(alpha: 0) + } else if progress > -0.3 { + // y = -(10/3)x + let alpha = -10.0 / 3.0 * progress + setProfileBannerFade(alpha: alpha) + } else { + setProfileBannerFade(alpha: 1) + } + } + + private func setProfileBannerFade(alpha: CGFloat) { + profileHeaderView.avatarImageView.alpha = alpha + profileHeaderView.editAvatarBackgroundView.alpha = alpha + profileHeaderView.nameTextFieldBackgroundView.alpha = alpha + profileHeaderView.nameTextField.alpha = alpha + profileHeaderView.usernameLabel.alpha = alpha } } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index be0676740..eb4a054b8 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -17,9 +17,10 @@ final class ProfileHeaderViewModel { // input let context: AppContext let isEditing = CurrentValueSubject(false) - let viewDidAppear = PassthroughSubject() + let viewDidAppear = CurrentValueSubject(false) let needsSetupBottomShadow = CurrentValueSubject(true) - + let isTitleViewContentOffsetSet = CurrentValueSubject(false) + // output let displayProfileInfo = ProfileInfo() let editProfileInfo = ProfileInfo() diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 9819f3f18..671f7c155 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -88,6 +88,10 @@ final class ProfileViewController: UIViewController, NeedsDependency { private var contentOffsets: [Int: CGFloat] = [:] var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation? + // title view nested in header + var titleView: DoubleTitleLabelNavigationBarTitleView { + profileHeaderViewController.titleView + } deinit { os_log("%{public}s[%{public}ld], %{public}s: deinit", ((#file as NSString).lastPathComponent), #line, #function) @@ -144,8 +148,8 @@ extension ProfileViewController { navigationItem.compactAppearance = barAppearance navigationItem.scrollEdgeAppearance = barAppearance - navigationItem.titleView = UIView() - + navigationItem.titleView = titleView + let editingAndUpdatingPublisher = Publishers.CombineLatest( viewModel.isEditing.eraseToAnyPublisher(), viewModel.isUpdating.eraseToAnyPublisher() @@ -292,6 +296,23 @@ extension ProfileViewController { profileSegmentedViewController.pagingViewController.pagingDelegate = self // bind view model + Publishers.CombineLatest( + viewModel.name.eraseToAnyPublisher(), + viewModel.statusesCount.eraseToAnyPublisher() + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] name, statusesCount in + guard let self = self else { return } + guard let title = name, let statusesCount = statusesCount, + let formattedStatusCount = MastodonMetricFormatter().string(from: statusesCount) else { + self.titleView.isHidden = true + return + } + let subtitle = L10n.Scene.Profile.subtitle(formattedStatusCount) + self.titleView.update(title: title, subtitle: subtitle) + self.titleView.isHidden = false + } + .store(in: &disposeBag) viewModel.name .receive(on: DispatchQueue.main) .sink { [weak self] name in @@ -396,6 +417,7 @@ extension ProfileViewController { let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut) animator.addAnimations { self.profileSegmentedViewController.view.alpha = isEditing ? 0.2 : 1.0 + self.profileHeaderViewController.profileHeaderView.statusDashboardView.alpha = isEditing ? 0.2 : 1.0 } animator.startAnimation() }) diff --git a/README.md b/README.md index 61f142bb8..23957fa16 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ arch -x86_64 pod install - [SwiftGen](https://github.com/SwiftGen/SwiftGen) - [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) - [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor) +- [TwitterProfile](https://github.com/OfTheWolf/TwitterProfile) - [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder) ## License