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-02-24 09:11:48 +01:00
final class HomeTimelineViewController : UIViewController , NeedsDependency {
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-02-23 09:45:00 +01:00
let settingBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( )
barButtonItem . tintColor = Asset . Colors . Label . highlight . color
barButtonItem . image = UIImage ( systemName : " gear " ) ? . withRenderingMode ( . alwaysTemplate )
return barButtonItem
} ( )
let composeBarButtonItem : UIBarButtonItem = {
let barButtonItem = UIBarButtonItem ( )
barButtonItem . tintColor = Asset . Colors . Label . highlight . color
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
} ( )
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-02-22 09:20:44 +01:00
title = L10n . Scene . HomeTimeline . title
2021-02-23 08:16:55 +01:00
view . backgroundColor = Asset . Colors . Background . systemGroupedBackground . color
2021-02-23 09:45:00 +01:00
navigationItem . titleView = {
let imageView = UIImageView ( image : Asset . Asset . mastodonTextLogo . image . withRenderingMode ( . alwaysTemplate ) )
imageView . tintColor = Asset . Colors . Label . primary . color
return imageView
} ( )
navigationItem . leftBarButtonItem = settingBarButtonItem
2021-02-26 11:27:47 +01: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
// s e t t i n g B a r B u t t o n I t e m . t a r g e t = s e l f
// s e t t i n g B a r B u t t o n I t e m . a c t i o n = # s e l e c t o r ( 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 e t t i n g B a r B u t t o n I t e m P r e s s e d ( _ : ) )
settingBarButtonItem . menu = UIMenu ( title : " Settings " , image : nil , identifier : nil , options : . displayInline , children : [
UIAction ( title : " Sign Out " , image : UIImage ( systemName : " escape " ) , attributes : . destructive ) { [ weak self ] action in
guard let self = self else { return }
self . signOutAction ( action )
}
] )
#endif
2021-02-23 09:45:00 +01:00
navigationItem . rightBarButtonItem = composeBarButtonItem
composeBarButtonItem . target = self
composeBarButtonItem . action = #selector ( HomeTimelineViewController . composeBarButtonItemPressed ( _ : ) )
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 ) ,
] )
viewModel . tableView = tableView
viewModel . contentOffsetAdjustableTimelineViewControllerDelegate = self
tableView . delegate = self
viewModel . setupDiffableDataSource (
for : tableView ,
dependency : self ,
2021-03-03 09:12:48 +01:00
statusTableViewCellDelegate : self ,
2021-02-07 07:42:50 +01:00
timelineMiddleLoaderTableViewCellDelegate : self
)
// b i n d r e f r e s h c o n t r o l
viewModel . isFetchingLatestTimeline
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isFetching in
guard let self = self else { return }
if ! isFetching {
UIView . animate ( withDuration : 0.5 ) { [ weak self ] in
guard let self = self else { return }
self . refreshControl . endRefreshing ( )
}
}
}
. store ( in : & disposeBag )
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
viewModel . viewDidAppear . send ( )
DispatchQueue . main . async { [ weak self ] in
guard let self = self else { return }
if ( self . viewModel . fetchedResultsController . fetchedObjects ? ? [ ] ) . count = = 0 {
self . viewModel . loadLatestStateMachine . enter ( HomeTimelineViewModel . LoadLatestState . Loading . self )
}
}
}
override func viewDidDisappear ( _ animated : Bool ) {
super . viewDidDisappear ( animated )
}
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-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 )
}
@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 )
2021-03-11 08:41:27 +01:00
let composeViewModel = ComposeViewModel ( context : context , composeKind : . toot )
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-02-24 10:23:01 +01:00
handleScrollViewDidScroll ( scrollView )
2021-02-07 07:42:50 +01:00
}
}
2021-02-24 10:23:01 +01:00
extension HomeTimelineViewController : LoadMoreConfigurableTableViewContainer {
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
typealias LoadingState = HomeTimelineViewModel . LoadOldestState . Loading
var loadMoreConfigurableTableView : UITableView { return tableView }
var loadMoreConfigurableStateMachine : GKStateMachine { return viewModel . loadoldestStateMachine }
}
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
extension HomeTimelineViewController : UITableViewDelegate {
2021-03-03 09:12:48 +01:00
// TODO:
// 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 {
// g u a r d l e t d i f f a b l e D a t a S o u r c e = v i e w M o d e l . d i f f a b l e D a t a S o u r c e e l s e { r e t u r n 1 0 0 }
// g u a r d l e t i t e m = d i f f a b l e D a t a S o u r c e . i t e m I d e n t i f i e r ( f o r : i n d e x P a t h ) e l s e { r e t u r n 1 0 0 }
//
// g u a r d l e t f r a m e = v i e w M o d e l . c e l l F r a m e C a c h e . o b j e c t ( f o r K e y : N S N u m b e r ( v a l u e : i t e m . h a s h V a l u e ) ) ? . c g R e c t V a l u e e l s e {
// r e t u r n 2 0 0
// }
// / / o s _ l o g ( " % { p u b l i c } s [ % { p u b l i c } l d ] , % { p u b l i c } s : c a c h e c e l l f r a m e % s " , ( ( # f i l e a s N S S t r i n g ) . l a s t P a t h C o m p o n e n t ) , # l i n e , # f u n c t i o n , f r a m e . d e b u g D e s c r i p t i o n )
//
// r e t u r n c e i l ( f r a m e . h e i g h t )
// }
func tableView ( _ tableView : UITableView , willDisplay cell : UITableViewCell , forRowAt indexPath : IndexPath ) {
handleTableView ( tableView , willDisplay : cell , forRowAt : indexPath )
2021-02-07 07:42:50 +01:00
}
2021-02-24 11:41:40 +01: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 configure ( cell : TimelineMiddleLoaderTableViewCell , upperTimelineTootID : String ? , timelineIndexobjectID : NSManagedObjectID ? ) {
guard let upperTimelineIndexObjectID = timelineIndexobjectID else {
return
}
viewModel . loadMiddleSateMachineList
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] ids in
2021-02-24 11:41:40 +01:00
guard let _ = self else { return }
2021-02-07 07:42:50 +01:00
if let stateMachine = ids [ upperTimelineIndexObjectID ] {
guard let state = stateMachine . currentState else {
assertionFailure ( )
return
}
// m a k e s u c c e s s s t a t e s a m e a s l o a d i n g d u e t o s n a p s h o t u p d a t i n g d e l a y
let isLoading = state is HomeTimelineViewModel . LoadMiddleState . Loading || state is HomeTimelineViewModel . LoadMiddleState . Success
cell . loadMoreButton . isHidden = isLoading
if isLoading {
cell . activityIndicatorView . startAnimating ( )
} else {
cell . activityIndicatorView . stopAnimating ( )
}
} else {
cell . loadMoreButton . isHidden = false
cell . activityIndicatorView . stopAnimating ( )
}
}
. store ( in : & cell . disposeBag )
var dict = viewModel . loadMiddleSateMachineList . value
if let _ = dict [ upperTimelineIndexObjectID ] {
// d o n o t h i n g
} else {
let stateMachine = GKStateMachine ( states : [
HomeTimelineViewModel . LoadMiddleState . Initial ( viewModel : viewModel , upperTimelineIndexObjectID : upperTimelineIndexObjectID ) ,
HomeTimelineViewModel . LoadMiddleState . Loading ( viewModel : viewModel , upperTimelineIndexObjectID : upperTimelineIndexObjectID ) ,
HomeTimelineViewModel . LoadMiddleState . Fail ( viewModel : viewModel , upperTimelineIndexObjectID : upperTimelineIndexObjectID ) ,
HomeTimelineViewModel . LoadMiddleState . Success ( viewModel : viewModel , upperTimelineIndexObjectID : upperTimelineIndexObjectID ) ,
] )
stateMachine . enter ( HomeTimelineViewModel . LoadMiddleState . Initial . self )
dict [ upperTimelineIndexObjectID ] = stateMachine
viewModel . loadMiddleSateMachineList . value = dict
}
}
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 }
switch item {
case . homeMiddleLoader ( let upper ) :
guard let stateMachine = viewModel . loadMiddleSateMachineList . value [ upper ] else {
assertionFailure ( )
return
}
stateMachine . enter ( HomeTimelineViewModel . LoadMiddleState . Loading . self )
default :
assertionFailure ( )
}
}
}
// 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 ) {
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 }
tableView . scrollToRow ( at : indexPath , at : . top , animated : true )
}
}
}
2021-02-24 09:11:48 +01:00
2021-03-10 07:36:28 +01:00
// MARK: - A V P l a y e r V i e w C o n t r o l l e r D e l e g a t e
extension HomeTimelineViewController : AVPlayerViewControllerDelegate {
func playerViewController ( _ playerViewController : AVPlayerViewController , willBeginFullScreenPresentationWithAnimationCoordinator coordinator : UIViewControllerTransitionCoordinator ) {
handlePlayerViewController ( playerViewController , willBeginFullScreenPresentationWithAnimationCoordinator : coordinator )
}
func playerViewController ( _ playerViewController : AVPlayerViewController , willEndFullScreenPresentationWithAnimationCoordinator coordinator : UIViewControllerTransitionCoordinator ) {
handlePlayerViewController ( playerViewController , willEndFullScreenPresentationWithAnimationCoordinator : coordinator )
}
}
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
2021-03-10 07:36:28 +01:00
extension HomeTimelineViewController : StatusTableViewCellDelegate {
weak var playerViewControllerDelegate : AVPlayerViewControllerDelegate ? { return self }
func parent ( ) -> UIViewController { return self }
}