2021-02-07 07:42:50 +01:00
//
// H o m e T i m e l i n e V i e w C o n t r o l l e r . s w i f t
// M a s t o d o n
//
// C r e a t e d b y s x i a o j i a n o n 2 0 2 1 / 2 / 5 .
//
import os . log
import UIKit
import AVKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import AlamofireImage
2021-11-03 07:24:52 +01:00
import StoreKit
2022-01-27 14:23:39 +01:00
import MastodonAsset
import MastodonLocalization
2021-02-07 07:42:50 +01:00
2021-04-28 09:02:34 +02:00
final class HomeTimelineViewController : UIViewController , NeedsDependency , MediaPreviewableViewController {
2021-02-07 07:42:50 +01:00
2021-09-28 13:58:14 +02:00
let logger = Logger ( subsystem : " HomeTimelineViewController " , category : " UI " )
2021-02-07 07:42:50 +01:00
weak var context : AppContext ! { willSet { precondition ( ! isViewLoaded ) } }
weak var coordinator : SceneCoordinator ! { willSet { precondition ( ! isViewLoaded ) } }
var disposeBag = Set < AnyCancellable > ( )
private ( set ) lazy var viewModel = HomeTimelineViewModel ( context : context )
2021-04-28 09:02:34 +02:00
let mediaPreviewTransitionController = MediaPreviewTransitionController ( )
2021-07-09 07:16:58 +02:00
let friendsAssetImageView : UIImageView = {
let imageView = UIImageView ( )
imageView . image = Asset . Asset . friends . image
imageView . contentMode = . scaleAspectFill
return imageView
} ( )
2021-04-28 09:02:34 +02:00
2021-04-21 08:46:31 +02:00
lazy var emptyView : UIStackView = {
let emptyView = UIStackView ( )
emptyView . axis = . vertical
emptyView . distribution = . fill
emptyView . isLayoutMarginsRelativeArrangement = true
return emptyView
} ( )
2021-03-29 11:44:52 +02:00
let titleView = HomeTimelineNavigationBarTitleView ( )
2021-02-23 09:45:00 +01:00
let settingBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( )
2021-10-29 08:58:09 +02:00
barButtonItem . tintColor = ThemeService . tintColor
2021-02-23 09:45:00 +01:00
barButtonItem . image = UIImage ( systemName : " gear " ) ? . withRenderingMode ( . alwaysTemplate )
return barButtonItem
} ( )
let composeBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( )
2021-10-29 08:58:09 +02:00
barButtonItem . tintColor = ThemeService . tintColor
2021-02-23 09:45:00 +01:00
barButtonItem . image = UIImage ( systemName : " square.and.pencil " ) ? . withRenderingMode ( . alwaysTemplate )
return barButtonItem
} ( )
2021-02-07 07:42:50 +01:00
let tableView : UITableView = {
let tableView = ControlContainableTableView ( )
2021-02-23 08:16:55 +01:00
tableView . register ( StatusTableViewCell . self , forCellReuseIdentifier : String ( describing : StatusTableViewCell . self ) )
2021-02-07 07:42:50 +01:00
tableView . register ( TimelineMiddleLoaderTableViewCell . self , forCellReuseIdentifier : String ( describing : TimelineMiddleLoaderTableViewCell . self ) )
tableView . register ( TimelineBottomLoaderTableViewCell . self , forCellReuseIdentifier : String ( describing : TimelineBottomLoaderTableViewCell . self ) )
tableView . rowHeight = UITableView . automaticDimension
tableView . separatorStyle = . none
tableView . backgroundColor = . clear
return tableView
} ( )
2021-03-29 11:44:52 +02:00
let publishProgressView : UIProgressView = {
let progressView = UIProgressView ( progressViewStyle : . bar )
progressView . alpha = 0
return progressView
} ( )
2021-02-07 07:42:50 +01:00
let refreshControl = UIRefreshControl ( )
deinit {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
}
2021-02-23 09:45:00 +01:00
2021-02-07 07:42:50 +01:00
}
extension HomeTimelineViewController {
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2021-07-05 10:07:17 +02:00
2021-02-22 09:20:44 +01:00
title = L10n . Scene . HomeTimeline . title
2021-07-06 12:00:39 +02:00
view . backgroundColor = ThemeService . shared . currentTheme . value . secondarySystemBackgroundColor
2021-07-05 10:07:17 +02:00
ThemeService . shared . currentTheme
. receive ( on : RunLoop . main )
. sink { [ weak self ] theme in
guard let self = self else { return }
self . view . backgroundColor = theme . secondarySystemBackgroundColor
}
. store ( in : & disposeBag )
2022-02-10 09:43:26 +01:00
viewModel . $ displaySettingBarButtonItem
2021-09-24 13:58:50 +02:00
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] displaySettingBarButtonItem in
guard let self = self else { return }
2021-09-26 12:29:08 +02:00
#if DEBUG
// d i s p l a y d e b u g m e n u
self . navigationItem . leftBarButtonItem = {
let barButtonItem = UIBarButtonItem ( )
barButtonItem . image = UIImage ( systemName : " ellipsis.circle " )
barButtonItem . menu = self . debugMenu
return barButtonItem
} ( )
#else
2021-09-24 13:58:50 +02:00
self . navigationItem . leftBarButtonItem = displaySettingBarButtonItem ? self . settingBarButtonItem : nil
2021-09-26 12:29:08 +02:00
#endif
2021-09-24 13:58:50 +02:00
}
. store ( in : & disposeBag )
2021-10-28 13:17:41 +02:00
#if DEBUG
// l o n g p r e s s t o t r i g g e r d e b u g m e n u
settingBarButtonItem . menu = debugMenu
#else
settingBarButtonItem . target = self
settingBarButtonItem . action = #selector ( HomeTimelineViewController . settingBarButtonItemPressed ( _ : ) )
#endif
2022-02-10 09:43:26 +01:00
viewModel . $ displayComposeBarButtonItem
2021-10-28 13:17:41 +02:00
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] displayComposeBarButtonItem in
guard let self = self else { return }
self . navigationItem . rightBarButtonItem = displayComposeBarButtonItem ? self . composeBarButtonItem : nil
}
. store ( in : & disposeBag )
composeBarButtonItem . target = self
composeBarButtonItem . action = #selector ( HomeTimelineViewController . composeBarButtonItemPressed ( _ : ) )
2021-03-29 11:44:52 +02:00
navigationItem . titleView = titleView
titleView . delegate = self
viewModel . homeTimelineNavigationBarTitleViewModel . state
2021-06-17 10:31:34 +02:00
. removeDuplicates ( )
2021-06-17 13:43:16 +02:00
. receive ( on : DispatchQueue . main )
2021-03-29 11:44:52 +02:00
. sink { [ weak self ] state in
guard let self = self else { return }
self . titleView . configure ( state : state )
}
. store ( in : & disposeBag )
2021-11-03 07:24:52 +01:00
viewModel . homeTimelineNavigationBarTitleViewModel . state
. removeDuplicates ( )
. filter { $0 = = . publishedButton }
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] _ in
guard let self = self else { return }
guard UserDefaults . shared . lastVersionPromptedForReview = = nil else { return }
guard UserDefaults . shared . processCompletedCount > 3 else { return }
guard let windowScene = self . view . window ? . windowScene else { return }
let version = UIApplication . appVersion ( )
UserDefaults . shared . lastVersionPromptedForReview = version
SKStoreReviewController . requestReview ( in : windowScene )
}
. store ( in : & disposeBag )
2021-02-07 07:42:50 +01:00
tableView . refreshControl = refreshControl
refreshControl . addTarget ( self , action : #selector ( HomeTimelineViewController . refreshControlValueChanged ( _ : ) ) , for : . valueChanged )
tableView . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( tableView )
NSLayoutConstraint . activate ( [
tableView . topAnchor . constraint ( equalTo : view . topAnchor ) ,
tableView . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
tableView . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
tableView . bottomAnchor . constraint ( equalTo : view . bottomAnchor ) ,
] )
2021-03-29 11:44:52 +02:00
publishProgressView . translatesAutoresizingMaskIntoConstraints = false
view . addSubview ( publishProgressView )
NSLayoutConstraint . activate ( [
publishProgressView . topAnchor . constraint ( equalTo : view . layoutMarginsGuide . topAnchor ) ,
publishProgressView . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
publishProgressView . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
] )
2021-02-07 07:42:50 +01:00
viewModel . tableView = tableView
viewModel . contentOffsetAdjustableTimelineViewControllerDelegate = self
tableView . delegate = self
viewModel . setupDiffableDataSource (
2022-01-27 14:23:39 +01:00
tableView : tableView ,
2021-03-03 09:12:48 +01:00
statusTableViewCellDelegate : self ,
2021-02-07 07:42:50 +01:00
timelineMiddleLoaderTableViewCellDelegate : self
)
2022-02-10 09:43:26 +01:00
// s e t u p b a t c h f e t c h
viewModel . listBatchFetchViewModel . setup ( scrollView : tableView )
viewModel . listBatchFetchViewModel . shouldFetch
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] _ in
guard let self = self else { return }
guard self . view . window != nil else { return }
self . viewModel . loadOldestStateMachine . enter ( HomeTimelineViewModel . LoadOldestState . Loading . self )
}
. store ( in : & disposeBag )
2021-02-07 07:42:50 +01:00
// b i n d r e f r e s h c o n t r o l
2022-01-27 14:23:39 +01:00
viewModel . didLoadLatest
2021-02-07 07:42:50 +01:00
. receive ( on : DispatchQueue . main )
2022-01-27 14:23:39 +01:00
. sink { [ weak self ] _ in
2021-02-07 07:42:50 +01:00
guard let self = self else { return }
2022-01-27 14:23:39 +01:00
UIView . animate ( withDuration : 0.5 ) { [ weak self ] in
guard let self = self else { return }
self . refreshControl . endRefreshing ( )
} completion : { _ in }
2021-02-07 07:42:50 +01:00
}
. store ( in : & disposeBag )
2021-03-29 11:44:52 +02:00
viewModel . homeTimelineNavigationBarTitleViewModel . publishingProgress
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] progress in
guard let self = self else { return }
guard progress > 0 else {
let dismissAnimator = UIViewPropertyAnimator ( duration : 0.1 , curve : . easeInOut )
dismissAnimator . addAnimations {
self . publishProgressView . alpha = 0
}
dismissAnimator . addCompletion { _ in
self . publishProgressView . setProgress ( 0 , animated : false )
}
dismissAnimator . startAnimation ( )
return
}
if self . publishProgressView . alpha = = 0 {
let progressAnimator = UIViewPropertyAnimator ( duration : 0.1 , curve : . easeOut )
progressAnimator . addAnimations {
self . publishProgressView . alpha = 1
}
progressAnimator . startAnimation ( )
}
self . publishProgressView . setProgress ( progress , animated : true )
}
. store ( in : & disposeBag )
2021-04-22 09:45:32 +02:00
viewModel . timelineIsEmpty
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isEmpty in
if isEmpty {
self ? . showEmptyView ( )
} else {
self ? . emptyView . removeFromSuperview ( )
}
}
. store ( in : & disposeBag )
2021-09-28 13:58:14 +02:00
NotificationCenter . default
. publisher ( for : . statusBarTapped , object : nil )
. throttle ( for : 0.5 , scheduler : DispatchQueue . main , latest : false )
. sink { [ weak self ] notification in
guard let self = self else { return }
guard let _ = self . view . window else { return } // d i s p l a y i n g
// h t t p s : / / d e v e l o p e r . l i m n e o s . n e t / i n d e x . p h p ? i o s = 1 3 . 1 . 3 & f r a m e w o r k = U I K i t C o r e . f r a m e w o r k & h e a d e r = U I S t a t u s B a r T a p A c t i o n . h
guard let action = notification . object as AnyObject ? ,
let xPosition = action . value ( forKey : " xPosition " ) as ? Double
else { return }
let viewFrameInWindow = self . view . convert ( self . view . frame , to : nil )
guard xPosition >= viewFrameInWindow . minX && xPosition <= viewFrameInWindow . maxX else { return }
// w o r k s o n i O S 1 4
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : receive notification \( xPosition ) " )
// c h e c k i f s c r o l l t o t o p
guard self . shouldRestoreScrollPosition ( ) else { return }
self . restorePositionWhenScrollToTop ( )
}
. store ( in : & disposeBag )
2021-02-07 07:42:50 +01:00
}
2021-04-01 08:39:15 +02:00
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
2022-01-27 14:23:39 +01:00
refreshControl . endRefreshing ( )
tableView . deselectRow ( with : transitionCoordinator , animated : animated )
2021-04-13 13:46:42 +02:00
2021-04-01 08:39:15 +02:00
// n e e d s t r i g g e r m a n u a l l y a f t e r o n b o a r d i n g d i s m i s s
2022-01-27 14:23:39 +01:00
setNeedsStatusBarAppearanceUpdate ( )
2021-04-01 08:39:15 +02:00
}
2021-02-07 07:42:50 +01:00
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
viewModel . viewDidAppear . send ( )
2022-02-10 09:43:26 +01:00
if let timestamp = viewModel . lastAutomaticFetchTimestamp {
2021-09-28 13:58:14 +02:00
let now = Date ( )
if now . timeIntervalSince ( timestamp ) > 60 {
2022-02-10 09:43:26 +01:00
self . viewModel . lastAutomaticFetchTimestamp = now
2021-09-28 13:58:14 +02:00
self . viewModel . homeTimelineNeedRefresh . send ( )
} else {
// d o n o t h i n g
}
} else {
2021-07-07 14:13:33 +02:00
self . viewModel . homeTimelineNeedRefresh . send ( )
2021-02-07 07:42:50 +01:00
}
}
override func viewWillTransition ( to size : CGSize , with coordinator : UIViewControllerTransitionCoordinator ) {
super . viewWillTransition ( to : size , with : coordinator )
coordinator . animate { _ in
// d o n o t h i n g
} completion : { _ in
// f i x A u t o L a y o u t c e l l h e i g h t n o t u p d a t e a f t e r r o t a t e i s s u e
self . viewModel . cellFrameCache . removeAllObjects ( )
self . tableView . reloadData ( )
}
}
}
extension HomeTimelineViewController {
2021-04-21 08:46:31 +02:00
func showEmptyView ( ) {
if emptyView . superview != nil {
return
}
view . addSubview ( emptyView )
emptyView . translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint . activate ( [
2021-07-09 07:16:58 +02:00
emptyView . topAnchor . constraint ( equalTo : view . layoutMarginsGuide . topAnchor ) ,
emptyView . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
emptyView . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
emptyView . bottomAnchor . constraint ( equalTo : view . layoutMarginsGuide . bottomAnchor )
2021-04-21 08:46:31 +02:00
] )
2021-04-21 11:58:56 +02:00
if emptyView . arrangedSubviews . count > 0 {
return
}
2021-04-21 08:46:31 +02:00
let findPeopleButton : PrimaryActionButton = {
let button = PrimaryActionButton ( )
button . setTitle ( L10n . Common . Controls . Actions . findPeople , for : . normal )
button . addTarget ( self , action : #selector ( HomeTimelineViewController . findPeopleButtonPressed ( _ : ) ) , for : . touchUpInside )
return button
} ( )
NSLayoutConstraint . activate ( [
findPeopleButton . heightAnchor . constraint ( equalToConstant : 46 )
] )
let manuallySearchButton : HighlightDimmableButton = {
let button = HighlightDimmableButton ( )
button . titleLabel ? . font = UIFontMetrics ( forTextStyle : . headline ) . scaledFont ( for : . systemFont ( ofSize : 15 , weight : . semibold ) )
button . setTitle ( L10n . Common . Controls . Actions . manuallySearch , for : . normal )
button . setTitleColor ( Asset . Colors . brandBlue . color , for : . normal )
button . addTarget ( self , action : #selector ( HomeTimelineViewController . manuallySearchButtonPressed ( _ : ) ) , for : . touchUpInside )
return button
} ( )
2021-07-09 07:16:58 +02:00
let topPaddingView = UIView ( )
let bottomPaddingView = UIView ( )
emptyView . addArrangedSubview ( topPaddingView )
emptyView . addArrangedSubview ( friendsAssetImageView )
emptyView . addArrangedSubview ( bottomPaddingView )
topPaddingView . translatesAutoresizingMaskIntoConstraints = false
bottomPaddingView . translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint . activate ( [
topPaddingView . heightAnchor . constraint ( equalTo : bottomPaddingView . heightAnchor , multiplier : 0.8 ) ,
] )
let buttonContainerStackView = UIStackView ( )
emptyView . addArrangedSubview ( buttonContainerStackView )
buttonContainerStackView . isLayoutMarginsRelativeArrangement = true
buttonContainerStackView . layoutMargins = UIEdgeInsets ( top : 0 , left : 32 , bottom : 22 , right : 32 )
buttonContainerStackView . axis = . vertical
buttonContainerStackView . spacing = 17
buttonContainerStackView . addArrangedSubview ( findPeopleButton )
buttonContainerStackView . addArrangedSubview ( manuallySearchButton )
2021-04-21 08:46:31 +02:00
}
}
extension HomeTimelineViewController {
@objc private func findPeopleButtonPressed ( _ sender : PrimaryActionButton ) {
2022-01-27 14:23:39 +01:00
// TODO:
2022-01-29 12:51:40 +01:00
// l e t v i e w M o d e l = S u g g e s t i o n A c c o u n t V i e w M o d e l ( c o n t e x t : c o n t e x t )
2022-01-27 14:23:39 +01:00
// v i e w M o d e l . d e l e g a t e = s e l f . v i e w M o d e l
// c o o r d i n a t o r . p r e s e n t ( s c e n e : . s u g g e s t i o n A c c o u n t ( v i e w M o d e l : v i e w M o d e l ) , f r o m : s e l f , t r a n s i t i o n : . m o d a l ( a n i m a t e d : t r u e , c o m p l e t i o n : n i l ) )
2021-04-21 08:46:31 +02:00
}
@objc private func manuallySearchButtonPressed ( _ sender : UIButton ) {
2021-07-15 14:38:32 +02:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
let searchDetailViewModel = SearchDetailViewModel ( )
coordinator . present ( scene : . searchDetail ( viewModel : searchDetailViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-04-21 08:46:31 +02:00
}
2021-02-07 07:42:50 +01:00
2021-02-23 09:45:00 +01:00
@objc private func settingBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2021-04-26 10:57:50 +02:00
guard let setting = context . settingService . currentSetting . value else { return }
let settingsViewModel = SettingsViewModel ( context : context , setting : setting )
coordinator . present ( scene : . settings ( viewModel : settingsViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-02-23 09:45:00 +01:00
}
@objc private func composeBarButtonItemPressed ( _ sender : UIBarButtonItem ) {
2021-02-07 07:42:50 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2022-01-27 14:23:39 +01:00
guard let authenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value else { return }
let composeViewModel = ComposeViewModel (
context : context ,
composeKind : . post ,
authenticationBox : authenticationBox
)
2021-03-11 08:41:27 +01:00
coordinator . present ( scene : . compose ( viewModel : composeViewModel ) , from : self , transition : . modal ( animated : true , completion : nil ) )
2021-02-07 07:42:50 +01:00
}
@objc private func refreshControlValueChanged ( _ sender : UIRefreshControl ) {
guard viewModel . loadLatestStateMachine . enter ( HomeTimelineViewModel . LoadLatestState . Loading . self ) else {
sender . endRefreshing ( )
return
}
}
2021-02-26 11:27:47 +01:00
@objc func signOutAction ( _ sender : UIAction ) {
guard let activeMastodonAuthenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value else {
return
}
context . authenticationService . signOutMastodonUser (
domain : activeMastodonAuthenticationBox . domain ,
userID : activeMastodonAuthenticationBox . userID
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] result in
guard let self = self else { return }
switch result {
case . failure ( let error ) :
assertionFailure ( error . localizedDescription )
case . success ( let isSignOut ) :
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: sign out %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , isSignOut ? " success " : " fail " )
guard isSignOut else { return }
self . coordinator . setup ( )
self . coordinator . setupOnboardingIfNeeds ( animated : true )
}
}
. store ( in : & disposeBag )
}
2021-02-07 07:42:50 +01:00
}
// MARK: - U I S c r o l l V i e w D e l e g a t e
extension HomeTimelineViewController {
func scrollViewDidScroll ( _ scrollView : UIScrollView ) {
2021-09-28 13:58:14 +02:00
switch scrollView {
case tableView :
viewModel . homeTimelineNavigationBarTitleViewModel . handleScrollViewDidScroll ( scrollView )
default :
break
}
}
func scrollViewShouldScrollToTop ( _ scrollView : UIScrollView ) -> Bool {
switch scrollView {
case tableView :
2021-09-29 11:05:40 +02:00
let indexPath = IndexPath ( row : 0 , section : 0 )
guard viewModel . diffableDataSource ? . itemIdentifier ( for : indexPath ) != nil else {
return true
}
// s a v e p o s i t i o n
2021-09-28 13:58:14 +02:00
savePositionBeforeScrollToTop ( )
2021-09-29 11:05:40 +02:00
// o v e r r i d e b y c u s t o m s c r o l l T o R o w
tableView . scrollToRow ( at : indexPath , at : . top , animated : true )
return false
2021-09-28 13:58:14 +02:00
default :
assertionFailure ( )
return true
}
}
private func savePositionBeforeScrollToTop ( ) {
2021-09-29 11:05:40 +02:00
// c h e c k s a v e a c t i o n i n t e r v a l
// s h o u l d n o t f a s t t h a n 0 . 5 s t o p r e v e n t s a v e w h e n s c r o l l T o T o p o n - f l y i n g
2022-01-27 14:23:39 +01:00
if let record = viewModel . scrollPositionRecord {
2021-09-29 11:05:40 +02:00
let now = Date ( )
guard now . timeIntervalSince ( record . timestamp ) > 0.5 else {
// s k i p t h i s s a v e a c t i o n
return
}
}
2021-09-28 13:58:14 +02:00
guard let diffableDataSource = viewModel . diffableDataSource else { return }
guard let anchorIndexPaths = tableView . indexPathsForVisibleRows ? . sorted ( ) else { return }
guard ! anchorIndexPaths . isEmpty else { return }
let anchorIndexPath = anchorIndexPaths [ anchorIndexPaths . count / 2 ]
guard let anchorItem = diffableDataSource . itemIdentifier ( for : anchorIndexPath ) else { return }
2021-04-13 13:46:42 +02:00
2021-09-28 13:58:14 +02:00
let offset : CGFloat = {
guard let anchorCell = tableView . cellForRow ( at : anchorIndexPath ) else { return 0 }
let cellFrameInView = tableView . convert ( anchorCell . frame , to : view )
return cellFrameInView . origin . y
} ( )
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : save position record for \( anchorIndexPath ) with offset: \( offset ) " )
2022-01-27 14:23:39 +01:00
viewModel . scrollPositionRecord = HomeTimelineViewModel . ScrollPositionRecord (
2021-09-28 13:58:14 +02:00
item : anchorItem ,
offset : offset ,
timestamp : Date ( )
)
}
private func shouldRestoreScrollPosition ( ) -> Bool {
// c h e c k i f s c r o l l t o t o p
guard self . tableView . safeAreaInsets . top > 0 else { return false }
let zeroOffset = - self . tableView . safeAreaInsets . top
return abs ( self . tableView . contentOffset . y - zeroOffset ) < 2.0
}
private func restorePositionWhenScrollToTop ( ) {
guard let diffableDataSource = self . viewModel . diffableDataSource else { return }
2022-01-27 14:23:39 +01:00
guard let record = self . viewModel . scrollPositionRecord ,
2021-09-28 13:58:14 +02:00
let indexPath = diffableDataSource . indexPath ( for : record . item )
else { return }
2022-01-27 14:23:39 +01:00
tableView . scrollToRow ( at : indexPath , at : . middle , animated : true )
viewModel . scrollPositionRecord = nil
2021-02-07 07:42:50 +01:00
}
}
// MARK: - U I T a b l e V i e w D e l e g a t e
2022-01-27 14:23:39 +01:00
extension HomeTimelineViewController : UITableViewDelegate , AutoGenerateTableViewDelegate {
// s o u r c e r y : i n l i n e : H o m e T i m e l i n e V i e w C o n t r o l l e r . A u t o G e n e r a t e T a b l e V i e w D e l e g a t e
// G e n e r a t e d u s i n g S o u r c e r y
// D O N O T E D I T
2021-04-13 13:46:42 +02:00
func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
aspectTableView ( tableView , didSelectRowAt : indexPath )
2021-03-10 14:19:56 +01:00
}
2022-01-27 14:23:39 +01:00
2021-04-30 13:28:06 +02:00
func tableView ( _ tableView : UITableView , contextMenuConfigurationForRowAt indexPath : IndexPath , point : CGPoint ) -> UIContextMenuConfiguration ? {
return aspectTableView ( tableView , contextMenuConfigurationForRowAt : indexPath , point : point )
}
2022-01-27 14:23:39 +01:00
2021-04-30 13:28:06 +02:00
func tableView ( _ tableView : UITableView , previewForHighlightingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
return aspectTableView ( tableView , previewForHighlightingContextMenuWithConfiguration : configuration )
}
func tableView ( _ tableView : UITableView , previewForDismissingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
return aspectTableView ( tableView , previewForDismissingContextMenuWithConfiguration : configuration )
}
2022-01-27 14:23:39 +01:00
2021-04-30 13:28:06 +02:00
func tableView ( _ tableView : UITableView , willPerformPreviewActionForMenuWith configuration : UIContextMenuConfiguration , animator : UIContextMenuInteractionCommitAnimating ) {
aspectTableView ( tableView , willPerformPreviewActionForMenuWith : configuration , animator : animator )
}
2022-01-27 14:23:39 +01:00
// s o u r c e r y : e n d
// f u n c t a b l e V i e w ( _ t a b l e V i e w : U I T a b l e V i e w , e s t i m a t e d H e i g h t F o r R o w A t i n d e x P a t h : I n d e x P a t h ) - > C G F l o a t {
// a s p e c t T a b l e V i e w ( t a b l e V i e w , e s t i m a t e d H e i g h t F o r R o w A t : i n d e x P a t h )
// }
//
// f u n c t a b l e V i e w ( _ t a b l e V i e w : U I T a b l e V i e w , w i l l D i s p l a y c e l l : U I T a b l e V i e w C e l l , f o r R o w A t i n d e x P a t h : I n d e x P a t h ) {
// a s p e c t T a b l e V i e w ( t a b l e V i e w , w i l l D i s p l a y : c e l l , f o r R o w A t : i n d e x P a t h )
// }
//
// f u n c t a b l e V i e w ( _ t a b l e V i e w : U I T a b l e V i e w , d i d E n d D i s p l a y i n g c e l l : U I T a b l e V i e w C e l l , f o r R o w A t i n d e x P a t h : I n d e x P a t h ) {
// a s p e c t T a b l e V i e w ( t a b l e V i e w , d i d E n d D i s p l a y i n g : c e l l , f o r R o w A t : i n d e x P a t h )
// }
2021-04-30 13:28:06 +02:00
2021-02-07 07:42:50 +01:00
}
// MARK: - C o n t e n t O f f s e t A d j u s t a b l e T i m e l i n e V i e w C o n t r o l l e r D e l e g a t e
extension HomeTimelineViewController : ContentOffsetAdjustableTimelineViewControllerDelegate {
func navigationBar ( ) -> UINavigationBar ? {
return navigationController ? . navigationBar
}
}
// MARK: - T i m e l i n e M i d d l e L o a d e r T a b l e V i e w C e l l D e l e g a t e
extension HomeTimelineViewController : TimelineMiddleLoaderTableViewCellDelegate {
func timelineMiddleLoaderTableViewCell ( _ cell : TimelineMiddleLoaderTableViewCell , loadMoreButtonDidPressed button : UIButton ) {
guard let diffableDataSource = viewModel . diffableDataSource else { return }
guard let indexPath = tableView . indexPath ( for : cell ) else { return }
guard let item = diffableDataSource . itemIdentifier ( for : indexPath ) else { return }
2022-01-27 14:23:39 +01:00
Task {
await viewModel . loadMore ( item : item )
2021-02-07 07:42:50 +01:00
}
}
}
// MARK: - S c r o l l V i e w C o n t a i n e r
extension HomeTimelineViewController : ScrollViewContainer {
var scrollView : UIScrollView { return tableView }
func scrollToTop ( animated : Bool ) {
2022-01-29 12:51:40 +01:00
if scrollView . contentOffset . y < scrollView . frame . height ,
viewModel . loadLatestStateMachine . canEnterState ( HomeTimelineViewModel . LoadLatestState . Loading . self ) ,
( scrollView . contentOffset . y + scrollView . adjustedContentInset . top ) = = 0.0 ,
! refreshControl . isRefreshing {
scrollView . scrollRectToVisible ( CGRect ( origin : CGPoint ( x : 0 , y : - refreshControl . frame . height ) , size : CGSize ( width : 1 , height : 1 ) ) , animated : animated )
DispatchQueue . main . async { [ weak self ] in
guard let self = self else { return }
self . refreshControl . beginRefreshing ( )
self . refreshControl . sendActions ( for : . valueChanged )
}
} else {
let indexPath = IndexPath ( row : 0 , section : 0 )
guard viewModel . diffableDataSource ? . itemIdentifier ( for : indexPath ) != nil else { return }
// s a v e p o s i t i o n
savePositionBeforeScrollToTop ( )
tableView . scrollToRow ( at : indexPath , at : . top , animated : true )
}
2021-02-07 07:42:50 +01:00
}
}
2021-02-24 09:11:48 +01:00
// MARK: - S t a t u s T a b l e V i e w C e l l D e l e g a t e
2022-01-27 14:23:39 +01:00
extension HomeTimelineViewController : StatusTableViewCellDelegate { }
2021-03-29 11:44:52 +02:00
// MARK: - H o m e T i m e l i n e N a v i g a t i o n B a r T i t l e V i e w D e l e g a t e
extension HomeTimelineViewController : HomeTimelineNavigationBarTitleViewDelegate {
2021-06-21 12:58:58 +02:00
func homeTimelineNavigationBarTitleView ( _ titleView : HomeTimelineNavigationBarTitleView , logoButtonDidPressed sender : UIButton ) {
2021-09-28 13:58:14 +02:00
if shouldRestoreScrollPosition ( ) {
restorePositionWhenScrollToTop ( )
} else {
savePositionBeforeScrollToTop ( )
scrollToTop ( animated : true )
}
2021-06-21 12:58:58 +02:00
}
2021-03-29 11:44:52 +02:00
func homeTimelineNavigationBarTitleView ( _ titleView : HomeTimelineNavigationBarTitleView , buttonDidPressed sender : UIButton ) {
switch titleView . state {
case . newPostButton :
guard let diffableDataSource = viewModel . diffableDataSource else { return }
let indexPath = IndexPath ( row : 0 , section : 0 )
guard diffableDataSource . itemIdentifier ( for : indexPath ) != nil else { return }
2021-09-29 11:05:40 +02:00
savePositionBeforeScrollToTop ( )
2021-03-29 11:44:52 +02:00
tableView . scrollToRow ( at : indexPath , at : . top , animated : true )
case . offlineButton :
// TODO: r e t r y
break
case . publishedButton :
break
default :
break
}
}
}
2021-05-21 09:23:02 +02:00
2022-01-27 14:23:39 +01:00
// e x t e n s i o n H o m e T i m e l i n e V i e w C o n t r o l l e r {
// o v e r r i d e v a r k e y C o m m a n d s : [ U I K e y C o m m a n d ] ? {
// r e t u r n n a v i g a t i o n K e y C o m m a n d s + s t a t u s N a v i g a t i o n K e y C o m m a n d s
// }
// }
//
// / / MARK: - S t a t u s T a b l e V i e w C o n t r o l l e r N a v i g a t e a b l e
// e x t e n s i o n H o m e T i m e l i n e V i e w C o n t r o l l e r : S t a t u s T a b l e V i e w C o n t r o l l e r N a v i g a t e a b l e {
// @ o b j c f u n c n a v i g a t e K e y C o m m a n d H a n d l e r R e l a y ( _ s e n d e r : U I K e y C o m m a n d ) {
// n a v i g a t e K e y C o m m a n d H a n d l e r ( s e n d e r )
// }
//
// @ o b j c f u n c s t a t u s K e y C o m m a n d H a n d l e r R e l a y ( _ s e n d e r : U I K e y C o m m a n d ) {
// s t a t u s K e y C o m m a n d H a n d l e r ( s e n d e r )
// }
// }