chore: [WIP] refactor profile UI

This commit is contained in:
CMK 2022-05-13 17:23:35 +08:00
parent c21b6e6a89
commit 503fcfab2a
28 changed files with 1586 additions and 1417 deletions

View File

@ -27,6 +27,7 @@
- [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect) - [SwiftUI-Introspect](https://github.com/siteline/SwiftUI-Introspect)
- [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON) - [SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)
- [Tabman](https://github.com/uias/Tabman) - [Tabman](https://github.com/uias/Tabman)
- [TabBarPager](https://github.com/TwidereProject/TabBarPager)
- [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS) - [TwidereX-iOS](https://github.com/TwidereProject/TwidereX-iOS)
- [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer) - [ThirdPartyMailer](https://github.com/vtourraine/ThirdPartyMailer)
- [TOCropViewController](https://github.com/TimOliver/TOCropViewController) - [TOCropViewController](https://github.com/TimOliver/TOCropViewController)

View File

@ -267,6 +267,7 @@
DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; }; DB47AB6227CF752B00CD73C7 /* MastodonUISnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB47AB6127CF752B00CD73C7 /* MastodonUISnapshotTests.swift */; };
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; }; DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; };
DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; }; DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; };
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */ = {isa = PBXBuildFile; productRef = DB486C0E282E41F200F69423 /* TabBarPager */; };
DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; }; DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; };
DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; }; DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; };
DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; };
@ -1429,6 +1430,7 @@
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */, DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
DB486C0F282E41F200F69423 /* TabBarPager in Frameworks */,
DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */, DB552D4F26BBD10C00E481F6 /* OrderedCollections in Frameworks */,
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */, DBAC64A1267E6D02007FE9FD /* Fuzi in Frameworks */,
@ -3448,6 +3450,7 @@
DBA5A52E26F07ED800CACBAA /* PanModal */, DBA5A52E26F07ED800CACBAA /* PanModal */,
DB3EA911281BBEA800598866 /* AlamofireImage */, DB3EA911281BBEA800598866 /* AlamofireImage */,
DB3EA913281BBEA800598866 /* Alamofire */, DB3EA913281BBEA800598866 /* Alamofire */,
DB486C0E282E41F200F69423 /* TabBarPager */,
); );
productName = Mastodon; productName = Mastodon;
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
@ -3670,6 +3673,7 @@
DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */, DBA5A52D26F07ED800CACBAA /* XCRemoteSwiftPackageReference "PanModal" */,
DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, DB8D8E2D28192EED009FD90F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */, DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */,
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */,
); );
productRefGroup = DB427DD325BAA00100D1B89D /* Products */; productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -5795,6 +5799,14 @@
minimumVersion = 5.4.0; minimumVersion = 5.4.0;
}; };
}; };
DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/TwidereProject/TabBarPager.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.1.0;
};
};
DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = { DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-collections.git"; repositoryURL = "https://github.com/apple/swift-collections.git";
@ -5959,6 +5971,11 @@
package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */; package = DB3EA8F6281BBA4C00598866 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire; productName = Alamofire;
}; };
DB486C0E282E41F200F69423 /* TabBarPager */ = {
isa = XCSwiftPackageProductDependency;
package = DB486C0D282E41F200F69423 /* XCRemoteSwiftPackageReference "TabBarPager" */;
productName = TabBarPager;
};
DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = { DB552D4E26BBD10C00E481F6 /* OrderedCollections */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */; package = DB552D4D26BBD10C00E481F6 /* XCRemoteSwiftPackageReference "swift-collections" */;

View File

@ -208,6 +208,15 @@
"version": "5.0.1" "version": "5.0.1"
} }
}, },
{
"package": "TabBarPager",
"repositoryURL": "https://github.com/TwidereProject/TabBarPager.git",
"state": {
"branch": null,
"revision": "488aa66d157a648901b61721212c0dec23d27ee5",
"version": "0.1.0"
}
},
{ {
"package": "Tabman", "package": "Tabman",
"repositoryURL": "https://github.com/uias/Tabman", "repositoryURL": "https://github.com/uias/Tabman",

View File

@ -8,12 +8,12 @@
import UIKit import UIKit
protocol ScrollViewContainer: UIViewController { protocol ScrollViewContainer: UIViewController {
var scrollView: UIScrollView? { get } var scrollView: UIScrollView { get }
func scrollToTop(animated: Bool) func scrollToTop(animated: Bool)
} }
extension ScrollViewContainer { extension ScrollViewContainer {
func scrollToTop(animated: Bool) { func scrollToTop(animated: Bool) {
scrollView?.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated) scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: animated)
} }
} }

View File

