//
// C o p y r i g h t ( c ) 2 0 1 8 O p e n W h i s p e r S y s t e m s . A l l r i g h t s r e s e r v e d .
//
import Foundation
import AVFoundation
import MediaPlayer
@objc
public protocol AttachmentApprovalViewControllerDelegate : class {
func attachmentApproval ( _ attachmentApproval : AttachmentApprovalViewController , didApproveAttachment attachment : SignalAttachment )
func attachmentApproval ( _ attachmentApproval : AttachmentApprovalViewController , didCancelAttachment attachment : SignalAttachment )
}
@objc
public class AttachmentApprovalViewController : OWSViewController , CaptioningToolbarDelegate , PlayerProgressBarDelegate , OWSVideoPlayerDelegate {
let TAG = " [AttachmentApprovalViewController] "
weak var delegate : AttachmentApprovalViewControllerDelegate ?
// W e s o m e t i m e s s h r i n k t h e a t t a c h m e n t v i e w s o t h a t i t r e m a i n s s o m e w h a t v i s i b l e
// w h e n t h e k e y b o a r d i s p r e s e n t e d .
enum AttachmentViewScale {
case fullsize , compact
}
// MARK: P r o p e r t i e s
let attachment : SignalAttachment
private var videoPlayer : OWSVideoPlayer ?
private ( set ) var bottomToolbar : UIView !
private ( set ) var mediaMessageView : MediaMessageView !
private ( set ) var scrollView : UIScrollView !
private ( set ) var contentContainer : UIView !
private ( set ) var playVideoButton : UIView ?
// MARK: I n i t i a l i z e r s
@ available ( * , unavailable , message : " use attachment: constructor instead. " )
required public init ? ( coder aDecoder : NSCoder ) {
fatalError ( " unimplemented " )
}
@objc
required public init ( attachment : SignalAttachment , delegate : AttachmentApprovalViewControllerDelegate ) {
assert ( ! attachment . hasError )
self . attachment = attachment
self . delegate = delegate
super . init ( nibName : nil , bundle : nil )
}
// MARK: V i e w L i f e c y c l e
override public func viewDidLoad ( ) {
super . viewDidLoad ( )
self . navigationItem . title = dialogTitle ( )
}
override public func viewWillLayoutSubviews ( ) {
Logger . debug ( " \( logTag ) in \( #function ) " )
super . viewWillLayoutSubviews ( )
// e . g . i f f l i p p i n g t o / f r o m l a n d s c a p e
updateMinZoomScaleForSize ( view . bounds . size )
}
private func dialogTitle ( ) -> String {
guard let filename = mediaMessageView . formattedFileName ( ) else {
return NSLocalizedString ( " ATTACHMENT_APPROVAL_DIALOG_TITLE " ,
comment : " Title for the 'attachment approval' dialog. " )
}
return filename
}
override public func viewWillAppear ( _ animated : Bool ) {
Logger . debug ( " \( logTag ) in \( #function ) " )
super . viewWillAppear ( animated )
CurrentAppContext ( ) . setStatusBarHidden ( true , animated : animated )
}
override public func viewDidAppear ( _ animated : Bool ) {
Logger . debug ( " \( logTag ) in \( #function ) " )
super . viewDidAppear ( animated )
}
override public func viewWillDisappear ( _ animated : Bool ) {
Logger . debug ( " \( logTag ) in \( #function ) " )
super . viewWillDisappear ( animated )
// S i n c e t h i s V C i s b e i n g d i s m i s s e d , t h e " s h o w s t a t u s b a r " a n i m a t i o n w o u l d f e e l l i k e
// i t ' s o c c u r i n g o n t h e p r e s e n t i n g v i e w c o n t r o l l e r - i t ' s b e t t e r n o t t o a n i m a t e a t a l l .
CurrentAppContext ( ) . setStatusBarHidden ( false , animated : false )
}
// MARK: - C r e a t e V i e w s
public override func loadView ( ) {
self . view = UIView ( )
self . mediaMessageView = MediaMessageView ( attachment : attachment , mode : . attachmentApproval )
// A n y t h i n g t h a t s h o u l d b e s h r u n k w h e n u s e r p o p s k e y b o a r d l i v e s i n t h e c o n t e n t C o n t a i n e r .
let contentContainer = UIView ( )
self . contentContainer = contentContainer
view . addSubview ( contentContainer )
contentContainer . autoPinEdgesToSuperviewEdges ( )
// S c r o l l V i e w - u s e d t o z o o m / p a n o n i m a g e s a n d v i d e o
scrollView = UIScrollView ( )
contentContainer . addSubview ( scrollView )
scrollView . delegate = self
scrollView . showsHorizontalScrollIndicator = false
scrollView . showsVerticalScrollIndicator = false
// P a n n i n g s h o u l d s t o p p r e t t y s o o n a f t e r t h e u s e r s t o p s s c r o l l i n g
scrollView . decelerationRate = UIScrollViewDecelerationRateFast
// W e w a n t s c r o l l v i e w c o n t e n t u p a n d b e h i n d t h e s y s t e m s t a t u s b a r c o n t e n t
// b u t w e w a n t o t h e r c o n t e n t ( e . g . b a r b u t t o n s ) t o r e s p e c t t h e t o p l a y o u t g u i d e .
self . automaticallyAdjustsScrollViewInsets = false
scrollView . autoPinEdgesToSuperviewEdges ( )
let backgroundColor = UIColor . black
self . view . backgroundColor = backgroundColor
// C r e a t e f u l l s c r e e n c o n t a i n e r v i e w s o t h e s c r o l l V i e w
// c a n c o m p u t e a n a p p r o p r i a t e c o n t e n t s i z e i n w h i c h t o c e n t e r
// o u r m e d i a v i e w .
let containerView = UIView . container ( )
scrollView . addSubview ( containerView )
containerView . autoPinEdgesToSuperviewEdges ( )
containerView . autoMatch ( . height , to : . height , of : self . view )
containerView . autoMatch ( . width , to : . width , of : self . view )
containerView . addSubview ( mediaMessageView )
mediaMessageView . autoPinEdgesToSuperviewEdges ( )
if isZoomable {
// A d d t o p a n d b o t t o m g r a d i e n t s t o e n s u r e t o o l b a r c o n t r o l s a r e l e g i b l e
// w h e n p l a c e d o v e r i m a g e / v i d e o p r e v i e w w h i c h m a y b e a c l a s h i n g c o l o r .
let topGradient = GradientView ( from : backgroundColor , to : UIColor . clear )
self . view . addSubview ( topGradient )
topGradient . autoPinWidthToSuperview ( )
topGradient . autoPinEdge ( toSuperviewEdge : . top )
topGradient . autoSetDimension ( . height , toSize : ScaleFromIPhone5 ( 60 ) )
}
// T o p T o o l b a r
let topToolbar = makeClearToolbar ( )
self . view . addSubview ( topToolbar )
topToolbar . autoPinWidthToSuperview ( )
topToolbar . autoPin ( toTopLayoutGuideOf : self , withInset : 0 )
topToolbar . setContentHuggingVerticalHigh ( )
topToolbar . setCompressionResistanceVerticalHigh ( )
let cancelButton = UIBarButtonItem ( barButtonSystemItem : . stop , target : self , action : #selector ( cancelPressed ) )
cancelButton . tintColor = UIColor . white
topToolbar . items = [ cancelButton ]
// B o t t o m T o o l b a r
let captioningToolbar = CaptioningToolbar ( )
captioningToolbar . captioningToolbarDelegate = self
self . bottomToolbar = captioningToolbar
// H i d e t h e p l a y b u t t o n e m b e d d e d i n t h e M e d i a V i e w a n d r e p l a c e i t w i t h o u r o w n .
// T h i s a l l o w s u s t o z o o m i n o n t h e m e d i a v i e w w i t h o u t z o o m i n g i n o n t h e b u t t o n
if attachment . isVideo {
guard let videoURL = attachment . dataUrl else {
owsFail ( " Missing videoURL " )
return
}
let player = OWSVideoPlayer ( url : videoURL )
self . videoPlayer = player
player . delegate = self
let playerView = VideoPlayerView ( )
playerView . player = player . avPlayer
self . mediaMessageView . addSubview ( playerView )
playerView . autoPinEdgesToSuperviewEdges ( )
let pauseGesture = UITapGestureRecognizer ( target : self , action : #selector ( didTapPlayerView ( _ : ) ) )
playerView . addGestureRecognizer ( pauseGesture )
let progressBar = PlayerProgressBar ( )
progressBar . player = player . avPlayer
progressBar . delegate = self
// w e d o n ' t w a n t t h e p r o g r e s s b a r t o z o o m d u r i n g " p i n c h - t o - z o o m "
// b u t w e d o w a n t i t t o s h r i n k w i t h t h e m e d i a c o n t e n t w h e n t h e u s e r
// p o p s t h e k e y b o a r d .
contentContainer . addSubview ( progressBar )
progressBar . autoPinEdge ( . top , to : . bottom , of : topToolbar )
progressBar . autoPinWidthToSuperview ( )
progressBar . autoSetDimension ( . height , toSize : 44 )
self . mediaMessageView . videoPlayButton ? . isHidden = true
let playButton = UIButton ( )
self . playVideoButton = playButton
playButton . accessibilityLabel = NSLocalizedString ( " PLAY_BUTTON_ACCESSABILITY_LABEL " , comment : " Accessibility label for button to start media playback " )
playButton . setBackgroundImage ( # imageLiteral ( resourceName : " play_button " ) , for : . normal )
playButton . contentMode = . scaleAspectFit
let playButtonWidth = ScaleFromIPhone5 ( 70 )
playButton . autoSetDimensions ( to : CGSize ( width : playButtonWidth , height : playButtonWidth ) )
self . contentContainer . addSubview ( playButton )
playButton . addTarget ( self , action : #selector ( playButtonTapped ) , for : . touchUpInside )
playButton . autoCenterInSuperview ( )
}
}
public func didTapPlayerView ( _ gestureRecognizer : UIGestureRecognizer ) {
assert ( self . videoPlayer != nil )
self . pauseVideo ( )
}
override public var inputAccessoryView : UIView ? {
self . bottomToolbar . layoutIfNeeded ( )
return self . bottomToolbar
}
override public var canBecomeFirstResponder : Bool {
return true
}
private func makeClearToolbar ( ) -> UIToolbar {
let toolbar = UIToolbar ( )
toolbar . backgroundColor = UIColor . clear
// M a k i n g a t o o l b a r t r a n s p a r e n t r e q u i r e s s e t t i n g a n e m p t y u i i m a g e
toolbar . setBackgroundImage ( UIImage ( ) , forToolbarPosition : . any , barMetrics : . default )
// h i d e 1 p x t o p - b o r d e r
toolbar . clipsToBounds = true
return toolbar
}
// MARK: - E v e n t H a n d l e r s
@objc
public func playButtonTapped ( ) {
self . playVideo ( )
}
func cancelPressed ( sender : UIButton ) {
self . delegate ? . attachmentApproval ( self , didCancelAttachment : attachment )
}
// MARK: C a p t i o n i n g T o o l b a r D e l e g a t e
func captioningToolbarDidBeginEditing ( _ captioningToolbar : CaptioningToolbar ) {
self . scaleAttachmentView ( . compact )
}
func captioningToolbarDidEndEditing ( _ captioningToolbar : CaptioningToolbar ) {
self . scaleAttachmentView ( . fullsize )
}
func captioningToolbarDidTapSend ( _ captioningToolbar : CaptioningToolbar , captionText : String ? ) {
self . approveAttachment ( captionText : captionText )
}
// MARK: V i d e o
private func playVideo ( ) {
Logger . info ( " \( TAG ) in \( #function ) " )
guard let videoPlayer = self . videoPlayer else {
owsFail ( " \( TAG ) video player was unexpectedly nil " )
return
}
guard let playVideoButton = self . playVideoButton else {
owsFail ( " \( TAG ) playVideoButton was unexpectedly nil " )
return
}
UIView . animate ( withDuration : 0.1 ) {
playVideoButton . alpha = 0.0
}
videoPlayer . play ( )
}
private func pauseVideo ( ) {
guard let videoPlayer = self . videoPlayer else {
owsFail ( " \( TAG ) video player was unexpectedly nil " )
return
}
videoPlayer . pause ( )
guard let playVideoButton = self . playVideoButton else {
owsFail ( " \( TAG ) playVideoButton was unexpectedly nil " )
return
}
UIView . animate ( withDuration : 0.1 ) {
playVideoButton . alpha = 1.0
}
}
@objc
public func videoPlayerDidPlayToCompletion ( _ videoPlayer : OWSVideoPlayer ) {
guard let playVideoButton = self . playVideoButton else {
owsFail ( " \( TAG ) playVideoButton was unexpectedly nil " )
return
}
UIView . animate ( withDuration : 0.1 ) {
playVideoButton . alpha = 1.0
}
}
public func playerProgressBarDidStartScrubbing ( _ playerProgressBar : PlayerProgressBar ) {
// [ s e l f . v i d e o P l a y e r p a u s e ] ;
guard let videoPlayer = self . videoPlayer else {
owsFail ( " \( TAG ) video player was unexpectedly nil " )
return
}
videoPlayer . pause ( )
}
public func playerProgressBar ( _ playerProgressBar : PlayerProgressBar , scrubbedToTime time : CMTime ) {
guard let videoPlayer = self . videoPlayer else {
owsFail ( " \( TAG ) video player was unexpectedly nil " )
return
}
videoPlayer . seek ( to : time )
}
public func playerProgressBar ( _ playerProgressBar : PlayerProgressBar , didFinishScrubbingAtTime time : CMTime , shouldResumePlayback : Bool ) {
guard let videoPlayer = self . videoPlayer else {
owsFail ( " \( TAG ) video player was unexpectedly nil " )
return
}
videoPlayer . seek ( to : time )
if ( shouldResumePlayback ) {
videoPlayer . play ( )
}
}
// MARK: H e l p e r s
var isZoomable : Bool {
return attachment . isImage || attachment . isVideo
}
private func approveAttachment ( captionText : String ? ) {
// T o o l b a r f l i c k e r s i n a n d o u t i f t h e r e a r e e r r o r s
// a n d r e m a i n s v i s i b l e m o m e n t a r i l y a f t e r s h a r e e x t e n s i o n i s d i s m i s s e d .
// I t ' s e a s i e s t t o j u s t h i d e i t a t t h i s p o i n t s i n c e w e ' r e d o n e w i t h i t .
shouldAllowAttachmentViewResizing = false
bottomToolbar . isUserInteractionEnabled = false
bottomToolbar . isHidden = true
attachment . captionText = captionText
delegate ? . attachmentApproval ( self , didApproveAttachment : attachment )
}
// W h e n t h e k e y b o a r d i s p o p p e d , i t c a n o b s c u r e t h e a t t a c h m e n t v i e w .
// s o w e s o m e t i m e s a l l o w r e s i z i n g t h e a t t a c h m e n t .
private var shouldAllowAttachmentViewResizing : Bool = true
private func scaleAttachmentView ( _ fit : AttachmentViewScale ) {
guard shouldAllowAttachmentViewResizing else {
if self . contentContainer . transform != CGAffineTransform . identity {
UIView . animate ( withDuration : 0.2 ) {
self . contentContainer . transform = CGAffineTransform . identity
}
}
return
}
switch fit {
case . fullsize :
UIView . animate ( withDuration : 0.2 ) {
self . contentContainer . transform = CGAffineTransform . identity
}
case . compact :
UIView . animate ( withDuration : 0.2 ) {
let kScaleFactor : CGFloat = 0.7
let scale = CGAffineTransform ( scaleX : kScaleFactor , y : kScaleFactor )
let originalHeight = self . scrollView . bounds . size . height
// P o s i t i o n t h e n e w s c a l e d i t e m t o b e c e n t e r e d w i t h r e s p e c t
// t o i t ' s n e w s i z e .
let heightDelta = originalHeight * ( 1 - kScaleFactor )
let translate = CGAffineTransform ( translationX : 0 , y : - heightDelta / 2 )
self . contentContainer . transform = scale . concatenating ( translate )
}
}
}
}
extension AttachmentApprovalViewController : UIScrollViewDelegate {
public func viewForZooming ( in scrollView : UIScrollView ) -> UIView ? {
if isZoomable {
return mediaMessageView
} else {
// d o n ' t z o o m f o r a u d i o o r g e n e r i c a t t a c h m e n t s .
return nil
}
}
fileprivate func updateMinZoomScaleForSize ( _ size : CGSize ) {
Logger . debug ( " \( logTag ) in \( #function ) " )
// E n s u r e b o u n d s h a v e b e e n c o m p u t e d
mediaMessageView . layoutIfNeeded ( )
guard mediaMessageView . bounds . width > 0 , mediaMessageView . bounds . height > 0 else {
Logger . warn ( " \( logTag ) bad bounds in \( #function ) " )
return
}
let widthScale = size . width / mediaMessageView . bounds . width
let heightScale = size . height / mediaMessageView . bounds . height
let minScale = min ( widthScale , heightScale )
scrollView . maximumZoomScale = minScale * 5.0
scrollView . minimumZoomScale = minScale
scrollView . zoomScale = minScale
}
// K e e p t h e m e d i a v i e w c e n t e r e d w i t h i n t h e s c r o l l v i e w a s y o u z o o m
public func scrollViewDidZoom ( _ scrollView : UIScrollView ) {
// T h e s c r o l l v i e w h a s z o o m e d , s o y o u n e e d t o r e - c e n t e r t h e c o n t e n t s
let scrollViewSize = self . scrollViewVisibleSize
// F i r s t a s s u m e t h a t m e d i a M e s s a g e V i e w c e n t e r c o i n c i d e s w i t h t h e c o n t e n t s c e n t e r
// T h i s i s c o r r e c t w h e n t h e m e d i a M e s s a g e V i e w i s b i g g e r t h a n s c r o l l V i e w d u e t o z o o m
var contentCenter = CGPoint ( x : ( scrollView . contentSize . width / 2 ) , y : ( scrollView . contentSize . height / 2 ) )
let scrollViewCenter = self . scrollViewCenter
// i f m e d i a M e s s a g e V i e w i s s m a l l e r t h a n t h e s c r o l l V i e w v i s i b l e s i z e - f i x t h e c o n t e n t c e n t e r a c c o r d i n g l y
if self . scrollView . contentSize . width < scrollViewSize . width {
contentCenter . x = scrollViewCenter . x
}
if self . scrollView . contentSize . height < scrollViewSize . height {
contentCenter . y = scrollViewCenter . y
}
self . mediaMessageView . center = contentCenter
}
// r e t u r n t h e s c r o l l v i e w c e n t e r
private var scrollViewCenter : CGPoint {
let size = scrollViewVisibleSize
return CGPoint ( x : ( size . width / 2 ) , y : ( size . height / 2 ) )
}
// R e t u r n s c r o l l v i e w s i z e w i t h o u t t h e a r e a o v e r l a p p i n g w i t h t a b a n d n a v b a r .
private var scrollViewVisibleSize : CGSize {
let contentInset = scrollView . contentInset
let scrollViewSize = scrollView . bounds . standardized . size
let width = scrollViewSize . width - ( contentInset . left + contentInset . right )
let height = scrollViewSize . height - ( contentInset . top + contentInset . bottom )
return CGSize ( width : width , height : height )
}
}
protocol CaptioningToolbarDelegate : class {
func captioningToolbarDidTapSend ( _ captioningToolbar : CaptioningToolbar , captionText : String ? )
func captioningToolbarDidBeginEditing ( _ captioningToolbar : CaptioningToolbar )
func captioningToolbarDidEndEditing ( _ captioningToolbar : CaptioningToolbar )
}
class CaptioningToolbar : UIView , UITextViewDelegate {
weak var captioningToolbarDelegate : CaptioningToolbarDelegate ?
private let sendButton : UIButton
private let textView : UITextView
private let bottomGradient : GradientView
private let lengthLimitLabel : UILabel
// L a y o u t C o n s t a n t s
let kMinTextViewHeight : CGFloat = 38
var maxTextViewHeight : CGFloat {
// A b o u t ~ 4 l i n e s i n p o r t r a i t a n d ~ 3 l i n e s i n l a n d s c a p e .
// O t h e r w i s e w e r i s k o b s c u r i n g t o o m u c h o f t h e c o n t e n t .
return UIDevice . current . orientation . isPortrait ? 160 : 100
}
var textViewHeightConstraint : NSLayoutConstraint !
var textViewHeight : CGFloat
required init ? ( coder aDecoder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
class MessageTextView : UITextView {
// W h e n c r e a t i n g n e w l i n e s , c o n t e n t O f f s e t i s a n i m a t e d , b u t b e c a u s e b e c a u s e
// w e a r e s i m u l t a n e o u s l y r e s i z i n g t h e t e x t v i e w , t h i s c a n c a u s e t h e
// t e x t i n t h e t e x t v i e w t o b e " t o o h i g h " i n t h e t e x t v i e w .
// S o l u t i o n i s t o d i s a b l e a n i m a t i o n f o r s e t t i n g c o n t e n t o f f s e t .
override func setContentOffset ( _ contentOffset : CGPoint , animated : Bool ) {
super . setContentOffset ( contentOffset , animated : false )
}
}
override var intrinsicContentSize : CGSize {
get {
// S i n c e w e h a v e ` s e l f . a u t o r e s i z i n g M a s k = U I V i e w A u t o r e s i z i n g F l e x i b l e H e i g h t ` , w e m u s t s p e c i f y
// a n i n t r i n s i c C o n t e n t S i z e . S p e c i f y i n g C G S i z e . z e r o c a u s e s t h e h e i g h t t o b e d e t e r m i n e d b y a u t o l a y o u t .
return CGSize . zero
}
}
init ( ) {
self . sendButton = UIButton ( type : . system )
self . bottomGradient = GradientView ( from : UIColor . clear , to : UIColor . black )
self . textView = MessageTextView ( )
self . textViewHeight = kMinTextViewHeight
self . lengthLimitLabel = UILabel ( )
super . init ( frame : CGRect . zero )
// S p e c i f y i n g a u t o r s i z i n g m a s k a n d a n i n t r i n s i c c o n t e n t s i z e a l l o w s p r o p e r
// s i z i n g w h e n u s e d a s a n i n p u t a c c e s s o r y v i e w .
self . autoresizingMask = . flexibleHeight
self . translatesAutoresizingMaskIntoConstraints = false
self . backgroundColor = UIColor . clear
textView . delegate = self
textView . backgroundColor = UIColor . white
textView . layer . cornerRadius = 4.0
textView . addBorder ( with : UIColor . lightGray )
textView . font = UIFont . ows_dynamicTypeBody
textView . returnKeyType = . done
let sendTitle = NSLocalizedString ( " ATTACHMENT_APPROVAL_SEND_BUTTON " , comment : " Label for 'send' button in the 'attachment approval' dialog. " )
sendButton . setTitle ( sendTitle , for : . normal )
sendButton . addTarget ( self , action : #selector ( didTapSend ) , for : . touchUpInside )
sendButton . titleLabel ? . font = UIFont . ows_mediumFont ( withSize : 16 )
sendButton . titleLabel ? . textAlignment = . center
sendButton . tintColor = UIColor . white
sendButton . backgroundColor = UIColor . ows_systemPrimaryButton
sendButton . layer . cornerRadius = 4
// S e n d B u t t o n S h a d o w - w i t h o u t t h i s t h e s e n d b u t t o n b o t t o m d o e s n ' t f e e l a l i g n e d w i t h t h e t o o l b a r .
let kSendButtonShadowOffset : CGFloat = 1
sendButton . layer . shadowColor = UIColor . darkGray . cgColor
sendButton . layer . shadowOffset = CGSize ( width : 0 , height : kSendButtonShadowOffset )
sendButton . layer . shadowOpacity = 0.8
sendButton . layer . shadowRadius = 0.0
sendButton . layer . masksToBounds = false
// I n c r e a s e h i t a r e a o f s e n d b u t t o n
sendButton . contentEdgeInsets = UIEdgeInsets ( top : 6 , left : 8 , bottom : 6 , right : 8 )
// L e n g t h L i m i t L a b e l s h o w n w h e n t h e u s e r i n p u t s t o o l o n g o f a m e s s a g e
lengthLimitLabel . textColor = . white
lengthLimitLabel . text = NSLocalizedString ( " ATTACHMENT_APPROVAL_CAPTION_LENGTH_LIMIT_REACHED " , comment : " One line label indicating the user can add no more text to the attachment caption. " )
lengthLimitLabel . textAlignment = . center
// A d d s h a d o w i n c a s e o v e r l a y e d o n w h i t e c o n t e n t
lengthLimitLabel . layer . shadowColor = UIColor . black . cgColor
lengthLimitLabel . layer . shadowOffset = CGSize ( width : 0.0 , height : 0.0 )
lengthLimitLabel . layer . shadowOpacity = 0.8
self . lengthLimitLabel . isHidden = true
let contentView = UIView ( )
addSubview ( contentView )
contentView . autoPinEdgesToSuperviewEdges ( )
contentView . addSubview ( bottomGradient )
contentView . addSubview ( sendButton )
contentView . addSubview ( textView )
contentView . addSubview ( lengthLimitLabel )
// L a y o u t
let kToolbarMargin : CGFloat = 8
// W e h a v e t o w r a p t h e t o o l b a r i t e m s i n a c o n t e n t v i e w b e c a u s e i O S ( a t l e a s t o n i O S 1 0 . 3 ) a s s i g n s t h e i n p u t A c c e s s o r y V i e w . l a y o u t M a r g i n s
// w h e n r e s i g n i n g f i r s t r e s p o n d e r ( v e r i f i e d b y a u d i t i n g w i t h ` l a y o u t M a r g i n s D i d C h a n g e ` ) .
// T h e e f f e c t o f t h i s i s t h a t i f w e w e r e t o a s s i g n t h e s e m a r g i n s t o s e l f . l a y o u t M a r g i n s , t h e y ' d b e b l o w n a w a y i f t h e
// u s e r d i s m i s s e s t h e k e y b o a r d , g i v i n g t h e i n p u t a c c e s s o r y v i e w a w o n k y l a y o u t .
contentView . layoutMargins = UIEdgeInsets ( top : kToolbarMargin , left : kToolbarMargin , bottom : kToolbarMargin , right : kToolbarMargin )
self . textViewHeightConstraint = textView . autoSetDimension ( . height , toSize : kMinTextViewHeight )
// W e p i n a l l t h r e e e d g e s e x p l i c i t l y r a t h e r t h a n d o i n g s o m e t h i n g l i k e :
// t e x t V i e w . a u t o P i n E d g e s ( t o S u p e r v i e w M a r g i n s E x c l u d i n g E d g e : . r i g h t )
// b e c a u s e t h a t m e t h o d u s e s ` l e a d i n g ` / ` t r a i l i n g ` r a t h e r t h a n ` l e f t ` v s . ` r i g h t ` .
// S o i t d o e s n ' t w o r k a s e x p e c t e d w i t h R T L l a y o u t s w h e n w e e x p l i c i t l y w a n t s o m e t h i n g
// t o b e o n t h e r i g h t s i d e f o r b o t h R T L a n d L T R l a y o u t s , l i k e w i t h t h e s e n d b u t t o n .
// I b e l i e v e t h i s i s a b u g i n P u r e L a y o u t . F i l e d h e r e : h t t p s : / / g i t h u b . c o m / P u r e L a y o u t / P u r e L a y o u t / i s s u e s / 2 0 9
textView . autoPinEdge ( toSuperviewMargin : . left )
textView . autoPinEdge ( toSuperviewMargin : . top )
textView . autoPinEdge ( toSuperviewMargin : . bottom )
sendButton . autoPinEdge ( . left , to : . right , of : textView , withOffset : kToolbarMargin )
// B e c a u s e t h e t e x t v i e w h a s a b o r d e r , t h e s e n d B u t t o n f e e l s u n a l i g n e d w i t h o u t t h i s s h a d o w a n d o f f s e t
sendButton . autoPinEdge ( . bottom , to : . bottom , of : textView , withOffset : - kSendButtonShadowOffset )
sendButton . autoPinEdge ( toSuperviewMargin : . right )
sendButton . setContentHuggingHigh ( )
sendButton . setCompressionResistanceHigh ( )
lengthLimitLabel . autoPinEdge ( toSuperviewMargin : . left )
lengthLimitLabel . autoPinEdge ( toSuperviewMargin : . right )
lengthLimitLabel . autoPinEdge ( . bottom , to : . top , of : textView , withOffset : - 6 )
lengthLimitLabel . setContentHuggingHigh ( )
lengthLimitLabel . setCompressionResistanceHigh ( )
let bottomGradientHeight = ScaleFromIPhone5 ( 100 )
bottomGradient . autoSetDimension ( . height , toSize : bottomGradientHeight )
bottomGradient . autoPinEdgesToSuperviewEdges ( with : . zero , excludingEdge : . top )
}
func didTapSend ( ) {
self . captioningToolbarDelegate ? . captioningToolbarDidTapSend ( self , captionText : self . textView . text )
}
// MARK: - U I T e x t V i e w D e l e g a t e
public func textViewDidChange ( _ textView : UITextView ) {
updateHeight ( textView : textView )
}
public func textView ( _ textView : UITextView , shouldChangeTextIn range : NSRange , replacementText text : String ) -> Bool {
let existingText : String = textView . text ? ? " "
let proposedText : String = ( existingText as NSString ) . replacingCharacters ( in : range , with : text )
guard proposedText . utf8 . count <= kOversizeTextMessageSizeThreshold else {
Logger . debug ( " \( self . logTag ) in \( #function ) long text was truncated " )
self . lengthLimitLabel . isHidden = false
// ` r a n g e ` r e p r e s e n t s t h e s e c t i o n o f t h e e x i s t i n g t e x t w e w i l l r e p l a c e . W e c a n r e - u s e t h a t s p a c e .
// R a n g e i s i n u n i t s o f N S S t r i n g s ' s s t a n d a r d U T F - 1 6 c h a r a c t e r s . S i n c e s o m e o f t h o s e c h a r s c o u l d b e
// r e p r e s e n t e d a s s i n g l e b y t e s i n u t f - 8 , w h i l e o t h e r s m a y b e 8 o r m o r e , t h e o n l y w a y t o b e s u r e i s
// t o j u s t m e a s u r e t h e u t f 8 e n c o d e d b y t e s o f t h e r e p l a c e d s u b s t r i n g .
let bytesAfterDelete : Int = ( existingText as NSString ) . replacingCharacters ( in : range , with : " " ) . utf8 . count
// A c c e p t a s m u c h o f t h e i n p u t a s w e c a n
let byteBudget : Int = Int ( kOversizeTextMessageSizeThreshold ) - bytesAfterDelete
if byteBudget >= 0 , let acceptableNewText = text . truncated ( toByteCount : UInt ( byteBudget ) ) {
textView . text = ( existingText as NSString ) . replacingCharacters ( in : range , with : acceptableNewText )
}
return false
}
self . lengthLimitLabel . isHidden = true
// T h o u g h w e c a n w r a p t h e t e x t , w e d o n ' t w a n t t o e n c o u r a g e m u l t l i n e c a p t i o n s , p l u s a " d o n e " b u t t o n
// a l l o w s t h e u s e r t o g e t t h e k e y b o a r d o u t o f t h e w a y w h i l e i n t h e a t t a c h m e n t a p p r o v a l v i e w .
if text = = " \n " {
textView . resignFirstResponder ( )
return false
} else {
return true
}
}
public func textViewDidBeginEditing ( _ textView : UITextView ) {
self . captioningToolbarDelegate ? . captioningToolbarDidBeginEditing ( self )
}
public func textViewDidEndEditing ( _ textView : UITextView ) {
self . captioningToolbarDelegate ? . captioningToolbarDidEndEditing ( self )
}
// MARK: - H e l p e r s
private func updateHeight ( textView : UITextView ) {
// c o m p u t e n e w h e i g h t a s s u m i n g w i d t h i s u n c h a n g e d
let currentSize = textView . frame . size
let newHeight = clampedTextViewHeight ( fixedWidth : currentSize . width )
if newHeight != self . textViewHeight {
Logger . debug ( " \( self . logTag ) TextView height changed: \( self . textViewHeight ) -> \( newHeight ) " )
self . textViewHeight = newHeight
self . textViewHeightConstraint ? . constant = textViewHeight
self . invalidateIntrinsicContentSize ( )
}
}
private func clampedTextViewHeight ( fixedWidth : CGFloat ) -> CGFloat {
let contentSize = textView . sizeThatFits ( CGSize ( width : fixedWidth , height : CGFloat . greatestFiniteMagnitude ) )
return CGFloatClamp ( contentSize . height , kMinTextViewHeight , maxTextViewHeight )
}
}