Skip to content

Commit 9a32511

Browse files
Explore using UITableView for the scrolling list
Individual rows still use the new SwiftUI layout. This makes scrolling feel smoother, since changes to row heights as content loads are animated in a way that makes sense rather than jumping the scroll. Post interaction is broken, will need to be restored. Contributes to IOS-409
1 parent 639d367 commit 9a32511

4 files changed

Lines changed: 115 additions & 7 deletions

File tree

Mastodon.xcodeproj/project.pbxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,7 @@
13091309
Notifications/NotificationRowViewModel.swift,
13101310
Notifications/NotificationsCacheManager.swift,
13111311
Timeline/HomeTimelineListViewController.swift,
1312+
Timeline/HomeTimelineTableViewController.swift,
13121313
Timeline/TimelineFeedLoader.swift,
13131314
);
13141315
target = DB427DD125BAA00100D1B89D /* Mastodon */;

Mastodon/In Progress New Layout and Datamodel/Timeline/HomeTimelineListViewController.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ enum MastodonTimelineOverlayView {
336336
}
337337

338338
@MainActor
339-
private class HomeTimelineListViewModel: ObservableObject {
339+
class HomeTimelineListViewModel: ObservableObject {
340340
public var parentVcPresentScene: ((SceneCoordinator.Scene, SceneCoordinator.Transition) -> ())?
341341
private var authenticatedUser: MastodonAuthenticationBox?
342342
private var instanceConfiguration: MastodonAuthentication.InstanceConfiguration?
@@ -375,7 +375,7 @@ private class HomeTimelineListViewModel: ObservableObject {
375375
private var feedLoaderResultsSubscription: AnyCancellable?
376376
private var feedLoaderErrorSubscription: AnyCancellable?
377377

378-
var scrollManager: ScrollManager?
378+
fileprivate var scrollManager: ScrollManager?
379379

380380
private var tailItemIds = [String]()
381381
private let displayPrepBatchSize = 10
@@ -1049,8 +1049,8 @@ extension MastodonTimelineOverlayView {
10491049
}
10501050
}
10511051

1052-
private struct HomeTimelinePostRowView: View {
1053-
1052+
struct HomeTimelinePostRowView: View {
1053+
10541054
@Environment(MastodonPostViewModel.self) private var viewModel
10551055
@ObservedObject var contentConcealModel: ContentConcealViewModel
10561056
let contentWidth: CGFloat
@@ -1066,7 +1066,7 @@ private struct HomeTimelinePostRowView: View {
10661066
viewModel.socialContextHeader
10671067

10681068
HStack(alignment: .top) {
1069-
1069+
10701070
AvatarView(size: .large, authorAvatarUrl: author?.avatarURL ?? viewModel.initialDisplayInfo.actionableAuthorStaticAvatar, goToProfile: {
10711071
goToProfile(author)
10721072
})
@@ -1144,7 +1144,7 @@ private struct HomeTimelinePostRowView: View {
11441144
}
11451145
}
11461146

1147-
@ViewBuilder func componentView(_ component: PostViewComponent) -> some View {
1147+
@ViewBuilder fileprivate func componentView(_ component: PostViewComponent) -> some View {
11481148
switch component {
11491149
case .content(let string):
11501150
PostContentView(text: string)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright © 2025 Mastodon gGmbH. All rights reserved.
2+
3+
import Combine
4+
import SwiftUI
5+
import UIKit
6+
7+
private enum Section {
8+
case main
9+
}
10+
11+
private var avatarSize = AvatarSize.large
12+
13+
@MainActor
14+
class HomeTimelineTableViewController: UITableViewController {
15+
16+
var viewModel = HomeTimelineListViewModel(timeline: .following)
17+
fileprivate var datasource: TimelineDataSource?
18+
fileprivate var viewModelSubscriptions = Set<AnyCancellable>()
19+
20+
init() {
21+
super.init(style: .plain)
22+
23+
viewModel.$timelineItems.sink { [weak self] items in
24+
guard let self else { return }
25+
var replacementSnapshot = NSDiffableDataSourceSnapshot<Section, TimelineItem>()
26+
replacementSnapshot.appendSections([.main])
27+
replacementSnapshot.appendItems(items)
28+
datasource?.apply(replacementSnapshot)
29+
30+
if items.count > 10 {
31+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
32+
self.tableView.scrollToRow(at: IndexPath(row: items.count / 2, section: 0), at: .top, animated: false)
33+
}
34+
}
35+
}.store(in: &viewModelSubscriptions)
36+
}
37+
38+
override func viewDidLoad() {
39+
tableView.delegate = self
40+
tableView.register(MastodonPostRowHostingCell.self, forCellReuseIdentifier: "cell")
41+
datasource = TimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, itemIdentifier in
42+
guard let self else { return UITableViewCell() }
43+
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MastodonPostRowHostingCell
44+
cell.contentWidth = self.postContentWidth(for: self.view.frame.size.width)
45+
switch itemIdentifier {
46+
case .loadingIndicator, .missingPosts:
47+
cell.configureToShowSomething()
48+
case .post(let viewModel):
49+
cell.viewModel = viewModel
50+
}
51+
return cell
52+
})
53+
tableView.dataSource = datasource
54+
55+
var snapshot = datasource?.snapshot()
56+
snapshot?.appendSections([.main])
57+
datasource?.apply(snapshot!)
58+
}
59+
60+
required init?(coder: NSCoder) {
61+
fatalError("init(coder:) has not been implemented")
62+
}
63+
64+
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
65+
super.viewWillTransition(to: size, with: coordinator)
66+
if let snapshot = datasource?.snapshot() {
67+
datasource?.applySnapshotUsingReloadData(snapshot)
68+
}
69+
}
70+
71+
func postContentWidth(for viewWidth: CGFloat) -> CGFloat {
72+
let usableWidth = viewWidth
73+
let contentWidth = max(1, usableWidth - (spacingBetweenGutterAndContent * 3) - avatarSize)
74+
return contentWidth
75+
}
76+
}
77+
78+
class MastodonPostRowHostingCell: UITableViewCell {
79+
var contentWidth: CGFloat = 10
80+
var widthConstraint: NSLayoutConstraint?
81+
82+
var viewModel: MastodonPostViewModel? {
83+
didSet {
84+
self.contentView.backgroundColor = .clear
85+
self.contentConfiguration = UIHostingConfiguration(content: {
86+
HomeTimelinePostRowView(contentConcealModel: .alwaysShow,
87+
contentWidth: contentWidth)
88+
.environment(viewModel)
89+
})
90+
}
91+
}
92+
93+
func configureToShowSomething() {
94+
self.contentView.backgroundColor = .yellow
95+
if self.widthConstraint == nil {
96+
widthConstraint = widthAnchor.constraint(equalToConstant: contentWidth)
97+
NSLayoutConstraint.activate([widthConstraint!])
98+
}
99+
widthConstraint?.constant = contentWidth
100+
setNeedsUpdateConstraints()
101+
}
102+
}
103+
104+
fileprivate class TimelineDataSource: UITableViewDiffableDataSource<Section, TimelineItem> {
105+
var contentWidth: CGFloat = 10
106+
}

Mastodon/Scene/Root/MainTab/MainTabBarController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ class MainTabBarController: UITabBarController {
5252
self.authenticationBox = authenticationBox
5353

5454
if UserDefaults.standard.testNewHomeTimeline {
55-
homeTimelineViewController = HomeTimelineListViewController()
55+
homeTimelineViewController = HomeTimelineTableViewController()
56+
// homeTimelineViewController = HomeTimelineListViewController()
5657
} else {
5758
homeTimelineViewController = HomeTimelineViewController()
5859
}

0 commit comments

Comments
 (0)