@ -148,9 +148,7 @@ extension DiscoveryCommunityViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryCommunityViewController: ScrollViewContainer { extension DiscoveryCommunityViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
extension DiscoveryCommunityViewController { extension DiscoveryCommunityViewController {

View File

@ -130,8 +130,8 @@ extension DiscoveryViewController {
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension DiscoveryViewController: ScrollViewContainer { extension DiscoveryViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView {
return (currentViewController as? ScrollViewContainer)?.scrollView return (currentViewController as? ScrollViewContainer)?.scrollView ?? UIScrollView()
} }
} }

View File

@ -168,8 +168,6 @@ extension DiscoveryForYouViewController: ProfileCardTableViewCellDelegate {
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryForYouViewController: ScrollViewContainer { extension DiscoveryForYouViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }

View File

@ -127,9 +127,7 @@ extension DiscoveryHashtagsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryHashtagsViewController: ScrollViewContainer { extension DiscoveryHashtagsViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
extension DiscoveryHashtagsViewController { extension DiscoveryHashtagsViewController {

View File

@ -127,9 +127,7 @@ extension DiscoveryNewsViewController: UITableViewDelegate {
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryNewsViewController: ScrollViewContainer { extension DiscoveryNewsViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
extension DiscoveryNewsViewController { extension DiscoveryNewsViewController {

View File

@ -160,9 +160,7 @@ extension DiscoveryPostsViewController: StatusTableViewCellDelegate { }
// MARK: ScrollViewContainer // MARK: ScrollViewContainer
extension DiscoveryPostsViewController: ScrollViewContainer { extension DiscoveryPostsViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { tableView }
tableView
}
} }
// MARK: - DiscoveryIntroBannerViewDelegate // MARK: - DiscoveryIntroBannerViewDelegate

View File

@ -537,13 +537,9 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension HomeTimelineViewController: ScrollViewContainer { extension HomeTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView? { return tableView } var scrollView: UIScrollView { return tableView }
func scrollToTop(animated: Bool) { func scrollToTop(animated: Bool) {
guard let scrollView = scrollView else {
return
}
if scrollView.contentOffset.y < scrollView.frame.height, if scrollView.contentOffset.y < scrollView.frame.height,
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self), viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
(scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0, (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) == 0.0,

View File

@ -203,9 +203,7 @@ extension NotificationTimelineViewController: NotificationTableViewCellDelegate
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension NotificationTimelineViewController: ScrollViewContainer { extension NotificationTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView { tableView }
var scrollView: UIScrollView? { tableView }
} }
extension NotificationTimelineViewController { extension NotificationTimelineViewController {

View File

@ -170,9 +170,9 @@ extension NotificationViewController {
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension NotificationViewController: ScrollViewContainer { extension NotificationViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView {
guard let viewController = currentViewController as? NotificationTimelineViewController else { guard let viewController = currentViewController as? NotificationTimelineViewController else {
return nil return UIScrollView()
} }
return viewController.scrollView return viewController.scrollView
} }

View File

@ -9,6 +9,9 @@ import os.log
import UIKit import UIKit
import Combine import Combine
import MetaTextKit import MetaTextKit
import MastodonLocalization
import TabBarPager
import XLPagerTabStrip
protocol ProfileAboutViewControllerDelegate: AnyObject { protocol ProfileAboutViewControllerDelegate: AnyObject {
func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta) func profileAboutViewController(_ viewController: ProfileAboutViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, metaLabel: MetaLabel, didSelectMeta meta: Meta)
@ -162,7 +165,17 @@ extension ProfileAboutViewController: ProfileFieldEditCollectionViewCellDelegate
// MARK: - ScrollViewContainer // MARK: - ScrollViewContainer
extension ProfileAboutViewController: ScrollViewContainer { extension ProfileAboutViewController: ScrollViewContainer {
var scrollView: UIScrollView? { var scrollView: UIScrollView { collectionView }
collectionView }
// MARK: - TabBarPage
extension ProfileAboutViewController: TabBarPage {
var pageScrollView: UIScrollView { scrollView }
}
// MARK: - IndicatorInfoProvider
extension ProfileAboutViewController: IndicatorInfoProvider {
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title: L10n.Scene.Profile.SegmentedControl.about)
} }
} }

View File

@ -15,7 +15,7 @@ import MastodonMeta
import MetaTextKit import MetaTextKit
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import Tabman import TabBarPager
protocol ProfileHeaderViewControllerDelegate: AnyObject { protocol ProfileHeaderViewControllerDelegate: AnyObject {
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
@ -28,6 +28,7 @@ final class ProfileHeaderViewController: UIViewController {
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
weak var delegate: ProfileHeaderViewControllerDelegate? weak var delegate: ProfileHeaderViewControllerDelegate?
weak var headerDelegate: TabBarPagerHeaderDelegate?
var viewModel: ProfileHeaderViewModel! var viewModel: ProfileHeaderViewModel!
@ -44,35 +45,35 @@ final class ProfileHeaderViewController: UIViewController {
let profileHeaderView = ProfileHeaderView() let profileHeaderView = ProfileHeaderView()
let buttonBar: TMBar.ButtonBar = { // let buttonBar: TMBar.ButtonBar = {
let buttonBar = TMBar.ButtonBar() // let buttonBar = TMBar.ButtonBar()
buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color // buttonBar.indicator.backgroundColor = Asset.Colors.Label.primary.color
buttonBar.backgroundView.style = .clear // buttonBar.backgroundView.style = .clear
buttonBar.layout.contentInset = .zero // buttonBar.layout.contentInset = .zero
return buttonBar // return buttonBar
}() // }()
func customizeButtonBarAppearance() { // func customizeButtonBarAppearance() {
// The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors // // The implmention use CATextlayer. Adapt for Dark Mode without dynamic colors
// Needs trigger update when `userInterfaceStyle` chagnes // // Needs trigger update when `userInterfaceStyle` chagnes
let userInterfaceStyle = traitCollection.userInterfaceStyle // let userInterfaceStyle = traitCollection.userInterfaceStyle
buttonBar.buttons.customize { button in // buttonBar.buttons.customize { button in
switch userInterfaceStyle { // switch userInterfaceStyle {
case .dark: // case .dark:
// Asset.Colors.Label.primary.color // // Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0) // button.selectedTintColor = UIColor(red: 238.0/255.0, green: 238.0/255.0, blue: 238.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color // // Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0) // button.tintColor = UIColor(red: 151.0/255.0, green: 157.0/255.0, blue: 173.0/255.0, alpha: 1.0)
default: // default:
// Asset.Colors.Label.primary.color // // Asset.Colors.Label.primary.color
button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0) // button.selectedTintColor = UIColor(red: 40.0/255.0, green: 44.0/255.0, blue: 55.0/255.0, alpha: 1.0)
// Asset.Colors.Label.secondary.color // // Asset.Colors.Label.secondary.color
button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6) // button.tintColor = UIColor(red: 60.0/255.0, green: 60.0/255.0, blue: 67.0/255.0, alpha: 0.6)
} // }
//
button.backgroundColor = .clear // button.backgroundColor = .clear
} // }
} // }
private var isBannerPinned = false private var isBannerPinned = false
private var bottomShadowAlpha: CGFloat = 0.0 private var bottomShadowAlpha: CGFloat = 0.0
@ -113,7 +114,7 @@ extension ProfileHeaderViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
customizeButtonBarAppearance() // customizeButtonBarAppearance()
view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor view.backgroundColor = ThemeService.shared.currentTheme.value.systemBackgroundColor
ThemeService.shared.currentTheme ThemeService.shared.currentTheme
@ -130,6 +131,7 @@ extension ProfileHeaderViewController {
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor), profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor), profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor), profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: profileHeaderView.bottomAnchor),
]) ])
profileHeaderView.preservesSuperviewLayoutMargins = true profileHeaderView.preservesSuperviewLayoutMargins = true
@ -262,14 +264,19 @@ extension ProfileHeaderViewController {
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
delegate?.profileHeaderViewController(self, viewLayoutDidUpdate: view) switch UIApplication.shared.applicationState {
setupBottomShadow() case .active:
headerDelegate?.viewLayoutDidUpdate(self)
setupBottomShadow()
default:
break
}
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) super.traitCollectionDidChange(previousTraitCollection)
customizeButtonBarAppearance() // customizeButtonBarAppearance()
} }
} }
@ -338,63 +345,63 @@ extension ProfileHeaderViewController {
} }
} }
func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) { // func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) {
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress) // // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
updateHeaderBottomShadow(progress: progress) // updateHeaderBottomShadow(progress: progress)
//
let bannerImageView = profileHeaderView.bannerImageView // let bannerImageView = profileHeaderView.bannerImageView
guard bannerImageView.bounds != .zero else { // guard bannerImageView.bounds != .zero else {
// wait layout finish // // wait layout finish
return // return
} // }
//
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 // // scroll from bottom to top: 1 -> 2 -> 3
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top { // if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
// 1 // // 1
// banner top pin to window top and expand // // 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 // // 3
// banner bottom pin to navigation bar bottom and // // banner bottom pin to navigation bar bottom and
// the `progress` growth to 1 then segmented control pin to top // // the `progress` growth to 1 then segmented 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 // // 2
// banner move with scrolling from bottom to top until the // // banner move with scrolling from bottom to top until the
// banner bottom higher than navigation bar bottom // // 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
} // }
//
// set title view offset // // set title view offset
let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil) // let nameTextFieldInWindow = profileHeaderView.nameTextField.superview!.convert(profileHeaderView.nameTextField.frame, to: nil)
let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y // let nameTextFieldTopToNavigationBarBottomOffset = containerSafeAreaInset.top - nameTextFieldInWindow.origin.y
let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset // let titleViewContentOffset: CGFloat = titleView.frame.height - nameTextFieldTopToNavigationBarBottomOffset
let transformY = max(0, titleViewContentOffset) // let transformY = max(0, titleViewContentOffset)
titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY) // titleView.containerView.transform = CGAffineTransform(translationX: 0, y: transformY)
viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height // viewModel.isTitleViewDisplaying.value = transformY < titleView.containerView.frame.height
//
if viewModel.viewDidAppear.value { // if viewModel.viewDidAppear.value {
viewModel.isTitleViewContentOffsetSet.value = true // viewModel.isTitleViewContentOffsetSet.value = true
} // }
//
// set avatar fade // // set avatar fade
if progress > 0 { // if progress > 0 {
setProfileAvatar(alpha: 0) // setProfileAvatar(alpha: 0)
} else if progress > -abs(throttle) { // } else if progress > -abs(throttle) {
// y = -(1/0.8T)x // // y = -(1/0.8T)x
let alpha = -1 / abs(0.8 * throttle) * progress // let alpha = -1 / abs(0.8 * throttle) * progress
setProfileAvatar(alpha: alpha) // setProfileAvatar(alpha: alpha)
} else { // } else {
setProfileAvatar(alpha: 1) // setProfileAvatar(alpha: 1)
} // }
} // }
private func setProfileAvatar(alpha: CGFloat) { private func setProfileAvatar(alpha: CGFloat) {
profileHeaderView.avatarImageViewBackgroundView.alpha = alpha profileHeaderView.avatarImageViewBackgroundView.alpha = alpha
@ -488,3 +495,6 @@ extension ProfileHeaderViewController: CropViewControllerDelegate {
cropViewController.dismiss(animated: true, completion: nil) cropViewController.dismiss(animated: true, completion: nil)
} }
} }
// MARK: - TabBarPagerHeader
extension ProfileHeaderViewController: TabBarPagerHeader { }

