feat: add title view for profile scene

This commit is contained in:
CMK 2021-04-09 19:44:48 +08:00
parent 9184ec4ecf
commit ff13121b18
7 changed files with 95 additions and 7 deletions

View File

@ -266,6 +266,7 @@
}
},
"profile": {
"subtitle": "%s posts",
"dashboard": {
"posts": "posts",
"following": "following",

View File

@ -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")

View File

@ -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";

View File

@ -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
}
}

View File

@ -17,9 +17,10 @@ final class ProfileHeaderViewModel {
// input
let context: AppContext
let isEditing = CurrentValueSubject<Bool, Never>(false)
let viewDidAppear = PassthroughSubject<Void, Never>()
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
// output
let displayProfileInfo = ProfileInfo()
let editProfileInfo = ProfileInfo()

View File

@ -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()
})

View File

@ -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