2020-08-31 23:28:50 +00:00
//
// C o m p o s e H o s t i n g C o n t r o l l e r . s w i f t
// T u s k e r
//
// C r e a t e d b y S h a d o w f a c t s o n 8 / 2 2 / 2 0 .
// C o p y r i g h t © 2 0 2 0 S h a d o w f a c t s . A l l r i g h t s r e s e r v e d .
//
import SwiftUI
import Combine
import Pachyderm
import PencilKit
class ComposeHostingController : UIHostingController < ComposeContainerView > {
let mastodonController : MastodonController
let uiState : ComposeUIState
var draft : Draft { uiState . draft }
private var cancellables = [ AnyCancellable ] ( )
2020-09-07 21:05:50 +00:00
private var keyboardHeight : CGFloat = 0
private var toolbarHeight : CGFloat = 44
2020-08-31 23:28:50 +00:00
private var mainToolbar : UIToolbar !
private var inputAccessoryToolbar : UIToolbar !
private var visibilityBarButtonItems = [ UIBarButtonItem ] ( )
override var inputAccessoryView : UIView ? { inputAccessoryToolbar }
init ( draft : Draft ? = nil , mastodonController : MastodonController ) {
self . mastodonController = mastodonController
let realDraft = draft ? ? Draft ( accountID : mastodonController . accountInfo ! . id )
DraftsManager . shared . add ( realDraft )
self . uiState = ComposeUIState ( draft : realDraft )
// w e n e e d o u r o w n e n v i r o n m e n t o b j e c t w r a p p e r s o t h a t w e c a n s e t t h e m a s t o d o n c o n t r o l l e r a s a n
// e n v i r o n m e n t o b j e c t a n d s e t u p t h e d r a f t c h a n g e l i s t e n e r w h i l e s t i l l h a v i n g a c o n c r e t e t y p e
// t o u s e a s t h e U I H o s t i n g C o n t r o l l e r t y p e p a r a m e t e r
let container = ComposeContainerView (
mastodonController : mastodonController ,
uiState : uiState
)
super . init ( rootView : container )
self . uiState . delegate = self
// m a i n t o o l b a r i s s h o w n a t t h e b o t t o m o f t h e s c r e e n , t h e i n p u t a c c e s s o r y i s a t t a c h e d t o t h e k e y b o a r d w h i l e e d i t i n g
mainToolbar = createToolbar ( )
inputAccessoryToolbar = createToolbar ( )
NotificationCenter . default . addObserver ( self , selector : #selector ( keyboardWillShow ( _ : ) ) , name : UIResponder . keyboardWillShowNotification , object : nil )
NotificationCenter . default . addObserver ( self , selector : #selector ( keyboardWillHide ( _ : ) ) , name : UIResponder . keyboardWillHideNotification , object : nil )
NotificationCenter . default . addObserver ( self , selector : #selector ( keyboardDidHide ( _ : ) ) , name : UIResponder . keyboardDidHideNotification , object : nil )
// a d d t h e h e i g h t o f t h e t o o l b a r i t s e l f t o t h e b o t t o m o f t h e s a f e a r e a s o c o n t e n t i n s i d e S w i f t U I S c r o l l V i e w d o e s n ' t u n d e r f l o w i t
2020-09-07 21:05:50 +00:00
updateAdditionalSafeAreaInsets ( )
2020-08-31 23:28:50 +00:00
pasteConfiguration = UIPasteConfiguration ( forAccepting : CompositionAttachment . self )
userActivity = UserActivityManager . newPostActivity ( )
self . uiState . $ draft
. flatMap ( \ . $ visibility )
. sink ( receiveValue : self . visibilityChanged )
. store ( in : & cancellables )
self . uiState . $ draft
. flatMap ( \ . objectWillChange )
. debounce ( for : . milliseconds ( 250 ) , scheduler : DispatchQueue . global ( qos : . utility ) )
. sink {
DraftsManager . save ( )
}
. store ( in : & cancellables )
}
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
// c a n ' t d o t h i s i n v i e w D i d L o a d b e c a u s e v i e w D i d L o a d i s n ' t c a l l e d f o r U I H o s t i n g C o n t r o l l e r
2020-09-13 17:19:56 +00:00
// i f m a i n T o o l b a r . s u p e r v i e w = = n i l {
// v i e w . a d d S u b v i e w ( m a i n T o o l b a r )
// N S L a y o u t C o n s t r a i n t . a c t i v a t e ( [
// m a i n T o o l b a r . l e a d i n g A n c h o r . c o n s t r a i n t ( e q u a l T o : v i e w . l e a d i n g A n c h o r ) ,
// m a i n T o o l b a r . t r a i l i n g A n c h o r . c o n s t r a i n t ( e q u a l T o : v i e w . t r a i l i n g A n c h o r ) ,
// / / u s e t h e t o p a n c h o r o f t h e t o o l b a r s o o u r a d d i t i o n a l S a f e A r e a I n s e t s ( w h i c h h a s t h e b o t t o m a s t h e t o o l b a r h e i g h t ) d o n ' t a f f e c t i t
// m a i n T o o l b a r . t o p A n c h o r . c o n s t r a i n t ( e q u a l T o : v i e w . s a f e A r e a L a y o u t G u i d e . b o t t o m A n c h o r ) ,
// ] )
// }
}
override func didMove ( toParent parent : UIViewController ? ) {
super . didMove ( toParent : parent )
if let parent = parent {
parent . view . addSubview ( mainToolbar )
2020-08-31 23:28:50 +00:00
NSLayoutConstraint . activate ( [
mainToolbar . leadingAnchor . constraint ( equalTo : view . leadingAnchor ) ,
mainToolbar . trailingAnchor . constraint ( equalTo : view . trailingAnchor ) ,
// u s e t h e t o p a n c h o r o f t h e t o o l b a r s o o u r a d d i t i o n a l S a f e A r e a I n s e t s ( w h i c h h a s t h e b o t t o m a s t h e t o o l b a r h e i g h t ) d o n ' t a f f e c t i t
mainToolbar . topAnchor . constraint ( equalTo : view . safeAreaLayoutGuide . bottomAnchor ) ,
] )
}
}
override func viewWillDisappear ( _ animated : Bool ) {
super . viewWillDisappear ( animated )
if ! draft . hasContent {
DraftsManager . shared . remove ( draft )
}
DraftsManager . save ( )
}
private func createToolbar ( ) -> UIToolbar {
let toolbar = UIToolbar ( )
toolbar . translatesAutoresizingMaskIntoConstraints = false
2020-09-13 17:19:56 +00:00
toolbar . isAccessibilityElement = true
2020-08-31 23:28:50 +00:00
2020-09-01 03:07:41 +00:00
let visibilityAction : Selector ?
if #available ( iOS 14.0 , * ) {
visibilityAction = nil
} else {
visibilityAction = #selector ( visibilityButtonPressed ( _ : ) )
}
let visibilityItem = UIBarButtonItem ( image : UIImage ( systemName : draft . visibility . imageName ) , style : . plain , target : self , action : visibilityAction )
2020-08-31 23:28:50 +00:00
visibilityBarButtonItems . append ( visibilityItem )
2020-09-01 03:07:41 +00:00
visibilityChanged ( draft . visibility )
2020-08-31 23:28:50 +00:00
toolbar . items = [
UIBarButtonItem ( title : " CW " , style : . plain , target : self , action : #selector ( cwButtonPressed ) ) ,
visibilityItem ,
UIBarButtonItem ( barButtonSystemItem : . flexibleSpace , target : nil , action : nil ) ,
UIBarButtonItem ( title : " Drafts " , style : . plain , target : self , action : #selector ( draftsButtonPresed ) )
]
return toolbar
}
2020-09-07 21:05:50 +00:00
private func updateAdditionalSafeAreaInsets ( ) {
additionalSafeAreaInsets = UIEdgeInsets ( top : 0 , left : 0 , bottom : toolbarHeight + keyboardHeight , right : 0 )
}
2020-08-31 23:28:50 +00:00
@objc private func keyboardWillShow ( _ notification : Foundation . Notification ) {
keyboardWillShow ( accessoryView : inputAccessoryToolbar , notification : notification )
}
func keyboardWillShow ( accessoryView : UIView , notification : Foundation . Notification ) {
mainToolbar . isHidden = true
accessoryView . alpha = 1
accessoryView . isHidden = false
2020-09-07 21:05:50 +00:00
// o n i O S 1 4 , S w i f t U I s a f e a r e a a u t o m a t i c a l l y i n c l u d e s t h e k e y b o a r d
if #available ( iOS 14.0 , * ) {
} else {
let userInfo = notification . userInfo !
let frame = userInfo [ UIResponder . keyboardFrameEndUserInfoKey ] as ! CGRect
// t e m p o r a r i l y r e s e t a d d ' l s a f e a r e a i n s e t s s o w e c a n a c c e s s t h e d e f a u l t i n s e t
additionalSafeAreaInsets = . zero
2020-10-12 02:14:45 +00:00
// t h e r e a r e a f e w e x t r a p o i n t s t h a t c o m e f r o m s o m e w h e r e , i t s e e m s t o b e f o u r
// a n d w i t h o u t i t , t h e a u t o c o m p l e t e s u g g e s t i o n s a r e c u t o f f : S
keyboardHeight = frame . height - view . safeAreaInsets . bottom - accessoryView . frame . height + 4
2020-09-07 21:05:50 +00:00
updateAdditionalSafeAreaInsets ( )
}
2020-08-31 23:28:50 +00:00
}
@objc private func keyboardWillHide ( _ notification : Foundation . Notification ) {
keyboardWillHide ( accessoryView : inputAccessoryToolbar , notification : notification )
}
func keyboardWillHide ( accessoryView : UIView , notification : Foundation . Notification ) {
mainToolbar . isHidden = false
let userInfo = notification . userInfo !
let durationObj = userInfo [ UIResponder . keyboardAnimationDurationUserInfoKey ] as ! NSNumber
let duration = TimeInterval ( durationObj . doubleValue )
let curveValue = userInfo [ UIResponder . keyboardAnimationCurveUserInfoKey ] as ! NSNumber
let curve = UIView . AnimationCurve ( rawValue : curveValue . intValue ) !
let curveOption : UIView . AnimationOptions
switch curve {
case . easeInOut :
curveOption = . curveEaseInOut
case . easeIn :
curveOption = . curveEaseIn
case . easeOut :
curveOption = . curveEaseOut
case . linear :
curveOption = . curveLinear
@ unknown default :
curveOption = . curveLinear
}
UIView . animate ( withDuration : duration , delay : 0 , options : curveOption ) {
accessoryView . alpha = 0
} completion : { ( finished ) in
accessoryView . alpha = 1
}
2020-09-07 21:05:50 +00:00
// o n i O S 1 4 , S w i f t U I s a f e a r e a a u t o m a t i c a l l y i n c l u d e s t h e k e y b o a r d
if #available ( iOS 14.0 , * ) {
} else {
keyboardHeight = 0
updateAdditionalSafeAreaInsets ( )
}
2020-08-31 23:28:50 +00:00
}
@objc private func keyboardDidHide ( _ notification : Foundation . Notification ) {
keyboardDidHide ( accessoryView : inputAccessoryToolbar , notification : notification )
}
func keyboardDidHide ( accessoryView : UIView , notification : Foundation . Notification ) {
accessoryView . isHidden = true
}
private func visibilityChanged ( _ newVisibility : Status . Visibility ) {
for item in visibilityBarButtonItems {
item . image = UIImage ( systemName : newVisibility . imageName )
2020-09-13 17:19:56 +00:00
item . image ! . accessibilityLabel = String ( format : NSLocalizedString ( " Visibility: %@ " , comment : " compose visiblity accessibility label " ) , draft . visibility . displayName )
2020-08-31 23:28:50 +00:00
item . accessibilityLabel = String ( format : NSLocalizedString ( " Visibility: %@ " , comment : " compose visiblity accessibility label " ) , draft . visibility . displayName )
2020-09-01 03:07:41 +00:00
if #available ( iOS 14.0 , * ) {
let elements = Status . Visibility . allCases . map { ( visibility ) -> UIMenuElement in
let state = visibility = = newVisibility ? UIMenuElement . State . on : . off
return UIAction ( title : visibility . displayName , image : UIImage ( systemName : visibility . unfilledImageName ) , identifier : nil , discoverabilityTitle : nil , attributes : [ ] , state : state ) { ( _ ) in
self . draft . visibility = visibility
}
}
item . menu = UIMenu ( title : " " , image : nil , identifier : nil , options : [ ] , children : elements )
}
2020-08-31 23:28:50 +00:00
}
}
override func canPaste ( _ itemProviders : [ NSItemProvider ] ) -> Bool {
guard itemProviders . allSatisfy ( { $0 . canLoadObject ( ofClass : CompositionAttachment . self ) } ) else { return false }
switch mastodonController . instance . instanceType {
case . pleroma :
return true
case . mastodon :
2020-09-07 18:43:39 +00:00
guard draft . attachments . allSatisfy ( { $0 . data . type = = . image } ) else { return false }
// t o d o : i f p r o v i d e r s a r e v i d e o s , t h i s t e c h n i c a l l y a l l o w s i n v a l i d v i d e o / i m a g e c o m b i n a t i o n s
2020-08-31 23:28:50 +00:00
return itemProviders . count + draft . attachments . count <= 4
}
}
override func paste ( itemProviders : [ NSItemProvider ] ) {
for provider in itemProviders where provider . canLoadObject ( ofClass : CompositionAttachment . self ) {
provider . loadObject ( ofClass : CompositionAttachment . self ) { ( object , error ) in
guard let attachment = object as ? CompositionAttachment else { return }
DispatchQueue . main . async {
self . draft . attachments . append ( attachment )
}
}
}
}
// MARK: I n t e r a c t i o n
@objc func cwButtonPressed ( ) {
draft . contentWarningEnabled = ! draft . contentWarningEnabled
}
@objc func visibilityButtonPressed ( _ sender : UIBarButtonItem ) {
2020-09-01 03:07:41 +00:00
// i f # a v a i l a b l e ( i O S 1 4 . 0 , * ) {
// } e l s e {
let alertController = UIAlertController ( currentVisibility : draft . visibility ) { ( visibility ) in
guard let visibility = visibility else { return }
self . draft . visibility = visibility
}
alertController . popoverPresentationController ? . barButtonItem = sender
present ( alertController , animated : true )
// }
2020-08-31 23:28:50 +00:00
}
@objc func draftsButtonPresed ( ) {
let draftsVC = DraftsTableViewController ( account : mastodonController . accountInfo ! , exclude : draft )
draftsVC . delegate = self
present ( UINavigationController ( rootViewController : draftsVC ) , animated : true )
}
}
extension ComposeHostingController : ComposeUIStateDelegate {
var assetPickerDelegate : AssetPickerViewControllerDelegate ? { self }
func dismissCompose ( ) {
self . dismiss ( animated : true )
}
func presentAssetPickerSheet ( ) {
let sheetContainer = AssetPickerSheetContainerViewController ( )
sheetContainer . assetPicker . assetPickerDelegate = self
self . present ( sheetContainer , animated : true )
}
func presentComposeDrawing ( ) {
let drawing : PKDrawing
if case let . edit ( id ) = uiState . composeDrawingMode ,
let attachment = draft . attachments . first ( where : { $0 . id = = id } ) ,
case let . drawing ( existingDrawing ) = attachment . data {
drawing = existingDrawing
} else {
drawing = PKDrawing ( )
}
let drawingVC = ComposeDrawingViewController ( editing : drawing )
drawingVC . delegate = self
let nav = UINavigationController ( rootViewController : drawingVC )
nav . modalPresentationStyle = . fullScreen
present ( nav , animated : true )
}
}
extension ComposeHostingController : AssetPickerViewControllerDelegate {
func assetPicker ( _ assetPicker : AssetPickerViewController , shouldAllowAssetOfType type : CompositionAttachmentData . AttachmentType ) -> Bool {
switch mastodonController . instance . instanceType {
case . pleroma :
return true
case . mastodon :
if ( type = = . video && draft . attachments . count > 0 ) ||
draft . attachments . contains ( where : { $0 . data . type = = . video } ) ||
assetPicker . currentCollectionSelectedAssets . contains ( where : { $0 . type = = . video } ) {
return false
}
return draft . attachments . count + assetPicker . currentCollectionSelectedAssets . count < 4
}
}
func assetPicker ( _ assetPicker : AssetPickerViewController , didSelectAttachments attachments : [ CompositionAttachmentData ] ) {
let attachments = attachments . map {
CompositionAttachment ( data : $0 )
}
draft . attachments . append ( contentsOf : attachments )
}
}
extension ComposeHostingController : DraftsTableViewControllerDelegate {
func draftSelectionCanceled ( ) {
}
func shouldSelectDraft ( _ draft : Draft , completion : @ escaping ( Bool ) -> Void ) {
if draft . inReplyToID != self . draft . inReplyToID ,
self . draft . hasContent {
let alertController = UIAlertController ( title : " Different Reply " , message : " The selected draft is a reply to a different status, do you wish to use it? " , preferredStyle : . alert )
alertController . addAction ( UIAlertAction ( title : " Cancel " , style : . cancel , handler : { ( _ ) in
completion ( false )
} ) )
alertController . addAction ( UIAlertAction ( title : " Restore Draft " , style : . default , handler : { ( _ ) in
completion ( true )
} ) )
// w e c a n ' t p r e s e n t t h e l a e r t o u r s e l v e s s i n c e t h e c o m p o s e V C i s a l r e a d y p r e s e n t i n g t h e d r a f t s e l e c t o r
// b u t p r e s e n t i n g o n t h e p r e s e n t e d v i e w c o n t r o l l e r s e e m s h a c k y , i s t h e r e a b e t t e r w a y t o d o t h i s ?
presentedViewController ! . present ( alertController , animated : true )
} else {
completion ( true )
}
}
func draftSelected ( _ draft : Draft ) {
if self . draft . hasContent {
DraftsManager . save ( )
} else {
DraftsManager . shared . remove ( self . draft )
}
uiState . draft = draft
}
func draftSelectionCompleted ( ) {
}
}
extension ComposeHostingController : UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss ( _ presentationController : UIPresentationController ) -> Bool {
return Preferences . shared . automaticallySaveDrafts || ! draft . hasContent
}
func presentationControllerDidAttemptToDismiss ( _ presentationController : UIPresentationController ) {
uiState . isShowingSaveDraftSheet = true
}
func presentationControllerDidDismiss ( _ presentationController : UIPresentationController ) {
DraftsManager . save ( )
}
}
extension ComposeHostingController : ComposeDrawingViewControllerDelegate {
func composeDrawingViewControllerClose ( _ drawingController : ComposeDrawingViewController ) {
dismiss ( animated : true )
}
func composeDrawingViewController ( _ drawingController : ComposeDrawingViewController , saveDrawing drawing : PKDrawing ) {
switch uiState . composeDrawingMode {
case nil , . createNew :
let attachment = CompositionAttachment ( data : . drawing ( drawing ) )
draft . attachments . append ( attachment )
case let . edit ( id ) :
let existing = draft . attachments . first { $0 . id = = id }
existing ? . data = . drawing ( drawing )
}
dismiss ( animated : true )
}
}