File diff suppressed because it is too large Load Diff

View File

@ -27,149 +27,181 @@ class ProfileViewModel: NSObject {
private var mastodonUserObserver: AnyCancellable? private var mastodonUserObserver: AnyCancellable?
private var currentMastodonUserObserver: AnyCancellable? private var currentMastodonUserObserver: AnyCancellable?
let postsUserTimelineViewModel: UserTimelineViewModel
let repliesUserTimelineViewModel: UserTimelineViewModel
let mediaUserTimelineViewModel: UserTimelineViewModel
let profileAboutViewModel: ProfileAboutViewModel
// input // input
let context: AppContext let context: AppContext
@Published var me: MastodonUser? @Published var me: MastodonUser?
@Published var user: MastodonUser? @Published var user: MastodonUser?
let viewDidAppear = PassthroughSubject<Void, Never>() let viewDidAppear = PassthroughSubject<Void, Never>()
@Published var isEditing = false
@Published var isUpdating = false
// output // output
let domain: CurrentValueSubject<String?, Never> @Published var userIdentifier: UserIdentifier? = nil
let userID: CurrentValueSubject<UserID?, Never>
let bannerImageURL: CurrentValueSubject<URL?, Never> // let domain: CurrentValueSubject<String?, Never>
let avatarImageURL: CurrentValueSubject<URL?, Never> // let userID: CurrentValueSubject<UserID?, Never>
let name: CurrentValueSubject<String?, Never> // let bannerImageURL: CurrentValueSubject<URL?, Never>
let username: CurrentValueSubject<String?, Never> // let avatarImageURL: CurrentValueSubject<URL?, Never>
let bioDescription: CurrentValueSubject<String?, Never> // let name: CurrentValueSubject<String?, Never>
let url: CurrentValueSubject<String?, Never> // let username: CurrentValueSubject<String?, Never>
let statusesCount: CurrentValueSubject<Int?, Never> // let bioDescription: CurrentValueSubject<String?, Never>
let followingCount: CurrentValueSubject<Int?, Never> // let url: CurrentValueSubject<String?, Never>
let followersCount: CurrentValueSubject<Int?, Never> // let statusesCount: CurrentValueSubject<Int?, Never>
let fields: CurrentValueSubject<[MastodonField], Never> // let followingCount: CurrentValueSubject<Int?, Never>
let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never> // let followersCount: CurrentValueSubject<Int?, Never>
// let fields: CurrentValueSubject<[MastodonField], Never>
// let emojiMeta: CurrentValueSubject<MastodonContent.Emojis, Never>
// fulfill this before editing // fulfill this before editing
let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil) let accountForEdit = CurrentValueSubject<Mastodon.Entity.Account?, Never>(nil)
let protected: CurrentValueSubject<Bool?, Never> // let protected: CurrentValueSubject<Bool?, Never>
let suspended: CurrentValueSubject<Bool, Never> // let suspended: CurrentValueSubject<Bool, Never>
let isEditing = CurrentValueSubject<Bool, Never>(false) //
let isUpdating = CurrentValueSubject<Bool, Never>(false) // let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none)
// let isFollowedBy = CurrentValueSubject<Bool, Never>(false)
let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none) // let isMuting = CurrentValueSubject<Bool, Never>(false)
let isFollowedBy = CurrentValueSubject<Bool, Never>(false) // let isBlocking = CurrentValueSubject<Bool, Never>(false)
let isMuting = CurrentValueSubject<Bool, Never>(false) // let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
let isBlocking = CurrentValueSubject<Bool, Never>(false) //
let isBlockedBy = CurrentValueSubject<Bool, Never>(false) // let isRelationshipActionButtonHidden = CurrentValueSubject<Bool, Never>(true)
// let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
let isRelationshipActionButtonHidden = CurrentValueSubject<Bool, Never>(true) // let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true) // let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true)
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true) //
let isMeBarButtonItemsHidden = CurrentValueSubject<Bool, Never>(true) // let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false)
// let needsPagingEnabled = CurrentValueSubject<Bool, Never>(true)
let needsPagePinToTop = CurrentValueSubject<Bool, Never>(false) // let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
let needsPagingEnabled = CurrentValueSubject<Bool, Never>(true)
let needsImageOverlayBlurred = CurrentValueSubject<Bool, Never>(false)
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) { init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
self.context = context self.context = context
self.user = mastodonUser self.user = mastodonUser
self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain) // self.domain = CurrentValueSubject(context.authenticationService.activeMastodonAuthenticationBox.value?.domain)
self.userID = CurrentValueSubject(mastodonUser?.id) // self.userID = CurrentValueSubject(mastodonUser?.id)
self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL()) // self.bannerImageURL = CurrentValueSubject(mastodonUser?.headerImageURL())
self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL()) // self.avatarImageURL = CurrentValueSubject(mastodonUser?.avatarImageURL())
self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback) // self.name = CurrentValueSubject(mastodonUser?.displayNameWithFallback)
self.username = CurrentValueSubject(mastodonUser?.acctWithDomain) // self.username = CurrentValueSubject(mastodonUser?.acctWithDomain)
self.bioDescription = CurrentValueSubject(mastodonUser?.note) // self.bioDescription = CurrentValueSubject(mastodonUser?.note)
self.url = CurrentValueSubject(mastodonUser?.url) // self.url = CurrentValueSubject(mastodonUser?.url)
self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) }) // self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.statusesCount) })
self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) }) // self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followingCount) })
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) }) // self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int($0.followersCount) })
self.protected = CurrentValueSubject(mastodonUser?.locked) // self.protected = CurrentValueSubject(mastodonUser?.locked)
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false) // self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
self.fields = CurrentValueSubject(mastodonUser?.fields ?? []) // self.fields = CurrentValueSubject(mastodonUser?.fields ?? [])
self.emojiMeta = CurrentValueSubject(mastodonUser?.emojis.asDictionary ?? [:]) // self.emojiMeta = CurrentValueSubject(mastodonUser?.emojis.asDictionary ?? [:])
self.postsUserTimelineViewModel = UserTimelineViewModel(
context: context,
queryFilter: .init(excludeReplies: true)
)
self.repliesUserTimelineViewModel = UserTimelineViewModel(
context: context,
queryFilter: .init(excludeReplies: true)
)
self.mediaUserTimelineViewModel = UserTimelineViewModel(
context: context,
queryFilter: .init(onlyMedia: true)
)
self.profileAboutViewModel = ProfileAboutViewModel(context: context)
super.init() super.init()
relationshipActionOptionSet // bind me
.compactMap { $0.highPriorityAction(except: []) }
.map { $0 == .none }
.assign(to: \.value, on: isRelationshipActionButtonHidden)
.store(in: &disposeBag)
// bind active authentication
context.authenticationService.activeMastodonAuthenticationBox context.authenticationService.activeMastodonAuthenticationBox
.receive(on: DispatchQueue.main)
.sink { [weak self] authenticationBox in .sink { [weak self] authenticationBox in
guard let self = self else { return } guard let self = self else { return }
guard let authenticationBox = authenticationBox else { self.me = authenticationBox?.authenticationRecord.object(in: context.managedObjectContext)?.user
self.domain.value = nil
self.me = nil
return
}
self.domain.value = authenticationBox.domain
self.me = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
} }
.store(in: &disposeBag) .store(in: &disposeBag)
// query relationship // bind user
let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in $user
user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) } .map { user -> UserIdentifier? in
} guard let user = user else { return nil }
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1) return MastodonUserIdentifier(domain: user.domain, userID: user.id)
// observe friendship
Publishers.CombineLatest3(
userRecord,
context.authenticationService.activeMastodonAuthenticationBox,
pendingRetryPublisher
)
.sink { [weak self] userRecord, authenticationBox, _ in
guard let self = self else { return }
guard let userRecord = userRecord,
let authenticationBox = authenticationBox
else { return }
Task {
do {
let response = try await self.updateRelationship(
record: userRecord,
authenticationBox: authenticationBox
)
// there are seconds delay after request follow before requested -> following. Query again when needs
guard let relationship = response.value.first else { return }
if relationship.requested == true {
let delay = pendingRetryPublisher.value
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let _ = self else { return }
pendingRetryPublisher.value = min(2 * delay, 60)
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function)
}
}
} catch {
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship failure: \(error.localizedDescription)")
}
} }
} .assign(to: &$userIdentifier)
.store(in: &disposeBag)
$userIdentifier.assign(to: &postsUserTimelineViewModel.$userIdentifier)
let isBlockingOrBlocked = Publishers.CombineLatest( $userIdentifier.assign(to: &repliesUserTimelineViewModel.$userIdentifier)
isBlocking, $userIdentifier.assign(to: &mediaUserTimelineViewModel.$userIdentifier)
isBlockedBy // $userIdentifier.assign(to: &profileAboutViewModel.$userIdentifier)
)
.map { $0 || $1 } // relationshipActionOptionSet
.share() // .compactMap { $0.highPriorityAction(except: []) }
// .map { $0 == .none }
// .assign(to: \.value, on: isRelationshipActionButtonHidden)
// .store(in: &disposeBag)
//
isBlockingOrBlocked //
.map { !$0 } // // query relationship
.assign(to: \.value, on: needsPagingEnabled) // let userRecord = $user.map { user -> ManagedObjectRecord<MastodonUser>? in
.store(in: &disposeBag) // user.flatMap { ManagedObjectRecord<MastodonUser>(objectID: $0.objectID) }
// }
isBlockingOrBlocked // let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
.map { $0 } //
.assign(to: \.value, on: needsImageOverlayBlurred) // // observe friendship
.store(in: &disposeBag) // Publishers.CombineLatest3(
// userRecord,
setup() // context.authenticationService.activeMastodonAuthenticationBox,
// pendingRetryPublisher
// )
// .sink { [weak self] userRecord, authenticationBox, _ in
// guard let self = self else { return }
// guard let userRecord = userRecord,
// let authenticationBox = authenticationBox
// else { return }
// Task {
// do {
// let response = try await self.updateRelationship(
// record: userRecord,
// authenticationBox: authenticationBox
// )
// // there are seconds delay after request follow before requested -> following. Query again when needs
// guard let relationship = response.value.first else { return }
// if relationship.requested == true {
// let delay = pendingRetryPublisher.value
// DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
// guard let _ = self else { return }
// pendingRetryPublisher.value = min(2 * delay, 60)
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function)
// }
// }
// } catch {
// self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Relationship] update user relationship failure: \(error.localizedDescription)")
// }
// }
// }
// .store(in: &disposeBag)
//
// let isBlockingOrBlocked = Publishers.CombineLatest(
// isBlocking,
// isBlockedBy
// )
// .map { $0 || $1 }
// .share()
//
// isBlockingOrBlocked
// .map { !$0 }
// .assign(to: \.value, on: needsPagingEnabled)
// .store(in: &disposeBag)
//
// isBlockingOrBlocked
// .map { $0 }
// .assign(to: \.value, on: needsImageOverlayBlurred)
// .store(in: &disposeBag)
//
// setup()
} }
} }
@ -245,101 +277,101 @@ extension ProfileViewModel {
} }
private func update(mastodonUser: MastodonUser?) { private func update(mastodonUser: MastodonUser?) {
self.userID.value = mastodonUser?.id // self.userID.value = mastodonUser?.id
self.bannerImageURL.value = mastodonUser?.headerImageURL() // self.bannerImageURL.value = mastodonUser?.headerImageURL()
self.avatarImageURL.value = mastodonUser?.avatarImageURL() // self.avatarImageURL.value = mastodonUser?.avatarImageURL()
self.name.value = mastodonUser?.displayNameWithFallback // self.name.value = mastodonUser?.displayNameWithFallback
self.username.value = mastodonUser?.acctWithDomain // self.username.value = mastodonUser?.acctWithDomain
self.bioDescription.value = mastodonUser?.note // self.bioDescription.value = mastodonUser?.note
self.url.value = mastodonUser?.url // self.url.value = mastodonUser?.url
self.statusesCount.value = mastodonUser.flatMap { Int($0.statusesCount) } // self.statusesCount.value = mastodonUser.flatMap { Int($0.statusesCount) }
self.followingCount.value = mastodonUser.flatMap { Int($0.followingCount) } // self.followingCount.value = mastodonUser.flatMap { Int($0.followingCount) }
self.followersCount.value = mastodonUser.flatMap { Int($0.followersCount) } // self.followersCount.value = mastodonUser.flatMap { Int($0.followersCount) }
self.protected.value = mastodonUser?.locked // self.protected.value = mastodonUser?.locked
self.suspended.value = mastodonUser?.suspended ?? false // self.suspended.value = mastodonUser?.suspended ?? false
self.fields.value = mastodonUser?.fields ?? [] // self.fields.value = mastodonUser?.fields ?? []
self.emojiMeta.value = mastodonUser?.emojis.asDictionary ?? [:] // self.emojiMeta.value = mastodonUser?.emojis.asDictionary ?? [:]
} }
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) { private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
guard let mastodonUser = mastodonUser, // guard let mastodonUser = mastodonUser,
let currentMastodonUser = currentMastodonUser else { // let currentMastodonUser = currentMastodonUser else {
// set relationship // // set relationship
self.relationshipActionOptionSet.value = .none // self.relationshipActionOptionSet.value = .none
self.isFollowedBy.value = false // self.isFollowedBy.value = false
self.isMuting.value = false // self.isMuting.value = false
self.isBlocking.value = false // self.isBlocking.value = false
self.isBlockedBy.value = false // self.isBlockedBy.value = false
//
// set bar button item state // // set bar button item state
self.isReplyBarButtonItemHidden.value = true // self.isReplyBarButtonItemHidden.value = true
self.isMoreMenuBarButtonItemHidden.value = true // self.isMoreMenuBarButtonItemHidden.value = true
self.isMeBarButtonItemsHidden.value = true // self.isMeBarButtonItemsHidden.value = true
return // return
} // }
//
if mastodonUser == currentMastodonUser { // if mastodonUser == currentMastodonUser {
self.relationshipActionOptionSet.value = [.edit] // self.relationshipActionOptionSet.value = [.edit]
// set bar button item state // // set bar button item state
self.isReplyBarButtonItemHidden.value = true // self.isReplyBarButtonItemHidden.value = true
self.isMoreMenuBarButtonItemHidden.value = true // self.isMoreMenuBarButtonItemHidden.value = true
self.isMeBarButtonItemsHidden.value = false // self.isMeBarButtonItemsHidden.value = false
} else { // } else {
// set with follow action default // // set with follow action default
var relationshipActionSet = RelationshipActionOptionSet([.follow]) // var relationshipActionSet = RelationshipActionOptionSet([.follow])
//
if mastodonUser.locked { // if mastodonUser.locked {
relationshipActionSet.insert(.request) // relationshipActionSet.insert(.request)
} // }
//
if mastodonUser.suspended { // if mastodonUser.suspended {
relationshipActionSet.insert(.suspended) // relationshipActionSet.insert(.suspended)
} // }
//
let isFollowing = mastodonUser.followingBy.contains(currentMastodonUser) // let isFollowing = mastodonUser.followingBy.contains(currentMastodonUser)
if isFollowing { // if isFollowing {
relationshipActionSet.insert(.following) // relationshipActionSet.insert(.following)
} // }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowing: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowing.description) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowing: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowing.description)
//
let isPending = mastodonUser.followRequestedBy.contains(currentMastodonUser) // let isPending = mastodonUser.followRequestedBy.contains(currentMastodonUser)
if isPending { // if isPending {
relationshipActionSet.insert(.pending) // relationshipActionSet.insert(.pending)
} // }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isPending: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isPending.description) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isPending: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isPending.description)
//
let isFollowedBy = currentMastodonUser.followingBy.contains(mastodonUser) // let isFollowedBy = currentMastodonUser.followingBy.contains(mastodonUser)
self.isFollowedBy.value = isFollowedBy // self.isFollowedBy.value = isFollowedBy
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowedBy.description) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowedBy.description)
//
let isMuting = mastodonUser.mutingBy.contains(currentMastodonUser) // let isMuting = mastodonUser.mutingBy.contains(currentMastodonUser)
if isMuting { // if isMuting {
relationshipActionSet.insert(.muting) // relationshipActionSet.insert(.muting)
} // }
self.isMuting.value = isMuting // self.isMuting.value = isMuting
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isMuting: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isMuting.description) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isMuting: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isMuting.description)
//
let isBlocking = mastodonUser.blockingBy.contains(currentMastodonUser) // let isBlocking = mastodonUser.blockingBy.contains(currentMastodonUser)
if isBlocking { // if isBlocking {
relationshipActionSet.insert(.blocking) // relationshipActionSet.insert(.blocking)
} // }
self.isBlocking.value = isBlocking // self.isBlocking.value = isBlocking
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlocking: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlocking.description) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlocking: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlocking.description)
//
let isBlockedBy = currentMastodonUser.blockingBy.contains(mastodonUser) // let isBlockedBy = currentMastodonUser.blockingBy.contains(mastodonUser)
if isBlockedBy { // if isBlockedBy {
relationshipActionSet.insert(.blocked) // relationshipActionSet.insert(.blocked)
} // }
self.isBlockedBy.value = isBlockedBy // self.isBlockedBy.value = isBlockedBy
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlockedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlockedBy.description) // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlockedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlockedBy.description)
//
self.relationshipActionOptionSet.value = relationshipActionSet // self.relationshipActionOptionSet.value = relationshipActionSet
//
// set bar button item state // // set bar button item state
self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy // self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy
self.isMoreMenuBarButtonItemHidden.value = false // self.isMoreMenuBarButtonItemHidden.value = false
self.isMeBarButtonItemsHidden.value = true // self.isMeBarButtonItemsHidden.value = true
} // }
} }
} }

