feat: add title view for profile scene
This commit is contained in:
parent
9184ec4ecf
commit
ff13121b18
|
@ -266,6 +266,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
|
"subtitle": "%s posts",
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"posts": "posts",
|
"posts": "posts",
|
||||||
"following": "following",
|
"following": "following",
|
||||||
|
|
|
@ -340,6 +340,10 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
internal enum Profile {
|
internal enum Profile {
|
||||||
|
/// %@ posts
|
||||||
|
internal static func subtitle(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Scene.Profile.Subtitle", String(describing: p1))
|
||||||
|
}
|
||||||
internal enum Dashboard {
|
internal enum Dashboard {
|
||||||
/// followers
|
/// followers
|
||||||
internal static let followers = L10n.tr("Localizable", "Scene.Profile.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.Media" = "Media";
|
||||||
"Scene.Profile.SegmentedControl.Posts" = "Posts";
|
"Scene.Profile.SegmentedControl.Posts" = "Posts";
|
||||||
"Scene.Profile.SegmentedControl.Replies" = "Replies";
|
"Scene.Profile.SegmentedControl.Replies" = "Replies";
|
||||||
|
"Scene.Profile.Subtitle" = "%@ posts";
|
||||||
"Scene.PublicTimeline.Title" = "Public";
|
"Scene.PublicTimeline.Title" = "Public";
|
||||||
"Scene.Register.Error.Item.Agreement" = "Agreement";
|
"Scene.Register.Error.Item.Agreement" = "Agreement";
|
||||||
"Scene.Register.Error.Item.Email" = "Email";
|
"Scene.Register.Error.Item.Email" = "Email";
|
||||||
|
|
|
@ -29,6 +29,16 @@ final class ProfileHeaderViewController: UIViewController {
|
||||||
|
|
||||||
var viewModel: ProfileHeaderViewModel!
|
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 profileHeaderView = ProfileHeaderView()
|
||||||
let pageSegmentedControl: UISegmentedControl = {
|
let pageSegmentedControl: UISegmentedControl = {
|
||||||
let segmenetedControl = UISegmentedControl(items: ["A", "B"])
|
let segmenetedControl = UISegmentedControl(items: ["A", "B"])
|
||||||
|
@ -97,6 +107,18 @@ extension ProfileHeaderViewController {
|
||||||
])
|
])
|
||||||
|
|
||||||
pageSegmentedControl.addTarget(self, action: #selector(ProfileHeaderViewController.pageSegmentedControlValueChanged(_:)), for: .valueChanged)
|
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
|
viewModel.needsSetupBottomShadow
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -176,7 +198,7 @@ extension ProfileHeaderViewController {
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
viewModel.viewDidAppear.send()
|
viewModel.viewDidAppear.value = true
|
||||||
|
|
||||||
// Deprecated:
|
// Deprecated:
|
||||||
// not needs this tweak due to force layout update in the parent
|
// 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 bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
|
||||||
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
|
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
|
||||||
|
|
||||||
|
// scroll from bottom to top: 1 -> 2 -> 3
|
||||||
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
|
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
|
||||||
|
// 1
|
||||||
|
// banner top pin to window top and expand
|
||||||
bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
|
bannerImageView.frame.origin.y = -bannerContainerInWindow.origin.y
|
||||||
bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
|
bannerImageView.frame.size.height = bannerContainerInWindow.origin.y + bannerContainerInWindow.size.height
|
||||||
} else if bannerContainerBottomOffset < containerSafeAreaInset.top {
|
} 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
|
bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||||
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
let bannerImageHeight = bannerContainerInWindow.size.height + containerSafeAreaInset.top + (containerSafeAreaInset.top - bannerContainerBottomOffset)
|
||||||
bannerImageView.frame.size.height = bannerImageHeight
|
bannerImageView.frame.size.height = bannerImageHeight
|
||||||
} else {
|
} 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.origin.y = -containerSafeAreaInset.top
|
||||||
bannerImageView.frame.size.height = bannerContainerInWindow.size.height + 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
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let isEditing = CurrentValueSubject<Bool, Never>(false)
|
let isEditing = CurrentValueSubject<Bool, Never>(false)
|
||||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
|
||||||
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
||||||
|
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let displayProfileInfo = ProfileInfo()
|
let displayProfileInfo = ProfileInfo()
|
||||||
let editProfileInfo = ProfileInfo()
|
let editProfileInfo = ProfileInfo()
|
||||||
|
|
|
@ -88,6 +88,10 @@ final class ProfileViewController: UIViewController, NeedsDependency {
|
||||||
private var contentOffsets: [Int: CGFloat] = [:]
|
private var contentOffsets: [Int: CGFloat] = [:]
|
||||||
var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation?
|
var currentPostTimelineTableViewContentSizeObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
// title view nested in header
|
||||||
|
var titleView: DoubleTitleLabelNavigationBarTitleView {
|
||||||
|
profileHeaderViewController.titleView
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: deinit", ((#file as NSString).lastPathComponent), #line, #function)
|
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.compactAppearance = barAppearance
|
||||||
navigationItem.scrollEdgeAppearance = barAppearance
|
navigationItem.scrollEdgeAppearance = barAppearance
|
||||||
|
|
||||||
navigationItem.titleView = UIView()
|
navigationItem.titleView = titleView
|
||||||
|
|
||||||
let editingAndUpdatingPublisher = Publishers.CombineLatest(
|
let editingAndUpdatingPublisher = Publishers.CombineLatest(
|
||||||
viewModel.isEditing.eraseToAnyPublisher(),
|
viewModel.isEditing.eraseToAnyPublisher(),
|
||||||
viewModel.isUpdating.eraseToAnyPublisher()
|
viewModel.isUpdating.eraseToAnyPublisher()
|
||||||
|
@ -292,6 +296,23 @@ extension ProfileViewController {
|
||||||
profileSegmentedViewController.pagingViewController.pagingDelegate = self
|
profileSegmentedViewController.pagingViewController.pagingDelegate = self
|
||||||
|
|
||||||
// bind view model
|
// 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
|
viewModel.name
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] name in
|
.sink { [weak self] name in
|
||||||
|
@ -396,6 +417,7 @@ extension ProfileViewController {
|
||||||
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
|
let animator = UIViewPropertyAnimator(duration: 0.33, curve: .easeInOut)
|
||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
self.profileSegmentedViewController.view.alpha = isEditing ? 0.2 : 1.0
|
self.profileSegmentedViewController.view.alpha = isEditing ? 0.2 : 1.0
|
||||||
|
self.profileHeaderViewController.profileHeaderView.statusDashboardView.alpha = isEditing ? 0.2 : 1.0
|
||||||
}
|
}
|
||||||
animator.startAnimation()
|
animator.startAnimation()
|
||||||
})
|
})
|
||||||
|
|
|
@ -54,6 +54,7 @@ arch -x86_64 pod install
|
||||||
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
- [SwiftGen](https://github.com/SwiftGen/SwiftGen)
|
||||||
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
|
||||||
- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor)
|
- [TwitterTextEditor](https://github.com/twitter/TwitterTextEditor)
|
||||||
|
- [TwitterProfile](https://github.com/OfTheWolf/TwitterProfile)
|
||||||
- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder)
|
- [UITextView-Placeholder](https://github.com/devxoul/UITextView-Placeholder)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
Loading…
Reference in New Issue