mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-01-31 09:35:13 +01:00
feat: add title view for profile scene
This commit is contained in:
parent
9184ec4ecf
commit
ff13121b18
@ -266,6 +266,7 @@
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"subtitle": "%s posts",
|
||||
"dashboard": {
|
||||
"posts": "posts",
|
||||
"following": "following",
|
||||
|
@ -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")
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user