View File

@ -7,40 +7,42 @@
import os.log import os.log
import UIKit import UIKit
import Pageboy import XLPagerTabStrip
import Tabman import TabBarPager
protocol ProfilePagingViewControllerDelegate: AnyObject { protocol ProfilePagingViewControllerDelegate: AnyObject {
func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int) func profilePagingViewController(_ viewController: ProfilePagingViewController, didScrollToPostCustomScrollViewContainerController customScrollViewContainerController: ScrollViewContainer, atIndex index: Int)
} }
final class ProfilePagingViewController: TabmanViewController { final class ProfilePagingViewController: ButtonBarPagerTabStripViewController, TabBarPageViewController {
weak var tabBarPageViewDelegate: TabBarPageViewDelegate?
weak var pagingDelegate: ProfilePagingViewControllerDelegate? weak var pagingDelegate: ProfilePagingViewControllerDelegate?
var viewModel: ProfilePagingViewModel! var viewModel: ProfilePagingViewModel!
// MARK: - TabBarPageViewController
// MARK: - PageboyViewControllerDelegate var currentPage: TabBarPage? {
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didCancelScrollToPageAt index: PageboyViewController.PageIndex, returnToPageAt previousIndex: PageboyViewController.PageIndex) { return viewModel.viewControllers[currentIndex]
super.pageboyViewController(pageboyViewController, didCancelScrollToPageAt: index, returnToPageAt: previousIndex)
// Fix the SDK bug for table view get row selected during swipe but cancel paging
guard previousIndex < viewModel.viewControllers.count else { return }
let viewController = viewModel.viewControllers[previousIndex]
if let tableView = viewController.scrollView as? UITableView {
for cell in tableView.visibleCells {
cell.setHighlighted(false, animated: false)
}
}
} }
override func pageboyViewController(_ pageboyViewController: PageboyViewController, didScrollToPageAt index: TabmanViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool) { var currentPageIndex: Int? {
super.pageboyViewController(pageboyViewController, didScrollToPageAt: index, direction: direction, animated: animated) currentIndex
}
// MARK: - ButtonBarPagerTabStripViewController
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
return viewModel.viewControllers
}
override func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
super.updateIndicator(for: viewController, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: progressPercentage, indexWasChanged: indexWasChanged)
let viewController = viewModel.viewControllers[index] guard indexWasChanged else { return }
(viewController as? StatusTableViewControllerNavigateable)?.overrideNavigationScrollPosition = .top let page = viewModel.viewControllers[toIndex]
pagingDelegate?.profilePagingViewController(self, didScrollToPostCustomScrollViewContainerController: viewController, atIndex: index) tabBarPageViewDelegate?.pageViewController(self, didPresentingTabBarPage: page, at: toIndex)
} }
// make key commands works // make key commands works
@ -49,7 +51,7 @@ final class ProfilePagingViewController: TabmanViewController {
} }
deinit { deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
} }
} }
@ -59,8 +61,8 @@ extension ProfilePagingViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .clear // view.backgroundColor = .clear
dataSource = viewModel // dataSource = viewModel
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
@ -74,17 +76,17 @@ extension ProfilePagingViewController {
// workaround to fix tab man responder chain issue // workaround to fix tab man responder chain issue
extension ProfilePagingViewController { extension ProfilePagingViewController {
override var keyCommands: [UIKeyCommand]? { // override var keyCommands: [UIKeyCommand]? {
return currentViewController?.keyCommands // return currentPage?.keyCommands
} // }
//
@objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) { // @objc func navigateKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender) // (currentViewController as? StatusTableViewControllerNavigateable)?.navigateKeyCommandHandlerRelay(sender)
//
} // }
//
@objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) { // @objc func statusKeyCommandHandlerRelay(_ sender: UIKeyCommand) {
(currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender) // (currentViewController as? StatusTableViewControllerNavigateable)?.statusKeyCommandHandlerRelay(sender)
} // }
} }

View File

@ -7,10 +7,9 @@
import os.log import os.log
import UIKit import UIKit
import Pageboy
import Tabman
import MastodonAsset import MastodonAsset
import MastodonLocalization import MastodonLocalization
import TabBarPager
final class ProfilePagingViewModel: NSObject { final class ProfilePagingViewModel: NSObject {
@ -32,7 +31,7 @@ final class ProfilePagingViewModel: NSObject {
super.init() super.init()
} }
var viewControllers: [ScrollViewContainer] { var viewControllers: [UIViewController & TabBarPage] {
return [ return [
postUserTimelineViewController, postUserTimelineViewController,
repliesUserTimelineViewController, repliesUserTimelineViewController,
@ -41,42 +40,42 @@ final class ProfilePagingViewModel: NSObject {
] ]
} }
let barItems: [TMBarItemable] = { // let barItems: [TMBarItemable] = {
let items = [ // let items = [
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts), // TMBarItem(title: L10n.Scene.Profile.SegmentedControl.posts),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.postsAndReplies), // TMBarItem(title: L10n.Scene.Profile.SegmentedControl.postsAndReplies),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media), // TMBarItem(title: L10n.Scene.Profile.SegmentedControl.media),
TMBarItem(title: L10n.Scene.Profile.SegmentedControl.about), // TMBarItem(title: L10n.Scene.Profile.SegmentedControl.about),
] // ]
return items // return items
}() // }()
deinit { deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
} }
} }
// MARK: - PageboyViewControllerDataSource //// MARK: - PageboyViewControllerDataSource
extension ProfilePagingViewModel: PageboyViewControllerDataSource { //extension ProfilePagingViewModel: PageboyViewControllerDataSource {
//
func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { // func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int {
return viewControllers.count // return viewControllers.count
} // }
//
func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? { // func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? {
return viewControllers[index] // return viewControllers[index]
} // }
//
func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { // func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? {
return .first // return .first
} // }
//
} //}
//
// MARK: - TMBarDataSource //// MARK: - TMBarDataSource
extension ProfilePagingViewModel: TMBarDataSource { //extension ProfilePagingViewModel: TMBarDataSource {
func barItem(for bar: TMBar, at index: Int) -> TMBarItemable { // func barItem(for bar: TMBar, at index: Int) -> TMBarItemable {
return barItems[index] // return barItems[index]
} // }
} //}

View File

@ -11,6 +11,8 @@ import AVKit
import Combine import Combine
import CoreDataStack import CoreDataStack
import GameplayKit import GameplayKit
import TabBarPager
import XLPagerTabStrip
final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { final class UserTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
@ -143,7 +145,14 @@ extension UserTimelineViewController: UITableViewDelegate, AutoGenerateTableView
// MARK: - CustomScrollViewContainerController // MARK: - CustomScrollViewContainerController
extension UserTimelineViewController: ScrollViewContainer { extension UserTimelineViewController: ScrollViewContainer {
var scrollView: UIScrollView? { return tableView } var scrollView: UIScrollView { return tableView }
}
// MARK: - TabBarPage
extension UserTimelineViewController: TabBarPage {
var pageScrollView: UIScrollView {
scrollView
}
} }
// MARK: - StatusTableViewCellDelegate // MARK: - StatusTableViewCellDelegate
@ -165,3 +174,10 @@ extension UserTimelineViewController: StatusTableViewControllerNavigateable {
statusKeyCommandHandler(sender) statusKeyCommandHandler(sender)
} }
} }
// MARK: - IndicatorInfoProvider
extension UserTimelineViewController: IndicatorInfoProvider {
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title: "Hello")
}
}

View File

@ -30,17 +30,14 @@ extension UserTimelineViewModel {
snapshot.appendSections([.main]) snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot) diffableDataSource?.apply(snapshot)
// trigger user timeline loading // trigger timeline reloading
Publishers.CombineLatest( $userIdentifier
$domain.removeDuplicates(), .receive(on: DispatchQueue.main)
$userID.removeDuplicates() .sink { [weak self] _ in
) guard let self = self else { return }
.receive(on: DispatchQueue.main) self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
.sink { [weak self] _ in }
guard let self = self else { return } .store(in: &disposeBag)
self.stateMachine.enter(UserTimelineViewModel.State.Reloading.self)
}
.store(in: &disposeBag)
let needsTimelineHidden = Publishers.CombineLatest3( let needsTimelineHidden = Publishers.CombineLatest3(
isBlocking, isBlocking,

View File

@ -50,7 +50,7 @@ extension UserTimelineViewModel.State {
guard let viewModel = viewModel else { return false } guard let viewModel = viewModel else { return false }
switch stateClass { switch stateClass {
case is Reloading.Type: case is Reloading.Type:
return viewModel.userID != nil return viewModel.userIdentifier != nil
default: default:
return false return false
} }
@ -132,7 +132,7 @@ extension UserTimelineViewModel.State {
let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last
guard let userID = viewModel.userID, !userID.isEmpty else { guard let userID = viewModel.userIdentifier?.userID, !userID.isEmpty else {
stateMachine.enter(Fail.self) stateMachine.enter(Fail.self)
return return
} }

View File

@ -19,8 +19,7 @@ final class UserTimelineViewModel {
// input // input
let context: AppContext let context: AppContext
@Published var domain: String? @Published var userIdentifier: UserIdentifier?
@Published var userID: String?
@Published var queryFilter: QueryFilter @Published var queryFilter: QueryFilter
let statusFetchedResultsController: StatusFetchedResultsController let statusFetchedResultsController: StatusFetchedResultsController
let listBatchFetchViewModel = ListBatchFetchViewModel() let listBatchFetchViewModel = ListBatchFetchViewModel()
@ -48,30 +47,25 @@ final class UserTimelineViewModel {
init( init(
context: AppContext, context: AppContext,
domain: String?,
userID: String?,
queryFilter: QueryFilter queryFilter: QueryFilter
) { ) {
self.context = context self.context = context
self.statusFetchedResultsController = StatusFetchedResultsController( self.statusFetchedResultsController = StatusFetchedResultsController(
managedObjectContext: context.managedObjectContext, managedObjectContext: context.managedObjectContext,
domain: domain, domain: nil,
additionalTweetPredicate: Status.notDeleted() additionalTweetPredicate: nil
) )
self.domain = domain
self.userID = userID
self.queryFilter = queryFilter self.queryFilter = queryFilter
// super.init() // super.init()
$domain context.authenticationService.activeMastodonAuthenticationBox
.map { $0?.domain }
.assign(to: \.value, on: statusFetchedResultsController.domain) .assign(to: \.value, on: statusFetchedResultsController.domain)
.store(in: &disposeBag) .store(in: &disposeBag)
} }
deinit { deinit {
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
} }
} }
@ -92,5 +86,4 @@ extension UserTimelineViewModel {
self.onlyMedia = onlyMedia self.onlyMedia = onlyMedia
} }
} }
} }

View File

@ -0,0 +1,28 @@
//
// UserIdentifier.swift
//
//
// Created by MainasuK on 2022-5-13.
//
import Foundation
import MastodonSDK
public protocol UserIdentifier {
var domain: String { get }
var userID: Mastodon.Entity.Account.ID { get }
}
public struct MastodonUserIdentifier: UserIdentifier {
public let domain: String
public var userID: Mastodon.Entity.Account.ID
public init(
domain: String,
userID: Mastodon.Entity.Account.ID
) {
self.domain = domain
self.userID = userID
}
}

View File

@ -1,14 +0,0 @@
//
// UserIdentifier.swift
//
//
// Created by MainasuK on 2022-1-12.
//
import Foundation
import MastodonSDK
public protocol UserIdentifier {
var domain: String { get }
var userID: Mastodon.Entity.Account.ID { get }
}

View File

@ -8,6 +8,7 @@ target 'Mastodon' do
# UI # UI
pod 'UITextField+Shake', '~> 1.2' pod 'UITextField+Shake', '~> 1.2'
pod 'XLPagerTabStrip', '~> 9.0.0'
# misc # misc
pod 'SwiftGen', '~> 6.4.0' pod 'SwiftGen', '~> 6.4.0'

View File

@ -8,6 +8,7 @@ PODS:
- Sourcery/CLI-Only (1.6.1) - Sourcery/CLI-Only (1.6.1)
- SwiftGen (6.4.0) - SwiftGen (6.4.0)
- "UITextField+Shake (1.2.1)" - "UITextField+Shake (1.2.1)"
- XLPagerTabStrip (9.0.0)
DEPENDENCIES: DEPENDENCIES:
- DateToolsSwift (~> 5.0.0) - DateToolsSwift (~> 5.0.0)
@ -17,6 +18,7 @@ DEPENDENCIES:
- Sourcery (~> 1.6.1) - Sourcery (~> 1.6.1)
- SwiftGen (~> 6.4.0) - SwiftGen (~> 6.4.0)
- "UITextField+Shake (~> 1.2)" - "UITextField+Shake (~> 1.2)"
- XLPagerTabStrip (~> 9.0.0)
SPEC REPOS: SPEC REPOS:
trunk: trunk:
@ -26,6 +28,7 @@ SPEC REPOS:
- Sourcery - Sourcery
- SwiftGen - SwiftGen
- "UITextField+Shake" - "UITextField+Shake"
- XLPagerTabStrip
EXTERNAL SOURCES: EXTERNAL SOURCES:
Keys: Keys:
@ -39,7 +42,8 @@ SPEC CHECKSUMS:
Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac Sourcery: f3759f803bd0739f74fc92a4341eed0473ce61ac
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
XLPagerTabStrip: 61c57fd61f611ee5f01ff1495ad6fbee8bf496c5
PODFILE CHECKSUM: 335d0ca70493d4c280d0f8fd7f26fe9be6a4e289 PODFILE CHECKSUM: 1ac960a2c981ef98f7c24a3bba57bdabc1f66103
COCOAPODS: 1.11.3 COCOAPODS: 1.11.3